@ricsam/isolate-fs 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 +52 -0
- package/dist/cjs/index.cjs +752 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/node-adapter.cjs +230 -0
- package/dist/cjs/node-adapter.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/index.mjs +708 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/node-adapter.mjs +186 -0
- package/dist/mjs/node-adapter.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/isolate.d.ts +308 -0
- package/dist/types/node-adapter.d.ts +24 -0
- package/package.json +41 -15
- package/CHANGELOG.md +0 -9
- package/src/fixtures/test-image.png +0 -0
- package/src/index.test.ts +0 -882
- package/src/index.ts +0 -997
- package/src/integration.test.ts +0 -288
- package/src/node-adapter.test.ts +0 -337
- package/src/node-adapter.ts +0 -300
- package/src/streaming.test.ts +0 -634
- package/tsconfig.json +0 -8
package/src/streaming.test.ts
DELETED
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Streaming Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests to verify that file uploads and downloads are truly streamed
|
|
5
|
-
* (not buffered) through the entire pipeline:
|
|
6
|
-
*
|
|
7
|
-
* Upload: Host ReadableStream -> Isolate request.body -> FileHandle.createWritable() -> Host writes to disk
|
|
8
|
-
* Download: Host request -> Isolate FileHandle.getFile() -> Host reads from disk -> Isolate Response -> Host
|
|
9
|
-
*
|
|
10
|
-
* These tests use instrumented FileSystemHandlers to track chunk-by-chunk streaming.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, test, afterEach } from "node:test";
|
|
13
|
-
import assert from "node:assert";
|
|
14
|
-
import { createRuntime, type RuntimeHandle } from "@ricsam/isolate-runtime";
|
|
15
|
-
import type { FileSystemHandler } from "./index.ts";
|
|
16
|
-
|
|
17
|
-
describe("Streaming Integration Tests", () => {
|
|
18
|
-
let runtime: RuntimeHandle | undefined;
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
if (runtime) {
|
|
22
|
-
runtime.dispose();
|
|
23
|
-
runtime = undefined;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("Upload Streaming (Host -> Isolate -> Filesystem)", () => {
|
|
28
|
-
test("streaming upload writes chunks progressively (not buffered)", async () => {
|
|
29
|
-
// Track all writeFile calls with timestamps
|
|
30
|
-
const writeCalls: Array<{ data: Uint8Array; timestamp: number }> = [];
|
|
31
|
-
|
|
32
|
-
const mockHandler: FileSystemHandler = {
|
|
33
|
-
async getFileHandle(path, options) {
|
|
34
|
-
// Allow file creation
|
|
35
|
-
},
|
|
36
|
-
async getDirectoryHandle() {},
|
|
37
|
-
async removeEntry() {},
|
|
38
|
-
async readDirectory() {
|
|
39
|
-
return [];
|
|
40
|
-
},
|
|
41
|
-
async readFile(path) {
|
|
42
|
-
// Combine all writes
|
|
43
|
-
const totalSize = writeCalls.reduce((sum, w) => sum + w.data.length, 0);
|
|
44
|
-
const combined = new Uint8Array(totalSize);
|
|
45
|
-
let offset = 0;
|
|
46
|
-
for (const write of writeCalls) {
|
|
47
|
-
combined.set(write.data, offset);
|
|
48
|
-
offset += write.data.length;
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
data: combined,
|
|
52
|
-
size: totalSize,
|
|
53
|
-
lastModified: Date.now(),
|
|
54
|
-
type: "application/octet-stream",
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
async writeFile(path, data) {
|
|
58
|
-
writeCalls.push({ data: new Uint8Array(data), timestamp: Date.now() });
|
|
59
|
-
},
|
|
60
|
-
async truncate() {},
|
|
61
|
-
async isSameEntry() {
|
|
62
|
-
return false;
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
runtime = await createRuntime({
|
|
67
|
-
console: { onLog: () => {} },
|
|
68
|
-
fs: { getDirectory: async () => mockHandler },
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Set up a server that reads request body chunk-by-chunk and writes to filesystem
|
|
72
|
-
await runtime.context.eval(
|
|
73
|
-
`
|
|
74
|
-
serve({
|
|
75
|
-
async fetch(request) {
|
|
76
|
-
const root = await getDirectory("/");
|
|
77
|
-
const fileHandle = await root.getFileHandle("upload.bin", { create: true });
|
|
78
|
-
const writable = await fileHandle.createWritable();
|
|
79
|
-
|
|
80
|
-
// Read request body chunk-by-chunk (streaming)
|
|
81
|
-
const reader = request.body.getReader();
|
|
82
|
-
let totalBytes = 0;
|
|
83
|
-
let chunkCount = 0;
|
|
84
|
-
|
|
85
|
-
while (true) {
|
|
86
|
-
const { done, value } = await reader.read();
|
|
87
|
-
if (done) break;
|
|
88
|
-
await writable.write(value);
|
|
89
|
-
totalBytes += value.length;
|
|
90
|
-
chunkCount++;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
await writable.close();
|
|
94
|
-
return Response.json({ totalBytes, chunkCount });
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
`,
|
|
98
|
-
{ promise: true }
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Create a streaming request with multiple chunks
|
|
102
|
-
const numChunks = 5;
|
|
103
|
-
const chunkSize = 1024;
|
|
104
|
-
let chunksSent = 0;
|
|
105
|
-
|
|
106
|
-
const stream = new ReadableStream({
|
|
107
|
-
pull(controller) {
|
|
108
|
-
if (chunksSent < numChunks) {
|
|
109
|
-
// Each chunk has distinct content for verification
|
|
110
|
-
const chunk = new Uint8Array(chunkSize).fill(chunksSent + 1);
|
|
111
|
-
controller.enqueue(chunk);
|
|
112
|
-
chunksSent++;
|
|
113
|
-
} else {
|
|
114
|
-
controller.close();
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const request = new Request("http://test/upload", {
|
|
120
|
-
method: "POST",
|
|
121
|
-
body: stream,
|
|
122
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
123
|
-
duplex: "half",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const response = await runtime.fetch.dispatchRequest(request, {
|
|
127
|
-
tick: () => runtime!.tick(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const result = (await response.json()) as {
|
|
131
|
-
totalBytes: number;
|
|
132
|
-
chunkCount: number;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// Verify the isolate received multiple chunks (not one buffered blob)
|
|
136
|
-
assert.strictEqual(result.totalBytes, numChunks * chunkSize);
|
|
137
|
-
assert.strictEqual(
|
|
138
|
-
result.chunkCount,
|
|
139
|
-
numChunks,
|
|
140
|
-
`Expected ${numChunks} chunks but got ${result.chunkCount} - data was buffered instead of streamed`
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Verify the filesystem received multiple write calls (streaming)
|
|
144
|
-
assert.strictEqual(
|
|
145
|
-
writeCalls.length,
|
|
146
|
-
numChunks,
|
|
147
|
-
`Expected ${numChunks} writeFile calls but got ${writeCalls.length} - filesystem writes were buffered`
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Verify each chunk has correct content
|
|
151
|
-
for (let i = 0; i < writeCalls.length; i++) {
|
|
152
|
-
assert.strictEqual(writeCalls[i]!.data.length, chunkSize);
|
|
153
|
-
assert.strictEqual(writeCalls[i]!.data[0], i + 1);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("large file upload streams without buffering entire file in memory", async () => {
|
|
158
|
-
const writeCalls: Array<{ size: number; timestamp: number }> = [];
|
|
159
|
-
let maxMemoryAtOnce = 0;
|
|
160
|
-
let currentMemory = 0;
|
|
161
|
-
|
|
162
|
-
const mockHandler: FileSystemHandler = {
|
|
163
|
-
async getFileHandle() {},
|
|
164
|
-
async getDirectoryHandle() {},
|
|
165
|
-
async removeEntry() {},
|
|
166
|
-
async readDirectory() {
|
|
167
|
-
return [];
|
|
168
|
-
},
|
|
169
|
-
async readFile() {
|
|
170
|
-
return {
|
|
171
|
-
data: new Uint8Array(0),
|
|
172
|
-
size: 0,
|
|
173
|
-
lastModified: Date.now(),
|
|
174
|
-
type: "application/octet-stream",
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
async writeFile(path, data) {
|
|
178
|
-
currentMemory += data.length;
|
|
179
|
-
maxMemoryAtOnce = Math.max(maxMemoryAtOnce, currentMemory);
|
|
180
|
-
writeCalls.push({ size: data.length, timestamp: Date.now() });
|
|
181
|
-
// Simulate write completing (memory released)
|
|
182
|
-
currentMemory -= data.length;
|
|
183
|
-
},
|
|
184
|
-
async truncate() {},
|
|
185
|
-
async isSameEntry() {
|
|
186
|
-
return false;
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
runtime = await createRuntime({
|
|
191
|
-
console: { onLog: () => {} },
|
|
192
|
-
fs: { getDirectory: async () => mockHandler },
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
await runtime.context.eval(
|
|
196
|
-
`
|
|
197
|
-
serve({
|
|
198
|
-
async fetch(request) {
|
|
199
|
-
const root = await getDirectory("/");
|
|
200
|
-
const fileHandle = await root.getFileHandle("large.bin", { create: true });
|
|
201
|
-
const writable = await fileHandle.createWritable();
|
|
202
|
-
|
|
203
|
-
const reader = request.body.getReader();
|
|
204
|
-
while (true) {
|
|
205
|
-
const { done, value } = await reader.read();
|
|
206
|
-
if (done) break;
|
|
207
|
-
await writable.write(value);
|
|
208
|
-
}
|
|
209
|
-
await writable.close();
|
|
210
|
-
return new Response("OK");
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
`,
|
|
214
|
-
{ promise: true }
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// Stream 1MB in 64KB chunks
|
|
218
|
-
const totalSize = 1024 * 1024;
|
|
219
|
-
const chunkSize = 64 * 1024;
|
|
220
|
-
let generated = 0;
|
|
221
|
-
|
|
222
|
-
const stream = new ReadableStream({
|
|
223
|
-
pull(controller) {
|
|
224
|
-
if (generated < totalSize) {
|
|
225
|
-
const size = Math.min(chunkSize, totalSize - generated);
|
|
226
|
-
controller.enqueue(new Uint8Array(size).fill(0x42));
|
|
227
|
-
generated += size;
|
|
228
|
-
} else {
|
|
229
|
-
controller.close();
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const request = new Request("http://test/upload", {
|
|
235
|
-
method: "POST",
|
|
236
|
-
body: stream,
|
|
237
|
-
// @ts-expect-error Node.js requires duplex for streaming bodies
|
|
238
|
-
duplex: "half",
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await runtime.fetch.dispatchRequest(request, {
|
|
242
|
-
tick: () => runtime!.tick(),
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Should have multiple write calls (streaming behavior)
|
|
246
|
-
const expectedChunks = Math.ceil(totalSize / chunkSize);
|
|
247
|
-
assert.ok(
|
|
248
|
-
writeCalls.length >= expectedChunks,
|
|
249
|
-
`Expected at least ${expectedChunks} writes but got ${writeCalls.length}`
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
// Max memory should be much less than total file size (streaming)
|
|
253
|
-
// Allow for some buffering but not the entire file
|
|
254
|
-
assert.ok(
|
|
255
|
-
maxMemoryAtOnce < totalSize / 2,
|
|
256
|
-
`Max memory ${maxMemoryAtOnce} exceeded half of total size ${totalSize} - file was buffered`
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe("Download Streaming (Filesystem -> Isolate -> Host)", () => {
|
|
262
|
-
test("streaming download sends chunks progressively (not buffered)", async () => {
|
|
263
|
-
const numChunks = 5;
|
|
264
|
-
const chunkSize = 1024;
|
|
265
|
-
let readCallCount = 0;
|
|
266
|
-
|
|
267
|
-
// Create data as multiple chunks
|
|
268
|
-
const chunks: Uint8Array[] = [];
|
|
269
|
-
for (let i = 0; i < numChunks; i++) {
|
|
270
|
-
chunks.push(new Uint8Array(chunkSize).fill(i + 1));
|
|
271
|
-
}
|
|
272
|
-
const totalData = new Uint8Array(numChunks * chunkSize);
|
|
273
|
-
let offset = 0;
|
|
274
|
-
for (const chunk of chunks) {
|
|
275
|
-
totalData.set(chunk, offset);
|
|
276
|
-
offset += chunk.length;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const mockHandler: FileSystemHandler = {
|
|
280
|
-
async getFileHandle() {},
|
|
281
|
-
async getDirectoryHandle() {},
|
|
282
|
-
async removeEntry() {},
|
|
283
|
-
async readDirectory() {
|
|
284
|
-
return [];
|
|
285
|
-
},
|
|
286
|
-
async readFile() {
|
|
287
|
-
readCallCount++;
|
|
288
|
-
return {
|
|
289
|
-
data: totalData,
|
|
290
|
-
size: totalData.length,
|
|
291
|
-
lastModified: Date.now(),
|
|
292
|
-
type: "application/octet-stream",
|
|
293
|
-
};
|
|
294
|
-
},
|
|
295
|
-
async writeFile() {},
|
|
296
|
-
async truncate() {},
|
|
297
|
-
async isSameEntry() {
|
|
298
|
-
return false;
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
runtime = await createRuntime({
|
|
303
|
-
console: { onLog: () => {} },
|
|
304
|
-
fs: { getDirectory: async () => mockHandler },
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
await runtime.context.eval(
|
|
308
|
-
`
|
|
309
|
-
serve({
|
|
310
|
-
async fetch(request) {
|
|
311
|
-
const root = await getDirectory("/");
|
|
312
|
-
const fileHandle = await root.getFileHandle("download.bin");
|
|
313
|
-
const file = await fileHandle.getFile();
|
|
314
|
-
|
|
315
|
-
// Return file directly - should stream
|
|
316
|
-
return new Response(file);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
`,
|
|
320
|
-
{ promise: true }
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
const request = new Request("http://test/download");
|
|
324
|
-
const response = await runtime.fetch.dispatchRequest(request, {
|
|
325
|
-
tick: () => runtime!.tick(),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Read response body chunk-by-chunk to verify streaming
|
|
329
|
-
const reader = response.body!.getReader();
|
|
330
|
-
const receivedChunks: Uint8Array[] = [];
|
|
331
|
-
|
|
332
|
-
while (true) {
|
|
333
|
-
const { done, value } = await reader.read();
|
|
334
|
-
if (done) break;
|
|
335
|
-
receivedChunks.push(value);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Verify we received the correct data
|
|
339
|
-
const totalReceived = receivedChunks.reduce((sum, c) => sum + c.length, 0);
|
|
340
|
-
assert.strictEqual(totalReceived, numChunks * chunkSize);
|
|
341
|
-
|
|
342
|
-
// Verify content is correct
|
|
343
|
-
const combined = new Uint8Array(totalReceived);
|
|
344
|
-
let combineOffset = 0;
|
|
345
|
-
for (const chunk of receivedChunks) {
|
|
346
|
-
combined.set(chunk, combineOffset);
|
|
347
|
-
combineOffset += chunk.length;
|
|
348
|
-
}
|
|
349
|
-
assert.deepStrictEqual(combined, totalData);
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
describe("WHATWG Compliance - Response(file) streaming", () => {
|
|
354
|
-
test("new Response(file) uses streaming (not buffered)", async () => {
|
|
355
|
-
// This test verifies that when Response(file) is used,
|
|
356
|
-
// the file content is streamed, not loaded entirely into memory first
|
|
357
|
-
|
|
358
|
-
const fileSize = 1024 * 1024; // 1MB
|
|
359
|
-
const fileData = new Uint8Array(fileSize).fill(0x42);
|
|
360
|
-
let readFileCallCount = 0;
|
|
361
|
-
|
|
362
|
-
const mockHandler: FileSystemHandler = {
|
|
363
|
-
async getFileHandle() {},
|
|
364
|
-
async getDirectoryHandle() {},
|
|
365
|
-
async removeEntry() {},
|
|
366
|
-
async readDirectory() {
|
|
367
|
-
return [];
|
|
368
|
-
},
|
|
369
|
-
async readFile() {
|
|
370
|
-
readFileCallCount++;
|
|
371
|
-
return {
|
|
372
|
-
data: fileData,
|
|
373
|
-
size: fileSize,
|
|
374
|
-
lastModified: Date.now(),
|
|
375
|
-
type: "application/octet-stream",
|
|
376
|
-
};
|
|
377
|
-
},
|
|
378
|
-
async writeFile() {},
|
|
379
|
-
async truncate() {},
|
|
380
|
-
async isSameEntry() {
|
|
381
|
-
return false;
|
|
382
|
-
},
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
runtime = await createRuntime({
|
|
386
|
-
console: { onLog: () => {} },
|
|
387
|
-
fs: { getDirectory: async () => mockHandler },
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
await runtime.context.eval(
|
|
391
|
-
`
|
|
392
|
-
serve({
|
|
393
|
-
async fetch(request) {
|
|
394
|
-
const root = await getDirectory("/");
|
|
395
|
-
const fileHandle = await root.getFileHandle("large.bin");
|
|
396
|
-
const file = await fileHandle.getFile();
|
|
397
|
-
|
|
398
|
-
// WHATWG spec: Response(file) should stream the file body
|
|
399
|
-
return new Response(file, {
|
|
400
|
-
headers: { "Content-Type": file.type }
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
`,
|
|
405
|
-
{ promise: true }
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
const request = new Request("http://test/file");
|
|
409
|
-
const response = await runtime.fetch.dispatchRequest(request, {
|
|
410
|
-
tick: () => runtime!.tick(),
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Read first chunk only
|
|
414
|
-
const reader = response.body!.getReader();
|
|
415
|
-
const { value: firstChunk } = await reader.read();
|
|
416
|
-
reader.releaseLock();
|
|
417
|
-
|
|
418
|
-
// File should have been read from disk
|
|
419
|
-
assert.ok(readFileCallCount >= 1, "readFile should have been called");
|
|
420
|
-
|
|
421
|
-
// First chunk should exist and have correct content
|
|
422
|
-
assert.ok(firstChunk, "Should receive at least one chunk");
|
|
423
|
-
assert.strictEqual(firstChunk[0], 0x42);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test("file.stream() returns a ReadableStream for chunk-by-chunk reading", async () => {
|
|
427
|
-
const fileData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
428
|
-
|
|
429
|
-
const mockHandler: FileSystemHandler = {
|
|
430
|
-
async getFileHandle() {},
|
|
431
|
-
async getDirectoryHandle() {},
|
|
432
|
-
async removeEntry() {},
|
|
433
|
-
async readDirectory() {
|
|
434
|
-
return [];
|
|
435
|
-
},
|
|
436
|
-
async readFile() {
|
|
437
|
-
return {
|
|
438
|
-
data: fileData,
|
|
439
|
-
size: fileData.length,
|
|
440
|
-
lastModified: Date.now(),
|
|
441
|
-
type: "application/octet-stream",
|
|
442
|
-
};
|
|
443
|
-
},
|
|
444
|
-
async writeFile() {},
|
|
445
|
-
async truncate() {},
|
|
446
|
-
async isSameEntry() {
|
|
447
|
-
return false;
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
runtime = await createRuntime({
|
|
452
|
-
console: { onLog: () => {} },
|
|
453
|
-
fs: { getDirectory: async () => mockHandler },
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const result = await runtime.context.eval(
|
|
457
|
-
`
|
|
458
|
-
(async () => {
|
|
459
|
-
const root = await getDirectory("/");
|
|
460
|
-
const fileHandle = await root.getFileHandle("test.bin");
|
|
461
|
-
const file = await fileHandle.getFile();
|
|
462
|
-
|
|
463
|
-
// WHATWG File.stream() should return a ReadableStream
|
|
464
|
-
const stream = file.stream();
|
|
465
|
-
const reader = stream.getReader();
|
|
466
|
-
|
|
467
|
-
const chunks = [];
|
|
468
|
-
while (true) {
|
|
469
|
-
const { done, value } = await reader.read();
|
|
470
|
-
if (done) break;
|
|
471
|
-
chunks.push(Array.from(value));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return JSON.stringify({
|
|
475
|
-
isReadableStream: stream instanceof ReadableStream,
|
|
476
|
-
chunkCount: chunks.length,
|
|
477
|
-
totalBytes: chunks.reduce((sum, c) => sum + c.length, 0)
|
|
478
|
-
});
|
|
479
|
-
})()
|
|
480
|
-
`,
|
|
481
|
-
{ promise: true }
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
const parsed = JSON.parse(result as string);
|
|
485
|
-
assert.strictEqual(parsed.isReadableStream, true, "file.stream() should return ReadableStream");
|
|
486
|
-
assert.strictEqual(parsed.totalBytes, fileData.length);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe("WHATWG Compliance - WritableStream streaming", () => {
|
|
491
|
-
test("writable.write(chunk) streams each chunk separately", async () => {
|
|
492
|
-
const writeCalls: Array<{ data: Uint8Array }> = [];
|
|
493
|
-
|
|
494
|
-
const mockHandler: FileSystemHandler = {
|
|
495
|
-
async getFileHandle() {},
|
|
496
|
-
async getDirectoryHandle() {},
|
|
497
|
-
async removeEntry() {},
|
|
498
|
-
async readDirectory() {
|
|
499
|
-
return [];
|
|
500
|
-
},
|
|
501
|
-
async readFile() {
|
|
502
|
-
return {
|
|
503
|
-
data: new Uint8Array(0),
|
|
504
|
-
size: 0,
|
|
505
|
-
lastModified: Date.now(),
|
|
506
|
-
type: "application/octet-stream",
|
|
507
|
-
};
|
|
508
|
-
},
|
|
509
|
-
async writeFile(path, data) {
|
|
510
|
-
writeCalls.push({ data: new Uint8Array(data) });
|
|
511
|
-
},
|
|
512
|
-
async truncate() {},
|
|
513
|
-
async isSameEntry() {
|
|
514
|
-
return false;
|
|
515
|
-
},
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
runtime = await createRuntime({
|
|
519
|
-
console: { onLog: () => {} },
|
|
520
|
-
fs: { getDirectory: async () => mockHandler },
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
await runtime.context.eval(
|
|
524
|
-
`
|
|
525
|
-
(async () => {
|
|
526
|
-
const root = await getDirectory("/");
|
|
527
|
-
const fileHandle = await root.getFileHandle("chunked.bin", { create: true });
|
|
528
|
-
const writable = await fileHandle.createWritable();
|
|
529
|
-
|
|
530
|
-
// Write multiple chunks - each should trigger a separate writeFile call
|
|
531
|
-
await writable.write(new Uint8Array([1, 2, 3]));
|
|
532
|
-
await writable.write(new Uint8Array([4, 5, 6]));
|
|
533
|
-
await writable.write(new Uint8Array([7, 8, 9]));
|
|
534
|
-
|
|
535
|
-
await writable.close();
|
|
536
|
-
})()
|
|
537
|
-
`,
|
|
538
|
-
{ promise: true }
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
// Each write() should result in a separate writeFile call (streaming)
|
|
542
|
-
assert.strictEqual(
|
|
543
|
-
writeCalls.length,
|
|
544
|
-
3,
|
|
545
|
-
`Expected 3 writeFile calls but got ${writeCalls.length} - writes were buffered`
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
// Verify content of each call
|
|
549
|
-
assert.deepStrictEqual(Array.from(writeCalls[0]!.data), [1, 2, 3]);
|
|
550
|
-
assert.deepStrictEqual(Array.from(writeCalls[1]!.data), [4, 5, 6]);
|
|
551
|
-
assert.deepStrictEqual(Array.from(writeCalls[2]!.data), [7, 8, 9]);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
test("pipeTo(writable) streams chunks from ReadableStream", async () => {
|
|
555
|
-
const writeCalls: Array<{ data: Uint8Array }> = [];
|
|
556
|
-
|
|
557
|
-
const mockHandler: FileSystemHandler = {
|
|
558
|
-
async getFileHandle() {},
|
|
559
|
-
async getDirectoryHandle() {},
|
|
560
|
-
async removeEntry() {},
|
|
561
|
-
async readDirectory() {
|
|
562
|
-
return [];
|
|
563
|
-
},
|
|
564
|
-
async readFile() {
|
|
565
|
-
return {
|
|
566
|
-
data: new Uint8Array(0),
|
|
567
|
-
size: 0,
|
|
568
|
-
lastModified: Date.now(),
|
|
569
|
-
type: "application/octet-stream",
|
|
570
|
-
};
|
|
571
|
-
},
|
|
572
|
-
async writeFile(path, data) {
|
|
573
|
-
writeCalls.push({ data: new Uint8Array(data) });
|
|
574
|
-
},
|
|
575
|
-
async truncate() {},
|
|
576
|
-
async isSameEntry() {
|
|
577
|
-
return false;
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
runtime = await createRuntime({
|
|
582
|
-
console: { onLog: () => {} },
|
|
583
|
-
fs: { getDirectory: async () => mockHandler },
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
await runtime.context.eval(
|
|
587
|
-
`
|
|
588
|
-
(async () => {
|
|
589
|
-
const root = await getDirectory("/");
|
|
590
|
-
const fileHandle = await root.getFileHandle("piped.bin", { create: true });
|
|
591
|
-
const writable = await fileHandle.createWritable();
|
|
592
|
-
|
|
593
|
-
// Create a ReadableStream with multiple chunks
|
|
594
|
-
let chunkIndex = 0;
|
|
595
|
-
const chunks = [
|
|
596
|
-
new Uint8Array([1, 2]),
|
|
597
|
-
new Uint8Array([3, 4]),
|
|
598
|
-
new Uint8Array([5, 6]),
|
|
599
|
-
];
|
|
600
|
-
|
|
601
|
-
const readable = new ReadableStream({
|
|
602
|
-
pull(controller) {
|
|
603
|
-
if (chunkIndex < chunks.length) {
|
|
604
|
-
controller.enqueue(chunks[chunkIndex]);
|
|
605
|
-
chunkIndex++;
|
|
606
|
-
} else {
|
|
607
|
-
controller.close();
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
// Pipe should stream chunks one by one
|
|
613
|
-
const reader = readable.getReader();
|
|
614
|
-
while (true) {
|
|
615
|
-
const { done, value } = await reader.read();
|
|
616
|
-
if (done) break;
|
|
617
|
-
await writable.write(value);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
await writable.close();
|
|
621
|
-
})()
|
|
622
|
-
`,
|
|
623
|
-
{ promise: true }
|
|
624
|
-
);
|
|
625
|
-
|
|
626
|
-
// Each chunk from the stream should result in a separate writeFile call
|
|
627
|
-
assert.strictEqual(
|
|
628
|
-
writeCalls.length,
|
|
629
|
-
3,
|
|
630
|
-
`Expected 3 writeFile calls but got ${writeCalls.length} - pipe was buffered`
|
|
631
|
-
);
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
});
|