@tanstack/ai-client 0.22.0 → 0.23.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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/audio-recorder.js +190 -213
  3. package/dist/esm/audio-recorder.js.map +1 -1
  4. package/dist/esm/chat-client.d.ts +172 -3
  5. package/dist/esm/chat-client.js +1656 -1386
  6. package/dist/esm/chat-client.js.map +1 -1
  7. package/dist/esm/cleared-stream-tracker.d.ts +23 -0
  8. package/dist/esm/cleared-stream-tracker.js +97 -0
  9. package/dist/esm/cleared-stream-tracker.js.map +1 -0
  10. package/dist/esm/client-persistor.d.ts +25 -12
  11. package/dist/esm/client-persistor.js +260 -235
  12. package/dist/esm/client-persistor.js.map +1 -1
  13. package/dist/esm/connection-adapters.d.ts +231 -10
  14. package/dist/esm/connection-adapters.js +989 -574
  15. package/dist/esm/connection-adapters.js.map +1 -1
  16. package/dist/esm/devtools-noop.d.ts +1 -0
  17. package/dist/esm/devtools-noop.js +79 -139
  18. package/dist/esm/devtools-noop.js.map +1 -1
  19. package/dist/esm/devtools.d.ts +31 -1
  20. package/dist/esm/devtools.js +977 -1127
  21. package/dist/esm/devtools.js.map +1 -1
  22. package/dist/esm/events.js +224 -226
  23. package/dist/esm/events.js.map +1 -1
  24. package/dist/esm/generation-client.d.ts +145 -2
  25. package/dist/esm/generation-client.js +659 -321
  26. package/dist/esm/generation-client.js.map +1 -1
  27. package/dist/esm/generation-reconstruct.d.ts +21 -0
  28. package/dist/esm/generation-reconstruct.js +85 -0
  29. package/dist/esm/generation-reconstruct.js.map +1 -0
  30. package/dist/esm/generation-types.d.ts +289 -3
  31. package/dist/esm/generation-types.js +356 -13
  32. package/dist/esm/generation-types.js.map +1 -1
  33. package/dist/esm/index.d.ts +9 -4
  34. package/dist/esm/index.js +7 -39
  35. package/dist/esm/interrupt-manager.d.ts +77 -0
  36. package/dist/esm/interrupt-manager.js +787 -0
  37. package/dist/esm/interrupt-manager.js.map +1 -0
  38. package/dist/esm/mcp-app-bridge.js +56 -64
  39. package/dist/esm/mcp-app-bridge.js.map +1 -1
  40. package/dist/esm/realtime-client.js +366 -440
  41. package/dist/esm/realtime-client.js.map +1 -1
  42. package/dist/esm/response-stream.js +19 -26
  43. package/dist/esm/response-stream.js.map +1 -1
  44. package/dist/esm/sse-parser.js +44 -47
  45. package/dist/esm/sse-parser.js.map +1 -1
  46. package/dist/esm/sse-utils.js +8 -9
  47. package/dist/esm/sse-utils.js.map +1 -1
  48. package/dist/esm/storage-adapters.d.ts +62 -0
  49. package/dist/esm/storage-adapters.js +174 -0
  50. package/dist/esm/storage-adapters.js.map +1 -0
  51. package/dist/esm/types.d.ts +212 -10
  52. package/dist/esm/types.js +38 -7
  53. package/dist/esm/types.js.map +1 -1
  54. package/dist/esm/video-generation-client.d.ts +113 -2
  55. package/dist/esm/video-generation-client.js +665 -379
  56. package/dist/esm/video-generation-client.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/chat-client.ts +1079 -61
  59. package/src/cleared-stream-tracker.ts +151 -0
  60. package/src/client-persistor.ts +102 -33
  61. package/src/connection-adapters.ts +1185 -142
  62. package/src/devtools-noop.ts +4 -3
  63. package/src/devtools.ts +121 -3
  64. package/src/generation-client.ts +563 -13
  65. package/src/generation-reconstruct.ts +121 -0
  66. package/src/generation-types.ts +727 -3
  67. package/src/index.ts +56 -1
  68. package/src/interrupt-manager.ts +1440 -0
  69. package/src/storage-adapters.ts +242 -0
  70. package/src/types.ts +301 -9
  71. package/src/video-generation-client.ts +479 -13
  72. package/dist/esm/index.js.map +0 -1
@@ -1,382 +1,668 @@
1
- import { GENERATION_EVENTS } from "./generation-types.js";
2
1
  import { createNoOpVideoDevtoolsBridge } from "./devtools-noop.js";
2
+ import { GENERATION_EVENTS, GENERATION_STREAM_TRUNCATED_MESSAGE, GENERATION_UNRESTORABLE_RESULT_MESSAGE, clientStateFromResumeStatus, createGenerationHydrationError, createGenerationResultSnapshot, parseGenerationResumeSnapshot, updateGenerationResumeSnapshot } from "./generation-types.js";
3
3
  import { parseSSEResponse } from "./sse-parser.js";
