@instantdb/resumable-stream 0.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "../src",
5
+ "module": "nodenext",
6
+ "moduleResolution": "nodenext"
7
+ }
8
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./build.json",
3
+ "include": [
4
+ "../src/**/*.ts",
5
+ "../src/**/*.cts",
6
+ "../src/**/*.tsx",
7
+ "../src/**/*.json"
8
+ ],
9
+ "exclude": [
10
+ "../src/**/*.mts",
11
+ "../src/package.json"
12
+ ],
13
+ "compilerOptions": {
14
+ "outDir": "../.tshy-build/commonjs"
15
+ }
16
+ }
package/.tshy/esm.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "./build.json",
3
+ "include": [
4
+ "../src/**/*.ts",
5
+ "../src/**/*.mts",
6
+ "../src/**/*.tsx",
7
+ "../src/**/*.json"
8
+ ],
9
+ "exclude": [
10
+ "../src/package.json"
11
+ ],
12
+ "compilerOptions": {
13
+ "outDir": "../.tshy-build/esm"
14
+ }
15
+ }
@@ -0,0 +1,46 @@
1
+
2
+ 
3
+ > @instantdb/resumable-stream@0.0.0 build /Users/daniel/projects/instant-private/client/packages/resumable-stream
4
+ > rm -rf dist; npm run build:tshy && npm run build:standalone && npm run check-exports
5
+
6
+
7
+ > @instantdb/resumable-stream@0.0.0 build:tshy
8
+ > tshy
9
+
10
+ ⠙
11
+ > @instantdb/resumable-stream@0.0.0 build:standalone
12
+ > vite build
13
+
14
+ vite v5.4.14 building for production...
15
+ transforming (1) src/index.tstransforming (51) ../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/md5.js✓ 73 modules transformed.
16
+ rendering chunks (1)...computing gzip size (0)...computing gzip size (1)...dist/standalone/index.umd.cjs 66.65 kB │ gzip: 20.91 kB
17
+ rendering chunks (1)...computing gzip size (1)...dist/standalone/index.js 105.81 kB │ gzip: 27.65 kB
18
+ ✓ built in 390ms
19
+ ⠙
20
+ > @instantdb/resumable-stream@0.0.0 check-exports
21
+ > attw --pack .
22
+
23
+ 
24
+ @instantdb/resumable-stream v0.0.0
25
+
26
+ Build tools:
27
+ - @arethetypeswrong/cli@^0.17.4
28
+ - typescript@^5.9.3
29
+ - vite@^5.2.0
30
+ - tshy@^3.0.2
31
+
32
+  No problems found 🌟
33
+
34
+
35
+ ┌───────────────────┬────────────────────────────────────────────┬───────────────────────────────┐
36
+ │ │ "@instantdb/resumable-stream/package.json" │ "@instantdb/resumable-stream" │
37
+ ├───────────────────┼────────────────────────────────────────────┼───────────────────────────────┤
38
+ │ node10 │ 🟢 (JSON) │ 🟢 │
39
+ ├───────────────────┼────────────────────────────────────────────┼───────────────────────────────┤
40
+ │ node16 (from CJS) │ 🟢 (JSON) │ 🟢 (CJS) │
41
+ ├───────────────────┼────────────────────────────────────────────┼───────────────────────────────┤
42
+ │ node16 (from ESM) │ 🟢 (JSON) │ 🟢 (ESM) │
43
+ ├───────────────────┼────────────────────────────────────────────┼───────────────────────────────┤
44
+ │ bundler │ 🟢 (JSON) │ 🟢 │
45
+ └───────────────────┴────────────────────────────────────────────┴───────────────────────────────┘
46
+ ⠙
@@ -0,0 +1,14 @@
1
+
2
+ 
3
+ > @instantdb/resumable-stream@0.0.0 test:ci /Users/daniel/projects/instant-private/client/packages/resumable-stream
4
+ > vitest run
5
+
6
+
7
+  RUN  v1.6.1 /Users/daniel/projects/instant-private/client/packages/resumable-stream
8
+
9
+ include: **/*.{test,spec}.?(c|m)[jt]s?(x)
10
+ exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
11
+ watch exclude: **/node_modules/**, **/dist/**
12
+ 
13
+ No test files found, exiting with code 1
14
+  ELIFECYCLE  Command failed with exit code 1.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ <p align="center">
2
+ <a href="https://instantdb.com">
3
+ <img alt="Shows the Instant logo" src="https://instantdb.com/img/icon/android-chrome-512x512.png" width="10%">
4
+ </a>
5
+ <h1 align="center">@instantdb/resumable-stream</h1>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a
10
+ href="https://discord.com/invite/VU53p7uQcE" >
11
+ <img height=20 src="https://img.shields.io/discord/1031957483243188235" />
12
+ </a>
13
+ <img src="https://img.shields.io/github/stars/instantdb/instant" alt="stars">
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://www.instantdb.com/docs/start-vanilla">Get Started</a> ·
18
+ <a href="https://instantdb.com/examples">Examples</a> ·
19
+ <a href="https://www.instantdb.com/docs/start-vanilla">Docs</a> ·
20
+ <a href="https://discord.com/invite/VU53p7uQcE">Discord</a>
21
+ <p>
22
+
23
+ Welcome to [Instant's](http://instantdb.com) resumable-stream library.
24
+
25
+ This is a drop-in replacement for Vercel's resumable-stream library using InstantDB streams.
26
+
27
+ Instant's streams have no dependency on Redis and they never expire.
28
+
29
+ ## Usage
30
+
31
+ You can provide your appId and adminToken as arguments to `createResumableStreamContext` or export `INSTANT_APP_ID` and `INSTANT_APP_ADMIN_TOKEN`.
32
+
33
+ ### Idempotent API
34
+
35
+ ```typescript
36
+ import { createResumableStreamContext } from '@instantdb/resumable-stream';
37
+ import { after } from 'next/server';
38
+
39
+ const streamContext = createResumableStreamContext({
40
+ waitUntil: after,
41
+ appId: YOUR_INSTANT_APP_ID, // or export INSTANT_APP_ID
42
+ adminToken: YOUR_INSTANT_APP_ADMIN_TOKEN, // or export INSTANT_APP_ADMIN_TOKEN
43
+ });
44
+
45
+ export async function GET(
46
+ req: NextRequest,
47
+ { params }: { params: Promise<{ streamId: string }> },
48
+ ) {
49
+ const { streamId } = await params;
50
+ const resumeAt = req.nextUrl.searchParams.get('resumeAt');
51
+ const stream = await streamContext.resumableStream(
52
+ streamId,
53
+ makeTestStream,
54
+ resumeAt ? parseInt(resumeAt) : undefined,
55
+ );
56
+ return new Response(stream, {
57
+ headers: {
58
+ 'Content-Type': 'text/event-stream',
59
+ },
60
+ });
61
+ }
62
+ ```
63
+
64
+ ### Usage with explicit resumption
65
+
66
+ ```typescript
67
+ import { createResumableStreamContext } from 'resumable-stream';
68
+ import { after } from 'next/server';
69
+
70
+ const streamContext = createResumableStreamContext({
71
+ waitUntil: after,
72
+ appId: YOUR_INSTANT_APP_ID, // or export INSTANT_APP_ID
73
+ adminToken: YOUR_INSTANT_APP_ADMIN_TOKEN, // or export INSTANT_APP_ADMIN_TOKEN
74
+ });
75
+
76
+ export async function POST(
77
+ req: NextRequest,
78
+ { params }: { params: Promise<{ streamId: string }> },
79
+ ) {
80
+ const { streamId } = await params;
81
+ const stream = await streamContext.createNewResumableStream(
82
+ streamId,
83
+ makeTestStream,
84
+ );
85
+ return new Response(stream, {
86
+ headers: {
87
+ 'Content-Type': 'text/event-stream',
88
+ },
89
+ });
90
+ }
91
+
92
+ export async function GET(
93
+ req: NextRequest,
94
+ { params }: { params: Promise<{ streamId: string }> },
95
+ ) {
96
+ const { streamId } = await params;
97
+ const resumeAt = req.nextUrl.searchParams.get('resumeAt');
98
+ const stream = await streamContext.resumeExistingStream(
99
+ streamId,
100
+ resumeAt ? parseInt(resumeAt) : undefined,
101
+ );
102
+ return new Response(stream, {
103
+ headers: {
104
+ 'Content-Type': 'text/event-stream',
105
+ },
106
+ });
107
+ }
108
+ ```
109
+
110
+ # Questions?
111
+
112
+ If you have any questions, feel free to drop us a line on our [Discord](https://discord.com/invite/VU53p7uQcE)
@@ -0,0 +1,341 @@
1
+ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import {
3
+ createResumableStreamContext,
4
+ type ResumableStreamContext,
5
+ } from '../../src/index.js';
6
+ import { createTestingStream, streamToBuffer } from './testing-stream.js';
7
+
8
+ type EphemeralApp = {
9
+ appId: string;
10
+ adminToken: string;
11
+ apiURI: string;
12
+ };
13
+
14
+ async function createEphemeralAppForTests(): Promise<EphemeralApp> {
15
+ const apiURI = process.env.INSTANT_API_URI || 'https://api.instantdb.com';
16
+ const response = await fetch(`${apiURI}/dash/apps/ephemeral`, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ body: JSON.stringify({
22
+ title: `resumable-stream-tests-${Date.now()}`,
23
+ }),
24
+ });
25
+
26
+ if (!response.ok) {
27
+ const body = await response.text();
28
+ throw new Error(
29
+ `Failed to create ephemeral app (${response.status}): ${body}`,
30
+ );
31
+ }
32
+
33
+ const json = (await response.json()) as any;
34
+ const appId = json?.app?.id ?? json?.appId;
35
+ const adminToken =
36
+ json?.app?.['admin-token'] ??
37
+ json?.app?.adminToken ??
38
+ json?.app?.admin_token ??
39
+ json?.adminToken;
40
+
41
+ if (!appId || !adminToken) {
42
+ throw new Error(
43
+ `Unexpected ephemeral app response: ${JSON.stringify(json)}`,
44
+ );
45
+ }
46
+
47
+ return {
48
+ appId,
49
+ adminToken,
50
+ apiURI,
51
+ };
52
+ }
53
+
54
+ function withScopedStreamIds(
55
+ context: ResumableStreamContext,
56
+ scope: string,
57
+ ): ResumableStreamContext {
58
+ const scopedId = (streamId: string) => `${scope}:${streamId}`;
59
+ return {
60
+ resumableStream: (streamId, makeStream, skipCharacters) =>
61
+ context.resumableStream(scopedId(streamId), makeStream, skipCharacters),
62
+ resumeExistingStream: (streamId, skipCharacters) =>
63
+ context.resumeExistingStream(scopedId(streamId), skipCharacters),
64
+ createNewResumableStream: (streamId, makeStream, skipCharacters) =>
65
+ context.createNewResumableStream(
66
+ scopedId(streamId),
67
+ makeStream,
68
+ skipCharacters,
69
+ ),
70
+ hasExistingStream: (streamId) =>
71
+ context.hasExistingStream(scopedId(streamId)),
72
+ };
73
+ }
74
+
75
+ describe('resumable stream', () => {
76
+ let app: EphemeralApp;
77
+ let resume: ResumableStreamContext;
78
+ let waitUntilPromises: Promise<any>[] = [];
79
+
80
+ beforeAll(async () => {
81
+ app = await createEphemeralAppForTests();
82
+ }, 60000);
83
+
84
+ beforeEach(() => {
85
+ const baseContext = createResumableStreamContext({
86
+ waitUntil: (p) => waitUntilPromises.push(p),
87
+ appId: app.appId,
88
+ adminToken: app.adminToken,
89
+ apiURI: app.apiURI,
90
+ });
91
+ resume = withScopedStreamIds(baseContext, crypto.randomUUID());
92
+ });
93
+
94
+ it('should act like a normal stream', async () => {
95
+ const { readable, writer } = createTestingStream();
96
+ const stream = await resume.resumableStream('test', () => readable);
97
+ writer.write('1\n');
98
+ writer.write('2\n');
99
+ writer.write('3\n');
100
+ writer.close();
101
+ const result2 = await streamToBuffer(stream);
102
+ expect(result2).toEqual('1\n2\n3\n');
103
+ });
104
+
105
+ it('should resume a done stream', async () => {
106
+ const { readable, writer } = createTestingStream();
107
+ const stream = await resume.resumableStream('test', () => readable);
108
+ const stream2 = await resume.resumableStream('test', () => readable);
109
+ writer.write('1\n');
110
+ writer.write('2\n');
111
+ writer.close();
112
+ const result = await streamToBuffer(stream);
113
+ const result2 = await streamToBuffer(stream2);
114
+ expect(result).toEqual('1\n2\n');
115
+ expect(result2).toEqual('1\n2\n');
116
+ });
117
+
118
+ it('hasExistingStream', async () => {
119
+ const { readable, writer } = createTestingStream();
120
+ expect(await resume.hasExistingStream('test')).toBe(null);
121
+ const stream = await resume.resumableStream('test', () => readable);
122
+ expect(await resume.hasExistingStream('test')).toBe(true);
123
+ expect(await resume.hasExistingStream('test2')).toBe(null);
124
+ const stream2 = await resume.resumableStream('test', () => readable);
125
+ expect(await resume.hasExistingStream('test')).toBe(true);
126
+ writer.write('1\n');
127
+ writer.write('2\n');
128
+ writer.close();
129
+
130
+ const result = await streamToBuffer(stream);
131
+ const result2 = await streamToBuffer(stream2);
132
+ expect(result).toEqual('1\n2\n');
133
+ expect(result2).toEqual('1\n2\n');
134
+ await Promise.all(waitUntilPromises);
135
+ expect(await resume.hasExistingStream('test')).toBe('DONE');
136
+ });
137
+
138
+ it('should resume a done stream reverse read', async () => {
139
+ const { readable, writer } = createTestingStream();
140
+ const stream = await resume.resumableStream('test', () => readable);
141
+ const stream2 = await resume.resumableStream('test', () => readable);
142
+ writer.write('1\n');
143
+ writer.write('2\n');
144
+ writer.close();
145
+ const result2 = await streamToBuffer(stream2);
146
+ const result = await streamToBuffer(stream);
147
+
148
+ expect(result).toEqual('1\n2\n');
149
+ expect(result2).toEqual('1\n2\n');
150
+ });
151
+
152
+ it('should resume an in-progress stream', async () => {
153
+ const { readable, writer } = createTestingStream();
154
+ const stream = await resume.resumableStream('test', () => readable);
155
+ writer.write('1\n');
156
+ const stream2 = await resume.resumableStream('test', () => readable);
157
+ writer.write('2\n');
158
+ writer.close();
159
+ const result = await streamToBuffer(stream);
160
+ const result2 = await streamToBuffer(stream2);
161
+ expect(result).toEqual('1\n2\n');
162
+ expect(result2).toEqual('1\n2\n');
163
+ });
164
+
165
+ it('should actually stream', async () => {
166
+ const { readable, writer } = createTestingStream();
167
+ const stream = await resume.resumableStream('test', () => readable);
168
+ writer.write('1\n');
169
+ const stream2 = await resume.resumableStream('test', () => readable);
170
+ const result = await streamToBuffer(stream, 1);
171
+ const result2 = await streamToBuffer(stream2, 1);
172
+ expect(result).toEqual('1\n');
173
+ expect(result2).toEqual('1\n');
174
+ writer.write('2\n');
175
+ writer.close();
176
+ const step1 = await streamToBuffer(stream);
177
+ const step2 = await streamToBuffer(stream2);
178
+ expect(step1).toEqual('2\n');
179
+ expect(step2).toEqual('2\n');
180
+ });
181
+
182
+ it('should actually stream producer first', async () => {
183
+ const { readable, writer } = createTestingStream();
184
+ const stream = await resume.resumableStream('test', () => readable);
185
+ writer.write('1\n');
186
+ const stream2 = await resume.resumableStream('test', () => readable);
187
+ const result = await streamToBuffer(stream, 1);
188
+ expect(result).toEqual('1\n');
189
+ writer.write('2\n');
190
+ writer.close();
191
+ const step1 = await streamToBuffer(stream);
192
+ const step2 = await streamToBuffer(stream2);
193
+ expect(step1).toEqual('2\n');
194
+ expect(step2).toEqual('1\n2\n');
195
+ });
196
+
197
+ it('should actually stream consumer first', async () => {
198
+ const { readable, writer } = createTestingStream();
199
+ const stream = await resume.resumableStream('test', () => readable);
200
+ writer.write('1\n');
201
+ const stream2 = await resume.resumableStream('test', () => readable);
202
+ const result2 = await streamToBuffer(stream2, 1);
203
+ expect(result2).toEqual('1\n');
204
+ writer.write('2\n');
205
+ writer.close();
206
+ const step1 = await streamToBuffer(stream);
207
+ const step2 = await streamToBuffer(stream2);
208
+ expect(step1).toEqual('1\n2\n');
209
+ expect(step2).toEqual('2\n');
210
+ });
211
+
212
+ it('should resume multiple streams', async () => {
213
+ const { readable, writer } = createTestingStream();
214
+ const stream = await resume.resumableStream('test', () => readable);
215
+ writer.write('1\n');
216
+ const stream2 = await resume.resumableStream('test', () => readable);
217
+ writer.write('2\n');
218
+ const stream3 = await resume.resumableStream('test', () => readable);
219
+ writer.close();
220
+ const result = await streamToBuffer(stream);
221
+ const result2 = await streamToBuffer(stream2);
222
+ const result3 = await streamToBuffer(stream3);
223
+ expect(result).toEqual('1\n2\n');
224
+ expect(result2).toEqual('1\n2\n');
225
+ expect(result3).toEqual('1\n2\n');
226
+ });
227
+
228
+ it('should differentiate between streams', async () => {
229
+ const { readable, writer } = createTestingStream();
230
+ const { readable: readable2, writer: writer2 } = createTestingStream();
231
+ const stream1 = await resume.resumableStream('test', () => readable);
232
+ const stream2 = await resume.resumableStream('test2', () => readable2);
233
+ const stream12 = await resume.resumableStream('test', () => readable);
234
+ const stream22 = await resume.resumableStream('test2', () => readable2);
235
+ writer.write('1\n');
236
+ writer.write('2\n');
237
+ writer.close();
238
+ writer2.write('writer2\n');
239
+ writer2.close();
240
+ const result1 = await streamToBuffer(stream1);
241
+ const result2 = await streamToBuffer(stream2);
242
+ const result12 = await streamToBuffer(stream12);
243
+ const result22 = await streamToBuffer(stream22);
244
+ expect(result1).toEqual('1\n2\n');
245
+ expect(result2).toEqual('writer2\n');
246
+ expect(result12).toEqual('1\n2\n');
247
+ expect(result22).toEqual('writer2\n');
248
+ });
249
+
250
+ it('should respect skipCharacters', async () => {
251
+ const { readable, writer } = createTestingStream();
252
+ const stream = await resume.resumableStream('test', () => readable);
253
+ writer.write('1\n');
254
+ writer.write('2\n');
255
+ const stream2 = await resume.resumableStream('test', () => readable, 2);
256
+ writer.close();
257
+ const result = await streamToBuffer(stream);
258
+ const result2 = await streamToBuffer(stream2);
259
+ expect(result).toEqual('1\n2\n');
260
+ expect(result2).toEqual('2\n');
261
+ });
262
+
263
+ it('should respect skipCharacters 2', async () => {
264
+ const { readable, writer } = createTestingStream();
265
+ const stream = await resume.resumableStream('test', () => readable);
266
+ writer.write('1\n');
267
+ writer.write('2\n');
268
+ const stream2 = await resume.resumableStream('test', () => readable, 4);
269
+ writer.close();
270
+ const result = await streamToBuffer(stream);
271
+ const result2 = await streamToBuffer(stream2);
272
+ expect(result).toEqual('1\n2\n');
273
+ expect(result2).toEqual('');
274
+ });
275
+
276
+ it('should respect skipCharacters 0', async () => {
277
+ const { readable, writer } = createTestingStream();
278
+ const stream = await resume.resumableStream('test', () => readable);
279
+ writer.write('1\n');
280
+ writer.write('2\n');
281
+ const stream2 = await resume.resumableStream('test', () => readable, 0);
282
+ writer.close();
283
+ const result = await streamToBuffer(stream);
284
+ const result2 = await streamToBuffer(stream2);
285
+ expect(result).toEqual('1\n2\n');
286
+ expect(result2).toEqual('1\n2\n');
287
+ });
288
+
289
+ // We return the stream because we can afford to hold on to the old streams
290
+ // Leaving this as a skipped test in case we want to offer a compat mode that
291
+ // returns null
292
+ it.skip('should return null if stream is done', async () => {
293
+ const { readable, writer } = createTestingStream();
294
+ const stream = await resume.resumableStream('test', () => readable);
295
+ writer.write('1\n');
296
+ writer.write('2\n');
297
+ writer.close();
298
+
299
+ const result = await streamToBuffer(stream);
300
+ expect(
301
+ await resume.resumableStream('test', () => {
302
+ throw new Error('Should never be called');
303
+ }),
304
+ ).toBeNull();
305
+ expect(result).toEqual('1\n2\n');
306
+ });
307
+
308
+ it('should support the deconstructed APIs', async () => {
309
+ const { readable, writer } = createTestingStream();
310
+ const stream = await resume.createNewResumableStream(
311
+ 'test',
312
+ () => readable,
313
+ );
314
+ const stream2 = await resume.resumeExistingStream('test');
315
+ writer.write('1\n');
316
+ writer.write('2\n');
317
+ writer.close();
318
+ const result = await streamToBuffer(stream);
319
+ const result2 = await streamToBuffer(stream2);
320
+ expect(result).toEqual('1\n2\n');
321
+ expect(result2).toEqual('1\n2\n');
322
+ });
323
+
324
+ // We return the stream because we can afford to hold on to the old streams
325
+ // Leaving this as a skipped test in case we want to offer a compat mode that
326
+ // returns null
327
+ it.skip('should return null if stream is done explicit APIs', async () => {
328
+ const { readable, writer } = createTestingStream();
329
+ const stream = await resume.createNewResumableStream(
330
+ 'test',
331
+ () => readable,
332
+ );
333
+ writer.write('1\n');
334
+ writer.write('2\n');
335
+ writer.close();
336
+
337
+ const result = await streamToBuffer(stream);
338
+ expect(await resume.resumeExistingStream('test')).toBeNull();
339
+ expect(result).toEqual('1\n2\n');
340
+ });
341
+ });
@@ -0,0 +1,87 @@
1
+ export function createTestingStream() {
2
+ let controller: ReadableStreamDefaultController<string> | undefined =
3
+ undefined;
4
+ const buffer: string[] = [];
5
+ const readable = new ReadableStream<string>({
6
+ start(c) {
7
+ controller = c;
8
+ if (buffer.length > 0) {
9
+ for (const chunk of buffer) {
10
+ controller.enqueue(chunk);
11
+ }
12
+ }
13
+ },
14
+ });
15
+
16
+ const writable = new WritableStream<string>({
17
+ write(chunk) {
18
+ if (controller) {
19
+ controller.enqueue(chunk);
20
+ }
21
+ buffer.push(chunk);
22
+ },
23
+ close() {
24
+ controller?.close();
25
+ },
26
+ abort(reason) {
27
+ controller?.error(reason);
28
+ },
29
+ });
30
+
31
+ return {
32
+ readable,
33
+ writer: writable.getWriter(),
34
+ buffer,
35
+ };
36
+ }
37
+
38
+ const readers = new WeakMap<
39
+ ReadableStream<string>,
40
+ ReadableStreamDefaultReader<string>
41
+ >();
42
+
43
+ export async function streamToBuffer(
44
+ stream: ReadableStream<string> | null | undefined,
45
+ maxNReads?: number,
46
+ ) {
47
+ if (stream === null) {
48
+ throw new Error('Stream should not be null');
49
+ }
50
+ if (stream === undefined) {
51
+ throw new Error('Stream should not be undefined');
52
+ }
53
+
54
+ const reader = (
55
+ readers.has(stream) ? readers.get(stream) : stream.getReader()
56
+ ) as ReadableStreamDefaultReader<string>;
57
+ readers.set(stream, reader);
58
+
59
+ const buffer: string[] = [];
60
+ function timeout(ms: number) {
61
+ return new Promise((_, reject) =>
62
+ setTimeout(
63
+ () =>
64
+ reject(new Error(`Timeout with buffer ${JSON.stringify(buffer)}`)),
65
+ ms,
66
+ ),
67
+ );
68
+ }
69
+
70
+ let i = 0;
71
+ while (true) {
72
+ const { done, value } = await (Promise.race([
73
+ reader.read(),
74
+ timeout(2000),
75
+ ]) as Promise<{ done: boolean; value: string }>);
76
+ if (!done) {
77
+ buffer.push(value);
78
+ }
79
+ if (maxNReads && ++i === maxNReads) {
80
+ break;
81
+ }
82
+ if (done) {
83
+ break;
84
+ }
85
+ }
86
+ return buffer.join('');
87
+ }
package/backup.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/__tests__/**/*.test.ts'],
6
+ exclude: ['vercel-resumable-stream/**', 'node_modules/**', 'dist/**'],
7
+ },
8
+ });