@tanstack/ai-client 0.11.1 → 0.11.3
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/chat-client.d.ts +9 -9
- package/dist/esm/chat-client.js +41 -26
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +8 -8
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/events.js +1 -0
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -3
- package/dist/esm/generation-client.js +9 -5
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/realtime-client.d.ts +3 -3
- package/dist/esm/realtime-client.js +56 -51
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/sse-parser.js +1 -4
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +3 -3
- package/dist/esm/video-generation-client.js +11 -7
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +20 -14
- package/src/connection-adapters.ts +14 -11
- package/src/generation-client.ts +18 -12
- package/src/generation-types.ts +1 -1
- package/src/realtime-client.ts +53 -43
- package/src/sse-parser.ts +1 -6
- package/src/video-generation-client.ts +24 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-generation-client.js","sources":["../../src/video-generation-client.ts"],"sourcesContent":["import { GENERATION_EVENTS } from './generation-types'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\nimport type {\n GenerationClientState,\n GenerationFetcher,\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 */\ninterface VideoCallbacks<TOutput> {\n onResult?: (result: VideoGenerateResult) => TOutput | null | void\n onError?: (error: Error) => void\n onProgress?: (progress: number, message?: string) => void\n onChunk?: (chunk: StreamChunk) => void\n onJobCreated?: (jobId: string) => void\n onStatusUpdate?: (status: VideoStatusInfo) => void\n onResultChange?: (result: TOutput | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onErrorChange?: (error: Error | undefined) => void\n onStatusChange?: (status: GenerationClientState) => void\n onJobIdChange?: (jobId: string | null) => void\n onVideoStatusChange?: (status: VideoStatusInfo | null) => void\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 connection: ConnectConnectionAdapter | undefined\n private fetcher:\n | GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n | undefined\n private body: Record<string, any>\n\n private result: TOutput | 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 abortController: AbortController | null = null\n private callbacksRef: VideoCallbacks<TOutput>\n\n constructor(\n options: VideoGenerationClientOptions<TOutput> &\n (\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n connection?: never\n }\n ),\n ) {\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.body = options.body ?? {}\n\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 }\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.isLoading) return\n\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)\n } else if (this.connection) {\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect([], mergedData, signal)\n await this.processStream(stream)\n } else {\n throw new Error(\n 'VideoGenerationClient requires either a connection or fetcher option',\n )\n }\n } catch (err: any) {\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.callbacksRef.onError?.(error)\n } finally {\n this.abortController = null\n this.setIsLoading(false)\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 ): 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))\n } else {\n this.setResult(result)\n this.setStatus('success')\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 private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<void> {\n for await (const chunk of source) {\n if (this.abortController?.signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\n\n switch (chunk.type) {\n case 'CUSTOM': {\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.callbacksRef.onProgress?.(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.callbacksRef.onProgress?.(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n // Prefer spec `message`; fall back to deprecated `error.message`\n const msg =\n (chunk.message as string | undefined) ||\n chunk.error?.message ||\n 'An error occurred'\n throw new Error(msg)\n }\n }\n }\n }\n\n /**\n * Abort any in-flight generation or polling.\n */\n stop(): void {\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 }\n }\n\n /**\n * Clear all state and return to idle.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.setJobId(null)\n this.setVideoStatus(null)\n this.setError(undefined)\n this.setStatus('idle')\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 // ===========================\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 // ===========================\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 return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged\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 return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n }\n\n private setJobId(jobId: string | null): void {\n this.jobId = jobId\n this.callbacksRef.onJobIdChange?.(jobId)\n }\n\n private setVideoStatus(status: VideoStatusInfo | null): void {\n this.videoStatus = status\n this.callbacksRef.onVideoStatusChange?.(status)\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video-generation-client.js","sources":["../../src/video-generation-client.ts"],"sourcesContent":["import { GENERATION_EVENTS } from './generation-types'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\nimport type {\n GenerationClientState,\n GenerationFetcher,\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}\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 private readonly fetcher:\n | GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n | undefined\n private body: Record<string, any>\n\n private result: TOutput | 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 abortController: AbortController | null = null\n private readonly callbacksRef: VideoCallbacks<TOutput>\n\n constructor(\n options: VideoGenerationClientOptions<TOutput> &\n (\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n connection?: never\n }\n ),\n ) {\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.body = options.body ?? {}\n\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 }\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.isLoading) return\n\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)\n } else if (this.connection) {\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect([], mergedData, signal)\n await this.processStream(stream)\n } else {\n throw new Error(\n 'VideoGenerationClient requires either a connection or fetcher option',\n )\n }\n } catch (err: any) {\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.callbacksRef.onError?.(error)\n } finally {\n this.abortController = null\n this.setIsLoading(false)\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 ): 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))\n } else {\n this.setResult(result)\n this.setStatus('success')\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 private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<void> {\n for await (const chunk of source) {\n if (this.abortController?.signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\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 'CUSTOM': {\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.callbacksRef.onProgress?.(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.callbacksRef.onProgress?.(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n // Prefer spec `message`; fall back to deprecated `error.message`\n const msg =\n (chunk.message as string | undefined) ||\n chunk.error?.message ||\n 'An error occurred'\n throw new Error(msg)\n }\n default:\n break\n }\n }\n }\n\n /**\n * Abort any in-flight generation or polling.\n */\n stop(): void {\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 }\n }\n\n /**\n * Clear all state and return to idle.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.setJobId(null)\n this.setVideoStatus(null)\n this.setError(undefined)\n this.setStatus('idle')\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 // ===========================\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 // ===========================\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 return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged\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 return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value\n this.result = rawResult as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n }\n\n private setJobId(jobId: string | null): void {\n this.jobId = jobId\n this.callbacksRef.onJobIdChange?.(jobId)\n }\n\n private setVideoStatus(status: VideoStatusInfo | null): void {\n this.videoStatus = status\n this.callbacksRef.onVideoStatusChange?.(status)\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai": "0.
|
|
35
|
-
"@tanstack/ai-event-client": "0.3.
|
|
34
|
+
"@tanstack/ai": "0.20.1",
|
|
35
|
+
"@tanstack/ai-event-client": "0.3.6"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@standard-schema/spec": "^1.1.0",
|
package/src/chat-client.ts
CHANGED
|
@@ -28,10 +28,10 @@ import type {
|
|
|
28
28
|
} from './types'
|
|
29
29
|
|
|
30
30
|
export class ChatClient {
|
|
31
|
-
private processor: StreamProcessor
|
|
31
|
+
private readonly processor: StreamProcessor
|
|
32
32
|
private connection: SubscribeConnectionAdapter
|
|
33
|
-
private uniqueId: string
|
|
34
|
-
private threadId: string
|
|
33
|
+
private readonly uniqueId: string
|
|
34
|
+
private readonly threadId: string
|
|
35
35
|
// Track the legacy `body` option and the canonical `forwardedProps`
|
|
36
36
|
// option as separate slots so that `updateOptions({ forwardedProps })`
|
|
37
37
|
// doesn't wipe a previously-set `body` (and vice versa). They are
|
|
@@ -45,13 +45,13 @@ export class ChatClient {
|
|
|
45
45
|
private status: ChatClientState = 'ready'
|
|
46
46
|
private connectionStatus: ConnectionStatus = 'disconnected'
|
|
47
47
|
private abortController: AbortController | null = null
|
|
48
|
-
private events: ChatClientEventEmitter
|
|
49
|
-
private clientToolsRef: { current: Map<string, AnyClientTool> }
|
|
48
|
+
private readonly events: ChatClientEventEmitter
|
|
49
|
+
private readonly clientToolsRef: { current: Map<string, AnyClientTool> }
|
|
50
50
|
private currentStreamId: string | null = null
|
|
51
51
|
private currentMessageId: string | null = null
|
|
52
|
-
private postStreamActions: Array<() => Promise<void>> = []
|
|
52
|
+
private readonly postStreamActions: Array<() => Promise<void>> = []
|
|
53
53
|
// Track pending client tool executions to await them before stream finalization
|
|
54
|
-
private pendingToolExecutions: Map<string, Promise<void>> = new Map()
|
|
54
|
+
private readonly pendingToolExecutions: Map<string, Promise<void>> = new Map()
|
|
55
55
|
// Flag to deduplicate continuation checks during action draining
|
|
56
56
|
private continuationPending = false
|
|
57
57
|
private subscriptionAbortController: AbortController | null = null
|
|
@@ -63,9 +63,9 @@ export class ChatClient {
|
|
|
63
63
|
private continuationSkipped = false
|
|
64
64
|
private draining = false
|
|
65
65
|
private sessionGenerating = false
|
|
66
|
-
private activeRunIds = new Set<string>()
|
|
66
|
+
private readonly activeRunIds = new Set<string>()
|
|
67
67
|
|
|
68
|
-
private callbacksRef: {
|
|
68
|
+
private readonly callbacksRef: {
|
|
69
69
|
current: {
|
|
70
70
|
onResponse: (response?: Response) => void | Promise<void>
|
|
71
71
|
onChunk: (chunk: StreamChunk) => void
|
|
@@ -126,10 +126,16 @@ export class ChatClient {
|
|
|
126
126
|
},
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Create StreamProcessor with event handlers
|
|
129
|
+
// Create StreamProcessor with event handlers.
|
|
130
|
+
// Use conditional spreads so we don't pass `undefined` into
|
|
131
|
+
// `StreamProcessorOptions` fields under `exactOptionalPropertyTypes`.
|
|
130
132
|
this.processor = new StreamProcessor({
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
...(options.streamProcessor?.chunkStrategy
|
|
134
|
+
? { chunkStrategy: options.streamProcessor.chunkStrategy }
|
|
135
|
+
: {}),
|
|
136
|
+
...(options.initialMessages
|
|
137
|
+
? { initialMessages: options.initialMessages }
|
|
138
|
+
: {}),
|
|
133
139
|
events: {
|
|
134
140
|
onMessagesChange: (messages: Array<UIMessage>) => {
|
|
135
141
|
this.callbacksRef.current.onMessagesChange(messages)
|
|
@@ -911,8 +917,8 @@ export class ChatClient {
|
|
|
911
917
|
if (this.draining) return
|
|
912
918
|
this.draining = true
|
|
913
919
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
920
|
+
let action: (() => Promise<void>) | undefined
|
|
921
|
+
while ((action = this.postStreamActions.shift()) !== undefined) {
|
|
916
922
|
await action()
|
|
917
923
|
}
|
|
918
924
|
} finally {
|
|
@@ -35,13 +35,7 @@ async function* readStreamLines(
|
|
|
35
35
|
const decoder = new TextDecoder()
|
|
36
36
|
let buffer = ''
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
while (true) {
|
|
40
|
-
// Check if aborted before reading
|
|
41
|
-
if (abortSignal?.aborted) {
|
|
42
|
-
break
|
|
43
|
-
}
|
|
44
|
-
|
|
38
|
+
while (!abortSignal?.aborted) {
|
|
45
39
|
const { done, value } = await reader.read()
|
|
46
40
|
if (done) break
|
|
47
41
|
|
|
@@ -183,8 +177,9 @@ export function normalizeConnectionAdapter(
|
|
|
183
177
|
return (async function* () {
|
|
184
178
|
while (!abortSignal?.aborted) {
|
|
185
179
|
let chunk: StreamChunk | null
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
const buffered = myBuffer.shift()
|
|
181
|
+
if (buffered !== undefined) {
|
|
182
|
+
chunk = buffered
|
|
188
183
|
} else {
|
|
189
184
|
chunk = await new Promise<StreamChunk | null>((resolve) => {
|
|
190
185
|
const onAbort = () => resolve(null)
|
|
@@ -351,12 +346,16 @@ export function fetchServerSentEvents(
|
|
|
351
346
|
}
|
|
352
347
|
|
|
353
348
|
const fetchClient = resolvedOptions.fetchClient ?? fetch
|
|
349
|
+
// `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`
|
|
350
|
+
// under `exactOptionalPropertyTypes`), so spread it conditionally
|
|
351
|
+
// rather than passing `undefined` explicitly.
|
|
352
|
+
const signal = abortSignal || resolvedOptions.signal
|
|
354
353
|
const response = await fetchClient(resolvedUrl, {
|
|
355
354
|
method: 'POST',
|
|
356
355
|
headers: requestHeaders,
|
|
357
356
|
body: JSON.stringify(requestBody),
|
|
358
357
|
credentials: resolvedOptions.credentials || 'same-origin',
|
|
359
|
-
signal
|
|
358
|
+
...(signal ? { signal } : {}),
|
|
360
359
|
})
|
|
361
360
|
|
|
362
361
|
if (!response.ok) {
|
|
@@ -483,12 +482,16 @@ export function fetchHttpStream(
|
|
|
483
482
|
}
|
|
484
483
|
|
|
485
484
|
const fetchClient = resolvedOptions.fetchClient ?? fetch
|
|
485
|
+
// `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`
|
|
486
|
+
// under `exactOptionalPropertyTypes`), so spread it conditionally
|
|
487
|
+
// rather than passing `undefined` explicitly.
|
|
488
|
+
const signal = abortSignal || resolvedOptions.signal
|
|
486
489
|
const response = await fetchClient(resolvedUrl, {
|
|
487
490
|
method: 'POST',
|
|
488
491
|
headers: requestHeaders,
|
|
489
492
|
body: JSON.stringify(requestBody),
|
|
490
493
|
credentials: resolvedOptions.credentials || 'same-origin',
|
|
491
|
-
signal
|
|
494
|
+
...(signal ? { signal } : {}),
|
|
492
495
|
})
|
|
493
496
|
|
|
494
497
|
if (!response.ok) {
|
package/src/generation-client.ts
CHANGED
|
@@ -11,15 +11,18 @@ import type {
|
|
|
11
11
|
/**
|
|
12
12
|
* Callbacks stored in a ref so hooks can update them without recreating the client.
|
|
13
13
|
*/
|
|
14
|
+
// All optional fields explicitly allow `| undefined` so callers can spread
|
|
15
|
+
// option bags (where each callback may be `undefined`) into the callbacks
|
|
16
|
+
// ref under `exactOptionalPropertyTypes`.
|
|
14
17
|
interface GenerationCallbacks<TResult, TOutput> {
|
|
15
|
-
onResult?: (result: TResult) => TOutput | null | void
|
|
16
|
-
onError?: (error: Error) => void
|
|
17
|
-
onProgress?: (progress: number, message?: string) => void
|
|
18
|
-
onChunk?: (chunk: StreamChunk) => void
|
|
19
|
-
onResultChange?: (result: TOutput | null) => void
|
|
20
|
-
onLoadingChange?: (isLoading: boolean) => void
|
|
21
|
-
onErrorChange?: (error: Error | undefined) => void
|
|
22
|
-
onStatusChange?: (status: GenerationClientState) => void
|
|
18
|
+
onResult?: ((result: TResult) => TOutput | null | void) | undefined
|
|
19
|
+
onError?: ((error: Error) => void) | undefined
|
|
20
|
+
onProgress?: ((progress: number, message?: string) => void) | undefined
|
|
21
|
+
onChunk?: ((chunk: StreamChunk) => void) | undefined
|
|
22
|
+
onResultChange?: ((result: TOutput | null) => void) | undefined
|
|
23
|
+
onLoadingChange?: ((isLoading: boolean) => void) | undefined
|
|
24
|
+
onErrorChange?: ((error: Error | undefined) => void) | undefined
|
|
25
|
+
onStatusChange?: ((status: GenerationClientState) => void) | undefined
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -62,15 +65,15 @@ export class GenerationClient<
|
|
|
62
65
|
TResult,
|
|
63
66
|
TOutput = TResult,
|
|
64
67
|
> {
|
|
65
|
-
private connection: ConnectConnectionAdapter | undefined
|
|
66
|
-
private fetcher: GenerationFetcher<TInput, TResult> | undefined
|
|
68
|
+
private readonly connection: ConnectConnectionAdapter | undefined
|
|
69
|
+
private readonly fetcher: GenerationFetcher<TInput, TResult> | undefined
|
|
67
70
|
private body: Record<string, any>
|
|
68
71
|
private result: TOutput | null = null
|
|
69
72
|
private isLoading = false
|
|
70
73
|
private error: Error | undefined = undefined
|
|
71
74
|
private status: GenerationClientState = 'idle'
|
|
72
75
|
private abortController: AbortController | null = null
|
|
73
|
-
private callbacksRef: GenerationCallbacks<TResult, TOutput>
|
|
76
|
+
private readonly callbacksRef: GenerationCallbacks<TResult, TOutput>
|
|
74
77
|
|
|
75
78
|
constructor(
|
|
76
79
|
options: GenerationClientOptions<TInput, TResult, TOutput> &
|
|
@@ -159,6 +162,7 @@ export class GenerationClient<
|
|
|
159
162
|
|
|
160
163
|
this.callbacksRef.onChunk?.(chunk)
|
|
161
164
|
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to generation lifecycle.
|
|
162
166
|
switch (chunk.type) {
|
|
163
167
|
case 'CUSTOM': {
|
|
164
168
|
if (chunk.name === GENERATION_EVENTS.RESULT) {
|
|
@@ -184,6 +188,8 @@ export class GenerationClient<
|
|
|
184
188
|
'An error occurred'
|
|
185
189
|
throw new Error(msg)
|
|
186
190
|
}
|
|
191
|
+
default:
|
|
192
|
+
break
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
}
|
|
@@ -286,7 +292,7 @@ export class GenerationClient<
|
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
// No onResult callback, or callback returned void → use raw value
|
|
289
|
-
this.result = rawResult as
|
|
295
|
+
this.result = rawResult as TOutput
|
|
290
296
|
this.callbacksRef.onResultChange?.(this.result)
|
|
291
297
|
}
|
|
292
298
|
|
package/src/generation-types.ts
CHANGED
|
@@ -98,7 +98,7 @@ export type GenerationTransport<TInput, TResult> =
|
|
|
98
98
|
* @template TResult - The result type returned by the generation
|
|
99
99
|
* @template TOutput - The output type after optional transform (defaults to TResult)
|
|
100
100
|
*/
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention -- _TInput is unused in the interface body but part of the public positional generic API (callers supply it for inference)
|
|
102
102
|
export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
103
103
|
/** Unique identifier for this generation client instance */
|
|
104
104
|
id?: string
|
package/src/realtime-client.ts
CHANGED
|
@@ -39,12 +39,13 @@ const TOKEN_REFRESH_BUFFER_MS = 60_000
|
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
41
|
export class RealtimeClient {
|
|
42
|
-
private options: RealtimeClientOptions
|
|
42
|
+
private readonly options: RealtimeClientOptions
|
|
43
43
|
private connection: RealtimeConnection | null = null
|
|
44
44
|
private token: RealtimeToken | null = null
|
|
45
45
|
private tokenRefreshTimeout: ReturnType<typeof setTimeout> | null = null
|
|
46
|
-
private clientTools: Map<string, AnyClientTool>
|
|
47
|
-
private stateChangeCallbacks: Set<RealtimeStateChangeCallback> =
|
|
46
|
+
private readonly clientTools: Map<string, AnyClientTool>
|
|
47
|
+
private readonly stateChangeCallbacks: Set<RealtimeStateChangeCallback> =
|
|
48
|
+
new Set()
|
|
48
49
|
private unsubscribers: Array<() => void> = []
|
|
49
50
|
|
|
50
51
|
private state: RealtimeClientState = {
|
|
@@ -168,7 +169,7 @@ export class RealtimeClient {
|
|
|
168
169
|
if (!this.connection || this.state.status !== 'connected') {
|
|
169
170
|
return
|
|
170
171
|
}
|
|
171
|
-
this.connection.startAudioCapture()
|
|
172
|
+
void this.connection.startAudioCapture()
|
|
172
173
|
this.updateState({ mode: 'listening' })
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -305,7 +306,7 @@ export class RealtimeClient {
|
|
|
305
306
|
* Call this when disposing of the client.
|
|
306
307
|
*/
|
|
307
308
|
destroy(): void {
|
|
308
|
-
this.disconnect()
|
|
309
|
+
void this.disconnect()
|
|
309
310
|
this.stateChangeCallbacks.clear()
|
|
310
311
|
}
|
|
311
312
|
|
|
@@ -344,7 +345,7 @@ export class RealtimeClient {
|
|
|
344
345
|
const refreshIn = Math.max(0, timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS)
|
|
345
346
|
|
|
346
347
|
this.tokenRefreshTimeout = setTimeout(() => {
|
|
347
|
-
this.refreshToken()
|
|
348
|
+
void this.refreshToken()
|
|
348
349
|
}, refreshIn)
|
|
349
350
|
}
|
|
350
351
|
|
|
@@ -407,20 +408,20 @@ export class RealtimeClient {
|
|
|
407
408
|
|
|
408
409
|
// Tool calls
|
|
409
410
|
this.unsubscribers.push(
|
|
410
|
-
this.connection.on(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
411
|
+
this.connection.on('tool_call', ({ toolCallId, toolName, input }) => {
|
|
412
|
+
if (!toolCallId) {
|
|
413
|
+
console.error(
|
|
414
|
+
'[RealtimeClient] tool_call missing toolCallId',
|
|
415
|
+
toolName,
|
|
416
|
+
)
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
const tool = this.clientTools.get(toolName)
|
|
420
|
+
const execute = tool?.execute
|
|
421
|
+
if (execute) {
|
|
422
|
+
void (async () => {
|
|
422
423
|
try {
|
|
423
|
-
const output = await
|
|
424
|
+
const output = await execute(input)
|
|
424
425
|
this.connection?.sendToolResult(
|
|
425
426
|
toolCallId,
|
|
426
427
|
typeof output === 'string' ? output : JSON.stringify(output),
|
|
@@ -433,16 +434,16 @@ export class RealtimeClient {
|
|
|
433
434
|
JSON.stringify({ error: errMsg }),
|
|
434
435
|
)
|
|
435
436
|
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
),
|
|
437
|
+
})()
|
|
438
|
+
} else {
|
|
439
|
+
this.connection?.sendToolResult(
|
|
440
|
+
toolCallId,
|
|
441
|
+
JSON.stringify({
|
|
442
|
+
error: `Unknown or non-executable tool: ${String(toolName)}`,
|
|
443
|
+
}),
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
}),
|
|
446
447
|
)
|
|
447
448
|
|
|
448
449
|
// Message complete
|
|
@@ -512,25 +513,34 @@ export class RealtimeClient {
|
|
|
512
513
|
semanticEagerness
|
|
513
514
|
if (!hasConfig) return
|
|
514
515
|
|
|
516
|
+
// `RealtimeToolConfig.inputSchema` is `Record<string, any>` (no
|
|
517
|
+
// `undefined` under `exactOptionalPropertyTypes`). Conditionally spread
|
|
518
|
+
// it so we don't pass `undefined` when the tool has no input schema.
|
|
515
519
|
const toolsConfig = tools
|
|
516
|
-
? Array.from(this.clientTools.values()).map((t) =>
|
|
517
|
-
|
|
518
|
-
description: t.description,
|
|
519
|
-
inputSchema: t.inputSchema
|
|
520
|
+
? Array.from(this.clientTools.values()).map((t) => {
|
|
521
|
+
const inputSchema = t.inputSchema
|
|
520
522
|
? convertSchemaToJsonSchema(t.inputSchema)
|
|
521
|
-
: undefined
|
|
522
|
-
|
|
523
|
+
: undefined
|
|
524
|
+
return {
|
|
525
|
+
name: t.name,
|
|
526
|
+
description: t.description,
|
|
527
|
+
...(inputSchema ? { inputSchema } : {}),
|
|
528
|
+
}
|
|
529
|
+
})
|
|
523
530
|
: undefined
|
|
524
531
|
|
|
532
|
+
// `RealtimeSessionConfig` declares each field as `field?: T` (no
|
|
533
|
+
// `undefined`). Spread each one conditionally so we don't violate
|
|
534
|
+
// `exactOptionalPropertyTypes` when the source is `T | undefined`.
|
|
525
535
|
this.connection.updateSession({
|
|
526
|
-
instructions,
|
|
527
|
-
voice,
|
|
528
|
-
vadMode,
|
|
529
|
-
tools: toolsConfig,
|
|
530
|
-
outputModalities,
|
|
531
|
-
temperature,
|
|
532
|
-
maxOutputTokens,
|
|
533
|
-
semanticEagerness,
|
|
536
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
537
|
+
...(voice !== undefined ? { voice } : {}),
|
|
538
|
+
...(vadMode !== undefined ? { vadMode } : {}),
|
|
539
|
+
...(toolsConfig !== undefined ? { tools: toolsConfig } : {}),
|
|
540
|
+
...(outputModalities !== undefined ? { outputModalities } : {}),
|
|
541
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
542
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
543
|
+
...(semanticEagerness !== undefined ? { semanticEagerness } : {}),
|
|
534
544
|
})
|
|
535
545
|
}
|
|
536
546
|
|
package/src/sse-parser.ts
CHANGED
|
@@ -11,12 +11,7 @@ async function* readStreamLines(
|
|
|
11
11
|
const decoder = new TextDecoder()
|
|
12
12
|
let buffer = ''
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
while (true) {
|
|
16
|
-
if (abortSignal?.aborted) {
|
|
17
|
-
break
|
|
18
|
-
}
|
|
19
|
-
|
|
14
|
+
while (!abortSignal?.aborted) {
|
|
20
15
|
const { done, value } = await reader.read()
|
|
21
16
|
if (done) break
|
|
22
17
|
|
|
@@ -14,19 +14,24 @@ import type {
|
|
|
14
14
|
/**
|
|
15
15
|
* Callbacks stored in a ref so hooks can update them without recreating the client.
|
|
16
16
|
*/
|
|
17
|
+
// All optional fields explicitly allow `| undefined` so callers can spread
|
|
18
|
+
// option bags (where each callback may be `undefined`) into the callbacks
|
|
19
|
+
// ref under `exactOptionalPropertyTypes`.
|
|
17
20
|
interface VideoCallbacks<TOutput> {
|
|
18
|
-
onResult?:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
onResult?:
|
|
22
|
+
| ((result: VideoGenerateResult) => TOutput | null | void)
|
|
23
|
+
| undefined
|
|
24
|
+
onError?: ((error: Error) => void) | undefined
|
|
25
|
+
onProgress?: ((progress: number, message?: string) => void) | undefined
|
|
26
|
+
onChunk?: ((chunk: StreamChunk) => void) | undefined
|
|
27
|
+
onJobCreated?: ((jobId: string) => void) | undefined
|
|
28
|
+
onStatusUpdate?: ((status: VideoStatusInfo) => void) | undefined
|
|
29
|
+
onResultChange?: ((result: TOutput | null) => void) | undefined
|
|
30
|
+
onLoadingChange?: ((isLoading: boolean) => void) | undefined
|
|
31
|
+
onErrorChange?: ((error: Error | undefined) => void) | undefined
|
|
32
|
+
onStatusChange?: ((status: GenerationClientState) => void) | undefined
|
|
33
|
+
onJobIdChange?: ((jobId: string | null) => void) | undefined
|
|
34
|
+
onVideoStatusChange?: ((status: VideoStatusInfo | null) => void) | undefined
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -65,8 +70,8 @@ interface VideoCallbacks<TOutput> {
|
|
|
65
70
|
* ```
|
|
66
71
|
*/
|
|
67
72
|
export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
68
|
-
private connection: ConnectConnectionAdapter | undefined
|
|
69
|
-
private fetcher:
|
|
73
|
+
private readonly connection: ConnectConnectionAdapter | undefined
|
|
74
|
+
private readonly fetcher:
|
|
70
75
|
| GenerationFetcher<VideoGenerateInput, VideoGenerateResult>
|
|
71
76
|
| undefined
|
|
72
77
|
private body: Record<string, any>
|
|
@@ -78,7 +83,7 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
78
83
|
private error: Error | undefined = undefined
|
|
79
84
|
private status: GenerationClientState = 'idle'
|
|
80
85
|
private abortController: AbortController | null = null
|
|
81
|
-
private callbacksRef: VideoCallbacks<TOutput>
|
|
86
|
+
private readonly callbacksRef: VideoCallbacks<TOutput>
|
|
82
87
|
|
|
83
88
|
constructor(
|
|
84
89
|
options: VideoGenerationClientOptions<TOutput> &
|
|
@@ -185,6 +190,7 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
185
190
|
|
|
186
191
|
this.callbacksRef.onChunk?.(chunk)
|
|
187
192
|
|
|
193
|
+
// 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.
|
|
188
194
|
switch (chunk.type) {
|
|
189
195
|
case 'CUSTOM': {
|
|
190
196
|
if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {
|
|
@@ -221,6 +227,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
221
227
|
'An error occurred'
|
|
222
228
|
throw new Error(msg)
|
|
223
229
|
}
|
|
230
|
+
default:
|
|
231
|
+
break
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
}
|
|
@@ -345,7 +353,7 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
345
353
|
}
|
|
346
354
|
|
|
347
355
|
// No onResult callback, or callback returned void → use raw value
|
|
348
|
-
this.result = rawResult as
|
|
356
|
+
this.result = rawResult as TOutput
|
|
349
357
|
this.callbacksRef.onResultChange?.(this.result)
|
|
350
358
|
}
|
|
351
359
|
|