@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.
@@ -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
- });
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "rootDir": "./src"
5
- },
6
- "include": ["src/**/*"],
7
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
8
- }