ai-resumable-stream 1.2.1 → 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 +60 -0
- package/dist/index.d.mts +24 -1
- package/dist/index.mjs +32 -5
- 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,6 +345,7 @@ await subscriber.quit();
|
|
|
309
345
|
async function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
|
|
310
346
|
startStream: (
|
|
311
347
|
stream: ReadableStream<UIMessageChunk>,
|
|
348
|
+
options?: StartStreamOptions,
|
|
312
349
|
) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
313
350
|
resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
314
351
|
stopStream: () => Promise<void>;
|
|
@@ -321,6 +358,11 @@ type CreateResumableUIMessageStream = {
|
|
|
321
358
|
abortController?: AbortController;
|
|
322
359
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
323
360
|
};
|
|
361
|
+
|
|
362
|
+
type StartStreamOptions = {
|
|
363
|
+
keepAlive?: Promise<void>;
|
|
364
|
+
onFlush?: () => void | Promise<void>;
|
|
365
|
+
};
|
|
324
366
|
```
|
|
325
367
|
|
|
326
368
|
### Return Values
|
|
@@ -330,11 +372,29 @@ type CreateResumableUIMessageStream = {
|
|
|
330
372
|
```typescript
|
|
331
373
|
async function startStream(
|
|
332
374
|
stream: ReadableStream<UIMessageChunk>,
|
|
375
|
+
options?: StartStreamOptions,
|
|
333
376
|
): Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
377
|
+
|
|
378
|
+
type StartStreamOptions = {
|
|
379
|
+
keepAlive?: Promise<void>;
|
|
380
|
+
onFlush?: () => void | Promise<void>;
|
|
381
|
+
};
|
|
334
382
|
```
|
|
335
383
|
|
|
336
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.
|
|
337
385
|
|
|
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.
|
|
397
|
+
|
|
338
398
|
#### `resumeStream`
|
|
339
399
|
|
|
340
400
|
```typescript
|
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,7 +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
|
|
66
|
+
startStream: (stream: ReadableStream<UIMessageChunk>, options?: StartStreamOptions) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
44
67
|
resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
45
68
|
stopStream: () => Promise<void>;
|
|
46
69
|
}>;
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { createResumableStreamContext } from "resumable-stream";
|
|
|
6
6
|
/**
|
|
7
7
|
* Converts an SSE stream to a UI message stream.
|
|
8
8
|
*/
|
|
9
|
-
function convertSSEToUIMessageStream(stream,
|
|
9
|
+
function convertSSEToUIMessageStream(stream, onFlush) {
|
|
10
10
|
return parseJsonEventStream({
|
|
11
11
|
stream: stream.pipeThrough(new TextEncoderStream()),
|
|
12
12
|
schema: uiMessageChunkSchema
|
|
@@ -15,7 +15,7 @@ function convertSSEToUIMessageStream(stream, onComplete) {
|
|
|
15
15
|
if (result.success) controller.enqueue(result.value);
|
|
16
16
|
},
|
|
17
17
|
flush() {
|
|
18
|
-
|
|
18
|
+
onFlush?.();
|
|
19
19
|
}
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
@@ -67,9 +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
|
-
async function startStream(stream) {
|
|
75
|
+
async function startStream(stream, options) {
|
|
76
|
+
const { keepAlive = Promise.resolve(), onFlush } = options ?? {};
|
|
73
77
|
/**
|
|
74
78
|
* Track client disconnect to avoid unbounded memory growth
|
|
75
79
|
*/
|
|
@@ -113,12 +117,18 @@ async function createResumableUIMessageStream(options) {
|
|
|
113
117
|
* Continues draining to Redis even if client disconnects
|
|
114
118
|
*/
|
|
115
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;
|
|
116
126
|
try {
|
|
117
127
|
while (true) {
|
|
118
128
|
const { done, value } = await reader.read();
|
|
119
129
|
if (done) {
|
|
120
|
-
redisController.close();
|
|
121
130
|
if (!clientCancelled) clientController.close();
|
|
131
|
+
sourceDone = true;
|
|
122
132
|
break;
|
|
123
133
|
}
|
|
124
134
|
/**
|
|
@@ -139,6 +149,23 @@ async function createResumableUIMessageStream(options) {
|
|
|
139
149
|
try {
|
|
140
150
|
await unsubscribe();
|
|
141
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 {}
|
|
166
|
+
try {
|
|
167
|
+
await onFlush?.();
|
|
168
|
+
} catch {}
|
|
142
169
|
}
|
|
143
170
|
})();
|
|
144
171
|
return createAsyncIterableStream(clientStream);
|