ai-resumable-stream 1.0.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/LICENSE +21 -0
- package/README.md +356 -0
- package/dist/index.mjs +87 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Chris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
<div align='center'>
|
|
2
|
+
|
|
3
|
+
# ai-resumable-stream
|
|
4
|
+
|
|
5
|
+
<p align="center">AI SDK: Resume and stop UI message streams</p>
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://www.npmjs.com/package/ai-resumable-stream" alt="ai-resumable-stream"><img src="https://img.shields.io/npm/dt/ai-resumable-stream?label=ai-resumable-stream"></a> <a href="https://github.com/zirkelc/ai-resumable-stream/actions/workflows/ci.yml" alt="CI"><img src="https://img.shields.io/github/actions/workflow/status/zirkelc/ai-resumable-stream/ci.yml?branch=main"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
This library provides resumable streaming for UI message streams created by [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) in the AI SDK. It uses Redis to persist stream data, allowing clients to resume interrupted streams or stop active streams from anywhere.
|
|
13
|
+
|
|
14
|
+
**Why?**
|
|
15
|
+
|
|
16
|
+
Streams are ephemeral — once data flows through, it's gone. This creates two hard problems:
|
|
17
|
+
|
|
18
|
+
**Resume is hard** because the server doesn't track what's been sent. If a client disconnects (network issue, page reload, tab switch), the stream keeps running on the server but the client loses all that data. When reconnecting, there's no way to replay missed chunks without persisting them somewhere.
|
|
19
|
+
|
|
20
|
+
**Stop is hard** because the client requesting "stop" isn't the same request that started the stream. The user clicks "Stop generating", which fires a new HTTP request, but the original stream is running in a different request/process. Without a central coordination point, you can't signal across requests.
|
|
21
|
+
|
|
22
|
+
This library implements resumable streams with Redis to support both:
|
|
23
|
+
|
|
24
|
+
- **Resuming**: Chunks are stored as they arrive, enabling replay on reconnect
|
|
25
|
+
- **Stopping**: Stop signals are broadcast to any process running the stream
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
```mermaid
|
|
30
|
+
sequenceDiagram
|
|
31
|
+
participant Client
|
|
32
|
+
participant Server
|
|
33
|
+
participant Redis
|
|
34
|
+
|
|
35
|
+
rect rgb(240, 248, 255)
|
|
36
|
+
Note over Client,Redis: Start Stream
|
|
37
|
+
Client->>Server: sendMessage()
|
|
38
|
+
Server->>Server: streamText()
|
|
39
|
+
Server->>Redis: Subscribe to stop channel
|
|
40
|
+
Server->>Redis: Subscribe to stream channel
|
|
41
|
+
Server->>Redis: Store chunks
|
|
42
|
+
Server-->>Client: Stream chunks
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
rect rgb(240, 255, 240)
|
|
46
|
+
Note over Client,Redis: Resume Stream
|
|
47
|
+
Client->>Server: resumeMessage()
|
|
48
|
+
Server->>Redis: Subscribe to stream channel
|
|
49
|
+
Redis-->>Server: Replay stored chunks
|
|
50
|
+
Server-->>Client: Stream past chunks
|
|
51
|
+
Redis-->>Server: Receive new chunks
|
|
52
|
+
Server-->>Client: Stream new chunks
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
rect rgb(255, 248, 240)
|
|
56
|
+
Note over Client,Redis: Stop Stream
|
|
57
|
+
Client->>Server: stopStream()
|
|
58
|
+
Server->>Redis: Publish to stop channel
|
|
59
|
+
Redis-->>Server: Notify subscriber
|
|
60
|
+
Server->>Server: AbortController.abort()
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
This library requires a [Redis](https://github.com/redis/node-redis) client.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install ai-resumable-stream redis
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
The library requires two Redis clients (pub/sub needs separate connections). The clients will connect automatically, if not already connected, but the library won't disconnect them afterwards. That means you can manage the connection lifecycle in your application and reuse clients across multiple streams.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { createClient } from "redis";
|
|
78
|
+
import { createResumableUIMessageStream } from "ai-resumable-stream";
|
|
79
|
+
|
|
80
|
+
const publisher = createClient({ url: process.env.REDIS_URL });
|
|
81
|
+
const subscriber = createClient({ url: process.env.REDIS_URL });
|
|
82
|
+
|
|
83
|
+
// Optional: connect clients immediately or let the library connect on demand
|
|
84
|
+
await publisher.connect();
|
|
85
|
+
await subscriber.connect();
|
|
86
|
+
|
|
87
|
+
const context = await createResumableUIMessageStream({
|
|
88
|
+
streamId: `stream-123`,
|
|
89
|
+
publisher,
|
|
90
|
+
subscriber,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `startStream`
|
|
95
|
+
|
|
96
|
+
Start a new stream and persist chunks to Redis. Returns a client stream that can be consumed immediately.
|
|
97
|
+
|
|
98
|
+
> [!TIP]
|
|
99
|
+
> The stream returned by `startStream` is both a readable stream and an async iterable.
|
|
100
|
+
> That means you can use `return stream` or `yield* stream`.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { streamText } from "ai";
|
|
104
|
+
|
|
105
|
+
async function sendMessage() {
|
|
106
|
+
// Optional: create AbortController to enable stopStream
|
|
107
|
+
const abortController = new AbortController();
|
|
108
|
+
|
|
109
|
+
// Create resumable stream context
|
|
110
|
+
const context = await createResumableUIMessageStream({
|
|
111
|
+
streamId: `stream-123`,
|
|
112
|
+
publisher,
|
|
113
|
+
subscriber,
|
|
114
|
+
abortController,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = streamText({
|
|
118
|
+
model: `gpt-4o`,
|
|
119
|
+
prompt: `Tell me a story`,
|
|
120
|
+
// Optional: pass abort signal to enable stopping
|
|
121
|
+
abortSignal: abortController.signal,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Start streaming - chunks are stored in Redis as they arrive
|
|
125
|
+
const stream = await context.startStream(result.toUIMessageStream());
|
|
126
|
+
|
|
127
|
+
// Return stream to client
|
|
128
|
+
return stream;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `resumeStream`
|
|
133
|
+
|
|
134
|
+
Resume an existing stream from Redis. Returns all past chunks followed by any remaining chunks, or `null` if no active stream exists.
|
|
135
|
+
|
|
136
|
+
> [!TIP]
|
|
137
|
+
> The stream returned by `resumeStream` is both a readable stream and an async iterable.
|
|
138
|
+
> That means you can use `return stream` or `yield* stream`.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
async function resumeMessage() {
|
|
142
|
+
// Create resumable stream context
|
|
143
|
+
const context = await createResumableUIMessageStream({
|
|
144
|
+
streamId: `stream-123`,
|
|
145
|
+
publisher,
|
|
146
|
+
subscriber,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Try to resume an existing stream
|
|
150
|
+
const stream = await context.resumeStream();
|
|
151
|
+
|
|
152
|
+
// If no stream exists, return early
|
|
153
|
+
if (!stream) {
|
|
154
|
+
console.log("No active stream to resume");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Return resumed stream to client
|
|
159
|
+
return stream;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `stopStream`
|
|
164
|
+
|
|
165
|
+
Stop an active stream from any client. Requires an `AbortController` to be passed when creating the context.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
async function stopMessage() {
|
|
169
|
+
const context = await createResumableUIMessageStream({
|
|
170
|
+
streamId: `stream-123`,
|
|
171
|
+
publisher,
|
|
172
|
+
subscriber,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await context.stopStream();
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
### tRPC
|
|
182
|
+
|
|
183
|
+
Server-side tRPC procedures for sending, resuming, and stopping streams:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// server/router.ts
|
|
187
|
+
import { z } from "zod";
|
|
188
|
+
import { streamText, type UIMessage, type UIMessageChunk } from "ai";
|
|
189
|
+
import { createClient } from "redis";
|
|
190
|
+
import { createResumableUIMessageStream } from "ai-resumable-stream";
|
|
191
|
+
import { publicProcedure, router } from "./trpc";
|
|
192
|
+
|
|
193
|
+
const publisher = createClient({ url: process.env.REDIS_URL });
|
|
194
|
+
const subscriber = createClient({ url: process.env.REDIS_URL });
|
|
195
|
+
|
|
196
|
+
export const appRouter = router({
|
|
197
|
+
sendMessage: publicProcedure
|
|
198
|
+
.input(z.object({ chatId: z.string(), message: z.custom<UIMessage>() }))
|
|
199
|
+
.mutation(async function* ({ input }): AsyncGenerator<UIMessageChunk> {
|
|
200
|
+
const { chatId, message } = input;
|
|
201
|
+
|
|
202
|
+
// TODO: Generate and save active stream ID for the chat
|
|
203
|
+
const activeStreamId = randomUUID();
|
|
204
|
+
await saveChat({ chatId, activeStreamId });
|
|
205
|
+
|
|
206
|
+
const abortController = new AbortController();
|
|
207
|
+
|
|
208
|
+
const context = await createResumableUIMessageStream({
|
|
209
|
+
streamId: activeStreamId,
|
|
210
|
+
publisher,
|
|
211
|
+
subscriber,
|
|
212
|
+
abortController,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = streamText({
|
|
216
|
+
model: openai("gpt-4o"),
|
|
217
|
+
messages: [message],
|
|
218
|
+
abortSignal: abortController.signal,
|
|
219
|
+
onFinish: async () => {
|
|
220
|
+
// TODO: Clear the active stream when finished
|
|
221
|
+
await saveChat({ chatId, activeStreamId: null });
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const stream = await context.startStream(result.toUIMessageStream());
|
|
226
|
+
|
|
227
|
+
yield* stream;
|
|
228
|
+
}),
|
|
229
|
+
|
|
230
|
+
resumeMessage: publicProcedure.input(z.object({ chatId: z.string() })).mutation(async function* ({
|
|
231
|
+
input,
|
|
232
|
+
}): AsyncGenerator<UIMessageChunk> {
|
|
233
|
+
const { chatId } = input;
|
|
234
|
+
|
|
235
|
+
// TODO: Get active stream ID for the chat
|
|
236
|
+
const { activeStreamId } = await getChat(chatId);
|
|
237
|
+
|
|
238
|
+
const context = await createResumableUIMessageStream({
|
|
239
|
+
streamId: activeStreamId,
|
|
240
|
+
publisher,
|
|
241
|
+
subscriber,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const stream = await context.resumeStream();
|
|
245
|
+
if (!stream) return;
|
|
246
|
+
|
|
247
|
+
yield* stream;
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
stopStream: publicProcedure
|
|
251
|
+
.input(z.object({ chatId: z.string() }))
|
|
252
|
+
.mutation(async ({ input }) => {
|
|
253
|
+
const { chatId } = input;
|
|
254
|
+
|
|
255
|
+
// TODO: Get active stream ID for the chat
|
|
256
|
+
const { activeStreamId } = await getChat(chatId);
|
|
257
|
+
|
|
258
|
+
const context = await createResumableUIMessageStream({
|
|
259
|
+
streamId: activeStreamId,
|
|
260
|
+
publisher,
|
|
261
|
+
subscriber,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await context.stopStream();
|
|
265
|
+
|
|
266
|
+
return { success: true };
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Configuration
|
|
272
|
+
|
|
273
|
+
### Options
|
|
274
|
+
|
|
275
|
+
| Option | Type | Required | Description |
|
|
276
|
+
| ----------------- | ---------------------------- | -------- | -------------------------------------------------------------- |
|
|
277
|
+
| `streamId` | `string` | Yes | Unique identifier for the stream |
|
|
278
|
+
| `publisher` | `Redis` | Yes | Redis client for publishing |
|
|
279
|
+
| `subscriber` | `Redis` | Yes | Redis client for subscribing (must be separate from publisher) |
|
|
280
|
+
| `abortController` | `AbortController` | No | Controller to enable `stopStream` functionality |
|
|
281
|
+
| `waitUntil` | `(promise: Promise) => void` | No | Keep serverless function alive until stream completes |
|
|
282
|
+
|
|
283
|
+
### Redis Connection
|
|
284
|
+
|
|
285
|
+
The library automatically connects Redis clients if they're not already connected. Clients are **not** disconnected after stream completion.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Clients can be connected or disconnected
|
|
289
|
+
const publisher = createClient({ url: redisUrl });
|
|
290
|
+
const subscriber = createClient({ url: redisUrl });
|
|
291
|
+
|
|
292
|
+
// Library connects if needed
|
|
293
|
+
const context = await createResumableUIMessageStream({
|
|
294
|
+
streamId: "stream-123",
|
|
295
|
+
publisher, // Will connect if not already connected
|
|
296
|
+
subscriber, // Will connect if not already connected
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Manage disconnection yourself when appropriate
|
|
300
|
+
await publisher.quit();
|
|
301
|
+
await subscriber.quit();
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## API Reference
|
|
305
|
+
|
|
306
|
+
### `createResumableUIMessageStream`
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
async function createResumableUIMessageStream(options: CreateResumableUIMessageStream): Promise<{
|
|
310
|
+
startStream: (
|
|
311
|
+
stream: ReadableStream<UIMessageChunk>,
|
|
312
|
+
) => Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
313
|
+
resumeStream: () => Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
314
|
+
stopStream: () => Promise<void>;
|
|
315
|
+
}>;
|
|
316
|
+
|
|
317
|
+
type CreateResumableUIMessageStream = {
|
|
318
|
+
streamId: string;
|
|
319
|
+
publisher: Redis;
|
|
320
|
+
subscriber: Redis;
|
|
321
|
+
abortController?: AbortController;
|
|
322
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Return Values
|
|
327
|
+
|
|
328
|
+
#### `startStream`
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
async function startStream(
|
|
332
|
+
stream: ReadableStream<UIMessageChunk>,
|
|
333
|
+
): Promise<AsyncIterableStream<UIMessageChunk>>;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Starts a new resumable stream. The input stream is tee'd—one branch goes to the client, the other is persisted to Redis.
|
|
337
|
+
|
|
338
|
+
#### `resumeStream`
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
async function resumeStream(): Promise<AsyncIterableStream<UIMessageChunk> | null>;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Resumes an existing stream. Returns `null` if:
|
|
345
|
+
|
|
346
|
+
- No stream exists for the given `streamId`
|
|
347
|
+
- The stream has already completed
|
|
348
|
+
- The stream TTL has expired
|
|
349
|
+
|
|
350
|
+
#### `stopStream`
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
async function stopStream(): Promise<void>;
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Publishes a stop message via Redis pub/sub. If an `abortController` was provided during creation, the stream's `AbortController.abort()` is called, ending the stream with an `abort` chunk.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { convertSSEToUIMessageStream, convertUIMessageToSSEStream, createAsyncIterableStream } from "ai-stream-utils/utils";
|
|
2
|
+
import { createResumableStreamContext } from "resumable-stream";
|
|
3
|
+
|
|
4
|
+
//#region src/resumable-ui-message-stream.ts
|
|
5
|
+
const KEY_PREFIX = `ai-resumable-stream`;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a resumable context for starting, resuming and stopping UI message streams.
|
|
8
|
+
*/
|
|
9
|
+
async function createResumableUIMessageStream(options) {
|
|
10
|
+
const { streamId, abortController, publisher, subscriber, waitUntil = null } = options;
|
|
11
|
+
const stopChannel = `${KEY_PREFIX}:stop:${streamId}`;
|
|
12
|
+
const context = createResumableStreamContext({
|
|
13
|
+
waitUntil,
|
|
14
|
+
publisher,
|
|
15
|
+
subscriber,
|
|
16
|
+
keyPrefix: KEY_PREFIX
|
|
17
|
+
});
|
|
18
|
+
await Promise.all([publisher.isOpen ? Promise.resolve() : publisher.connect(), subscriber.isOpen ? Promise.resolve() : subscriber.connect()]);
|
|
19
|
+
/**
|
|
20
|
+
* Unsubscribe from stop channel
|
|
21
|
+
*/
|
|
22
|
+
async function unsubscribe() {
|
|
23
|
+
if (!abortController) return;
|
|
24
|
+
await subscriber.unsubscribe(stopChannel);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set up stop subscription if abortController provided
|
|
28
|
+
*/
|
|
29
|
+
if (abortController) {
|
|
30
|
+
await subscriber.subscribe(stopChannel, () => {
|
|
31
|
+
abortController.abort();
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Cleanup when abort signal fires
|
|
35
|
+
*/
|
|
36
|
+
abortController.signal.addEventListener(`abort`, () => {
|
|
37
|
+
unsubscribe();
|
|
38
|
+
}, { once: true });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Start a new stream by creating a new resumable stream in Redis and returning a client stream for the UI.
|
|
42
|
+
*/
|
|
43
|
+
async function startStream(stream) {
|
|
44
|
+
/**
|
|
45
|
+
* Tee the stream into two streams: one for the client and one for the resumable stream in Redis.
|
|
46
|
+
*/
|
|
47
|
+
const [clientStream, resumableStream] = stream.tee();
|
|
48
|
+
const sseStream = convertUIMessageToSSEStream(resumableStream);
|
|
49
|
+
/**
|
|
50
|
+
* Create a new resumable stream in Redis with the stream ID.
|
|
51
|
+
*/
|
|
52
|
+
await context.createNewResumableStream(streamId, () => sseStream);
|
|
53
|
+
return createAsyncIterableStream(clientStream.pipeThrough(new TransformStream({
|
|
54
|
+
transform(chunk, controller) {
|
|
55
|
+
controller.enqueue(chunk);
|
|
56
|
+
},
|
|
57
|
+
flush() {
|
|
58
|
+
unsubscribe();
|
|
59
|
+
}
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resume an existing stream by fetching the resumable stream from Redis using the stream ID.
|
|
64
|
+
*/
|
|
65
|
+
async function resumeStream() {
|
|
66
|
+
/**
|
|
67
|
+
* Resume the existing stream from Redis using the stream ID.
|
|
68
|
+
*/
|
|
69
|
+
const resumableStream = await context.resumeExistingStream(streamId);
|
|
70
|
+
if (!resumableStream) return null;
|
|
71
|
+
return createAsyncIterableStream(convertSSEToUIMessageStream(resumableStream));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Publish a stop message to the stop channel, which will trigger the abortController to abort the stream.
|
|
75
|
+
*/
|
|
76
|
+
async function stopStream() {
|
|
77
|
+
await publisher.publish(stopChannel, `stop`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
startStream,
|
|
81
|
+
resumeStream,
|
|
82
|
+
stopStream
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { createResumableUIMessageStream };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-resumable-stream",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI SDK: resume and stop UI message streams",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-sdk",
|
|
8
|
+
"redis",
|
|
9
|
+
"resumable",
|
|
10
|
+
"resumable-stream",
|
|
11
|
+
"stream"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Chris Cook",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/zirkelc/ai-resumable-stream.git"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.mjs",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepublishOnly": "pnpm build",
|
|
29
|
+
"build": "tsdown",
|
|
30
|
+
"test": "vitest",
|
|
31
|
+
"lint": "oxlint --fix",
|
|
32
|
+
"lint:ci": "oxlint",
|
|
33
|
+
"format": "oxfmt --write",
|
|
34
|
+
"format:ci": "oxfmt --check",
|
|
35
|
+
"prepare": "husky"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"ai-stream-utils": "^1.5.0",
|
|
39
|
+
"resumable-stream": "^2.2.10"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@ai-sdk/provider": "^3.0.8",
|
|
43
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
44
|
+
"@total-typescript/tsconfig": "^1.0.4",
|
|
45
|
+
"@types/node": "^25.2.3",
|
|
46
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
47
|
+
"ai": "^6.0.79",
|
|
48
|
+
"husky": "^9.1.7",
|
|
49
|
+
"lint-staged": "^16.2.7",
|
|
50
|
+
"oxfmt": "^0.31.0",
|
|
51
|
+
"oxlint": "^1.46.0",
|
|
52
|
+
"pkg-pr-new": "^0.0.63",
|
|
53
|
+
"publint": "^0.3.17",
|
|
54
|
+
"redis": "^5.10.0",
|
|
55
|
+
"redis-memory-server": "^0.16.0",
|
|
56
|
+
"tsdown": "^0.20.3",
|
|
57
|
+
"tsx": "^4.21.0",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
60
|
+
},
|
|
61
|
+
"lint-staged": {
|
|
62
|
+
"*.{js,ts,tsx,jsx}": [
|
|
63
|
+
"pnpm lint",
|
|
64
|
+
"pnpm format"
|
|
65
|
+
],
|
|
66
|
+
"*.{json,jsonc}": [
|
|
67
|
+
"pnpm format"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "pnpm@10.0.0"
|
|
71
|
+
}
|