@deltakit/react 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -109,8 +109,9 @@ function useAutoScroll(dependencies, options) {
109
109
  }
110
110
 
111
111
  // src/use-stream-chat.ts
112
- var import_core = require("@deltakit/core");
113
112
  var import_react2 = require("react");
113
+
114
+ // src/chat-controller.ts
114
115
  var counter = 0;
115
116
  function generateId() {
116
117
  return `msg_${Date.now()}_${++counter}`;
@@ -118,30 +119,403 @@ function generateId() {
118
119
  function createMessage(role, parts) {
119
120
  return { id: generateId(), role, parts };
120
121
  }
122
+ function createChatTransportContext(options) {
123
+ const helpers = {
124
+ appendPart: options.appendPart,
125
+ appendText: options.appendText,
126
+ setMessages: options.setMessages
127
+ };
128
+ return {
129
+ emit: (event) => {
130
+ options.eventHandler(event, helpers);
131
+ },
132
+ ensureAssistantMessage: () => {
133
+ options.setMessages((prev) => {
134
+ const last = prev[prev.length - 1];
135
+ if (last?.role === "assistant") {
136
+ return prev;
137
+ }
138
+ return [...prev, createMessage("assistant", [])];
139
+ });
140
+ },
141
+ fail: (error) => {
142
+ options.setError(error);
143
+ options.onError?.(error);
144
+ options.setIsLoading(false);
145
+ options.setRunId(null);
146
+ },
147
+ finish: () => {
148
+ options.setIsLoading(false);
149
+ options.setRunId(null);
150
+ const finalMessages = options.getMessages();
151
+ const lastMessage = finalMessages[finalMessages.length - 1];
152
+ if (lastMessage?.role === "assistant") {
153
+ options.onMessage?.(lastMessage);
154
+ }
155
+ options.onFinish?.(finalMessages);
156
+ },
157
+ getMessages: options.getMessages,
158
+ setRunId: (runId) => {
159
+ options.setRunId(runId);
160
+ }
161
+ };
162
+ }
163
+
164
+ // src/transports.ts
165
+ var import_core = require("@deltakit/core");
166
+ function toError(error) {
167
+ return error instanceof Error ? error : new Error(String(error));
168
+ }
169
+ function isAbortError(error) {
170
+ return error instanceof DOMException && error.name === "AbortError";
171
+ }
172
+ function resolveRunId(response) {
173
+ if (!response || typeof response !== "object") {
174
+ throw new Error("Background SSE start response did not contain a run id");
175
+ }
176
+ const maybeRunId = "runId" in response ? response.runId : "job_id" in response ? response.job_id : null;
177
+ if (typeof maybeRunId !== "string" || maybeRunId.length === 0) {
178
+ throw new Error("Background SSE start response did not contain a run id");
179
+ }
180
+ return maybeRunId;
181
+ }
182
+ function resolveUrl(url, runId) {
183
+ return typeof url === "function" ? url(runId) : url.replace(":runId", runId);
184
+ }
185
+ async function streamFetchSSE(response, context, signal) {
186
+ if (!response.ok) {
187
+ throw new Error(
188
+ `SSE request failed: ${response.status} ${response.statusText}`
189
+ );
190
+ }
191
+ if (!response.body) {
192
+ throw new Error("Response body is null \u2014 SSE streaming not supported");
193
+ }
194
+ for await (const event of (0, import_core.parseSSEStream)(response.body, signal)) {
195
+ context.emit(event);
196
+ }
197
+ }
198
+ function createDirectSSETransport(config) {
199
+ return {
200
+ start: ({ context, message }) => {
201
+ const controller = new AbortController();
202
+ const run = {
203
+ close: () => {
204
+ controller.abort();
205
+ },
206
+ stop: () => {
207
+ controller.abort();
208
+ }
209
+ };
210
+ const fetchImpl = config.fetch ?? fetch;
211
+ void (async () => {
212
+ try {
213
+ const response = await fetchImpl(config.api, {
214
+ body: JSON.stringify({ message, ...config.body }),
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ ...config.headers
218
+ },
219
+ method: config.method ?? "POST",
220
+ signal: controller.signal
221
+ });
222
+ await streamFetchSSE(response, context, controller.signal);
223
+ context.finish();
224
+ } catch (error) {
225
+ if (!isAbortError(error)) {
226
+ context.fail(toError(error));
227
+ }
228
+ }
229
+ })();
230
+ return run;
231
+ }
232
+ };
233
+ }
234
+ function createBackgroundSSETransport(config) {
235
+ const fetchImpl = config.fetch ?? fetch;
236
+ const connect = (runId, context) => {
237
+ const controller = new AbortController();
238
+ void (async () => {
239
+ try {
240
+ context.ensureAssistantMessage();
241
+ const response = await fetchImpl(resolveUrl(config.eventsApi, runId), {
242
+ headers: config.eventHeaders,
243
+ method: "GET",
244
+ signal: controller.signal
245
+ });
246
+ await streamFetchSSE(response, context, controller.signal);
247
+ context.finish();
248
+ } catch (error) {
249
+ if (!isAbortError(error)) {
250
+ context.fail(toError(error));
251
+ }
252
+ }
253
+ })();
254
+ return {
255
+ close: () => {
256
+ controller.abort();
257
+ },
258
+ stop: () => {
259
+ controller.abort();
260
+ if (config.cancelApi) {
261
+ void fetchImpl(resolveUrl(config.cancelApi, runId), {
262
+ method: "POST"
263
+ });
264
+ }
265
+ },
266
+ runId
267
+ };
268
+ };
269
+ return {
270
+ resume: ({ context, runId }) => {
271
+ context.setRunId(runId);
272
+ return connect(runId, context);
273
+ },
274
+ start: ({ context, message }) => {
275
+ const startController = new AbortController();
276
+ let activeRun;
277
+ void (async () => {
278
+ try {
279
+ const response = await fetchImpl(config.startApi, {
280
+ body: JSON.stringify({ message, ...config.startBody }),
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ ...config.startHeaders
284
+ },
285
+ method: config.startMethod ?? "POST",
286
+ signal: startController.signal
287
+ });
288
+ if (!response.ok) {
289
+ throw new Error(
290
+ `Background SSE start failed: ${response.status} ${response.statusText}`
291
+ );
292
+ }
293
+ const data = await response.json();
294
+ const runId = (config.resolveRunId ?? resolveRunId)(data);
295
+ context.setRunId(runId);
296
+ activeRun = connect(runId, context);
297
+ } catch (error) {
298
+ if (!isAbortError(error)) {
299
+ context.fail(toError(error));
300
+ }
301
+ }
302
+ })();
303
+ return {
304
+ close: () => {
305
+ startController.abort();
306
+ void activeRun?.close?.();
307
+ },
308
+ stop: () => {
309
+ startController.abort();
310
+ activeRun?.stop?.();
311
+ },
312
+ runId: null
313
+ };
314
+ }
315
+ };
316
+ }
317
+ function defaultParseWebSocketMessage(data) {
318
+ if (typeof data !== "string") {
319
+ return null;
320
+ }
321
+ const parsed = JSON.parse(data);
322
+ return parsed;
323
+ }
324
+ function createWebSocketTransport(config) {
325
+ const parseMessage = config.parseMessage ?? defaultParseWebSocketMessage;
326
+ const serializeMessage = config.serializeMessage ?? JSON.stringify;
327
+ let resolvedRunId = null;
328
+ const applyIncomingEvents = (parsed, context) => {
329
+ const events = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
330
+ for (const item of events) {
331
+ const nextRunId = config.resolveRunId?.(item) ?? null;
332
+ if (nextRunId) {
333
+ resolvedRunId = nextRunId;
334
+ context.setRunId(nextRunId);
335
+ }
336
+ context.emit(item);
337
+ if (item.type === "done") {
338
+ context.finish();
339
+ } else if (item.type === "error") {
340
+ context.fail(
341
+ new Error(
342
+ "message" in item && typeof item.message === "string" ? item.message : "Stream error"
343
+ )
344
+ );
345
+ }
346
+ }
347
+ };
348
+ return {
349
+ resume: ({ context, runId }) => {
350
+ context.setRunId(runId);
351
+ context.ensureAssistantMessage();
352
+ const socket = new WebSocket(
353
+ typeof config.url === "function" ? config.url(runId) : config.url,
354
+ config.protocols
355
+ );
356
+ let manuallyClosed = false;
357
+ let sentResumePayload = false;
358
+ const sendResumePayload = () => {
359
+ if (sentResumePayload || socket.readyState !== WebSocket.OPEN) {
360
+ return;
361
+ }
362
+ sentResumePayload = true;
363
+ const payload = config.buildResumePayload?.(runId) ?? { [config.runIdKey ?? "runId"]: runId };
364
+ socket.send(serializeMessage(payload));
365
+ };
366
+ socket.onopen = sendResumePayload;
367
+ socket.onmessage = (event) => {
368
+ try {
369
+ applyIncomingEvents(parseMessage(event.data), context);
370
+ } catch (error) {
371
+ context.fail(toError(error));
372
+ }
373
+ };
374
+ socket.onerror = () => {
375
+ context.fail(new Error("WebSocket connection failed"));
376
+ };
377
+ socket.onclose = () => {
378
+ if (!manuallyClosed) {
379
+ context.finish();
380
+ }
381
+ };
382
+ queueMicrotask(sendResumePayload);
383
+ return {
384
+ close: () => {
385
+ manuallyClosed = true;
386
+ socket.close();
387
+ },
388
+ stop: () => {
389
+ manuallyClosed = true;
390
+ const stopRunId = resolvedRunId ?? runId;
391
+ if (stopRunId && config.cancelUrl) {
392
+ void fetch(resolveUrl(config.cancelUrl, stopRunId), {
393
+ method: "POST"
394
+ });
395
+ }
396
+ socket.close();
397
+ },
398
+ runId
399
+ };
400
+ },
401
+ start: ({ context, message }) => {
402
+ const runId = config.runId ?? config.getResumeKey?.() ?? null;
403
+ const socket = new WebSocket(
404
+ typeof config.url === "function" ? config.url(runId) : config.url,
405
+ config.protocols
406
+ );
407
+ let manuallyClosed = false;
408
+ let sentStartPayload = false;
409
+ const sendStartPayload = () => {
410
+ if (sentStartPayload || socket.readyState !== WebSocket.OPEN) {
411
+ return;
412
+ }
413
+ sentStartPayload = true;
414
+ context.ensureAssistantMessage();
415
+ const payload = {
416
+ message,
417
+ ...config.body
418
+ };
419
+ if (runId) {
420
+ payload[config.runIdKey ?? "runId"] = runId;
421
+ }
422
+ socket.send(serializeMessage(payload));
423
+ };
424
+ socket.onopen = sendStartPayload;
425
+ socket.onmessage = (event) => {
426
+ try {
427
+ applyIncomingEvents(parseMessage(event.data), context);
428
+ } catch (error) {
429
+ context.fail(toError(error));
430
+ }
431
+ };
432
+ socket.onerror = () => {
433
+ context.fail(new Error("WebSocket connection failed"));
434
+ };
435
+ socket.onclose = () => {
436
+ if (!manuallyClosed) {
437
+ context.finish();
438
+ }
439
+ };
440
+ queueMicrotask(sendStartPayload);
441
+ return {
442
+ close: () => {
443
+ manuallyClosed = true;
444
+ socket.close();
445
+ },
446
+ stop: () => {
447
+ manuallyClosed = true;
448
+ const stopRunId = resolvedRunId ?? runId;
449
+ if (stopRunId && config.cancelUrl) {
450
+ void fetch(resolveUrl(config.cancelUrl, stopRunId), {
451
+ method: "POST"
452
+ });
453
+ }
454
+ socket.close();
455
+ },
456
+ runId
457
+ };
458
+ }
459
+ };
460
+ }
461
+ function resolveTransport(options) {
462
+ if (typeof options.transport === "object" && options.transport) {
463
+ return options.transport;
464
+ }
465
+ const transportKind = options.transport ?? "sse";
466
+ if (transportKind === "background-sse") {
467
+ const config = options.transportOptions?.backgroundSSE;
468
+ if (!config) {
469
+ throw new Error(
470
+ '`transportOptions.backgroundSSE` is required when transport is "background-sse"'
471
+ );
472
+ }
473
+ return createBackgroundSSETransport(config);
474
+ }
475
+ if (transportKind === "websocket") {
476
+ const config = options.transportOptions?.websocket;
477
+ if (!config) {
478
+ throw new Error(
479
+ '`transportOptions.websocket` is required when transport is "websocket"'
480
+ );
481
+ }
482
+ return createWebSocketTransport(config);
483
+ }
484
+ const sseConfig = options.transportOptions?.sse ?? {
485
+ api: options.api,
486
+ body: options.body,
487
+ headers: options.headers
488
+ };
489
+ if (!sseConfig.api) {
490
+ throw new Error(
491
+ "`api` or `transportOptions.sse.api` is required when using the default SSE transport"
492
+ );
493
+ }
494
+ return createDirectSSETransport({
495
+ ...sseConfig,
496
+ api: sseConfig.api
497
+ });
498
+ }
499
+
500
+ // src/use-stream-chat.ts
121
501
  function defaultOnEvent(event, helpers) {
122
502
  if (event.type === "text_delta") {
123
503
  helpers.appendText(event.delta);
124
504
  }
125
505
  }
