@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.
@@ -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":";;AAkEO,MAAM,sBAAqD;AAAA,EAgBhE,YACE,SAQA;AAlBF,SAAQ,SAAyB;AACjC,SAAQ,QAAuB;AAC/B,SAAQ,cAAsC;AAC9C,SAAQ,YAAY;AACpB,SAAQ,QAA2B;AACnC,SAAQ,SAAgC;AACxC,SAAQ,kBAA0C;AAahD,SAAK,aAAa,QAAQ;AAC1B,SAAK,UAAU,QAAQ;AACvB,SAAK,OAAO,QAAQ,QAAQ,CAAA;AAE5B,SAAK,eAAe;AAAA,MAClB,UAAU,QAAQ;AAAA,MAClB,SAAS,QAAQ;AAAA,MACjB,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,cAAc,QAAQ;AAAA,MACtB,gBAAgB,QAAQ;AAAA,MACxB,gBAAgB,QAAQ;AAAA,MACxB,iBAAiB,QAAQ;AAAA,MACzB,eAAe,QAAQ;AAAA,MACvB,gBAAgB,QAAQ;AAAA,MACxB,eAAe,QAAQ;AAAA,MACvB,qBAAqB,QAAQ;AAAA,IAAA;AAAA,EAEjC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,OAA0C;AACvD,QAAI,KAAK,UAAW;AAEpB,SAAK,aAAa,IAAI;AACtB,SAAK,UAAU,YAAY;AAC3B,SAAK,SAAS,MAAS;AACvB,SAAK,SAAS,IAAI;AAClB,SAAK,eAAe,IAAI;AAExB,UAAM,kBAAkB,IAAI,gBAAA;AAC5B,SAAK,kBAAkB;AACvB,UAAM,EAAE,WAAW;AAEnB,QAAI;AACF,UAAI,KAAK,SAAS;AAChB,cAAM,KAAK,oBAAoB,OAAO,MAAM;AAAA,MAC9C,WAAW,KAAK,YAAY;AAC1B,cAAM,aAAa,EAAE,GAAG,KAAK,MAAM,GAAG,MAAA;AACtC,cAAM,SAAS,KAAK,WAAW,QAAQ,CAAA,GAAI,YAAY,MAAM;AAC7D,cAAM,KAAK,cAAc,MAAM;AAAA,MACjC,OAAO;AACL,cAAM,IAAI;AAAA,UACR;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF,SAAS,KAAU;AACjB,UAAI,OAAO,QAAS;AACpB,YAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,SAAS,KAAK;AACnB,WAAK,UAAU,OAAO;AACtB,WAAK,aAAa,UAAU,KAAK;AAAA,IACnC,UAAA;AACE,WAAK,kBAAkB;AACvB,WAAK,aAAa,KAAK;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBACZ,OACA,QACe;AACf,QAAI,CAAC,KAAK,QAAS;AAGnB,UAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,EAAE,QAAQ;AACnD,QAAI,OAAO,QAAS;AAEpB,QAAI,kBAAkB,UAAU;AAE9B,YAAM,KAAK,cAAc,iBAAiB,QAAQ,MAAM,CAAC;AAAA,IAC3D,OAAO;AACL,WAAK,UAAU,MAAM;AACrB,WAAK,UAAU,SAAS;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,cACZ,QACe;AACf,qBAAiB,SAAS,QAAQ;AAChC,UAAI,KAAK,iBAAiB,OAAO,QAAS;AAE1C,WAAK,aAAa,UAAU,KAAK;AAEjC,cAAQ,MAAM,MAAA;AAAA,QACZ,KAAK,UAAU;AACb,cAAI,MAAM,SAAS,kBAAkB,mBAAmB;AACtD,kBAAM,EAAE,UAAU,MAAM;AACxB,iBAAK,SAAS,KAAK;AACnB,iBAAK,aAAa,eAAe,KAAK;AAAA,UACxC,WAAW,MAAM,SAAS,kBAAkB,cAAc;AACxD,kBAAM,aAAa,MAAM;AACzB,iBAAK,eAAe,UAAU;AAC9B,iBAAK,aAAa,iBAAiB,UAAU;AAC7C,gBAAI,WAAW,aAAa,QAAW;AACrC,mBAAK,aAAa,aAAa,WAAW,QAAQ;AAAA,YACpD;AAAA,UACF,WAAW,MAAM,SAAS,kBAAkB,QAAQ;AAClD,iBAAK,UAAU,MAAM,KAA4B;AAAA,UACnD,WAAW,MAAM,SAAS,kBAAkB,UAAU;AACpD,kBAAM,EAAE,UAAU,QAAA,IAAY,MAAM;AAIpC,iBAAK,aAAa,aAAa,UAAU,OAAO;AAAA,UAClD;AACA;AAAA,QACF;AAAA,QACA,KAAK,gBAAgB;AACnB,eAAK,UAAU,SAAS;AACxB;AAAA,QACF;AAAA,QACA,KAAK,aAAa;AAEhB,gBAAM,MACH,MAAM,WACP,MAAM,OAAO,WACb;AACF,gBAAM,IAAI,MAAM,GAAG;AAAA,QACrB;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,iBAAiB;AACxB,WAAK,gBAAgB,MAAA;AACrB,WAAK,kBAAkB;AAAA,IACzB;AACA,SAAK,aAAa,KAAK;AACvB,QAAI,KAAK,WAAW,cAAc;AAChC,WAAK,UAAU,MAAM;AAAA,IACvB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,KAAA;AACL,SAAK,UAAU,IAAI;AACnB,SAAK,SAAS,IAAI;AAClB,SAAK,eAAe,IAAI;AACxB,SAAK,SAAS,MAAS;AACvB,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,cACE,SAYM;AACN,QAAI,QAAQ,SAAS,QAAW;AAC9B,WAAK,OAAO,QAAQ,QAAQ,CAAA;AAAA,IAC9B;AACA,QAAI,QAAQ,aAAa,QAAW;AAClC,WAAK,aAAa,WAAW,QAAQ;AAAA,IACvC;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,UAAU,QAAQ;AAAA,IACtC;AACA,QAAI,QAAQ,eAAe,QAAW;AACpC,WAAK,aAAa,aAAa,QAAQ;AAAA,IACzC;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,UAAU,QAAQ;AAAA,IACtC;AACA,QAAI,QAAQ,iBAAiB,QAAW;AACtC,WAAK,aAAa,eAAe,QAAQ;AAAA,IAC3C;AACA,QAAI,QAAQ,mBAAmB,QAAW;AACxC,WAAK,aAAa,iBAAiB,QAAQ;AAAA,IAC7C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,YAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAA0B;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,iBAAyC;AACvC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,eAAwB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAmC;AACjC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMQ,UAAU,WAA6C;AAC7D,QAAI,cAAc,MAAM;AACtB,WAAK,SAAS;AACd,WAAK,aAAa,iBAAiB,IAAI;AACvC;AAAA,IACF;AAEA,QAAI,KAAK,aAAa,UAAU;AAC9B,YAAM,cAAc,KAAK,aAAa,SAAS,SAAS;AACxD,UAAI,gBAAgB,MAAM;AAExB;AAAA,MACF;AACA,UAAI,gBAAgB,QAAW;AAE7B,aAAK,SAAS;AACd,aAAK,aAAa,iBAAiB,KAAK,MAAM;AAC9C;AAAA,MACF;AAAA,IACF;AAGA,SAAK,SAAS;AACd,SAAK,aAAa,iBAAiB,KAAK,MAAM;AAAA,EAChD;AAAA,EAEQ,SAAS,OAA4B;AAC3C,SAAK,QAAQ;AACb,SAAK,aAAa,gBAAgB,KAAK;AAAA,EACzC;AAAA,EAEQ,eAAe,QAAsC;AAC3D,SAAK,cAAc;AACnB,SAAK,aAAa,sBAAsB,MAAM;AAAA,EAChD;AAAA,EAEQ,aAAa,WAA0B;AAC7C,SAAK,YAAY;AACjB,SAAK,aAAa,kBAAkB,SAAS;AAAA,EAC/C;AAAA,EAEQ,SAAS,OAAgC;AAC/C,SAAK,QAAQ;AACb,SAAK,aAAa,gBAAgB,KAAK;AAAA,EACzC;AAAA,EAEQ,UAAU,QAAqC;AACrD,SAAK,SAAS;AACd,SAAK,aAAa,iBAAiB,MAAM;AAAA,EAC3C;AACF;"}
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.1",
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.19.1",
35
- "@tanstack/ai-event-client": "0.3.4"
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",
@@ -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
- chunkStrategy: options.streamProcessor?.chunkStrategy,
132
- initialMessages: options.initialMessages,
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
- while (this.postStreamActions.length > 0) {
915
- const action = this.postStreamActions.shift()!
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
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
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
- if (myBuffer.length > 0) {
187
- chunk = myBuffer.shift()!
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: abortSignal || resolvedOptions.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: abortSignal || resolvedOptions.signal,
494
+ ...(signal ? { signal } : {}),
492
495
  })
