ai-resumable-stream 1.1.1 → 1.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/README.md +4 -4
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +96 -12
- 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)
|
|
@@ -333,7 +333,7 @@ async function startStream(
|
|
|
333
333
|
): Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
334
334
|
```
|
|
335
335
|
|
|
336
|
-
Starts a new resumable stream.
|
|
336
|
+
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
337
|
|
|
338
338
|
#### `resumeStream`
|
|
339
339
|
|
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,6 +35,9 @@ 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
43
|
startStream: (stream: ReadableStream<UIMessageChunk>) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
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, onComplete) {
|
|
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
|
+
onComplete?.();
|
|
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,86 @@ 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
72
|
async function startStream(stream) {
|
|
44
73
|
/**
|
|
45
|
-
*
|
|
74
|
+
* Track client disconnect to avoid unbounded memory growth
|
|
46
75
|
*/
|
|
47
|
-
|
|
48
|
-
const sseStream = convertUIMessageToSSEStream(resumableStream);
|
|
76
|
+
let clientCancelled = false;
|
|
49
77
|
/**
|
|
50
|
-
*
|
|
78
|
+
* Client stream for sending UI message chunks directly to the client without conversion.
|
|
51
79
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
controller
|
|
80
|
+
let clientController;
|
|
81
|
+
const clientStream = new ReadableStream({
|
|
82
|
+
start(controller) {
|
|
83
|
+
clientController = controller;
|
|
56
84
|
},
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
cancel() {
|
|
86
|
+
clientCancelled = true;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* Redis stream with SSE conversion for resumable-stream persistence in Redis.
|
|
91
|
+
* JsonToSseTransformStream converts UIMessageChunk → SSE string and adds [DONE] on flush.
|
|
92
|
+
*/
|
|
93
|
+
let redisController;
|
|
94
|
+
const redisStream = new ReadableStream({ start(controller) {
|
|
95
|
+
redisController = controller;
|
|
96
|
+
} }).pipeThrough(new JsonToSseTransformStream());
|
|
97
|
+
/**
|
|
98
|
+
* Get reader synchronously to fail fast if stream is locked
|
|
99
|
+
*/
|
|
100
|
+
const reader = stream.getReader();
|
|
101
|
+
/**
|
|
102
|
+
* Register Redis stream with resumable-stream for persistence.
|
|
103
|
+
* Release reader lock if registration fails to avoid locking the source stream.
|
|
104
|
+
*/
|
|
105
|
+
try {
|
|
106
|
+
await context.createNewResumableStream(streamId, () => redisStream);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
reader.releaseLock();
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Single drain loop.
|
|
113
|
+
* Continues draining to Redis even if client disconnects
|
|
114
|
+
*/
|
|
115
|
+
(async () => {
|
|
116
|
+
try {
|
|
117
|
+
while (true) {
|
|
118
|
+
const { done, value } = await reader.read();
|
|
119
|
+
if (done) {
|
|
120
|
+
redisController.close();
|
|
121
|
+
if (!clientCancelled) clientController.close();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Always enqueue to Redis for persistence
|
|
126
|
+
*/
|
|
127
|
+
redisController.enqueue(value);
|
|
128
|
+
/**
|
|
129
|
+
* Only enqueue to client if still connected to avoid unbounded memory growth.
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
if (!clientCancelled) clientController.enqueue(value);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
redisController.error(error);
|
|
136
|
+
if (!clientCancelled) clientController.error(error);
|
|
137
|
+
} finally {
|
|
138
|
+
reader.releaseLock();
|
|
139
|
+
try {
|
|
140
|
+
await unsubscribe();
|
|
141
|
+
} catch {}
|
|
59
142
|
}
|
|
60
|
-
}))
|
|
143
|
+
})();
|
|
144
|
+
return createAsyncIterableStream(clientStream);
|
|
61
145
|
}
|
|
62
146
|
/**
|
|
63
147
|
* 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.2.1",
|
|
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",
|