@ricsam/isolate-fetch 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/cjs/index.cjs +1800 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/cjs/stream-state.cjs +230 -0
- package/dist/cjs/stream-state.cjs.map +10 -0
- package/{src/index.ts → dist/mjs/index.mjs} +357 -923
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/mjs/stream-state.mjs +199 -0
- package/dist/mjs/stream-state.mjs.map +10 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/isolate.d.ts +267 -0
- package/dist/types/stream-state.d.ts +61 -0
- package/package.json +41 -13
- package/CHANGELOG.md +0 -9
- package/src/debug-delayed.test.ts +0 -89
- package/src/debug-streaming.test.ts +0 -81
- package/src/download-streaming-simple.test.ts +0 -167
- package/src/download-streaming.test.ts +0 -286
- package/src/form-data.test.ts +0 -824
- package/src/formdata.test.ts +0 -212
- package/src/headers.test.ts +0 -582
- package/src/host-backed-stream.test.ts +0 -363
- package/src/index.test.ts +0 -274
- package/src/integration.test.ts +0 -665
- package/src/request.test.ts +0 -482
- package/src/response.test.ts +0 -520
- package/src/serve.test.ts +0 -425
- package/src/stream-state.test.ts +0 -338
- package/src/stream-state.ts +0 -337
- package/src/upload-streaming.test.ts +0 -373
- package/src/websocket.test.ts +0 -627
- package/tsconfig.json +0 -8
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upload Streaming Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for streaming request bodies from native (Node.js) to isolate.
|
|
5
|
-
*
|
|
6
|
-
* Note: These tests may generate warnings about async activity after test ends.
|
|
7
|
-
* This is a known limitation in the stream cleanup code (stream-state.ts) where
|
|
8
|
-
* the native stream reader cancel operation is not properly awaited. The tests
|
|
9
|
-
* themselves pass correctly - the warnings are informational only.
|
|
10
|
-
*/
|
|
11
|
-
import { test, describe, beforeEach, afterEach, it } from "node:test";
|
|
12
|
-
import assert from "node:assert";
|
|
13
|
-
import ivm from "isolated-vm";
|
|
14
|
-
import {
|
|
15
|
-
setupFetch,
|
|
16
|
-
clearAllInstanceState,
|
|
17
|
-
type FetchHandle,
|
|
18
|
-
} from "./index.ts";
|
|
19
|
-
import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
|
|
20
|
-
import { clearStreamRegistryForContext } from "./stream-state.ts";
|
|
21
|
-
|
|
22
|
-
describe("Upload Streaming", () => {
|
|
23
|
-
let isolate: ivm.Isolate;
|
|
24
|
-
let context: ivm.Context;
|
|
25
|
-
let fetchHandle: FetchHandle;
|
|
26
|
-
let timersHandle: TimersHandle;
|
|
27
|
-
|
|
28
|
-
beforeEach(async () => {
|
|
29
|
-
isolate = new ivm.Isolate();
|
|
30
|
-
context = await isolate.createContext();
|
|
31
|
-
clearAllInstanceState();
|
|
32
|
-
timersHandle = await setupTimers(context);
|
|
33
|
-
fetchHandle = await setupFetch(context);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
fetchHandle.dispose();
|
|
38
|
-
timersHandle.dispose();
|
|
39
|
-
clearStreamRegistryForContext(context);
|
|
40
|
-
context.release();
|
|
41
|
-
isolate.dispose();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("request.text() consumes streaming body", async () => {
|
|
45
|
-
context.evalSync(`
|
|
46
|
-
serve({
|
|
47
|
-
async fetch(request) {
|
|
48
|
-
const text = await request.text();
|
|
49
|
-
return new Response("Received: " + text);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
`);
|
|
53
|
-
|
|
54
|
-
const chunks = ["Hello ", "World ", "Stream!"];
|
|
55
|
-
let index = 0;
|
|
56
|
-
const stream = new ReadableStream({
|
|
57
|
-
pull(controller) {
|
|
58
|
-
if (index < chunks.length) {
|
|
59
|
-
controller.enqueue(new TextEncoder().encode(chunks[index++]));
|
|
60
|
-
} else {
|
|
61
|
-
controller.close();
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const request = new Request("http://test/", {
|
|
67
|
-
method: "POST",
|
|
68
|
-
body: stream,
|
|
69
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
70
|
-
duplex: "half",
|
|
71
|
-
});
|
|
72
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
73
|
-
tick: () => timersHandle.tick(),
|
|
74
|
-
});
|
|
75
|
-
assert.strictEqual(await response.text(), "Received: Hello World Stream!");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("request.json() consumes streaming JSON body", async () => {
|
|
79
|
-
context.evalSync(`
|
|
80
|
-
serve({
|
|
81
|
-
async fetch(request) {
|
|
82
|
-
const data = await request.json();
|
|
83
|
-
return Response.json({ received: data });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
`);
|
|
87
|
-
|
|
88
|
-
const jsonParts = ['{"foo":', '"bar",', '"num":', "42}"];
|
|
89
|
-
let index = 0;
|
|
90
|
-
const stream = new ReadableStream({
|
|
91
|
-
pull(controller) {
|
|
92
|
-
if (index < jsonParts.length) {
|
|
93
|
-
controller.enqueue(new TextEncoder().encode(jsonParts[index++]));
|
|
94
|
-
} else {
|
|
95
|
-
controller.close();
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const request = new Request("http://test/", {
|
|
101
|
-
method: "POST",
|
|
102
|
-
body: stream,
|
|
103
|
-
headers: { "Content-Type": "application/json" },
|
|
104
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
105
|
-
duplex: "half",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
109
|
-
tick: () => timersHandle.tick(),
|
|
110
|
-
});
|
|
111
|
-
const json = (await response.json()) as { received: { foo: string; num: number } };
|
|
112
|
-
assert.deepStrictEqual(json.received, { foo: "bar", num: 42 });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("request.arrayBuffer() consumes streaming body", async () => {
|
|
116
|
-
context.evalSync(`
|
|
117
|
-
serve({
|
|
118
|
-
async fetch(request) {
|
|
119
|
-
const buffer = await request.arrayBuffer();
|
|
120
|
-
return new Response("Length: " + buffer.byteLength);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
`);
|
|
124
|
-
|
|
125
|
-
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
126
|
-
let offset = 0;
|
|
127
|
-
const stream = new ReadableStream({
|
|
128
|
-
pull(controller) {
|
|
129
|
-
if (offset < data.length) {
|
|
130
|
-
controller.enqueue(data.slice(offset, offset + 3));
|
|
131
|
-
offset += 3;
|
|
132
|
-
} else {
|
|
133
|
-
controller.close();
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const request = new Request("http://test/", {
|
|
139
|
-
method: "POST",
|
|
140
|
-
body: stream,
|
|
141
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
142
|
-
duplex: "half",
|
|
143
|
-
});
|
|
144
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
145
|
-
tick: () => timersHandle.tick(),
|
|
146
|
-
});
|
|
147
|
-
assert.strictEqual(await response.text(), "Length: 10");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("request.body is readable stream for streaming uploads", async () => {
|
|
151
|
-
context.evalSync(`
|
|
152
|
-
serve({
|
|
153
|
-
async fetch(request) {
|
|
154
|
-
const isStream = request.body instanceof HostBackedReadableStream;
|
|
155
|
-
if (!isStream) {
|
|
156
|
-
return new Response("Not a stream: " + typeof request.body);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const reader = request.body.getReader();
|
|
160
|
-
const chunks = [];
|
|
161
|
-
while (true) {
|
|
162
|
-
const { done, value } = await reader.read();
|
|
163
|
-
if (done) break;
|
|
164
|
-
chunks.push(new TextDecoder().decode(value));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return new Response("isStream: true, text: " + chunks.join(""));
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
`);
|
|
171
|
-
|
|
172
|
-
let count = 0;
|
|
173
|
-
const stream = new ReadableStream({
|
|
174
|
-
pull(controller) {
|
|
175
|
-
if (count < 3) {
|
|
176
|
-
controller.enqueue(new TextEncoder().encode(`chunk${count}`));
|
|
177
|
-
count++;
|
|
178
|
-
} else {
|
|
179
|
-
controller.close();
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const request = new Request("http://test/", {
|
|
185
|
-
method: "POST",
|
|
186
|
-
body: stream,
|
|
187
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
188
|
-
duplex: "half",
|
|
189
|
-
});
|
|
190
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
191
|
-
tick: () => timersHandle.tick(),
|
|
192
|
-
});
|
|
193
|
-
assert.strictEqual(
|
|
194
|
-
await response.text(),
|
|
195
|
-
"isStream: true, text: chunk0chunk1chunk2"
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("large streaming upload (1MB) with backpressure", async () => {
|
|
200
|
-
context.evalSync(`
|
|
201
|
-
serve({
|
|
202
|
-
async fetch(request) {
|
|
203
|
-
const buffer = await request.arrayBuffer();
|
|
204
|
-
return new Response("Bytes: " + buffer.byteLength);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
`);
|
|
208
|
-
|
|
209
|
-
const chunkSize = 64 * 1024;
|
|
210
|
-
const totalSize = 1024 * 1024;
|
|
211
|
-
let generated = 0;
|
|
212
|
-
|
|
213
|
-
const stream = new ReadableStream({
|
|
214
|
-
pull(controller) {
|
|
215
|
-
if (generated < totalSize) {
|
|
216
|
-
const size = Math.min(chunkSize, totalSize - generated);
|
|
217
|
-
controller.enqueue(new Uint8Array(size).fill(0x42));
|
|
218
|
-
generated += size;
|
|
219
|
-
} else {
|
|
220
|
-
controller.close();
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const request = new Request("http://test/", {
|
|
226
|
-
method: "POST",
|
|
227
|
-
body: stream,
|
|
228
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
229
|
-
duplex: "half",
|
|
230
|
-
});
|
|
231
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
232
|
-
tick: () => timersHandle.tick(),
|
|
233
|
-
});
|
|
234
|
-
assert.strictEqual(await response.text(), "Bytes: 1048576");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("streaming upload with multiple small chunks", async () => {
|
|
238
|
-
context.evalSync(`
|
|
239
|
-
serve({
|
|
240
|
-
async fetch(request) {
|
|
241
|
-
const reader = request.body.getReader();
|
|
242
|
-
let chunkCount = 0;
|
|
243
|
-
let totalBytes = 0;
|
|
244
|
-
|
|
245
|
-
while (true) {
|
|
246
|
-
const { done, value } = await reader.read();
|
|
247
|
-
if (done) break;
|
|
248
|
-
chunkCount++;
|
|
249
|
-
totalBytes += value.length;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return Response.json({ chunkCount, totalBytes });
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
`);
|
|
256
|
-
|
|
257
|
-
let chunksSent = 0;
|
|
258
|
-
const numChunks = 10;
|
|
259
|
-
const stream = new ReadableStream({
|
|
260
|
-
pull(controller) {
|
|
261
|
-
if (chunksSent < numChunks) {
|
|
262
|
-
controller.enqueue(new Uint8Array([chunksSent]));
|
|
263
|
-
chunksSent++;
|
|
264
|
-
} else {
|
|
265
|
-
controller.close();
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
const request = new Request("http://test/", {
|
|
271
|
-
method: "POST",
|
|
272
|
-
body: stream,
|
|
273
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
274
|
-
duplex: "half",
|
|
275
|
-
});
|
|
276
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
277
|
-
tick: () => timersHandle.tick(),
|
|
278
|
-
});
|
|
279
|
-
const result = (await response.json()) as { chunkCount: number; totalBytes: number };
|
|
280
|
-
assert.strictEqual(result.chunkCount, 10);
|
|
281
|
-
assert.strictEqual(result.totalBytes, 10);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("handles empty streaming body", async () => {
|
|
285
|
-
context.evalSync(`
|
|
286
|
-
serve({
|
|
287
|
-
async fetch(request) {
|
|
288
|
-
const text = await request.text();
|
|
289
|
-
return new Response("Empty: " + (text.length === 0));
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
`);
|
|
293
|
-
|
|
294
|
-
const stream = new ReadableStream({
|
|
295
|
-
start(controller) {
|
|
296
|
-
controller.close();
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const request = new Request("http://test/", {
|
|
301
|
-
method: "POST",
|
|
302
|
-
body: stream,
|
|
303
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
304
|
-
duplex: "half",
|
|
305
|
-
});
|
|
306
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
307
|
-
tick: () => timersHandle.tick(),
|
|
308
|
-
});
|
|
309
|
-
assert.strictEqual(await response.text(), "Empty: true");
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("binary data is preserved in streaming upload", async () => {
|
|
313
|
-
context.evalSync(`
|
|
314
|
-
serve({
|
|
315
|
-
async fetch(request) {
|
|
316
|
-
const buffer = await request.arrayBuffer();
|
|
317
|
-
const bytes = new Uint8Array(buffer);
|
|
318
|
-
const sum = bytes.reduce((a, b) => a + b, 0);
|
|
319
|
-
return Response.json({
|
|
320
|
-
length: buffer.byteLength,
|
|
321
|
-
sum: sum
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
`);
|
|
326
|
-
|
|
327
|
-
// Create specific binary data
|
|
328
|
-
const data = new Uint8Array([0x00, 0xff, 0x80, 0x01, 0xfe]);
|
|
329
|
-
let sent = false;
|
|
330
|
-
const stream = new ReadableStream({
|
|
331
|
-
pull(controller) {
|
|
332
|
-
if (!sent) {
|
|
333
|
-
controller.enqueue(data);
|
|
334
|
-
sent = true;
|
|
335
|
-
} else {
|
|
336
|
-
controller.close();
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const request = new Request("http://test/", {
|
|
342
|
-
method: "POST",
|
|
343
|
-
body: stream,
|
|
344
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
345
|
-
duplex: "half",
|
|
346
|
-
});
|
|
347
|
-
const response = await fetchHandle.dispatchRequest(request, {
|
|
348
|
-
tick: () => timersHandle.tick(),
|
|
349
|
-
});
|
|
350
|
-
const result = (await response.json()) as { length: number; sum: number };
|
|
351
|
-
assert.strictEqual(result.length, 5);
|
|
352
|
-
// 0x00 + 0xff + 0x80 + 0x01 + 0xfe = 0 + 255 + 128 + 1 + 254 = 638
|
|
353
|
-
assert.strictEqual(result.sum, 638);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("request with non-streaming body still works", async () => {
|
|
357
|
-
context.evalSync(`
|
|
358
|
-
serve({
|
|
359
|
-
async fetch(request) {
|
|
360
|
-
const text = await request.text();
|
|
361
|
-
return new Response("Got: " + text);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
`);
|
|
365
|
-
|
|
366
|
-
const request = new Request("http://test/", {
|
|
367
|
-
method: "POST",
|
|
368
|
-
body: "non-streaming body",
|
|
369
|
-
});
|
|
370
|
-
const response = await fetchHandle.dispatchRequest(request);
|
|
371
|
-
assert.strictEqual(await response.text(), "Got: non-streaming body");
|
|
372
|
-
});
|
|
373
|
-
});
|