{"version":3,"file":"stream-handler.mjs","sources":["../../../src/lib/api/stream-handler.ts"],"sourcesContent":["/**\n * @file Stream Handler\n * @description Streaming response handling utilities for Server-Sent Events (SSE),\n * NDJSON streams, and custom streaming formats with subscription management.\n */\n\nimport type { StreamController, StreamEvent, StreamOptions } from './types';\n\n/**\n * Default stream options\n */\nconst DEFAULT_STREAM_OPTIONS: StreamOptions = {\n  ndjson: false,\n  sse: false,\n  bufferSize: 1000,\n  inactivityTimeout: 30000,\n};\n\n/**\n * Create a stream controller for handling streaming responses\n *\n * @typeParam T - Type of streamed data items\n * @param response - Response with readable body stream\n * @param options - Stream handling options\n * @returns Stream controller for subscribing to events\n *\n * @example\n * ```typescript\n * // NDJSON streaming\n * const stream = streamResponse<Message>(response, { ndjson: true });\n *\n * const unsubscribe = stream.subscribe((event) => {\n *   if (event.type === 'data') {\n *     console.log('Received:', event.data);\n *   }\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * stream.abort();\n * ```\n */\nexport function streamResponse<T>(\n  response: Response | { body: ReadableStream<Uint8Array> | null },\n  options: StreamOptions = {}\n): StreamController<T> {\n  const opts = { ...DEFAULT_STREAM_OPTIONS, ...options };\n  const subscribers: Set<(event: StreamEvent<T>) => void> = new Set();\n  const dataBuffer: T[] = [];\n  let isActive = true;\n  let abortController: AbortController | null = new AbortController();\n  let inactivityTimer: ReturnType<typeof setTimeout> | null = null;\n\n  // Get the body stream\n  const { body } = response;\n  if (body === null || body === undefined) {\n    throw new Error('Response body is not a readable stream');\n  }\n\n  const reader = body.getReader();\n  const decoder = new TextDecoder();\n  let buffer = '';\n\n  // Emit event to all subscribers\n  const emit = (event: StreamEvent<T>): void => {\n    for (const subscriber of subscribers) {\n      subscriber(event);\n    }\n  };\n\n  // Reset inactivity timer\n  const resetInactivityTimer = (): void => {\n    if (inactivityTimer !== null) {\n      clearTimeout(inactivityTimer);\n    }\n    if (\n      opts.inactivityTimeout !== undefined &&\n      opts.inactivityTimeout !== null &&\n      opts.inactivityTimeout > 0\n    ) {\n      inactivityTimer = setTimeout(() => {\n        emit({\n          type: 'error',\n          error: new Error('Stream inactivity timeout'),\n          timestamp: Date.now(),\n        });\n        cleanup();\n      }, opts.inactivityTimeout);\n    }\n  };\n\n  // Parse incoming data\n  const parseData = (text: string): void => {\n    buffer += text;\n\n    if (opts.sse === true) {\n      // Parse Server-Sent Events\n      parseSSE(buffer, (data, remainder) => {\n        buffer = remainder;\n        if (data !== null && data !== undefined) {\n          addData(data as T);\n        }\n      });\n    } else if (opts.ndjson === true) {\n      // Parse NDJSON (newline-delimited JSON)\n      parseNDJSON(buffer, (data, remainder) => {\n        buffer = remainder;\n        if (data !== null && data !== undefined) {\n          addData(data as T);\n        }\n      });\n    } else if (opts.parser !== undefined) {\n      // Custom parser\n      const lines = buffer.split('\\n');\n      buffer = lines.pop() ?? '';\n\n      for (const line of lines) {\n        if (line.trim() !== '') {\n          const data = opts.parser(line);\n          if (data !== null && data !== undefined) {\n            addData(data as T);\n          }\n        }\n      }\n    } else {\n      // Raw text\n      addData(text as unknown as T);\n      buffer = '';\n    }\n  };\n\n  // Add data to buffer and emit\n  const addData = (data: T): void => {\n    // Limit buffer size\n    if (\n      opts.bufferSize !== undefined &&\n      opts.bufferSize !== null &&\n      dataBuffer.length >= opts.bufferSize\n    ) {\n      dataBuffer.shift();\n    }\n    dataBuffer.push(data);\n\n    emit({\n      type: 'data',\n      data,\n      timestamp: Date.now(),\n    });\n\n    resetInactivityTimer();\n  };\n\n  // Cleanup resources\n  const cleanup = (): void => {\n    isActive = false;\n    if (inactivityTimer) {\n      clearTimeout(inactivityTimer);\n      inactivityTimer = null;\n    }\n    if (abortController) {\n      abortController = null;\n    }\n  };\n\n  // Start reading\n  const read = async (): Promise<void> => {\n    try {\n      resetInactivityTimer();\n\n      while (isActive) {\n        const { done, value } = await reader.read();\n\n        if (done) {\n          // Handle remaining buffer\n          if (buffer.trim()) {\n            parseData('');\n          }\n\n          emit({ type: 'end', timestamp: Date.now() });\n          cleanup();\n          break;\n        }\n\n        const text = decoder.decode(value, { stream: true });\n        parseData(text);\n      }\n    } catch (error) {\n      if (error instanceof DOMException && error.name === 'AbortError') {\n        emit({ type: 'abort', timestamp: Date.now() });\n      } else {\n        emit({\n          type: 'error',\n          error: error instanceof Error ? error : new Error(String(error)),\n          timestamp: Date.now(),\n        });\n      }\n      cleanup();\n    }\n  };\n\n  // Start reading in background\n  void read();\n\n  return {\n    subscribe: (callback: (event: StreamEvent<T>) => void) => {\n      subscribers.add(callback);\n      return () => {\n        subscribers.delete(callback);\n      };\n    },\n    abort: () => {\n      if (abortController) {\n        void reader.cancel();\n        emit({ type: 'abort', timestamp: Date.now() });\n        cleanup();\n      }\n    },\n    isActive: () => isActive,\n    getData: () => [...dataBuffer],\n  };\n}\n\n/**\n * Parse Server-Sent Events format\n */\nfunction parseSSE(\n  buffer: string,\n  callback: (data: unknown | null, remainder: string) => void\n): void {\n  const events = buffer.split('\\n\\n');\n  const remainder = events.pop() ?? '';\n\n  for (const event of events) {\n    if (!event.trim()) continue;\n\n    const lines = event.split('\\n');\n    let data = '';\n    let eventType = 'message';\n\n    for (const line of lines) {\n      if (line.startsWith('data:')) {\n        data += line.slice(5).trim();\n      } else if (line.startsWith('event:')) {\n        eventType = line.slice(6).trim();\n      }\n    }\n\n    if (data !== '') {\n      try {\n        const parsedData: unknown = JSON.parse(data);\n        callback({ type: eventType, data: parsedData }, '');\n      } catch {\n        callback({ type: eventType, data: data as unknown }, '');\n      }\n    }\n  }\n\n  callback(null, remainder);\n}\n\n/**\n * Parse NDJSON format\n */\nfunction parseNDJSON(\n  buffer: string,\n  callback: (data: unknown | null, remainder: string) => void\n): void {\n  const lines = buffer.split('\\n');\n  const remainder = lines.pop() ?? '';\n\n  for (const line of lines) {\n    if (!line.trim()) continue;\n\n    try {\n      const data: unknown = JSON.parse(line);\n      callback(data, '');\n    } catch {\n      // Skip invalid JSON lines\n      console.warn('[StreamResponse] Invalid JSON line:', line);\n    }\n  }\n\n  callback(null, remainder);\n}\n"],"names":["DEFAULT_STREAM_OPTIONS","streamResponse","response","options","opts","subscribers","dataBuffer","isActive","abortController","inactivityTimer","body","reader","decoder","buffer","emit","event","subscriber","resetInactivityTimer","cleanup","parseData","text","parseSSE","data","remainder","addData","parseNDJSON","lines","line","done","value","error","callback","events","eventType","parsedData"],"mappings":"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"}