@tanstack/ai-client 0.22.1 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/audio-recorder.js +190 -213
  3. package/dist/esm/audio-recorder.js.map +1 -1
  4. package/dist/esm/chat-client.d.ts +172 -3
  5. package/dist/esm/chat-client.js +1656 -1386
  6. package/dist/esm/chat-client.js.map +1 -1
  7. package/dist/esm/cleared-stream-tracker.d.ts +23 -0
  8. package/dist/esm/cleared-stream-tracker.js +97 -0
  9. package/dist/esm/cleared-stream-tracker.js.map +1 -0
  10. package/dist/esm/client-persistor.d.ts +25 -12
  11. package/dist/esm/client-persistor.js +260 -235
  12. package/dist/esm/client-persistor.js.map +1 -1
  13. package/dist/esm/connection-adapters.d.ts +231 -10
  14. package/dist/esm/connection-adapters.js +989 -574
  15. package/dist/esm/connection-adapters.js.map +1 -1
  16. package/dist/esm/devtools-noop.d.ts +1 -0
  17. package/dist/esm/devtools-noop.js +79 -139
  18. package/dist/esm/devtools-noop.js.map +1 -1
  19. package/dist/esm/devtools.d.ts +31 -1
  20. package/dist/esm/devtools.js +977 -1127
  21. package/dist/esm/devtools.js.map +1 -1
  22. package/dist/esm/events.js +224 -226
  23. package/dist/esm/events.js.map +1 -1
  24. package/dist/esm/generation-client.d.ts +145 -2
  25. package/dist/esm/generation-client.js +659 -321
  26. package/dist/esm/generation-client.js.map +1 -1
  27. package/dist/esm/generation-reconstruct.d.ts +21 -0
  28. package/dist/esm/generation-reconstruct.js +85 -0
  29. package/dist/esm/generation-reconstruct.js.map +1 -0
  30. package/dist/esm/generation-types.d.ts +289 -3
  31. package/dist/esm/generation-types.js +356 -13
  32. package/dist/esm/generation-types.js.map +1 -1
  33. package/dist/esm/index.d.ts +9 -4
  34. package/dist/esm/index.js +7 -39
  35. package/dist/esm/interrupt-manager.d.ts +77 -0
  36. package/dist/esm/interrupt-manager.js +787 -0
  37. package/dist/esm/interrupt-manager.js.map +1 -0
  38. package/dist/esm/mcp-app-bridge.js +56 -64
  39. package/dist/esm/mcp-app-bridge.js.map +1 -1
  40. package/dist/esm/realtime-client.js +366 -440
  41. package/dist/esm/realtime-client.js.map +1 -1
  42. package/dist/esm/response-stream.js +19 -26
  43. package/dist/esm/response-stream.js.map +1 -1
  44. package/dist/esm/sse-parser.js +44 -47
  45. package/dist/esm/sse-parser.js.map +1 -1
  46. package/dist/esm/sse-utils.js +8 -9
  47. package/dist/esm/sse-utils.js.map +1 -1
  48. package/dist/esm/storage-adapters.d.ts +62 -0
  49. package/dist/esm/storage-adapters.js +174 -0
  50. package/dist/esm/storage-adapters.js.map +1 -0
  51. package/dist/esm/types.d.ts +212 -10
  52. package/dist/esm/types.js +38 -7
  53. package/dist/esm/types.js.map +1 -1
  54. package/dist/esm/video-generation-client.d.ts +113 -2
  55. package/dist/esm/video-generation-client.js +665 -379
  56. package/dist/esm/video-generation-client.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/chat-client.ts +1079 -61
  59. package/src/cleared-stream-tracker.ts +151 -0
  60. package/src/client-persistor.ts +102 -33
  61. package/src/connection-adapters.ts +1185 -142
  62. package/src/devtools-noop.ts +4 -3
  63. package/src/devtools.ts +121 -3
  64. package/src/generation-client.ts +563 -13
  65. package/src/generation-reconstruct.ts +121 -0
  66. package/src/generation-types.ts +727 -3
  67. package/src/index.ts +56 -1
  68. package/src/interrupt-manager.ts +1440 -0
  69. package/src/storage-adapters.ts +242 -0
  70. package/src/types.ts +301 -9
  71. package/src/video-generation-client.ts +479 -13
  72. package/dist/esm/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"realtime-client.js","sources":["../../src/realtime-client.ts"],"sourcesContent":["import { convertSchemaToJsonSchema } from '@tanstack/ai/client'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n RealtimeToken,\n} from '@tanstack/ai/client'\nimport type {\n RealtimeClientOptions,\n RealtimeClientState,\n RealtimeConnection,\n RealtimeStateChangeCallback,\n} from './realtime-types'\n\n// Token refresh buffer - refresh 1 minute before expiry\nconst TOKEN_REFRESH_BUFFER_MS = 60_000\n\n/**\n * Client for managing realtime voice conversations.\n *\n * Handles connection lifecycle, audio I/O, message state,\n * and tool execution for realtime voice-to-voice AI interactions.\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * tools: [myTool.client(handler)],\n * onMessage: (msg) => console.log('Message:', msg),\n * })\n *\n * await client.connect()\n * ```\n */\nexport class RealtimeClient {\n private readonly options: RealtimeClientOptions\n private connection: RealtimeConnection | null = null\n private token: RealtimeToken | null = null\n private tokenRefreshTimeout: ReturnType<typeof setTimeout> | null = null\n private readonly clientTools: Map<string, AnyClientTool>\n private readonly stateChangeCallbacks: Set<RealtimeStateChangeCallback> =\n new Set()\n private unsubscribers: Array<() => void> = []\n\n private state: RealtimeClientState = {\n status: 'idle',\n mode: 'idle',\n messages: [],\n pendingUserTranscript: null,\n pendingAssistantTranscript: null,\n error: null,\n }\n\n constructor(options: RealtimeClientOptions) {\n this.options = {\n autoPlayback: true,\n autoCapture: true,\n vadMode: 'server',\n ...options,\n }\n\n // Build client tools map\n this.clientTools = new Map()\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientTools.set(tool.name, tool)\n }\n }\n }\n\n // ============================================================================\n // Connection Lifecycle\n // ============================================================================\n\n /**\n * Connect to the realtime session.\n * Fetches a token and establishes the connection.\n */\n async connect(): Promise<void> {\n if (this.state.status === 'connected') {\n return\n }\n\n this.updateState({ status: 'connecting', error: null })\n\n try {\n // Fetch token from server\n this.token = await this.options.getToken()\n\n // Schedule token refresh\n this.scheduleTokenRefresh()\n\n // Connect via adapter (pass tools for providers like ElevenLabs that need them at connect time)\n const toolsList =\n this.clientTools.size > 0\n ? Array.from(this.clientTools.values())\n : undefined\n\n this.connection = await this.options.adapter.connect(\n this.token,\n toolsList,\n )\n\n // Subscribe to connection events\n this.subscribeToConnectionEvents()\n\n // Auto-configure session with client-provided settings\n this.applySessionConfig()\n\n // Start audio capture if configured\n if (this.options.autoCapture) {\n await this.connection.startAudioCapture()\n }\n\n this.updateState({ status: 'connected', mode: 'listening' })\n this.options.onConnect?.()\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n this.updateState({ status: 'error', error: err })\n this.options.onError?.(err)\n throw err\n }\n }\n\n /**\n * Disconnect from the realtime session.\n */\n async disconnect(): Promise<void> {\n if (this.tokenRefreshTimeout) {\n clearTimeout(this.tokenRefreshTimeout)\n this.tokenRefreshTimeout = null\n }\n\n // Unsubscribe from all events\n for (const unsub of this.unsubscribers) {\n unsub()\n }\n this.unsubscribers = []\n\n if (this.connection) {\n await this.connection.disconnect()\n this.connection = null\n }\n\n this.token = null\n this.updateState({\n status: 'idle',\n mode: 'idle',\n pendingUserTranscript: null,\n pendingAssistantTranscript: null,\n })\n this.options.onDisconnect?.()\n }\n\n // ============================================================================\n // Voice Control\n // ============================================================================\n\n /**\n * Start listening for voice input.\n * Only needed when vadMode is 'manual'.\n */\n startListening(): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n void this.connection.startAudioCapture()\n this.updateState({ mode: 'listening' })\n }\n\n /**\n * Stop listening for voice input.\n * Only needed when vadMode is 'manual'.\n */\n stopListening(): void {\n if (!this.connection) {\n return\n }\n this.connection.stopAudioCapture()\n this.updateState({ mode: 'idle' })\n }\n\n /**\n * Interrupt the current assistant response.\n */\n interrupt(): void {\n if (!this.connection) {\n return\n }\n this.connection.interrupt()\n }\n\n // ============================================================================\n // Text Input\n // ============================================================================\n\n /**\n * Send a text message instead of voice.\n */\n sendText(text: string): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n\n // Add user message\n const userMessage: RealtimeMessage = {\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'text', content: text }],\n }\n this.addMessage(userMessage)\n\n // Send to provider\n this.connection.sendText(text)\n }\n\n /**\n * Send an image to the conversation.\n * @param imageData - Base64-encoded image data or a URL\n * @param mimeType - MIME type of the image (e.g., 'image/png', 'image/jpeg')\n */\n sendImage(imageData: string, mimeType: string): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n\n // Add user message with image part\n const userMessage: RealtimeMessage = {\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'image', data: imageData, mimeType }],\n }\n this.addMessage(userMessage)\n\n // Send to provider\n this.connection.sendImage(imageData, mimeType)\n }\n\n // ============================================================================\n // State Access\n // ============================================================================\n\n /** Get current connection status */\n get status(): RealtimeStatus {\n return this.state.status\n }\n\n /** Get current mode */\n get mode(): RealtimeMode {\n return this.state.mode\n }\n\n /** Get conversation messages */\n get messages(): Array<RealtimeMessage> {\n return this.state.messages\n }\n\n /** Get current error, if any */\n get error(): Error | null {\n return this.state.error\n }\n\n /** Get pending user transcript (while user is speaking) */\n get pendingUserTranscript(): string | null {\n return this.state.pendingUserTranscript\n }\n\n /** Get pending assistant transcript (while assistant is speaking) */\n get pendingAssistantTranscript(): string | null {\n return this.state.pendingAssistantTranscript\n }\n\n /** Get audio visualization data */\n get audio(): AudioVisualization | null {\n return this.connection?.getAudioVisualization() ?? null\n }\n\n /**\n * Update the session configuration.\n * This applies changes to the active connection and persists them for future reconnections.\n */\n updateSession(config: Partial<RealtimeSessionConfig>): void {\n // Persist type-compatible fields so future (re)connections use the updated\n // config. `tools` is intentionally excluded: a session's tool configs are\n // serialized descriptors, whereas `options.tools` holds executable client\n // tools tracked separately via `clientTools`.\n const o = this.options\n if (config.instructions !== undefined) o.instructions = config.instructions\n if (config.voice !== undefined) o.voice = config.voice\n if (config.vadMode !== undefined) o.vadMode = config.vadMode\n if (config.outputModalities !== undefined) {\n o.outputModalities = config.outputModalities\n }\n if (config.temperature !== undefined) o.temperature = config.temperature\n if (config.maxOutputTokens !== undefined) {\n o.maxOutputTokens = config.maxOutputTokens\n }\n if (config.semanticEagerness !== undefined) {\n o.semanticEagerness = config.semanticEagerness\n }\n if (config.providerOptions !== undefined) {\n o.providerOptions = config.providerOptions\n }\n\n if (this.connection) {\n this.applySessionConfig()\n }\n }\n\n // ============================================================================\n // State Subscription\n // ============================================================================\n\n /**\n * Subscribe to state changes.\n * @returns Unsubscribe function\n */\n onStateChange(callback: RealtimeStateChangeCallback): () => void {\n this.stateChangeCallbacks.add(callback)\n return () => {\n this.stateChangeCallbacks.delete(callback)\n }\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Clean up resources.\n * Call this when disposing of the client.\n */\n destroy(): void {\n void this.disconnect()\n this.stateChangeCallbacks.clear()\n }\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private updateState(updates: Partial<RealtimeClientState>): void {\n this.state = { ...this.state, ...updates }\n\n // Notify callbacks\n for (const callback of this.stateChangeCallbacks) {\n callback(this.state)\n }\n\n // Notify specific callbacks\n if ('status' in updates && updates.status !== undefined) {\n this.options.onStatusChange?.(updates.status)\n }\n if ('mode' in updates && updates.mode !== undefined) {\n this.options.onModeChange?.(updates.mode)\n }\n }\n\n private addMessage(message: RealtimeMessage): void {\n this.updateState({\n messages: [...this.state.messages, message],\n })\n this.options.onMessage?.(message)\n }\n\n private scheduleTokenRefresh(): void {\n if (!this.token) return\n\n const timeUntilExpiry = this.token.expiresAt - Date.now()\n const refreshIn = Math.max(0, timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS)\n\n this.tokenRefreshTimeout = setTimeout(() => {\n void this.refreshToken()\n }, refreshIn)\n }\n\n private async refreshToken(): Promise<void> {\n try {\n this.token = await this.options.getToken()\n this.scheduleTokenRefresh()\n this.connection?.updateToken?.(this.token)\n // Note: Some providers may require reconnection with new token\n // This is handled by the adapter implementation\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n this.updateState({ error: err })\n this.options.onError?.(err)\n }\n }\n\n private subscribeToConnectionEvents(): void {\n if (!this.connection) return\n\n // Status changes\n this.unsubscribers.push(\n this.connection.on('status_change', ({ status }) => {\n this.updateState({ status })\n }),\n )\n\n // Mode changes\n this.unsubscribers.push(\n this.connection.on('mode_change', ({ mode }) => {\n this.updateState({ mode })\n }),\n )\n\n // Transcripts (streaming)\n // User transcripts are added as messages when final (no separate message_complete for user input)\n // Assistant transcripts are streamed, final message comes via message_complete\n this.unsubscribers.push(\n this.connection.on('transcript', ({ role, transcript, isFinal }) => {\n if (role === 'user') {\n this.updateState({\n pendingUserTranscript: isFinal ? null : transcript,\n })\n // Add user message when transcript is finalized\n if (isFinal && transcript) {\n this.addMessage({\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript, durationMs: 0 }],\n })\n }\n } else {\n // Assistant transcripts - just update pending, message_complete handles final\n this.updateState({\n pendingAssistantTranscript: isFinal ? null : transcript,\n })\n }\n }),\n )\n\n // Tool calls\n this.unsubscribers.push(\n this.connection.on('tool_call', ({ toolCallId, toolName, input }) => {\n if (!toolCallId) {\n console.error(\n '[RealtimeClient] tool_call missing toolCallId',\n toolName,\n )\n return\n }\n const tool = this.clientTools.get(toolName)\n const execute = tool?.execute\n if (execute) {\n void (async () => {\n try {\n const output = await execute(input)\n this.connection?.sendToolResult(\n toolCallId,\n typeof output === 'string' ? output : JSON.stringify(output),\n )\n } catch (error) {\n const errMsg =\n error instanceof Error ? error.message : String(error)\n this.connection?.sendToolResult(\n toolCallId,\n JSON.stringify({ error: errMsg }),\n )\n }\n })()\n } else {\n this.connection?.sendToolResult(\n toolCallId,\n JSON.stringify({\n error: `Unknown or non-executable tool: ${String(toolName)}`,\n }),\n )\n }\n }),\n )\n\n // Message complete\n this.unsubscribers.push(\n this.connection.on('message_complete', ({ message }) => {\n // Replace pending message with final version if needed\n const existingIndex = this.state.messages.findIndex(\n (m) => m.id === message.id,\n )\n if (existingIndex >= 0) {\n const newMessages = [...this.state.messages]\n newMessages[existingIndex] = message\n this.updateState({ messages: newMessages })\n } else {\n this.addMessage(message)\n }\n }),\n )\n\n // Interruption\n this.unsubscribers.push(\n this.connection.on('interrupted', ({ messageId }) => {\n if (messageId) {\n const newMessages = this.state.messages.map((m) =>\n m.id === messageId ? { ...m, interrupted: true } : m,\n )\n this.updateState({ messages: newMessages })\n }\n this.updateState({\n mode: 'listening',\n pendingAssistantTranscript: null,\n })\n this.options.onInterrupted?.()\n }),\n )\n\n // Errors\n this.unsubscribers.push(\n this.connection.on('error', ({ error }) => {\n this.updateState({ error })\n this.options.onError?.(error)\n }),\n )\n\n this.unsubscribers.push(\n this.connection.on('go_away', ({ timeLeft }) => {\n this.options.onGoAway?.(timeLeft)\n }),\n )\n\n this.unsubscribers.push(\n this.connection.on('usage', (usage) => {\n this.options.onUsage?.(usage)\n }),\n )\n }\n\n private applySessionConfig(): void {\n if (!this.connection) return\n\n const {\n instructions,\n voice,\n vadMode,\n tools,\n outputModalities,\n temperature,\n maxOutputTokens,\n semanticEagerness,\n } = this.options\n const hasConfig =\n instructions ||\n voice ||\n vadMode ||\n (tools && tools.length > 0) ||\n outputModalities ||\n temperature !== undefined ||\n maxOutputTokens !== undefined ||\n semanticEagerness\n if (!hasConfig) return\n\n // `RealtimeToolConfig.inputSchema`/`outputSchema` are `Record<string, any>`\n // (no `undefined` under `exactOptionalPropertyTypes`). Conditionally spread\n // each so we don't pass `undefined` when the tool has no such schema.\n const toolsConfig = tools\n ? Array.from(this.clientTools.values()).map((t) => {\n const inputSchema = t.inputSchema\n ? convertSchemaToJsonSchema(t.inputSchema)\n : undefined\n const outputSchema = t.outputSchema\n ? convertSchemaToJsonSchema(t.outputSchema)\n : undefined\n return {\n name: t.name,\n description: t.description,\n ...(inputSchema ? { inputSchema } : {}),\n ...(outputSchema ? { outputSchema } : {}),\n }\n })\n : undefined\n\n // `RealtimeSessionConfig` declares each field as `field?: T` (no\n // `undefined`). Spread each one conditionally so we don't violate\n // `exactOptionalPropertyTypes` when the source is `T | undefined`.\n this.connection.updateSession({\n ...(instructions !== undefined ? { instructions } : {}),\n ...(voice !== undefined ? { voice } : {}),\n ...(vadMode !== undefined ? { vadMode } : {}),\n ...(toolsConfig !== undefined ? { tools: toolsConfig } : {}),\n ...(outputModalities !== undefined ? { outputModalities } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(semanticEagerness !== undefined ? { semanticEagerness } : {}),\n })\n }\n\n private generateId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"realtime-client.js","names":[],"sources":["../../src/realtime-client.ts"],"sourcesContent":["import { convertSchemaToJsonSchema } from '@tanstack/ai/client'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n RealtimeToken,\n} from '@tanstack/ai/client'\nimport type {\n RealtimeClientOptions,\n RealtimeClientState,\n RealtimeConnection,\n RealtimeStateChangeCallback,\n} from './realtime-types'\n\n// Token refresh buffer - refresh 1 minute before expiry\nconst TOKEN_REFRESH_BUFFER_MS = 60_000\n\n/**\n * Client for managing realtime voice conversations.\n *\n * Handles connection lifecycle, audio I/O, message state,\n * and tool execution for realtime voice-to-voice AI interactions.\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * tools: [myTool.client(handler)],\n * onMessage: (msg) => console.log('Message:', msg),\n * })\n *\n * await client.connect()\n * ```\n */\nexport class RealtimeClient {\n private readonly options: RealtimeClientOptions\n private connection: RealtimeConnection | null = null\n private token: RealtimeToken | null = null\n private tokenRefreshTimeout: ReturnType<typeof setTimeout> | null = null\n private readonly clientTools: Map<string, AnyClientTool>\n private readonly stateChangeCallbacks: Set<RealtimeStateChangeCallback> =\n new Set()\n private unsubscribers: Array<() => void> = []\n\n private state: RealtimeClientState = {\n status: 'idle',\n mode: 'idle',\n messages: [],\n pendingUserTranscript: null,\n pendingAssistantTranscript: null,\n error: null,\n }\n\n constructor(options: RealtimeClientOptions) {\n this.options = {\n autoPlayback: true,\n autoCapture: true,\n vadMode: 'server',\n ...options,\n }\n\n // Build client tools map\n this.clientTools = new Map()\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientTools.set(tool.name, tool)\n }\n }\n }\n\n // ============================================================================\n // Connection Lifecycle\n // ============================================================================\n\n /**\n * Connect to the realtime session.\n * Fetches a token and establishes the connection.\n */\n async connect(): Promise<void> {\n if (this.state.status === 'connected') {\n return\n }\n\n this.updateState({ status: 'connecting', error: null })\n\n try {\n // Fetch token from server\n this.token = await this.options.getToken()\n\n // Schedule token refresh\n this.scheduleTokenRefresh()\n\n // Connect via adapter (pass tools for providers like ElevenLabs that need them at connect time)\n const toolsList =\n this.clientTools.size > 0\n ? Array.from(this.clientTools.values())\n : undefined\n\n this.connection = await this.options.adapter.connect(\n this.token,\n toolsList,\n )\n\n // Subscribe to connection events\n this.subscribeToConnectionEvents()\n\n // Auto-configure session with client-provided settings\n this.applySessionConfig()\n\n // Start audio capture if configured\n if (this.options.autoCapture) {\n await this.connection.startAudioCapture()\n }\n\n this.updateState({ status: 'connected', mode: 'listening' })\n this.options.onConnect?.()\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n this.updateState({ status: 'error', error: err })\n this.options.onError?.(err)\n throw err\n }\n }\n\n /**\n * Disconnect from the realtime session.\n */\n async disconnect(): Promise<void> {\n if (this.tokenRefreshTimeout) {\n clearTimeout(this.tokenRefreshTimeout)\n this.tokenRefreshTimeout = null\n }\n\n // Unsubscribe from all events\n for (const unsub of this.unsubscribers) {\n unsub()\n }\n this.unsubscribers = []\n\n if (this.connection) {\n await this.connection.disconnect()\n this.connection = null\n }\n\n this.token = null\n this.updateState({\n status: 'idle',\n mode: 'idle',\n pendingUserTranscript: null,\n pendingAssistantTranscript: null,\n })\n this.options.onDisconnect?.()\n }\n\n // ============================================================================\n // Voice Control\n // ============================================================================\n\n /**\n * Start listening for voice input.\n * Only needed when vadMode is 'manual'.\n */\n startListening(): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n void this.connection.startAudioCapture()\n this.updateState({ mode: 'listening' })\n }\n\n /**\n * Stop listening for voice input.\n * Only needed when vadMode is 'manual'.\n */\n stopListening(): void {\n if (!this.connection) {\n return\n }\n this.connection.stopAudioCapture()\n this.updateState({ mode: 'idle' })\n }\n\n /**\n * Interrupt the current assistant response.\n */\n interrupt(): void {\n if (!this.connection) {\n return\n }\n this.connection.interrupt()\n }\n\n // ============================================================================\n // Text Input\n // ============================================================================\n\n /**\n * Send a text message instead of voice.\n */\n sendText(text: string): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n\n // Add user message\n const userMessage: RealtimeMessage = {\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'text', content: text }],\n }\n this.addMessage(userMessage)\n\n // Send to provider\n this.connection.sendText(text)\n }\n\n /**\n * Send an image to the conversation.\n * @param imageData - Base64-encoded image data or a URL\n * @param mimeType - MIME type of the image (e.g., 'image/png', 'image/jpeg')\n */\n sendImage(imageData: string, mimeType: string): void {\n if (!this.connection || this.state.status !== 'connected') {\n return\n }\n\n // Add user message with image part\n const userMessage: RealtimeMessage = {\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'image', data: imageData, mimeType }],\n }\n this.addMessage(userMessage)\n\n // Send to provider\n this.connection.sendImage(imageData, mimeType)\n }\n\n // ============================================================================\n // State Access\n // ============================================================================\n\n /** Get current connection status */\n get status(): RealtimeStatus {\n return this.state.status\n }\n\n /** Get current mode */\n get mode(): RealtimeMode {\n return this.state.mode\n }\n\n /** Get conversation messages */\n get messages(): Array<RealtimeMessage> {\n return this.state.messages\n }\n\n /** Get current error, if any */\n get error(): Error | null {\n return this.state.error\n }\n\n /** Get pending user transcript (while user is speaking) */\n get pendingUserTranscript(): string | null {\n return this.state.pendingUserTranscript\n }\n\n /** Get pending assistant transcript (while assistant is speaking) */\n get pendingAssistantTranscript(): string | null {\n return this.state.pendingAssistantTranscript\n }\n\n /** Get audio visualization data */\n get audio(): AudioVisualization | null {\n return this.connection?.getAudioVisualization() ?? null\n }\n\n /**\n * Update the session configuration.\n * This applies changes to the active connection and persists them for future reconnections.\n */\n updateSession(config: Partial<RealtimeSessionConfig>): void {\n // Persist type-compatible fields so future (re)connections use the updated\n // config. `tools` is intentionally excluded: a session's tool configs are\n // serialized descriptors, whereas `options.tools` holds executable client\n // tools tracked separately via `clientTools`.\n const o = this.options\n if (config.instructions !== undefined) o.instructions = config.instructions\n if (config.voice !== undefined) o.voice = config.voice\n if (config.vadMode !== undefined) o.vadMode = config.vadMode\n if (config.outputModalities !== undefined) {\n o.outputModalities = config.outputModalities\n }\n if (config.temperature !== undefined) o.temperature = config.temperature\n if (config.maxOutputTokens !== undefined) {\n o.maxOutputTokens = config.maxOutputTokens\n }\n if (config.semanticEagerness !== undefined) {\n o.semanticEagerness = config.semanticEagerness\n }\n if (config.providerOptions !== undefined) {\n o.providerOptions = config.providerOptions\n }\n\n if (this.connection) {\n this.applySessionConfig()\n }\n }\n\n // ============================================================================\n // State Subscription\n // ============================================================================\n\n /**\n * Subscribe to state changes.\n * @returns Unsubscribe function\n */\n onStateChange(callback: RealtimeStateChangeCallback): () => void {\n this.stateChangeCallbacks.add(callback)\n return () => {\n this.stateChangeCallbacks.delete(callback)\n }\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Clean up resources.\n * Call this when disposing of the client.\n */\n destroy(): void {\n void this.disconnect()\n this.stateChangeCallbacks.clear()\n }\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private updateState(updates: Partial<RealtimeClientState>): void {\n this.state = { ...this.state, ...updates }\n\n // Notify callbacks\n for (const callback of this.stateChangeCallbacks) {\n callback(this.state)\n }\n\n // Notify specific callbacks\n if ('status' in updates && updates.status !== undefined) {\n this.options.onStatusChange?.(updates.status)\n }\n if ('mode' in updates && updates.mode !== undefined) {\n this.options.onModeChange?.(updates.mode)\n }\n }\n\n private addMessage(message: RealtimeMessage): void {\n this.updateState({\n messages: [...this.state.messages, message],\n })\n this.options.onMessage?.(message)\n }\n\n private scheduleTokenRefresh(): void {\n if (!this.token) return\n\n const timeUntilExpiry = this.token.expiresAt - Date.now()\n const refreshIn = Math.max(0, timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS)\n\n this.tokenRefreshTimeout = setTimeout(() => {\n void this.refreshToken()\n }, refreshIn)\n }\n\n private async refreshToken(): Promise<void> {\n try {\n this.token = await this.options.getToken()\n this.scheduleTokenRefresh()\n this.connection?.updateToken?.(this.token)\n // Note: Some providers may require reconnection with new token\n // This is handled by the adapter implementation\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n this.updateState({ error: err })\n this.options.onError?.(err)\n }\n }\n\n private subscribeToConnectionEvents(): void {\n if (!this.connection) return\n\n // Status changes\n this.unsubscribers.push(\n this.connection.on('status_change', ({ status }) => {\n this.updateState({ status })\n }),\n )\n\n // Mode changes\n this.unsubscribers.push(\n this.connection.on('mode_change', ({ mode }) => {\n this.updateState({ mode })\n }),\n )\n\n // Transcripts (streaming)\n // User transcripts are added as messages when final (no separate message_complete for user input)\n // Assistant transcripts are streamed, final message comes via message_complete\n this.unsubscribers.push(\n this.connection.on('transcript', ({ role, transcript, isFinal }) => {\n if (role === 'user') {\n this.updateState({\n pendingUserTranscript: isFinal ? null : transcript,\n })\n // Add user message when transcript is finalized\n if (isFinal && transcript) {\n this.addMessage({\n id: this.generateId(),\n role: 'user',\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript, durationMs: 0 }],\n })\n }\n } else {\n // Assistant transcripts - just update pending, message_complete handles final\n this.updateState({\n pendingAssistantTranscript: isFinal ? null : transcript,\n })\n }\n }),\n )\n\n // Tool calls\n this.unsubscribers.push(\n this.connection.on('tool_call', ({ toolCallId, toolName, input }) => {\n if (!toolCallId) {\n console.error(\n '[RealtimeClient] tool_call missing toolCallId',\n toolName,\n )\n return\n }\n const tool = this.clientTools.get(toolName)\n const execute = tool?.execute\n if (execute) {\n void (async () => {\n try {\n const output = await execute(input)\n this.connection?.sendToolResult(\n toolCallId,\n typeof output === 'string' ? output : JSON.stringify(output),\n )\n } catch (error) {\n const errMsg =\n error instanceof Error ? error.message : String(error)\n this.connection?.sendToolResult(\n toolCallId,\n JSON.stringify({ error: errMsg }),\n )\n }\n })()\n } else {\n this.connection?.sendToolResult(\n toolCallId,\n JSON.stringify({\n error: `Unknown or non-executable tool: ${String(toolName)}`,\n }),\n )\n }\n }),\n )\n\n // Message complete\n this.unsubscribers.push(\n this.connection.on('message_complete', ({ message }) => {\n // Replace pending message with final version if needed\n const existingIndex = this.state.messages.findIndex(\n (m) => m.id === message.id,\n )\n if (existingIndex >= 0) {\n const newMessages = [...this.state.messages]\n newMessages[existingIndex] = message\n this.updateState({ messages: newMessages })\n } else {\n this.addMessage(message)\n }\n }),\n )\n\n // Interruption\n this.unsubscribers.push(\n this.connection.on('interrupted', ({ messageId }) => {\n if (messageId) {\n const newMessages = this.state.messages.map((m) =>\n m.id === messageId ? { ...m, interrupted: true } : m,\n )\n this.updateState({ messages: newMessages })\n }\n this.updateState({\n mode: 'listening',\n pendingAssistantTranscript: null,\n })\n this.options.onInterrupted?.()\n }),\n )\n\n // Errors\n this.unsubscribers.push(\n this.connection.on('error', ({ error }) => {\n this.updateState({ error })\n this.options.onError?.(error)\n }),\n )\n\n this.unsubscribers.push(\n this.connection.on('go_away', ({ timeLeft }) => {\n this.options.onGoAway?.(timeLeft)\n }),\n )\n\n this.unsubscribers.push(\n this.connection.on('usage', (usage) => {\n this.options.onUsage?.(usage)\n }),\n )\n }\n\n private applySessionConfig(): void {\n if (!this.connection) return\n\n const {\n instructions,\n voice,\n vadMode,\n tools,\n outputModalities,\n temperature,\n maxOutputTokens,\n semanticEagerness,\n } = this.options\n const hasConfig =\n instructions ||\n voice ||\n vadMode ||\n (tools && tools.length > 0) ||\n outputModalities ||\n temperature !== undefined ||\n maxOutputTokens !== undefined ||\n semanticEagerness\n if (!hasConfig) return\n\n // `RealtimeToolConfig.inputSchema`/`outputSchema` are `Record<string, any>`\n // (no `undefined` under `exactOptionalPropertyTypes`). Conditionally spread\n // each so we don't pass `undefined` when the tool has no such schema.\n const toolsConfig = tools\n ? Array.from(this.clientTools.values()).map((t) => {\n const inputSchema = t.inputSchema\n ? convertSchemaToJsonSchema(t.inputSchema)\n : undefined\n const outputSchema = t.outputSchema\n ? convertSchemaToJsonSchema(t.outputSchema)\n : undefined\n return {\n name: t.name,\n description: t.description,\n ...(inputSchema ? { inputSchema } : {}),\n ...(outputSchema ? { outputSchema } : {}),\n }\n })\n : undefined\n\n // `RealtimeSessionConfig` declares each field as `field?: T` (no\n // `undefined`). Spread each one conditionally so we don't violate\n // `exactOptionalPropertyTypes` when the source is `T | undefined`.\n this.connection.updateSession({\n ...(instructions !== undefined ? { instructions } : {}),\n ...(voice !== undefined ? { voice } : {}),\n ...(vadMode !== undefined ? { vadMode } : {}),\n ...(toolsConfig !== undefined ? { tools: toolsConfig } : {}),\n ...(outputModalities !== undefined ? { outputModalities } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(semanticEagerness !== undefined ? { semanticEagerness } : {}),\n })\n }\n\n private generateId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n}\n"],"mappings":"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"}
