@tanstack/ai-client 0.26.0 → 0.29.0
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.
- package/dist/esm/byok/client.d.ts +52 -0
- package/dist/esm/byok/client.js +300 -0
- package/dist/esm/byok/client.js.map +1 -0
- package/dist/esm/byok/passkey.d.ts +43 -0
- package/dist/esm/byok/passkey.js +251 -0
- package/dist/esm/byok/passkey.js.map +1 -0
- package/dist/esm/byok/resolve.d.ts +8 -0
- package/dist/esm/byok/resolve.js +20 -0
- package/dist/esm/byok/resolve.js.map +1 -0
- package/dist/esm/byok/storage.d.ts +15 -0
- package/dist/esm/byok/storage.js +16 -0
- package/dist/esm/byok/storage.js.map +1 -0
- package/dist/esm/byok.d.ts +8 -0
- package/dist/esm/byok.js +5 -0
- package/dist/esm/chat-client.d.ts +19 -3
- package/dist/esm/chat-client.js +82 -42
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +35 -10
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +23 -4
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/interrupt-manager.d.ts +1 -0
- package/dist/esm/interrupt-manager.js +15 -6
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/message-date-normalizer.d.ts +3 -0
- package/dist/esm/message-date-normalizer.js +35 -0
- package/dist/esm/message-date-normalizer.js.map +1 -0
- package/dist/esm/sse-parser.js +8 -1
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/storage-adapters.js +13 -1
- package/dist/esm/storage-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +34 -3
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +3 -1
- package/dist/esm/video-generation-client.js +25 -6
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -3
- package/src/byok/client.ts +360 -0
- package/src/byok/passkey.ts +399 -0
- package/src/byok/resolve.ts +30 -0
- package/src/byok/storage.ts +29 -0
- package/src/byok.ts +20 -0
- package/src/chat-client.ts +165 -72
- package/src/connection-adapters.ts +46 -14
- package/src/generation-client.ts +48 -4
- package/src/generation-types.ts +16 -0
- package/src/interrupt-manager.ts +20 -6
- package/src/message-date-normalizer.ts +29 -0
- package/src/sse-parser.ts +10 -0
- package/src/storage-adapters.ts +13 -2
- package/src/types.ts +36 -3
- package/src/video-generation-client.ts +45 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-generation-client.js","names":[],"sources":["../../src/video-generation-client.ts"],"sourcesContent":["import {\n GENERATION_EVENTS,\n GENERATION_STREAM_TRUNCATED_MESSAGE,\n GENERATION_UNRESTORABLE_RESULT_MESSAGE,\n clientStateFromResumeStatus,\n createGenerationHydrationError,\n createGenerationResultSnapshot,\n parseGenerationResumeSnapshot,\n updateGenerationResumeSnapshot,\n} from './generation-types'\nimport { createNoOpVideoDevtoolsBridge } from './devtools-noop'\nimport { parseSSEResponse } from './sse-parser'\nimport { restoreInboundChunk } from '@tanstack/ai/client'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type {\n ConnectConnectionAdapter,\n GenerationHydrationResult,\n RunAgentInputContext,\n} from './connection-adapters'\nimport type {\n AIDevtoolsClientMetadata,\n AIDevtoolsGenerationProgress,\n VideoDevtoolsBridge,\n VideoDevtoolsBridgeOptions,\n} from './devtools'\nimport type {\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n GenerationResumeSnapshot,\n GenerationResumeState,\n GenerationTransport,\n VideoGenerateInput,\n VideoGenerateResult,\n VideoGenerationClientOptions,\n VideoStatusInfo,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\n// All optional fields explicitly allow `| undefined` so callers can spread\n// option bags (where each callback may be `undefined`) into the callbacks\n// ref under `exactOptionalPropertyTypes`.\ninterface VideoCallbacks<TOutput> {\n onResult?:\n | ((result: VideoGenerateResult) => TOutput | null | void)\n | undefined\n onError?: ((error: Error) => void) | undefined\n onProgress?: ((progress: number, message?: string) => void) | undefined\n onChunk?: ((chunk: StreamChunk) => void) | undefined\n onJobCreated?: ((jobId: string) => void) | undefined\n onStatusUpdate?: ((status: VideoStatusInfo) => void) | undefined\n onResultChange?: ((result: TOutput | null) => void) | undefined\n onLoadingChange?: ((isLoading: boolean) => void) | undefined\n onErrorChange?: ((error: Error | undefined) => void) | undefined\n onStatusChange?: ((status: GenerationClientState) => void) | undefined\n onJobIdChange?: ((jobId: string | null) => void) | undefined\n onVideoStatusChange?: ((status: VideoStatusInfo | null) => void) | undefined\n onResumeSnapshotChange?:\n | ((snapshot: GenerationResumeSnapshot | undefined) => void)\n | undefined\n onResumeStateChange?:\n | ((resumeState: GenerationResumeState | null) => void)\n | undefined\n}\n\n/**\n * A specialized client for job-based video generation.\n *\n * Video generation is asynchronous: a job is created, then polled for status\n * until completion. This client handles the full lifecycle.\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Server handles the polling loop internally and\n * streams status updates via CUSTOM events.\n * - **Fetcher** — Direct async function that returns a completed\n * `VideoGenerateResult`.\n *\n * @example\n * ```typescript\n * // With streaming connection adapter (server-driven polling)\n * const client = new VideoGenerationClient({\n * connection: fetchServerSentEvents('/api/generate/video'),\n * onResultChange: setResult,\n * onVideoStatusChange: setVideoStatus,\n * })\n *\n * // With fetcher (direct result)\n * const client = new VideoGenerationClient({\n * fetcher: async (input) => {\n * const res = await fetch('/api/video/generate', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json() // { jobId, status: 'completed', url, expiresAt }\n * },\n * })\n *\n * await client.generate({ prompt: 'A flying car over a city' })\n * ```\n */\nexport class VideoGenerationClient<TOutput = VideoGenerateResult> {\n private readonly connection: ConnectConnectionAdapter | undefined\n // Persistence handlers supplied as options (e.g. alongside a `fetcher`), used\n // when the connection doesn't carry its own — the connection's handlers take\n // precedence when both exist.\n private readonly hydrateGenerationHandler:\n | ConnectConnectionAdapter['hydrateGeneration']\n | undefined\n private readonly joinRunHandler:\n | ConnectConnectionAdapter['joinRun']\n | undefined\n private readonly fetcher:\n | GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n | undefined\n private uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: VideoDevtoolsBridge<TOutput>\n private threadId: string\n // Server-driven mode (`persistence: true`): no local snapshot store; on mount\n // the client hydrates the last generation for `threadId` from the server.\n private readonly serverDriven: boolean = false\n private body: Record<string, any>\n\n private result: TOutput | null = null\n private input: VideoGenerateInput | null = null\n private progress: AIDevtoolsGenerationProgress | null = null\n private jobId: string | null = null\n private videoStatus: VideoStatusInfo | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private resumeSnapshot: GenerationResumeSnapshot | undefined\n private abortController: AbortController | null = null\n private rejoinedRunId: string | undefined\n private readonly callbacksRef: VideoCallbacks<TOutput>\n private devtoolsMounted = false\n private disposed = false\n private serverHydrationStarted = false\n\n constructor(\n options: Omit<\n VideoGenerationClientOptions<TOutput>,\n 'persistence' | 'threadId'\n > &\n GenerationPersistenceOptions &\n GenerationTransport<VideoGenerateInput, VideoGenerateResult>,\n ) {\n // `threadId` is the only identity. Do not mint a random id during\n // construct: hooks build this client during render.\n this.threadId = options.threadId ?? ''\n this.uniqueId = this.threadId\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.hydrateGenerationHandler = options.hydrateGeneration\n this.joinRunHandler = options.joinRun\n this.body = options.body ?? {}\n // `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:\n // hydrate the last generation for `threadId` from the server on mount).\n this.serverDriven = options.persistence === true\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onJobCreated: options.onJobCreated,\n onStatusUpdate: options.onStatusUpdate,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n onJobIdChange: options.onJobIdChange,\n onVideoStatusChange: options.onVideoStatusChange,\n onResumeSnapshotChange: options.onResumeSnapshotChange,\n onResumeStateChange: options.onResumeStateChange,\n }\n\n this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools)\n this.devtoolsBridge = (\n options.devtoolsBridgeFactory ?? createNoOpVideoDevtoolsBridge\n )<TOutput>(this.buildDevtoolsBridgeOptions())\n\n // Mount hydration (`maybeHydrateFromServer`) is deliberately NOT run here. The framework\n // hooks build this client inside `useMemo`, so the constructor executes in\n // React's render phase; hydrating here would re-fire the hydrate GET on\n // every discarded/speculative render, flooding the connection pool when\n // several clients mount together. It is kicked off once from\n // `mountDevtools`, which the hooks call from a commit-phase mount effect.\n }\n\n private buildDevtoolsBridgeOptions(): VideoDevtoolsBridgeOptions<TOutput> {\n const client = this\n return {\n get hookId() {\n return client.uniqueId\n },\n get clientId() {\n return client.uniqueId\n },\n get threadId() {\n return client.threadId\n },\n metadata: this.devtoolsMetadata,\n getCoreState: () => ({\n input: this.input,\n result: this.result,\n progress: this.progress,\n status: this.status,\n isLoading: this.isLoading,\n jobId: this.jobId,\n videoStatus: this.videoStatus,\n ...(this.error ? { error: this.error.message } : {}),\n }),\n }\n }\n\n mountDevtools(): void {\n this.ensureThreadId()\n // Mounting revives a disposed client. Framework hooks call this from\n // their mount effect, so a dispose → remount cycle (e.g. React\n // StrictMode's mount → cleanup → mount replay against the same memoized\n // client) leaves the client usable again.\n this.disposed = false\n this.maybeHydrateFromServer()\n // Re-attach to an already-loaded `running` snapshot (remount case); see the\n // note in GenerationClient.mountDevtools. Guarded by rejoinInFlight.\n this.maybeResumeInFlight()\n if (this.devtoolsMounted) {\n return\n }\n\n this.devtoolsMounted = true\n this.devtoolsBridge.emitRegistered()\n this.devtoolsBridge.emitSnapshot()\n }\n\n /**\n * Trigger video generation.\n * Only one generation can be in-flight at a time.\n */\n async generate(input: VideoGenerateInput): Promise<void> {\n if (this.disposed) return\n if (this.isLoading) return\n this.mountDevtools()\n\n this.input = input\n this.progress = null\n const runId = this.devtoolsBridge.beginRun(input)\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n this.setJobId(null)\n this.setVideoStatus(null)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n await this.generateWithFetcher(input, signal, runId)\n } else if (this.connection) {\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect(\n [],\n mergedData,\n signal,\n this.createRunContext(runId),\n )\n await this.processStream(stream, runId, signal)\n } else {\n throw new Error(\n 'VideoGenerationClient requires either a connection or fetcher option',\n )\n }\n if (!signal.aborted && this.status === 'success') {\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:completed',\n 'completed',\n )\n }\n } catch (err: unknown) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n this.setError(error)\n this.setStatus('error')\n this.recordResumeSnapshotError(error)\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:errored',\n 'errored',\n error.message,\n )\n this.callbacksRef.onError?.(error)\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n }\n\n /**\n * Direct fetcher mode: call fetcher and set result.\n */\n private async generateWithFetcher(\n input: VideoGenerateInput,\n signal: AbortSignal,\n runId: string,\n ): Promise<void> {\n if (!this.fetcher) return\n\n // Fetcher returns a completed result directly, or a Response with SSE body\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(parseSSEResponse(result, signal), runId, signal)\n } else {\n this.devtoolsBridge.ensureRunStarted(runId)\n this.setResult(result)\n this.setStatus('success')\n this.completePlainFetcherResumeSnapshot(result)\n }\n }\n\n /**\n * Process a stream of AG-UI events from the streaming connection adapter.\n * The server handles the polling loop and streams status updates.\n *\n * Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends\n * without a terminal chunk — see the note on\n * `GenerationClient.processStream`. Video runs are long enough that a proxy\n * idle timeout mid-poll is the likeliest way to hit it.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n fallbackRunId: string,\n signal: AbortSignal,\n ): Promise<void> {\n let streamRunId: string | undefined\n let sawTerminalChunk = false\n\n for await (const raw of source) {\n if (signal.aborted) break\n\n const chunk = restoreInboundChunk(raw)\n this.callbacksRef.onChunk?.(chunk)\n this.observeResumeSnapshot(chunk)\n const chunkRunId =\n 'runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined\n\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to video generation lifecycle.\n switch (chunk.type) {\n case 'RUN_STARTED': {\n streamRunId = chunk.runId\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n break\n }\n case 'CUSTOM': {\n this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId)\n if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {\n const { jobId } = chunk.value as { jobId: string }\n this.setJobId(jobId)\n this.callbacksRef.onJobCreated?.(jobId)\n } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {\n const statusInfo = chunk.value as VideoStatusInfo\n this.setVideoStatus(statusInfo)\n this.callbacksRef.onStatusUpdate?.(statusInfo)\n if (statusInfo.progress !== undefined) {\n this.setProgress(statusInfo.progress)\n }\n } else if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as VideoGenerateResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.setProgress(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n streamRunId = chunk.runId\n sawTerminalChunk = true\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n this.devtoolsBridge.ensureRunStarted(\n chunkRunId ?? streamRunId ?? fallbackRunId,\n )\n // Spec RUN_ERROR message. Missing message uses this fallback.\n const msg =\n (chunk.message as string | undefined) || 'An error occurred'\n throw new Error(msg)\n }\n default:\n break\n }\n }\n\n // An aborted read is a deliberate stop/dispose, not a truncation.\n if (!sawTerminalChunk && !signal.aborted) {\n throw new Error(GENERATION_STREAM_TRUNCATED_MESSAGE)\n }\n }\n\n /**\n * Abort any in-flight generation or polling.\n */\n stop(): void {\n const runId = this.devtoolsBridge.getActiveRunId()\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n if (runId) {\n this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled')\n }\n }\n // A stopped run is no longer resumable. Without this the in-memory\n // snapshot stays `running`, and a remount's `maybeResumeInFlight` would\n // rejoin a run the user just cancelled.\n if (this.resumeSnapshot && this.resumeSnapshot.status === 'running') {\n this.resumeSnapshot = {\n ...this.resumeSnapshot,\n resumeState: null,\n status: 'idle',\n }\n this.notifyResumeSnapshotChanged()\n }\n }\n\n /**\n * Clear all state and return to idle. Also drops the client's in-memory\n * resume snapshot, so a remount restores nothing. The server-side record is\n * untouched — this client no longer writes one — so a full page reload under\n * `persistence: true` re-hydrates the last generation again.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.input = null\n this.progress = null\n this.devtoolsBridge.resetRuns()\n this.setJobId(null)\n this.setVideoStatus(null)\n this.setError(undefined)\n this.setStatus('idle')\n this.clearResumeSnapshot()\n this.devtoolsBridge.emitState()\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n VideoGenerationClientOptions<TOutput>,\n | 'body'\n | 'onResult'\n | 'onError'\n | 'onProgress'\n | 'onChunk'\n | 'onJobCreated'\n | 'onStatusUpdate'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n if (options.onJobCreated !== undefined) {\n this.callbacksRef.onJobCreated = options.onJobCreated\n }\n if (options.onStatusUpdate !== undefined) {\n this.callbacksRef.onStatusUpdate = options.onStatusUpdate\n }\n }\n\n dispose(): void {\n this.disposed = true\n // Teardown, NOT a user cancel (see GenerationClient.dispose): abort in-flight\n // delivery but keep the `running` snapshot resumable so a remount rejoins.\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.devtoolsBridge.dispose()\n this.devtoolsMounted = false\n this.serverHydrationStarted = false\n this.rejoinedRunId = undefined\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getJobId(): string | null {\n return this.jobId\n }\n\n getVideoStatus(): VideoStatusInfo | null {\n return this.videoStatus\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n getResumeSnapshot(): GenerationResumeSnapshot | undefined {\n return this.resumeSnapshot\n ? {\n ...this.resumeSnapshot,\n ...(this.resumeSnapshot.pendingArtifacts\n ? { pendingArtifacts: [...this.resumeSnapshot.pendingArtifacts] }\n : {}),\n ...(this.resumeSnapshot.result\n ? {\n result: {\n ...this.resumeSnapshot.result,\n ...(this.resumeSnapshot.result.artifacts\n ? { artifacts: [...this.resumeSnapshot.result.artifacts] }\n : {}),\n },\n }\n : {}),\n ...(this.resumeSnapshot.error\n ? { error: { ...this.resumeSnapshot.error } }\n : {}),\n ...(this.resumeSnapshot.lastEvent\n ? { lastEvent: { ...this.resumeSnapshot.lastEvent } }\n : {}),\n }\n : undefined\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: VideoGenerateResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n this.devtoolsBridge.recordResultChange()\n return\n }\n\n const completedStatus = this.createCompletedVideoStatus(rawResult)\n if (this.progress?.value !== 100) {\n this.setProgress(100, this.progress?.message)\n }\n this.setJobId(rawResult.jobId)\n this.setVideoStatus(completedStatus)\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged, just re-emit\n this.devtoolsBridge.emitState()\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value as\n // TOutput. When the caller did not supply an onResult transform,\n // `TOutput` defaults to `VideoGenerateResult`, so the runtime cast is\n // sound.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to VideoGenerateResult when no onResult transform is supplied\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n }\n\n private setJobId(jobId: string | null): void {\n this.jobId = jobId\n this.callbacksRef.onJobIdChange?.(jobId)\n this.devtoolsBridge.recordJobIdChange()\n }\n\n private setVideoStatus(status: VideoStatusInfo | null): void {\n this.videoStatus = status\n this.callbacksRef.onVideoStatusChange?.(status)\n this.devtoolsBridge.recordVideoStatusChange()\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n this.devtoolsBridge.recordLoadingChange()\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n this.devtoolsBridge.recordErrorChange(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n this.devtoolsBridge.recordStatusChange(status)\n }\n\n private setProgress(value: number, message?: string): void {\n this.progress = {\n value,\n ...(message ? { message } : {}),\n }\n if (message === undefined) {\n this.callbacksRef.onProgress?.(value)\n } else {\n this.callbacksRef.onProgress?.(value, message)\n }\n this.devtoolsBridge.recordProgressChange()\n }\n\n private createCompletedVideoStatus(\n result: VideoGenerateResult,\n ): VideoStatusInfo {\n return {\n jobId: result.jobId,\n status: result.status,\n progress: 100,\n url: result.url,\n }\n }\n\n private createDevtoolsMetadata(\n metadata?: Partial<AIDevtoolsClientMetadata>,\n ): AIDevtoolsClientMetadata {\n return {\n hookName: metadata?.hookName ?? 'useGenerateVideo',\n outputKind: metadata?.outputKind ?? 'video',\n ...(metadata?.framework ? { framework: metadata.framework } : {}),\n ...(metadata?.name ? { name: metadata.name } : {}),\n }\n }\n\n private ensureThreadId(): string {\n if (!this.threadId) {\n this.threadId = this.generateUniqueId('video')\n }\n this.uniqueId = this.threadId\n return this.threadId\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private createRunContext(runId: string): RunAgentInputContext {\n return {\n threadId: this.threadId,\n runId,\n }\n }\n\n private observeResumeSnapshot(chunk: StreamChunk): void {\n this.resumeSnapshot = updateGenerationResumeSnapshot(\n this.resumeSnapshot,\n chunk,\n )\n this.notifyResumeSnapshotChanged()\n }\n\n /** Notify the internal snapshot listener AND emit the public resume state. */\n private notifyResumeSnapshotChanged(): void {\n this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)\n this.emitResumeState()\n }\n\n /** Derive the public `resumeState` from the internal snapshot. */\n private emitResumeState(): void {\n const snapshot = this.resumeSnapshot\n const state = snapshot?.resumeState\n const resumeState: GenerationResumeState | null = state\n ? {\n ...state,\n ...(snapshot?.pendingArtifacts && snapshot.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...snapshot.pendingArtifacts] }\n : {}),\n }\n : null\n this.callbacksRef.onResumeStateChange?.(resumeState)\n }\n\n /**\n * Repaint the normal fields from a restored snapshot so a reload presents the\n * video in `result` / `status` / `error` / `jobId`, never a snapshot object.\n * `isLoading` stays false (no auto-tail). Not re-persisted (it came from\n * storage / the server).\n *\n * A `complete` snapshot with no durable video artifact cannot be rebuilt, so\n * it repaints as an error rather than a `success` with a `null` result — see\n * the note on `GenerationClient.repaintFromSnapshot`.\n */\n private repaintFromSnapshot(snapshot: GenerationResumeSnapshot): void {\n this.resumeSnapshot = snapshot\n this.notifyResumeSnapshotChanged()\n this.setStatus(clientStateFromResumeStatus(snapshot.status))\n this.setError(\n snapshot.error\n ? Object.assign(\n new Error(snapshot.error.message),\n snapshot.error.code ? { code: snapshot.error.code } : {},\n )\n : undefined,\n )\n if (snapshot.result?.providerJobId)\n this.setJobId(snapshot.result.providerJobId)\n const restored = this.reconstructVideoResult(snapshot)\n if (restored !== null) {\n this.setResult(restored)\n } else if (snapshot.status === 'complete') {\n this.reportUnrestorableResult()\n }\n }\n\n /**\n * Report a `complete` snapshot with no durable video artifact to rebuild\n * from. Runs after the status/error repaint above, so it wins over the\n * snapshot's own `complete` status.\n */\n private reportUnrestorableResult(): void {\n const error = new Error(GENERATION_UNRESTORABLE_RESULT_MESSAGE)\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Repaint a restored snapshot (client store or server hydrate) and, when it\n * reports a run still in flight, tail that run to completion via `joinRun`\n * (from the connection, or the `joinRun` option when the transport can't\n * carry one).\n *\n * A `running` snapshot that no `joinRun` handler can tail is repainted as an\n * interrupted error instead of a `generating` status that would never\n * settle: an interrupted generation cannot be resumed, only re-run.\n */\n private repaintRestoredSnapshot(\n snapshot: GenerationResumeSnapshot,\n activeRunId?: string,\n ): void {\n if (snapshot.status !== 'running') {\n this.repaintFromSnapshot(snapshot)\n return\n }\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n const runId = activeRunId ?? snapshot.resumeState?.runId\n if (runId && joinRun) {\n this.repaintFromSnapshot(snapshot)\n this.rejoinInFlight(runId)\n return\n }\n this.repaintFromSnapshot({\n ...snapshot,\n resumeState: null,\n status: 'error',\n error: {\n message:\n 'The previous generation was interrupted before it finished and cannot be resumed — generate again to retry.',\n },\n })\n }\n\n /**\n * Rebuild a `VideoGenerateResult` from a restored snapshot: the video's bytes\n * are served from the durable artifact URL, so the restored result renders\n * from your own origin. Returns `null` when there is no durable video artifact.\n */\n private reconstructVideoResult(\n snapshot: GenerationResumeSnapshot,\n ): VideoGenerateResult | null {\n const result = snapshot.result\n const artifacts = result?.artifacts ?? []\n const output = artifacts.find(\n (a) =>\n a.role === 'output' && a.source.mediaType === 'video' && a.url != null,\n )\n if (!output?.url) return null\n return {\n jobId: result?.providerJobId ?? '',\n status: 'completed',\n url: output.url,\n ...(result?.expiresAt ? { expiresAt: new Date(result.expiresAt) } : {}),\n artifacts,\n }\n }\n\n /**\n * The plain (non-Response) fetcher path never observes stream chunks, so\n * the terminal snapshot is built here from the fetcher's own result. A\n * stale `error` from a previous run is intentionally dropped — this run\n * succeeded.\n */\n private completePlainFetcherResumeSnapshot(rawResult: unknown): void {\n const previous = this.resumeSnapshot\n const result = createGenerationResultSnapshot(rawResult)\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'complete',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(result\n ? { result }\n : previous?.result\n ? { result: { ...previous.result } }\n : {}),\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Records a transport-level failure (network drop, throwing callback) in\n * the snapshot. Without this, only a server-emitted RUN_ERROR chunk would\n * mark the snapshot `error`, leaving a persisted record that claims the\n * run is still in flight.\n */\n private recordResumeSnapshotError(error: Error): void {\n // Surface the failure on the observable fields FIRST, unconditionally (see\n // the note in GenerationClient.recordResumeSnapshotError): a RUN_ERROR\n // already flipped the snapshot to `error`, so the early-return would else\n // skip this and leave `status` stuck on `generating`. The guard avoids a\n // duplicate `error` emission on the live `generate()` path.\n if (this.status !== 'error') this.setStatus('error')\n this.setError(error)\n if (this.resumeSnapshot?.status === 'error') return\n if (!this.resumeSnapshot && !this.serverDriven) return\n const previous = this.resumeSnapshot\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'error',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(previous?.result ? { result: { ...previous.result } } : {}),\n error: { message: error.message },\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Drop the client's in-memory snapshot and re-emit. Purely local — this\n * client writes no storage, so nothing persisted is removed.\n */\n private clearResumeSnapshot(): void {\n this.resumeSnapshot = undefined\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Server-driven mount hydration entry point (`persistence: true`). Runs at\n * most once, from the commit-phase mount path (`mountDevtools`) — never the\n * constructor / render phase — so remounts and speculative renders can't\n * re-fire the hydrate GET.\n */\n private maybeHydrateFromServer(): void {\n if (!this.serverDriven || this.serverHydrationStarted) return\n this.serverHydrationStarted = true\n if (this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler) {\n this.hydrateFromServer()\n } else {\n // `persistence: true` without any hydrate source can never restore\n // anything — warn rather than silently no-op.\n console.warn(\n '[TanStack AI] `persistence: true` (server-driven) needs a `hydrateGeneration` handler — either a connection that implements one (e.g. `fetchServerSentEvents` / `fetchHttpStream`, or `stream()` / `rpcStream()` with persistence handlers) or the `hydrateGeneration` option. Without one, nothing is persisted or restored.',\n )\n }\n }\n\n /**\n * Server-driven mount hydration (`persistence: true`). The client holds no\n * local snapshot; on mount it asks the server — keyed by the stable threadId —\n * for the last generation's resume snapshot, validates it, and repaints it. It\n * never auto-starts a run, and never blocks: a `generate()` that starts first\n * owns the client and hydration backs off, mirroring the chat client.\n *\n * A genuine **miss** (no record for the thread) is silent; a genuine\n * **failure** (transport error, authorize rejection, malformed body, a record\n * the validator rejects) surfaces through `status` / `error` / `onError` — see\n * the note on `GenerationClient.hydrateFromServer`.\n */\n private hydrateFromServer(): void {\n const hydrate =\n this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler\n if (!hydrate) return\n // A send that already started owns the client; don't stomp it.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n void (async () => {\n let res: GenerationHydrationResult\n try {\n res = await hydrate(this.threadId)\n } catch (cause) {\n this.failHydration(\n createGenerationHydrationError(\n 'the request to the server did not succeed',\n cause,\n ),\n )\n return\n }\n // No record for this thread — a fresh thread, not a failure.\n if (!res.resumeSnapshot) return\n const snapshot = parseGenerationResumeSnapshot(res.resumeSnapshot)\n if (!snapshot) {\n this.failHydration(\n createGenerationHydrationError(\n 'the server returned a record this client cannot read (unknown schema version, or a missing/invalid `status` or `resumeState`)',\n ),\n )\n return\n }\n // Re-check: a send may have started while the fetch was in flight.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle')\n return\n // A run still generating on the server: re-attach and finish it in place.\n this.repaintRestoredSnapshot(snapshot, res.activeRun?.runId)\n })()\n }\n\n /**\n * Surface a hydration failure on the observable fields. Skipped when a\n * `generate()` took ownership while the hydrate GET was in flight.\n */\n private failHydration(error: Error): void {\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Re-attach to an already-loaded `running` snapshot (remount case); see the\n * note in GenerationClient.maybeResumeInFlight. Guarded by `rejoinInFlight`.\n */\n private maybeResumeInFlight(): void {\n if (this.resumeSnapshot?.status !== 'running') return\n const runId = this.resumeSnapshot.resumeState?.runId\n if (runId) this.rejoinInFlight(runId)\n }\n\n /**\n * Re-attach to a video run still generating and stream it to completion,\n * mirroring the chat client's mount-time rejoin. Reuses `processStream`, so\n * the job status and result repaint from the replayed chunks. A live\n * `generate()` owns the client and is never stomped; a run is rejoined once.\n */\n private rejoinInFlight(runId: string): void {\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n if (!joinRun) return\n if (this.rejoinedRunId === runId) return\n if (this.isLoading || this.abortController) return\n this.rejoinedRunId = runId\n const controller = new AbortController()\n this.abortController = controller\n this.setIsLoading(true)\n this.setStatus('generating')\n void (async () => {\n try {\n await this.processStream(\n joinRun(runId, controller.signal),\n runId,\n controller.signal,\n )\n } catch (error) {\n if (!controller.signal.aborted) {\n const failure =\n error instanceof Error ? error : new Error(String(error))\n // Settles `status`/`error` AND rewrites the snapshot to a terminal\n // `error` with a null `resumeState`, so the next mount does not\n // rejoin this run again.\n this.recordResumeSnapshotError(failure)\n this.callbacksRef.onError?.(failure)\n }\n } finally {\n // Only reset if this rejoin still owns the client: a `stop()` +\n // fresh `generate()` may have replaced the controller while the tail\n // was settling, and that live run owns `isLoading` now.\n if (this.abortController === controller) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n })()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video-generation-client.js","names":[],"sources":["../../src/video-generation-client.ts"],"sourcesContent":["import { ByokBlockedError, ByokMissingError } from '@tanstack/ai/byok'\nimport {\n prepareResolvedByokHeaders,\n resolveByokProviderId,\n} from './byok/resolve'\nimport {\n GENERATION_EVENTS,\n GENERATION_STREAM_TRUNCATED_MESSAGE,\n GENERATION_UNRESTORABLE_RESULT_MESSAGE,\n clientStateFromResumeStatus,\n createGenerationHydrationError,\n createGenerationResultSnapshot,\n parseGenerationResumeSnapshot,\n updateGenerationResumeSnapshot,\n} from './generation-types'\nimport { createNoOpVideoDevtoolsBridge } from './devtools-noop'\nimport { parseSSEResponse } from './sse-parser'\nimport { restoreInboundChunk } from '@tanstack/ai/client'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type { ByokClient } from './byok'\nimport type {\n ConnectConnectionAdapter,\n GenerationHydrationResult,\n RunAgentInputContext,\n} from './connection-adapters'\nimport type {\n AIDevtoolsClientMetadata,\n AIDevtoolsGenerationProgress,\n VideoDevtoolsBridge,\n VideoDevtoolsBridgeOptions,\n} from './devtools'\nimport type {\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n GenerationResumeSnapshot,\n GenerationResumeState,\n GenerationTransport,\n VideoGenerateInput,\n VideoGenerateResult,\n VideoGenerationClientOptions,\n VideoStatusInfo,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\n// All optional fields explicitly allow `| undefined` so callers can spread\n// option bags (where each callback may be `undefined`) into the callbacks\n// ref under `exactOptionalPropertyTypes`.\ninterface VideoCallbacks<TOutput> {\n onResult?:\n | ((result: VideoGenerateResult) => TOutput | null | void)\n | undefined\n onError?: ((error: Error) => void) | undefined\n onProgress?: ((progress: number, message?: string) => void) | undefined\n onChunk?: ((chunk: StreamChunk) => void) | undefined\n onJobCreated?: ((jobId: string) => void) | undefined\n onStatusUpdate?: ((status: VideoStatusInfo) => void) | undefined\n onResultChange?: ((result: TOutput | null) => void) | undefined\n onLoadingChange?: ((isLoading: boolean) => void) | undefined\n onErrorChange?: ((error: Error | undefined) => void) | undefined\n onStatusChange?: ((status: GenerationClientState) => void) | undefined\n onJobIdChange?: ((jobId: string | null) => void) | undefined\n onVideoStatusChange?: ((status: VideoStatusInfo | null) => void) | undefined\n onResumeSnapshotChange?:\n | ((snapshot: GenerationResumeSnapshot | undefined) => void)\n | undefined\n onResumeStateChange?:\n | ((resumeState: GenerationResumeState | null) => void)\n | undefined\n}\n\n/**\n * A specialized client for job-based video generation.\n *\n * Video generation is asynchronous: a job is created, then polled for status\n * until completion. This client handles the full lifecycle.\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Server handles the polling loop internally and\n * streams status updates via CUSTOM events.\n * - **Fetcher** — Direct async function that returns a completed\n * `VideoGenerateResult`.\n *\n * @example\n * ```typescript\n * // With streaming connection adapter (server-driven polling)\n * const client = new VideoGenerationClient({\n * connection: fetchServerSentEvents('/api/generate/video'),\n * onResultChange: setResult,\n * onVideoStatusChange: setVideoStatus,\n * })\n *\n * // With fetcher (direct result)\n * const client = new VideoGenerationClient({\n * fetcher: async (input) => {\n * const res = await fetch('/api/video/generate', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json() // { jobId, status: 'completed', url, expiresAt }\n * },\n * })\n *\n * await client.generate({ prompt: 'A flying car over a city' })\n * ```\n */\nexport class VideoGenerationClient<TOutput = VideoGenerateResult> {\n private readonly connection: ConnectConnectionAdapter | undefined\n // Persistence handlers supplied as options (e.g. alongside a `fetcher`), used\n // when the connection doesn't carry its own — the connection's handlers take\n // precedence when both exist.\n private readonly hydrateGenerationHandler:\n | ConnectConnectionAdapter['hydrateGeneration']\n | undefined\n private readonly joinRunHandler:\n | ConnectConnectionAdapter['joinRun']\n | undefined\n private readonly fetcher:\n | GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n | undefined\n private uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: VideoDevtoolsBridge<TOutput>\n private threadId: string\n // Server-driven mode (`persistence: true`): no local snapshot store; on mount\n // the client hydrates the last generation for `threadId` from the server.\n private readonly serverDriven: boolean = false\n private body: Record<string, any>\n private byok: ByokClient | undefined\n private byokProvider: (() => string | undefined) | undefined\n\n private result: TOutput | null = null\n private input: VideoGenerateInput | null = null\n private progress: AIDevtoolsGenerationProgress | null = null\n private jobId: string | null = null\n private videoStatus: VideoStatusInfo | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private resumeSnapshot: GenerationResumeSnapshot | undefined\n private abortController: AbortController | null = null\n private rejoinedRunId: string | undefined\n private readonly callbacksRef: VideoCallbacks<TOutput>\n private devtoolsMounted = false\n private disposed = false\n private serverHydrationStarted = false\n\n constructor(\n options: Omit<\n VideoGenerationClientOptions<TOutput>,\n 'persistence' | 'threadId'\n > &\n GenerationPersistenceOptions &\n GenerationTransport<VideoGenerateInput, VideoGenerateResult>,\n ) {\n // `threadId` is the only identity. Do not mint a random id during\n // construct: hooks build this client during render.\n this.threadId = options.threadId ?? ''\n this.uniqueId = this.threadId\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.hydrateGenerationHandler = options.hydrateGeneration\n this.joinRunHandler = options.joinRun\n this.body = options.body ?? {}\n this.byok = options.byok\n this.byokProvider = options.byokProvider\n // `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:\n // hydrate the last generation for `threadId` from the server on mount).\n this.serverDriven = options.persistence === true\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onJobCreated: options.onJobCreated,\n onStatusUpdate: options.onStatusUpdate,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n onJobIdChange: options.onJobIdChange,\n onVideoStatusChange: options.onVideoStatusChange,\n onResumeSnapshotChange: options.onResumeSnapshotChange,\n onResumeStateChange: options.onResumeStateChange,\n }\n\n this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools)\n this.devtoolsBridge = (\n options.devtoolsBridgeFactory ?? createNoOpVideoDevtoolsBridge\n )<TOutput>(this.buildDevtoolsBridgeOptions())\n\n // Mount hydration (`maybeHydrateFromServer`) is deliberately NOT run here. The framework\n // hooks build this client inside `useMemo`, so the constructor executes in\n // React's render phase; hydrating here would re-fire the hydrate GET on\n // every discarded/speculative render, flooding the connection pool when\n // several clients mount together. It is kicked off once from\n // `mountDevtools`, which the hooks call from a commit-phase mount effect.\n }\n\n private buildDevtoolsBridgeOptions(): VideoDevtoolsBridgeOptions<TOutput> {\n const client = this\n return {\n get hookId() {\n return client.uniqueId\n },\n get clientId() {\n return client.uniqueId\n },\n get threadId() {\n return client.threadId\n },\n metadata: this.devtoolsMetadata,\n getCoreState: () => ({\n input: this.input,\n result: this.result,\n progress: this.progress,\n status: this.status,\n isLoading: this.isLoading,\n jobId: this.jobId,\n videoStatus: this.videoStatus,\n ...(this.error ? { error: this.error.message } : {}),\n }),\n }\n }\n\n mountDevtools(): void {\n this.ensureThreadId()\n // Mounting revives a disposed client. Framework hooks call this from\n // their mount effect, so a dispose → remount cycle (e.g. React\n // StrictMode's mount → cleanup → mount replay against the same memoized\n // client) leaves the client usable again.\n this.disposed = false\n this.maybeHydrateFromServer()\n // Re-attach to an already-loaded `running` snapshot (remount case); see the\n // note in GenerationClient.mountDevtools. Guarded by rejoinInFlight.\n this.maybeResumeInFlight()\n if (this.devtoolsMounted) {\n return\n }\n\n this.devtoolsMounted = true\n this.devtoolsBridge.emitRegistered()\n this.devtoolsBridge.emitSnapshot()\n }\n\n /**\n * Trigger video generation.\n * Only one generation can be in-flight at a time.\n */\n async generate(input: VideoGenerateInput): Promise<void> {\n if (this.disposed) return\n if (this.isLoading) return\n this.mountDevtools()\n\n this.input = input\n this.progress = null\n const runId = this.devtoolsBridge.beginRun(input)\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n this.setJobId(null)\n this.setVideoStatus(null)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n let headers: Record<string, string> | undefined\n if (this.byok) {\n const provider = resolveByokProviderId(\n this.byokProvider,\n this.body.provider,\n )\n headers = await prepareResolvedByokHeaders(this.byok, provider)\n }\n\n if (this.fetcher) {\n await this.generateWithFetcher(input, signal, runId, headers)\n } else if (this.connection) {\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect(\n [],\n mergedData,\n signal,\n this.createRunContext(runId, headers),\n )\n await this.processStream(stream, runId, signal)\n } else {\n throw new Error(\n 'VideoGenerationClient requires either a connection or fetcher option',\n )\n }\n if (!signal.aborted && this.status === 'success') {\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:completed',\n 'completed',\n )\n }\n } catch (err: unknown) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n if (error instanceof ByokMissingError) {\n this.byok?.request(error.provider, 'missing')\n }\n if (error instanceof ByokBlockedError && error.reason === 'locked') {\n this.byok?.request(error.provider, 'locked')\n }\n this.setError(error)\n this.setStatus('error')\n this.recordResumeSnapshotError(error)\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:errored',\n 'errored',\n error.message,\n )\n this.callbacksRef.onError?.(error)\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n }\n\n /**\n * Direct fetcher mode: call fetcher and set result.\n */\n private async generateWithFetcher(\n input: VideoGenerateInput,\n signal: AbortSignal,\n runId: string,\n headers?: Record<string, string>,\n ): Promise<void> {\n if (!this.fetcher) return\n\n // Fetcher returns a completed result directly, or a Response with SSE body\n const result = await this.fetcher(\n input,\n headers === undefined ? { signal } : { signal, headers },\n )\n if (signal.aborted) return\n\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(parseSSEResponse(result, signal), runId, signal)\n } else {\n this.devtoolsBridge.ensureRunStarted(runId)\n this.setResult(result)\n this.setStatus('success')\n this.completePlainFetcherResumeSnapshot(result)\n }\n }\n\n /**\n * Process a stream of AG-UI events from the streaming connection adapter.\n * The server handles the polling loop and streams status updates.\n *\n * Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends\n * without a terminal chunk — see the note on\n * `GenerationClient.processStream`. Video runs are long enough that a proxy\n * idle timeout mid-poll is the likeliest way to hit it.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n fallbackRunId: string,\n signal: AbortSignal,\n ): Promise<void> {\n let streamRunId: string | undefined\n let sawTerminalChunk = false\n\n for await (const raw of source) {\n if (signal.aborted) break\n\n const chunk = restoreInboundChunk(raw)\n this.callbacksRef.onChunk?.(chunk)\n this.observeResumeSnapshot(chunk)\n const chunkRunId =\n 'runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined\n\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to video generation lifecycle.\n switch (chunk.type) {\n case 'RUN_STARTED': {\n streamRunId = chunk.runId\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n break\n }\n case 'CUSTOM': {\n this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId)\n if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {\n const { jobId } = chunk.value as { jobId: string }\n this.setJobId(jobId)\n this.callbacksRef.onJobCreated?.(jobId)\n } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {\n const statusInfo = chunk.value as VideoStatusInfo\n this.setVideoStatus(statusInfo)\n this.callbacksRef.onStatusUpdate?.(statusInfo)\n if (statusInfo.progress !== undefined) {\n this.setProgress(statusInfo.progress)\n }\n } else if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as VideoGenerateResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.setProgress(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n streamRunId = chunk.runId\n sawTerminalChunk = true\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n this.devtoolsBridge.ensureRunStarted(\n chunkRunId ?? streamRunId ?? fallbackRunId,\n )\n // Spec RUN_ERROR message. Missing message uses this fallback.\n const msg =\n (chunk.message as string | undefined) || 'An error occurred'\n throw new Error(msg)\n }\n default:\n break\n }\n }\n\n // An aborted read is a deliberate stop/dispose, not a truncation.\n if (!sawTerminalChunk && !signal.aborted) {\n throw new Error(GENERATION_STREAM_TRUNCATED_MESSAGE)\n }\n }\n\n /**\n * Abort any in-flight generation or polling.\n */\n stop(): void {\n const runId = this.devtoolsBridge.getActiveRunId()\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n if (runId) {\n this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled')\n }\n }\n // A stopped run is no longer resumable. Without this the in-memory\n // snapshot stays `running`, and a remount's `maybeResumeInFlight` would\n // rejoin a run the user just cancelled.\n if (this.resumeSnapshot && this.resumeSnapshot.status === 'running') {\n this.resumeSnapshot = {\n ...this.resumeSnapshot,\n resumeState: null,\n status: 'idle',\n }\n this.notifyResumeSnapshotChanged()\n }\n }\n\n /**\n * Clear all state and return to idle. Also drops the client's in-memory\n * resume snapshot, so a remount restores nothing. The server-side record is\n * untouched — this client no longer writes one — so a full page reload under\n * `persistence: true` re-hydrates the last generation again.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.input = null\n this.progress = null\n this.devtoolsBridge.resetRuns()\n this.setJobId(null)\n this.setVideoStatus(null)\n this.setError(undefined)\n this.setStatus('idle')\n this.clearResumeSnapshot()\n this.devtoolsBridge.emitState()\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n VideoGenerationClientOptions<TOutput>,\n | 'body'\n | 'byok'\n | 'byokProvider'\n | 'onResult'\n | 'onError'\n | 'onProgress'\n | 'onChunk'\n | 'onJobCreated'\n | 'onStatusUpdate'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.byok !== undefined) {\n this.byok = options.byok\n }\n if (options.byokProvider !== undefined) {\n this.byokProvider = options.byokProvider\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n if (options.onJobCreated !== undefined) {\n this.callbacksRef.onJobCreated = options.onJobCreated\n }\n if (options.onStatusUpdate !== undefined) {\n this.callbacksRef.onStatusUpdate = options.onStatusUpdate\n }\n }\n\n dispose(): void {\n this.disposed = true\n // Teardown, NOT a user cancel (see GenerationClient.dispose): abort in-flight\n // delivery but keep the `running` snapshot resumable so a remount rejoins.\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.devtoolsBridge.dispose()\n this.devtoolsMounted = false\n this.serverHydrationStarted = false\n this.rejoinedRunId = undefined\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getJobId(): string | null {\n return this.jobId\n }\n\n getVideoStatus(): VideoStatusInfo | null {\n return this.videoStatus\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n getResumeSnapshot(): GenerationResumeSnapshot | undefined {\n return this.resumeSnapshot\n ? {\n ...this.resumeSnapshot,\n ...(this.resumeSnapshot.pendingArtifacts\n ? { pendingArtifacts: [...this.resumeSnapshot.pendingArtifacts] }\n : {}),\n ...(this.resumeSnapshot.result\n ? {\n result: {\n ...this.resumeSnapshot.result,\n ...(this.resumeSnapshot.result.artifacts\n ? { artifacts: [...this.resumeSnapshot.result.artifacts] }\n : {}),\n },\n }\n : {}),\n ...(this.resumeSnapshot.error\n ? { error: { ...this.resumeSnapshot.error } }\n : {}),\n ...(this.resumeSnapshot.lastEvent\n ? { lastEvent: { ...this.resumeSnapshot.lastEvent } }\n : {}),\n }\n : undefined\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: VideoGenerateResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n this.devtoolsBridge.recordResultChange()\n return\n }\n\n const completedStatus = this.createCompletedVideoStatus(rawResult)\n if (this.progress?.value !== 100) {\n this.setProgress(100, this.progress?.message)\n }\n this.setJobId(rawResult.jobId)\n this.setVideoStatus(completedStatus)\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged, just re-emit\n this.devtoolsBridge.emitState()\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value as\n // TOutput. When the caller did not supply an onResult transform,\n // `TOutput` defaults to `VideoGenerateResult`, so the runtime cast is\n // sound.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to VideoGenerateResult when no onResult transform is supplied\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n }\n\n private setJobId(jobId: string | null): void {\n this.jobId = jobId\n this.callbacksRef.onJobIdChange?.(jobId)\n this.devtoolsBridge.recordJobIdChange()\n }\n\n private setVideoStatus(status: VideoStatusInfo | null): void {\n this.videoStatus = status\n this.callbacksRef.onVideoStatusChange?.(status)\n this.devtoolsBridge.recordVideoStatusChange()\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n this.devtoolsBridge.recordLoadingChange()\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n this.devtoolsBridge.recordErrorChange(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n this.devtoolsBridge.recordStatusChange(status)\n }\n\n private setProgress(value: number, message?: string): void {\n this.progress = {\n value,\n ...(message ? { message } : {}),\n }\n if (message === undefined) {\n this.callbacksRef.onProgress?.(value)\n } else {\n this.callbacksRef.onProgress?.(value, message)\n }\n this.devtoolsBridge.recordProgressChange()\n }\n\n private createCompletedVideoStatus(\n result: VideoGenerateResult,\n ): VideoStatusInfo {\n return {\n jobId: result.jobId,\n status: result.status,\n progress: 100,\n url: result.url,\n }\n }\n\n private createDevtoolsMetadata(\n metadata?: Partial<AIDevtoolsClientMetadata>,\n ): AIDevtoolsClientMetadata {\n return {\n hookName: metadata?.hookName ?? 'useGenerateVideo',\n outputKind: metadata?.outputKind ?? 'video',\n ...(metadata?.framework ? { framework: metadata.framework } : {}),\n ...(metadata?.name ? { name: metadata.name } : {}),\n }\n }\n\n private ensureThreadId(): string {\n if (!this.threadId) {\n this.threadId = this.generateUniqueId('video')\n }\n this.uniqueId = this.threadId\n return this.threadId\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private createRunContext(\n runId: string,\n headers?: Record<string, string>,\n ): RunAgentInputContext {\n return {\n threadId: this.threadId,\n runId,\n ...(headers ? { headers } : {}),\n }\n }\n\n private observeResumeSnapshot(chunk: StreamChunk): void {\n this.resumeSnapshot = updateGenerationResumeSnapshot(\n this.resumeSnapshot,\n chunk,\n )\n this.notifyResumeSnapshotChanged()\n }\n\n /** Notify the internal snapshot listener AND emit the public resume state. */\n private notifyResumeSnapshotChanged(): void {\n this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)\n this.emitResumeState()\n }\n\n /** Derive the public `resumeState` from the internal snapshot. */\n private emitResumeState(): void {\n const snapshot = this.resumeSnapshot\n const state = snapshot?.resumeState\n const resumeState: GenerationResumeState | null = state\n ? {\n ...state,\n ...(snapshot?.pendingArtifacts && snapshot.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...snapshot.pendingArtifacts] }\n : {}),\n }\n : null\n this.callbacksRef.onResumeStateChange?.(resumeState)\n }\n\n /**\n * Repaint the normal fields from a restored snapshot so a reload presents the\n * video in `result` / `status` / `error` / `jobId`, never a snapshot object.\n * `isLoading` stays false (no auto-tail). Not re-persisted (it came from\n * storage / the server).\n *\n * A `complete` snapshot with no durable video artifact cannot be rebuilt, so\n * it repaints as an error rather than a `success` with a `null` result — see\n * the note on `GenerationClient.repaintFromSnapshot`.\n */\n private repaintFromSnapshot(snapshot: GenerationResumeSnapshot): void {\n this.resumeSnapshot = snapshot\n this.notifyResumeSnapshotChanged()\n this.setStatus(clientStateFromResumeStatus(snapshot.status))\n this.setError(\n snapshot.error\n ? Object.assign(\n new Error(snapshot.error.message),\n snapshot.error.code ? { code: snapshot.error.code } : {},\n )\n : undefined,\n )\n if (snapshot.result?.providerJobId)\n this.setJobId(snapshot.result.providerJobId)\n const restored = this.reconstructVideoResult(snapshot)\n if (restored !== null) {\n this.setResult(restored)\n } else if (snapshot.status === 'complete') {\n this.reportUnrestorableResult()\n }\n }\n\n /**\n * Report a `complete` snapshot with no durable video artifact to rebuild\n * from. Runs after the status/error repaint above, so it wins over the\n * snapshot's own `complete` status.\n */\n private reportUnrestorableResult(): void {\n const error = new Error(GENERATION_UNRESTORABLE_RESULT_MESSAGE)\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Repaint a restored snapshot (client store or server hydrate) and, when it\n * reports a run still in flight, tail that run to completion via `joinRun`\n * (from the connection, or the `joinRun` option when the transport can't\n * carry one).\n *\n * A `running` snapshot that no `joinRun` handler can tail is repainted as an\n * interrupted error instead of a `generating` status that would never\n * settle: an interrupted generation cannot be resumed, only re-run.\n */\n private repaintRestoredSnapshot(\n snapshot: GenerationResumeSnapshot,\n activeRunId?: string,\n ): void {\n if (snapshot.status !== 'running') {\n this.repaintFromSnapshot(snapshot)\n return\n }\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n const runId = activeRunId ?? snapshot.resumeState?.runId\n if (runId && joinRun) {\n this.repaintFromSnapshot(snapshot)\n this.rejoinInFlight(runId)\n return\n }\n this.repaintFromSnapshot({\n ...snapshot,\n resumeState: null,\n status: 'error',\n error: {\n message:\n 'The previous generation was interrupted before it finished and cannot be resumed — generate again to retry.',\n },\n })\n }\n\n /**\n * Rebuild a `VideoGenerateResult` from a restored snapshot: the video's bytes\n * are served from the durable artifact URL, so the restored result renders\n * from your own origin. Returns `null` when there is no durable video artifact.\n */\n private reconstructVideoResult(\n snapshot: GenerationResumeSnapshot,\n ): VideoGenerateResult | null {\n const result = snapshot.result\n const artifacts = result?.artifacts ?? []\n const output = artifacts.find(\n (a) =>\n a.role === 'output' && a.source.mediaType === 'video' && a.url != null,\n )\n if (!output?.url) return null\n return {\n jobId: result?.providerJobId ?? '',\n status: 'completed',\n url: output.url,\n ...(result?.expiresAt ? { expiresAt: new Date(result.expiresAt) } : {}),\n artifacts,\n }\n }\n\n /**\n * The plain (non-Response) fetcher path never observes stream chunks, so\n * the terminal snapshot is built here from the fetcher's own result. A\n * stale `error` from a previous run is intentionally dropped — this run\n * succeeded.\n */\n private completePlainFetcherResumeSnapshot(rawResult: unknown): void {\n const previous = this.resumeSnapshot\n const result = createGenerationResultSnapshot(rawResult)\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'complete',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(result\n ? { result }\n : previous?.result\n ? { result: { ...previous.result } }\n : {}),\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Records a transport-level failure (network drop, throwing callback) in\n * the snapshot. Without this, only a server-emitted RUN_ERROR chunk would\n * mark the snapshot `error`, leaving a persisted record that claims the\n * run is still in flight.\n */\n private recordResumeSnapshotError(error: Error): void {\n // Surface the failure on the observable fields FIRST, unconditionally (see\n // the note in GenerationClient.recordResumeSnapshotError): a RUN_ERROR\n // already flipped the snapshot to `error`, so the early-return would else\n // skip this and leave `status` stuck on `generating`. The guard avoids a\n // duplicate `error` emission on the live `generate()` path.\n if (this.status !== 'error') this.setStatus('error')\n this.setError(error)\n if (this.resumeSnapshot?.status === 'error') return\n if (!this.resumeSnapshot && !this.serverDriven) return\n const previous = this.resumeSnapshot\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'error',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(previous?.result ? { result: { ...previous.result } } : {}),\n error: { message: error.message },\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Drop the client's in-memory snapshot and re-emit. Purely local — this\n * client writes no storage, so nothing persisted is removed.\n */\n private clearResumeSnapshot(): void {\n this.resumeSnapshot = undefined\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Server-driven mount hydration entry point (`persistence: true`). Runs at\n * most once, from the commit-phase mount path (`mountDevtools`) — never the\n * constructor / render phase — so remounts and speculative renders can't\n * re-fire the hydrate GET.\n */\n private maybeHydrateFromServer(): void {\n if (!this.serverDriven || this.serverHydrationStarted) return\n this.serverHydrationStarted = true\n if (this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler) {\n this.hydrateFromServer()\n } else {\n // `persistence: true` without any hydrate source can never restore\n // anything — warn rather than silently no-op.\n console.warn(\n '[TanStack AI] `persistence: true` (server-driven) needs a `hydrateGeneration` handler — either a connection that implements one (e.g. `fetchServerSentEvents` / `fetchHttpStream`, or `stream()` / `rpcStream()` with persistence handlers) or the `hydrateGeneration` option. Without one, nothing is persisted or restored.',\n )\n }\n }\n\n /**\n * Server-driven mount hydration (`persistence: true`). The client holds no\n * local snapshot; on mount it asks the server — keyed by the stable threadId —\n * for the last generation's resume snapshot, validates it, and repaints it. It\n * never auto-starts a run, and never blocks: a `generate()` that starts first\n * owns the client and hydration backs off, mirroring the chat client.\n *\n * A genuine **miss** (no record for the thread) is silent; a genuine\n * **failure** (transport error, authorize rejection, malformed body, a record\n * the validator rejects) surfaces through `status` / `error` / `onError` — see\n * the note on `GenerationClient.hydrateFromServer`.\n */\n private hydrateFromServer(): void {\n const hydrate =\n this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler\n if (!hydrate) return\n // A send that already started owns the client; don't stomp it.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n void (async () => {\n let res: GenerationHydrationResult\n try {\n res = await hydrate(this.threadId)\n } catch (cause) {\n this.failHydration(\n createGenerationHydrationError(\n 'the request to the server did not succeed',\n cause,\n ),\n )\n return\n }\n // No record for this thread — a fresh thread, not a failure.\n if (!res.resumeSnapshot) return\n const snapshot = parseGenerationResumeSnapshot(res.resumeSnapshot)\n if (!snapshot) {\n this.failHydration(\n createGenerationHydrationError(\n 'the server returned a record this client cannot read (unknown schema version, or a missing/invalid `status` or `resumeState`)',\n ),\n )\n return\n }\n // Re-check: a send may have started while the fetch was in flight.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle')\n return\n // A run still generating on the server: re-attach and finish it in place.\n this.repaintRestoredSnapshot(snapshot, res.activeRun?.runId)\n })()\n }\n\n /**\n * Surface a hydration failure on the observable fields. Skipped when a\n * `generate()` took ownership while the hydrate GET was in flight.\n */\n private failHydration(error: Error): void {\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Re-attach to an already-loaded `running` snapshot (remount case); see the\n * note in GenerationClient.maybeResumeInFlight. Guarded by `rejoinInFlight`.\n */\n private maybeResumeInFlight(): void {\n if (this.resumeSnapshot?.status !== 'running') return\n const runId = this.resumeSnapshot.resumeState?.runId\n if (runId) this.rejoinInFlight(runId)\n }\n\n /**\n * Re-attach to a video run still generating and stream it to completion,\n * mirroring the chat client's mount-time rejoin. Reuses `processStream`, so\n * the job status and result repaint from the replayed chunks. A live\n * `generate()` owns the client and is never stomped; a run is rejoined once.\n */\n private rejoinInFlight(runId: string): void {\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n if (!joinRun) return\n if (this.rejoinedRunId === runId) return\n if (this.isLoading || this.abortController) return\n this.rejoinedRunId = runId\n const controller = new AbortController()\n this.abortController = controller\n this.setIsLoading(true)\n this.setStatus('generating')\n void (async () => {\n try {\n await this.processStream(\n joinRun(runId, controller.signal),\n runId,\n controller.signal,\n )\n } catch (error) {\n if (!controller.signal.aborted) {\n const failure =\n error instanceof Error ? error : new Error(String(error))\n // Settles `status`/`error` AND rewrites the snapshot to a terminal\n // `error` with a null `resumeState`, so the next mount does not\n // rejoin this run again.\n this.recordResumeSnapshotError(failure)\n this.callbacksRef.onError?.(failure)\n }\n } finally {\n // Only reset if this rejoin still owns the client: a `stop()` +\n // fresh `generate()` may have replaced the controller while the tail\n // was settling, and that live run owns `isLoading` now.\n if (this.abortController === controller) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n })()\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI chat, realtime sessions, streaming transports, and media generations.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"./devtools": {
|
|
43
43
|
"types": "./dist/esm/devtools.d.ts",
|
|
44
44
|
"import": "./dist/esm/devtools.js"
|
|
45
|
+
},
|
|
46
|
+
"./byok": {
|
|
47
|
+
"types": "./dist/esm/byok.d.ts",
|
|
48
|
+
"import": "./dist/esm/byok.js"
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
"files": [
|
|
@@ -49,8 +53,8 @@
|
|
|
49
53
|
"src"
|
|
50
54
|
],
|
|
51
55
|
"dependencies": {
|
|
52
|
-
"@tanstack/ai": "^0.
|
|
53
|
-
"@tanstack/ai-event-client": "^0.
|
|
56
|
+
"@tanstack/ai": "^0.50.0",
|
|
57
|
+
"@tanstack/ai-event-client": "^0.11.0",
|
|
54
58
|
"@tanstack/ai-utils": "^0.4.0"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ByokBlockedError,
|
|
3
|
+
byokHeaderName,
|
|
4
|
+
isProviderId,
|
|
5
|
+
maskKey,
|
|
6
|
+
} from '@tanstack/ai/byok'
|
|
7
|
+
import { memoryStorage } from './storage'
|
|
8
|
+
import type { ProviderId } from '@tanstack/ai/byok'
|
|
9
|
+
import type { Keyring, KeyringStorage } from './storage'
|
|
10
|
+
|
|
11
|
+
export type KeyStatus =
|
|
12
|
+
| { state: 'empty' }
|
|
13
|
+
| { state: 'set'; masked: string }
|
|
14
|
+
| { state: 'locked'; masked: string }
|
|
15
|
+
| { state: 'error'; masked: string; message: string }
|
|
16
|
+
|
|
17
|
+
export type ByokPrompt = {
|
|
18
|
+
provider: ProviderId
|
|
19
|
+
reason: 'missing' | 'locked'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ByokSnapshot = {
|
|
23
|
+
status: Partial<Record<string, KeyStatus>>
|
|
24
|
+
locked: boolean
|
|
25
|
+
prompt: ByokPrompt | null
|
|
26
|
+
storageError: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DefineByokOptions {
|
|
30
|
+
storage?: KeyringStorage
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {
|
|
34
|
+
status: {},
|
|
35
|
+
locked: false,
|
|
36
|
+
prompt: null,
|
|
37
|
+
storageError: null,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireProviderId(value: string): ProviderId {
|
|
41
|
+
if (!isProviderId(value)) {
|
|
42
|
+
throw new Error(`Invalid BYOK provider id: ${value}`)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function errorMessage(error: unknown, fallback: string): string {
|
|
48
|
+
return error instanceof Error && error.message.length > 0
|
|
49
|
+
? error.message
|
|
50
|
+
: fallback
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isByokCeremonyCancel(error: unknown): boolean {
|
|
54
|
+
if (
|
|
55
|
+
typeof DOMException !== 'undefined' &&
|
|
56
|
+
error instanceof DOMException &&
|
|
57
|
+
error.name === 'NotAllowedError'
|
|
58
|
+
) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
if (error.name === 'NotAllowedError') return true
|
|
63
|
+
const message = error.message.toLowerCase()
|
|
64
|
+
return message.includes('cancelled') || message.includes('not allowed')
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitizeKeyring(value: unknown): Keyring {
|
|
70
|
+
if (typeof value !== 'object' || value === null) return {}
|
|
71
|
+
const keys: Keyring = {}
|
|
72
|
+
for (const [provider, key] of Object.entries(value)) {
|
|
73
|
+
if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {
|
|
74
|
+
keys[provider] = key
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return keys
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class ByokClient {
|
|
81
|
+
readonly storage: KeyringStorage
|
|
82
|
+
#keys: Keyring = {}
|
|
83
|
+
#statuses: Partial<Record<string, KeyStatus>> = {}
|
|
84
|
+
#locked: boolean
|
|
85
|
+
#prompt: ByokPrompt | null = null
|
|
86
|
+
#coverageAll = false
|
|
87
|
+
#coverage: Record<string, boolean> = {}
|
|
88
|
+
readonly #listeners = new Set<() => void>()
|
|
89
|
+
#snapshot: ByokSnapshot
|
|
90
|
+
#storageError: string | null = null
|
|
91
|
+
readonly #ready: Promise<void>
|
|
92
|
+
|
|
93
|
+
constructor(options: DefineByokOptions = {}) {
|
|
94
|
+
this.storage = options.storage ?? memoryStorage()
|
|
95
|
+
this.#locked = Boolean(this.storage.unlockable)
|
|
96
|
+
this.#snapshot = this.#buildSnapshot()
|
|
97
|
+
this.#ready = this.#hydrate()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolves when constructor hydration (peek/load) finishes. */
|
|
101
|
+
ready = (): Promise<void> => this.#ready
|
|
102
|
+
|
|
103
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
104
|
+
this.#listeners.add(listener)
|
|
105
|
+
return () => {
|
|
106
|
+
this.#listeners.delete(listener)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSnapshot = (): ByokSnapshot => this.#snapshot
|
|
111
|
+
|
|
112
|
+
#buildSnapshot(): ByokSnapshot {
|
|
113
|
+
return {
|
|
114
|
+
status: { ...this.#statuses },
|
|
115
|
+
locked: this.#locked,
|
|
116
|
+
prompt: this.#prompt,
|
|
117
|
+
storageError: this.#storageError,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
keys(): Keyring {
|
|
122
|
+
return { ...this.#keys }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
request(provider: ProviderId, reason: ByokPrompt['reason']): void {
|
|
126
|
+
this.#prompt = { provider: requireProviderId(provider), reason }
|
|
127
|
+
this.#emit()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `true` — the server can fill any slug from env.
|
|
132
|
+
* `false` — no env coverage.
|
|
133
|
+
* A record merges per-id flags and clears the all/none boolean.
|
|
134
|
+
*/
|
|
135
|
+
setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void {
|
|
136
|
+
if (typeof flags === 'boolean') {
|
|
137
|
+
this.#coverageAll = flags
|
|
138
|
+
this.#coverage = {}
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
this.#coverageAll = false
|
|
142
|
+
this.#coverage = { ...this.#coverage, ...flags }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#hasCoverage(provider: string): boolean {
|
|
146
|
+
return this.#coverageAll || this.#coverage[provider] === true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
headers(provider?: ProviderId): Record<string, string> {
|
|
150
|
+
const headers: Record<string, string> = {}
|
|
151
|
+
if (provider) {
|
|
152
|
+
requireProviderId(provider)
|
|
153
|
+
const key = this.#keys[provider]
|
|
154
|
+
if (key) headers[byokHeaderName(provider)] = key
|
|
155
|
+
return headers
|
|
156
|
+
}
|
|
157
|
+
for (const [id, key] of Object.entries(this.#keys)) {
|
|
158
|
+
if (key) headers[byokHeaderName(id)] = key
|
|
159
|
+
}
|
|
160
|
+
return headers
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async prepare(provider?: ProviderId): Promise<void> {
|
|
164
|
+
await this.#ready
|
|
165
|
+
if (this.storage.unlockable && this.#locked) {
|
|
166
|
+
await this.unlock()
|
|
167
|
+
}
|
|
168
|
+
if (!provider) return
|
|
169
|
+
requireProviderId(provider)
|
|
170
|
+
if (this.#keys[provider]) return
|
|
171
|
+
if (this.#hasCoverage(provider)) return
|
|
172
|
+
this.request(provider, 'missing')
|
|
173
|
+
throw new ByokBlockedError(provider, 'missing')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async update(
|
|
177
|
+
providerOrKey: ProviderId | string,
|
|
178
|
+
key?: string,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
await this.#ready
|
|
181
|
+
let provider: ProviderId
|
|
182
|
+
let nextKey: string
|
|
183
|
+
if (key === undefined) {
|
|
184
|
+
if (!this.#prompt) {
|
|
185
|
+
throw new Error('byok.update(key) cannot run when prompt is null')
|
|
186
|
+
}
|
|
187
|
+
provider = this.#prompt.provider
|
|
188
|
+
nextKey = providerOrKey
|
|
189
|
+
} else {
|
|
190
|
+
provider = requireProviderId(providerOrKey)
|
|
191
|
+
nextKey = key
|
|
192
|
+
}
|
|
193
|
+
nextKey = nextKey.trim()
|
|
194
|
+
if (nextKey.length === 0) {
|
|
195
|
+
throw new Error('BYOK key must be non-empty')
|
|
196
|
+
}
|
|
197
|
+
if (this.storage.unlockable && this.#locked) {
|
|
198
|
+
await this.unlock()
|
|
199
|
+
}
|
|
200
|
+
const previousKeys = this.#keys
|
|
201
|
+
const previousStatuses = { ...this.#statuses }
|
|
202
|
+
const previousLocked = this.#locked
|
|
203
|
+
const previousPrompt = this.#prompt
|
|
204
|
+
const previousStorageError = this.#storageError
|
|
205
|
+
const next = { ...this.#keys, [provider]: nextKey }
|
|
206
|
+
try {
|
|
207
|
+
await this.storage.save(next)
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.#keys = previousKeys
|
|
210
|
+
this.#statuses = previousStatuses
|
|
211
|
+
this.#locked = previousLocked
|
|
212
|
+
this.#prompt = previousPrompt
|
|
213
|
+
this.#storageError = previousStorageError
|
|
214
|
+
this.#statuses[provider] = {
|
|
215
|
+
state: 'error',
|
|
216
|
+
masked: maskKey(nextKey),
|
|
217
|
+
message: errorMessage(error, `Failed to persist ${provider} key`),
|
|
218
|
+
}
|
|
219
|
+
this.#emit()
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
this.#keys = next
|
|
223
|
+
this.#statuses[provider] = { state: 'set', masked: maskKey(nextKey) }
|
|
224
|
+
this.#locked = false
|
|
225
|
+
this.#prompt = null
|
|
226
|
+
this.#storageError = null
|
|
227
|
+
this.#emit()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async clear(provider?: ProviderId): Promise<void> {
|
|
231
|
+
await this.#ready
|
|
232
|
+
if (provider) {
|
|
233
|
+
requireProviderId(provider)
|
|
234
|
+
if (this.storage.unlockable && this.#locked) {
|
|
235
|
+
await this.unlock()
|
|
236
|
+
}
|
|
237
|
+
const previousKeys = this.#keys
|
|
238
|
+
const previousStatuses = { ...this.#statuses }
|
|
239
|
+
const next = { ...this.#keys }
|
|
240
|
+
delete next[provider]
|
|
241
|
+
try {
|
|
242
|
+
await this.storage.save(next)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.#keys = previousKeys
|
|
245
|
+
this.#statuses = previousStatuses
|
|
246
|
+
this.#emit()
|
|
247
|
+
throw error
|
|
248
|
+
}
|
|
249
|
+
this.#keys = next
|
|
250
|
+
delete this.#statuses[provider]
|
|
251
|
+
this.#emit()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
const previousKeys = this.#keys
|
|
255
|
+
const previousStatuses = this.#statuses
|
|
256
|
+
const previousLocked = this.#locked
|
|
257
|
+
const previousPrompt = this.#prompt
|
|
258
|
+
try {
|
|
259
|
+
await this.storage.clear()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
this.#keys = previousKeys
|
|
262
|
+
this.#statuses = previousStatuses
|
|
263
|
+
this.#locked = previousLocked
|
|
264
|
+
this.#prompt = previousPrompt
|
|
265
|
+
this.#emit()
|
|
266
|
+
throw error
|
|
267
|
+
}
|
|
268
|
+
this.#keys = {}
|
|
269
|
+
this.#statuses = {}
|
|
270
|
+
this.#locked = false
|
|
271
|
+
this.#prompt = null
|
|
272
|
+
this.#storageError = null
|
|
273
|
+
this.#emit()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async unlock(): Promise<void> {
|
|
277
|
+
await this.#ready
|
|
278
|
+
if (!this.storage.unlockable) return
|
|
279
|
+
try {
|
|
280
|
+
const loaded = sanitizeKeyring(await this.storage.load())
|
|
281
|
+
this.#keys = { ...loaded, ...this.#keys }
|
|
282
|
+
for (const [id, value] of Object.entries(loaded)) {
|
|
283
|
+
if (!isProviderId(id) || !value) continue
|
|
284
|
+
const existing = this.#statuses[id]
|
|
285
|
+
if (!existing || existing.state === 'locked') {
|
|
286
|
+
this.#statuses[id] = { state: 'set', masked: maskKey(value) }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.#locked = false
|
|
290
|
+
this.#storageError = null
|
|
291
|
+
this.#emit()
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (isByokCeremonyCancel(error)) {
|
|
294
|
+
const provider = this.#lockedProvider()
|
|
295
|
+
if (provider) {
|
|
296
|
+
this.request(provider, 'locked')
|
|
297
|
+
throw new ByokBlockedError(provider, 'locked')
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
this.#storageError = errorMessage(error, 'Failed to unlock keyring')
|
|
301
|
+
this.#emit()
|
|
302
|
+
throw error instanceof Error ? error : new Error(String(error))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#lockedProvider(): ProviderId | undefined {
|
|
307
|
+
if (this.#prompt && isProviderId(this.#prompt.provider)) {
|
|
308
|
+
return this.#prompt.provider
|
|
309
|
+
}
|
|
310
|
+
for (const [id, status] of Object.entries(this.#statuses)) {
|
|
311
|
+
if (status?.state === 'locked' && isProviderId(id)) return id
|
|
312
|
+
}
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async #hydrate(): Promise<void> {
|
|
317
|
+
if (this.storage.unlockable) {
|
|
318
|
+
if (!this.storage.peek) return
|
|
319
|
+
try {
|
|
320
|
+
const preview = await this.storage.peek()
|
|
321
|
+
for (const [id, last4] of Object.entries(preview)) {
|
|
322
|
+
if (!isProviderId(id) || this.#statuses[id]) continue
|
|
323
|
+
this.#statuses[id] = {
|
|
324
|
+
state: 'locked',
|
|
325
|
+
masked: last4 ? last4 : '••',
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.#locked = Object.keys(preview).length > 0
|
|
329
|
+
this.#storageError = null
|
|
330
|
+
this.#emit()
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.#storageError = errorMessage(error, 'Failed to read keyring')
|
|
333
|
+
this.#emit()
|
|
334
|
+
}
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const loaded = sanitizeKeyring(await this.storage.load())
|
|
339
|
+
this.#keys = { ...loaded, ...this.#keys }
|
|
340
|
+
for (const [id, value] of Object.entries(loaded)) {
|
|
341
|
+
if (!isProviderId(id) || !value) continue
|
|
342
|
+
this.#statuses[id] = { state: 'set', masked: maskKey(value) }
|
|
343
|
+
}
|
|
344
|
+
this.#storageError = null
|
|
345
|
+
this.#emit()
|
|
346
|
+
} catch (error) {
|
|
347
|
+
this.#storageError = errorMessage(error, 'Failed to read keyring')
|
|
348
|
+
this.#emit()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#emit(): void {
|
|
353
|
+
this.#snapshot = this.#buildSnapshot()
|
|
354
|
+
for (const listener of this.#listeners) listener()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function defineByok(options: DefineByokOptions = {}): ByokClient {
|
|
359
|
+
return new ByokClient(options)
|
|
360
|
+
}
|