ai-resumable-stream 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -301,6 +301,42 @@ await publisher.quit();
301
301
  await subscriber.quit();
302
302
  ```
303
303
 
304
+ ### Keep Alive
305
+
306
+ By default, the resumable-stream producer tears down immediately when the source stream ends. This means resume requests arriving during post-stream work (e.g. saving the assistant message to the database) will get `null` back, even though the full response is still in memory.
307
+
308
+ Pass a `keepAlive` promise to `startStream` to defer teardown until your post-stream work is complete:
309
+
310
+ ```typescript
311
+ const { promise, resolve } = Promise.withResolvers<void>();
312
+
313
+ const stream = await context.startStream(result.toUIMessageStream(), {
314
+ keepAlive: promise,
315
+ });
316
+
317
+ yield * stream;
318
+
319
+ // Producer stays alive — resume requests are served from the in-memory buffer
320
+ await saveAssistantMessage(result);
321
+
322
+ // Done — producer tears down
323
+ resolve();
324
+ ```
325
+
326
+ ### Flush
327
+
328
+ The `onFlush` callback is invoked after the producer has torn down (Redis stream closed, sentinel set to DONE). Use it for cleanup tasks like removing the active stream ID from the database. Errors thrown by `onFlush` are silently caught.
329
+
330
+ ```typescript
331
+ const stream = await context.startStream(result.toUIMessageStream(), {
332
+ onFlush: async () => {
333
+ await saveChat({ chatId, activeStreamId: null });
334
+ },
335
+ });
336
+ ```
337
+
338
+ When used together with `keepAlive`, `onFlush` fires after the `keepAlive` promise resolves and the producer tears down.
339
+
304
340
  ## API Reference
305
341
 
306
342
  ### `createResumableUIMessageStream`
@@ -309,7 +345,7 @@ await subscriber.quit();
309
345
  async function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
310
346
  startStream: (
311
347
  stream: ReadableStream<UIMessageChunk>,
312
- options?: { onFlush?: () => void | Promise<void> },
348
+ options?: StartStreamOptions,
313
349
  ) => Promise<AsyncIterableStream<UIMessageChunk>>;
314
350
  resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
315
351
  stopStream: () => Promise<void>;
@@ -322,6 +358,11 @@ type CreateResumableUIMessageStream = {
322
358
  abortController?: AbortController;
323
359
  waitUntil?: (promise: Promise<unknown>) => void;
324
360
  };
361
+
362
+ type StartStreamOptions = {
363
+ keepAlive?: Promise<void>;
364
+ onFlush?: () => void | Promise<void>;
365
+ };
325
366
  ```
326
367
 
327
368
  ### Return Values
@@ -331,13 +372,28 @@ type CreateResumableUIMessageStream = {
331
372
  ```typescript
332
373
  async function startStream(
333
374
  stream: ReadableStream<UIMessageChunk>,
334
- options?: { onFlush?: () => void | Promise<void> },
375
+ options?: StartStreamOptions,
335
376
  ): Promise<AsyncIterableStream<UIMessageChunk>>;
377
+
378
+ type StartStreamOptions = {
379
+ keepAlive?: Promise<void>;
380
+ onFlush?: () => void | Promise<void>;
381
+ };
336
382
  ```
337
383
 
338
384
  Starts a new resumable stream. A single drain loop reads from the source and sends chunks to both the client and Redis simultaneously. If the client disconnects, chunks continue flowing to Redis for resumability.
339
385
 
340
- The optional `onFlush` callback is invoked after the stream finishes draining to Redis and cleanup is complete, regardless of how the stream ended (complete, error, or abort). Use it for cleanup tasks like removing the active stream ID from the database. Errors thrown by `onFlush` are silently caught.
386
+ ##### `keepAlive`
387
+
388
+ A promise that defers closing the Redis stream until it resolves. This keeps the resumable-stream producer alive after the source stream ends, so late resume requests (e.g. during post-stream DB writes) can still be served from the in-memory chunk buffer.
389
+
390
+ If the promise rejects, the producer tears down normally. If the source stream errors, the `keepAlive` promise is not awaited.
391
+
392
+ ##### `onFlush`
393
+
394
+ A callback invoked after the Redis stream is closed and the producer has torn down, regardless of how the stream ended (complete, error, or abort). Use it for cleanup tasks like removing the active stream ID from the database. Errors thrown by `onFlush` are silently caught.
395
+
396
+ When used together with `keepAlive`, `onFlush` fires after the `keepAlive` promise resolves and the producer tears down.
341
397
 
342
398
  #### `resumeStream`
343
399
 
package/dist/index.d.mts CHANGED
@@ -33,6 +33,29 @@ type CreateResumableUIMessageStream = {
33
33
  */
34
34
  waitUntil?: (promise: Promise<unknown>) => void;
35
35
  };
36
+ type StartStreamOptions = {
37
+ /**
38
+ * A promise that is awaited before closing the Redis stream, keeping the
39
+ * resumable-stream producer alive so late resume requests can be served
40
+ * from the in-memory buffer. The producer tears down when the promise resolves.
41
+ *
42
+ * Use `Promise.withResolvers()` to create a deferred promise and resolve it
43
+ * after post-stream work (e.g. DB writes) is complete:
44
+ * ```ts
45
+ * const { promise, resolve } = Promise.withResolvers<void>();
46
+ * const stream = await ctx.startStream(input, { keepAlive: promise });
47
+ * for await (const chunk of stream) { ... }
48
+ * await saveToDb();
49
+ * resolve();
50
+ * ```
51
+ */
52
+ keepAlive?: Promise<void>;
53
+ /**
54
+ * Called after the Redis stream is closed and the producer has torn down.
55
+ * Use for post-teardown cleanup.
56
+ */
57
+ onFlush?: () => void | Promise<void>;
58
+ };
36
59
  /**
37
60
  * Creates a resumable context for starting, resuming and stopping UI message streams.
38
61
  *
@@ -40,9 +63,7 @@ type CreateResumableUIMessageStream = {
40
63
  * eagerly and enqueues directly to the output stream.
41
64
  */
42
65
  declare function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
43
- startStream: (stream: ReadableStream<UIMessageChunk>, options?: {
44
- onFlush?: () => void | Promise<void>;
45
- }) => Promise<AsyncIterableStream<UIMessageChunk>>;
66
+ startStream: (stream: ReadableStream<UIMessageChunk>, options?: StartStreamOptions) => Promise<AsyncIterableStream<UIMessageChunk>>;
46
67
  resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
47
68
  stopStream: () => Promise<void>;
48
69
  }>;
