- Add missing 'offline' to AgentStatus union type (types/index.ts) - Add max-retry circuit breaker to useSSE; error state is now reachable - Wire typed SSE payloads (SSEPayloadMap discriminated union) into useRealtimeSync - Add Vitest + 20 unit tests: useSSE lifecycle, back-off, circuit breaker, event parsing, cleanup; useRealtimeSync event-to-invalidation mapping - Rebase on dev to remove stale CUB-119 legacy-deletion commit and align CI workflow (dev already consolidated into single dev.yml) - Tests: npm test → 20/20 pass; Build: npm run build → 0 errors
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
/**
|
|
* useRealtimeSync — mounts the SSE connection once at the app level and
|
|
* wires incoming events to React Query cache invalidation.
|
|
*
|
|
* Event → query key mapping:
|
|
* agent.status → ['agents']
|
|
* agent.task → ['tasks'], ['agents']
|
|
* agent.progress → ['tasks'], ['agents']
|
|
* fleet.update → ['agents'], ['sessions'], ['tasks']
|
|
*/
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { useCallback } from 'react'
|
|
import { useSSE, type SSEStatus } from './useSSE'
|
|
import type { SSEMessage } from '../services/sse'
|
|
|
|
export function useRealtimeSync(): { sseStatus: SSEStatus } {
|
|
const queryClient = useQueryClient()
|
|
|
|
const handleMessage = useCallback(
|
|
(raw: { type: string; data: unknown }) => {
|
|
// Cast to discriminated union — the backend contract guarantees these shapes
|
|
const msg = raw as SSEMessage
|
|
|
|
switch (msg.type) {
|
|
case 'agent.status':
|
|
// msg.data: AgentStatusEvent { agentId, status, reason? }
|
|
void msg.data.agentId // retained for type-narrowing — ensures payload matches contract
|
|
queryClient.invalidateQueries({ queryKey: ['agents'] })
|
|
break
|
|
|
|
case 'agent.task':
|
|
// msg.data: AgentTaskEvent { agentId, taskId, title, action }
|
|
void msg.data.agentId
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
|
queryClient.invalidateQueries({ queryKey: ['agents'] })
|
|
break
|
|
|
|
case 'agent.progress':
|
|
// msg.data: AgentProgressEvent { agentId, taskId, progress, message? }
|
|
void msg.data.agentId
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
|
queryClient.invalidateQueries({ queryKey: ['agents'] })
|
|
break
|
|
|
|
case 'fleet.update':
|
|
// msg.data: FleetUpdateEvent { timestamp, agentCount }
|
|
void msg.data.agentCount
|
|
queryClient.invalidateQueries({ queryKey: ['agents'] })
|
|
queryClient.invalidateQueries({ queryKey: ['sessions'] })
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
|
break
|
|
|
|
default:
|
|
// 'connected' and unknown events — no action needed
|
|
break
|
|
}
|
|
},
|
|
[queryClient],
|
|
)
|
|
|
|
const { status: sseStatus } = useSSE({ onMessage: handleMessage })
|
|
|
|
return { sseStatus }
|
|
}
|