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 +59 -3
- package/dist/index.d.mts +24 -3
- package/dist/index.mjs +26 -3
- package/package.json +1 -1
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?:
|
|
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?:
|
|
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
|
-
|
|
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.
|
|
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 {}
|