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