re-uploading work
This commit is contained in:
318
docs/web-ui/websocket-usage.md
Normal file
318
docs/web-ui/websocket-usage.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# 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 (
|
||||
<WebSocketProvider>
|
||||
{/* Your app routes */}
|
||||
</WebSocketProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<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
|
||||
|
||||
```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 (
|
||||
<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:
|
||||
|
||||
```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 (
|
||||
<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:
|
||||
|
||||
```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);
|
||||
```
|
||||
Reference in New Issue
Block a user