@@ -1,32 +1,25 @@
1
- class UnsupportedResponseStreamError extends Error {
2
- missingFeature;
3
- constructor(missingFeature) {
4
- super(createUnsupportedResponseStreamMessage(missingFeature));
5
- this.name = "UnsupportedResponseStreamError";
6
- this.missingFeature = missingFeature;
7
- }
8
- }
1
+ //#region src/response-stream.ts
2
+ var UnsupportedResponseStreamError = class extends Error {
3
+ missingFeature;
4
+ constructor(missingFeature) {
5
+ super(createUnsupportedResponseStreamMessage(missingFeature));
6
+ this.name = "UnsupportedResponseStreamError";
7
+ this.missingFeature = missingFeature;
8
+ }
9
+ };
9
10
  function createUnsupportedResponseStreamMessage(missingFeature) {
10
- return `Streaming fetch responses are not supported in this runtime because ${missingFeature} is unavailable. React Native users need a compatible fetch/stream/TextDecoder polyfill, or should use a fetch, XHR, or custom transport that can deliver streaming chunks.`;
11
+ return `Streaming fetch responses are not supported in this runtime because ${missingFeature} is unavailable. React Native users need a compatible fetch/stream/TextDecoder polyfill, or should use a fetch, XHR, or custom transport that can deliver streaming chunks.`;
11
12
  }
