@ricsam/isolate-fetch 0.0.1 → 0.1.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/CHANGELOG.md +9 -0
- package/package.json +27 -7
- package/src/debug-delayed.test.ts +89 -0
- package/src/debug-streaming.test.ts +81 -0
- package/src/download-streaming-simple.test.ts +167 -0
- package/src/download-streaming.test.ts +286 -0
- package/src/form-data.test.ts +824 -0
- package/src/formdata.test.ts +212 -0
- package/src/headers.test.ts +582 -0
- package/src/host-backed-stream.test.ts +363 -0
- package/src/index.test.ts +274 -0
- package/src/index.ts +2325 -0
- package/src/integration.test.ts +665 -0
- package/src/request.test.ts +482 -0
- package/src/response.test.ts +520 -0
- package/src/serve.test.ts +425 -0
- package/src/stream-state.test.ts +338 -0
- package/src/stream-state.ts +337 -0
- package/src/upload-streaming.test.ts +373 -0
- package/src/websocket.test.ts +627 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
package/CHANGELOG.md
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/isolate-fetch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "node --test --experimental-strip-types 'src/**/*.test.ts'",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@ricsam/isolate-core": "*",
|
|
20
|
+
"isolated-vm": "^6"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@ricsam/isolate-test-utils": "*",
|
|
24
|
+
"@types/node": "^24",
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"isolated-vm": "^6"
|
|
29
|
+
}
|
|
10
30
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
import {
|
|
5
|
+
setupFetch,
|
|
6
|
+
clearAllInstanceState,
|
|
7
|
+
type FetchHandle,
|
|
8
|
+
} from "./index.ts";
|
|
9
|
+
import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
|
|
10
|
+
import { setupConsole } from "@ricsam/isolate-console";
|
|
11
|
+
import { clearStreamRegistryForContext } from "./stream-state.ts";
|
|
12
|
+
|
|
13
|
+
describe("Debug Delayed Streaming", () => {
|
|
14
|
+
it("delayed streaming response with setTimeout", { timeout: 10000 }, async () => {
|
|
15
|
+
const isolate = new ivm.Isolate();
|
|
16
|
+
const context = await isolate.createContext();
|
|
17
|
+
clearAllInstanceState();
|
|
18
|
+
|
|
19
|
+
const logs: string[] = [];
|
|
20
|
+
await setupConsole(context, {
|
|
21
|
+
onLog: (level, ...args) => {
|
|
22
|
+
logs.push(`[${level}] ${args.join(' ')}`);
|
|
23
|
+
console.log(`[ISOLATE] ${args.join(' ')}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const timersHandle = await setupTimers(context);
|
|
28
|
+
const fetchHandle = await setupFetch(context);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
context.evalSync(`
|
|
32
|
+
console.log('Setting up serve');
|
|
33
|
+
serve({
|
|
34
|
+
async fetch(request) {
|
|
35
|
+
console.log('Fetch handler called');
|
|
36
|
+
let count = 0;
|
|
37
|
+
const stream = new ReadableStream({
|
|
38
|
+
async pull(controller) {
|
|
39
|
+
console.log('pull() called, count =', count);
|
|
40
|
+
if (count < 3) {
|
|
41
|
+
console.log('Starting setTimeout');
|
|
42
|
+
await new Promise(r => setTimeout(r, 10));
|
|
43
|
+
console.log('setTimeout resolved');
|
|
44
|
+
const data = "delayed" + count;
|
|
45
|
+
console.log('Enqueuing:', data);
|
|
46
|
+
controller.enqueue(new TextEncoder().encode(data));
|
|
47
|
+
count++;
|
|
48
|
+
console.log('Enqueued, returning');
|
|
49
|
+
} else {
|
|
50
|
+
console.log('Closing stream');
|
|
51
|
+
controller.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
console.log('Creating Response');
|
|
56
|
+
const response = new Response(stream);
|
|
57
|
+
console.log('Response created, returning');
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
console.log('Serve registered');
|
|
62
|
+
`);
|
|
63
|
+
|
|
64
|
+
console.log('Calling dispatchRequest');
|
|
65
|
+
const response = await fetchHandle.dispatchRequest(
|
|
66
|
+
new Request("http://test/"),
|
|
67
|
+
{
|
|
68
|
+
tick: async () => {
|
|
69
|
+
// Advance virtual time by 50ms each tick to process timers
|
|
70
|
+
await timersHandle.tick(50);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
console.log('Got response, status:', response.status);
|
|
76
|
+
console.log('Calling response.text()');
|
|
77
|
+
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
console.log('Got text:', text);
|
|
80
|
+
assert.strictEqual(text, "delayed0delayed1delayed2");
|
|
81
|
+
} finally {
|
|
82
|
+
fetchHandle.dispose();
|
|
83
|
+
timersHandle.dispose();
|
|
84
|
+
clearStreamRegistryForContext(context);
|
|
85
|
+
context.release();
|
|
86
|
+
isolate.dispose();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
import {
|
|
5
|
+
setupFetch,
|
|
6
|
+
clearAllInstanceState,
|
|
7
|
+
type FetchHandle,
|
|
8
|
+
} from "./index.ts";
|
|
9
|
+
import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
|
|
10
|
+
import { setupConsole } from "@ricsam/isolate-console";
|
|
11
|
+
import { clearStreamRegistryForContext } from "./stream-state.ts";
|
|
12
|
+
|
|
13
|
+
describe("Debug Streaming", () => {
|
|
14
|
+
it("debug pull-based stream", { timeout: 5000 }, async () => {
|
|
15
|
+
const isolate = new ivm.Isolate();
|
|
16
|
+
const context = await isolate.createContext();
|
|
17
|
+
clearAllInstanceState();
|
|
18
|
+
|
|
19
|
+
const logs: string[] = [];
|
|
20
|
+
await setupConsole(context, {
|
|
21
|
+
onLog: (level, ...args) => {
|
|
22
|
+
logs.push(`[${level}] ${args.join(' ')}`);
|
|
23
|
+
console.log(`[ISOLATE] ${args.join(' ')}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const timersHandle = await setupTimers(context);
|
|
28
|
+
const fetchHandle = await setupFetch(context);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
context.evalSync(`
|
|
32
|
+
console.log('Setting up serve');
|
|
33
|
+
serve({
|
|
34
|
+
async fetch(request) {
|
|
35
|
+
console.log('Fetch handler called');
|
|
36
|
+
let count = 0;
|
|
37
|
+
const stream = new ReadableStream({
|
|
38
|
+
pull(controller) {
|
|
39
|
+
console.log('pull() called, count =', count);
|
|
40
|
+
if (count < 3) {
|
|
41
|
+
const data = "chunk" + count;
|
|
42
|
+
console.log('Enqueuing:', data);
|
|
43
|
+
controller.enqueue(new TextEncoder().encode(data));
|
|
44
|
+
count++;
|
|
45
|
+
} else {
|
|
46
|
+
console.log('Closing stream');
|
|
47
|
+
controller.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
console.log('Creating Response');
|
|
52
|
+
const response = new Response(stream);
|
|
53
|
+
console.log('Response created, returning');
|
|
54
|
+
return response;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
console.log('Serve registered');
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
console.log('Calling dispatchRequest');
|
|
61
|
+
const response = await fetchHandle.dispatchRequest(
|
|
62
|
+
new Request("http://test/"),
|
|
63
|
+
{ tick: () => timersHandle.tick() }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
console.log('Got response, status:', response.status);
|
|
67
|
+
console.log('Response body:', response.body);
|
|
68
|
+
console.log('Calling response.text()');
|
|
69
|
+
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
console.log('Got text:', text);
|
|
72
|
+
assert.strictEqual(text, "chunk0chunk1chunk2");
|
|
73
|
+
} finally {
|
|
74
|
+
fetchHandle.dispose();
|
|
75
|
+
timersHandle.dispose();
|
|
76
|
+
clearStreamRegistryForContext(context);
|
|
77
|
+
context.release();
|
|
78
|
+
isolate.dispose();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
import {
|
|
5
|
+
setupFetch,
|
|
6
|
+
clearAllInstanceState,
|
|
7
|
+
type FetchHandle,
|
|
8
|
+
} from "./index.ts";
|
|
9
|
+
import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
|
|
10
|
+
import { clearStreamRegistryForContext } from "./stream-state.ts";
|
|
11
|
+
|
|
12
|
+
describe("Download Streaming Simple", () => {
|
|
13
|
+
it("Test 1: sync start", async () => {
|
|
14
|
+
const isolate = new ivm.Isolate();
|
|
15
|
+
const context = await isolate.createContext();
|
|
16
|
+
clearAllInstanceState();
|
|
17
|
+
|
|
18
|
+
const timersHandle = await setupTimers(context);
|
|
19
|
+
const fetchHandle = await setupFetch(context);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
context.evalSync(`
|
|
23
|
+
serve({
|
|
24
|
+
async fetch(request) {
|
|
25
|
+
const stream = new ReadableStream({
|
|
26
|
+
start(controller) {
|
|
27
|
+
controller.enqueue(new TextEncoder().encode("chunk1"));
|
|
28
|
+
controller.enqueue(new TextEncoder().encode("chunk2"));
|
|
29
|
+
controller.close();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return new Response(stream);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const response = await fetchHandle.dispatchRequest(
|
|
38
|
+
new Request("http://test/"),
|
|
39
|
+
{ tick: () => timersHandle.tick() }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
assert.strictEqual(text, "chunk1chunk2");
|
|
44
|
+
} finally {
|
|
45
|
+
fetchHandle.dispose();
|
|
46
|
+
timersHandle.dispose();
|
|
47
|
+
clearStreamRegistryForContext(context);
|
|
48
|
+
context.release();
|
|
49
|
+
isolate.dispose();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("Test 2: non-streaming", async () => {
|
|
54
|
+
const isolate = new ivm.Isolate();
|
|
55
|
+
const context = await isolate.createContext();
|
|
56
|
+
clearAllInstanceState();
|
|
57
|
+
|
|
58
|
+
const timersHandle = await setupTimers(context);
|
|
59
|
+
const fetchHandle = await setupFetch(context);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
context.evalSync(`
|
|
63
|
+
serve({
|
|
64
|
+
async fetch(request) {
|
|
65
|
+
return new Response("buffered");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const response = await fetchHandle.dispatchRequest(
|
|
71
|
+
new Request("http://test/")
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
assert.strictEqual(text, "buffered");
|
|
76
|
+
} finally {
|
|
77
|
+
fetchHandle.dispose();
|
|
78
|
+
timersHandle.dispose();
|
|
79
|
+
clearStreamRegistryForContext(context);
|
|
80
|
+
context.release();
|
|
81
|
+
isolate.dispose();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("Test 3: sync start again", async () => {
|
|
86
|
+
const isolate = new ivm.Isolate();
|
|
87
|
+
const context = await isolate.createContext();
|
|
88
|
+
clearAllInstanceState();
|
|
89
|
+
|
|
90
|
+
const timersHandle = await setupTimers(context);
|
|
91
|
+
const fetchHandle = await setupFetch(context);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
context.evalSync(`
|
|
95
|
+
serve({
|
|
96
|
+
async fetch(request) {
|
|
97
|
+
const stream = new ReadableStream({
|
|
98
|
+
start(controller) {
|
|
99
|
+
controller.enqueue(new TextEncoder().encode("test"));
|
|
100
|
+
controller.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return new Response(stream);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
const response = await fetchHandle.dispatchRequest(
|
|
109
|
+
new Request("http://test/"),
|
|
110
|
+
{ tick: () => timersHandle.tick() }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
assert.strictEqual(text, "test");
|
|
115
|
+
} finally {
|
|
116
|
+
fetchHandle.dispose();
|
|
117
|
+
timersHandle.dispose();
|
|
118
|
+
clearStreamRegistryForContext(context);
|
|
119
|
+
context.release();
|
|
120
|
+
isolate.dispose();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("Test 4: pull-based stream", { timeout: 10000 }, async () => {
|
|
125
|
+
const isolate = new ivm.Isolate();
|
|
126
|
+
const context = await isolate.createContext();
|
|
127
|
+
clearAllInstanceState();
|
|
128
|
+
|
|
129
|
+
const timersHandle = await setupTimers(context);
|
|
130
|
+
const fetchHandle = await setupFetch(context);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
context.evalSync(`
|
|
134
|
+
serve({
|
|
135
|
+
async fetch(request) {
|
|
136
|
+
let count = 0;
|
|
137
|
+
const stream = new ReadableStream({
|
|
138
|
+
pull(controller) {
|
|
139
|
+
if (count < 3) {
|
|
140
|
+
controller.enqueue(new TextEncoder().encode("chunk" + count));
|
|
141
|
+
count++;
|
|
142
|
+
} else {
|
|
143
|
+
controller.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return new Response(stream);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
const response = await fetchHandle.dispatchRequest(
|
|
153
|
+
new Request("http://test/"),
|
|
154
|
+
{ tick: () => timersHandle.tick() }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const text = await response.text();
|
|
158
|
+
assert.strictEqual(text, "chunk0chunk1chunk2");
|
|
159
|
+
} finally {
|
|
160
|
+
fetchHandle.dispose();
|
|
161
|
+
timersHandle.dispose();
|
|
162
|
+
clearStreamRegistryForContext(context);
|
|
163
|
+
context.release();
|
|
164
|
+
isolate.dispose();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
import {
|
|
5
|
+
setupFetch,
|
|
6
|
+
clearAllInstanceState,
|
|
7
|
+
type FetchHandle,
|
|
8
|
+
} from "./index.ts";
|
|
9
|
+
import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
|
|
10
|
+
import { clearStreamRegistryForContext } from "./stream-state.ts";
|
|
11
|
+
|
|
12
|
+
describe("Download Streaming", () => {
|
|
13
|
+
let isolate: ivm.Isolate;
|
|
14
|
+
let context: ivm.Context;
|
|
15
|
+
let fetchHandle: FetchHandle;
|
|
16
|
+
let timersHandle: TimersHandle;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
isolate = new ivm.Isolate();
|
|
20
|
+
context = await isolate.createContext();
|
|
21
|
+
clearAllInstanceState();
|
|
22
|
+
|
|
23
|
+
// Setup timers first (needed for setTimeout in stream pump)
|
|
24
|
+
timersHandle = await setupTimers(context);
|
|
25
|
+
|
|
26
|
+
// Then setup fetch
|
|
27
|
+
fetchHandle = await setupFetch(context);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
fetchHandle.dispose();
|
|
32
|
+
timersHandle.dispose();
|
|
33
|
+
clearStreamRegistryForContext(context);
|
|
34
|
+
context.release();
|
|
35
|
+
isolate.dispose();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("Response with ReadableStream body (sync start) streams to native", async () => {
|
|
39
|
+
context.evalSync(`
|
|
40
|
+
serve({
|
|
41
|
+
async fetch(request) {
|
|
42
|
+
const stream = new ReadableStream({
|
|
43
|
+
start(controller) {
|
|
44
|
+
controller.enqueue(new TextEncoder().encode("chunk1"));
|
|
45
|
+
controller.enqueue(new TextEncoder().encode("chunk2"));
|
|
46
|
+
controller.close();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return new Response(stream);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
const response = await fetchHandle.dispatchRequest(
|
|
55
|
+
new Request("http://test/"),
|
|
56
|
+
{ tick: () => timersHandle.tick() }
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
assert.strictEqual(text, "chunk1chunk2");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("Response with ReadableStream body (pull-based) streams to native", async () => {
|
|
64
|
+
context.evalSync(`
|
|
65
|
+
serve({
|
|
66
|
+
async fetch(request) {
|
|
67
|
+
let count = 0;
|
|
68
|
+
const stream = new ReadableStream({
|
|
69
|
+
pull(controller) {
|
|
70
|
+
if (count < 3) {
|
|
71
|
+
controller.enqueue(new TextEncoder().encode("chunk" + count));
|
|
72
|
+
count++;
|
|
73
|
+
} else {
|
|
74
|
+
controller.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return new Response(stream);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
const response = await fetchHandle.dispatchRequest(
|
|
84
|
+
new Request("http://test/"),
|
|
85
|
+
{ tick: () => timersHandle.tick() }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
assert.strictEqual(text, "chunk0chunk1chunk2");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("Response with HostBackedReadableStream body streams to native", async () => {
|
|
93
|
+
context.evalSync(`
|
|
94
|
+
serve({
|
|
95
|
+
async fetch(request) {
|
|
96
|
+
// Create a HostBackedReadableStream manually
|
|
97
|
+
const stream = new HostBackedReadableStream();
|
|
98
|
+
const streamId = stream._getStreamId();
|
|
99
|
+
|
|
100
|
+
// Push data directly to the stream
|
|
101
|
+
__Stream_push(streamId, Array.from(new TextEncoder().encode("host")));
|
|
102
|
+
__Stream_push(streamId, Array.from(new TextEncoder().encode("backed")));
|
|
103
|
+
__Stream_close(streamId);
|
|
104
|
+
|
|
105
|
+
return new Response(stream);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
const response = await fetchHandle.dispatchRequest(
|
|
111
|
+
new Request("http://test/"),
|
|
112
|
+
{ tick: () => timersHandle.tick() }
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const text = await response.text();
|
|
116
|
+
assert.strictEqual(text, "hostbacked");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("delayed streaming response with setTimeout works", async () => {
|
|
120
|
+
context.evalSync(`
|
|
121
|
+
serve({
|
|
122
|
+
async fetch(request) {
|
|
123
|
+
let count = 0;
|
|
124
|
+
const stream = new ReadableStream({
|
|
125
|
+
async pull(controller) {
|
|
126
|
+
if (count < 3) {
|
|
127
|
+
await new Promise(r => setTimeout(r, 10));
|
|
128
|
+
controller.enqueue(new TextEncoder().encode("delayed" + count));
|
|
129
|
+
count++;
|
|
130
|
+
} else {
|
|
131
|
+
controller.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return new Response(stream);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
`);
|
|
139
|
+
|
|
140
|
+
const response = await fetchHandle.dispatchRequest(
|
|
141
|
+
new Request("http://test/"),
|
|
142
|
+
{ tick: () => timersHandle.tick(50) } // Advance 50ms to process timers
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
assert.strictEqual(text, "delayed0delayed1delayed2");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("streaming response preserves headers", async () => {
|
|
150
|
+
context.evalSync(`
|
|
151
|
+
serve({
|
|
152
|
+
async fetch(request) {
|
|
153
|
+
const stream = new ReadableStream({
|
|
154
|
+
start(controller) {
|
|
155
|
+
controller.enqueue(new TextEncoder().encode("test"));
|
|
156
|
+
controller.close();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return new Response(stream, {
|
|
160
|
+
status: 201,
|
|
161
|
+
statusText: "Created",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "text/plain",
|
|
164
|
+
"X-Custom": "value"
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
`);
|
|
170
|
+
|
|
171
|
+
const response = await fetchHandle.dispatchRequest(
|
|
172
|
+
new Request("http://test/"),
|
|
173
|
+
{ tick: () => timersHandle.tick() }
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(response.status, 201);
|
|
177
|
+
assert.strictEqual(response.headers.get("Content-Type"), "text/plain");
|
|
178
|
+
assert.strictEqual(response.headers.get("X-Custom"), "value");
|
|
179
|
+
assert.strictEqual(await response.text(), "test");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("stream error propagates to native consumer", async () => {
|
|
183
|
+
context.evalSync(`
|
|
184
|
+
serve({
|
|
185
|
+
async fetch(request) {
|
|
186
|
+
let count = 0;
|
|
187
|
+
const stream = new ReadableStream({
|
|
188
|
+
pull(controller) {
|
|
189
|
+
if (count < 1) {
|
|
190
|
+
controller.enqueue(new TextEncoder().encode("ok"));
|
|
191
|
+
count++;
|
|
192
|
+
} else {
|
|
193
|
+
controller.error(new Error("Stream failed"));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return new Response(stream);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
`);
|
|
201
|
+
|
|
202
|
+
const response = await fetchHandle.dispatchRequest(
|
|
203
|
+
new Request("http://test/"),
|
|
204
|
+
{ tick: () => timersHandle.tick() }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// The first chunk should be readable
|
|
208
|
+
const reader = response.body!.getReader();
|
|
209
|
+
const firstRead = await reader.read();
|
|
210
|
+
assert.strictEqual(firstRead.done, false);
|
|
211
|
+
assert.deepStrictEqual(
|
|
212
|
+
new TextDecoder().decode(firstRead.value),
|
|
213
|
+
"ok"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Second read should throw
|
|
217
|
+
await assert.rejects(reader.read());
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("multiple chunks read sequentially", async () => {
|
|
221
|
+
context.evalSync(`
|
|
222
|
+
serve({
|
|
223
|
+
async fetch(request) {
|
|
224
|
+
const stream = new ReadableStream({
|
|
225
|
+
start(controller) {
|
|
226
|
+
controller.enqueue(new TextEncoder().encode("a"));
|
|
227
|
+
controller.enqueue(new TextEncoder().encode("b"));
|
|
228
|
+
controller.enqueue(new TextEncoder().encode("c"));
|
|
229
|
+
controller.close();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return new Response(stream);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
`);
|
|
236
|
+
|
|
237
|
+
const response = await fetchHandle.dispatchRequest(
|
|
238
|
+
new Request("http://test/"),
|
|
239
|
+
{ tick: () => timersHandle.tick() }
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const reader = response.body!.getReader();
|
|
243
|
+
const chunks: string[] = [];
|
|
244
|
+
|
|
245
|
+
while (true) {
|
|
246
|
+
const { done, value } = await reader.read();
|
|
247
|
+
if (done) break;
|
|
248
|
+
chunks.push(new TextDecoder().decode(value));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
assert.deepStrictEqual(chunks, ["a", "b", "c"]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("non-streaming response still works", async () => {
|
|
255
|
+
context.evalSync(`
|
|
256
|
+
serve({
|
|
257
|
+
async fetch(request) {
|
|
258
|
+
return new Response("buffered response");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
`);
|
|
262
|
+
|
|
263
|
+
const response = await fetchHandle.dispatchRequest(
|
|
264
|
+
new Request("http://test/")
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
assert.strictEqual(await response.text(), "buffered response");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("Response.json() still works", async () => {
|
|
271
|
+
context.evalSync(`
|
|
272
|
+
serve({
|
|
273
|
+
async fetch(request) {
|
|
274
|
+
return Response.json({ hello: "world" });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
const response = await fetchHandle.dispatchRequest(
|
|
280
|
+
new Request("http://test/")
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
assert.deepStrictEqual(data, { hello: "world" });
|
|
285
|
+
});
|
|
286
|
+
});
|