8.7 KiB
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):
import { WebSocketProvider } from "@/contexts/WebSocketContext";
function App() {
return (
<WebSocketProvider>
{/* Your app routes */}
</WebSocketProvider>
);
}
2. Subscribe to Entity Notifications
Use useEntityNotifications to receive real-time updates for a specific entity type:
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 (
<div>
{connected && <span>Live updates enabled</span>}
{/* Rest of your component */}
</div>
);
}
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
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
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
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:
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
useCallbackto prevent re-subscriptions - Include all dependencies in the
useCallbackdependency array - Use
queryClient.invalidateQueriesfor 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
connectedstate - Don't use the deprecated
useWebSocketfrom@/hooks/useWebSocket.ts
Connection Status
Display connection status to users:
function MyPage() {
const { connected } = useEntityNotifications("event", handleNotification);
return (
<div>
{connected ? (
<div className="flex items-center gap-2 text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse" />
<span>Live updates</span>
</div>
) : (
<div className="text-gray-400">Connecting...</div>
)}
</div>
);
}
Configuration
WebSocket connection is configured via environment variables:
# .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
WebSocketProvideris only added once inApp.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:
- Is
WebSocketProviderwrapping your app? - Is the notifier service running? (
make run-notifier) - Is
connectedreturningtrue? - Is the entity type spelled correctly?
- 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
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 (
<div>
<div className="flex items-center justify-between">
<h1>Events</h1>
{connected && (
<div className="text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse" />
Live updates
</div>
)}
</div>
{/* Render events */}
</div>
);
}
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:
// ❌ OLD - creates separate connection
import { useEntityNotifications } from "@/hooks/useWebSocket";
const { connected } = useEntityNotifications("event", () => {
queryClient.invalidateQueries({ queryKey: ["events"] });
});
Update to:
// ✅ NEW - uses shared connection
import { useEntityNotifications } from "@/contexts/WebSocketContext";
const handleNotification = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["events"] });
}, [queryClient]);
const { connected } = useEntityNotifications("event", handleNotification);