493
496
 
494
497
  if (!response.ok) {
@@ -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 unknown as TOutput
295
+ this.result = rawResult as TOutput
290
296
  this.callbacksRef.onResultChange?.(this.result)
291
297
  }
292
298
 
@@ -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
@@ -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> = new Set()
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
- 'tool_call',
412
- async ({ toolCallId, toolName, input }) => {
413
- if (!toolCallId) {
414
- console.error(
415
- '[RealtimeClient] tool_call missing toolCallId',
416
- toolName,
417
- )
418
- return
419
- }
420
- const tool = this.clientTools.get(toolName)
421
- if (tool?.execute) {
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 tool.execute(input)
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
- } else {
437
- this.connection?.sendToolResult(
438
- toolCallId,
439
- JSON.stringify({
440
- error: `Unknown or non-executable tool: ${String(toolName)}`,
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
- name: t.name,
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
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
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?: (result: VideoGenerateResult) => TOutput | null | void
19
- onError?: (error: Error) => void
20
- onProgress?: (progress: number, message?: string) => void
21
- onChunk?: (chunk: StreamChunk) => void
22
- onJobCreated?: (jobId: string) => void
23
- onStatusUpdate?: (status: VideoStatusInfo) => void
24
- onResultChange?: (result: TOutput | null) => void
25
- onLoadingChange?: (isLoading: boolean) => void
26
- onErrorChange?: (error: Error | undefined) => void
27
- onStatusChange?: (status: GenerationClientState) => void
28
- onJobIdChange?: (jobId: string | null) => void
29
- onVideoStatusChange?: (status: VideoStatusInfo | null) => void
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 unknown as TOutput
356
+ this.result = rawResult as TOutput
349
357
  this.callbacksRef.onResultChange?.(this.result)
350
358
  }
351
359