12
13
  function getResponseStreamReader(response) {
13
- if (!response.body) {
14
- throw new UnsupportedResponseStreamError("Response.body");
15
- }
16
- if (typeof response.body.getReader !== "function") {
17
- throw new UnsupportedResponseStreamError("Response.body.getReader");
18
- }
19
- return response.body.getReader();
14
+ if (!response.body) throw new UnsupportedResponseStreamError("Response.body");
15
+ if (typeof response.body.getReader !== "function") throw new UnsupportedResponseStreamError("Response.body.getReader");
16
+ return response.body.getReader();
20
17
  }
21
18
  function createResponseStreamTextDecoder() {
22
- if (typeof globalThis.TextDecoder !== "function") {
23
- throw new UnsupportedResponseStreamError("TextDecoder");
24
- }
25
- return new globalThis.TextDecoder();
19
+ if (typeof globalThis.TextDecoder !== "function") throw new UnsupportedResponseStreamError("TextDecoder");
20
+ return new globalThis.TextDecoder();
26
21
  }
27
- export {
28
- UnsupportedResponseStreamError,
29
- createResponseStreamTextDecoder,
30
- getResponseStreamReader
31
- };
32
- //# sourceMappingURL=response-stream.js.map
22
+ //#endregion
23
+ export { UnsupportedResponseStreamError, createResponseStreamTextDecoder, getResponseStreamReader };
24
+
25
+ //# sourceMappingURL=response-stream.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"response-stream.js","sources":["../../src/response-stream.ts"],"sourcesContent":["export type MissingResponseStreamFeature =\n | 'TextDecoder'\n | 'Response.body'\n | 'Response.body.getReader'\n\nexport class UnsupportedResponseStreamError extends Error {\n readonly missingFeature: MissingResponseStreamFeature\n\n constructor(missingFeature: MissingResponseStreamFeature) {\n super(createUnsupportedResponseStreamMessage(missingFeature))\n this.name = 'UnsupportedResponseStreamError'\n this.missingFeature = missingFeature\n }\n}\n\nfunction createUnsupportedResponseStreamMessage(\n missingFeature: MissingResponseStreamFeature,\n): string {\n return `Streaming fetch responses are not supported in this runtime because ${missingFeature} is unavailable. React Native users need a compatible fetch/stream/TextDecoder polyfill, or should use a fetch, XHR, or custom transport that can deliver streaming chunks.`\n}\n\nexport function getResponseStreamReader(\n response: Response,\n): ReadableStreamDefaultReader<Uint8Array> {\n if (!response.body) {\n throw new UnsupportedResponseStreamError('Response.body')\n }\n\n if (typeof response.body.getReader !== 'function') {\n throw new UnsupportedResponseStreamError('Response.body.getReader')\n }\n\n return response.body.getReader()\n}\n\nexport function createResponseStreamTextDecoder(): TextDecoder {\n if (typeof globalThis.TextDecoder !== 'function') {\n throw new UnsupportedResponseStreamError('TextDecoder')\n }\n\n return new globalThis.TextDecoder()\n}\n"],"names":[],"mappings":"AAKO,MAAM,uCAAuC,MAAM;AAAA,EAC/C;AAAA,EAET,YAAY,gBAA8C;AACxD,UAAM,uCAAuC,cAAc,CAAC;AAC5D,SAAK,OAAO;AACZ,SAAK,iBAAiB;AAAA,EACxB;AACF;AAEA,SAAS,uCACP,gBACQ;AACR,SAAO,uEAAuE,cAAc;AAC9F;AAEO,SAAS,wBACd,UACyC;AACzC,MAAI,CAAC,SAAS,MAAM;AAClB,UAAM,IAAI,+BAA+B,eAAe;AAAA,EAC1D;AAEA,MAAI,OAAO,SAAS,KAAK,cAAc,YAAY;AACjD,UAAM,IAAI,+BAA+B,yBAAyB;AAAA,EACpE;AAEA,SAAO,SAAS,KAAK,UAAA;AACvB;AAEO,SAAS,kCAA+C;AAC7D,MAAI,OAAO,WAAW,gBAAgB,YAAY;AAChD,UAAM,IAAI,+BAA+B,aAAa;AAAA,EACxD;AAEA,SAAO,IAAI,WAAW,YAAA;AACxB;"}
1
+ {"version":3,"file":"response-stream.js","names":[],"sources":["../../src/response-stream.ts"],"sourcesContent":["export type MissingResponseStreamFeature =\n | 'TextDecoder'\n | 'Response.body'\n | 'Response.body.getReader'\n\nexport class UnsupportedResponseStreamError extends Error {\n readonly missingFeature: MissingResponseStreamFeature\n\n constructor(missingFeature: MissingResponseStreamFeature) {\n super(createUnsupportedResponseStreamMessage(missingFeature))\n this.name = 'UnsupportedResponseStreamError'\n this.missingFeature = missingFeature\n }\n}\n\nfunction createUnsupportedResponseStreamMessage(\n missingFeature: MissingResponseStreamFeature,\n): string {\n return `Streaming fetch responses are not supported in this runtime because ${missingFeature} is unavailable. React Native users need a compatible fetch/stream/TextDecoder polyfill, or should use a fetch, XHR, or custom transport that can deliver streaming chunks.`\n}\n\nexport function getResponseStreamReader(\n response: Response,\n): ReadableStreamDefaultReader<Uint8Array> {\n if (!response.body) {\n throw new UnsupportedResponseStreamError('Response.body')\n }\n\n if (typeof response.body.getReader !== 'function') {\n throw new UnsupportedResponseStreamError('Response.body.getReader')\n }\n\n return response.body.getReader()\n}\n\nexport function createResponseStreamTextDecoder(): TextDecoder {\n if (typeof globalThis.TextDecoder !== 'function') {\n throw new UnsupportedResponseStreamError('TextDecoder')\n }\n\n return new globalThis.TextDecoder()\n}\n"],"mappings":";AAKA,IAAa,iCAAb,cAAoD,MAAM;CACxD;CAEA,YAAY,gBAA8C;EACxD,MAAM,uCAAuC,cAAc,CAAC;EAC5D,KAAK,OAAO;EACZ,KAAK,iBAAiB;CACxB;AACF;AAEA,SAAS,uCACP,gBACQ;CACR,OAAO,uEAAuE,eAAe;AAC/F;AAEA,SAAgB,wBACd,UACyC;CACzC,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,+BAA+B,eAAe;CAG1D,IAAI,OAAO,SAAS,KAAK,cAAc,YACrC,MAAM,IAAI,+BAA+B,yBAAyB;CAGpE,OAAO,SAAS,KAAK,UAAU;AACjC;AAEA,SAAgB,kCAA+C;CAC7D,IAAI,OAAO,WAAW,gBAAgB,YACpC,MAAM,IAAI,+BAA+B,aAAa;CAGxD,OAAO,IAAI,WAAW,YAAY;AACpC"}
@@ -1,52 +1,49 @@
1
- import { getResponseStreamReader, createResponseStreamTextDecoder } from "./response-stream.js";
1
+ import { createResponseStreamTextDecoder, getResponseStreamReader } from "./response-stream.js";
2
2
  import { parseSseDataLine } from "./sse-utils.js";
