ai-resumable-stream 1.1.1 → 1.3.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 +8 -4
- package/dist/index.d.mts +7 -2
- package/dist/index.mjs +101 -13
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ sequenceDiagram
|
|
|
38
38
|
Server->>Server: streamText()
|
|
39
39
|
Server->>Redis: Subscribe to stop channel
|
|
40
40
|
Server->>Redis: Subscribe to stream channel
|
|
41
|
-
Server->>Redis: Store chunks
|
|
41
|
+
Server->>Redis: Store chunks (UI-to-SSE conversion)
|
|
42
42
|
Server-->>Client: Stream chunks
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -47,9 +47,9 @@ sequenceDiagram
|
|
|
47
47
|
Client->>Server: resumeMessage()
|
|
48
48
|
Server->>Redis: Subscribe to stream channel
|
|
49
49
|
Redis-->>Server: Replay stored chunks
|
|
50
|
-
Server-->>Client: Stream past chunks
|
|
50
|
+
Server-->>Client: Stream past chunks (SSE-to-UI conversion)
|
|
51
51
|
Redis-->>Server: Receive new chunks
|
|
52
|
-
Server-->>Client: Stream new chunks
|
|
52
|
+
Server-->>Client: Stream new chunks (SSE-to-UI conversion)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
rect rgb(255, 248, 240)
|
|
@@ -309,6 +309,7 @@ await subscriber.quit();
|
|
|
309
309
|
async function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
|
|
310
310
|
startStream: (
|
|
311
311
|
stream: ReadableStream<UIMessageChunk>,
|
|
312
|
+
options?: { onFlush?: () => void | Promise<void> },
|
|
312
313
|
) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
313
314
|
resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
314
315
|
stopStream: () => Promise<void>;
|
|
@@ -330,10 +331,13 @@ type CreateResumableUIMessageStream = {
|
|
|
330
331
|
```typescript
|
|
331
332
|
async function startStream(
|
|
332
333
|
stream: ReadableStream<UIMessageChunk>,
|
|
334
|
+
options?: { onFlush?: () => void | Promise<void> },
|
|
333
335
|
): Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
334
336
|
```
|
|
335
337
|
|
|
336
|
-
Starts a new resumable stream.
|
|
338
|
+
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
|
+
|
|
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.
|
|
337
341
|
|
|
338
342
|
#### `resumeStream`
|
|
339
343
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AsyncIterableStream } from "ai-stream-utils/utils";
|
|
2
1
|
import { UIMessageChunk } from "ai";
|
|
2
|
+
import { AsyncIterableStream } from "ai-stream-utils";
|
|
3
3
|
import { createClient } from "redis";
|
|
4
4
|
|
|
5
5
|
//#region src/resumable-ui-message-stream.d.ts
|
|
@@ -35,9 +35,14 @@ type CreateResumableUIMessageStream = {
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* Creates a resumable context for starting, resuming and stopping UI message streams.
|
|
38
|
+
*
|
|
39
|
+
* Leverages resumable-stream's internal eager drain pattern which drains the source
|
|
40
|
+
* eagerly and enqueues directly to the output stream.
|
|
38
41
|
*/
|
|
39
42
|
declare function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
|
|
40
|
-
startStream: (stream: ReadableStream<UIMessageChunk
|
|
43
|
+
startStream: (stream: ReadableStream<UIMessageChunk>, options?: {
|
|
44
|
+
onFlush?: () => void | Promise<void>;
|
|
45
|
+
}) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
41
46
|
resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
42
47
|
stopStream: () => Promise<void>;
|
|
43
48
|
}>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JsonToSseTransformStream, parseJsonEventStream, uiMessageChunkSchema } from "ai";
|
|
2
|
+
import { createAsyncIterableStream } from "ai-stream-utils";
|
|
2
3
|
import { createResumableStreamContext } from "resumable-stream";
|
|
3
4
|
|
|
5
|
+
//#region src/convert-sse-stream-to-ui-message-stream.ts
|
|
6
|
+
/**
|
|
7
|
+
* Converts an SSE stream to a UI message stream.
|
|
8
|
+
*/
|
|
9
|
+
function convertSSEToUIMessageStream(stream, onFlush) {
|
|
10
|
+
return parseJsonEventStream({
|
|
11
|
+
stream: stream.pipeThrough(new TextEncoderStream()),
|
|
12
|
+
schema: uiMessageChunkSchema
|
|
13
|
+
}).pipeThrough(new TransformStream({
|
|
14
|
+
transform(result, controller) {
|
|
15
|
+
if (result.success) controller.enqueue(result.value);
|
|
16
|
+
},
|
|
17
|
+
flush() {
|
|
18
|
+
onFlush?.();
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
4
24
|
//#region src/resumable-ui-message-stream.ts
|
|
5
25
|
const KEY_PREFIX = `ai-resumable-stream`;
|
|
6
26
|
/**
|
|
7
27
|
* Creates a resumable context for starting, resuming and stopping UI message streams.
|
|
28
|
+
*
|
|
29
|
+
* Leverages resumable-stream's internal eager drain pattern which drains the source
|
|
30
|
+
* eagerly and enqueues directly to the output stream.
|
|
8
31
|
*/
|
|
9
32
|
async function createResumableUIMessageStream(options) {
|
|
10
33
|
const { streamId, abortController, publisher, subscriber, waitUntil = null } = options;
|
|
@@ -39,25 +62,90 @@ async function createResumableUIMessageStream(options) {
|
|
|
39
62
|
}
|
|
40
63
|
/**
|
|
41
64
|
* Start a new stream by creating a new resumable stream in Redis and returning a client stream for the UI.
|
|
65
|
+
*
|
|
66
|
+
* Uses a single drain loop that:
|
|
67
|
+
* 1. Reads from source stream
|
|
68
|
+
* 2. Sends UIMessageChunk directly to client stream (no conversion)
|
|
69
|
+
* 3. Sends SSE to Redis stream → resumable-stream → Redis
|
|
70
|
+
* 4. Propagates errors to both streams
|
|
42
71
|
*/
|
|
43
|
-
async function startStream(stream) {
|
|
72
|
+
async function startStream(stream, options) {
|
|
73
|
+
const { onFlush } = options ?? {};
|
|
44
74
|
/**
|
|
45
|
-
*
|
|
75
|
+
* Track client disconnect to avoid unbounded memory growth
|
|
46
76
|
*/
|
|
47
|
-
|
|
48
|
-
const sseStream = convertUIMessageToSSEStream(resumableStream);
|
|
77
|
+
let clientCancelled = false;
|
|
49
78
|
/**
|
|
50
|
-
*
|
|
79
|
+
* Client stream for sending UI message chunks directly to the client without conversion.
|
|
51
80
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
controller
|
|
81
|
+
let clientController;
|
|
82
|
+
const clientStream = new ReadableStream({
|
|
83
|
+
start(controller) {
|
|
84
|
+
clientController = controller;
|
|
56
85
|
},
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
cancel() {
|
|
87
|
+
clientCancelled = true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Redis stream with SSE conversion for resumable-stream persistence in Redis.
|
|
92
|
+
* JsonToSseTransformStream converts UIMessageChunk → SSE string and adds [DONE] on flush.
|
|
93
|
+
*/
|
|
94
|
+
let redisController;
|
|
95
|
+
const redisStream = new ReadableStream({ start(controller) {
|
|
96
|
+
redisController = controller;
|
|
97
|
+
} }).pipeThrough(new JsonToSseTransformStream());
|
|
98
|
+
/**
|
|
99
|
+
* Get reader synchronously to fail fast if stream is locked
|
|
100
|
+
*/
|
|
101
|
+
const reader = stream.getReader();
|
|
102
|
+
/**
|
|
103
|
+
* Register Redis stream with resumable-stream for persistence.
|
|
104
|
+
* Release reader lock if registration fails to avoid locking the source stream.
|
|
105
|
+
*/
|
|
106
|
+
try {
|
|
107
|
+
await context.createNewResumableStream(streamId, () => redisStream);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Single drain loop.
|
|
114
|
+
* Continues draining to Redis even if client disconnects
|
|
115
|
+
*/
|
|
116
|
+
(async () => {
|
|
117
|
+
try {
|
|
118
|
+
while (true) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) {
|
|
121
|
+
redisController.close();
|
|
122
|
+
if (!clientCancelled) clientController.close();
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Always enqueue to Redis for persistence
|
|
127
|
+
*/
|
|
128
|
+
redisController.enqueue(value);
|
|
129
|
+
/**
|
|
130
|
+
* Only enqueue to client if still connected to avoid unbounded memory growth.
|
|
131
|
+
*
|
|
132
|
+
*/
|
|
133
|
+
if (!clientCancelled) clientController.enqueue(value);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
redisController.error(error);
|
|
137
|
+
if (!clientCancelled) clientController.error(error);
|
|
138
|
+
} finally {
|
|
139
|
+
reader.releaseLock();
|
|
140
|
+
try {
|
|
141
|
+
await unsubscribe();
|
|
142
|
+
} catch {}
|
|
143
|
+
try {
|
|
144
|
+
await onFlush?.();
|
|
145
|
+
} catch {}
|
|
59
146
|
}
|
|
60
|
-
}))
|
|
147
|
+
})();
|
|
148
|
+
return createAsyncIterableStream(clientStream);
|
|
61
149
|
}
|
|
62
150
|
/**
|
|
63
151
|
* Resume an existing stream by fetching the resumable stream from Redis using the stream ID.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-resumable-stream",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "AI SDK: resume and stop UI message streams",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"ai-stream-utils": "^
|
|
29
|
+
"ai-stream-utils": "^2.0.0",
|
|
30
30
|
"resumable-stream": "^2.2.10"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"ai": "^6.0.79",
|
|
39
39
|
"husky": "^9.1.7",
|
|
40
40
|
"lint-staged": "^16.2.7",
|
|
41
|
+
"mitata": "^1.0.34",
|
|
41
42
|
"oxfmt": "^0.31.0",
|
|
42
43
|
"oxlint": "^1.46.0",
|
|
43
44
|
"pkg-pr-new": "^0.0.63",
|
|
@@ -47,7 +48,8 @@
|
|
|
47
48
|
"tsdown": "^0.20.3",
|
|
48
49
|
"tsx": "^4.21.0",
|
|
49
50
|
"typescript": "^5.9.3",
|
|
50
|
-
"vitest": "^4.0.18"
|
|
51
|
+
"vitest": "^4.0.18",
|
|
52
|
+
"zod": "^4.3.6"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
55
|
"ai": "^6.0.0",
|