@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.
- package/.tshy/build.json +8 -0
- package/.tshy/commonjs.json +16 -0
- package/.tshy/esm.json +15 -0
- package/.turbo/turbo-build.log +46 -0
- package/.turbo/turbo-test$colon$ci.log +14 -0
- package/README.md +112 -0
- package/__tests__/src/resumable-stream.test.ts +341 -0
- package/__tests__/src/testing-stream.ts +87 -0
- package/backup.ts +8 -0
- package/dist/commonjs/index.d.ts +63 -0
- package/dist/commonjs/index.d.ts.map +1 -0
- package/dist/commonjs/index.js +103 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/esm/index.d.ts +63 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +100 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/standalone/index.js +3402 -0
- package/dist/standalone/index.umd.cjs +5 -0
- package/instantdb-resumable-stream-0.0.0.tgz +0 -0
- package/package.json +69 -0
- package/src/index.ts +207 -0
- package/tsconfig.cjs.dev.json +15 -0
- package/tsconfig.dev.json +14 -0
- package/tsconfig.json +12 -0
- package/tsconfig.test.json +5 -0
- package/vite.config.ts +19 -0
package/.tshy/build.json
ADDED
|
@@ -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,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
|
+
[1G[0K[1G[0K⠙[1G[0K
|
|
11
|
+
> @instantdb/resumable-stream@0.0.0 build:standalone
|
|
12
|
+
> vite build
|
|
13
|
+
|
|
14
|
+
[1G[0K[36mvite v5.4.14 [32mbuilding for production...[36m[39m
|
|
15
|
+
[2K[1Gtransforming (1) [2msrc/index.ts[22m[2K[1Gtransforming (51) [2m../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/md5.js[22m[2K[1G[2K[1G[2K[1G[32m✓[39m 73 modules transformed.
|
|
16
|
+
[2K[1Grendering chunks (1)...[2K[1G[2K[1Gcomputing gzip size (0)...[2K[1Gcomputing gzip size (1)...[2K[1G[2mdist/standalone/[22m[36mindex.umd.cjs [39m[1m[2m66.65 kB[22m[1m[22m[2m │ gzip: 20.91 kB[22m
|
|
17
|
+
[2K[1Grendering chunks (1)...[2K[1G[2K[1Gcomputing gzip size (1)...[2K[1G[2mdist/standalone/[22m[36mindex.js [39m[1m[2m105.81 kB[22m[1m[22m[2m │ gzip: 27.65 kB[22m
|
|
18
|
+
[32m✓ built in 390ms[39m
|
|
19
|
+
[1G[0K⠙[1G[0K
|
|
20
|
+
> @instantdb/resumable-stream@0.0.0 check-exports
|
|
21
|
+
> attw --pack .
|
|
22
|
+
|
|
23
|
+
[1G[0K
|
|
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
|
+
[0m No problems found 🌟[0m
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[90m┌───────────────────[39m[90m┬────────────────────────────────────────────[39m[90m┬───────────────────────────────┐[39m
|
|
36
|
+
[90m│[39m[31m [39m[90m│[39m[31m [1m[92m"@instantdb/resumable-stream/package.json"[31m[22m [39m[90m│[39m[31m [1m[92m"@instantdb/resumable-stream"[31m[22m [39m[90m│[39m
|
|
37
|
+
[90m├───────────────────[39m[90m┼────────────────────────────────────────────[39m[90m┼───────────────────────────────┤[39m
|
|
38
|
+
[90m│[39m node10 [90m│[39m 🟢 (JSON) [90m│[39m 🟢 [90m│[39m
|
|
39
|
+
[90m├───────────────────[39m[90m┼────────────────────────────────────────────[39m[90m┼───────────────────────────────┤[39m
|
|
40
|
+
[90m│[39m node16 (from CJS) [90m│[39m 🟢 (JSON) [90m│[39m 🟢 (CJS) [90m│[39m
|
|
41
|
+
[90m├───────────────────[39m[90m┼────────────────────────────────────────────[39m[90m┼───────────────────────────────┤[39m
|
|
42
|
+
[90m│[39m node16 (from ESM) [90m│[39m 🟢 (JSON) [90m│[39m 🟢 (ESM) [90m│[39m
|
|
43
|
+
[90m├───────────────────[39m[90m┼────────────────────────────────────────────[39m[90m┼───────────────────────────────┤[39m
|
|
44
|
+
[90m│[39m bundler [90m│[39m 🟢 (JSON) [90m│[39m 🟢 [90m│[39m
|
|
45
|
+
[90m└───────────────────[39m[90m┴────────────────────────────────────────────[39m[90m┴───────────────────────────────┘[39m
|
|
46
|
+
[1G[0K⠙[1G[0K
|
|
@@ -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
|
+
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/daniel/projects/instant-private/client/packages/resumable-stream[39m
|
|
8
|
+
|
|
9
|
+
[2minclude: [22m[33m**/*.{test,spec}.?(c|m)[jt]s?(x)[39m
|
|
10
|
+
[2mexclude: [22m[33m**/node_modules/**[2m, [22m**/dist/**[2m, [22m**/cypress/**[2m, [22m**/.{idea,git,cache,output,temp}/**[2m, [22m**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*[39m
|
|
11
|
+
[2mwatch exclude: [22m[33m**/node_modules/**[2m, [22m**/dist/**[39m
|
|
12
|
+
[31m
|
|
13
|
+
No test files found, exiting with code 1[39m
|
|
14
|
+
[41m[30m ELIFECYCLE [39m[49m [31mCommand failed with exit code 1.[39m
|
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
|
+
}
|