3
+ //#region src/sse-parser.ts
4
+ /**
5
+ * Read lines from a stream (newline-delimited)
6
+ */
3
7
  async function* readStreamLines(reader, abortSignal) {
4
- try {
5
- const decoder = createResponseStreamTextDecoder();
6
- let buffer = "";
7
- while (!abortSignal?.aborted) {
8
- const { done, value } = await reader.read();
9
- if (done) break;
10
- buffer += decoder.decode(value, { stream: true });
11
- const lines = buffer.split("\n");
12
- buffer = lines.pop() || "";
13
- for (const line of lines) {
14
- if (line.trim()) {
15
- yield line;
16
- }
17
- }
18
- }
19
- if (buffer.trim()) {
20
- yield buffer;
21
- }
22
- } finally {
23
- reader.releaseLock();
24
- }
8
+ try {
9
+ const decoder = createResponseStreamTextDecoder();
10
+ let buffer = "";
11
+ while (!abortSignal?.aborted) {
12
+ const { done, value } = await reader.read();
13
+ if (done) break;
14
+ buffer += decoder.decode(value, { stream: true });
15
+ const lines = buffer.split("\n");
16
+ buffer = lines.pop() || "";
17
+ for (const line of lines) if (line.trim()) yield line;
18
+ }
19
+ if (buffer.trim()) yield buffer;
20
+ } finally {
21
+ reader.releaseLock();
22
+ }
25
23
  }
