|
|
@@ -0,0 +1,88 @@
|
|
|
+import { useEffect, useCallback, useRef } from 'react'
|
|
|
+import { useQueryClient } from '@tanstack/react-query'
|
|
|
+import { wsClient } from '../api/client'
|
|
|
+
|
|
|
+interface UseExecutionListUpdatesOptions {
|
|
|
+ /** Workflow ID to filter updates (optional - if not provided, listens to all executions) */
|
|
|
+ workflowId?: string
|
|
|
+ /** Whether the hook is active */
|
|
|
+ enabled?: boolean
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Hook that subscribes to WebSocket execution events and invalidates
|
|
|
+ * the execution list query when relevant events occur.
|
|
|
+ *
|
|
|
+ * Replaces polling with real-time WebSocket updates.
|
|
|
+ */
|
|
|
+export function useExecutionListUpdates(options: UseExecutionListUpdatesOptions = {}) {
|
|
|
+ const { workflowId, enabled = true } = options
|
|
|
+ const queryClient = useQueryClient()
|
|
|
+ const handlerRef = useRef<((data: any) => void) | null>(null)
|
|
|
+
|
|
|
+ const handleExecutionEvent = useCallback((data: any) => {
|
|
|
+ const channel = data._channel || ''
|
|
|
+
|
|
|
+ // Only process lifecycle events that affect the list
|
|
|
+ const isLifecycleEvent =
|
|
|
+ channel.includes('.started') ||
|
|
|
+ channel.includes('.completed') ||
|
|
|
+ channel.includes('.failed') ||
|
|
|
+ channel.includes('.cancelled')
|
|
|
+
|
|
|
+ if (!isLifecycleEvent) return
|
|
|
+
|
|
|
+ // If filtering by workflowId, check if this event is for our workflow
|
|
|
+ if (workflowId && data.workflowId && data.workflowId !== workflowId) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Invalidate execution list queries to trigger a refetch
|
|
|
+ // This is more efficient than polling every 5 seconds
|
|
|
+ if (workflowId) {
|
|
|
+ // Invalidate specific workflow's execution list
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['executions', workflowId] })
|
|
|
+ } else {
|
|
|
+ // Invalidate all execution lists
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['executions'] })
|
|
|
+ }
|
|
|
+ }, [workflowId, queryClient])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!enabled) return
|
|
|
+
|
|
|
+ // Store handler ref for cleanup
|
|
|
+ handlerRef.current = handleExecutionEvent
|
|
|
+
|
|
|
+ // Subscribe to all execution events
|
|
|
+ wsClient.subscribe(['executions.*'])
|
|
|
+ wsClient.on('executions.*', handleExecutionEvent)
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ if (handlerRef.current) {
|
|
|
+ wsClient.off('executions.*', handlerRef.current)
|
|
|
+ }
|
|
|
+ wsClient.unsubscribe(['executions.*'])
|
|
|
+ }
|
|
|
+ }, [enabled, handleExecutionEvent])
|
|
|
+
|
|
|
+ // Handle reconnection - refetch to sync state
|
|
|
+ useEffect(() => {
|
|
|
+ if (!enabled) return
|
|
|
+
|
|
|
+ const handleReconnect = () => {
|
|
|
+ // On reconnect, invalidate to get fresh data
|
|
|
+ if (workflowId) {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['executions', workflowId] })
|
|
|
+ } else {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['executions'] })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ wsClient.onReconnect(handleReconnect)
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ wsClient.offReconnect(handleReconnect)
|
|
|
+ }
|
|
|
+ }, [enabled, workflowId, queryClient])
|
|
|
+}
|