4
- class VideoGenerationClient {
5
- connection;
6
- fetcher;
7
- uniqueId;
8
- devtoolsMetadata;
9
- devtoolsBridge;
10
- threadId;
11
- body;
12
- result = null;
13
- input = null;
14
- progress = null;
15
- jobId = null;
16
- videoStatus = null;
17
- isLoading = false;
18
- error = void 0;
19
- status = "idle";
20
- abortController = null;
21
- callbacksRef;
22
- devtoolsMounted = false;
23
- constructor(options) {
24
- this.uniqueId = options.id ?? this.generateUniqueId("video");
25
- this.threadId = this.uniqueId;
26
- this.connection = options.connection;
27
- this.fetcher = options.fetcher;
28
- this.body = options.body ?? {};
29
- this.callbacksRef = {
30
- onResult: options.onResult,
31
- onError: options.onError,
32
- onProgress: options.onProgress,
33
- onChunk: options.onChunk,
34
- onJobCreated: options.onJobCreated,
35
- onStatusUpdate: options.onStatusUpdate,
36
- onResultChange: options.onResultChange,
37
- onLoadingChange: options.onLoadingChange,
38
- onErrorChange: options.onErrorChange,
39
- onStatusChange: options.onStatusChange,
40
- onJobIdChange: options.onJobIdChange,
41
- onVideoStatusChange: options.onVideoStatusChange
42
- };
43
- this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools);
44
- this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpVideoDevtoolsBridge)(this.buildDevtoolsBridgeOptions());
45
- }
46
- buildDevtoolsBridgeOptions() {
47
- return {
48
- hookId: this.uniqueId,
49
- clientId: this.uniqueId,
50
- threadId: this.threadId,
51
- metadata: this.devtoolsMetadata,
52
- getCoreState: () => ({
53
- input: this.input,
54
- result: this.result,
55
- progress: this.progress,
56
- status: this.status,
57
- isLoading: this.isLoading,
58
- jobId: this.jobId,
59
- videoStatus: this.videoStatus,
60
- ...this.error ? { error: this.error.message } : {}
61
- })
62
- };
63
- }
64
- mountDevtools() {
65
- if (this.devtoolsMounted) {
66
- return;
67
- }
68
- this.devtoolsMounted = true;
69
- this.devtoolsBridge.emitRegistered();
70
- this.devtoolsBridge.emitSnapshot();
71
- }
72
- /**
73
- * Trigger video generation.
74
- * Only one generation can be in-flight at a time.
75
- */
76
- async generate(input) {
77
- this.mountDevtools();
78
- if (this.isLoading) return;
79
- this.input = input;
80
- this.progress = null;
81
- const runId = this.devtoolsBridge.beginRun(input);
82
- this.setIsLoading(true);
83
- this.setStatus("generating");
84
- this.setError(void 0);
85
- this.setJobId(null);
86
- this.setVideoStatus(null);
87
- const abortController = new AbortController();
88
- this.abortController = abortController;
89
- const { signal } = abortController;
90
- try {
91
- if (this.fetcher) {
92
- await this.generateWithFetcher(input, signal, runId);
93
- } else if (this.connection) {
94
- const mergedData = { ...this.body, ...input };
95
- const stream = this.connection.connect(
96
- [],
97
- mergedData,
98
- signal,
99
- this.createRunContext(runId)
100
- );
101
- await this.processStream(stream, runId);
102
- } else {
103
- throw new Error(
104
- "VideoGenerationClient requires either a connection or fetcher option"
105
- );
106
- }
107
- if (!signal.aborted && this.status === "success") {
108
- this.devtoolsBridge.finishRun(
109
- this.devtoolsBridge.getActiveRunId() ?? runId,
110
- "run:completed",
111
- "completed"
112
- );
113
- }
114
- } catch (err) {
115
- if (signal.aborted) return;
116
- const error = err instanceof Error ? err : new Error(String(err));
117
- this.setError(error);
118
- this.setStatus("error");
119
- this.devtoolsBridge.finishRun(
120
- this.devtoolsBridge.getActiveRunId() ?? runId,
121
- "run:errored",
122
- "errored",
123
- error.message
124
- );
125
- this.callbacksRef.onError?.(error);
126
- } finally {
127
- this.abortController = null;
128
- this.setIsLoading(false);
129
- }
130
- }
131
- /**
132
- * Direct fetcher mode: call fetcher and set result.
133
- */
134
- async generateWithFetcher(input, signal, runId) {
135
- if (!this.fetcher) return;
136
- const result = await this.fetcher(input, { signal });
137
- if (signal.aborted) return;
138
- if (result instanceof Response) {
139
- await this.processStream(parseSSEResponse(result, signal), runId);
140
- } else {
141
- this.devtoolsBridge.ensureRunStarted(runId);
142
- this.setResult(result);
143
- this.setStatus("success");
144
- }
145
- }
146
- /**
147
- * Process a stream of AG-UI events from the streaming connection adapter.
148
- * The server handles the polling loop and streams status updates.
149
- */
150
- async processStream(source, fallbackRunId) {
151
- let streamRunId;
152
- for await (const chunk of source) {
153
- if (this.abortController?.signal.aborted) break;
154
- this.callbacksRef.onChunk?.(chunk);
155
- const chunkRunId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
156
- switch (chunk.type) {
157
- case "RUN_STARTED": {
158
- streamRunId = chunk.runId;
159
- this.devtoolsBridge.ensureRunStarted(chunk.runId);
160
- break;
161
- }
162
- case "CUSTOM": {
163
- this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId);
164
- if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {
165
- const { jobId } = chunk.value;
166
- this.setJobId(jobId);
167
- this.callbacksRef.onJobCreated?.(jobId);
168
- } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {
169
- const statusInfo = chunk.value;
170
- this.setVideoStatus(statusInfo);
171
- this.callbacksRef.onStatusUpdate?.(statusInfo);
172
- if (statusInfo.progress !== void 0) {
173
- this.setProgress(statusInfo.progress);
174
- }
175
- } else if (chunk.name === GENERATION_EVENTS.RESULT) {
176
- this.setResult(chunk.value);
177
- } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
178
- const { progress, message } = chunk.value;
179
- this.setProgress(progress, message);
180
- }
181
- break;
182
- }
183
- case "RUN_FINISHED": {
184
- streamRunId = chunk.runId;
185
- this.devtoolsBridge.ensureRunStarted(chunk.runId);
186
- this.setStatus("success");
187
- break;
188
- }
189
- case "RUN_ERROR": {
190
- this.devtoolsBridge.ensureRunStarted(
191
- chunkRunId ?? streamRunId ?? fallbackRunId
192
- );
193
- const msg = chunk.message || chunk.error?.message || "An error occurred";
194
- throw new Error(msg);
195
- }
196
- }
197
- }
198
- }
199
- /**
200
- * Abort any in-flight generation or polling.
201
- */
202
- stop() {
203
- const runId = this.devtoolsBridge.getActiveRunId();
204
- if (this.abortController) {
205
- this.abortController.abort();
206
- this.abortController = null;
207
- }
208
- this.setIsLoading(false);
209
- if (this.status === "generating") {
210
- this.setStatus("idle");
211
- if (runId) {
212
- this.devtoolsBridge.finishRun(runId, "run:cancelled", "cancelled");
213
- }
214
- }
215
- }
216
- /**
217
- * Clear all state and return to idle.
218
- */
219
- reset() {
220
- this.stop();
221
- this.setResult(null);
222
- this.input = null;
223
- this.progress = null;
224
- this.devtoolsBridge.resetRuns();
225
- this.setJobId(null);
226
- this.setVideoStatus(null);
227
- this.setError(void 0);
228
- this.setStatus("idle");
229
- this.devtoolsBridge.emitState();
230
- }
231
- /**
232
- * Update options without recreating the client.
233
- */
234
- updateOptions(options) {
235
- if (options.body !== void 0) {
236
- this.body = options.body ?? {};
237
- }
238
- if (options.onResult !== void 0) {
239
- this.callbacksRef.onResult = options.onResult;
240
- }
241
- if (options.onError !== void 0) {
242
- this.callbacksRef.onError = options.onError;
243
- }
244
- if (options.onProgress !== void 0) {
245
- this.callbacksRef.onProgress = options.onProgress;
246
- }
247
- if (options.onChunk !== void 0) {
248
- this.callbacksRef.onChunk = options.onChunk;
249
- }
250
- if (options.onJobCreated !== void 0) {
251
- this.callbacksRef.onJobCreated = options.onJobCreated;
252
- }
253
- if (options.onStatusUpdate !== void 0) {
254
- this.callbacksRef.onStatusUpdate = options.onStatusUpdate;
255
- }
256
- }
257
- dispose() {
258
- this.stop();
259
- this.devtoolsBridge.dispose();
260
- this.devtoolsMounted = false;
261
- }
262
- // ===========================
263
- // Getters
264
- // ===========================
265
- getResult() {
266
- return this.result;
267
- }
268
- getJobId() {
269
- return this.jobId;
270
- }
271
- getVideoStatus() {
272
- return this.videoStatus;
273
- }
274
- getIsLoading() {
275
- return this.isLoading;
276
- }
277
- getError() {
278
- return this.error;
279
- }
280
- getStatus() {
281
- return this.status;
282
- }
283
- // ===========================
284
- // Private state setters
285
- // ===========================
286
- setResult(rawResult) {
287
- if (rawResult === null) {
288
- this.result = null;
289
- this.callbacksRef.onResultChange?.(null);
290
- this.devtoolsBridge.recordResultChange();
291
- return;
292
- }
293
- const completedStatus = this.createCompletedVideoStatus(rawResult);
294
- if (this.progress?.value !== 100) {
295
- this.setProgress(100, this.progress?.message);
296
- }
297
- this.setJobId(rawResult.jobId);
298
- this.setVideoStatus(completedStatus);
299
- if (this.callbacksRef.onResult) {
300
- const transformed = this.callbacksRef.onResult(rawResult);
301
- if (transformed === null) {
302
- this.devtoolsBridge.emitState();
303
- return;
304
- }
305
- if (transformed !== void 0) {
306
- this.result = transformed;
307
- this.callbacksRef.onResultChange?.(this.result);
308
- this.devtoolsBridge.recordResultChange();
309
- return;
310
- }
311
- }
312
- this.result = rawResult;
313
- this.callbacksRef.onResultChange?.(this.result);
314
- this.devtoolsBridge.recordResultChange();
315
- }
316
- setJobId(jobId) {
317
- this.jobId = jobId;
318
- this.callbacksRef.onJobIdChange?.(jobId);
319
- this.devtoolsBridge.recordJobIdChange();
320
- }
321
- setVideoStatus(status) {
322
- this.videoStatus = status;
323
- this.callbacksRef.onVideoStatusChange?.(status);
324
- this.devtoolsBridge.recordVideoStatusChange();
325
- }
326
- setIsLoading(isLoading) {
327
- this.isLoading = isLoading;
328
- this.callbacksRef.onLoadingChange?.(isLoading);
329
- this.devtoolsBridge.recordLoadingChange();
330
- }
331
- setError(error) {
332
- this.error = error;
333
- this.callbacksRef.onErrorChange?.(error);
334
- this.devtoolsBridge.recordErrorChange(error);
335
- }
336
- setStatus(status) {
337
- this.status = status;
338
- this.callbacksRef.onStatusChange?.(status);
339
- this.devtoolsBridge.recordStatusChange(status);
340
- }
341
- setProgress(value, message) {
342
- this.progress = {
343
- value,
344
- ...message ? { message } : {}
345
- };
346
- if (message === void 0) {
347
- this.callbacksRef.onProgress?.(value);
348
- } else {
349
- this.callbacksRef.onProgress?.(value, message);
350
- }
351
- this.devtoolsBridge.recordProgressChange();
352
- }
353
- createCompletedVideoStatus(result) {
354
- return {
355
- jobId: result.jobId,
356
- status: result.status,
357
- progress: 100,
358
- url: result.url
359
- };
360
- }
361
- createDevtoolsMetadata(metadata) {
362
- return {
363
- hookName: metadata?.hookName ?? "useGenerateVideo",
364
- outputKind: metadata?.outputKind ?? "video",
365
- ...metadata?.framework ? { framework: metadata.framework } : {},
366
- ...metadata?.name ? { name: metadata.name } : {}
367
- };
368
- }
369
- generateUniqueId(prefix) {
370
- return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
371
- }
372
- createRunContext(runId) {
373
- return {
374
- threadId: this.threadId,
375
- runId
376
- };
377
- }
378
- }
379
- export {
380
- VideoGenerationClient
4
+ //#region src/video-generation-client.ts
5
+ /**
6
+ * A specialized client for job-based video generation.
7
+ *
8
+ * Video generation is asynchronous: a job is created, then polled for status
9
+ * until completion. This client handles the full lifecycle.
10
+ *
11
+ * Supports two transport modes:
12
+ * - **ConnectConnectionAdapter** — Server handles the polling loop internally and
13
+ * streams status updates via CUSTOM events.
14
+ * - **Fetcher** — Direct async function that returns a completed
15
+ * `VideoGenerateResult`.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // With streaming connection adapter (server-driven polling)
20
+ * const client = new VideoGenerationClient({
21
+ * connection: fetchServerSentEvents('/api/generate/video'),
22
+ * onResultChange: setResult,
23
+ * onVideoStatusChange: setVideoStatus,
24
+ * })
25
+ *
26
+ * // With fetcher (direct result)
27
+ * const client = new VideoGenerationClient({
28
+ * fetcher: async (input) => {
29
+ * const res = await fetch('/api/video/generate', {
30
+ * method: 'POST',
31
+ * body: JSON.stringify(input),
32
+ * })
33
+ * return res.json() // { jobId, status: 'completed', url, expiresAt }
34
+ * },
35
+ * })
36
+ *
37
+ * await client.generate({ prompt: 'A flying car over a city' })
38
+ * ```
39
+ */
40
+ var VideoGenerationClient = class {
41
+ connection;
42
+ hydrateGenerationHandler;
43
+ joinRunHandler;
44
+ fetcher;
45
+ uniqueId;
46
+ devtoolsMetadata;
47
+ devtoolsBridge;
48
+ threadId;
49
+ serverDriven = false;
50
+ body;
51
+ result = null;
52
+ input = null;
53
+ progress = null;
54
+ jobId = null;
55
+ videoStatus = null;
56
+ isLoading = false;
57
+ error = void 0;
58
+ status = "idle";
59
+ resumeSnapshot;
60
+ abortController = null;
61
+ rejoinedRunId;
62
+ callbacksRef;
63
+ devtoolsMounted = false;
64
+ disposed = false;
65
+ serverHydrationStarted = false;
66
+ constructor(options) {
67
+ this.uniqueId = options.threadId ?? options.id ?? this.generateUniqueId("video");
68
+ this.threadId = options.threadId ?? this.uniqueId;
69
+ this.connection = options.connection;
70
+ this.fetcher = options.fetcher;
71
+ this.hydrateGenerationHandler = options.hydrateGeneration;
72
+ this.joinRunHandler = options.joinRun;
73
+ this.body = options.body ?? {};
74
+ this.serverDriven = options.persistence === true;
75
+ this.callbacksRef = {
76
+ onResult: options.onResult,
77
+ onError: options.onError,
78
+ onProgress: options.onProgress,
79
+ onChunk: options.onChunk,
80
+ onJobCreated: options.onJobCreated,
81
+ onStatusUpdate: options.onStatusUpdate,
82
+ onResultChange: options.onResultChange,
83
+ onLoadingChange: options.onLoadingChange,
84
+ onErrorChange: options.onErrorChange,
85
+ onStatusChange: options.onStatusChange,
86
+ onJobIdChange: options.onJobIdChange,
87
+ onVideoStatusChange: options.onVideoStatusChange,
88
+ onResumeSnapshotChange: options.onResumeSnapshotChange,
89
+ onResumeStateChange: options.onResumeStateChange
90
+ };
91
+ this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools);
92
+ this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpVideoDevtoolsBridge)(this.buildDevtoolsBridgeOptions());
93
+ }
94
+ buildDevtoolsBridgeOptions() {
95
+ return {
96
+ hookId: this.uniqueId,
97
+ clientId: this.uniqueId,
98
+ threadId: this.threadId,
99
+ metadata: this.devtoolsMetadata,
100
+ getCoreState: () => ({
101
+ input: this.input,
102
+ result: this.result,
103
+ progress: this.progress,
104
+ status: this.status,
105
+ isLoading: this.isLoading,
106
+ jobId: this.jobId,
107
+ videoStatus: this.videoStatus,
108
+ ...this.error ? { error: this.error.message } : {}
109
+ })
110
+ };
111
+ }
112
+ mountDevtools() {
113
+ this.disposed = false;
114
+ this.maybeHydrateFromServer();
115
+ this.maybeResumeInFlight();
116
+ if (this.devtoolsMounted) return;
117
+ this.devtoolsMounted = true;
118
+ this.devtoolsBridge.emitRegistered();
119
+ this.devtoolsBridge.emitSnapshot();
120
+ }
121
+ /**
122
+ * Trigger video generation.
123
+ * Only one generation can be in-flight at a time.
124
+ */
125
+ async generate(input) {
126
+ if (this.disposed) return;
127
+ if (this.isLoading) return;
128
+ this.mountDevtools();
129
+ this.input = input;
130
+ this.progress = null;
131
+ const runId = this.devtoolsBridge.beginRun(input);
132
+ this.setIsLoading(true);
133
+ this.setStatus("generating");
134
+ this.setError(void 0);
135
+ this.setJobId(null);
136
+ this.setVideoStatus(null);
137
+ const abortController = new AbortController();
138
+ this.abortController = abortController;
139
+ const { signal } = abortController;
140
+ try {
141
+ if (this.fetcher) await this.generateWithFetcher(input, signal, runId);
142
+ else if (this.connection) {
143
+ const mergedData = {
144
+ ...this.body,
145
+ ...input
146
+ };
147
+ const stream = this.connection.connect([], mergedData, signal, this.createRunContext(runId));
148
+ await this.processStream(stream, runId, signal);
149
+ } else throw new Error("VideoGenerationClient requires either a connection or fetcher option");
150
+ if (!signal.aborted && this.status === "success") this.devtoolsBridge.finishRun(this.devtoolsBridge.getActiveRunId() ?? runId, "run:completed", "completed");
151
+ } catch (err) {
152
+ if (signal.aborted) return;
153
+ const error = err instanceof Error ? err : new Error(String(err));
154
+ this.setError(error);
155
+ this.setStatus("error");
156
+ this.recordResumeSnapshotError(error);
157
+ this.devtoolsBridge.finishRun(this.devtoolsBridge.getActiveRunId() ?? runId, "run:errored", "errored", error.message);
158
+ this.callbacksRef.onError?.(error);
159
+ } finally {
160
+ if (this.abortController === abortController) {
161
+ this.abortController = null;
162
+ this.setIsLoading(false);
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Direct fetcher mode: call fetcher and set result.
168
+ */
169
+ async generateWithFetcher(input, signal, runId) {
170
+ if (!this.fetcher) return;
171
+ const result = await this.fetcher(input, { signal });
172
+ if (signal.aborted) return;
173
+ if (result instanceof Response) await this.processStream(parseSSEResponse(result, signal), runId, signal);
174
+ else {
175
+ this.devtoolsBridge.ensureRunStarted(runId);
176
+ this.setResult(result);
177
+ this.setStatus("success");
178
+ this.completePlainFetcherResumeSnapshot(result);
179
+ }
180
+ }
181
+ /**
182
+ * Process a stream of AG-UI events from the streaming connection adapter.
183
+ * The server handles the polling loop and streams status updates.
184
+ *
185
+ * Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends
186
+ * without a terminal chunk — see the note on
187
+ * `GenerationClient.processStream`. Video runs are long enough that a proxy
188
+ * idle timeout mid-poll is the likeliest way to hit it.
189
+ */
190
+ async processStream(source, fallbackRunId, signal) {
191
+ let streamRunId;
192
+ let sawTerminalChunk = false;
193
+ for await (const chunk of source) {
194
+ if (signal.aborted) break;
195
+ this.callbacksRef.onChunk?.(chunk);
196
+ this.observeResumeSnapshot(chunk);
197
+ const chunkRunId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
198
+ switch (chunk.type) {
199
+ case "RUN_STARTED":
200
+ streamRunId = chunk.runId;
201
+ this.devtoolsBridge.ensureRunStarted(chunk.runId);
202
+ break;
203
+ case "CUSTOM":
204
+ this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId);
205
+ if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {
206
+ const { jobId } = chunk.value;
207
+ this.setJobId(jobId);
208
+ this.callbacksRef.onJobCreated?.(jobId);
209
+ } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {
210
+ const statusInfo = chunk.value;
211
+ this.setVideoStatus(statusInfo);
212
+ this.callbacksRef.onStatusUpdate?.(statusInfo);
213
+ if (statusInfo.progress !== void 0) this.setProgress(statusInfo.progress);
214
+ } else if (chunk.name === GENERATION_EVENTS.RESULT) this.setResult(chunk.value);
215
+ else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
216
+ const { progress, message } = chunk.value;
217
+ this.setProgress(progress, message);
218
+ }
219
+ break;
220
+ case "RUN_FINISHED":
221
+ streamRunId = chunk.runId;
222
+ sawTerminalChunk = true;
223
+ this.devtoolsBridge.ensureRunStarted(chunk.runId);
224
+ this.setStatus("success");
225
+ break;
226
+ case "RUN_ERROR": {
227
+ this.devtoolsBridge.ensureRunStarted(chunkRunId ?? streamRunId ?? fallbackRunId);
228
+ const msg = chunk.message || chunk.error?.message || "An error occurred";
229
+ throw new Error(msg);
230
+ }
231
+ default: break;
232
+ }
233
+ }
234
+ if (!sawTerminalChunk && !signal.aborted) throw new Error(GENERATION_STREAM_TRUNCATED_MESSAGE);
235
+ }
236
+ /**
237
+ * Abort any in-flight generation or polling.
238
+ */
239
+ stop() {
240
+ const runId = this.devtoolsBridge.getActiveRunId();
241
+ if (this.abortController) {
242
+ this.abortController.abort();
243
+ this.abortController = null;
244
+ }
245
+ this.setIsLoading(false);
246
+ if (this.status === "generating") {
247
+ this.setStatus("idle");
248
+ if (runId) this.devtoolsBridge.finishRun(runId, "run:cancelled", "cancelled");
249
+ }
250
+ if (this.resumeSnapshot && this.resumeSnapshot.status === "running") {
251
+ this.resumeSnapshot = {
252
+ ...this.resumeSnapshot,
253
+ resumeState: null,
254
+ status: "idle"
255
+ };
256
+ this.notifyResumeSnapshotChanged();
257
+ }
258
+ }
259
+ /**
260
+ * Clear all state and return to idle. Also drops the client's in-memory
261
+ * resume snapshot, so a remount restores nothing. The server-side record is
262
+ * untouched — this client no longer writes one — so a full page reload under
263
+ * `persistence: true` re-hydrates the last generation again.
264
+ */
265
+ reset() {
266
+ this.stop();
267
+ this.setResult(null);
268
+ this.input = null;
269
+ this.progress = null;
270
+ this.devtoolsBridge.resetRuns();
271
+ this.setJobId(null);
272
+ this.setVideoStatus(null);
273
+ this.setError(void 0);
274
+ this.setStatus("idle");
275
+ this.clearResumeSnapshot();
276
+ this.devtoolsBridge.emitState();
277
+ }
278
+ /**
279
+ * Update options without recreating the client.
280
+ */
281
+ updateOptions(options) {
282
+ if (options.body !== void 0) this.body = options.body ?? {};
283
+ if (options.onResult !== void 0) this.callbacksRef.onResult = options.onResult;
284
+ if (options.onError !== void 0) this.callbacksRef.onError = options.onError;
285
+ if (options.onProgress !== void 0) this.callbacksRef.onProgress = options.onProgress;
286
+ if (options.onChunk !== void 0) this.callbacksRef.onChunk = options.onChunk;
287
+ if (options.onJobCreated !== void 0) this.callbacksRef.onJobCreated = options.onJobCreated;
288
+ if (options.onStatusUpdate !== void 0) this.callbacksRef.onStatusUpdate = options.onStatusUpdate;
289
+ }
290
+ dispose() {
291
+ this.disposed = true;
292
+ if (this.abortController) {
293
+ this.abortController.abort();
294
+ this.abortController = null;
295
+ }
296
+ this.setIsLoading(false);
297
+ this.devtoolsBridge.dispose();
298
+ this.devtoolsMounted = false;
299
+ this.serverHydrationStarted = false;
300
+ this.rejoinedRunId = void 0;
301
+ }
302
+ getResult() {
303
+ return this.result;
304
+ }
305
+ getJobId() {
306
+ return this.jobId;
307
+ }
308
+ getVideoStatus() {
309
+ return this.videoStatus;
310
+ }
311
+ getIsLoading() {
312
+ return this.isLoading;
313
+ }
314
+ getError() {
315
+ return this.error;
316
+ }
317
+ getStatus() {
318
+ return this.status;
319
+ }
320
+ getResumeSnapshot() {
321
+ return this.resumeSnapshot ? {
322
+ ...this.resumeSnapshot,
323
+ ...this.resumeSnapshot.pendingArtifacts ? { pendingArtifacts: [...this.resumeSnapshot.pendingArtifacts] } : {},
324
+ ...this.resumeSnapshot.result ? { result: {
325
+ ...this.resumeSnapshot.result,
326
+ ...this.resumeSnapshot.result.artifacts ? { artifacts: [...this.resumeSnapshot.result.artifacts] } : {}
327
+ } } : {},
328
+ ...this.resumeSnapshot.error ? { error: { ...this.resumeSnapshot.error } } : {},
329
+ ...this.resumeSnapshot.lastEvent ? { lastEvent: { ...this.resumeSnapshot.lastEvent } } : {}
330
+ } : void 0;
331
+ }
332
+ setResult(rawResult) {
333
+ if (rawResult === null) {
334
+ this.result = null;
335
+ this.callbacksRef.onResultChange?.(null);
336
+ this.devtoolsBridge.recordResultChange();
337
+ return;
338
+ }
339
+ const completedStatus = this.createCompletedVideoStatus(rawResult);
340
+ if (this.progress?.value !== 100) this.setProgress(100, this.progress?.message);
341
+ this.setJobId(rawResult.jobId);
342
+ this.setVideoStatus(completedStatus);
343
+ if (this.callbacksRef.onResult) {
344
+ const transformed = this.callbacksRef.onResult(rawResult);
345
+ if (transformed === null) {
346
+ this.devtoolsBridge.emitState();
347
+ return;
348
+ }
349
+ if (transformed !== void 0) {
350
+ this.result = transformed;
351
+ this.callbacksRef.onResultChange?.(this.result);
352
+ this.devtoolsBridge.recordResultChange();
353
+ return;
354
+ }
355
+ }
356
+ this.result = rawResult;
357
+ this.callbacksRef.onResultChange?.(this.result);
358
+ this.devtoolsBridge.recordResultChange();
359
+ }
360
+ setJobId(jobId) {
361
+ this.jobId = jobId;
362
+ this.callbacksRef.onJobIdChange?.(jobId);
363
+ this.devtoolsBridge.recordJobIdChange();
364
+ }
365
+ setVideoStatus(status) {
366
+ this.videoStatus = status;
367
+ this.callbacksRef.onVideoStatusChange?.(status);
368
+ this.devtoolsBridge.recordVideoStatusChange();
369
+ }
370
+ setIsLoading(isLoading) {
371
+ this.isLoading = isLoading;
372
+ this.callbacksRef.onLoadingChange?.(isLoading);
373
+ this.devtoolsBridge.recordLoadingChange();
374
+ }
375
+ setError(error) {
376
+ this.error = error;
377
+ this.callbacksRef.onErrorChange?.(error);
378
+ this.devtoolsBridge.recordErrorChange(error);
379
+ }
380
+ setStatus(status) {
381
+ this.status = status;
382
+ this.callbacksRef.onStatusChange?.(status);
383
+ this.devtoolsBridge.recordStatusChange(status);
384
+ }
385
+ setProgress(value, message) {
386
+ this.progress = {
387
+ value,
388
+ ...message ? { message } : {}
389
+ };
390
+ if (message === void 0) this.callbacksRef.onProgress?.(value);
391
+ else this.callbacksRef.onProgress?.(value, message);
392
+ this.devtoolsBridge.recordProgressChange();
393
+ }
394
+ createCompletedVideoStatus(result) {
395
+ return {
396
+ jobId: result.jobId,
397
+ status: result.status,
398
+ progress: 100,
399
+ url: result.url
400
+ };
401
+ }
402
+ createDevtoolsMetadata(metadata) {
403
+ return {
404
+ hookName: metadata?.hookName ?? "useGenerateVideo",
405
+ outputKind: metadata?.outputKind ?? "video",
406
+ ...metadata?.framework ? { framework: metadata.framework } : {},
407
+ ...metadata?.name ? { name: metadata.name } : {}
408
+ };
409
+ }
410
+ generateUniqueId(prefix) {
411
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
412
+ }
413
+ createRunContext(runId) {
414
+ return {
415
+ threadId: this.threadId,
416
+ runId
417
+ };
418
+ }
419
+ observeResumeSnapshot(chunk) {
420
+ this.resumeSnapshot = updateGenerationResumeSnapshot(this.resumeSnapshot, chunk);
421
+ this.notifyResumeSnapshotChanged();
422
+ }
423
+ /** Notify the internal snapshot listener AND emit the public resume state. */
424
+ notifyResumeSnapshotChanged() {
425
+ this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot);
426
+ this.emitResumeState();
427
+ }
428
+ /** Derive the public `resumeState` from the internal snapshot. */
429
+ emitResumeState() {
430
+ const snapshot = this.resumeSnapshot;
431
+ const state = snapshot?.resumeState;
432
+ const resumeState = state ? {
433
+ ...state,
434
+ ...snapshot?.pendingArtifacts && snapshot.pendingArtifacts.length > 0 ? { pendingArtifacts: [...snapshot.pendingArtifacts] } : {}
435
+ } : null;
436
+ this.callbacksRef.onResumeStateChange?.(resumeState);
437
+ }
438
+ /**
439
+ * Repaint the normal fields from a restored snapshot so a reload presents the
440
+ * video in `result` / `status` / `error` / `jobId`, never a snapshot object.
441
+ * `isLoading` stays false (no auto-tail). Not re-persisted (it came from
442
+ * storage / the server).
443
+ *
444
+ * A `complete` snapshot with no durable video artifact cannot be rebuilt, so
445
+ * it repaints as an error rather than a `success` with a `null` result — see
446
+ * the note on `GenerationClient.repaintFromSnapshot`.
447
+ */
448
+ repaintFromSnapshot(snapshot) {
449
+ this.resumeSnapshot = snapshot;
450
+ this.notifyResumeSnapshotChanged();
451
+ this.setStatus(clientStateFromResumeStatus(snapshot.status));
452
+ this.setError(snapshot.error ? Object.assign(new Error(snapshot.error.message), snapshot.error.code ? { code: snapshot.error.code } : {}) : void 0);
453
+ if (snapshot.result?.providerJobId) this.setJobId(snapshot.result.providerJobId);
454
+ const restored = this.reconstructVideoResult(snapshot);
455
+ if (restored !== null) this.setResult(restored);
456
+ else if (snapshot.status === "complete") this.reportUnrestorableResult();
457
+ }
458
+ /**
459
+ * Report a `complete` snapshot with no durable video artifact to rebuild
460
+ * from. Runs after the status/error repaint above, so it wins over the
461
+ * snapshot's own `complete` status.
462
+ */
463
+ reportUnrestorableResult() {
464
+ const error = new Error(GENERATION_UNRESTORABLE_RESULT_MESSAGE);
465
+ this.setStatus("error");
466
+ this.setError(error);
467
+ this.callbacksRef.onError?.(error);
468
+ }
469
+ /**
470
+ * Repaint a restored snapshot (client store or server hydrate) and, when it
471
+ * reports a run still in flight, tail that run to completion via `joinRun`
472
+ * (from the connection, or the `joinRun` option when the transport can't
473
+ * carry one).
474
+ *
475
+ * A `running` snapshot that no `joinRun` handler can tail is repainted as an
476
+ * interrupted error instead of a `generating` status that would never
477
+ * settle: an interrupted generation cannot be resumed, only re-run.
478
+ */
479
+ repaintRestoredSnapshot(snapshot, activeRunId) {
480
+ if (snapshot.status !== "running") {
481
+ this.repaintFromSnapshot(snapshot);
482
+ return;
483
+ }
484
+ const joinRun = this.connection?.joinRun ?? this.joinRunHandler;
485
+ const runId = activeRunId ?? snapshot.resumeState?.runId;
486
+ if (runId && joinRun) {
487
+ this.repaintFromSnapshot(snapshot);
488
+ this.rejoinInFlight(runId);
489
+ return;
490
+ }
491
+ this.repaintFromSnapshot({
492
+ ...snapshot,
493
+ resumeState: null,
494
+ status: "error",
495
+ error: { message: "The previous generation was interrupted before it finished and cannot be resumed — generate again to retry." }
496
+ });
497
+ }
498
+ /**
499
+ * Rebuild a `VideoGenerateResult` from a restored snapshot: the video's bytes
500
+ * are served from the durable artifact URL, so the restored result renders
501
+ * from your own origin. Returns `null` when there is no durable video artifact.
502
+ */
503
+ reconstructVideoResult(snapshot) {
504
+ const result = snapshot.result;
505
+ const artifacts = result?.artifacts ?? [];
506
+ const output = artifacts.find((a) => a.role === "output" && a.source.mediaType === "video" && a.url != null);
507
+ if (!output?.url) return null;
508
+ return {
509
+ jobId: result?.providerJobId ?? "",
510
+ status: "completed",
511
+ url: output.url,
512
+ ...result?.expiresAt ? { expiresAt: new Date(result.expiresAt) } : {},
513
+ artifacts
514
+ };
515
+ }
516
+ /**
517
+ * The plain (non-Response) fetcher path never observes stream chunks, so
518
+ * the terminal snapshot is built here from the fetcher's own result. A
519
+ * stale `error` from a previous run is intentionally dropped — this run
520
+ * succeeded.
521
+ */
522
+ completePlainFetcherResumeSnapshot(rawResult) {
523
+ const previous = this.resumeSnapshot;
524
+ const result = createGenerationResultSnapshot(rawResult);
525
+ this.resumeSnapshot = {
526
+ schemaVersion: 1,
527
+ resumeState: null,
528
+ status: "complete",
529
+ ...previous?.activity ? { activity: previous.activity } : {},
530
+ ...previous?.pendingArtifacts && previous.pendingArtifacts.length > 0 ? { pendingArtifacts: [...previous.pendingArtifacts] } : {},
531
+ ...result ? { result } : previous?.result ? { result: { ...previous.result } } : {}
532
+ };
533
+ this.notifyResumeSnapshotChanged();
534
+ }
535
+ /**
536
+ * Records a transport-level failure (network drop, throwing callback) in
537
+ * the snapshot. Without this, only a server-emitted RUN_ERROR chunk would
538
+ * mark the snapshot `error`, leaving a persisted record that claims the
539
+ * run is still in flight.
540
+ */
541
+ recordResumeSnapshotError(error) {
542
+ if (this.status !== "error") this.setStatus("error");
543
+ this.setError(error);
544
+ if (this.resumeSnapshot?.status === "error") return;
545
+ if (!this.resumeSnapshot && !this.serverDriven) return;
546
+ const previous = this.resumeSnapshot;
547
+ this.resumeSnapshot = {
548
+ schemaVersion: 1,
549
+ resumeState: null,
550
+ status: "error",
551
+ ...previous?.activity ? { activity: previous.activity } : {},
552
+ ...previous?.pendingArtifacts && previous.pendingArtifacts.length > 0 ? { pendingArtifacts: [...previous.pendingArtifacts] } : {},
553
+ ...previous?.result ? { result: { ...previous.result } } : {},
554
+ error: { message: error.message }
555
+ };
556
+ this.notifyResumeSnapshotChanged();
557
+ }
558
+ /**
559
+ * Drop the client's in-memory snapshot and re-emit. Purely local — this
560
+ * client writes no storage, so nothing persisted is removed.
561
+ */
562
+ clearResumeSnapshot() {
563
+ this.resumeSnapshot = void 0;
564
+ this.notifyResumeSnapshotChanged();
565
+ }
566
+ /**
567
+ * Server-driven mount hydration entry point (`persistence: true`). Runs at
568
+ * most once, from the commit-phase mount path (`mountDevtools`) — never the
569
+ * constructor / render phase — so remounts and speculative renders can't
570
+ * re-fire the hydrate GET.
571
+ */
572
+ maybeHydrateFromServer() {
573
+ if (!this.serverDriven || this.serverHydrationStarted) return;
574
+ this.serverHydrationStarted = true;
575
+ if (this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler) this.hydrateFromServer();
576
+ else console.warn("[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.");
577
+ }
578
+ /**
579
+ * Server-driven mount hydration (`persistence: true`). The client holds no
580
+ * local snapshot; on mount it asks the server — keyed by the stable threadId —
581
+ * for the last generation's resume snapshot, validates it, and repaints it. It
582
+ * never auto-starts a run, and never blocks: a `generate()` that starts first
583
+ * owns the client and hydration backs off, mirroring the chat client.
584
+ *
585
+ * A genuine **miss** (no record for the thread) is silent; a genuine
586
+ * **failure** (transport error, authorize rejection, malformed body, a record
587
+ * the validator rejects) surfaces through `status` / `error` / `onError` — see
588
+ * the note on `GenerationClient.hydrateFromServer`.
589
+ */
590
+ hydrateFromServer() {
591
+ const hydrate = this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler;
592
+ if (!hydrate) return;
593
+ if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
594
+ (async () => {
595
+ let res;
596
+ try {
597
+ res = await hydrate(this.threadId);
598
+ } catch (cause) {
599
+ this.failHydration(createGenerationHydrationError("the request to the server did not succeed", cause));
600
+ return;
601
+ }
602
+ if (!res.resumeSnapshot) return;
603
+ const snapshot = parseGenerationResumeSnapshot(res.resumeSnapshot);
604
+ if (!snapshot) {
605
+ this.failHydration(createGenerationHydrationError("the server returned a record this client cannot read (unknown schema version, or a missing/invalid `status` or `resumeState`)"));
606
+ return;
607
+ }
608
+ if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
609
+ this.repaintRestoredSnapshot(snapshot, res.activeRun?.runId);
610
+ })();
611
+ }
612
+ /**
613
+ * Surface a hydration failure on the observable fields. Skipped when a
614
+ * `generate()` took ownership while the hydrate GET was in flight.
615
+ */
616
+ failHydration(error) {
617
+ if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
618
+ this.setStatus("error");
619
+ this.setError(error);
620
+ this.callbacksRef.onError?.(error);
621
+ }
622
+ /**
623
+ * Re-attach to an already-loaded `running` snapshot (remount case); see the
624
+ * note in GenerationClient.maybeResumeInFlight. Guarded by `rejoinInFlight`.
625
+ */
626
+ maybeResumeInFlight() {
627
+ if (this.resumeSnapshot?.status !== "running") return;
628
+ const runId = this.resumeSnapshot.resumeState?.runId;
629
+ if (runId) this.rejoinInFlight(runId);
630
+ }
631
+ /**
632
+ * Re-attach to a video run still generating and stream it to completion,
633
+ * mirroring the chat client's mount-time rejoin. Reuses `processStream`, so
634
+ * the job status and result repaint from the replayed chunks. A live
635
+ * `generate()` owns the client and is never stomped; a run is rejoined once.
636
+ */
637
+ rejoinInFlight(runId) {
638
+ const joinRun = this.connection?.joinRun ?? this.joinRunHandler;
639
+ if (!joinRun) return;
640
+ if (this.rejoinedRunId === runId) return;
641
+ if (this.isLoading || this.abortController) return;
642
+ this.rejoinedRunId = runId;
643
+ const controller = new AbortController();
644
+ this.abortController = controller;
645
+ this.setIsLoading(true);
646
+ this.setStatus("generating");
647
+ (async () => {
648
+ try {
649
+ await this.processStream(joinRun(runId, controller.signal), runId, controller.signal);
650
+ } catch (error) {
651
+ if (!controller.signal.aborted) {
652
+ const failure = error instanceof Error ? error : new Error(String(error));
653
+ this.recordResumeSnapshotError(failure);
654
+ this.callbacksRef.onError?.(failure);
655
+ }
656
+ } finally {
657
+ if (this.abortController === controller) {
658
+ this.abortController = null;
659
+ this.setIsLoading(false);
660
+ }
661
+ }
662
+ })();
663
+ }
381
664
  };
382
- //# sourceMappingURL=video-generation-client.js.map
665
+ //#endregion
666
+ export { VideoGenerationClient };
667
+
668
+ //# sourceMappingURL=video-generation-client.js.map