24
+ /**
25
+ * Parse a Response body as Server-Sent Events, yielding StreamChunks.
26
+ *
27
+ * Used by GenerationClient to parse SSE Responses returned from fetchers
28
+ * (e.g., TanStack Start server functions using `toServerSentEventsResponse()`).
29
+ */
26
30
  async function* parseSSEResponse(response, abortSignal) {
27
- if (!response.ok) {
28
- throw new Error(
29
- `HTTP error! status: ${response.status} ${response.statusText}`
30
- );
31
- }
32
- const reader = getResponseStreamReader(response);
33
- for await (const line of readStreamLines(reader, abortSignal)) {
34
- const data = parseSseDataLine(line);
35
- if (data === "[DONE]") {
36
- console.warn(
37
- "[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator."
38
- );
39
- continue;
40
- }
41
- try {
42
- const parsed = JSON.parse(data);
43
- yield parsed;
44
- } catch (parseError) {
45
- console.warn("Failed to parse SSE chunk:", data);
46
- }
47
- }
31
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
32
+ const reader = getResponseStreamReader(response);
33
+ for await (const line of readStreamLines(reader, abortSignal)) {
34
+ const data = parseSseDataLine(line);
35
+ if (data === "[DONE]") {
36
+ console.warn("[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.");
37
+ continue;
38
+ }
39
+ try {
40
+ yield JSON.parse(data);
41
+ } catch (parseError) {
42
+ console.warn("Failed to parse SSE chunk:", data);
43
+ }
44
+ }
48
45
  }
