{"version":3,"file":"sse-client.mjs","sources":["../../../src/lib/realtime/sse-client.ts"],"sourcesContent":["/**\n * @file SSE Client\n * @description Low-level Server-Sent Events client\n */\n\nimport { env } from '../../config/env';\n\n/**\n * SSE connection state\n */\nexport type SSEState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';\n\n/**\n * SSE message handler\n */\nexport type SSEMessageHandler = (data: unknown, event?: string) => void;\n\n/**\n * SSE error handler\n */\nexport type SSEErrorHandler = (error: Event) => void;\n\n/**\n * SSE state change handler\n */\nexport type SSEStateChangeHandler = (state: SSEState) => void;\n\n/**\n * SSE client configuration\n */\nexport interface SSEClientConfig {\n  url: string;\n  withCredentials?: boolean;\n  reconnect?: boolean;\n  reconnectInterval?: number;\n  maxReconnectAttempts?: number;\n  eventTypes?: string[];\n}\n\n/**\n * Default configuration\n */\nconst defaultConfig: Partial<SSEClientConfig> = {\n  withCredentials: true,\n  reconnect: true,\n  reconnectInterval: 3000,\n  maxReconnectAttempts: 10,\n  eventTypes: ['message'],\n};\n\n/**\n * SSE client class\n */\nexport class SSEClient {\n  private eventSource: EventSource | null = null;\n  private config: SSEClientConfig;\n  private state: SSEState = 'disconnected';\n  private reconnectAttempts = 0;\n  private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n\n  private messageHandlers: Map<string, Set<SSEMessageHandler>> = new Map();\n  private errorHandlers: Set<SSEErrorHandler> = new Set();\n  private stateChangeHandlers: Set<SSEStateChangeHandler> = new Set();\n\n  /**\n   * Store bound event handler references for proper cleanup\n   * This fixes a memory leak where createEventHandler() created new functions\n   * that couldn't be properly removed with removeEventListener\n   */\n  private boundEventHandlers: Map<string, (event: MessageEvent) => void> = new Map();\n\n  constructor(config: SSEClientConfig) {\n    this.config = { ...defaultConfig, ...config };\n  }\n\n  /**\n   * Get current connection state\n   */\n  getState(): SSEState {\n    return this.state;\n  }\n\n  /**\n   * Check if connected\n   */\n  isConnected(): boolean {\n    return this.state === 'connected' && this.eventSource?.readyState === EventSource.OPEN;\n  }\n\n  /**\n   * Connect to SSE endpoint\n   */\n  connect(): void {\n    if (this.eventSource?.readyState === EventSource.OPEN) {\n      return;\n    }\n\n    this.setState('connecting');\n\n    try {\n      this.eventSource = new EventSource(this.config.url, {\n        withCredentials: this.config.withCredentials ?? false,\n      });\n      this.setupEventListeners();\n    } catch (error) {\n      console.error('[SSE] Connection error:', error);\n      this.handleDisconnect();\n    }\n  }\n\n  /**\n   * Disconnect from SSE endpoint\n   */\n  disconnect(): void {\n    this.clearReconnectTimeout();\n\n    if (this.eventSource) {\n      this.eventSource.close();\n      this.eventSource = null;\n    }\n\n    this.setState('disconnected');\n    this.reconnectAttempts = 0;\n  }\n\n  /**\n   * Subscribe to messages of a specific event type\n   *\n   * Note: Uses stored bound handler references for proper cleanup.\n   * This prevents memory leaks from orphaned event listeners.\n   */\n  on(eventType: string, handler: SSEMessageHandler): () => void {\n    if (!this.messageHandlers.has(eventType)) {\n      this.messageHandlers.set(eventType, new Set());\n\n      // Add event listener if connected\n      if (this.eventSource) {\n        this.addEventTypeListener(eventType);\n      }\n    }\n\n    const handlers = this.messageHandlers.get(eventType);\n    if (handlers) {\n      handlers.add(handler);\n    }\n\n    return () => {\n      const handlers = this.messageHandlers.get(eventType);\n      if (handlers) {\n        handlers.delete(handler);\n        if (handlers.size === 0) {\n          this.messageHandlers.delete(eventType);\n          // Use stored bound handler reference for proper cleanup\n          const boundHandler = this.boundEventHandlers.get(eventType);\n          if (boundHandler) {\n            this.eventSource?.removeEventListener(eventType, boundHandler as EventListener);\n            this.boundEventHandlers.delete(eventType);\n          }\n        }\n      }\n    };\n  }\n\n  /**\n   * Subscribe to all 'message' events (default)\n   */\n  onMessage(handler: SSEMessageHandler): () => void {\n    return this.on('message', handler);\n  }\n\n  /**\n   * Subscribe to errors\n   */\n  onError(handler: SSEErrorHandler): () => void {\n    this.errorHandlers.add(handler);\n    return () => this.errorHandlers.delete(handler);\n  }\n\n  /**\n   * Subscribe to state changes\n   */\n  onStateChange(handler: SSEStateChangeHandler): () => void {\n    this.stateChangeHandlers.add(handler);\n    return () => this.stateChangeHandlers.delete(handler);\n  }\n\n  /**\n   * Setup EventSource event listeners\n   */\n  private setupEventListeners(): void {\n    if (!this.eventSource) return;\n\n    this.eventSource.onopen = () => {\n      this.setState('connected');\n      this.reconnectAttempts = 0;\n    };\n\n    this.eventSource.onerror = (event) => {\n      console.error('[SSE] Error:', event);\n      this.errorHandlers.forEach((handler) => handler(event));\n\n      if (this.eventSource?.readyState === EventSource.CLOSED) {\n        this.handleDisconnect();\n      }\n    };\n\n    // Setup listeners for configured event types\n    const eventTypes = [\n      ...new Set([...(this.config.eventTypes ?? ['message']), ...this.messageHandlers.keys()]),\n    ];\n\n    eventTypes.forEach((eventType) => {\n      this.addEventTypeListener(eventType);\n    });\n  }\n\n  /**\n   * Add listener for specific event type\n   *\n   * Note: Stores bound handler reference for proper cleanup later.\n   */\n  private addEventTypeListener(eventType: string): void {\n    if (!this.eventSource) return;\n\n    // Check if we already have a bound handler for this event type\n    if (!this.boundEventHandlers.has(eventType)) {\n      const handler = this.createEventHandler(eventType);\n      this.boundEventHandlers.set(eventType, handler);\n    }\n\n    const boundHandler = this.boundEventHandlers.get(eventType);\n    if (boundHandler) {\n      this.eventSource.addEventListener(eventType, boundHandler as EventListener);\n    }\n  }\n\n  /**\n   * Create event handler for event type\n   */\n  private createEventHandler(eventType: string): (event: MessageEvent) => void {\n    return (event: MessageEvent) => {\n      // eslint-disable-next-line prefer-destructuring -- event.data has 'any' type which is not safe to destructure\n      let data: unknown = event.data;\n\n      // Try to parse JSON\n      try {\n        data = JSON.parse(event.data as string) as unknown;\n      } catch {\n        // Keep as string if not valid JSON\n      }\n\n      // Notify handlers for this event type\n      const handlers = this.messageHandlers.get(eventType);\n      handlers?.forEach((handler) => handler(data, eventType));\n    };\n  }\n\n  /**\n   * Handle disconnection\n   */\n  private handleDisconnect(): void {\n    this.eventSource = null;\n\n    if (\n      this.config.reconnect === true &&\n      this.reconnectAttempts < (this.config.maxReconnectAttempts ?? 10)\n    ) {\n      this.setState('reconnecting');\n      this.scheduleReconnect();\n    } else {\n      this.setState('disconnected');\n    }\n  }\n\n  /**\n   * Schedule reconnection\n   */\n  private scheduleReconnect(): void {\n    this.clearReconnectTimeout();\n\n    const delay = this.calculateBackoff();\n    this.reconnectAttempts++;\n\n    console.info(`[SSE] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);\n    this.reconnectTimeout = setTimeout(() => {\n      this.connect();\n    }, delay);\n  }\n\n  /**\n   * Calculate exponential backoff delay\n   */\n  private calculateBackoff(): number {\n    const base = this.config.reconnectInterval ?? 3000;\n    const delay = base * Math.pow(1.5, this.reconnectAttempts);\n    const jitter = Math.random() * 1000;\n    return Math.min(delay + jitter, 30000); // Max 30 seconds\n  }\n\n  /**\n   * Clear reconnect timeout\n   */\n  private clearReconnectTimeout(): void {\n    if (this.reconnectTimeout !== null) {\n      clearTimeout(this.reconnectTimeout);\n      this.reconnectTimeout = null;\n    }\n  }\n\n  /**\n   * Update and broadcast state\n   */\n  private setState(state: SSEState): void {\n    this.state = state;\n    this.stateChangeHandlers.forEach((handler) => handler(state));\n  }\n}\n\n/**\n * Create SSE client with default configuration\n */\nexport function createSSEClient(\n  path: string = '/events',\n  config?: Partial<SSEClientConfig>\n): SSEClient {\n  const baseUrl = env.apiBaseUrl ?? '';\n  const url = `${baseUrl}${path}`;\n\n  return new SSEClient({ url, ...config });\n}\n"],"names":["defaultConfig","SSEClient","config","error","eventType","handler","handlers","boundHandler","event","data","delay","jitter","state","createSSEClient","path","url","env"],"mappings":"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"}