126
506
  function useStreamChat(options) {
127
- const {
128
- api,
129
- headers,
130
- body,
131
- initialMessages,
132
- onEvent,
133
- onMessage,
134
- onError,
135
- onFinish
136
- } = options;
137
- const [messages, setMessages] = (0, import_react2.useState)(
138
- initialMessages ?? []
139
- );
507
+ const { initialMessages, onEvent, onMessage, onError, onFinish } = options;
508
+ const [messages, setMessages] = (0, import_react2.useState)(initialMessages ?? []);
140
509
  const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
141
510
  const [error, setError] = (0, import_react2.useState)(null);
142
- const abortRef = (0, import_react2.useRef)(null);
511
+ const [runId, setRunId] = (0, import_react2.useState)(null);
512
+ const runRef = (0, import_react2.useRef)(null);
513
+ const resumedRunIdRef = (0, import_react2.useRef)(null);
514
+ const manuallyStoppedRef = (0, import_react2.useRef)(false);
143
515
  const messagesRef = (0, import_react2.useRef)(messages);
144
516
  messagesRef.current = messages;
517
+ const transportOptionsRef = (0, import_react2.useRef)(options.transportOptions);
518
+ transportOptionsRef.current = options.transportOptions;
145
519
  const appendText = (0, import_react2.useCallback)((delta) => {
146
520
  setMessages((prev) => {
147
521
  const last = prev[prev.length - 1];
@@ -157,29 +531,69 @@ function useStreamChat(options) {
157
531
  } else {
158
532
  parts.push({ type: "text", text: delta });
159
533
  }
160
- const updated = { ...last, parts };
161
- return [...prev.slice(0, -1), updated];
534
+ return [...prev.slice(0, -1), { ...last, parts }];
162
535
  });
163
536
  }, []);
164
537
  const appendPart = (0, import_react2.useCallback)((part) => {
165
538
  setMessages((prev) => {
166
539
  const last = prev[prev.length - 1];
167
540
  if (!last || last.role !== "assistant") return prev;
168
- const updated = {
169
- ...last,
170
- parts: [...last.parts, part]
171
- };
172
- return [...prev.slice(0, -1), updated];
541
+ return [
542
+ ...prev.slice(0, -1),
543
+ {
544
+ ...last,
545
+ parts: [...last.parts, part]
546
+ }
547
+ ];
173
548
  });
174
549
  }, []);
550
+ const transportRef = (0, import_react2.useRef)(null);
551
+ if (!transportRef.current) {
552
+ transportRef.current = resolveTransport(options);
553
+ }
554
+ const transport = transportRef.current;
555
+ const eventHandler = onEvent ?? defaultOnEvent;
556
+ const eventHandlerRef = (0, import_react2.useRef)(eventHandler);
557
+ eventHandlerRef.current = eventHandler;
558
+ const onErrorRef = (0, import_react2.useRef)(onError);
559
+ onErrorRef.current = onError;
560
+ const onFinishRef = (0, import_react2.useRef)(onFinish);
561
+ onFinishRef.current = onFinish;
562
+ const onMessageRef = (0, import_react2.useRef)(onMessage);
563
+ onMessageRef.current = onMessage;
564
+ const transportContext = (0, import_react2.useMemo)(
565
+ () => createChatTransportContext({
566
+ appendPart,
567
+ appendText,
568
+ eventHandler: (event, helpers) => eventHandlerRef.current(event, helpers),
569
+ getMessages: () => messagesRef.current,
570
+ onError: (...args) => onErrorRef.current?.(...args),
571
+ onFinish: (...args) => onFinishRef.current?.(...args),
572
+ onMessage: (...args) => onMessageRef.current?.(...args),
573
+ setError,
574
+ setIsLoading,
575
+ setMessages,
576
+ setRunId: (next) => {
577
+ setRunId(next);
578
+ transportOptionsRef.current?.backgroundSSE?.onRunIdChange?.(next);
579
+ transportOptionsRef.current?.websocket?.onRunIdChange?.(next);
580
+ }
581
+ }),
582
+ [appendPart, appendText]
583
+ );
175
584
  const stop = (0, import_react2.useCallback)(() => {
176
- abortRef.current?.abort();
177
- abortRef.current = null;
585
+ const activeRun = runRef.current;
586
+ if (!activeRun?.stop) {
587
+ return;
588
+ }
589
+ manuallyStoppedRef.current = true;
590
+ void activeRun.stop();
591
+ runRef.current = null;
178
592
  setIsLoading(false);
179
593
  }, []);
180
594
  const sendMessage = (0, import_react2.useCallback)(
181
595
  (text) => {
182
- if (abortRef.current) {
596
+ if (runRef.current || isLoading) {
183
597
  return;
184
598
  }
185
599
  const userMessage = createMessage("user", [
@@ -194,85 +608,64 @@ function useStreamChat(options) {
194
608
  onMessage?.(userMessage);
195
609
  setError(null);
196
610
  setIsLoading(true);
197
- const controller = new AbortController();
198
- abortRef.current = controller;
199
- const eventHandler = onEvent ?? defaultOnEvent;
200
- const helpers = {
201
- appendText,
202
- appendPart,
203
- setMessages
204
- };
205
- (async () => {
206
- try {
207
- const response = await fetch(api, {
208
- method: "POST",
209
- headers: {
210
- "Content-Type": "application/json",
211
- ...headers
212
- },
213
- body: JSON.stringify({ message: text, ...body }),
214
- signal: controller.signal
215
- });
216
- if (!response.ok) {
217
- throw new Error(
218
- `SSE request failed: ${response.status} ${response.statusText}`
219
- );
220
- }
221
- if (!response.body) {
222
- throw new Error(
223
- "Response body is null \u2014 SSE streaming not supported"
224
- );
225
- }
226
- for await (const event of (0, import_core.parseSSEStream)(
227
- response.body,
228
- controller.signal
229
- )) {
230
- eventHandler(event, helpers);
231
- }
232
- const finalMessages = messagesRef.current;
233
- const lastMessage = finalMessages[finalMessages.length - 1];
234
- if (lastMessage?.role === "assistant") {
235
- onMessage?.(lastMessage);
236
- }
237
- onFinish?.(finalMessages);
238
- } catch (err) {
239
- if (err instanceof DOMException && err.name === "AbortError") {
240
- return;
241
- }
242
- const error2 = err instanceof Error ? err : new Error(String(err));
243
- setError(error2);
244
- onError?.(error2);
245
- } finally {
246
- abortRef.current = null;
247
- setIsLoading(false);
248
- }
249
- })();
611
+ resumedRunIdRef.current = null;
612
+ manuallyStoppedRef.current = false;
613
+ const run = transport.start({ context: transportContext, message: text });
614
+ runRef.current = run ?? null;
615
+ if (run?.runId) {
616
+ setRunId(run.runId);
617
+ }
250
618
  },
251
- [
252
- api,
253
- headers,
254
- body,
255
- onEvent,
256
- onMessage,
257
- onError,
258
- onFinish,
259
- appendText,
260
- appendPart
261
- ]
619
+ [isLoading, onMessage, transport, transportContext]
262
620
  );
621
+ const candidateRunId = options.transportOptions?.backgroundSSE?.runId ?? options.transportOptions?.backgroundSSE?.getResumeKey?.() ?? options.transportOptions?.websocket?.runId ?? options.transportOptions?.websocket?.getResumeKey?.() ?? null;
622
+ (0, import_react2.useEffect)(() => {
623
+ if (runRef.current) {
624
+ return;
625
+ }
626
+ if (!candidateRunId) {
627
+ return;
628
+ }
629
+ if (manuallyStoppedRef.current) {
630
+ return;
631
+ }
632
+ if (resumedRunIdRef.current === candidateRunId) {
633
+ return;
634
+ }
635
+ if (!transport.resume) {
636
+ return;
637
+ }
638
+ resumedRunIdRef.current = candidateRunId;
639
+ setError(null);
640
+ setIsLoading(true);
641
+ const run = transport.resume({
642
+ context: transportContext,
643
+ runId: candidateRunId
644
+ });
645
+ runRef.current = run ?? null;
646
+ setRunId(candidateRunId);
647
+ }, [candidateRunId, transport, transportContext]);
263
648
  (0, import_react2.useEffect)(() => {
264
649
  return () => {
265
- abortRef.current?.abort();
266
- abortRef.current = null;
650
+ void runRef.current?.close?.();
651
+ runRef.current = null;
267
652
  };
268
653
  }, []);
654
+ const prevIsLoadingRef = (0, import_react2.useRef)(isLoading);
655
+ (0, import_react2.useEffect)(() => {
656
+ if (prevIsLoadingRef.current && !isLoading) {
657
+ runRef.current = null;
658
+ }
659
+ prevIsLoadingRef.current = isLoading;
660
+ }, [isLoading]);
269
661
  return {
270
- messages,
271
- isLoading,
272
662
  error,
663
+ isLoading,
664
+ messages,
665
+ runId,
273
666
  sendMessage,
274
- stop,
275
- setMessages
667
+ setMessages,
668
+ stop
276
669
  };
277
670
  }
278
671
  // Annotate the CommonJS export names for ESM import in node: