{"version":3,"file":"RealtimeProvider.mjs","sources":["../../../src/lib/realtime/RealtimeProvider.tsx"],"sourcesContent":["/* @refresh reset */\n/**\n * @file Realtime Provider\n * @description Manages WebSocket/SSE connection and distributes events by channel.\n *\n * This provider handles real-time communication (WebSocket/SSE), distinct from\n * the streaming/ module which handles React 18 SSR HTML streaming.\n */\n\nimport React, {\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from 'react';\nimport {\n  RealtimeContext,\n  type RealtimeContextValue,\n  type ConnectionState,\n} from '../contexts/RealtimeContext';\nimport { WebSocketClient, createWebSocketClient, type WebSocketState } from './websocket-client';\nimport { SSEClient, createSSEClient, type SSEState } from './sse-client';\nimport {\n  StreamQueryCacheUpdater,\n  type StreamEvent,\n  type CacheUpdateStrategy,\n} from './stream-to-query-cache';\nimport { queryClient } from '../queries/queryClient';\nimport { useFeatureFlag } from '../flags/useFeatureFlag';\nimport { flagKeys } from '../flags/flag-keys';\n\n/**\n * Realtime connection type\n */\nexport type RealtimeConnectionType = 'websocket' | 'sse';\n\n/**\n * Realtime provider configuration\n */\nexport interface RealtimeProviderConfig {\n  type: RealtimeConnectionType;\n  url?: string;\n  autoConnect?: boolean;\n  cacheStrategies?: CacheUpdateStrategy[];\n}\n\n/**\n * Realtime provider props\n */\nexport interface RealtimeProviderProps {\n  children: ReactNode;\n  config?: RealtimeProviderConfig;\n}\n\n/**\n * Default configuration\n */\nconst defaultConfig: RealtimeProviderConfig = {\n  type: 'websocket',\n  autoConnect: true,\n  cacheStrategies: [],\n};\n\n/**\n * Realtime provider component\n *\n * Manages WebSocket or SSE connections for real-time data streaming.\n * Respects the 'real-time-updates' feature flag - when disabled, no connections are established.\n *\n * @example\n * ```tsx\n * <RealtimeProvider config={{ type: 'websocket', url: '/ws' }}>\n *   <App />\n * </RealtimeProvider>\n * ```\n */\nexport function RealtimeProvider({\n  children,\n  config = defaultConfig,\n}: RealtimeProviderProps): React.ReactElement {\n  // Check if real-time updates are enabled via feature flag\n  const realtimeEnabled = useFeatureFlag(flagKeys.REAL_TIME_UPDATES);\n\n  const mergedConfig = { ...defaultConfig, ...config };\n\n  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');\n\n  const clientRef = useRef<WebSocketClient | SSEClient | null>(null);\n  const cacheUpdaterRef = useRef<StreamQueryCacheUpdater | null>(null);\n  const channelHandlersRef = useRef<Map<string, Set<(data: unknown) => void>>>(new Map());\n\n  /**\n   * Map WebSocketState/SSEState to ConnectionState\n   */\n  const mapToConnectionState = (state: WebSocketState | SSEState): ConnectionState => {\n    // Both WebSocketState and SSEState align with ConnectionState except for 'error'\n    // WebSocket has 'error', but ConnectionState also has 'error', so direct mapping works\n    return state as ConnectionState;\n  };\n\n  /**\n   * Initialize the realtime client\n   * Only establishes connection if real-time updates feature flag is enabled\n   */\n  useEffect(() => {\n    // Skip initialization if real-time updates are disabled via feature flag\n    if (!realtimeEnabled) {\n      console.info('[RealtimeProvider] Real-time updates disabled via feature flag');\n      return;\n    }\n\n    // Create appropriate client\n    if (mergedConfig.type === 'websocket') {\n      clientRef.current = createWebSocketClient(mergedConfig.url ?? '/ws');\n    } else {\n      clientRef.current = createSSEClient(mergedConfig.url ?? '/events');\n    }\n\n    // Setup cache updater\n    if (mergedConfig.cacheStrategies !== undefined && mergedConfig.cacheStrategies.length > 0) {\n      cacheUpdaterRef.current = new StreamQueryCacheUpdater({\n        queryClient,\n        strategies: mergedConfig.cacheStrategies,\n      });\n    }\n\n    // Setup state change handler\n    const client = clientRef.current;\n    const unsubState = client.onStateChange((state) =>\n      setConnectionState(mapToConnectionState(state))\n    );\n\n    // Setup message handler\n    const unsubMessage = client.onMessage((data: unknown) => {\n      // Process cache updates\n      if (cacheUpdaterRef.current && isStreamEvent(data)) {\n        cacheUpdaterRef.current.processEvent(data);\n      }\n\n      // Route to channel handlers\n      if (typeof data === 'object' && data !== null && 'channel' in data) {\n        const channelData = data as { channel: string; data: unknown };\n        const handlers = channelHandlersRef.current.get(channelData.channel);\n        handlers?.forEach((handler) => handler(channelData.data));\n      }\n    });\n\n    // Auto-connect if enabled\n    if (mergedConfig.autoConnect === true) {\n      client.connect();\n    }\n\n    return () => {\n      unsubState();\n      unsubMessage();\n      client.disconnect();\n    };\n  }, [\n    realtimeEnabled,\n    mergedConfig.type,\n    mergedConfig.url,\n    mergedConfig.autoConnect,\n    mergedConfig.cacheStrategies,\n  ]);\n\n  /**\n   * Connect to realtime server\n   * Only connects if real-time updates feature flag is enabled\n   */\n  const connect = useCallback(() => {\n    if (!realtimeEnabled) {\n      console.warn(\n        '[RealtimeProvider] Cannot connect - real-time updates disabled via feature flag'\n      );\n      return;\n    }\n    clientRef.current?.connect();\n  }, [realtimeEnabled]);\n\n  /**\n   * Disconnect from realtime server\n   */\n  const disconnect = useCallback(() => {\n    clientRef.current?.disconnect();\n  }, []);\n\n  /**\n   * Subscribe to a channel\n   */\n  const subscribe = useCallback((channel: string, handler: (data: unknown) => void) => {\n    // Track handler locally\n    if (!channelHandlersRef.current.has(channel)) {\n      channelHandlersRef.current.set(channel, new Set());\n    }\n    const handlers = channelHandlersRef.current.get(channel);\n    if (handlers) {\n      handlers.add(handler);\n    }\n\n    // Subscribe via client if WebSocket\n    let clientUnsub: (() => void) | undefined;\n    if (clientRef.current instanceof WebSocketClient) {\n      clientUnsub = clientRef.current.subscribe(channel, handler);\n    } else if (clientRef.current instanceof SSEClient) {\n      clientUnsub = clientRef.current.on(channel, handler);\n    }\n\n    // Return unsubscribe function\n    return () => {\n      const handlers = channelHandlersRef.current.get(channel);\n      if (handlers) {\n        handlers.delete(handler);\n        if (handlers.size === 0) {\n          channelHandlersRef.current.delete(channel);\n        }\n      }\n      clientUnsub?.();\n    };\n  }, []);\n\n  /**\n   * Send data to a channel (WebSocket only)\n   */\n  const send = useCallback((channel: string, data: unknown) => {\n    if (clientRef.current instanceof WebSocketClient) {\n      clientRef.current.sendToChannel(channel, data);\n    } else {\n      console.warn('[RealtimeProvider] SSE does not support sending data');\n    }\n  }, []);\n\n  // PERFORMANCE: Memoize context value to prevent unnecessary re-renders\n  const value: RealtimeContextValue = useMemo(\n    () => ({\n      connectionState,\n      isConnected: connectionState === 'connected',\n      connect,\n      disconnect,\n      subscribe,\n      send,\n      realtimeEnabled,\n    }),\n    [connectionState, connect, disconnect, subscribe, send, realtimeEnabled]\n  );\n\n  return <RealtimeContext.Provider value={value}>{children}</RealtimeContext.Provider>;\n}\n\n/**\n * Type guard for stream events\n */\nfunction isStreamEvent(data: unknown): data is StreamEvent {\n  return (\n    typeof data === 'object' &&\n    data !== null &&\n    'type' in data &&\n    'entity' in data &&\n    'timestamp' in data\n  );\n}\n\n/**\n * Hook to access realtime context\n *\n * @example\n * ```tsx\n * function MyComponent() {\n *   const { isConnected, subscribe, send } = useRealtimeContext();\n *   // ...\n * }\n * ```\n */\n// eslint-disable-next-line react-refresh/only-export-components -- Hook is closely related to provider component\nexport function useRealtimeContext(): RealtimeContextValue {\n  const context = useContext(RealtimeContext);\n  if (!context.isConnected && context.connectionState === 'disconnected') {\n    console.warn('[useRealtimeContext] Must be used within RealtimeProvider');\n  }\n  return context;\n}\n"],"names":["defaultConfig","RealtimeProvider","children","config","realtimeEnabled","useFeatureFlag","flagKeys","mergedConfig","connectionState","setConnectionState","useState","clientRef","useRef","cacheUpdaterRef","channelHandlersRef","mapToConnectionState","state","useEffect","createWebSocketClient","createSSEClient","StreamQueryCacheUpdater","queryClient","client","unsubState","unsubMessage","data","isStreamEvent","channelData","handler","connect","useCallback","disconnect","subscribe","channel","handlers","clientUnsub","WebSocketClient","SSEClient","send","value","useMemo","jsx","RealtimeContext","useRealtimeContext","context","useContext"],"mappings":"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"}