package/dist/index.mjs CHANGED
@@ -67,10 +67,13 @@ async function createResumableUIMessageStream(options) {
67
67
  * 1. Reads from source stream
68
68
  * 2. Sends UIMessageChunk directly to client stream (no conversion)
69
69
  * 3. Sends SSE to Redis stream → resumable-stream → Redis
70
- * 4. Propagates errors to both streams
70
+ * 4. If `keepAlive` is provided, awaits it before closing the Redis stream
71
+ * 5. Closes the Redis stream, triggering resumable-stream teardown
72
+ * 6. Calls `onFlush` after teardown
73
+ * 7. Propagates errors to both streams
71
74
  */
72
75
  async function startStream(stream, options) {
73
- const { onFlush } = options ?? {};
76
+ const { keepAlive = Promise.resolve(), onFlush } = options ?? {};
74
77
  /**
75
78
  * Track client disconnect to avoid unbounded memory growth
76
79
  */
@@ -114,12 +117,18 @@ async function createResumableUIMessageStream(options) {
114
117
  * Continues draining to Redis even if client disconnects
115
118
  */
116
119
  (async () => {
120
+ /**
121
+ * Tracks whether the source stream completed successfully (reader.read() returned done: true).
122
+ * Used to guard the keepAlive await in finally — on error, the caller may never resolve
123
+ * the keepAlive promise, so awaiting it would hang the drain loop forever.
124
+ */
125
+ let sourceDone = false;
117
126
  try {
118
127
  while (true) {
119
128
  const { done, value } = await reader.read();
120
129
  if (done) {
121
- redisController.close();
122
130
  if (!clientCancelled) clientController.close();
131
+ sourceDone = true;
123
132
  break;
124
133
  }
125
134
  /**
@@ -140,6 +149,20 @@ async function createResumableUIMessageStream(options) {
140
149
  try {
141
150
  await unsubscribe();
142
151
  } catch {}
152
+ /**
153
+ * Await keepAlive promise before closing the Redis stream.
154
+ * This keeps the resumable-stream producer alive so late resume requests
155
+ * can be served from the in-memory chunk buffer.
156
+ * Defaults to a resolved promise (no-op) when not provided.
157
+ * Only await on successful source completion — on error the caller
158
+ * may never resolve the promise, which would hang the drain loop.
159
+ */
160
+ if (sourceDone) try {
161
+ await keepAlive;
162
+ } catch {}
163
+ try {
164
+ redisController.close();
165
+ } catch {}
143
166
  try {
144
167
  await onFlush?.();
145
168
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-resumable-stream",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "AI SDK: resume and stop UI message streams",
5
5
  "keywords": [
6
6
  "ai",