49
- export {
50
- parseSSEResponse
51
- };
52
- //# sourceMappingURL=sse-parser.js.map
46
+ //#endregion
47
+ export { parseSSEResponse };
48
+
49
+ //# sourceMappingURL=sse-parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse-parser.js","sources":["../../src/sse-parser.ts"],"sourcesContent":["import {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type { StreamChunk } from '@tanstack/ai/client'\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = createResponseStreamTextDecoder()\n let buffer = ''\n\n while (!abortSignal?.aborted) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Parse a Response body as Server-Sent Events, yielding StreamChunks.\n *\n * Used by GenerationClient to parse SSE Responses returned from fetchers\n * (e.g., TanStack Start server functions using `toServerSentEventsResponse()`).\n */\nexport async function* parseSSEResponse(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n const reader = getResponseStreamReader(response)\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n const data = parseSseDataLine(line)\n\n if (data === '[DONE]') {\n console.warn(\n '[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.',\n )\n continue\n }\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n}\n"],"names":[],"mappings":";;AAUA,gBAAgB,gBACd,QACA,aACwB;AACxB,MAAI;AACF,UAAM,UAAU,gCAAA;AAChB,QAAI,SAAS;AAEb,WAAO,CAAC,aAAa,SAAS;AAC5B,YAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,UAAI,KAAM;AAEV,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM;AAChD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAE/B,eAAS,MAAM,SAAS;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,QAAQ;AACf,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ;AACjB,YAAM;AAAA,IACR;AAAA,EACF,UAAA;AACE,WAAO,YAAA;AAAA,EACT;AACF;AAQA,gBAAuB,iBACrB,UACA,aAC6B;AAC7B,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAAA;AAAA,EAEjE;AAEA,QAAM,SAAS,wBAAwB,QAAQ;AAE/C,mBAAiB,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;AAC7D,UAAM,OAAO,iBAAiB,IAAI;AAElC,QAAI,SAAS,UAAU;AACrB,cAAQ;AAAA,QACN;AAAA,MAAA;AAEF;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAsB,KAAK,MAAM,IAAI;AAC3C,YAAM;AAAA,IACR,SAAS,YAAY;AACnB,cAAQ,KAAK,8BAA8B,IAAI;AAAA,IACjD;AAAA,EACF;AACF;"}
1
+ {"version":3,"file":"sse-parser.js","names":[],"sources":["../../src/sse-parser.ts"],"sourcesContent":["import {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type { StreamChunk } from '@tanstack/ai/client'\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = createResponseStreamTextDecoder()\n let buffer = ''\n\n while (!abortSignal?.aborted) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Parse a Response body as Server-Sent Events, yielding StreamChunks.\n *\n * Used by GenerationClient to parse SSE Responses returned from fetchers\n * (e.g., TanStack Start server functions using `toServerSentEventsResponse()`).\n */\nexport async function* parseSSEResponse(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n const reader = getResponseStreamReader(response)\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n const data = parseSseDataLine(line)\n\n if (data === '[DONE]') {\n console.warn(\n '[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.',\n )\n continue\n }\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n}\n"],"mappings":";;;;;;AAUA,gBAAgB,gBACd,QACA,aACwB;CACxB,IAAI;EACF,MAAM,UAAU,gCAAgC;EAChD,IAAI,SAAS;EAEb,OAAO,CAAC,aAAa,SAAS;GAC5B,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;GAEV,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;GAChD,MAAM,QAAQ,OAAO,MAAM,IAAI;GAE/B,SAAS,MAAM,IAAI,KAAK;GAExB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,KAAK,GACZ,MAAM;EAGZ;EAEA,IAAI,OAAO,KAAK,GACd,MAAM;CAEV,UAAU;EACR,OAAO,YAAY;CACrB;AACF;;;;;;;AAQA,gBAAuB,iBACrB,UACA,aAC6B;CAC7B,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,uBAAuB,SAAS,OAAO,GAAG,SAAS,YACrD;CAGF,MAAM,SAAS,wBAAwB,QAAQ;CAE/C,WAAW,MAAM,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;EAC7D,MAAM,OAAO,iBAAiB,IAAI;EAElC,IAAI,SAAS,UAAU;GACrB,QAAQ,KACN,uJACF;GACA;EACF;EAEA,IAAI;GAEF,MAD4B,KAAK,MAAM,IACjC;EACR,SAAS,YAAY;GACnB,QAAQ,KAAK,8BAA8B,IAAI;EACjD;CACF;AACF"}
@@ -1,11 +1,10 @@
1
+ //#region src/sse-utils.ts
1
2
  function parseSseDataLine(line) {
2
- if (!line.startsWith("data:")) {
3
- return line;
4
- }
5
- const data = line.slice(5);
6
- return data.startsWith(" ") ? data.slice(1) : data;
3
+ if (!line.startsWith("data:")) return line;
4
+ const data = line.slice(5);
5
+ return data.startsWith(" ") ? data.slice(1) : data;
7
6
  }
