# WebSocket Usage in Web UI ## Overview The Attune web UI uses a **single shared WebSocket connection** for real-time notifications across all pages. This connection is managed by `WebSocketProvider` and accessed via React hooks. ## Architecture ``` App.tsx └── WebSocketProvider (manages single WS connection) ├── EventsPage (subscribes to "event" notifications) ├── ExecutionsPage (subscribes to "execution" notifications) └── DashboardPage (subscribes to multiple entity types) ``` Only **one WebSocket connection** is created per browser tab, regardless of how many pages subscribe to notifications. ## Basic Usage ### 1. Ensure Provider is Configured The `WebSocketProvider` should be set up in `App.tsx` (already configured): ```typescript import { WebSocketProvider } from "@/contexts/WebSocketContext"; function App() { return ( {/* Your app routes */} ); } ``` ### 2. Subscribe to Entity Notifications Use `useEntityNotifications` to receive real-time updates for a specific entity type: ```typescript import { useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useEntityNotifications } from "@/contexts/WebSocketContext"; function MyPage() { const queryClient = useQueryClient(); // IMPORTANT: Wrap handler in useCallback for stable reference const handleNotification = useCallback(() => { // Invalidate queries to refetch data queryClient.invalidateQueries({ queryKey: ["myEntity"] }); }, [queryClient]); const { connected } = useEntityNotifications("myEntity", handleNotification); return (
{connected && Live updates enabled} {/* Rest of your component */}
); } ``` ## Available Entity Types Subscribe to these entity types to receive notifications: - `"event"` - Event creation/updates - `"execution"` - Execution status changes - `"enforcement"` - Rule enforcement events - `"inquiry"` - Human-in-the-loop interactions - `"action"` - Action changes (from pack updates) - `"rule"` - Rule changes - `"trigger"` - Trigger changes - `"sensor"` - Sensor changes ## Advanced Usage ### Multiple Subscriptions in One Component ```typescript function DashboardPage() { const queryClient = useQueryClient(); const handleEventNotification = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["events"] }); }, [queryClient]); const handleExecutionNotification = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["executions"] }); }, [queryClient]); useEntityNotifications("event", handleEventNotification); useEntityNotifications("execution", handleExecutionNotification); // Both subscriptions share the same WebSocket connection } ``` ### Custom Notification Handling ```typescript import { useCallback } from "react"; import { useEntityNotifications, Notification } from "@/contexts/WebSocketContext"; function MyPage() { const handleNotification = useCallback((notification: Notification) => { console.log("Received notification:", notification); // Access notification details console.log("Entity type:", notification.entity_type); console.log("Entity ID:", notification.entity_id); console.log("Payload:", notification.payload); console.log("Timestamp:", notification.timestamp); // Custom logic based on notification if (notification.entity_id === specificId) { // Do something specific } }, []); useEntityNotifications("execution", handleNotification); } ``` ### Conditional Subscriptions ```typescript function MyPage({ entityType }: { entityType: string }) { const [enabled, setEnabled] = useState(true); const handleNotification = useCallback(() => { // Handle notification }, []); // Only subscribe when enabled is true useEntityNotifications(entityType, handleNotification, enabled); } ``` ### Direct Context Access For advanced use cases, access the WebSocket context directly: ```typescript import { useWebSocketContext } from "@/contexts/WebSocketContext"; function AdvancedComponent() { const { connected, subscribe, unsubscribe } = useWebSocketContext(); useEffect(() => { const handler = (notification) => { // Custom handling }; // Subscribe to a custom filter subscribe("entity_type:execution", handler); return () => { unsubscribe("entity_type:execution", handler); }; }, [subscribe, unsubscribe]); } ``` ## Important Guidelines ### ✅ DO - **Always wrap handlers in `useCallback`** to prevent re-subscriptions - Include all dependencies in the `useCallback` dependency array - Use `queryClient.invalidateQueries` for simple cache invalidation - Show connection status in the UI when relevant ### ❌ DON'T - Don't create inline functions as handlers (causes re-subscriptions) - Don't create multiple WebSocket connections (use the context) - Don't forget to handle the `connected` state - Don't use the deprecated `useWebSocket` from `@/hooks/useWebSocket.ts` ## Connection Status Display connection status to users: ```typescript function MyPage() { const { connected } = useEntityNotifications("event", handleNotification); return (
{connected ? (
Live updates
) : (
Connecting...
)}
); } ``` ## Configuration WebSocket connection is configured via environment variables: ```bash # .env.local or .env.development VITE_WS_URL=ws://localhost:8081 ``` The provider automatically appends `/ws` to the URL if not present. ## Troubleshooting ### Multiple Connections Opening **Problem**: Browser DevTools shows multiple WebSocket connections. **Solution**: - Ensure `WebSocketProvider` is only added once in `App.tsx` - Check that handlers are wrapped in `useCallback` - Verify React StrictMode isn't causing double-mounting in development ### Notifications Not Received **Problem**: Component doesn't receive notifications. **Checklist**: 1. Is `WebSocketProvider` wrapping your app? 2. Is the notifier service running? (`make run-notifier`) 3. Is `connected` returning `true`? 4. Is the entity type spelled correctly? 5. Are notifications actually being sent? (check server logs) ### Connection Keeps Reconnecting **Problem**: WebSocket disconnects and reconnects repeatedly. **Possible causes**: - Notifier service restarting - Network issues - Authentication token expired (WebSocket doesn't currently use auth) - Check console for error messages ## Example: Complete Page ```typescript import { useState, useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useEntityNotifications } from "@/contexts/WebSocketContext"; import { useEvents } from "@/hooks/useEvents"; export default function EventsPage() { const queryClient = useQueryClient(); const [page, setPage] = useState(1); // Real-time updates const handleEventNotification = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["events"] }); }, [queryClient]); const { connected } = useEntityNotifications("event", handleEventNotification); // Fetch data const { data, isLoading } = useEvents({ page, pageSize: 20 }); return (

Events

{connected && (
Live updates
)}
{/* Render events */}
); } ``` ## Performance Notes - The single shared connection significantly reduces network overhead - Subscriptions are reference-counted (only subscribe to each filter once) - Handlers are called synchronously when notifications arrive - No polling is needed - all updates are push-based via WebSocket ## Migration from Old Pattern If you find code using the old pattern: ```typescript // ❌ OLD - creates separate connection import { useEntityNotifications } from "@/hooks/useWebSocket"; const { connected } = useEntityNotifications("event", () => { queryClient.invalidateQueries({ queryKey: ["events"] }); }); ``` Update to: ```typescript // ✅ NEW - uses shared connection import { useEntityNotifications } from "@/contexts/WebSocketContext"; const handleNotification = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["events"] }); }, [queryClient]); const { connected } = useEntityNotifications("event", handleNotification); ```