@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/src/index.ts DELETED
@@ -1,997 +0,0 @@
1
- import ivm from "isolated-vm";
2
- import { setupCore, clearAllInstanceState } from "@ricsam/isolate-core";
3
-
4
- export { clearAllInstanceState };
5
-
6
- // ============================================================================
7
- // FileSystemHandler Interface
8
- // ============================================================================
9
-
10
- export interface FileSystemHandler {
11
- /** Get or create a file handle at the given path */
12
- getFileHandle(
13
- path: string,
14
- options?: { create?: boolean }
15
- ): Promise<void>;
16
-
17
- /** Get or create a directory handle at the given path */
18
- getDirectoryHandle(
19
- path: string,
20
- options?: { create?: boolean }
21
- ): Promise<void>;
22
-
23
- /** Remove a file or directory at the given path */
24
- removeEntry(
25
- path: string,
26
- options?: { recursive?: boolean }
27
- ): Promise<void>;
28
-
29
- /** List contents of a directory */
30
- readDirectory(
31
- path: string
32
- ): Promise<Array<{ name: string; kind: "file" | "directory" }>>;
33
-
34
- /** Read file content */
35
- readFile(
36
- path: string
37
- ): Promise<{ data: Uint8Array; size: number; lastModified: number; type: string }>;
38
-
39
- /** Write data to a file */
40
- writeFile(
41
- path: string,
42
- data: Uint8Array,
43
- position?: number
44
- ): Promise<void>;
45
-
46
- /** Truncate a file to a specific size */
47
- truncateFile(path: string, size: number): Promise<void>;
48
-
49
- /** Get file metadata without reading content */
50
- getFileMetadata(
51
- path: string
52
- ): Promise<{ size: number; lastModified: number; type: string }>;
53
- }
54
-
55
- export interface FsOptions {
56
- /** Get a file system handler for the given path */
57
- getDirectory(path: string): Promise<FileSystemHandler>;
58
- }
59
-
60
- export interface FsHandle {
61
- dispose(): void;
62
- }
63
-
64
- // ============================================================================
65
- // Instance State Management
66
- // ============================================================================
67
-
68
- const instanceStateMap = new WeakMap<ivm.Context, Map<number, unknown>>();
69
- let nextInstanceId = 1;
70
-
71
- function getInstanceStateMapForContext(
72
- context: ivm.Context
73
- ): Map<number, unknown> {
74
- let map = instanceStateMap.get(context);
75
- if (!map) {
76
- map = new Map();
77
- instanceStateMap.set(context, map);
78
- }
79
- return map;
80
- }
81
-
82
- // ============================================================================
83
- // State Types
84
- // ============================================================================
85
-
86
- interface DirectoryHandleState {
87
- instanceId: number;
88
- path: string; // Path within handler's root, e.g., "/" or "/subdir"
89
- name: string; // Directory name, e.g., "" for root or "subdir"
90
- handler: FileSystemHandler; // Handler for this directory tree
91
- }
92
-
93
- interface FileHandleState {
94
- instanceId: number;
95
- path: string; // Path within handler's root, e.g., "/file.txt"
96
- name: string; // File name, e.g., "file.txt"
97
- handler: FileSystemHandler; // Handler for this file
98
- }
99
-
100
- interface WritableStreamState {
101
- instanceId: number;
102
- filePath: string; // Path to the file being written
103
- position: number; // Current write position (for seek)
104
- buffer: Uint8Array[]; // Buffered writes before close
105
- closed: boolean; // Whether stream has been closed
106
- handler: FileSystemHandler; // Handler for this stream
107
- }
108
-
109
- // ============================================================================
110
- // FileSystemDirectoryHandle Implementation
111
- // ============================================================================
112
-
113
- function setupFileSystemDirectoryHandle(
114
- context: ivm.Context,
115
- stateMap: Map<number, unknown>
116
- ): void {
117
- const global = context.global;
118
-
119
- // Property getters
120
- global.setSync(
121
- "__FileSystemDirectoryHandle_get_name",
122
- new ivm.Callback((instanceId: number) => {
123
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
124
- return state?.name ?? "";
125
- })
126
- );
127
-
128
- global.setSync(
129
- "__FileSystemDirectoryHandle_get_path",
130
- new ivm.Callback((instanceId: number) => {
131
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
132
- return state?.path ?? "/";
133
- })
134
- );
135
-
136
- // getFileHandle - async reference
137
- const getFileHandleRef = new ivm.Reference(
138
- async (instanceId: number, name: string, optionsJson: string) => {
139
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
140
- if (!state) {
141
- throw new Error("[NotFoundError]Directory handle not found");
142
- }
143
-
144
- const options = JSON.parse(optionsJson) as { create?: boolean };
145
- const childPath = state.path === "/" ? `/${name}` : `${state.path}/${name}`;
146
-
147
- try {
148
- await state.handler.getFileHandle(childPath, options);
149
- } catch (err) {
150
- if (err instanceof Error) {
151
- throw new Error(err.message);
152
- }
153
- throw err;
154
- }
155
-
156
- // Create file handle state with parent's handler
157
- const fileInstanceId = nextInstanceId++;
158
- const fileState: FileHandleState = {
159
- instanceId: fileInstanceId,
160
- path: childPath,
161
- name,
162
- handler: state.handler,
163
- };
164
- stateMap.set(fileInstanceId, fileState);
165
-
166
- return JSON.stringify({ instanceId: fileInstanceId });
167
- }
168
- );
169
- global.setSync("__FileSystemDirectoryHandle_getFileHandle_ref", getFileHandleRef);
170
-
171
- // getDirectoryHandle - async reference
172
- const getDirectoryHandleRef = new ivm.Reference(
173
- async (instanceId: number, name: string, optionsJson: string) => {
174
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
175
- if (!state) {
176
- throw new Error("[NotFoundError]Directory handle not found");
177
- }
178
-
179
- const options = JSON.parse(optionsJson) as { create?: boolean };
180
- const childPath = state.path === "/" ? `/${name}` : `${state.path}/${name}`;
181
-
182
- try {
183
- await state.handler.getDirectoryHandle(childPath, options);
184
- } catch (err) {
185
- if (err instanceof Error) {
186
- throw new Error(err.message);
187
- }
188
- throw err;
189
- }
190
-
191
- // Create directory handle state with parent's handler
192
- const dirInstanceId = nextInstanceId++;
193
- const dirState: DirectoryHandleState = {
194
- instanceId: dirInstanceId,
195
- path: childPath,
196
- name,
197
- handler: state.handler,
198
- };
199
- stateMap.set(dirInstanceId, dirState);
200
-
201
- return JSON.stringify({ instanceId: dirInstanceId });
202
- }
203
- );
204
- global.setSync("__FileSystemDirectoryHandle_getDirectoryHandle_ref", getDirectoryHandleRef);
205
-
206
- // removeEntry - async reference
207
- const removeEntryRef = new ivm.Reference(
208
- async (instanceId: number, name: string, optionsJson: string) => {
209
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
210
- if (!state) {
211
- throw new Error("[NotFoundError]Directory handle not found");
212
- }
213
-
214
- const options = JSON.parse(optionsJson) as { recursive?: boolean };
215
- const childPath = state.path === "/" ? `/${name}` : `${state.path}/${name}`;
216
-
217
- try {
218
- await state.handler.removeEntry(childPath, options);
219
- } catch (err) {
220
- if (err instanceof Error) {
221
- throw new Error(err.message);
222
- }
223
- throw err;
224
- }
225
- }
226
- );
227
- global.setSync("__FileSystemDirectoryHandle_removeEntry_ref", removeEntryRef);
228
-
229
- // readDirectory - async reference (for entries/keys/values)
230
- const readDirectoryRef = new ivm.Reference(async (instanceId: number) => {
231
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
232
- if (!state) {
233
- throw new Error("[NotFoundError]Directory handle not found");
234
- }
235
-
236
- try {
237
- const entries = await state.handler.readDirectory(state.path);
238
-
239
- // Create handle states for each entry and return with instance IDs
240
- const result = entries.map((entry) => {
241
- const entryId = nextInstanceId++;
242
- const entryPath = state.path === "/" ? `/${entry.name}` : `${state.path}/${entry.name}`;
243
-
244
- if (entry.kind === "file") {
245
- const fileState: FileHandleState = {
246
- instanceId: entryId,
247
- path: entryPath,
248
- name: entry.name,
249
- handler: state.handler,
250
- };
251
- stateMap.set(entryId, fileState);
252
- } else {
253
- const dirState: DirectoryHandleState = {
254
- instanceId: entryId,
255
- path: entryPath,
256
- name: entry.name,
257
- handler: state.handler,
258
- };
259
- stateMap.set(entryId, dirState);
260
- }
261
-
262
- return {
263
- name: entry.name,
264
- kind: entry.kind,
265
- instanceId: entryId,
266
- };
267
- });
268
-
269
- return JSON.stringify(result);
270
- } catch (err) {
271
- if (err instanceof Error) {
272
- throw new Error(err.message);
273
- }
274
- throw err;
275
- }
276
- });
277
- global.setSync("__FileSystemDirectoryHandle_readDirectory_ref", readDirectoryRef);
278
-
279
- // isSameEntry - sync callback
280
- global.setSync(
281
- "__FileSystemDirectoryHandle_isSameEntry",
282
- new ivm.Callback((id1: number, id2: number) => {
283
- const state1 = stateMap.get(id1) as DirectoryHandleState | undefined;
284
- const state2 = stateMap.get(id2) as DirectoryHandleState | undefined;
285
- if (!state1 || !state2) return false;
286
- return state1.path === state2.path;
287
- })
288
- );
289
-
290
- // resolve - async reference
291
- const resolveRef = new ivm.Reference(
292
- async (instanceId: number, descendantId: number) => {
293
- const state = stateMap.get(instanceId) as DirectoryHandleState | undefined;
294
- const descendantState = stateMap.get(descendantId) as
295
- | DirectoryHandleState
296
- | FileHandleState
297
- | undefined;
298
-
299
- if (!state || !descendantState) {
300
- return "null";
301
- }
302
-
303
- // Check if descendant is actually a descendant
304
- const basePath = state.path === "/" ? "" : state.path;
305
- if (!descendantState.path.startsWith(basePath + "/") && descendantState.path !== state.path) {
306
- return "null";
307
- }
308
-
309
- // Build path components
310
- const relativePath = descendantState.path.slice(basePath.length);
311
- const components = relativePath.split("/").filter((c) => c.length > 0);
312
-
313
- return JSON.stringify(components);
314
- }
315
- );
316
- global.setSync("__FileSystemDirectoryHandle_resolve_ref", resolveRef);
317
-
318
- // Inject FileSystemDirectoryHandle class
319
- const directoryHandleCode = `
320
- (function() {
321
- const _directoryHandleInstanceIds = new WeakMap();
322
-
323
- function __decodeError(err) {
324
- if (!(err instanceof Error)) return err;
325
- const match = err.message.match(/^\\[(TypeError|RangeError|NotFoundError|TypeMismatchError|InvalidModificationError|Error)\\](.*)$/);
326
- if (match) {
327
- if (['NotFoundError', 'TypeMismatchError', 'InvalidModificationError'].includes(match[1])) {
328
- return new DOMException(match[2], match[1]);
329
- }
330
- const ErrorType = globalThis[match[1]] || Error;
331
- return new ErrorType(match[2]);
332
- }
333
- return err;
334
- }
335
-
336
- class FileSystemDirectoryHandle {
337
- constructor(path, name) {
338
- // Internal construction from instance ID
339
- if (typeof path === 'number' && name === null) {
340
- _directoryHandleInstanceIds.set(this, path);
341
- return;
342
- }
343
- const instanceId = __FileSystemDirectoryHandle_construct(path, name);
344
- _directoryHandleInstanceIds.set(this, instanceId);
345
- }
346
-
347
- static _fromInstanceId(instanceId) {
348
- return new FileSystemDirectoryHandle(instanceId, null);
349
- }
350
-
351
- _getInstanceId() {
352
- return _directoryHandleInstanceIds.get(this);
353
- }
354
-
355
- get kind() {
356
- return 'directory';
357
- }
358
-
359
- get name() {
360
- return __FileSystemDirectoryHandle_get_name(this._getInstanceId());
361
- }
362
-
363
- getFileHandle(name, options = {}) {
364
- try {
365
- const resultJson = __FileSystemDirectoryHandle_getFileHandle_ref.applySyncPromise(
366
- undefined,
367
- [this._getInstanceId(), name, JSON.stringify(options)]
368
- );
369
- const result = JSON.parse(resultJson);
370
- return FileSystemFileHandle._fromInstanceId(result.instanceId);
371
- } catch (err) {
372
- throw __decodeError(err);
373
- }
374
- }
375
-
376
- getDirectoryHandle(name, options = {}) {
377
- try {
378
- const resultJson = __FileSystemDirectoryHandle_getDirectoryHandle_ref.applySyncPromise(
379
- undefined,
380
- [this._getInstanceId(), name, JSON.stringify(options)]
381
- );
382
- const result = JSON.parse(resultJson);
383
- return FileSystemDirectoryHandle._fromInstanceId(result.instanceId);
384
- } catch (err) {
385
- throw __decodeError(err);
386
- }
387
- }
388
-
389
- removeEntry(name, options = {}) {
390
- try {
391
- __FileSystemDirectoryHandle_removeEntry_ref.applySyncPromise(
392
- undefined,
393
- [this._getInstanceId(), name, JSON.stringify(options)]
394
- );
395
- } catch (err) {
396
- throw __decodeError(err);
397
- }
398
- }
399
-
400
- async *entries() {
401
- let entriesJson;
402
- try {
403
- entriesJson = __FileSystemDirectoryHandle_readDirectory_ref.applySyncPromise(
404
- undefined,
405
- [this._getInstanceId()]
406
- );
407
- } catch (err) {
408
- throw __decodeError(err);
409
- }
410
- const entries = JSON.parse(entriesJson);
411
- for (const entry of entries) {
412
- if (entry.kind === 'file') {
413
- yield [entry.name, FileSystemFileHandle._fromInstanceId(entry.instanceId)];
414
- } else {
415
- yield [entry.name, FileSystemDirectoryHandle._fromInstanceId(entry.instanceId)];
416
- }
417
- }
418
- }
419
-
420
- async *keys() {
421
- for await (const [name] of this.entries()) {
422
- yield name;
423
- }
424
- }
425
-
426
- async *values() {
427
- for await (const [, handle] of this.entries()) {
428
- yield handle;
429
- }
430
- }
431
-
432
- [Symbol.asyncIterator]() {
433
- return this.entries();
434
- }
435
-
436
- isSameEntry(other) {
437
- if (!(other instanceof FileSystemDirectoryHandle)) {
438
- return false;
439
- }
440
- return __FileSystemDirectoryHandle_isSameEntry(
441
- this._getInstanceId(),
442
- other._getInstanceId()
443
- );
444
- }
445
-
446
- resolve(possibleDescendant) {
447
- try {
448
- const resultJson = __FileSystemDirectoryHandle_resolve_ref.applySyncPromise(
449
- undefined,
450
- [this._getInstanceId(), possibleDescendant._getInstanceId()]
451
- );
452
- return resultJson === 'null' ? null : JSON.parse(resultJson);
453
- } catch (err) {
454
- throw __decodeError(err);
455
- }
456
- }
457
- }
458
-
459
- globalThis.FileSystemDirectoryHandle = FileSystemDirectoryHandle;
460
- })();
461
- `;
462
-
463
- context.evalSync(directoryHandleCode);
464
- }
465
-
466
- // ============================================================================
467
- // FileSystemFileHandle Implementation
468
- // ============================================================================
469
-
470
- function setupFileSystemFileHandle(
471
- context: ivm.Context,
472
- stateMap: Map<number, unknown>
473
- ): void {
474
- const global = context.global;
475
-
476
- // Property getters
477
- global.setSync(
478
- "__FileSystemFileHandle_get_name",
479
- new ivm.Callback((instanceId: number) => {
480
- const state = stateMap.get(instanceId) as FileHandleState | undefined;
481
- return state?.name ?? "";
482
- })
483
- );
484
-
485
- global.setSync(
486
- "__FileSystemFileHandle_get_path",
487
- new ivm.Callback((instanceId: number) => {
488
- const state = stateMap.get(instanceId) as FileHandleState | undefined;
489
- return state?.path ?? "";
490
- })
491
- );
492
-
493
- // getFile - async reference
494
- const getFileRef = new ivm.Reference(async (instanceId: number) => {
495
- const state = stateMap.get(instanceId) as FileHandleState | undefined;
496
- if (!state) {
497
- throw new Error("[NotFoundError]File handle not found");
498
- }
499
-
500
- try {
501
- const fileData = await state.handler.readFile(state.path);
502
- return JSON.stringify({
503
- name: state.name,
504
- data: Array.from(fileData.data),
505
- size: fileData.size,
506
- lastModified: fileData.lastModified,
507
- type: fileData.type,
508
- });
509
- } catch (err) {
510
- if (err instanceof Error) {
511
- throw new Error(err.message);
512
- }
513
- throw err;
514
- }
515
- });
516
- global.setSync("__FileSystemFileHandle_getFile_ref", getFileRef);
517
-
518
- // createWritable - async reference
519
- const createWritableRef = new ivm.Reference(
520
- async (instanceId: number, _optionsJson: string) => {
521
- const state = stateMap.get(instanceId) as FileHandleState | undefined;
522
- if (!state) {
523
- throw new Error("[NotFoundError]File handle not found");
524
- }
525
-
526
- // Create writable stream state with handler reference
527
- const streamInstanceId = nextInstanceId++;
528
- const streamState: WritableStreamState = {
529
- instanceId: streamInstanceId,
530
- filePath: state.path,
531
- position: 0,
532
- buffer: [],
533
- closed: false,
534
- handler: state.handler,
535
- };
536
- stateMap.set(streamInstanceId, streamState);
537
-
538
- return streamInstanceId;
539
- }
540
- );
541
- global.setSync("__FileSystemFileHandle_createWritable_ref", createWritableRef);
542
-
543
- // isSameEntry - sync callback
544
- global.setSync(
545
- "__FileSystemFileHandle_isSameEntry",
546
- new ivm.Callback((id1: number, id2: number) => {
547
- const state1 = stateMap.get(id1) as FileHandleState | undefined;
548
- const state2 = stateMap.get(id2) as FileHandleState | undefined;
549
- if (!state1 || !state2) return false;
550
- return state1.path === state2.path;
551
- })
552
- );
553
-
554
- // Inject FileSystemFileHandle class
555
- const fileHandleCode = `
556
- (function() {
557
- const _fileHandleInstanceIds = new WeakMap();
558
-
559
- function __decodeError(err) {
560
- if (!(err instanceof Error)) return err;
561
- const match = err.message.match(/^\\[(TypeError|RangeError|NotFoundError|TypeMismatchError|InvalidModificationError|Error)\\](.*)$/);
562
- if (match) {
563
- if (['NotFoundError', 'TypeMismatchError', 'InvalidModificationError'].includes(match[1])) {
564
- return new DOMException(match[2], match[1]);
565
- }
566
- const ErrorType = globalThis[match[1]] || Error;
567
- return new ErrorType(match[2]);
568
- }
569
- return err;
570
- }
571
-
572
- class FileSystemFileHandle {
573
- constructor(path, name) {
574
- // Internal construction from instance ID
575
- if (typeof path === 'number' && name === null) {
576
- _fileHandleInstanceIds.set(this, path);
577
- return;
578
- }
579
- const instanceId = __FileSystemFileHandle_construct(path, name);
580
- _fileHandleInstanceIds.set(this, instanceId);
581
- }
582
-
583
- static _fromInstanceId(instanceId) {
584
- return new FileSystemFileHandle(instanceId, null);
585
- }
586
-
587
- _getInstanceId() {
588
- return _fileHandleInstanceIds.get(this);
589
- }
590
-
591
- get kind() {
592
- return 'file';
593
- }
594
-
595
- get name() {
596
- return __FileSystemFileHandle_get_name(this._getInstanceId());
597
- }
598
-
599
- getFile() {
600
- try {
601
- const metadataJson = __FileSystemFileHandle_getFile_ref.applySyncPromise(
602
- undefined,
603
- [this._getInstanceId()]
604
- );
605
- const metadata = JSON.parse(metadataJson);
606
- // Create File object from metadata and content
607
- const content = new Uint8Array(metadata.data);
608
- return new File([content], metadata.name, {
609
- type: metadata.type,
610
- lastModified: metadata.lastModified
611
- });
612
- } catch (err) {
613
- throw __decodeError(err);
614
- }
615
- }
616
-
617
- createWritable(options = {}) {
618
- try {
619
- const streamId = __FileSystemFileHandle_createWritable_ref.applySyncPromise(
620
- undefined,
621
- [this._getInstanceId(), JSON.stringify(options)]
622
- );
623
- return FileSystemWritableFileStream._fromInstanceId(streamId);
624
- } catch (err) {
625
- throw __decodeError(err);
626
- }
627
- }
628
-
629
- isSameEntry(other) {
630
- if (!(other instanceof FileSystemFileHandle)) {
631
- return false;
632
- }
633
- return __FileSystemFileHandle_isSameEntry(
634
- this._getInstanceId(),
635
- other._getInstanceId()
636
- );
637
- }
638
- }
639
-
640
- globalThis.FileSystemFileHandle = FileSystemFileHandle;
641
- })();
642
- `;
643
-
644
- context.evalSync(fileHandleCode);
645
- }
646
-
647
- // ============================================================================
648
- // FileSystemWritableFileStream Implementation
649
- // ============================================================================
650
-
651
- function setupFileSystemWritableFileStream(
652
- context: ivm.Context,
653
- stateMap: Map<number, unknown>
654
- ): void {
655
- const global = context.global;
656
-
657
- // write - async reference
658
- const writeRef = new ivm.Reference(
659
- async (instanceId: number, bytesJson: string, position: number | null) => {
660
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
661
- if (!state) {
662
- throw new Error("[InvalidStateError]Stream not found");
663
- }
664
- if (state.closed) {
665
- throw new Error("[InvalidStateError]Stream is closed");
666
- }
667
-
668
- const bytes = JSON.parse(bytesJson) as number[];
669
- const data = new Uint8Array(bytes);
670
-
671
- // Update position if specified
672
- if (position !== null) {
673
- state.position = position;
674
- }
675
-
676
- // Write to handler
677
- try {
678
- await state.handler.writeFile(state.filePath, data, state.position);
679
- state.position += data.length;
680
- } catch (err) {
681
- if (err instanceof Error) {
682
- throw new Error(err.message);
683
- }
684
- throw err;
685
- }
686
- }
687
- );
688
- global.setSync("__FileSystemWritableFileStream_write_ref", writeRef);
689
-
690
- // seek - sync callback
691
- global.setSync(
692
- "__FileSystemWritableFileStream_seek",
693
- new ivm.Callback((instanceId: number, position: number) => {
694
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
695
- if (!state) {
696
- throw new Error("[InvalidStateError]Stream not found");
697
- }
698
- if (state.closed) {
699
- throw new Error("[InvalidStateError]Stream is closed");
700
- }
701
- state.position = position;
702
- })
703
- );
704
-
705
- // truncate - async reference
706
- const truncateRef = new ivm.Reference(async (instanceId: number, size: number) => {
707
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
708
- if (!state) {
709
- throw new Error("[InvalidStateError]Stream not found");
710
- }
711
- if (state.closed) {
712
- throw new Error("[InvalidStateError]Stream is closed");
713
- }
714
-
715
- try {
716
- await state.handler.truncateFile(state.filePath, size);
717
- // Adjust position if it's beyond the new size
718
- if (state.position > size) {
719
- state.position = size;
720
- }
721
- } catch (err) {
722
- if (err instanceof Error) {
723
- throw new Error(err.message);
724
- }
725
- throw err;
726
- }
727
- });
728
- global.setSync("__FileSystemWritableFileStream_truncate_ref", truncateRef);
729
-
730
- // close - async reference
731
- const closeRef = new ivm.Reference(async (instanceId: number) => {
732
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
733
- if (!state) {
734
- throw new Error("[InvalidStateError]Stream not found");
735
- }
736
- if (state.closed) {
737
- throw new Error("[InvalidStateError]Stream is already closed");
738
- }
739
-
740
- state.closed = true;
741
- });
742
- global.setSync("__FileSystemWritableFileStream_close_ref", closeRef);
743
-
744
- // abort - async reference
745
- const abortRef = new ivm.Reference(async (instanceId: number, _reason: string | null) => {
746
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
747
- if (!state) {
748
- throw new Error("[InvalidStateError]Stream not found");
749
- }
750
-
751
- state.closed = true;
752
- state.buffer = []; // Discard any buffered data
753
- });
754
- global.setSync("__FileSystemWritableFileStream_abort_ref", abortRef);
755
-
756
- // locked - sync callback
757
- global.setSync(
758
- "__FileSystemWritableFileStream_get_locked",
759
- new ivm.Callback((instanceId: number) => {
760
- const state = stateMap.get(instanceId) as WritableStreamState | undefined;
761
- return state ? !state.closed : false;
762
- })
763
- );
764
-
765
- // Inject FileSystemWritableFileStream class
766
- const writableStreamCode = `
767
- (function() {
768
- const _writableStreamInstanceIds = new WeakMap();
769
-
770
- function __decodeError(err) {
771
- if (!(err instanceof Error)) return err;
772
- const match = err.message.match(/^\\[(TypeError|RangeError|InvalidStateError|NotFoundError|Error)\\](.*)$/);
773
- if (match) {
774
- if (['InvalidStateError', 'NotFoundError'].includes(match[1])) {
775
- return new DOMException(match[2], match[1]);
776
- }
777
- const ErrorType = globalThis[match[1]] || Error;
778
- return new ErrorType(match[2]);
779
- }
780
- return err;
781
- }
782
-
783
- class FileSystemWritableFileStream {
784
- constructor(instanceId) {
785
- _writableStreamInstanceIds.set(this, instanceId);
786
- }
787
-
788
- static _fromInstanceId(instanceId) {
789
- return new FileSystemWritableFileStream(instanceId);
790
- }
791
-
792
- _getInstanceId() {
793
- return _writableStreamInstanceIds.get(this);
794
- }
795
-
796
- write(data) {
797
- try {
798
- // Handle different data types
799
- let writeData;
800
- let position = null;
801
- let type = 'write';
802
-
803
- if (data && typeof data === 'object' && !ArrayBuffer.isView(data) &&
804
- !(data instanceof Blob) && !(data instanceof ArrayBuffer) &&
805
- !Array.isArray(data) && typeof data.type === 'string') {
806
- // WriteParams object: { type, data, position, size }
807
- type = data.type || 'write';
808
- if (type === 'seek') {
809
- return this.seek(data.position);
810
- }
811
- if (type === 'truncate') {
812
- return this.truncate(data.size);
813
- }
814
- writeData = data.data;
815
- position = data.position ?? null;
816
- } else {
817
- writeData = data;
818
- }
819
-
820
- // Convert data to bytes array for transfer
821
- let bytes;
822
- if (typeof writeData === 'string') {
823
- bytes = Array.from(new TextEncoder().encode(writeData));
824
- } else if (writeData instanceof Blob) {
825
- // Synchronously get blob bytes - use the internal callback
826
- const blobText = writeData.text ? writeData.text() : '';
827
- bytes = Array.from(new TextEncoder().encode(blobText));
828
- } else if (writeData instanceof ArrayBuffer) {
829
- bytes = Array.from(new Uint8Array(writeData));
830
- } else if (ArrayBuffer.isView(writeData)) {
831
- bytes = Array.from(new Uint8Array(writeData.buffer, writeData.byteOffset, writeData.byteLength));
832
- } else if (Array.isArray(writeData)) {
833
- bytes = writeData;
834
- } else {
835
- throw new TypeError('Invalid data type for write');
836
- }
837
-
838
- __FileSystemWritableFileStream_write_ref.applySyncPromise(
839
- undefined,
840
- [this._getInstanceId(), JSON.stringify(bytes), position]
841
- );
842
- } catch (err) {
843
- throw __decodeError(err);
844
- }
845
- }
846
-
847
- seek(position) {
848
- try {
849
- __FileSystemWritableFileStream_seek(this._getInstanceId(), position);
850
- } catch (err) {
851
- throw __decodeError(err);
852
- }
853
- }
854
-
855
- truncate(size) {
856
- try {
857
- __FileSystemWritableFileStream_truncate_ref.applySyncPromise(
858
- undefined,
859
- [this._getInstanceId(), size]
860
- );
861
- } catch (err) {
862
- throw __decodeError(err);
863
- }
864
- }
865
-
866
- close() {
867
- try {
868
- __FileSystemWritableFileStream_close_ref.applySyncPromise(
869
- undefined,
870
- [this._getInstanceId()]
871
- );
872
- } catch (err) {
873
- throw __decodeError(err);
874
- }
875
- }
876
-
877
- abort(reason) {
878
- try {
879
- __FileSystemWritableFileStream_abort_ref.applySyncPromise(
880
- undefined,
881
- [this._getInstanceId(), reason ? String(reason) : null]
882
- );
883
- } catch (err) {
884
- throw __decodeError(err);
885
- }
886
- }
887
-
888
- get locked() {
889
- return __FileSystemWritableFileStream_get_locked(this._getInstanceId());
890
- }
891
- }
892
-
893
- globalThis.FileSystemWritableFileStream = FileSystemWritableFileStream;
894
- })();
895
- `;
896
-
897
- context.evalSync(writableStreamCode);
898
- }
899
-
900
- // ============================================================================
901
- // Global getDirectory(path) Implementation
902
- // ============================================================================
903
-
904
- function setupGetDirectoryGlobal(
905
- context: ivm.Context,
906
- stateMap: Map<number, unknown>,
907
- options: FsOptions
908
- ): void {
909
- const global = context.global;
910
-
911
- // getDirectory - async reference that creates directory handle at specified path
912
- const getDirectoryRef = new ivm.Reference(async (path: string) => {
913
- // Get handler for this path from the options factory
914
- const handler = await options.getDirectory(path);
915
-
916
- const instanceId = nextInstanceId++;
917
- // Path is "/" since handler is rooted at the requested path
918
- const state: DirectoryHandleState = {
919
- instanceId,
920
- path: "/",
921
- name: path.split("/").filter(Boolean).pop() || "",
922
- handler,
923
- };
924
- stateMap.set(instanceId, state);
925
- return instanceId;
926
- });
927
- global.setSync("__getDirectory_ref", getDirectoryRef);
928
-
929
- // Inject global getDirectory (async)
930
- const getDirectoryCode = `
931
- (function() {
932
- globalThis.getDirectory = async function(path) {
933
- const instanceId = await __getDirectory_ref.applySyncPromise(undefined, [path]);
934
- return FileSystemDirectoryHandle._fromInstanceId(instanceId);
935
- };
936
- })();
937
- `;
938
- context.evalSync(getDirectoryCode);
939
- }
940
-
941
- // ============================================================================
942
- // Main Setup Function
943
- // ============================================================================
944
-
945
- /**
946
- * Setup File System Access API in an isolated-vm context
947
- *
948
- * Provides an OPFS-compatible FileSystemDirectoryHandle API
949
- *
950
- * @example
951
- * const handle = await setupFs(context, {
952
- * getDirectory: async (path) => {
953
- * // Return a FileSystemHandler rooted at the given path
954
- * return createNodeFileSystemHandler(`./data${path}`);
955
- * }
956
- * });
957
- *
958
- * await context.eval(`
959
- * const root = await getDirectory("/uploads");
960
- * const fileHandle = await root.getFileHandle("test.txt", { create: true });
961
- * const writable = await fileHandle.createWritable();
962
- * await writable.write("hello world");
963
- * await writable.close();
964
- * `);
965
- */
966
- export async function setupFs(
967
- context: ivm.Context,
968
- options: FsOptions
969
- ): Promise<FsHandle> {
970
- // Setup core APIs first (Blob, File, AbortController, Streams, etc.)
971
- await setupCore(context);
972
-
973
- const stateMap = getInstanceStateMapForContext(context);
974
-
975
- // Setup FileSystemDirectoryHandle
976
- setupFileSystemDirectoryHandle(context, stateMap);
977
-
978
- // Setup FileSystemFileHandle
979
- setupFileSystemFileHandle(context, stateMap);
980
-
981
- // Setup FileSystemWritableFileStream
982
- setupFileSystemWritableFileStream(context, stateMap);
983
-
984
- // Setup global getDirectory(path)
985
- setupGetDirectoryGlobal(context, stateMap, options);
986
-
987
- return {
988
- dispose() {
989
- // Clear state for this context
990
- stateMap.clear();
991
- },
992
- };
993
- }
994
-
995
- // Export node adapter
996
- export { createNodeFileSystemHandler } from "./node-adapter.ts";
997
- export type { NodeFileSystemHandlerOptions } from "./node-adapter.ts";