8
- export {
9
- parseSseDataLine
10
- };
11
- //# sourceMappingURL=sse-utils.js.map
7
+ //#endregion
8
+ export { parseSseDataLine };
9
+
10
+ //# sourceMappingURL=sse-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse-utils.js","sources":["../../src/sse-utils.ts"],"sourcesContent":["export function parseSseDataLine(line: string): string {\n if (!line.startsWith('data:')) {\n return line\n }\n\n const data = line.slice(5)\n return data.startsWith(' ') ? data.slice(1) : data\n}\n"],"names":[],"mappings":"AAAO,SAAS,iBAAiB,MAAsB;AACrD,MAAI,CAAC,KAAK,WAAW,OAAO,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,SAAO,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AAChD;"}
1
+ {"version":3,"file":"sse-utils.js","names":[],"sources":["../../src/sse-utils.ts"],"sourcesContent":["export function parseSseDataLine(line: string): string {\n if (!line.startsWith('data:')) {\n return line\n }\n\n const data = line.slice(5)\n return data.startsWith(' ') ? data.slice(1) : data\n}\n"],"mappings":";AAAA,SAAgB,iBAAiB,MAAsB;CACrD,IAAI,CAAC,KAAK,WAAW,OAAO,GAC1B,OAAO;CAGT,MAAM,OAAO,KAAK,MAAM,CAAC;CACzB,OAAO,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AAChD"}
@@ -0,0 +1,62 @@
1
+ import { ChatPersistedState, ChatStorageAdapter } from './types.js';
2
+ export interface WebStoragePersistenceOptions {
3
+ keyPrefix?: string;
4
+ /**
5
+ * Defaults to `JSON.stringify`. Override only for values JSON can't
6
+ * round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a
7
+ * `Date` rather than an ISO string).
8
+ */
9
+ serialize?: (value: ChatPersistedState) => string;
10
+ /** Defaults to `JSON.parse`. */
11
+ deserialize?: (value: string) => ChatPersistedState;
12
+ }
13
+ export interface IndexedDBPersistenceOptions {
14
+ databaseName?: string;
15
+ objectStoreName?: string;
16
+ keyPrefix?: string;
17
+ }
18
+ type StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB';
19
+ /**
20
+ * Thrown by a storage adapter when its backing store is absent — most commonly
21
+ * during server-side rendering, where `localStorage` / `sessionStorage` /
22
+ * `indexedDB` do not exist on `globalThis`. The adapters check availability
23
+ * lazily, **per operation**, so constructing an adapter never throws; the error
24
+ * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for
25
+ * IndexedDB). The chat persistence layer treats adapter failures as best-effort
26
+ * (storage errors never break chat setup or streaming).
27
+ */
28
+ export declare class StorageUnavailableError extends Error {
29
+ constructor(storageName: StorageName);
30
+ }
31
+ /**
32
+ * A `ChatStorageAdapter` backed by `window.localStorage` (persists across
33
+ * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which
34
+ * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and
35
+ * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the
36
+ * adapter can be constructed safely on the server.
37
+ *
38
+ * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /
39
+ * `JSON.parse`, so the common case needs no codec.
40
+ */
41
+ export declare function localStoragePersistence(options?: WebStoragePersistenceOptions): ChatStorageAdapter<ChatPersistedState>;
42
+ /**
43
+ * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab
44
+ * and cleared when it closes). Identical to {@link localStoragePersistence} in
45
+ * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy
46
+ * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that
47
+ * defaults to `JSON.stringify` / `JSON.parse`.
48
+ */
49
+ export declare function sessionStoragePersistence(options?: WebStoragePersistenceOptions): ChatStorageAdapter<ChatPersistedState>;
50
+ /**
51
+ * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web
52
+ * Storage or that benefit from structured-clone storage. All operations are
53
+ * async and the database opens lazily on first use; keys are namespaced with
54
+ * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.
55
+ * SSR) each operation rejects with {@link StorageUnavailableError}.
56
+ *
57
+ * No serialize/deserialize codec is needed or accepted — values are stored via
58
+ * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.
59
+ * round-trip without a JSON step.
60
+ */
61
+ export declare function indexedDBPersistence(options?: IndexedDBPersistenceOptions): ChatStorageAdapter<ChatPersistedState>;
62
+ export {};
@@ -0,0 +1,174 @@
1
+ //#region src/storage-adapters.ts
2
+ /**
3
+ * Thrown by a storage adapter when its backing store is absent — most commonly
4
+ * during server-side rendering, where `localStorage` / `sessionStorage` /
5
+ * `indexedDB` do not exist on `globalThis`. The adapters check availability
6
+ * lazily, **per operation**, so constructing an adapter never throws; the error
7
+ * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for
8
+ * IndexedDB). The chat persistence layer treats adapter failures as best-effort
9
+ * (storage errors never break chat setup or streaming).
10
+ */
11
+ var StorageUnavailableError = class extends Error {
12
+ constructor(storageName) {
13
+ super(`${storageName} is not available in this environment.`);
14
+ this.name = "StorageUnavailableError";
15
+ }
16
+ };
17
+ function stringifyJson(value) {
18
+ const stringify = JSON.stringify;
19
+ const serialized = stringify(value);
20
+ if (typeof serialized !== "string") throw new TypeError("The value is not JSON serializable.");
21
+ return serialized;
22
+ }
23
+ function createWebStoragePersistence(storageName, options) {
24
+ const keyPrefix = options.keyPrefix ?? "tanstack-ai:";
25
+ const serialize = options.serialize ?? stringifyJson;
26
+ const deserialize = options.deserialize ?? JSON.parse;
27
+ const key = (id) => `${keyPrefix}${id}`;
28
+ const getStorage = () => {
29
+ const storage = globalThis[storageName];
30
+ if (!storage) throw new StorageUnavailableError(storageName);
31
+ return storage;
32
+ };
33
+ return {
34
+ getItem(id) {
35
+ const item = getStorage().getItem(key(id));
36
+ return item === null ? null : deserialize(item);
37
+ },
38
+ setItem(id, value) {
39
+ getStorage().setItem(key(id), serialize(value));
40
+ },
41
+ removeItem(id) {
42
+ getStorage().removeItem(key(id));
43
+ }
44
+ };
45
+ }
46
+ /**
47
+ * A `ChatStorageAdapter` backed by `window.localStorage` (persists across
48
+ * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which
49
+ * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and
50
+ * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the
51
+ * adapter can be constructed safely on the server.
52
+ *
53
+ * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /
54
+ * `JSON.parse`, so the common case needs no codec.
55
+ */
56
+ function localStoragePersistence(options = {}) {
57
+ return createWebStoragePersistence("localStorage", options);
58
+ }
59
+ /**
60
+ * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab
61
+ * and cleared when it closes). Identical to {@link localStoragePersistence} in
62
+ * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy
63
+ * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that
64
+ * defaults to `JSON.stringify` / `JSON.parse`.
65
+ */
66
+ function sessionStoragePersistence(options = {}) {
67
+ return createWebStoragePersistence("sessionStorage", options);
68
+ }
69
+ /**
70
+ * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web
71
+ * Storage or that benefit from structured-clone storage. All operations are
72
+ * async and the database opens lazily on first use; keys are namespaced with
73
+ * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.
74
+ * SSR) each operation rejects with {@link StorageUnavailableError}.
75
+ *
76
+ * No serialize/deserialize codec is needed or accepted — values are stored via
77
+ * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.
78
+ * round-trip without a JSON step.
79
+ */
80
+ function indexedDBPersistence(options = {}) {
81
+ const databaseName = options.databaseName ?? "tanstack-ai";
82
+ const objectStoreName = options.objectStoreName ?? "persistence";
83
+ const keyPrefix = options.keyPrefix ?? "tanstack-ai:";
84
+ let databasePromise;
85
+ const openDatabase = () => {
86
+ if (databasePromise) return databasePromise;
87
+ databasePromise = new Promise((resolve, reject) => {
88
+ const factory = globalThis.indexedDB;
89
+ if (!factory) {
90
+ reject(new StorageUnavailableError("indexedDB"));
91
+ return;
92
+ }
93
+ let request;
94
+ let openFailed = false;
95
+ try {
96
+ request = factory.open(databaseName);
97
+ } catch (error) {
98
+ reject(error);
99
+ return;
100
+ }
101
+ request.onupgradeneeded = () => {
102
+ if (!request.result.objectStoreNames.contains(objectStoreName)) request.result.createObjectStore(objectStoreName);
103
+ };
104
+ request.onerror = () => {
105
+ openFailed = true;
106
+ reject(request.error ?? /* @__PURE__ */ new Error(`Failed to open ${databaseName}.`));
107
+ };
108
+ request.onblocked = () => {
109
+ openFailed = true;
110
+ reject(/* @__PURE__ */ new Error(`Opening IndexedDB database "${databaseName}" was blocked.`));
111
+ };
112
+ request.onsuccess = () => {
113
+ const database = request.result;
114
+ if (openFailed) {
115
+ database.close();
116
+ return;
117
+ }
118
+ database.onversionchange = () => {
119
+ database.close();
120
+ databasePromise = void 0;
121
+ };
122
+ resolve(database);
123
+ };
124
+ }).catch((error) => {
125
+ databasePromise = void 0;
126
+ throw error;
127
+ });
128
+ return databasePromise;
129
+ };
130
+ const runRequest = async (mode, createRequest) => {
131
+ const database = await openDatabase();
132
+ return new Promise((resolve, reject) => {
133
+ let request;
134
+ let result;
135
+ try {
136
+ const transaction = database.transaction(objectStoreName, mode);
137
+ request = createRequest(transaction.objectStore(objectStoreName));
138
+ request.onsuccess = () => {
139
+ result = request.result;
140
+ };
141
+ request.onerror = () => {
142
+ reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed."));
143
+ };
144
+ transaction.oncomplete = () => {
145
+ resolve(result);
146
+ };
147
+ transaction.onerror = () => {
148
+ reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
149
+ };
150
+ transaction.onabort = () => {
151
+ reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted."));
152
+ };
153
+ } catch (error) {
154
+ reject(error);
155
+ }
156
+ });
157
+ };
158
+ const key = (id) => `${keyPrefix}${id}`;
159
+ return {
160
+ getItem(id) {
161
+ return runRequest("readonly", (store) => store.get(key(id)));
162
+ },
163
+ setItem(id, value) {
164
+ return runRequest("readwrite", (store) => store.put(value, key(id))).then(() => void 0);
165
+ },
166
+ removeItem(id) {
167
+ return runRequest("readwrite", (store) => store.delete(key(id))).then(() => void 0);
168
+ }
169
+ };
170
+ }
171
+ //#endregion
172
+ export { StorageUnavailableError, indexedDBPersistence, localStoragePersistence, sessionStoragePersistence };
173
+
174
+ //# sourceMappingURL=storage-adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-adapters.js","names":[],"sources":["../../src/storage-adapters.ts"],"sourcesContent":["import type { ChatPersistedState, ChatStorageAdapter } from './types'\n\nexport interface WebStoragePersistenceOptions {\n keyPrefix?: string\n /**\n * Defaults to `JSON.stringify`. Override only for values JSON can't\n * round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a\n * `Date` rather than an ISO string).\n */\n serialize?: (value: ChatPersistedState) => string\n /** Defaults to `JSON.parse`. */\n deserialize?: (value: string) => ChatPersistedState\n}\n\nexport interface IndexedDBPersistenceOptions {\n databaseName?: string\n objectStoreName?: string\n keyPrefix?: string\n}\n\ntype StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB'\n\n/**\n * Thrown by a storage adapter when its backing store is absent — most commonly\n * during server-side rendering, where `localStorage` / `sessionStorage` /\n * `indexedDB` do not exist on `globalThis`. The adapters check availability\n * lazily, **per operation**, so constructing an adapter never throws; the error\n * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for\n * IndexedDB). The chat persistence layer treats adapter failures as best-effort\n * (storage errors never break chat setup or streaming).\n */\nexport class StorageUnavailableError extends Error {\n constructor(storageName: StorageName) {\n super(`${storageName} is not available in this environment.`)\n this.name = 'StorageUnavailableError'\n }\n}\n\nfunction stringifyJson(value: ChatPersistedState): string {\n const stringify: (input: unknown) => unknown = JSON.stringify\n const serialized = stringify(value)\n if (typeof serialized !== 'string') {\n throw new TypeError('The value is not JSON serializable.')\n }\n return serialized\n}\n\nfunction createWebStoragePersistence(\n storageName: 'localStorage' | 'sessionStorage',\n options: WebStoragePersistenceOptions,\n): ChatStorageAdapter<ChatPersistedState> {\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n const serialize = options.serialize ?? stringifyJson\n const deserialize = options.deserialize ?? JSON.parse\n const key = (id: string) => `${keyPrefix}${id}`\n\n const getStorage = (): Storage => {\n const browserGlobals: {\n localStorage?: Storage\n sessionStorage?: Storage\n } = globalThis\n const storage = browserGlobals[storageName]\n if (!storage) {\n throw new StorageUnavailableError(storageName)\n }\n return storage\n }\n\n return {\n getItem(id) {\n const item = getStorage().getItem(key(id))\n return item === null ? null : deserialize(item)\n },\n setItem(id, value) {\n getStorage().setItem(key(id), serialize(value))\n },\n removeItem(id) {\n getStorage().removeItem(key(id))\n },\n }\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.localStorage` (persists across\n * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which\n * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and\n * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the\n * adapter can be constructed safely on the server.\n *\n * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /\n * `JSON.parse`, so the common case needs no codec.\n */\nexport function localStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('localStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab\n * and cleared when it closes). Identical to {@link localStoragePersistence} in\n * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy\n * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that\n * defaults to `JSON.stringify` / `JSON.parse`.\n */\nexport function sessionStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('sessionStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web\n * Storage or that benefit from structured-clone storage. All operations are\n * async and the database opens lazily on first use; keys are namespaced with\n * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.\n * SSR) each operation rejects with {@link StorageUnavailableError}.\n *\n * No serialize/deserialize codec is needed or accepted — values are stored via\n * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.\n * round-trip without a JSON step.\n */\nexport function indexedDBPersistence(\n options: IndexedDBPersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n const databaseName = options.databaseName ?? 'tanstack-ai'\n const objectStoreName = options.objectStoreName ?? 'persistence'\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n let databasePromise: Promise<IDBDatabase> | undefined\n\n const openDatabase = (): Promise<IDBDatabase> => {\n if (databasePromise) {\n return databasePromise\n }\n\n databasePromise = new Promise<IDBDatabase>((resolve, reject) => {\n const browserGlobals: { indexedDB?: IDBFactory } = globalThis\n const factory = browserGlobals.indexedDB\n if (!factory) {\n reject(new StorageUnavailableError('indexedDB'))\n return\n }\n\n let request: IDBOpenDBRequest\n let openFailed = false\n try {\n request = factory.open(databaseName)\n } catch (error) {\n reject(error)\n return\n }\n\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(objectStoreName)) {\n request.result.createObjectStore(objectStoreName)\n }\n }\n request.onerror = () => {\n openFailed = true\n reject(request.error ?? new Error(`Failed to open ${databaseName}.`))\n }\n request.onblocked = () => {\n openFailed = true\n reject(\n new Error(\n `Opening IndexedDB database \"${databaseName}\" was blocked.`,\n ),\n )\n }\n request.onsuccess = () => {\n const database = request.result\n if (openFailed) {\n database.close()\n return\n }\n database.onversionchange = () => {\n database.close()\n databasePromise = undefined\n }\n resolve(database)\n }\n }).catch((error: unknown) => {\n databasePromise = undefined\n throw error\n })\n\n return databasePromise\n }\n\n const runRequest = async <TResult>(\n mode: IDBTransactionMode,\n createRequest: (store: IDBObjectStore) => IDBRequest<TResult>,\n ): Promise<TResult> => {\n const database = await openDatabase()\n return new Promise<TResult>((resolve, reject) => {\n let request: IDBRequest<TResult>\n let result: TResult\n try {\n const transaction = database.transaction(objectStoreName, mode)\n request = createRequest(transaction.objectStore(objectStoreName))\n request.onsuccess = () => {\n result = request.result\n }\n request.onerror = () => {\n reject(request.error ?? new Error('IndexedDB request failed.'))\n }\n transaction.oncomplete = () => {\n resolve(result)\n }\n transaction.onerror = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction failed.'),\n )\n }\n transaction.onabort = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction aborted.'),\n )\n }\n } catch (error) {\n reject(error)\n }\n })\n }\n\n const key = (id: string) => `${keyPrefix}${id}`\n return {\n getItem(id) {\n return runRequest('readonly', (store) => store.get(key(id)))\n },\n setItem(id, value) {\n return runRequest('readwrite', (store) => store.put(value, key(id))).then(\n () => undefined,\n )\n },\n removeItem(id) {\n return runRequest('readwrite', (store) => store.delete(key(id))).then(\n () => undefined,\n )\n },\n }\n}\n"],"mappings":"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"}