@componentor/fs 1.1.7

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.
@@ -0,0 +1,2732 @@
1
+ // src/constants.ts
2
+ var constants = {
3
+ // File access modes
4
+ F_OK: 0,
5
+ R_OK: 4,
6
+ W_OK: 2,
7
+ X_OK: 1,
8
+ // Copy file flags
9
+ COPYFILE_EXCL: 1,
10
+ COPYFILE_FICLONE: 2,
11
+ COPYFILE_FICLONE_FORCE: 4,
12
+ // File open flags
13
+ O_RDONLY: 0,
14
+ O_WRONLY: 1,
15
+ O_RDWR: 2,
16
+ O_CREAT: 64,
17
+ O_EXCL: 128,
18
+ O_TRUNC: 512,
19
+ O_APPEND: 1024,
20
+ // File type masks
21
+ S_IFMT: 61440,
22
+ S_IFREG: 32768,
23
+ S_IFDIR: 16384,
24
+ S_IFLNK: 40960
25
+ };
26
+ function flagsToString(flags) {
27
+ if (typeof flags === "string") return flags;
28
+ const map = {
29
+ [constants.O_RDONLY]: "r",
30
+ [constants.O_WRONLY]: "w",
31
+ [constants.O_RDWR]: "r+",
32
+ [constants.O_CREAT | constants.O_WRONLY]: "w",
33
+ [constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC]: "w",
34
+ [constants.O_CREAT | constants.O_RDWR]: "w+",
35
+ [constants.O_APPEND | constants.O_WRONLY]: "a",
36
+ [constants.O_APPEND | constants.O_RDWR]: "a+"
37
+ };
38
+ return map[flags] || "r";
39
+ }
40
+
41
+ // src/errors.ts
42
+ var FSError = class extends Error {
43
+ code;
44
+ syscall;
45
+ path;
46
+ original;
47
+ constructor(message, code, options) {
48
+ super(message);
49
+ this.name = "FSError";
50
+ this.code = code;
51
+ this.syscall = options?.syscall;
52
+ this.path = options?.path;
53
+ this.original = options?.original;
54
+ }
55
+ };
56
+ function createENOENT(path) {
57
+ return new FSError(`ENOENT: No such file or directory, '${path}'`, "ENOENT", { path });
58
+ }
59
+ function createEEXIST(path, operation) {
60
+ const message = `EEXIST: File exists, '${path}'`;
61
+ return new FSError(message, "EEXIST", { path });
62
+ }
63
+ function createEACCES(path, syscall) {
64
+ return new FSError(`EACCES: permission denied, access '${path}'`, "EACCES", { syscall, path });
65
+ }
66
+ function createEISDIR(path, operation = "operation") {
67
+ return new FSError(`EISDIR: illegal operation on a directory, ${operation} '${path}'`, "EISDIR", { path });
68
+ }
69
+ function createELOOP(path) {
70
+ return new FSError(`ELOOP: Too many symbolic links, '${path}'`, "ELOOP", { path });
71
+ }
72
+ function createEINVAL(path) {
73
+ return new FSError(`EINVAL: Invalid argument, '${path}'`, "EINVAL", { path });
74
+ }
75
+ function createECORRUPTED(path) {
76
+ return new FSError(`ECORRUPTED: Pack file integrity check failed, '${path}'`, "ECORRUPTED", { path });
77
+ }
78
+ function wrapError(err) {
79
+ if (err instanceof FSError) return err;
80
+ const error = err;
81
+ if (typeof error.code === "string") {
82
+ const fsErr = new FSError(error.message, error.code);
83
+ fsErr.original = error;
84
+ return fsErr;
85
+ }
86
+ const wrapped = new FSError(error.message || "Unknown error", "UNKNOWN");
87
+ wrapped.original = error;
88
+ return wrapped;
89
+ }
90
+
91
+ // src/path-utils.ts
92
+ var normalizeCache = /* @__PURE__ */ new Map();
93
+ var CACHE_MAX_SIZE = 1e3;
94
+ function normalize(path) {
95
+ if (path === void 0 || path === null) {
96
+ throw new TypeError("Path cannot be undefined or null");
97
+ }
98
+ if (typeof path !== "string") {
99
+ throw new TypeError(`Expected string path, got ${typeof path}`);
100
+ }
101
+ if (path === "") {
102
+ return "/";
103
+ }
104
+ const cached = normalizeCache.get(path);
105
+ if (cached !== void 0) {
106
+ return cached;
107
+ }
108
+ const parts = path.split("/");
109
+ const stack = [];
110
+ for (const part of parts) {
111
+ if (part === "" || part === ".") {
112
+ continue;
113
+ } else if (part === "..") {
114
+ if (stack.length > 0) stack.pop();
115
+ } else {
116
+ stack.push(part);
117
+ }
118
+ }
119
+ const result = "/" + stack.join("/");
120
+ if (normalizeCache.size >= CACHE_MAX_SIZE) {
121
+ const deleteCount = CACHE_MAX_SIZE / 4;
122
+ let count = 0;
123
+ for (const key of normalizeCache.keys()) {
124
+ if (count++ >= deleteCount) break;
125
+ normalizeCache.delete(key);
126
+ }
127
+ }
128
+ normalizeCache.set(path, result);
129
+ return result;
130
+ }
131
+ function dirname(path) {
132
+ const normalized = normalize(path);
133
+ const parts = normalized.split("/").filter(Boolean);
134
+ if (parts.length < 2) return "/";
135
+ return "/" + parts.slice(0, -1).join("/");
136
+ }
137
+ function isRoot(path) {
138
+ const normalized = normalize(path);
139
+ return normalized === "/" || normalized === "";
140
+ }
141
+ function segments(path) {
142
+ return normalize(path).split("/").filter(Boolean);
143
+ }
144
+
145
+ // src/handle-manager.ts
146
+ var FILE_HANDLE_POOL_SIZE = 50;
147
+ var DIR_CACHE_MAX_SIZE = 200;
148
+ var HandleManager = class {
149
+ rootPromise;
150
+ dirCache = /* @__PURE__ */ new Map();
151
+ fileHandlePool = /* @__PURE__ */ new Map();
152
+ constructor() {
153
+ this.rootPromise = navigator.storage.getDirectory();
154
+ }
155
+ /**
156
+ * Get the root directory handle
157
+ */
158
+ async getRoot() {
159
+ return this.rootPromise;
160
+ }
161
+ /**
162
+ * Cache a directory handle with LRU eviction
163
+ */
164
+ cacheDirHandle(path, handle) {
165
+ if (this.dirCache.size >= DIR_CACHE_MAX_SIZE) {
166
+ const firstKey = this.dirCache.keys().next().value;
167
+ if (firstKey) this.dirCache.delete(firstKey);
168
+ }
169
+ this.dirCache.set(path, handle);
170
+ }
171
+ /**
172
+ * Clear directory cache for a path and its children
173
+ */
174
+ clearCache(path = "") {
175
+ const normalizedPath = normalize(path);
176
+ if (normalizedPath === "/" || normalizedPath === "") {
177
+ this.dirCache.clear();
178
+ this.fileHandlePool.clear();
179
+ return;
180
+ }
181
+ if (this.dirCache.size > 0) {
182
+ for (const key of this.dirCache.keys()) {
183
+ if (key === normalizedPath || key.startsWith(normalizedPath + "/")) {
184
+ this.dirCache.delete(key);
185
+ }
186
+ }
187
+ }
188
+ if (this.fileHandlePool.size > 0) {
189
+ for (const key of this.fileHandlePool.keys()) {
190
+ if (key === normalizedPath || key.startsWith(normalizedPath + "/")) {
191
+ this.fileHandlePool.delete(key);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Get a file handle from the pool or create a new one
198
+ */
199
+ async getPooledFileHandle(path, create = false) {
200
+ const normalizedPath = normalize(path);
201
+ const pooled = this.fileHandlePool.get(normalizedPath);
202
+ if (pooled) {
203
+ return pooled;
204
+ }
205
+ const { fileHandle } = await this.getHandle(normalizedPath, { create });
206
+ if (!fileHandle) return null;
207
+ if (this.fileHandlePool.size >= FILE_HANDLE_POOL_SIZE) {
208
+ const firstKey = this.fileHandlePool.keys().next().value;
209
+ if (firstKey) this.fileHandlePool.delete(firstKey);
210
+ }
211
+ this.fileHandlePool.set(normalizedPath, fileHandle);
212
+ return fileHandle;
213
+ }
214
+ /**
215
+ * Invalidate a specific file handle from the pool
216
+ */
217
+ invalidateFileHandle(path) {
218
+ const normalizedPath = normalize(path);
219
+ this.fileHandlePool.delete(normalizedPath);
220
+ }
221
+ /**
222
+ * Get file or directory handle for a path
223
+ */
224
+ async getHandle(path, opts = {}) {
225
+ const parts = segments(path);
226
+ if (parts.length === 0) {
227
+ const root = await this.rootPromise;
228
+ return { dir: root, name: "", fileHandle: null, dirHandle: root };
229
+ }
230
+ let dir = await this.rootPromise;
231
+ let currentPath = "";
232
+ for (let i = 0; i < parts.length - 1; i++) {
233
+ currentPath += "/" + parts[i];
234
+ if (this.dirCache.has(currentPath)) {
235
+ dir = this.dirCache.get(currentPath);
236
+ continue;
237
+ }
238
+ try {
239
+ dir = await dir.getDirectoryHandle(parts[i], { create: opts.create });
240
+ this.cacheDirHandle(currentPath, dir);
241
+ } catch {
242
+ throw createENOENT(path);
243
+ }
244
+ }
245
+ const name = parts[parts.length - 1];
246
+ try {
247
+ if (opts.kind === "directory") {
248
+ const dirHandle = await dir.getDirectoryHandle(name, { create: opts.create });
249
+ return { dir, name, fileHandle: null, dirHandle };
250
+ } else {
251
+ const fileHandle = await dir.getFileHandle(name, { create: opts.create });
252
+ return { dir, name, fileHandle, dirHandle: null };
253
+ }
254
+ } catch {
255
+ if (!opts.create) {
256
+ return { dir, name, fileHandle: null, dirHandle: null };
257
+ }
258
+ throw createENOENT(path);
259
+ }
260
+ }
261
+ /**
262
+ * Get directory handle with caching
263
+ */
264
+ async getDirectoryHandle(path) {
265
+ const normalizedPath = normalize(path);
266
+ if (normalizedPath === "/" || normalizedPath === "") {
267
+ return this.rootPromise;
268
+ }
269
+ if (this.dirCache.has(normalizedPath)) {
270
+ return this.dirCache.get(normalizedPath);
271
+ }
272
+ const parts = segments(normalizedPath);
273
+ let dir = await this.rootPromise;
274
+ let currentPath = "";
275
+ for (const part of parts) {
276
+ currentPath += "/" + part;
277
+ if (this.dirCache.has(currentPath)) {
278
+ dir = this.dirCache.get(currentPath);
279
+ continue;
280
+ }
281
+ dir = await dir.getDirectoryHandle(part);
282
+ this.cacheDirHandle(currentPath, dir);
283
+ }
284
+ return dir;
285
+ }
286
+ /**
287
+ * Ensure parent directory exists
288
+ */
289
+ async ensureParentDir(path) {
290
+ const parentPath = dirname(path);
291
+ if (parentPath === "/" || parentPath === "") return;
292
+ const parts = segments(parentPath);
293
+ let dir = await this.rootPromise;
294
+ let currentPath = "";
295
+ for (const part of parts) {
296
+ currentPath += "/" + part;
297
+ if (this.dirCache.has(currentPath)) {
298
+ dir = this.dirCache.get(currentPath);
299
+ continue;
300
+ }
301
+ dir = await dir.getDirectoryHandle(part, { create: true });
302
+ this.cacheDirHandle(currentPath, dir);
303
+ }
304
+ }
305
+ /**
306
+ * Create directory (with automatic parent creation)
307
+ */
308
+ async mkdir(path) {
309
+ const normalizedPath = normalize(path);
310
+ this.clearCache(normalizedPath);
311
+ const parts = segments(normalizedPath);
312
+ let dir = await this.rootPromise;
313
+ for (let i = 0; i < parts.length; i++) {
314
+ const part = parts[i];
315
+ const subPath = "/" + parts.slice(0, i + 1).join("/");
316
+ if (this.dirCache.has(subPath)) {
317
+ dir = this.dirCache.get(subPath);
318
+ } else {
319
+ dir = await dir.getDirectoryHandle(part, { create: true });
320
+ this.cacheDirHandle(subPath, dir);
321
+ }
322
+ }
323
+ }
324
+ };
325
+
326
+ // src/symlink-manager.ts
327
+ var SYMLINK_FILE = "/.opfs-symlinks.json";
328
+ var MAX_SYMLINK_DEPTH = 10;
329
+ var SymlinkManager = class {
330
+ cache = null;
331
+ cacheCount = 0;
332
+ // Track count to avoid Object.keys() calls
333
+ resolvedCache = /* @__PURE__ */ new Map();
334
+ // Cache resolved paths
335
+ dirty = false;
336
+ handleManager;
337
+ useSync;
338
+ loadPromise = null;
339
+ // Avoid multiple concurrent loads
340
+ diskLoaded = false;
341
+ // Track if we've loaded from disk
342
+ constructor(handleManager, useSync) {
343
+ this.handleManager = handleManager;
344
+ this.useSync = useSync;
345
+ this.cache = {};
346
+ this.cacheCount = 0;
347
+ }
348
+ /**
349
+ * Reset all symlink state (called when root directory is cleared)
350
+ */
351
+ reset() {
352
+ this.cache = {};
353
+ this.cacheCount = 0;
354
+ this.resolvedCache.clear();
355
+ this.dirty = false;
356
+ this.loadPromise = null;
357
+ this.diskLoaded = false;
358
+ }
359
+ /**
360
+ * Load symlinks from metadata file
361
+ * Uses loadPromise to avoid multiple concurrent disk reads
362
+ */
363
+ async load() {
364
+ if (this.diskLoaded) return this.cache;
365
+ if (this.loadPromise) return this.loadPromise;
366
+ this.loadPromise = this.loadFromDisk();
367
+ const result = await this.loadPromise;
368
+ this.loadPromise = null;
369
+ return result;
370
+ }
371
+ /**
372
+ * Actually read from disk
373
+ */
374
+ async loadFromDisk() {
375
+ try {
376
+ const { fileHandle } = await this.handleManager.getHandle(SYMLINK_FILE);
377
+ if (!fileHandle) {
378
+ this.diskLoaded = true;
379
+ return this.cache;
380
+ }
381
+ const file = await fileHandle.getFile();
382
+ const text = await file.text();
383
+ this.cache = JSON.parse(text);
384
+ this.cacheCount = Object.keys(this.cache).length;
385
+ this.diskLoaded = true;
386
+ } catch {
387
+ if (!this.cache) {
388
+ this.cache = {};
389
+ this.cacheCount = 0;
390
+ }
391
+ this.diskLoaded = true;
392
+ }
393
+ return this.cache;
394
+ }
395
+ /**
396
+ * Save symlinks to metadata file
397
+ */
398
+ async save() {
399
+ if (!this.cache) return;
400
+ const data = JSON.stringify(this.cache);
401
+ const { fileHandle } = await this.handleManager.getHandle(SYMLINK_FILE, { create: true });
402
+ if (!fileHandle) return;
403
+ const buffer = new TextEncoder().encode(data);
404
+ if (this.useSync) {
405
+ const access = await fileHandle.createSyncAccessHandle();
406
+ access.truncate(0);
407
+ let written = 0;
408
+ while (written < buffer.length) {
409
+ written += access.write(buffer.subarray(written), { at: written });
410
+ }
411
+ access.close();
412
+ } else {
413
+ const writable = await fileHandle.createWritable();
414
+ await writable.write(buffer);
415
+ await writable.close();
416
+ }
417
+ this.dirty = false;
418
+ }
419
+ /**
420
+ * Flush pending changes if dirty
421
+ */
422
+ async flush() {
423
+ if (this.dirty) {
424
+ await this.save();
425
+ }
426
+ }
427
+ /**
428
+ * Resolve a path through symlinks
429
+ * Fast synchronous path when cache is already loaded
430
+ * Uses resolved cache for O(1) repeated lookups
431
+ *
432
+ * OPTIMIZATION: If we haven't loaded from disk yet AND no symlinks have been
433
+ * created in this session, we skip the disk check entirely. This makes pure
434
+ * file operations (no symlinks) very fast.
435
+ */
436
+ async resolve(path, maxDepth = MAX_SYMLINK_DEPTH) {
437
+ if (this.cacheCount === 0) {
438
+ return path;
439
+ }
440
+ const cached = this.resolvedCache.get(path);
441
+ if (cached !== void 0) {
442
+ return cached;
443
+ }
444
+ return this.resolveSync(path, this.cache, maxDepth);
445
+ }
446
+ /**
447
+ * Synchronous resolution helper - caches the result
448
+ */
449
+ resolveSync(path, symlinks, maxDepth) {
450
+ let currentPath = path;
451
+ let depth = 0;
452
+ while (symlinks[currentPath] && depth < maxDepth) {
453
+ currentPath = symlinks[currentPath];
454
+ depth++;
455
+ }
456
+ if (depth >= maxDepth) {
457
+ throw createELOOP(path);
458
+ }
459
+ if (currentPath !== path) {
460
+ this.resolvedCache.set(path, currentPath);
461
+ }
462
+ return currentPath;
463
+ }
464
+ /**
465
+ * Clear the resolved path cache (called when symlinks change)
466
+ */
467
+ clearResolvedCache() {
468
+ this.resolvedCache.clear();
469
+ }
470
+ /**
471
+ * Check if a path is a symlink
472
+ */
473
+ async isSymlink(path) {
474
+ const symlinks = await this.load();
475
+ return !!symlinks[path];
476
+ }
477
+ /**
478
+ * Get symlink target
479
+ */
480
+ async readlink(path) {
481
+ const normalizedPath = normalize(path);
482
+ const symlinks = await this.load();
483
+ if (!symlinks[normalizedPath]) {
484
+ throw createEINVAL(path);
485
+ }
486
+ return symlinks[normalizedPath];
487
+ }
488
+ /**
489
+ * Create a symlink
490
+ */
491
+ async symlink(target, path, checkExists) {
492
+ const normalizedPath = normalize(path);
493
+ const normalizedTarget = normalize(target);
494
+ const symlinks = await this.load();
495
+ if (symlinks[normalizedPath]) {
496
+ throw createEEXIST(normalizedPath);
497
+ }
498
+ await checkExists();
499
+ symlinks[normalizedPath] = normalizedTarget;
500
+ this.cacheCount++;
501
+ this.clearResolvedCache();
502
+ this.dirty = true;
503
+ await this.flush();
504
+ }
505
+ /**
506
+ * Create multiple symlinks efficiently
507
+ */
508
+ async symlinkBatch(links, checkExists) {
509
+ const symlinks = await this.load();
510
+ const normalizedLinks = links.map(({ target, path }) => ({
511
+ normalizedPath: normalize(path),
512
+ normalizedTarget: normalize(target)
513
+ }));
514
+ for (const { normalizedPath } of normalizedLinks) {
515
+ if (symlinks[normalizedPath]) {
516
+ throw createEEXIST(normalizedPath);
517
+ }
518
+ }
519
+ await Promise.all(normalizedLinks.map(({ normalizedPath }) => checkExists(normalizedPath)));
520
+ for (const { normalizedPath, normalizedTarget } of normalizedLinks) {
521
+ symlinks[normalizedPath] = normalizedTarget;
522
+ }
523
+ this.cacheCount += links.length;
524
+ this.clearResolvedCache();
525
+ this.dirty = true;
526
+ await this.flush();
527
+ }
528
+ /**
529
+ * Remove a symlink
530
+ */
531
+ async unlink(path) {
532
+ const symlinks = await this.load();
533
+ if (symlinks[path]) {
534
+ delete symlinks[path];
535
+ this.cacheCount--;
536
+ this.clearResolvedCache();
537
+ this.dirty = true;
538
+ await this.flush();
539
+ return true;
540
+ }
541
+ return false;
542
+ }
543
+ /**
544
+ * Rename/move a symlink
545
+ */
546
+ async rename(oldPath, newPath) {
547
+ const symlinks = await this.load();
548
+ if (symlinks[oldPath]) {
549
+ const target = symlinks[oldPath];
550
+ delete symlinks[oldPath];
551
+ symlinks[newPath] = target;
552
+ this.clearResolvedCache();
553
+ this.dirty = true;
554
+ await this.flush();
555
+ return true;
556
+ }
557
+ return false;
558
+ }
559
+ /**
560
+ * Get all symlinks in a directory
561
+ */
562
+ async getSymlinksInDir(dirPath) {
563
+ const symlinks = await this.load();
564
+ const result = [];
565
+ for (const symlinkPath of Object.keys(symlinks)) {
566
+ const parts = symlinkPath.split("/").filter(Boolean);
567
+ const parentPath = "/" + parts.slice(0, -1).join("/");
568
+ if (parentPath === dirPath || dirPath === "/" && parts.length === 1) {
569
+ result.push(parts[parts.length - 1]);
570
+ }
571
+ }
572
+ return result;
573
+ }
574
+ /**
575
+ * Check if path is the symlink metadata file
576
+ */
577
+ isMetadataFile(name) {
578
+ return name === SYMLINK_FILE.replace(/^\/+/, "");
579
+ }
580
+ };
581
+
582
+ // src/packed-storage.ts
583
+ var CRC32_TABLE = new Uint32Array(256);
584
+ for (let i = 0; i < 256; i++) {
585
+ let c = i;
586
+ for (let j = 0; j < 8; j++) {
587
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
588
+ }
589
+ CRC32_TABLE[i] = c;
590
+ }
591
+ function crc32(data) {
592
+ let crc = 4294967295;
593
+ for (let i = 0; i < data.length; i++) {
594
+ crc = CRC32_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
595
+ }
596
+ return (crc ^ 4294967295) >>> 0;
597
+ }
598
+ var PACK_FILE = "/.opfs-pack";
599
+ var PackedStorage = class {
600
+ handleManager;
601
+ useSync;
602
+ index = null;
603
+ indexLoaded = false;
604
+ constructor(handleManager, useSync) {
605
+ this.handleManager = handleManager;
606
+ this.useSync = useSync;
607
+ }
608
+ /**
609
+ * Reset pack storage state (memory only)
610
+ */
611
+ reset() {
612
+ this.index = null;
613
+ this.indexLoaded = false;
614
+ }
615
+ /**
616
+ * Clear pack storage completely (deletes pack file from disk)
617
+ */
618
+ async clear() {
619
+ this.index = null;
620
+ this.indexLoaded = false;
621
+ try {
622
+ const root = await this.handleManager.getRoot();
623
+ await root.removeEntry(PACK_FILE.replace(/^\//, ""));
624
+ } catch {
625
+ }
626
+ }
627
+ /**
628
+ * Load pack index from disk (always reloads to support hybrid mode)
629
+ * Verifies CRC32 checksum for integrity
630
+ */
631
+ async loadIndex() {
632
+ try {
633
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
634
+ if (!fileHandle) {
635
+ return {};
636
+ }
637
+ if (this.useSync) {
638
+ const access = await fileHandle.createSyncAccessHandle();
639
+ const size = access.getSize();
640
+ if (size < 8) {
641
+ access.close();
642
+ return {};
643
+ }
644
+ const header = new Uint8Array(8);
645
+ access.read(header, { at: 0 });
646
+ const view = new DataView(header.buffer);
647
+ const indexLen = view.getUint32(0, true);
648
+ const storedCrc = view.getUint32(4, true);
649
+ const contentSize = size - 8;
650
+ const content = new Uint8Array(contentSize);
651
+ access.read(content, { at: 8 });
652
+ access.close();
653
+ const calculatedCrc = crc32(content);
654
+ if (calculatedCrc !== storedCrc) {
655
+ throw createECORRUPTED(PACK_FILE);
656
+ }
657
+ const indexJson = new TextDecoder().decode(content.subarray(0, indexLen));
658
+ return JSON.parse(indexJson);
659
+ } else {
660
+ const file = await fileHandle.getFile();
661
+ const data = new Uint8Array(await file.arrayBuffer());
662
+ if (data.length < 8) {
663
+ return {};
664
+ }
665
+ const view = new DataView(data.buffer);
666
+ const indexLen = view.getUint32(0, true);
667
+ const storedCrc = view.getUint32(4, true);
668
+ const content = data.subarray(8);
669
+ const calculatedCrc = crc32(content);
670
+ if (calculatedCrc !== storedCrc) {
671
+ throw createECORRUPTED(PACK_FILE);
672
+ }
673
+ const indexJson = new TextDecoder().decode(content.subarray(0, indexLen));
674
+ return JSON.parse(indexJson);
675
+ }
676
+ } catch {
677
+ return {};
678
+ }
679
+ }
680
+ /**
681
+ * Check if a path exists in the pack
682
+ */
683
+ async has(path) {
684
+ const index = await this.loadIndex();
685
+ return path in index;
686
+ }
687
+ /**
688
+ * Get file size from pack (for stat)
689
+ */
690
+ async getSize(path) {
691
+ const index = await this.loadIndex();
692
+ const entry = index[path];
693
+ return entry ? entry.size : null;
694
+ }
695
+ /**
696
+ * Read a file from the pack
697
+ */
698
+ async read(path) {
699
+ const index = await this.loadIndex();
700
+ const entry = index[path];
701
+ if (!entry) return null;
702
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
703
+ if (!fileHandle) return null;
704
+ const buffer = new Uint8Array(entry.size);
705
+ if (this.useSync) {
706
+ const access = await fileHandle.createSyncAccessHandle();
707
+ access.read(buffer, { at: entry.offset });
708
+ access.close();
709
+ } else {
710
+ const file = await fileHandle.getFile();
711
+ const data = new Uint8Array(await file.arrayBuffer());
712
+ buffer.set(data.subarray(entry.offset, entry.offset + entry.size));
713
+ }
714
+ return buffer;
715
+ }
716
+ /**
717
+ * Read multiple files from the pack in a single operation
718
+ * Loads index once, reads all data in parallel
719
+ */
720
+ async readBatch(paths) {
721
+ const results = /* @__PURE__ */ new Map();
722
+ if (paths.length === 0) return results;
723
+ const index = await this.loadIndex();
724
+ const toRead = [];
725
+ for (const path of paths) {
726
+ const entry = index[path];
727
+ if (entry) {
728
+ toRead.push({ path, offset: entry.offset, size: entry.size });
729
+ } else {
730
+ results.set(path, null);
731
+ }
732
+ }
733
+ if (toRead.length === 0) return results;
734
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
735
+ if (!fileHandle) {
736
+ for (const { path } of toRead) {
737
+ results.set(path, null);
738
+ }
739
+ return results;
740
+ }
741
+ if (this.useSync) {
742
+ const access = await fileHandle.createSyncAccessHandle();
743
+ for (const { path, offset, size } of toRead) {
744
+ const buffer = new Uint8Array(size);
745
+ access.read(buffer, { at: offset });
746
+ results.set(path, buffer);
747
+ }
748
+ access.close();
749
+ } else {
750
+ const file = await fileHandle.getFile();
751
+ const data = new Uint8Array(await file.arrayBuffer());
752
+ for (const { path, offset, size } of toRead) {
753
+ const buffer = new Uint8Array(size);
754
+ buffer.set(data.subarray(offset, offset + size));
755
+ results.set(path, buffer);
756
+ }
757
+ }
758
+ return results;
759
+ }
760
+ /**
761
+ * Write multiple files to the pack in a single operation
762
+ * This is the key optimization - 100 files become 1 write!
763
+ * Includes CRC32 checksum for integrity verification.
764
+ * Note: This replaces the entire pack with the new entries
765
+ */
766
+ async writeBatch(entries) {
767
+ if (entries.length === 0) return;
768
+ const encoder = new TextEncoder();
769
+ let totalDataSize = 0;
770
+ for (const { data } of entries) {
771
+ totalDataSize += data.length;
772
+ }
773
+ const newIndex = {};
774
+ let headerSize = 8;
775
+ let prevHeaderSize = 0;
776
+ while (headerSize !== prevHeaderSize) {
777
+ prevHeaderSize = headerSize;
778
+ let currentOffset = headerSize;
779
+ for (const { path, data } of entries) {
780
+ newIndex[path] = { offset: currentOffset, size: data.length };
781
+ currentOffset += data.length;
782
+ }
783
+ const indexBuf = encoder.encode(JSON.stringify(newIndex));
784
+ headerSize = 8 + indexBuf.length;
785
+ }
786
+ const finalIndexBuf = encoder.encode(JSON.stringify(newIndex));
787
+ const totalSize = headerSize + totalDataSize;
788
+ const packBuffer = new Uint8Array(totalSize);
789
+ const view = new DataView(packBuffer.buffer);
790
+ packBuffer.set(finalIndexBuf, 8);
791
+ for (const { path, data } of entries) {
792
+ const entry = newIndex[path];
793
+ packBuffer.set(data, entry.offset);
794
+ }
795
+ const content = packBuffer.subarray(8);
796
+ const checksum = crc32(content);
797
+ view.setUint32(0, finalIndexBuf.length, true);
798
+ view.setUint32(4, checksum, true);
799
+ await this.writePackFile(packBuffer);
800
+ this.index = newIndex;
801
+ }
802
+ /**
803
+ * Write the pack file to OPFS
804
+ */
805
+ async writePackFile(data) {
806
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE, { create: true });
807
+ if (!fileHandle) return;
808
+ if (this.useSync) {
809
+ const access = await fileHandle.createSyncAccessHandle();
810
+ access.truncate(data.length);
811
+ access.write(data, { at: 0 });
812
+ access.close();
813
+ } else {
814
+ const writable = await fileHandle.createWritable();
815
+ await writable.write(data);
816
+ await writable.close();
817
+ }
818
+ }
819
+ /**
820
+ * Remove a path from the pack index
821
+ * Note: Doesn't reclaim space, just removes from index and recalculates CRC32
822
+ */
823
+ async remove(path) {
824
+ const index = await this.loadIndex();
825
+ if (!(path in index)) return false;
826
+ delete index[path];
827
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
828
+ if (!fileHandle) return true;
829
+ const encoder = new TextEncoder();
830
+ const newIndexBuf = encoder.encode(JSON.stringify(index));
831
+ if (this.useSync) {
832
+ const access = await fileHandle.createSyncAccessHandle();
833
+ const size = access.getSize();
834
+ const oldHeader = new Uint8Array(8);
835
+ access.read(oldHeader, { at: 0 });
836
+ const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true);
837
+ const dataStart = 8 + oldIndexLen;
838
+ const dataSize = size - dataStart;
839
+ const dataPortion = new Uint8Array(dataSize);
840
+ if (dataSize > 0) {
841
+ access.read(dataPortion, { at: dataStart });
842
+ }
843
+ const newContent = new Uint8Array(newIndexBuf.length + dataSize);
844
+ newContent.set(newIndexBuf, 0);
845
+ if (dataSize > 0) {
846
+ newContent.set(dataPortion, newIndexBuf.length);
847
+ }
848
+ const checksum = crc32(newContent);
849
+ const newHeader = new Uint8Array(8);
850
+ const view = new DataView(newHeader.buffer);
851
+ view.setUint32(0, newIndexBuf.length, true);
852
+ view.setUint32(4, checksum, true);
853
+ const newFile = new Uint8Array(8 + newContent.length);
854
+ newFile.set(newHeader, 0);
855
+ newFile.set(newContent, 8);
856
+ access.truncate(newFile.length);
857
+ access.write(newFile, { at: 0 });
858
+ access.close();
859
+ } else {
860
+ const file = await fileHandle.getFile();
861
+ const oldData = new Uint8Array(await file.arrayBuffer());
862
+ if (oldData.length < 8) return true;
863
+ const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true);
864
+ const dataStart = 8 + oldIndexLen;
865
+ const dataPortion = oldData.subarray(dataStart);
866
+ const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length);
867
+ newContent.set(newIndexBuf, 0);
868
+ newContent.set(dataPortion, newIndexBuf.length);
869
+ const checksum = crc32(newContent);
870
+ const newFile = new Uint8Array(8 + newContent.length);
871
+ const view = new DataView(newFile.buffer);
872
+ view.setUint32(0, newIndexBuf.length, true);
873
+ view.setUint32(4, checksum, true);
874
+ newFile.set(newContent, 8);
875
+ const writable = await fileHandle.createWritable();
876
+ await writable.write(newFile);
877
+ await writable.close();
878
+ }
879
+ return true;
880
+ }
881
+ /**
882
+ * Check if pack file is being used (has entries)
883
+ */
884
+ async isEmpty() {
885
+ const index = await this.loadIndex();
886
+ return Object.keys(index).length === 0;
887
+ }
888
+ };
889
+
890
+ // src/file-handle.ts
891
+ function createFileHandle(resolvedPath, initialPosition, context) {
892
+ let position = initialPosition;
893
+ return {
894
+ fd: Math.floor(Math.random() * 1e6),
895
+ async read(buffer, offset = 0, length = buffer.length, pos = null) {
896
+ const readPos = pos !== null ? pos : position;
897
+ const data = await context.readFile(resolvedPath);
898
+ const bytesToRead = Math.min(length, data.length - readPos);
899
+ buffer.set(data.subarray(readPos, readPos + bytesToRead), offset);
900
+ if (pos === null) position += bytesToRead;
901
+ return { bytesRead: bytesToRead, buffer };
902
+ },
903
+ async write(buffer, offset = 0, length = buffer.length, pos = null) {
904
+ const writePos = pos !== null ? pos : position;
905
+ let existingData = new Uint8Array(0);
906
+ try {
907
+ existingData = await context.readFile(resolvedPath);
908
+ } catch (e) {
909
+ if (e.code !== "ENOENT") throw e;
910
+ }
911
+ const dataToWrite = buffer.subarray(offset, offset + length);
912
+ const newSize = Math.max(existingData.length, writePos + length);
913
+ const newData = new Uint8Array(newSize);
914
+ newData.set(existingData, 0);
915
+ newData.set(dataToWrite, writePos);
916
+ await context.writeFile(resolvedPath, newData);
917
+ if (pos === null) position += length;
918
+ return { bytesWritten: length, buffer };
919
+ },
920
+ async close() {
921
+ },
922
+ async stat() {
923
+ return context.stat(resolvedPath);
924
+ },
925
+ async truncate(len = 0) {
926
+ return context.truncate(resolvedPath, len);
927
+ },
928
+ async sync() {
929
+ },
930
+ async datasync() {
931
+ },
932
+ async readFile(options) {
933
+ return context.readFile(resolvedPath, options);
934
+ },
935
+ async writeFile(data, options) {
936
+ return context.writeFile(resolvedPath, data, options);
937
+ },
938
+ async appendFile(data, options) {
939
+ return context.appendFile(resolvedPath, data, options);
940
+ },
941
+ [Symbol.asyncDispose]: async function() {
942
+ }
943
+ };
944
+ }
945
+
946
+ // src/streams.ts
947
+ function createReadStream(path, options, context) {
948
+ const { start = 0, end = Infinity, highWaterMark = 64 * 1024 } = options;
949
+ let position = start;
950
+ let closed = false;
951
+ let cachedData = null;
952
+ return new ReadableStream({
953
+ async pull(controller) {
954
+ if (closed) {
955
+ controller.close();
956
+ return;
957
+ }
958
+ try {
959
+ if (cachedData === null) {
960
+ cachedData = await context.readFile(path);
961
+ }
962
+ const endPos = Math.min(end, cachedData.length);
963
+ const chunk = cachedData.subarray(position, Math.min(position + highWaterMark, endPos));
964
+ if (chunk.length === 0 || position >= endPos) {
965
+ controller.close();
966
+ closed = true;
967
+ cachedData = null;
968
+ return;
969
+ }
970
+ position += chunk.length;
971
+ controller.enqueue(chunk);
972
+ } catch (err) {
973
+ controller.error(err);
974
+ }
975
+ },
976
+ cancel() {
977
+ closed = true;
978
+ cachedData = null;
979
+ }
980
+ });
981
+ }
982
+ function createWriteStream(path, options, context) {
983
+ const { flags = "w", start = 0 } = options;
984
+ const chunks = [];
985
+ let position = start;
986
+ return new WritableStream({
987
+ async write(chunk) {
988
+ chunks.push({ data: chunk, position });
989
+ position += chunk.length;
990
+ },
991
+ async close() {
992
+ let existingData = new Uint8Array(0);
993
+ if (!flags.includes("w")) {
994
+ try {
995
+ existingData = await context.readFile(path);
996
+ } catch (e) {
997
+ if (e.code !== "ENOENT") throw e;
998
+ }
999
+ }
1000
+ let maxSize = existingData.length;
1001
+ for (const { data, position: position2 } of chunks) {
1002
+ maxSize = Math.max(maxSize, position2 + data.length);
1003
+ }
1004
+ const finalData = new Uint8Array(maxSize);
1005
+ if (!flags.includes("w")) {
1006
+ finalData.set(existingData, 0);
1007
+ }
1008
+ for (const { data, position: position2 } of chunks) {
1009
+ finalData.set(data, position2);
1010
+ }
1011
+ await context.writeFile(path, finalData);
1012
+ }
1013
+ });
1014
+ }
1015
+
1016
+ // src/opfs-worker-proxy.ts
1017
+ var OPFSWorker = class {
1018
+ worker = null;
1019
+ pendingRequests = /* @__PURE__ */ new Map();
1020
+ nextId = 1;
1021
+ readyPromise;
1022
+ readyResolve;
1023
+ /** File system constants */
1024
+ constants = constants;
1025
+ constructor(options = {}) {
1026
+ this.readyPromise = new Promise((resolve) => {
1027
+ this.readyResolve = resolve;
1028
+ });
1029
+ this.initWorker(options);
1030
+ }
1031
+ initWorker(options) {
1032
+ const { workerUrl, workerOptions = { type: "module" } } = options;
1033
+ if (workerUrl) {
1034
+ this.worker = new Worker(workerUrl, workerOptions);
1035
+ } else {
1036
+ throw new Error(
1037
+ 'OPFSWorker requires a workerUrl option pointing to the worker script. Example: new OPFSWorker({ workerUrl: new URL("./opfs-worker.js", import.meta.url) })'
1038
+ );
1039
+ }
1040
+ this.worker.onmessage = (event) => {
1041
+ const { id, type, result, error } = event.data;
1042
+ if (type === "ready") {
1043
+ this.readyResolve();
1044
+ return;
1045
+ }
1046
+ if (id !== void 0) {
1047
+ const pending = this.pendingRequests.get(id);
1048
+ if (pending) {
1049
+ this.pendingRequests.delete(id);
1050
+ if (error) {
1051
+ const fsError = new FSError(error.message, error.code || "UNKNOWN");
1052
+ pending.reject(fsError);
1053
+ } else {
1054
+ pending.resolve(result);
1055
+ }
1056
+ }
1057
+ }
1058
+ };
1059
+ this.worker.onerror = (event) => {
1060
+ console.error("[OPFSWorker] Worker error:", event);
1061
+ };
1062
+ }
1063
+ /**
1064
+ * Wait for the worker to be ready
1065
+ */
1066
+ async ready() {
1067
+ return this.readyPromise;
1068
+ }
1069
+ /**
1070
+ * Terminate the worker
1071
+ */
1072
+ terminate() {
1073
+ if (this.worker) {
1074
+ this.worker.terminate();
1075
+ this.worker = null;
1076
+ for (const [, pending] of this.pendingRequests) {
1077
+ pending.reject(new Error("Worker terminated"));
1078
+ }
1079
+ this.pendingRequests.clear();
1080
+ }
1081
+ }
1082
+ call(method, args, transfer) {
1083
+ return new Promise((resolve, reject) => {
1084
+ if (!this.worker) {
1085
+ reject(new Error("Worker not initialized or terminated"));
1086
+ return;
1087
+ }
1088
+ const id = this.nextId++;
1089
+ this.pendingRequests.set(id, {
1090
+ resolve,
1091
+ reject
1092
+ });
1093
+ const message = { id, method, args };
1094
+ if (transfer && transfer.length > 0) {
1095
+ this.worker.postMessage(message, transfer);
1096
+ } else {
1097
+ this.worker.postMessage(message);
1098
+ }
1099
+ });
1100
+ }
1101
+ // File operations
1102
+ async readFile(path, options) {
1103
+ const result = await this.call("readFile", [path, options]);
1104
+ return result;
1105
+ }
1106
+ async writeFile(path, data, options) {
1107
+ await this.call("writeFile", [path, data, options]);
1108
+ }
1109
+ async readFileBatch(paths) {
1110
+ return this.call("readFileBatch", [paths]);
1111
+ }
1112
+ async writeFileBatch(entries) {
1113
+ await this.call("writeFileBatch", [entries]);
1114
+ }
1115
+ async appendFile(path, data, options) {
1116
+ await this.call("appendFile", [path, data, options]);
1117
+ }
1118
+ async copyFile(src, dest, mode) {
1119
+ await this.call("copyFile", [src, dest, mode]);
1120
+ }
1121
+ async unlink(path) {
1122
+ await this.call("unlink", [path]);
1123
+ }
1124
+ async truncate(path, len) {
1125
+ await this.call("truncate", [path, len]);
1126
+ }
1127
+ // Directory operations
1128
+ async mkdir(path) {
1129
+ await this.call("mkdir", [path]);
1130
+ }
1131
+ async rmdir(path) {
1132
+ await this.call("rmdir", [path]);
1133
+ }
1134
+ async readdir(path, options) {
1135
+ const result = await this.call("readdir", [path, options]);
1136
+ if (options?.withFileTypes && Array.isArray(result)) {
1137
+ return result.map((item) => {
1138
+ if (typeof item === "object" && "name" in item) {
1139
+ const entry = item;
1140
+ return {
1141
+ name: entry.name,
1142
+ isFile: () => entry._isFile ?? false,
1143
+ isDirectory: () => entry._isDir ?? false,
1144
+ isSymbolicLink: () => entry._isSymlink ?? false
1145
+ };
1146
+ }
1147
+ return item;
1148
+ });
1149
+ }
1150
+ return result;
1151
+ }
1152
+ async cp(src, dest, options) {
1153
+ await this.call("cp", [src, dest, options]);
1154
+ }
1155
+ async rm(path, options) {
1156
+ await this.call("rm", [path, options]);
1157
+ }
1158
+ // Stat operations
1159
+ async stat(path) {
1160
+ const result = await this.call("stat", [path]);
1161
+ return this.deserializeStats(result);
1162
+ }
1163
+ async lstat(path) {
1164
+ const result = await this.call("lstat", [path]);
1165
+ return this.deserializeStats(result);
1166
+ }
1167
+ deserializeStats(data) {
1168
+ const ctime = new Date(data.ctime);
1169
+ const mtime = new Date(data.mtime);
1170
+ return {
1171
+ type: data.type,
1172
+ size: data.size,
1173
+ mode: data.mode,
1174
+ ctime,
1175
+ ctimeMs: data.ctimeMs,
1176
+ mtime,
1177
+ mtimeMs: data.mtimeMs,
1178
+ target: data.target,
1179
+ isFile: () => data.type === "file",
1180
+ isDirectory: () => data.type === "dir",
1181
+ isSymbolicLink: () => data.type === "symlink"
1182
+ };
1183
+ }
1184
+ async exists(path) {
1185
+ return this.call("exists", [path]);
1186
+ }
1187
+ async access(path, mode) {
1188
+ await this.call("access", [path, mode]);
1189
+ }
1190
+ async statfs(path) {
1191
+ return this.call("statfs", [path]);
1192
+ }
1193
+ async du(path) {
1194
+ return this.call("du", [path]);
1195
+ }
1196
+ // Symlink operations
1197
+ async symlink(target, path) {
1198
+ await this.call("symlink", [target, path]);
1199
+ }
1200
+ async readlink(path) {
1201
+ return this.call("readlink", [path]);
1202
+ }
1203
+ async symlinkBatch(links) {
1204
+ await this.call("symlinkBatch", [links]);
1205
+ }
1206
+ async realpath(path) {
1207
+ return this.call("realpath", [path]);
1208
+ }
1209
+ // Other operations
1210
+ async rename(oldPath, newPath) {
1211
+ await this.call("rename", [oldPath, newPath]);
1212
+ }
1213
+ async mkdtemp(prefix) {
1214
+ return this.call("mkdtemp", [prefix]);
1215
+ }
1216
+ async chmod(path, mode) {
1217
+ await this.call("chmod", [path, mode]);
1218
+ }
1219
+ async chown(path, uid, gid) {
1220
+ await this.call("chown", [path, uid, gid]);
1221
+ }
1222
+ async utimes(path, atime, mtime) {
1223
+ await this.call("utimes", [path, atime, mtime]);
1224
+ }
1225
+ async lutimes(path, atime, mtime) {
1226
+ await this.call("lutimes", [path, atime, mtime]);
1227
+ }
1228
+ /**
1229
+ * Reset internal caches to free memory
1230
+ * Useful for long-running benchmarks or after bulk operations
1231
+ */
1232
+ async resetCache() {
1233
+ await this.call("resetCache", []);
1234
+ }
1235
+ /**
1236
+ * Force full garbage collection by reinitializing the OPFS instance in the worker
1237
+ * This completely releases all handles and caches, preventing memory leaks in long-running operations
1238
+ * More aggressive than resetCache() - use when resetCache() isn't sufficient
1239
+ */
1240
+ async gc() {
1241
+ await this.call("gc", []);
1242
+ }
1243
+ };
1244
+
1245
+ // src/opfs-hybrid.ts
1246
+ var OPFSHybrid = class {
1247
+ mainFs;
1248
+ workerFs = null;
1249
+ readBackend;
1250
+ writeBackend;
1251
+ workerUrl;
1252
+ workerReady = null;
1253
+ verbose;
1254
+ constructor(options = {}) {
1255
+ this.readBackend = options.read ?? "main";
1256
+ this.writeBackend = options.write ?? "worker";
1257
+ this.workerUrl = options.workerUrl;
1258
+ this.verbose = options.verbose ?? false;
1259
+ this.mainFs = new OPFS({ useSync: false, verbose: this.verbose });
1260
+ if (this.readBackend === "worker" || this.writeBackend === "worker") {
1261
+ if (!this.workerUrl) {
1262
+ throw new Error("workerUrl is required when using worker backend");
1263
+ }
1264
+ this.workerFs = new OPFSWorker({ workerUrl: this.workerUrl });
1265
+ this.workerReady = this.workerFs.ready();
1266
+ }
1267
+ }
1268
+ /**
1269
+ * Wait for all backends to be ready
1270
+ */
1271
+ async ready() {
1272
+ if (this.workerReady) {
1273
+ await this.workerReady;
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Terminate worker if active
1278
+ */
1279
+ terminate() {
1280
+ if (this.workerFs) {
1281
+ this.workerFs.terminate();
1282
+ this.workerFs = null;
1283
+ }
1284
+ }
1285
+ getReadFs() {
1286
+ if (this.readBackend === "worker" && this.workerFs) {
1287
+ return this.workerFs;
1288
+ }
1289
+ return this.mainFs;
1290
+ }
1291
+ getWriteFs() {
1292
+ if (this.writeBackend === "worker" && this.workerFs) {
1293
+ return this.workerFs;
1294
+ }
1295
+ return this.mainFs;
1296
+ }
1297
+ // ============ Read Operations ============
1298
+ async readFile(path, options) {
1299
+ return this.getReadFs().readFile(path, options);
1300
+ }
1301
+ async readFileBatch(paths) {
1302
+ return this.getReadFs().readFileBatch(paths);
1303
+ }
1304
+ async readdir(path, options) {
1305
+ return this.getReadFs().readdir(path, options);
1306
+ }
1307
+ async stat(path) {
1308
+ return this.getReadFs().stat(path);
1309
+ }
1310
+ async lstat(path) {
1311
+ return this.getReadFs().lstat(path);
1312
+ }
1313
+ async exists(path) {
1314
+ return this.getReadFs().exists(path);
1315
+ }
1316
+ async access(path, mode) {
1317
+ return this.getReadFs().access(path, mode);
1318
+ }
1319
+ async readlink(path) {
1320
+ return this.getReadFs().readlink(path);
1321
+ }
1322
+ async realpath(path) {
1323
+ return this.getReadFs().realpath(path);
1324
+ }
1325
+ async statfs(path) {
1326
+ return this.getReadFs().statfs(path);
1327
+ }
1328
+ async du(path) {
1329
+ return this.getReadFs().du(path);
1330
+ }
1331
+ // ============ Write Operations ============
1332
+ async writeFile(path, data, options) {
1333
+ return this.getWriteFs().writeFile(path, data, options);
1334
+ }
1335
+ async writeFileBatch(entries) {
1336
+ return this.getWriteFs().writeFileBatch(entries);
1337
+ }
1338
+ async appendFile(path, data, options) {
1339
+ return this.getWriteFs().appendFile(path, data, options);
1340
+ }
1341
+ async mkdir(path) {
1342
+ return this.getWriteFs().mkdir(path);
1343
+ }
1344
+ async rmdir(path) {
1345
+ if (this.readBackend !== this.writeBackend && this.workerFs) {
1346
+ await this.workerFs.rmdir(path);
1347
+ this.mainFs.resetCache();
1348
+ } else {
1349
+ return this.getWriteFs().rmdir(path);
1350
+ }
1351
+ }
1352
+ async unlink(path) {
1353
+ return this.getWriteFs().unlink(path);
1354
+ }
1355
+ async truncate(path, len) {
1356
+ return this.getWriteFs().truncate(path, len);
1357
+ }
1358
+ async symlink(target, path) {
1359
+ if (this.readBackend !== this.writeBackend && this.workerFs) {
1360
+ await this.workerFs.symlink(target, path);
1361
+ this.mainFs.resetCache();
1362
+ } else {
1363
+ return this.getWriteFs().symlink(target, path);
1364
+ }
1365
+ }
1366
+ async symlinkBatch(symlinks) {
1367
+ if (this.readBackend !== this.writeBackend && this.workerFs) {
1368
+ await this.workerFs.symlinkBatch(symlinks);
1369
+ this.mainFs.resetCache();
1370
+ } else {
1371
+ return this.getWriteFs().symlinkBatch(symlinks);
1372
+ }
1373
+ }
1374
+ async rename(oldPath, newPath) {
1375
+ return this.getWriteFs().rename(oldPath, newPath);
1376
+ }
1377
+ async copyFile(src, dest, mode) {
1378
+ return this.getWriteFs().copyFile(src, dest, mode);
1379
+ }
1380
+ async cp(src, dest, options) {
1381
+ return this.getWriteFs().cp(src, dest, options);
1382
+ }
1383
+ async rm(path, options) {
1384
+ return this.getWriteFs().rm(path, options);
1385
+ }
1386
+ async chmod(path, mode) {
1387
+ return this.getWriteFs().chmod(path, mode);
1388
+ }
1389
+ async chown(path, uid, gid) {
1390
+ return this.getWriteFs().chown(path, uid, gid);
1391
+ }
1392
+ async utimes(path, atime, mtime) {
1393
+ return this.getWriteFs().utimes(path, atime, mtime);
1394
+ }
1395
+ async lutimes(path, atime, mtime) {
1396
+ return this.getWriteFs().lutimes(path, atime, mtime);
1397
+ }
1398
+ async mkdtemp(prefix) {
1399
+ return this.getWriteFs().mkdtemp(prefix);
1400
+ }
1401
+ /**
1402
+ * Reset internal caches on both backends
1403
+ */
1404
+ async resetCache() {
1405
+ this.mainFs.resetCache();
1406
+ if (this.workerFs) {
1407
+ await this.workerFs.resetCache();
1408
+ }
1409
+ }
1410
+ /**
1411
+ * Force full garbage collection on both backends
1412
+ * More aggressive than resetCache() - reinitializes the worker's OPFS instance
1413
+ */
1414
+ async gc() {
1415
+ this.mainFs.resetCache();
1416
+ if (this.workerFs) {
1417
+ await this.workerFs.gc();
1418
+ }
1419
+ }
1420
+ };
1421
+
1422
+ // src/index.ts
1423
+ var OPFS = class {
1424
+ useSync;
1425
+ verbose;
1426
+ handleManager;
1427
+ symlinkManager;
1428
+ packedStorage;
1429
+ watchCallbacks = /* @__PURE__ */ new Map();
1430
+ tmpCounter = 0;
1431
+ /** Hybrid instance when workerUrl is provided */
1432
+ hybrid = null;
1433
+ /** File system constants */
1434
+ constants = constants;
1435
+ constructor(options = {}) {
1436
+ const { useSync = true, verbose = false, workerUrl, read, write } = options;
1437
+ this.verbose = verbose;
1438
+ if (workerUrl) {
1439
+ this.hybrid = new OPFSHybrid({
1440
+ workerUrl,
1441
+ read: read ?? "main",
1442
+ write: write ?? "worker",
1443
+ verbose
1444
+ });
1445
+ this.useSync = false;
1446
+ this.handleManager = new HandleManager();
1447
+ this.symlinkManager = new SymlinkManager(this.handleManager, false);
1448
+ this.packedStorage = new PackedStorage(this.handleManager, false);
1449
+ } else {
1450
+ this.useSync = useSync && typeof FileSystemFileHandle !== "undefined" && "createSyncAccessHandle" in FileSystemFileHandle.prototype;
1451
+ this.handleManager = new HandleManager();
1452
+ this.symlinkManager = new SymlinkManager(this.handleManager, this.useSync);
1453
+ this.packedStorage = new PackedStorage(this.handleManager, this.useSync);
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Wait for the filesystem to be ready (only needed for hybrid mode)
1458
+ */
1459
+ async ready() {
1460
+ if (this.hybrid) {
1461
+ await this.hybrid.ready();
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Terminate any background workers (only needed for hybrid mode)
1466
+ */
1467
+ terminate() {
1468
+ if (this.hybrid) {
1469
+ this.hybrid.terminate();
1470
+ }
1471
+ }
1472
+ log(method, ...args) {
1473
+ if (this.verbose) {
1474
+ console.log(`[OPFS] ${method}:`, ...args);
1475
+ }
1476
+ }
1477
+ logError(method, err) {
1478
+ if (this.verbose) {
1479
+ console.error(`[OPFS] ${method} error:`, err);
1480
+ }
1481
+ }
1482
+ /**
1483
+ * Execute tasks with limited concurrency to avoid overwhelming the system
1484
+ * @param items - Array of items to process
1485
+ * @param maxConcurrent - Maximum number of concurrent operations (default: 10)
1486
+ * @param taskFn - Function to execute for each item
1487
+ */
1488
+ async limitConcurrency(items, maxConcurrent, taskFn) {
1489
+ if (items.length === 0) return;
1490
+ if (items.length <= 2) {
1491
+ for (const item of items) {
1492
+ await taskFn(item);
1493
+ }
1494
+ return;
1495
+ }
1496
+ if (items.length <= maxConcurrent) {
1497
+ await Promise.all(items.map(taskFn));
1498
+ return;
1499
+ }
1500
+ const queue = [...items];
1501
+ const workers = Array.from({ length: maxConcurrent }).map(async () => {
1502
+ while (queue.length) {
1503
+ const item = queue.shift();
1504
+ if (item !== void 0) await taskFn(item);
1505
+ }
1506
+ });
1507
+ await Promise.all(workers);
1508
+ }
1509
+ /**
1510
+ * Read file contents
1511
+ */
1512
+ async readFile(path, options = {}) {
1513
+ if (this.hybrid) {
1514
+ return this.hybrid.readFile(path, options);
1515
+ }
1516
+ this.log("readFile", path, options);
1517
+ try {
1518
+ const normalizedPath = normalize(path);
1519
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
1520
+ let fileHandle = null;
1521
+ try {
1522
+ fileHandle = await this.handleManager.getPooledFileHandle(resolvedPath);
1523
+ } catch {
1524
+ }
1525
+ if (fileHandle) {
1526
+ let buffer;
1527
+ if (this.useSync) {
1528
+ const access = await fileHandle.createSyncAccessHandle();
1529
+ const size = access.getSize();
1530
+ buffer = new Uint8Array(size);
1531
+ access.read(buffer);
1532
+ access.close();
1533
+ } else {
1534
+ const file = await fileHandle.getFile();
1535
+ buffer = new Uint8Array(await file.arrayBuffer());
1536
+ }
1537
+ return options.encoding ? new TextDecoder(options.encoding).decode(buffer) : buffer;
1538
+ }
1539
+ const packedData = await this.packedStorage.read(resolvedPath);
1540
+ if (packedData) {
1541
+ return options.encoding ? new TextDecoder(options.encoding).decode(packedData) : packedData;
1542
+ }
1543
+ throw createENOENT(path);
1544
+ } catch (err) {
1545
+ this.logError("readFile", err);
1546
+ throw wrapError(err);
1547
+ }
1548
+ }
1549
+ /**
1550
+ * Read multiple files efficiently in a batch operation
1551
+ * Uses packed storage batch read (single index load), falls back to individual files
1552
+ * Returns results in the same order as input paths
1553
+ */
1554
+ async readFileBatch(paths) {
1555
+ if (this.hybrid) {
1556
+ return this.hybrid.readFileBatch(paths);
1557
+ }
1558
+ this.log("readFileBatch", `${paths.length} files`);
1559
+ if (paths.length === 0) return [];
1560
+ try {
1561
+ const resolvedPaths = await Promise.all(
1562
+ paths.map(async (path) => {
1563
+ const normalizedPath = normalize(path);
1564
+ return this.symlinkManager.resolve(normalizedPath);
1565
+ })
1566
+ );
1567
+ const packedResults = await this.packedStorage.readBatch(resolvedPaths);
1568
+ const results = new Array(paths.length);
1569
+ const needsIndividualRead = [];
1570
+ for (let i = 0; i < paths.length; i++) {
1571
+ const packedData = packedResults.get(resolvedPaths[i]);
1572
+ if (packedData) {
1573
+ results[i] = { path: paths[i], data: packedData };
1574
+ } else {
1575
+ needsIndividualRead.push({ index: i, resolvedPath: resolvedPaths[i] });
1576
+ }
1577
+ }
1578
+ if (needsIndividualRead.length > 0) {
1579
+ await Promise.all(
1580
+ needsIndividualRead.map(async ({ index, resolvedPath }) => {
1581
+ try {
1582
+ const fileHandle = await this.handleManager.getPooledFileHandle(resolvedPath);
1583
+ if (!fileHandle) {
1584
+ results[index] = { path: paths[index], data: null, error: createENOENT(paths[index]) };
1585
+ return;
1586
+ }
1587
+ let buffer;
1588
+ if (this.useSync) {
1589
+ const access = await fileHandle.createSyncAccessHandle();
1590
+ const size = access.getSize();
1591
+ buffer = new Uint8Array(size);
1592
+ access.read(buffer);
1593
+ access.close();
1594
+ } else {
1595
+ const file = await fileHandle.getFile();
1596
+ buffer = new Uint8Array(await file.arrayBuffer());
1597
+ }
1598
+ results[index] = { path: paths[index], data: buffer };
1599
+ } catch (err) {
1600
+ results[index] = { path: paths[index], data: null, error: err };
1601
+ }
1602
+ })
1603
+ );
1604
+ }
1605
+ return results;
1606
+ } catch (err) {
1607
+ this.logError("readFileBatch", err);
1608
+ throw wrapError(err);
1609
+ }
1610
+ }
1611
+ /**
1612
+ * Write data to a file
1613
+ */
1614
+ async writeFile(path, data, options = {}) {
1615
+ if (this.hybrid) {
1616
+ return this.hybrid.writeFile(path, data, options);
1617
+ }
1618
+ this.log("writeFile", path);
1619
+ try {
1620
+ const normalizedPath = normalize(path);
1621
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
1622
+ const { fileHandle } = await this.handleManager.getHandle(resolvedPath, { create: true });
1623
+ const buffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
1624
+ if (this.useSync) {
1625
+ const access = await fileHandle.createSyncAccessHandle();
1626
+ access.truncate(buffer.length);
1627
+ access.write(buffer, { at: 0 });
1628
+ access.close();
1629
+ } else {
1630
+ const writable = await fileHandle.createWritable();
1631
+ await writable.write(buffer);
1632
+ await writable.close();
1633
+ }
1634
+ } catch (err) {
1635
+ this.logError("writeFile", err);
1636
+ throw wrapError(err);
1637
+ }
1638
+ }
1639
+ /**
1640
+ * Write multiple files efficiently in a batch operation
1641
+ * Uses packed storage (single file) for maximum performance
1642
+ */
1643
+ async writeFileBatch(entries) {
1644
+ if (this.hybrid) {
1645
+ return this.hybrid.writeFileBatch(entries);
1646
+ }
1647
+ this.log("writeFileBatch", `${entries.length} files`);
1648
+ if (entries.length === 0) return;
1649
+ try {
1650
+ const encoder = new TextEncoder();
1651
+ const packEntries = await Promise.all(
1652
+ entries.map(async ({ path, data }) => {
1653
+ const normalizedPath = normalize(path);
1654
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
1655
+ return {
1656
+ path: resolvedPath,
1657
+ data: typeof data === "string" ? encoder.encode(data) : data
1658
+ };
1659
+ })
1660
+ );
1661
+ await this.packedStorage.writeBatch(packEntries);
1662
+ } catch (err) {
1663
+ this.logError("writeFileBatch", err);
1664
+ throw wrapError(err);
1665
+ }
1666
+ }
1667
+ /**
1668
+ * Create a directory
1669
+ */
1670
+ async mkdir(path) {
1671
+ if (this.hybrid) {
1672
+ return this.hybrid.mkdir(path);
1673
+ }
1674
+ this.log("mkdir", path);
1675
+ try {
1676
+ await this.handleManager.mkdir(path);
1677
+ } catch (err) {
1678
+ this.logError("mkdir", err);
1679
+ throw wrapError(err);
1680
+ }
1681
+ }
1682
+ /**
1683
+ * Remove a directory
1684
+ */
1685
+ async rmdir(path) {
1686
+ if (this.hybrid) {
1687
+ return this.hybrid.rmdir(path);
1688
+ }
1689
+ this.log("rmdir", path);
1690
+ try {
1691
+ const normalizedPath = normalize(path);
1692
+ this.handleManager.clearCache(normalizedPath);
1693
+ if (isRoot(normalizedPath)) {
1694
+ const root = await this.handleManager.getRoot();
1695
+ const entries = [];
1696
+ for await (const [name2] of root.entries()) {
1697
+ entries.push(name2);
1698
+ }
1699
+ await this.limitConcurrency(
1700
+ entries,
1701
+ 10,
1702
+ (name2) => root.removeEntry(name2, { recursive: true })
1703
+ );
1704
+ this.symlinkManager.reset();
1705
+ this.packedStorage.reset();
1706
+ return;
1707
+ }
1708
+ const pathSegments = segments(normalizedPath);
1709
+ const name = pathSegments.pop();
1710
+ let dir = await this.handleManager.getRoot();
1711
+ for (const part of pathSegments) {
1712
+ dir = await dir.getDirectoryHandle(part);
1713
+ }
1714
+ try {
1715
+ await dir.removeEntry(name, { recursive: true });
1716
+ } catch {
1717
+ throw createENOENT(path);
1718
+ }
1719
+ } catch (err) {
1720
+ this.logError("rmdir", err);
1721
+ throw wrapError(err);
1722
+ }
1723
+ }
1724
+ /**
1725
+ * Remove a file or symlink
1726
+ */
1727
+ async unlink(path) {
1728
+ if (this.hybrid) {
1729
+ return this.hybrid.unlink(path);
1730
+ }
1731
+ this.log("unlink", path);
1732
+ try {
1733
+ const normalizedPath = normalize(path);
1734
+ this.handleManager.clearCache(normalizedPath);
1735
+ const isSymlink = await this.symlinkManager.isSymlink(normalizedPath);
1736
+ if (isSymlink) {
1737
+ await this.symlinkManager.unlink(normalizedPath);
1738
+ return;
1739
+ }
1740
+ const inPack = await this.packedStorage.has(normalizedPath);
1741
+ if (inPack) {
1742
+ await this.packedStorage.remove(normalizedPath);
1743
+ return;
1744
+ }
1745
+ const { dir, name, fileHandle } = await this.handleManager.getHandle(normalizedPath);
1746
+ if (!fileHandle) throw createENOENT(path);
1747
+ try {
1748
+ await dir.removeEntry(name);
1749
+ } catch {
1750
+ throw createENOENT(path);
1751
+ }
1752
+ } catch (err) {
1753
+ this.logError("unlink", err);
1754
+ throw wrapError(err);
1755
+ }
1756
+ }
1757
+ /**
1758
+ * Read directory contents
1759
+ */
1760
+ async readdir(path, options) {
1761
+ if (this.hybrid) {
1762
+ return this.hybrid.readdir(path, options);
1763
+ }
1764
+ this.log("readdir", path, options);
1765
+ try {
1766
+ const normalizedPath = normalize(path);
1767
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
1768
+ const dir = await this.handleManager.getDirectoryHandle(resolvedPath);
1769
+ const withFileTypes = options?.withFileTypes === true;
1770
+ const symlinksInDir = await this.symlinkManager.getSymlinksInDir(resolvedPath);
1771
+ const hasSymlinks = symlinksInDir.length > 0;
1772
+ const symlinkSet = hasSymlinks ? new Set(symlinksInDir) : null;
1773
+ const entryNames = /* @__PURE__ */ new Set();
1774
+ const entries = [];
1775
+ for await (const [name, handle] of dir.entries()) {
1776
+ if (this.symlinkManager.isMetadataFile(name)) continue;
1777
+ entryNames.add(name);
1778
+ if (withFileTypes) {
1779
+ const isSymlink = hasSymlinks && symlinkSet.has(name);
1780
+ entries.push({
1781
+ name,
1782
+ isFile: () => !isSymlink && handle.kind === "file",
1783
+ isDirectory: () => !isSymlink && handle.kind === "directory",
1784
+ isSymbolicLink: () => isSymlink
1785
+ });
1786
+ } else {
1787
+ entries.push(name);
1788
+ }
1789
+ }
1790
+ if (hasSymlinks) {
1791
+ for (const name of symlinksInDir) {
1792
+ if (!entryNames.has(name)) {
1793
+ if (withFileTypes) {
1794
+ entries.push({
1795
+ name,
1796
+ isFile: () => false,
1797
+ isDirectory: () => false,
1798
+ isSymbolicLink: () => true
1799
+ });
1800
+ } else {
1801
+ entries.push(name);
1802
+ }
1803
+ }
1804
+ }
1805
+ }
1806
+ return entries;
1807
+ } catch (err) {
1808
+ this.logError("readdir", err);
1809
+ throw wrapError(err);
1810
+ }
1811
+ }
1812
+ /**
1813
+ * Get file/directory statistics (follows symlinks)
1814
+ */
1815
+ async stat(path) {
1816
+ if (this.hybrid) {
1817
+ return this.hybrid.stat(path);
1818
+ }
1819
+ this.log("stat", path);
1820
+ try {
1821
+ const normalizedPath = normalize(path);
1822
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
1823
+ const defaultDate = /* @__PURE__ */ new Date(0);
1824
+ if (isRoot(resolvedPath)) {
1825
+ return {
1826
+ type: "dir",
1827
+ size: 0,
1828
+ mode: 16877,
1829
+ ctime: defaultDate,
1830
+ ctimeMs: 0,
1831
+ mtime: defaultDate,
1832
+ mtimeMs: 0,
1833
+ isFile: () => false,
1834
+ isDirectory: () => true,
1835
+ isSymbolicLink: () => false
1836
+ };
1837
+ }
1838
+ const pathSegments = segments(resolvedPath);
1839
+ const name = pathSegments.pop();
1840
+ let dir = await this.handleManager.getRoot();
1841
+ for (const part of pathSegments) {
1842
+ try {
1843
+ dir = await dir.getDirectoryHandle(part);
1844
+ } catch {
1845
+ throw createENOENT(path);
1846
+ }
1847
+ }
1848
+ const [fileResult, dirResult] = await Promise.allSettled([
1849
+ dir.getFileHandle(name),
1850
+ dir.getDirectoryHandle(name)
1851
+ ]);
1852
+ if (fileResult.status === "fulfilled") {
1853
+ const fileHandle = fileResult.value;
1854
+ const file = await fileHandle.getFile();
1855
+ const mtime = file.lastModified ? new Date(file.lastModified) : defaultDate;
1856
+ return {
1857
+ type: "file",
1858
+ size: file.size,
1859
+ mode: 33188,
1860
+ ctime: mtime,
1861
+ ctimeMs: mtime.getTime(),
1862
+ mtime,
1863
+ mtimeMs: mtime.getTime(),
1864
+ isFile: () => true,
1865
+ isDirectory: () => false,
1866
+ isSymbolicLink: () => false
1867
+ };
1868
+ }
1869
+ if (dirResult.status === "fulfilled") {
1870
+ return {
1871
+ type: "dir",
1872
+ size: 0,
1873
+ mode: 16877,
1874
+ ctime: defaultDate,
1875
+ ctimeMs: 0,
1876
+ mtime: defaultDate,
1877
+ mtimeMs: 0,
1878
+ isFile: () => false,
1879
+ isDirectory: () => true,
1880
+ isSymbolicLink: () => false
1881
+ };
1882
+ }
1883
+ const packedSize = await this.packedStorage.getSize(resolvedPath);
1884
+ if (packedSize !== null) {
1885
+ return {
1886
+ type: "file",
1887
+ size: packedSize,
1888
+ mode: 33188,
1889
+ ctime: defaultDate,
1890
+ ctimeMs: 0,
1891
+ mtime: defaultDate,
1892
+ mtimeMs: 0,
1893
+ isFile: () => true,
1894
+ isDirectory: () => false,
1895
+ isSymbolicLink: () => false
1896
+ };
1897
+ }
1898
+ throw createENOENT(path);
1899
+ } catch (err) {
1900
+ this.logError("stat", err);
1901
+ throw wrapError(err);
1902
+ }
1903
+ }
1904
+ /**
1905
+ * Get file/directory statistics (does not follow symlinks)
1906
+ */
1907
+ async lstat(path) {
1908
+ if (this.hybrid) {
1909
+ return this.hybrid.lstat(path);
1910
+ }
1911
+ this.log("lstat", path);
1912
+ try {
1913
+ const normalizedPath = normalize(path);
1914
+ const isSymlink = await this.symlinkManager.isSymlink(normalizedPath);
1915
+ if (isSymlink) {
1916
+ const target = await this.symlinkManager.readlink(normalizedPath);
1917
+ return {
1918
+ type: "symlink",
1919
+ target,
1920
+ size: target.length,
1921
+ mode: 41471,
1922
+ ctime: /* @__PURE__ */ new Date(0),
1923
+ ctimeMs: 0,
1924
+ mtime: /* @__PURE__ */ new Date(0),
1925
+ mtimeMs: 0,
1926
+ isFile: () => false,
1927
+ isDirectory: () => false,
1928
+ isSymbolicLink: () => true
1929
+ };
1930
+ }
1931
+ return this.stat(path);
1932
+ } catch (err) {
1933
+ this.logError("lstat", err);
1934
+ throw wrapError(err);
1935
+ }
1936
+ }
1937
+ /**
1938
+ * Rename a file or directory
1939
+ */
1940
+ async rename(oldPath, newPath) {
1941
+ if (this.hybrid) {
1942
+ return this.hybrid.rename(oldPath, newPath);
1943
+ }
1944
+ this.log("rename", oldPath, newPath);
1945
+ try {
1946
+ const normalizedOld = normalize(oldPath);
1947
+ const normalizedNew = normalize(newPath);
1948
+ this.handleManager.clearCache(normalizedOld);
1949
+ this.handleManager.clearCache(normalizedNew);
1950
+ const renamed = await this.symlinkManager.rename(normalizedOld, normalizedNew);
1951
+ if (renamed) return;
1952
+ const stat = await this.stat(normalizedOld);
1953
+ if (stat.isFile()) {
1954
+ const [data] = await Promise.all([
1955
+ this.readFile(normalizedOld),
1956
+ this.handleManager.ensureParentDir(normalizedNew)
1957
+ ]);
1958
+ await this.writeFile(normalizedNew, data);
1959
+ await this.unlink(normalizedOld);
1960
+ } else if (stat.isDirectory()) {
1961
+ await this.mkdir(normalizedNew);
1962
+ const entries = await this.readdir(normalizedOld);
1963
+ await this.limitConcurrency(
1964
+ entries,
1965
+ 10,
1966
+ (entry) => this.rename(`${normalizedOld}/${entry}`, `${normalizedNew}/${entry}`)
1967
+ );
1968
+ await this.rmdir(normalizedOld);
1969
+ }
1970
+ } catch (err) {
1971
+ this.logError("rename", err);
1972
+ throw wrapError(err);
1973
+ }
1974
+ }
1975
+ /**
1976
+ * Create a symbolic link
1977
+ */
1978
+ async symlink(target, path) {
1979
+ if (this.hybrid) {
1980
+ return this.hybrid.symlink(target, path);
1981
+ }
1982
+ this.log("symlink", target, path);
1983
+ try {
1984
+ const normalizedPath = normalize(path);
1985
+ this.handleManager.clearCache(normalizedPath);
1986
+ await this.symlinkManager.symlink(target, path, async () => {
1987
+ const { fileHandle, dirHandle } = await this.handleManager.getHandle(normalizedPath);
1988
+ if (fileHandle || dirHandle) {
1989
+ throw createEEXIST(path);
1990
+ }
1991
+ });
1992
+ } catch (err) {
1993
+ this.logError("symlink", err);
1994
+ throw wrapError(err);
1995
+ }
1996
+ }
1997
+ /**
1998
+ * Read symlink target
1999
+ */
2000
+ async readlink(path) {
2001
+ if (this.hybrid) {
2002
+ return this.hybrid.readlink(path);
2003
+ }
2004
+ this.log("readlink", path);
2005
+ try {
2006
+ return await this.symlinkManager.readlink(path);
2007
+ } catch (err) {
2008
+ this.logError("readlink", err);
2009
+ throw wrapError(err);
2010
+ }
2011
+ }
2012
+ /**
2013
+ * Create multiple symlinks efficiently
2014
+ */
2015
+ async symlinkBatch(links) {
2016
+ if (this.hybrid) {
2017
+ return this.hybrid.symlinkBatch(links);
2018
+ }
2019
+ this.log("symlinkBatch", links.length, "links");
2020
+ try {
2021
+ for (const { path } of links) {
2022
+ this.handleManager.clearCache(normalize(path));
2023
+ }
2024
+ await this.symlinkManager.symlinkBatch(links, async (normalizedPath) => {
2025
+ try {
2026
+ const { fileHandle, dirHandle } = await this.handleManager.getHandle(normalizedPath);
2027
+ if (fileHandle || dirHandle) {
2028
+ throw createEEXIST(normalizedPath);
2029
+ }
2030
+ } catch (err) {
2031
+ if (err.code === "ENOENT") return;
2032
+ throw err;
2033
+ }
2034
+ });
2035
+ } catch (err) {
2036
+ this.logError("symlinkBatch", err);
2037
+ throw wrapError(err);
2038
+ }
2039
+ }
2040
+ /**
2041
+ * Check file accessibility
2042
+ */
2043
+ async access(path, mode = constants.F_OK) {
2044
+ if (this.hybrid) {
2045
+ return this.hybrid.access(path, mode);
2046
+ }
2047
+ this.log("access", path, mode);
2048
+ try {
2049
+ const normalizedPath = normalize(path);
2050
+ await this.stat(normalizedPath);
2051
+ } catch (err) {
2052
+ this.logError("access", err);
2053
+ throw createEACCES(path);
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Append data to a file
2058
+ */
2059
+ async appendFile(path, data, options = {}) {
2060
+ if (this.hybrid) {
2061
+ return this.hybrid.appendFile(path, data, options);
2062
+ }
2063
+ this.log("appendFile", path);
2064
+ try {
2065
+ const normalizedPath = normalize(path);
2066
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
2067
+ let existingData = new Uint8Array(0);
2068
+ try {
2069
+ const result = await this.readFile(resolvedPath);
2070
+ existingData = result instanceof Uint8Array ? result : new TextEncoder().encode(result);
2071
+ } catch (err) {
2072
+ if (err.code !== "ENOENT") throw err;
2073
+ }
2074
+ const newData = typeof data === "string" ? new TextEncoder().encode(data) : data;
2075
+ const combined = new Uint8Array(existingData.length + newData.length);
2076
+ combined.set(existingData, 0);
2077
+ combined.set(newData, existingData.length);
2078
+ await this.writeFile(resolvedPath, combined, options);
2079
+ } catch (err) {
2080
+ this.logError("appendFile", err);
2081
+ throw wrapError(err);
2082
+ }
2083
+ }
2084
+ /**
2085
+ * Copy a file
2086
+ */
2087
+ async copyFile(src, dest, mode = 0) {
2088
+ if (this.hybrid) {
2089
+ return this.hybrid.copyFile(src, dest, mode);
2090
+ }
2091
+ this.log("copyFile", src, dest, mode);
2092
+ try {
2093
+ const normalizedSrc = normalize(src);
2094
+ const normalizedDest = normalize(dest);
2095
+ const resolvedSrc = await this.symlinkManager.resolve(normalizedSrc);
2096
+ if (mode & constants.COPYFILE_EXCL) {
2097
+ try {
2098
+ await this.stat(normalizedDest);
2099
+ throw createEEXIST(dest);
2100
+ } catch (err) {
2101
+ if (err.code !== "ENOENT") throw err;
2102
+ }
2103
+ }
2104
+ const [data] = await Promise.all([
2105
+ this.readFile(resolvedSrc),
2106
+ this.handleManager.ensureParentDir(normalizedDest)
2107
+ ]);
2108
+ await this.writeFile(normalizedDest, data);
2109
+ } catch (err) {
2110
+ this.logError("copyFile", err);
2111
+ throw wrapError(err);
2112
+ }
2113
+ }
2114
+ /**
2115
+ * Copy files/directories recursively
2116
+ */
2117
+ async cp(src, dest, options = {}) {
2118
+ if (this.hybrid) {
2119
+ return this.hybrid.cp(src, dest, options);
2120
+ }
2121
+ this.log("cp", src, dest, options);
2122
+ try {
2123
+ const normalizedSrc = normalize(src);
2124
+ const normalizedDest = normalize(dest);
2125
+ const { recursive = false, force = false, errorOnExist = false } = options;
2126
+ const srcStat = await this.stat(normalizedSrc);
2127
+ if (srcStat.isDirectory()) {
2128
+ if (!recursive) {
2129
+ throw createEISDIR(src);
2130
+ }
2131
+ let destExists = false;
2132
+ try {
2133
+ await this.stat(normalizedDest);
2134
+ destExists = true;
2135
+ if (errorOnExist && !force) {
2136
+ throw createEEXIST(dest);
2137
+ }
2138
+ } catch (err) {
2139
+ if (err.code !== "ENOENT") throw err;
2140
+ }
2141
+ if (!destExists) {
2142
+ await this.mkdir(normalizedDest);
2143
+ }
2144
+ const entries = await this.readdir(normalizedSrc);
2145
+ await this.limitConcurrency(
2146
+ entries,
2147
+ 10,
2148
+ (entry) => this.cp(`${normalizedSrc}/${entry}`, `${normalizedDest}/${entry}`, options)
2149
+ );
2150
+ } else {
2151
+ if (errorOnExist) {
2152
+ try {
2153
+ await this.stat(normalizedDest);
2154
+ throw createEEXIST(dest);
2155
+ } catch (err) {
2156
+ if (err.code !== "ENOENT") throw err;
2157
+ }
2158
+ }
2159
+ await this.copyFile(normalizedSrc, normalizedDest);
2160
+ }
2161
+ } catch (err) {
2162
+ this.logError("cp", err);
2163
+ throw wrapError(err);
2164
+ }
2165
+ }
2166
+ /**
2167
+ * Check if path exists
2168
+ */
2169
+ async exists(path) {
2170
+ if (this.hybrid) {
2171
+ return this.hybrid.exists(path);
2172
+ }
2173
+ this.log("exists", path);
2174
+ try {
2175
+ await this.stat(normalize(path));
2176
+ return true;
2177
+ } catch {
2178
+ return false;
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Resolve symlinks to get real path
2183
+ */
2184
+ async realpath(path) {
2185
+ if (this.hybrid) {
2186
+ return this.hybrid.realpath(path);
2187
+ }
2188
+ this.log("realpath", path);
2189
+ const normalizedPath = normalize(path);
2190
+ return this.symlinkManager.resolve(normalizedPath);
2191
+ }
2192
+ /**
2193
+ * Remove files and directories
2194
+ */
2195
+ async rm(path, options = {}) {
2196
+ if (this.hybrid) {
2197
+ return this.hybrid.rm(path, options);
2198
+ }
2199
+ this.log("rm", path, options);
2200
+ try {
2201
+ const normalizedPath = normalize(path);
2202
+ const { recursive = false, force = false } = options;
2203
+ try {
2204
+ const stat = await this.lstat(normalizedPath);
2205
+ if (stat.isSymbolicLink()) {
2206
+ await this.unlink(normalizedPath);
2207
+ } else if (stat.isDirectory()) {
2208
+ if (!recursive) {
2209
+ throw createEISDIR(path);
2210
+ }
2211
+ await this.rmdir(normalizedPath);
2212
+ } else {
2213
+ await this.unlink(normalizedPath);
2214
+ }
2215
+ } catch (err) {
2216
+ if (err.code === "ENOENT" && force) {
2217
+ return;
2218
+ }
2219
+ throw err;
2220
+ }
2221
+ } catch (err) {
2222
+ this.logError("rm", err);
2223
+ throw wrapError(err);
2224
+ }
2225
+ }
2226
+ /**
2227
+ * Truncate file to specified length
2228
+ */
2229
+ async truncate(path, len = 0) {
2230
+ if (this.hybrid) {
2231
+ return this.hybrid.truncate(path, len);
2232
+ }
2233
+ this.log("truncate", path, len);
2234
+ try {
2235
+ const normalizedPath = normalize(path);
2236
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
2237
+ this.handleManager.clearCache(resolvedPath);
2238
+ const { fileHandle } = await this.handleManager.getHandle(resolvedPath);
2239
+ if (!fileHandle) throw createENOENT(path);
2240
+ if (this.useSync) {
2241
+ const access = await fileHandle.createSyncAccessHandle();
2242
+ access.truncate(len);
2243
+ access.close();
2244
+ } else {
2245
+ const file = await fileHandle.getFile();
2246
+ const data = new Uint8Array(await file.arrayBuffer());
2247
+ const finalData = new Uint8Array(len);
2248
+ const copyLen = Math.min(len, data.length);
2249
+ if (copyLen > 0) {
2250
+ finalData.set(data.subarray(0, copyLen), 0);
2251
+ }
2252
+ const writable = await fileHandle.createWritable();
2253
+ await writable.write(finalData);
2254
+ await writable.close();
2255
+ }
2256
+ } catch (err) {
2257
+ this.logError("truncate", err);
2258
+ throw wrapError(err);
2259
+ }
2260
+ }
2261
+ /**
2262
+ * Create a unique temporary directory
2263
+ */
2264
+ async mkdtemp(prefix) {
2265
+ if (this.hybrid) {
2266
+ return this.hybrid.mkdtemp(prefix);
2267
+ }
2268
+ this.log("mkdtemp", prefix);
2269
+ try {
2270
+ const normalizedPrefix = normalize(prefix);
2271
+ const suffix = `${Date.now()}-${++this.tmpCounter}-${Math.random().toString(36).slice(2, 8)}`;
2272
+ const path = `${normalizedPrefix}${suffix}`;
2273
+ await this.mkdir(path);
2274
+ return path;
2275
+ } catch (err) {
2276
+ this.logError("mkdtemp", err);
2277
+ throw wrapError(err);
2278
+ }
2279
+ }
2280
+ /**
2281
+ * Change file mode (no-op for OPFS compatibility)
2282
+ */
2283
+ async chmod(path, mode) {
2284
+ if (this.hybrid) {
2285
+ return this.hybrid.chmod(path, mode);
2286
+ }
2287
+ this.log("chmod", path, mode);
2288
+ await this.stat(normalize(path));
2289
+ }
2290
+ /**
2291
+ * Change file owner (no-op for OPFS compatibility)
2292
+ */
2293
+ async chown(path, uid, gid) {
2294
+ if (this.hybrid) {
2295
+ return this.hybrid.chown(path, uid, gid);
2296
+ }
2297
+ this.log("chown", path, uid, gid);
2298
+ await this.stat(normalize(path));
2299
+ }
2300
+ /**
2301
+ * Update file timestamps (no-op for OPFS compatibility)
2302
+ */
2303
+ async utimes(path, atime, mtime) {
2304
+ if (this.hybrid) {
2305
+ return this.hybrid.utimes(path, atime, mtime);
2306
+ }
2307
+ this.log("utimes", path, atime, mtime);
2308
+ await this.stat(normalize(path));
2309
+ }
2310
+ /**
2311
+ * Update symlink timestamps (no-op)
2312
+ */
2313
+ async lutimes(path, atime, mtime) {
2314
+ if (this.hybrid) {
2315
+ return this.hybrid.lutimes(path, atime, mtime);
2316
+ }
2317
+ this.log("lutimes", path, atime, mtime);
2318
+ await this.lstat(normalize(path));
2319
+ }
2320
+ /**
2321
+ * Open file and return FileHandle
2322
+ */
2323
+ async open(path, flags = "r", mode = 438) {
2324
+ this.log("open", path, flags, mode);
2325
+ try {
2326
+ const normalizedPath = normalize(path);
2327
+ const flagStr = flagsToString(flags);
2328
+ const shouldCreate = flagStr.includes("w") || flagStr.includes("a") || flagStr.includes("+");
2329
+ const shouldTruncate = flagStr.includes("w");
2330
+ const shouldAppend = flagStr.includes("a");
2331
+ if (shouldCreate) {
2332
+ await this.handleManager.ensureParentDir(normalizedPath);
2333
+ }
2334
+ const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
2335
+ const { fileHandle } = await this.handleManager.getHandle(resolvedPath, { create: shouldCreate });
2336
+ if (!fileHandle && !shouldCreate) {
2337
+ throw createENOENT(path);
2338
+ }
2339
+ if (shouldTruncate && fileHandle) {
2340
+ await this.truncate(resolvedPath, 0);
2341
+ }
2342
+ const initialPosition = shouldAppend ? (await this.stat(resolvedPath)).size : 0;
2343
+ return createFileHandle(resolvedPath, initialPosition, {
2344
+ readFile: (p, o) => this.readFile(p, o),
2345
+ writeFile: (p, d) => this.writeFile(p, d),
2346
+ stat: (p) => this.stat(p),
2347
+ truncate: (p, l) => this.truncate(p, l),
2348
+ appendFile: (p, d, o) => this.appendFile(p, d, o)
2349
+ });
2350
+ } catch (err) {
2351
+ this.logError("open", err);
2352
+ throw wrapError(err);
2353
+ }
2354
+ }
2355
+ /**
2356
+ * Open directory for iteration
2357
+ */
2358
+ async opendir(path) {
2359
+ this.log("opendir", path);
2360
+ try {
2361
+ const normalizedPath = normalize(path);
2362
+ const entries = await this.readdir(normalizedPath, { withFileTypes: true });
2363
+ let index = 0;
2364
+ return {
2365
+ path: normalizedPath,
2366
+ async read() {
2367
+ if (index >= entries.length) return null;
2368
+ return entries[index++];
2369
+ },
2370
+ async close() {
2371
+ index = entries.length;
2372
+ },
2373
+ async *[Symbol.asyncIterator]() {
2374
+ for (const entry of entries) {
2375
+ yield entry;
2376
+ }
2377
+ }
2378
+ };
2379
+ } catch (err) {
2380
+ this.logError("opendir", err);
2381
+ throw wrapError(err);
2382
+ }
2383
+ }
2384
+ /**
2385
+ * Watch for file changes
2386
+ */
2387
+ watch(path, options = {}) {
2388
+ this.log("watch", path, options);
2389
+ const normalizedPath = normalize(path);
2390
+ const { recursive = false, signal } = options;
2391
+ const callbacks = /* @__PURE__ */ new Set();
2392
+ const id = /* @__PURE__ */ Symbol("watcher");
2393
+ this.watchCallbacks.set(id, { path: normalizedPath, callbacks, recursive });
2394
+ if (signal) {
2395
+ signal.addEventListener("abort", () => {
2396
+ this.watchCallbacks.delete(id);
2397
+ });
2398
+ }
2399
+ const self2 = this;
2400
+ return {
2401
+ close() {
2402
+ self2.watchCallbacks.delete(id);
2403
+ },
2404
+ ref() {
2405
+ return this;
2406
+ },
2407
+ unref() {
2408
+ return this;
2409
+ },
2410
+ [Symbol.asyncIterator]() {
2411
+ const queue = [];
2412
+ let resolver = null;
2413
+ callbacks.add((eventType, filename) => {
2414
+ const event = { eventType, filename };
2415
+ if (resolver) {
2416
+ resolver({ value: event, done: false });
2417
+ resolver = null;
2418
+ } else {
2419
+ queue.push(event);
2420
+ }
2421
+ });
2422
+ return {
2423
+ next() {
2424
+ if (queue.length > 0) {
2425
+ return Promise.resolve({ value: queue.shift(), done: false });
2426
+ }
2427
+ return new Promise((resolve) => {
2428
+ resolver = resolve;
2429
+ });
2430
+ },
2431
+ return() {
2432
+ return Promise.resolve({ done: true, value: void 0 });
2433
+ }
2434
+ };
2435
+ }
2436
+ };
2437
+ }
2438
+ /**
2439
+ * Create read stream
2440
+ */
2441
+ createReadStream(path, options = {}) {
2442
+ this.log("createReadStream", path, options);
2443
+ const normalizedPath = normalize(path);
2444
+ return createReadStream(normalizedPath, options, {
2445
+ readFile: (p) => this.readFile(p)
2446
+ });
2447
+ }
2448
+ /**
2449
+ * Create write stream
2450
+ */
2451
+ createWriteStream(path, options = {}) {
2452
+ this.log("createWriteStream", path, options);
2453
+ const normalizedPath = normalize(path);
2454
+ return createWriteStream(normalizedPath, options, {
2455
+ readFile: (p) => this.readFile(p),
2456
+ writeFile: (p, d) => this.writeFile(p, d)
2457
+ });
2458
+ }
2459
+ /**
2460
+ * Get file statistics (alias for stat)
2461
+ */
2462
+ async backFile(path) {
2463
+ this.log("backFile", path);
2464
+ try {
2465
+ return await this.stat(normalize(path));
2466
+ } catch (err) {
2467
+ if (err.code === "ENOENT") throw err;
2468
+ throw createENOENT(path);
2469
+ }
2470
+ }
2471
+ /**
2472
+ * Get disk usage for a path
2473
+ */
2474
+ async du(path) {
2475
+ if (this.hybrid) {
2476
+ return this.hybrid.du(path);
2477
+ }
2478
+ this.log("du", path);
2479
+ const normalizedPath = normalize(path);
2480
+ const stat = await this.stat(normalizedPath);
2481
+ return { path: normalizedPath, size: stat.size };
2482
+ }
2483
+ /**
2484
+ * Get filesystem statistics (similar to Node.js fs.statfs)
2485
+ * Uses the Storage API to get quota and usage information
2486
+ * Note: Values are estimates for the entire origin, not per-path
2487
+ */
2488
+ async statfs(path) {
2489
+ if (this.hybrid) {
2490
+ return this.hybrid.statfs(path);
2491
+ }
2492
+ this.log("statfs", path);
2493
+ try {
2494
+ if (path) {
2495
+ await this.stat(normalize(path));
2496
+ }
2497
+ if (typeof navigator === "undefined" || !navigator.storage?.estimate) {
2498
+ throw new Error("Storage API not available");
2499
+ }
2500
+ const estimate = await navigator.storage.estimate();
2501
+ const usage = estimate.usage ?? 0;
2502
+ const quota = estimate.quota ?? 0;
2503
+ const bsize = 4096;
2504
+ return {
2505
+ type: 0,
2506
+ bsize,
2507
+ blocks: Math.floor(quota / bsize),
2508
+ bfree: Math.floor((quota - usage) / bsize),
2509
+ bavail: Math.floor((quota - usage) / bsize),
2510
+ files: 0,
2511
+ ffree: 0,
2512
+ usage,
2513
+ quota
2514
+ };
2515
+ } catch (err) {
2516
+ this.logError("statfs", err);
2517
+ throw wrapError(err);
2518
+ }
2519
+ }
2520
+ /**
2521
+ * Reset internal caches
2522
+ * Useful when external processes modify the filesystem
2523
+ */
2524
+ resetCache() {
2525
+ if (this.hybrid) {
2526
+ this.hybrid.resetCache();
2527
+ return;
2528
+ }
2529
+ this.symlinkManager.reset();
2530
+ this.packedStorage.reset();
2531
+ this.handleManager.clearCache();
2532
+ }
2533
+ /**
2534
+ * Force full garbage collection
2535
+ * Releases all handles and caches, reinitializes the worker in hybrid mode
2536
+ * Use this for long-running operations to prevent memory leaks
2537
+ */
2538
+ async gc() {
2539
+ if (this.hybrid) {
2540
+ await this.hybrid.gc();
2541
+ return;
2542
+ }
2543
+ this.symlinkManager.reset();
2544
+ await this.packedStorage.clear();
2545
+ this.handleManager.clearCache();
2546
+ }
2547
+ };
2548
+
2549
+ // src/opfs-worker.ts
2550
+ var fs = null;
2551
+ function getFS() {
2552
+ if (!fs) {
2553
+ fs = new OPFS({ useSync: true, verbose: false });
2554
+ }
2555
+ return fs;
2556
+ }
2557
+ self.onmessage = async (event) => {
2558
+ const { id, method, args } = event.data;
2559
+ try {
2560
+ const opfs = getFS();
2561
+ let result;
2562
+ const transfer = [];
2563
+ switch (method) {
2564
+ // File operations
2565
+ case "readFile": {
2566
+ const data = await opfs.readFile(args[0], args[1]);
2567
+ if (data instanceof Uint8Array) {
2568
+ result = data;
2569
+ transfer.push(data.buffer);
2570
+ } else {
2571
+ result = data;
2572
+ }
2573
+ break;
2574
+ }
2575
+ case "writeFile":
2576
+ await opfs.writeFile(args[0], args[1], args[2]);
2577
+ result = void 0;
2578
+ break;
2579
+ case "readFileBatch": {
2580
+ const results = await opfs.readFileBatch(args[0]);
2581
+ for (const r of results) {
2582
+ if (r.data) {
2583
+ transfer.push(r.data.buffer);
2584
+ }
2585
+ }
2586
+ result = results;
2587
+ break;
2588
+ }
2589
+ case "writeFileBatch":
2590
+ await opfs.writeFileBatch(args[0]);
2591
+ result = void 0;
2592
+ break;
2593
+ case "appendFile":
2594
+ await opfs.appendFile(args[0], args[1], args[2]);
2595
+ result = void 0;
2596
+ break;
2597
+ case "copyFile":
2598
+ await opfs.copyFile(args[0], args[1], args[2]);
2599
+ result = void 0;
2600
+ break;
2601
+ case "unlink":
2602
+ await opfs.unlink(args[0]);
2603
+ result = void 0;
2604
+ break;
2605
+ case "truncate":
2606
+ await opfs.truncate(args[0], args[1]);
2607
+ result = void 0;
2608
+ break;
2609
+ // Directory operations
2610
+ case "mkdir":
2611
+ await opfs.mkdir(args[0]);
2612
+ result = void 0;
2613
+ break;
2614
+ case "rmdir":
2615
+ await opfs.rmdir(args[0]);
2616
+ result = void 0;
2617
+ break;
2618
+ case "readdir":
2619
+ result = await opfs.readdir(args[0], args[1]);
2620
+ break;
2621
+ case "cp":
2622
+ await opfs.cp(args[0], args[1], args[2]);
2623
+ result = void 0;
2624
+ break;
2625
+ case "rm":
2626
+ await opfs.rm(args[0], args[1]);
2627
+ result = void 0;
2628
+ break;
2629
+ // Stat operations
2630
+ case "stat":
2631
+ result = serializeStats(await opfs.stat(args[0]));
2632
+ break;
2633
+ case "lstat":
2634
+ result = serializeStats(await opfs.lstat(args[0]));
2635
+ break;
2636
+ case "exists":
2637
+ result = await opfs.exists(args[0]);
2638
+ break;
2639
+ case "access":
2640
+ await opfs.access(args[0], args[1]);
2641
+ result = void 0;
2642
+ break;
2643
+ case "statfs":
2644
+ result = await opfs.statfs(args[0]);
2645
+ break;
2646
+ case "du":
2647
+ result = await opfs.du(args[0]);
2648
+ break;
2649
+ // Symlink operations
2650
+ case "symlink":
2651
+ await opfs.symlink(args[0], args[1]);
2652
+ result = void 0;
2653
+ break;
2654
+ case "readlink":
2655
+ result = await opfs.readlink(args[0]);
2656
+ break;
2657
+ case "symlinkBatch":
2658
+ await opfs.symlinkBatch(args[0]);
2659
+ result = void 0;
2660
+ break;
2661
+ case "realpath":
2662
+ result = await opfs.realpath(args[0]);
2663
+ break;
2664
+ // Other operations
2665
+ case "rename":
2666
+ await opfs.rename(args[0], args[1]);
2667
+ result = void 0;
2668
+ break;
2669
+ case "mkdtemp":
2670
+ result = await opfs.mkdtemp(args[0]);
2671
+ break;
2672
+ case "chmod":
2673
+ await opfs.chmod(args[0], args[1]);
2674
+ result = void 0;
2675
+ break;
2676
+ case "chown":
2677
+ await opfs.chown(args[0], args[1], args[2]);
2678
+ result = void 0;
2679
+ break;
2680
+ case "utimes":
2681
+ await opfs.utimes(args[0], args[1], args[2]);
2682
+ result = void 0;
2683
+ break;
2684
+ case "lutimes":
2685
+ await opfs.lutimes(args[0], args[1], args[2]);
2686
+ result = void 0;
2687
+ break;
2688
+ case "resetCache":
2689
+ opfs.resetCache();
2690
+ result = void 0;
2691
+ break;
2692
+ case "gc":
2693
+ fs = null;
2694
+ fs = new OPFS({ useSync: true, verbose: false });
2695
+ result = void 0;
2696
+ break;
2697
+ default:
2698
+ throw new Error(`Unknown method: ${method}`);
2699
+ }
2700
+ const response = { id, result };
2701
+ if (transfer.length > 0) {
2702
+ self.postMessage(response, transfer);
2703
+ } else {
2704
+ self.postMessage(response);
2705
+ }
2706
+ } catch (err) {
2707
+ const error = err;
2708
+ const response = {
2709
+ id,
2710
+ error: {
2711
+ message: error.message,
2712
+ code: error.code
2713
+ }
2714
+ };
2715
+ self.postMessage(response);
2716
+ }
2717
+ };
2718
+ function serializeStats(stats) {
2719
+ return {
2720
+ type: stats.type,
2721
+ size: stats.size,
2722
+ mode: stats.mode,
2723
+ ctime: stats.ctime.toISOString(),
2724
+ ctimeMs: stats.ctimeMs,
2725
+ mtime: stats.mtime.toISOString(),
2726
+ mtimeMs: stats.mtimeMs,
2727
+ target: stats.target
2728
+ };
2729
+ }
2730
+ self.postMessage({ type: "ready" });
2731
+ //# sourceMappingURL=opfs-worker.js.map
2732
+ //# sourceMappingURL=opfs-worker.js.map