@componentor/fs 2.0.12 → 3.0.1

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,2031 @@
1
+ // src/vfs/layout.ts
2
+ var VFS_MAGIC = 1447449377;
3
+ var VFS_VERSION = 1;
4
+ var DEFAULT_BLOCK_SIZE = 4096;
5
+ var DEFAULT_INODE_COUNT = 1e4;
6
+ var INODE_SIZE = 64;
7
+ var SUPERBLOCK = {
8
+ SIZE: 64,
9
+ MAGIC: 0,
10
+ // uint32 - 0x56465321
11
+ VERSION: 4,
12
+ // uint32
13
+ INODE_COUNT: 8,
14
+ // uint32 - total inodes allocated
15
+ BLOCK_SIZE: 12,
16
+ // uint32 - data block size (default 4096)
17
+ TOTAL_BLOCKS: 16,
18
+ // uint32 - total data blocks
19
+ FREE_BLOCKS: 20,
20
+ // uint32 - available data blocks
21
+ INODE_OFFSET: 24,
22
+ // float64 - byte offset to inode table
23
+ PATH_OFFSET: 32,
24
+ // float64 - byte offset to path table
25
+ DATA_OFFSET: 40,
26
+ // float64 - byte offset to data region
27
+ BITMAP_OFFSET: 48,
28
+ // float64 - byte offset to free block bitmap
29
+ PATH_USED: 56,
30
+ // uint32 - bytes used in path table
31
+ RESERVED: 60
32
+ // uint32
33
+ };
34
+ var INODE = {
35
+ TYPE: 0,
36
+ // uint8 - 0=free, 1=file, 2=directory, 3=symlink
37
+ FLAGS: 1,
38
+ // uint8[3] - reserved
39
+ PATH_OFFSET: 4,
40
+ // uint32 - byte offset into path table
41
+ PATH_LENGTH: 8,
42
+ // uint16 - length of path string
43
+ RESERVED_10: 10,
44
+ // uint16
45
+ MODE: 12,
46
+ // uint32 - permissions (e.g. 0o100644)
47
+ SIZE: 16,
48
+ // float64 - file content size in bytes (using f64 for >4GB)
49
+ FIRST_BLOCK: 24,
50
+ // uint32 - index of first data block
51
+ BLOCK_COUNT: 28,
52
+ // uint32 - number of contiguous data blocks
53
+ MTIME: 32,
54
+ // float64 - last modification time (ms since epoch)
55
+ CTIME: 40,
56
+ // float64 - creation/change time (ms since epoch)
57
+ ATIME: 48,
58
+ // float64 - last access time (ms since epoch)
59
+ UID: 56,
60
+ // uint32 - owner
61
+ GID: 60
62
+ // uint32 - group
63
+ };
64
+ var INODE_TYPE = {
65
+ FREE: 0,
66
+ FILE: 1,
67
+ DIRECTORY: 2,
68
+ SYMLINK: 3
69
+ };
70
+ var DEFAULT_FILE_MODE = 33188;
71
+ var DEFAULT_DIR_MODE = 16877;
72
+ var DEFAULT_SYMLINK_MODE = 41471;
73
+ var DEFAULT_UMASK = 18;
74
+ var S_IFMT = 61440;
75
+ var MAX_SYMLINK_DEPTH = 40;
76
+ var INITIAL_PATH_TABLE_SIZE = 256 * 1024;
77
+ var INITIAL_DATA_BLOCKS = 1024;
78
+ function calculateLayout(inodeCount = DEFAULT_INODE_COUNT, blockSize = DEFAULT_BLOCK_SIZE, totalBlocks = INITIAL_DATA_BLOCKS) {
79
+ const inodeTableOffset = SUPERBLOCK.SIZE;
80
+ const inodeTableSize = inodeCount * INODE_SIZE;
81
+ const pathTableOffset = inodeTableOffset + inodeTableSize;
82
+ const pathTableSize = INITIAL_PATH_TABLE_SIZE;
83
+ const bitmapOffset = pathTableOffset + pathTableSize;
84
+ const bitmapSize = Math.ceil(totalBlocks / 8);
85
+ const dataOffset = Math.ceil((bitmapOffset + bitmapSize) / blockSize) * blockSize;
86
+ const totalSize = dataOffset + totalBlocks * blockSize;
87
+ return {
88
+ inodeTableOffset,
89
+ inodeTableSize,
90
+ pathTableOffset,
91
+ pathTableSize,
92
+ bitmapOffset,
93
+ bitmapSize,
94
+ dataOffset,
95
+ totalSize,
96
+ totalBlocks
97
+ };
98
+ }
99
+
100
+ // src/errors.ts
101
+ var CODE_TO_STATUS = {
102
+ OK: 0,
103
+ ENOENT: 1,
104
+ EEXIST: 2,
105
+ EISDIR: 3,
106
+ ENOTDIR: 4,
107
+ ENOTEMPTY: 5,
108
+ EACCES: 6,
109
+ EINVAL: 7,
110
+ EBADF: 8,
111
+ ELOOP: 9,
112
+ ENOSPC: 10
113
+ };
114
+
115
+ // src/vfs/engine.ts
116
+ var encoder = new TextEncoder();
117
+ var decoder = new TextDecoder();
118
+ var VFSEngine = class {
119
+ handle;
120
+ pathIndex = /* @__PURE__ */ new Map();
121
+ // path → inode index
122
+ inodeCount = 0;
123
+ blockSize = DEFAULT_BLOCK_SIZE;
124
+ totalBlocks = 0;
125
+ freeBlocks = 0;
126
+ inodeTableOffset = 0;
127
+ pathTableOffset = 0;
128
+ pathTableUsed = 0;
129
+ pathTableSize = 0;
130
+ bitmapOffset = 0;
131
+ dataOffset = 0;
132
+ umask = DEFAULT_UMASK;
133
+ processUid = 0;
134
+ processGid = 0;
135
+ strictPermissions = false;
136
+ debug = false;
137
+ // File descriptor table
138
+ fdTable = /* @__PURE__ */ new Map();
139
+ nextFd = 3;
140
+ // 0=stdin, 1=stdout, 2=stderr reserved
141
+ // Reusable buffers to avoid allocations
142
+ inodeBuf = new Uint8Array(INODE_SIZE);
143
+ inodeView = new DataView(this.inodeBuf.buffer);
144
+ // In-memory inode cache — eliminates disk reads for hot inodes
145
+ inodeCache = /* @__PURE__ */ new Map();
146
+ superblockBuf = new Uint8Array(SUPERBLOCK.SIZE);
147
+ superblockView = new DataView(this.superblockBuf.buffer);
148
+ // In-memory bitmap cache — eliminates bitmap reads from OPFS
149
+ bitmap = null;
150
+ bitmapDirtyLo = Infinity;
151
+ // lowest dirty byte index
152
+ bitmapDirtyHi = -1;
153
+ // highest dirty byte index (inclusive)
154
+ superblockDirty = false;
155
+ // Free inode hint — skip O(n) scan
156
+ freeInodeHint = 0;
157
+ init(handle, opts) {
158
+ this.handle = handle;
159
+ this.processUid = opts?.uid ?? 0;
160
+ this.processGid = opts?.gid ?? 0;
161
+ this.umask = opts?.umask ?? DEFAULT_UMASK;
162
+ this.strictPermissions = opts?.strictPermissions ?? false;
163
+ this.debug = opts?.debug ?? false;
164
+ const size = handle.getSize();
165
+ if (size === 0) {
166
+ this.format();
167
+ } else {
168
+ this.mount();
169
+ }
170
+ }
171
+ /** Format a fresh VFS */
172
+ format() {
173
+ const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
174
+ this.inodeCount = DEFAULT_INODE_COUNT;
175
+ this.blockSize = DEFAULT_BLOCK_SIZE;
176
+ this.totalBlocks = layout.totalBlocks;
177
+ this.freeBlocks = layout.totalBlocks;
178
+ this.inodeTableOffset = layout.inodeTableOffset;
179
+ this.pathTableOffset = layout.pathTableOffset;
180
+ this.pathTableSize = layout.pathTableSize;
181
+ this.pathTableUsed = 0;
182
+ this.bitmapOffset = layout.bitmapOffset;
183
+ this.dataOffset = layout.dataOffset;
184
+ this.handle.truncate(layout.totalSize);
185
+ this.writeSuperblock();
186
+ const zeroBuf = new Uint8Array(layout.inodeTableSize);
187
+ this.handle.write(zeroBuf, { at: this.inodeTableOffset });
188
+ this.bitmap = new Uint8Array(layout.bitmapSize);
189
+ this.handle.write(this.bitmap, { at: this.bitmapOffset });
190
+ this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
191
+ this.handle.flush();
192
+ }
193
+ /** Mount an existing VFS from disk */
194
+ mount() {
195
+ this.handle.read(this.superblockBuf, { at: 0 });
196
+ const v = this.superblockView;
197
+ const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
198
+ if (magic !== VFS_MAGIC) {
199
+ throw new Error(`Invalid VFS: bad magic 0x${magic.toString(16)}`);
200
+ }
201
+ this.inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
202
+ this.blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
203
+ this.totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
204
+ this.freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
205
+ this.inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
206
+ this.pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
207
+ this.dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
208
+ this.bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
209
+ this.pathTableUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
210
+ this.pathTableSize = this.bitmapOffset - this.pathTableOffset;
211
+ const bitmapSize = Math.ceil(this.totalBlocks / 8);
212
+ this.bitmap = new Uint8Array(bitmapSize);
213
+ this.handle.read(this.bitmap, { at: this.bitmapOffset });
214
+ this.rebuildIndex();
215
+ }
216
+ writeSuperblock() {
217
+ const v = this.superblockView;
218
+ v.setUint32(SUPERBLOCK.MAGIC, VFS_MAGIC, true);
219
+ v.setUint32(SUPERBLOCK.VERSION, VFS_VERSION, true);
220
+ v.setUint32(SUPERBLOCK.INODE_COUNT, this.inodeCount, true);
221
+ v.setUint32(SUPERBLOCK.BLOCK_SIZE, this.blockSize, true);
222
+ v.setUint32(SUPERBLOCK.TOTAL_BLOCKS, this.totalBlocks, true);
223
+ v.setUint32(SUPERBLOCK.FREE_BLOCKS, this.freeBlocks, true);
224
+ v.setFloat64(SUPERBLOCK.INODE_OFFSET, this.inodeTableOffset, true);
225
+ v.setFloat64(SUPERBLOCK.PATH_OFFSET, this.pathTableOffset, true);
226
+ v.setFloat64(SUPERBLOCK.DATA_OFFSET, this.dataOffset, true);
227
+ v.setFloat64(SUPERBLOCK.BITMAP_OFFSET, this.bitmapOffset, true);
228
+ v.setUint32(SUPERBLOCK.PATH_USED, this.pathTableUsed, true);
229
+ this.handle.write(this.superblockBuf, { at: 0 });
230
+ }
231
+ /** Flush pending bitmap and superblock writes to disk (one write each) */
232
+ markBitmapDirty(lo, hi) {
233
+ if (lo < this.bitmapDirtyLo) this.bitmapDirtyLo = lo;
234
+ if (hi > this.bitmapDirtyHi) this.bitmapDirtyHi = hi;
235
+ }
236
+ commitPending() {
237
+ if (this.bitmapDirtyHi >= 0) {
238
+ const lo = this.bitmapDirtyLo;
239
+ const hi = this.bitmapDirtyHi;
240
+ this.handle.write(this.bitmap.subarray(lo, hi + 1), { at: this.bitmapOffset + lo });
241
+ this.bitmapDirtyLo = Infinity;
242
+ this.bitmapDirtyHi = -1;
243
+ }
244
+ if (this.superblockDirty) {
245
+ this.writeSuperblock();
246
+ this.superblockDirty = false;
247
+ }
248
+ }
249
+ /** Rebuild in-memory path→inode index from disk */
250
+ rebuildIndex() {
251
+ this.pathIndex.clear();
252
+ for (let i = 0; i < this.inodeCount; i++) {
253
+ const inode = this.readInode(i);
254
+ if (inode.type === INODE_TYPE.FREE) continue;
255
+ const path = this.readPath(inode.pathOffset, inode.pathLength);
256
+ this.pathIndex.set(path, i);
257
+ }
258
+ }
259
+ // ========== Low-level inode I/O ==========
260
+ readInode(idx) {
261
+ const cached = this.inodeCache.get(idx);
262
+ if (cached) return cached;
263
+ const offset = this.inodeTableOffset + idx * INODE_SIZE;
264
+ this.handle.read(this.inodeBuf, { at: offset });
265
+ const v = this.inodeView;
266
+ const inode = {
267
+ type: v.getUint8(INODE.TYPE),
268
+ pathOffset: v.getUint32(INODE.PATH_OFFSET, true),
269
+ pathLength: v.getUint16(INODE.PATH_LENGTH, true),
270
+ mode: v.getUint32(INODE.MODE, true),
271
+ size: v.getFloat64(INODE.SIZE, true),
272
+ firstBlock: v.getUint32(INODE.FIRST_BLOCK, true),
273
+ blockCount: v.getUint32(INODE.BLOCK_COUNT, true),
274
+ mtime: v.getFloat64(INODE.MTIME, true),
275
+ ctime: v.getFloat64(INODE.CTIME, true),
276
+ atime: v.getFloat64(INODE.ATIME, true),
277
+ uid: v.getUint32(INODE.UID, true),
278
+ gid: v.getUint32(INODE.GID, true)
279
+ };
280
+ this.inodeCache.set(idx, inode);
281
+ return inode;
282
+ }
283
+ writeInode(idx, inode) {
284
+ if (inode.type === INODE_TYPE.FREE) {
285
+ this.inodeCache.delete(idx);
286
+ } else {
287
+ this.inodeCache.set(idx, inode);
288
+ }
289
+ const v = this.inodeView;
290
+ v.setUint8(INODE.TYPE, inode.type);
291
+ v.setUint8(INODE.FLAGS, 0);
292
+ v.setUint8(INODE.FLAGS + 1, 0);
293
+ v.setUint8(INODE.FLAGS + 2, 0);
294
+ v.setUint32(INODE.PATH_OFFSET, inode.pathOffset, true);
295
+ v.setUint16(INODE.PATH_LENGTH, inode.pathLength, true);
296
+ v.setUint16(INODE.RESERVED_10, 0, true);
297
+ v.setUint32(INODE.MODE, inode.mode, true);
298
+ v.setFloat64(INODE.SIZE, inode.size, true);
299
+ v.setUint32(INODE.FIRST_BLOCK, inode.firstBlock, true);
300
+ v.setUint32(INODE.BLOCK_COUNT, inode.blockCount, true);
301
+ v.setFloat64(INODE.MTIME, inode.mtime, true);
302
+ v.setFloat64(INODE.CTIME, inode.ctime, true);
303
+ v.setFloat64(INODE.ATIME, inode.atime, true);
304
+ v.setUint32(INODE.UID, inode.uid, true);
305
+ v.setUint32(INODE.GID, inode.gid, true);
306
+ const offset = this.inodeTableOffset + idx * INODE_SIZE;
307
+ this.handle.write(this.inodeBuf, { at: offset });
308
+ }
309
+ // ========== Path table I/O ==========
310
+ readPath(offset, length) {
311
+ const buf = new Uint8Array(length);
312
+ this.handle.read(buf, { at: this.pathTableOffset + offset });
313
+ return decoder.decode(buf);
314
+ }
315
+ appendPath(path) {
316
+ const bytes = encoder.encode(path);
317
+ const offset = this.pathTableUsed;
318
+ if (offset + bytes.byteLength > this.pathTableSize) {
319
+ this.growPathTable(offset + bytes.byteLength);
320
+ }
321
+ this.handle.write(bytes, { at: this.pathTableOffset + offset });
322
+ this.pathTableUsed += bytes.byteLength;
323
+ this.superblockDirty = true;
324
+ return { offset, length: bytes.byteLength };
325
+ }
326
+ growPathTable(needed) {
327
+ const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
328
+ const growth = newSize - this.pathTableSize;
329
+ const dataSize = this.totalBlocks * this.blockSize;
330
+ const dataBuf = new Uint8Array(dataSize);
331
+ this.handle.read(dataBuf, { at: this.dataOffset });
332
+ const newTotalSize = this.handle.getSize() + growth;
333
+ this.handle.truncate(newTotalSize);
334
+ const newBitmapOffset = this.bitmapOffset + growth;
335
+ const newDataOffset = this.dataOffset + growth;
336
+ this.handle.write(dataBuf, { at: newDataOffset });
337
+ this.handle.write(this.bitmap, { at: newBitmapOffset });
338
+ this.pathTableSize = newSize;
339
+ this.bitmapOffset = newBitmapOffset;
340
+ this.dataOffset = newDataOffset;
341
+ this.superblockDirty = true;
342
+ }
343
+ // ========== Bitmap I/O ==========
344
+ allocateBlocks(count) {
345
+ if (count === 0) return 0;
346
+ const bitmap = this.bitmap;
347
+ let run = 0;
348
+ let start = 0;
349
+ for (let i = 0; i < this.totalBlocks; i++) {
350
+ const byteIdx = i >>> 3;
351
+ const bitIdx = i & 7;
352
+ const used = bitmap[byteIdx] >>> bitIdx & 1;
353
+ if (used) {
354
+ run = 0;
355
+ start = i + 1;
356
+ } else {
357
+ run++;
358
+ if (run === count) {
359
+ for (let j = start; j <= i; j++) {
360
+ const bj = j >>> 3;
361
+ const bi = j & 7;
362
+ bitmap[bj] |= 1 << bi;
363
+ }
364
+ this.markBitmapDirty(start >>> 3, i >>> 3);
365
+ this.freeBlocks -= count;
366
+ this.superblockDirty = true;
367
+ return start;
368
+ }
369
+ }
370
+ }
371
+ return this.growAndAllocate(count);
372
+ }
373
+ growAndAllocate(count) {
374
+ const oldTotal = this.totalBlocks;
375
+ const newTotal = Math.max(oldTotal * 2, oldTotal + count);
376
+ const addedBlocks = newTotal - oldTotal;
377
+ const newFileSize = this.dataOffset + newTotal * this.blockSize;
378
+ this.handle.truncate(newFileSize);
379
+ const newBitmapSize = Math.ceil(newTotal / 8);
380
+ const newBitmap = new Uint8Array(newBitmapSize);
381
+ newBitmap.set(this.bitmap);
382
+ this.bitmap = newBitmap;
383
+ this.totalBlocks = newTotal;
384
+ this.freeBlocks += addedBlocks;
385
+ const start = oldTotal;
386
+ for (let j = start; j < start + count; j++) {
387
+ const bj = j >>> 3;
388
+ const bi = j & 7;
389
+ this.bitmap[bj] |= 1 << bi;
390
+ }
391
+ this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
392
+ this.freeBlocks -= count;
393
+ this.superblockDirty = true;
394
+ return start;
395
+ }
396
+ freeBlockRange(start, count) {
397
+ if (count === 0) return;
398
+ const bitmap = this.bitmap;
399
+ for (let i = start; i < start + count; i++) {
400
+ const byteIdx = i >>> 3;
401
+ const bitIdx = i & 7;
402
+ bitmap[byteIdx] &= ~(1 << bitIdx);
403
+ }
404
+ this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
405
+ this.freeBlocks += count;
406
+ this.superblockDirty = true;
407
+ }
408
+ // updateSuperblockFreeBlocks is no longer needed — superblock writes are coalesced via commitPending()
409
+ // ========== Inode allocation ==========
410
+ findFreeInode() {
411
+ for (let i = this.freeInodeHint; i < this.inodeCount; i++) {
412
+ if (this.inodeCache.has(i)) continue;
413
+ const offset = this.inodeTableOffset + i * INODE_SIZE;
414
+ const typeBuf = new Uint8Array(1);
415
+ this.handle.read(typeBuf, { at: offset });
416
+ if (typeBuf[0] === INODE_TYPE.FREE) {
417
+ this.freeInodeHint = i + 1;
418
+ return i;
419
+ }
420
+ }
421
+ const idx = this.growInodeTable();
422
+ this.freeInodeHint = idx + 1;
423
+ return idx;
424
+ }
425
+ growInodeTable() {
426
+ const oldCount = this.inodeCount;
427
+ const newCount = oldCount * 2;
428
+ const growth = (newCount - oldCount) * INODE_SIZE;
429
+ const afterInodeOffset = this.inodeTableOffset + oldCount * INODE_SIZE;
430
+ const afterSize = this.handle.getSize() - afterInodeOffset;
431
+ const afterBuf = new Uint8Array(afterSize);
432
+ this.handle.read(afterBuf, { at: afterInodeOffset });
433
+ this.handle.truncate(this.handle.getSize() + growth);
434
+ this.handle.write(afterBuf, { at: afterInodeOffset + growth });
435
+ const zeroes = new Uint8Array(growth);
436
+ this.handle.write(zeroes, { at: afterInodeOffset });
437
+ this.pathTableOffset += growth;
438
+ this.bitmapOffset += growth;
439
+ this.dataOffset += growth;
440
+ this.inodeCount = newCount;
441
+ this.superblockDirty = true;
442
+ return oldCount;
443
+ }
444
+ // ========== Data I/O ==========
445
+ readData(firstBlock, blockCount, size) {
446
+ const buf = new Uint8Array(size);
447
+ const offset = this.dataOffset + firstBlock * this.blockSize;
448
+ this.handle.read(buf, { at: offset });
449
+ return buf;
450
+ }
451
+ writeData(firstBlock, data) {
452
+ const offset = this.dataOffset + firstBlock * this.blockSize;
453
+ this.handle.write(data, { at: offset });
454
+ }
455
+ // ========== Path resolution ==========
456
+ resolvePath(path, depth = 0) {
457
+ if (depth > MAX_SYMLINK_DEPTH) return void 0;
458
+ const idx = this.pathIndex.get(path);
459
+ if (idx === void 0) return void 0;
460
+ const inode = this.readInode(idx);
461
+ if (inode.type === INODE_TYPE.SYMLINK) {
462
+ const target = decoder.decode(this.readData(inode.firstBlock, inode.blockCount, inode.size));
463
+ const resolved = target.startsWith("/") ? target : this.resolveRelative(path, target);
464
+ return this.resolvePath(resolved, depth + 1);
465
+ }
466
+ return idx;
467
+ }
468
+ /** Resolve symlinks in intermediate path components */
469
+ resolvePathComponents(path, followLast = true) {
470
+ const parts = path.split("/").filter(Boolean);
471
+ let current = "/";
472
+ for (let i = 0; i < parts.length; i++) {
473
+ const isLast = i === parts.length - 1;
474
+ current = current === "/" ? "/" + parts[i] : current + "/" + parts[i];
475
+ const idx = this.pathIndex.get(current);
476
+ if (idx === void 0) return void 0;
477
+ const inode = this.readInode(idx);
478
+ if (inode.type === INODE_TYPE.SYMLINK && (!isLast || followLast)) {
479
+ const target = decoder.decode(this.readData(inode.firstBlock, inode.blockCount, inode.size));
480
+ const resolved = target.startsWith("/") ? target : this.resolveRelative(current, target);
481
+ if (isLast) {
482
+ return this.resolvePath(resolved);
483
+ }
484
+ const remaining = parts.slice(i + 1).join("/");
485
+ const newPath = resolved + (remaining ? "/" + remaining : "");
486
+ return this.resolvePathComponents(newPath, followLast);
487
+ }
488
+ }
489
+ return this.pathIndex.get(current);
490
+ }
491
+ resolveRelative(from, target) {
492
+ const dir = from.substring(0, from.lastIndexOf("/")) || "/";
493
+ const parts = (dir + "/" + target).split("/").filter(Boolean);
494
+ const resolved = [];
495
+ for (const p of parts) {
496
+ if (p === ".") continue;
497
+ if (p === "..") {
498
+ resolved.pop();
499
+ continue;
500
+ }
501
+ resolved.push(p);
502
+ }
503
+ return "/" + resolved.join("/");
504
+ }
505
+ // ========== Core inode creation helper ==========
506
+ createInode(path, type, mode, size, data) {
507
+ const idx = this.findFreeInode();
508
+ const { offset: pathOff, length: pathLen } = this.appendPath(path);
509
+ const now = Date.now();
510
+ let firstBlock = 0;
511
+ let blockCount = 0;
512
+ if (data && data.byteLength > 0) {
513
+ blockCount = Math.ceil(data.byteLength / this.blockSize);
514
+ firstBlock = this.allocateBlocks(blockCount);
515
+ this.writeData(firstBlock, data);
516
+ }
517
+ const inode = {
518
+ type,
519
+ pathOffset: pathOff,
520
+ pathLength: pathLen,
521
+ mode,
522
+ size,
523
+ firstBlock,
524
+ blockCount,
525
+ mtime: now,
526
+ ctime: now,
527
+ atime: now,
528
+ uid: this.processUid,
529
+ gid: this.processGid
530
+ };
531
+ this.writeInode(idx, inode);
532
+ this.pathIndex.set(path, idx);
533
+ return idx;
534
+ }
535
+ // ========== Public API — called by server worker dispatch ==========
536
+ /** Normalize a path: ensure leading /, resolve . and .. */
537
+ normalizePath(p) {
538
+ if (p.charCodeAt(0) !== 47) p = "/" + p;
539
+ if (p.length === 1) return p;
540
+ if (p.indexOf("/.") === -1 && p.indexOf("//") === -1 && p.charCodeAt(p.length - 1) !== 47) {
541
+ return p;
542
+ }
543
+ const parts = p.split("/").filter(Boolean);
544
+ const resolved = [];
545
+ for (const part of parts) {
546
+ if (part === ".") continue;
547
+ if (part === "..") {
548
+ resolved.pop();
549
+ continue;
550
+ }
551
+ resolved.push(part);
552
+ }
553
+ return "/" + resolved.join("/");
554
+ }
555
+ // ---- READ ----
556
+ read(path) {
557
+ const t0 = this.debug ? performance.now() : 0;
558
+ path = this.normalizePath(path);
559
+ let idx = this.pathIndex.get(path);
560
+ if (idx !== void 0) {
561
+ const inode2 = this.inodeCache.get(idx);
562
+ if (inode2) {
563
+ if (inode2.type === INODE_TYPE.SYMLINK) {
564
+ idx = this.resolvePathComponents(path, true);
565
+ } else if (inode2.type === INODE_TYPE.DIRECTORY) {
566
+ return { status: CODE_TO_STATUS.EISDIR, data: null };
567
+ } else {
568
+ const data2 = inode2.size > 0 ? this.readData(inode2.firstBlock, inode2.blockCount, inode2.size) : new Uint8Array(0);
569
+ if (this.debug) {
570
+ const t1 = performance.now();
571
+ console.log(`[VFS read] path=${path} size=${inode2.size} TOTAL=${(t1 - t0).toFixed(3)}ms (fast)`);
572
+ }
573
+ return { status: 0, data: data2 };
574
+ }
575
+ }
576
+ }
577
+ if (idx === void 0) idx = this.resolvePathComponents(path, true);
578
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
579
+ const inode = this.readInode(idx);
580
+ if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR, data: null };
581
+ const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
582
+ if (this.debug) {
583
+ const t1 = performance.now();
584
+ console.log(`[VFS read] path=${path} size=${inode.size} TOTAL=${(t1 - t0).toFixed(3)}ms (slow path)`);
585
+ }
586
+ return { status: 0, data };
587
+ }
588
+ // ---- WRITE ----
589
+ write(path, data, flags = 0) {
590
+ const t0 = this.debug ? performance.now() : 0;
591
+ path = this.normalizePath(path);
592
+ const t1 = this.debug ? performance.now() : 0;
593
+ const parentStatus = this.ensureParent(path);
594
+ if (parentStatus !== 0) return { status: parentStatus };
595
+ const t2 = this.debug ? performance.now() : 0;
596
+ const existingIdx = this.resolvePathComponents(path, true);
597
+ const t3 = this.debug ? performance.now() : 0;
598
+ let tAlloc = t3, tData = t3, tInode = t3;
599
+ if (existingIdx !== void 0) {
600
+ const inode = this.readInode(existingIdx);
601
+ if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
602
+ const neededBlocks = Math.ceil(data.byteLength / this.blockSize);
603
+ if (neededBlocks <= inode.blockCount) {
604
+ tAlloc = this.debug ? performance.now() : 0;
605
+ this.writeData(inode.firstBlock, data);
606
+ tData = this.debug ? performance.now() : 0;
607
+ if (neededBlocks < inode.blockCount) {
608
+ this.freeBlockRange(inode.firstBlock + neededBlocks, inode.blockCount - neededBlocks);
609
+ }
610
+ } else {
611
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
612
+ const newFirst = this.allocateBlocks(neededBlocks);
613
+ tAlloc = this.debug ? performance.now() : 0;
614
+ this.writeData(newFirst, data);
615
+ tData = this.debug ? performance.now() : 0;
616
+ inode.firstBlock = newFirst;
617
+ }
618
+ inode.size = data.byteLength;
619
+ inode.blockCount = neededBlocks;
620
+ inode.mtime = Date.now();
621
+ this.writeInode(existingIdx, inode);
622
+ tInode = this.debug ? performance.now() : 0;
623
+ } else {
624
+ const mode = DEFAULT_FILE_MODE & ~(this.umask & 511);
625
+ this.createInode(path, INODE_TYPE.FILE, mode, data.byteLength, data);
626
+ tAlloc = this.debug ? performance.now() : 0;
627
+ tData = tAlloc;
628
+ tInode = tAlloc;
629
+ }
630
+ if (flags & 1) {
631
+ this.commitPending();
632
+ this.handle.flush();
633
+ }
634
+ const tFlush = this.debug ? performance.now() : 0;
635
+ if (this.debug) {
636
+ const existing = existingIdx !== void 0;
637
+ console.log(`[VFS write] path=${path} size=${data.byteLength} ${existing ? "UPDATE" : "CREATE"} normalize=${(t1 - t0).toFixed(3)}ms parent=${(t2 - t1).toFixed(3)}ms resolve=${(t3 - t2).toFixed(3)}ms alloc=${(tAlloc - t3).toFixed(3)}ms data=${(tData - tAlloc).toFixed(3)}ms inode=${(tInode - tData).toFixed(3)}ms flush=${(tFlush - tInode).toFixed(3)}ms TOTAL=${(tFlush - t0).toFixed(3)}ms`);
638
+ }
639
+ return { status: 0 };
640
+ }
641
+ // ---- APPEND ----
642
+ append(path, data) {
643
+ path = this.normalizePath(path);
644
+ const existingIdx = this.resolvePathComponents(path, true);
645
+ if (existingIdx === void 0) {
646
+ return this.write(path, data);
647
+ }
648
+ const inode = this.readInode(existingIdx);
649
+ if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
650
+ const existing = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
651
+ const combined = new Uint8Array(existing.byteLength + data.byteLength);
652
+ combined.set(existing);
653
+ combined.set(data, existing.byteLength);
654
+ const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
655
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
656
+ const newFirst = this.allocateBlocks(neededBlocks);
657
+ this.writeData(newFirst, combined);
658
+ inode.firstBlock = newFirst;
659
+ inode.blockCount = neededBlocks;
660
+ inode.size = combined.byteLength;
661
+ inode.mtime = Date.now();
662
+ this.writeInode(existingIdx, inode);
663
+ this.commitPending();
664
+ return { status: 0 };
665
+ }
666
+ // ---- UNLINK ----
667
+ unlink(path) {
668
+ path = this.normalizePath(path);
669
+ const idx = this.pathIndex.get(path);
670
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
671
+ const inode = this.readInode(idx);
672
+ if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
673
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
674
+ inode.type = INODE_TYPE.FREE;
675
+ this.writeInode(idx, inode);
676
+ this.pathIndex.delete(path);
677
+ if (idx < this.freeInodeHint) this.freeInodeHint = idx;
678
+ this.commitPending();
679
+ return { status: 0 };
680
+ }
681
+ // ---- STAT ----
682
+ stat(path) {
683
+ path = this.normalizePath(path);
684
+ const idx = this.resolvePathComponents(path, true);
685
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
686
+ return this.encodeStatResponse(idx);
687
+ }
688
+ // ---- LSTAT (no symlink follow) ----
689
+ lstat(path) {
690
+ path = this.normalizePath(path);
691
+ const idx = this.pathIndex.get(path);
692
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
693
+ return this.encodeStatResponse(idx);
694
+ }
695
+ encodeStatResponse(idx) {
696
+ const inode = this.readInode(idx);
697
+ const buf = new Uint8Array(49);
698
+ const view = new DataView(buf.buffer);
699
+ view.setUint8(0, inode.type);
700
+ view.setUint32(1, inode.mode, true);
701
+ view.setFloat64(5, inode.size, true);
702
+ view.setFloat64(13, inode.mtime, true);
703
+ view.setFloat64(21, inode.ctime, true);
704
+ view.setFloat64(29, inode.atime, true);
705
+ view.setUint32(37, inode.uid, true);
706
+ view.setUint32(41, inode.gid, true);
707
+ view.setUint32(45, idx, true);
708
+ return { status: 0, data: buf };
709
+ }
710
+ // ---- MKDIR ----
711
+ mkdir(path, flags = 0) {
712
+ path = this.normalizePath(path);
713
+ const recursive = (flags & 1) !== 0;
714
+ if (recursive) {
715
+ return this.mkdirRecursive(path);
716
+ }
717
+ if (this.pathIndex.has(path)) return { status: CODE_TO_STATUS.EEXIST, data: null };
718
+ const parentStatus = this.ensureParent(path);
719
+ if (parentStatus !== 0) return { status: parentStatus, data: null };
720
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
721
+ this.createInode(path, INODE_TYPE.DIRECTORY, mode, 0);
722
+ this.commitPending();
723
+ const pathBytes = encoder.encode(path);
724
+ return { status: 0, data: pathBytes };
725
+ }
726
+ mkdirRecursive(path) {
727
+ const parts = path.split("/").filter(Boolean);
728
+ let current = "";
729
+ let firstCreated = null;
730
+ for (const part of parts) {
731
+ current += "/" + part;
732
+ if (this.pathIndex.has(current)) {
733
+ const idx = this.pathIndex.get(current);
734
+ const inode = this.readInode(idx);
735
+ if (inode.type !== INODE_TYPE.DIRECTORY) {
736
+ return { status: CODE_TO_STATUS.ENOTDIR, data: null };
737
+ }
738
+ continue;
739
+ }
740
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
741
+ this.createInode(current, INODE_TYPE.DIRECTORY, mode, 0);
742
+ if (!firstCreated) firstCreated = current;
743
+ }
744
+ this.commitPending();
745
+ const result = firstCreated ? encoder.encode(firstCreated) : void 0;
746
+ return { status: 0, data: result ?? null };
747
+ }
748
+ // ---- RMDIR ----
749
+ rmdir(path, flags = 0) {
750
+ path = this.normalizePath(path);
751
+ const recursive = (flags & 1) !== 0;
752
+ const idx = this.pathIndex.get(path);
753
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
754
+ const inode = this.readInode(idx);
755
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
756
+ const children = this.getDirectChildren(path);
757
+ if (children.length > 0) {
758
+ if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
759
+ for (const child of this.getAllDescendants(path)) {
760
+ const childIdx = this.pathIndex.get(child);
761
+ const childInode = this.readInode(childIdx);
762
+ this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
763
+ childInode.type = INODE_TYPE.FREE;
764
+ this.writeInode(childIdx, childInode);
765
+ this.pathIndex.delete(child);
766
+ }
767
+ }
768
+ inode.type = INODE_TYPE.FREE;
769
+ this.writeInode(idx, inode);
770
+ this.pathIndex.delete(path);
771
+ if (idx < this.freeInodeHint) this.freeInodeHint = idx;
772
+ this.commitPending();
773
+ return { status: 0 };
774
+ }
775
+ // ---- READDIR ----
776
+ readdir(path, flags = 0) {
777
+ path = this.normalizePath(path);
778
+ const idx = this.resolvePathComponents(path, true);
779
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
780
+ const inode = this.readInode(idx);
781
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
782
+ const withFileTypes = (flags & 1) !== 0;
783
+ const children = this.getDirectChildren(path);
784
+ if (withFileTypes) {
785
+ let totalSize2 = 4;
786
+ const entries = [];
787
+ for (const childPath of children) {
788
+ const name = childPath.substring(childPath.lastIndexOf("/") + 1);
789
+ const nameBytes = encoder.encode(name);
790
+ const childIdx = this.pathIndex.get(childPath);
791
+ const childInode = this.readInode(childIdx);
792
+ entries.push({ name: nameBytes, type: childInode.type });
793
+ totalSize2 += 2 + nameBytes.byteLength + 1;
794
+ }
795
+ const buf2 = new Uint8Array(totalSize2);
796
+ const view2 = new DataView(buf2.buffer);
797
+ view2.setUint32(0, entries.length, true);
798
+ let offset2 = 4;
799
+ for (const entry of entries) {
800
+ view2.setUint16(offset2, entry.name.byteLength, true);
801
+ offset2 += 2;
802
+ buf2.set(entry.name, offset2);
803
+ offset2 += entry.name.byteLength;
804
+ buf2[offset2++] = entry.type;
805
+ }
806
+ return { status: 0, data: buf2 };
807
+ }
808
+ let totalSize = 4;
809
+ const nameEntries = [];
810
+ for (const childPath of children) {
811
+ const name = childPath.substring(childPath.lastIndexOf("/") + 1);
812
+ const nameBytes = encoder.encode(name);
813
+ nameEntries.push(nameBytes);
814
+ totalSize += 2 + nameBytes.byteLength;
815
+ }
816
+ const buf = new Uint8Array(totalSize);
817
+ const view = new DataView(buf.buffer);
818
+ view.setUint32(0, nameEntries.length, true);
819
+ let offset = 4;
820
+ for (const nameBytes of nameEntries) {
821
+ view.setUint16(offset, nameBytes.byteLength, true);
822
+ offset += 2;
823
+ buf.set(nameBytes, offset);
824
+ offset += nameBytes.byteLength;
825
+ }
826
+ return { status: 0, data: buf };
827
+ }
828
+ // ---- RENAME ----
829
+ rename(oldPath, newPath) {
830
+ oldPath = this.normalizePath(oldPath);
831
+ newPath = this.normalizePath(newPath);
832
+ const idx = this.pathIndex.get(oldPath);
833
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
834
+ const parentStatus = this.ensureParent(newPath);
835
+ if (parentStatus !== 0) return { status: parentStatus };
836
+ const existingIdx = this.pathIndex.get(newPath);
837
+ if (existingIdx !== void 0) {
838
+ const existingInode = this.readInode(existingIdx);
839
+ this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
840
+ existingInode.type = INODE_TYPE.FREE;
841
+ this.writeInode(existingIdx, existingInode);
842
+ this.pathIndex.delete(newPath);
843
+ }
844
+ const inode = this.readInode(idx);
845
+ const { offset: pathOff, length: pathLen } = this.appendPath(newPath);
846
+ inode.pathOffset = pathOff;
847
+ inode.pathLength = pathLen;
848
+ inode.mtime = Date.now();
849
+ this.writeInode(idx, inode);
850
+ this.pathIndex.delete(oldPath);
851
+ this.pathIndex.set(newPath, idx);
852
+ if (inode.type === INODE_TYPE.DIRECTORY) {
853
+ const prefix = oldPath === "/" ? "/" : oldPath + "/";
854
+ const toRename = [];
855
+ for (const [p, i] of this.pathIndex) {
856
+ if (p.startsWith(prefix)) {
857
+ toRename.push([p, i]);
858
+ }
859
+ }
860
+ for (const [p, i] of toRename) {
861
+ const suffix = p.substring(oldPath.length);
862
+ const childNewPath = newPath + suffix;
863
+ const childInode = this.readInode(i);
864
+ const { offset: cpo, length: cpl } = this.appendPath(childNewPath);
865
+ childInode.pathOffset = cpo;
866
+ childInode.pathLength = cpl;
867
+ this.writeInode(i, childInode);
868
+ this.pathIndex.delete(p);
869
+ this.pathIndex.set(childNewPath, i);
870
+ }
871
+ }
872
+ this.commitPending();
873
+ return { status: 0 };
874
+ }
875
+ // ---- EXISTS ----
876
+ exists(path) {
877
+ path = this.normalizePath(path);
878
+ const idx = this.resolvePathComponents(path, true);
879
+ const buf = new Uint8Array(1);
880
+ buf[0] = idx !== void 0 ? 1 : 0;
881
+ return { status: 0, data: buf };
882
+ }
883
+ // ---- TRUNCATE ----
884
+ truncate(path, len = 0) {
885
+ path = this.normalizePath(path);
886
+ const idx = this.resolvePathComponents(path, true);
887
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
888
+ const inode = this.readInode(idx);
889
+ if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
890
+ if (len === 0) {
891
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
892
+ inode.firstBlock = 0;
893
+ inode.blockCount = 0;
894
+ inode.size = 0;
895
+ } else if (len < inode.size) {
896
+ const neededBlocks = Math.ceil(len / this.blockSize);
897
+ if (neededBlocks < inode.blockCount) {
898
+ this.freeBlockRange(inode.firstBlock + neededBlocks, inode.blockCount - neededBlocks);
899
+ }
900
+ inode.blockCount = neededBlocks;
901
+ inode.size = len;
902
+ } else if (len > inode.size) {
903
+ const neededBlocks = Math.ceil(len / this.blockSize);
904
+ if (neededBlocks > inode.blockCount) {
905
+ const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
906
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
907
+ const newFirst = this.allocateBlocks(neededBlocks);
908
+ const newData = new Uint8Array(len);
909
+ newData.set(oldData);
910
+ this.writeData(newFirst, newData);
911
+ inode.firstBlock = newFirst;
912
+ }
913
+ inode.blockCount = neededBlocks;
914
+ inode.size = len;
915
+ }
916
+ inode.mtime = Date.now();
917
+ this.writeInode(idx, inode);
918
+ this.commitPending();
919
+ return { status: 0 };
920
+ }
921
+ // ---- COPY ----
922
+ copy(srcPath, destPath, flags = 0) {
923
+ srcPath = this.normalizePath(srcPath);
924
+ destPath = this.normalizePath(destPath);
925
+ const srcIdx = this.resolvePathComponents(srcPath, true);
926
+ if (srcIdx === void 0) return { status: CODE_TO_STATUS.ENOENT };
927
+ const srcInode = this.readInode(srcIdx);
928
+ if (srcInode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
929
+ if (flags & 1 && this.pathIndex.has(destPath)) {
930
+ return { status: CODE_TO_STATUS.EEXIST };
931
+ }
932
+ const data = srcInode.size > 0 ? this.readData(srcInode.firstBlock, srcInode.blockCount, srcInode.size) : new Uint8Array(0);
933
+ return this.write(destPath, data);
934
+ }
935
+ // ---- ACCESS ----
936
+ access(path, mode = 0) {
937
+ path = this.normalizePath(path);
938
+ const idx = this.resolvePathComponents(path, true);
939
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
940
+ if (mode === 0) return { status: 0 };
941
+ if (!this.strictPermissions) return { status: 0 };
942
+ const inode = this.readInode(idx);
943
+ const filePerm = this.getEffectivePermission(inode);
944
+ if (mode & 4 && !(filePerm & 4)) return { status: CODE_TO_STATUS.EACCES };
945
+ if (mode & 2 && !(filePerm & 2)) return { status: CODE_TO_STATUS.EACCES };
946
+ if (mode & 1 && !(filePerm & 1)) return { status: CODE_TO_STATUS.EACCES };
947
+ return { status: 0 };
948
+ }
949
+ getEffectivePermission(inode) {
950
+ const modeBits = inode.mode & 511;
951
+ if (this.processUid === inode.uid) return modeBits >>> 6 & 7;
952
+ if (this.processGid === inode.gid) return modeBits >>> 3 & 7;
953
+ return modeBits & 7;
954
+ }
955
+ // ---- REALPATH ----
956
+ realpath(path) {
957
+ path = this.normalizePath(path);
958
+ const idx = this.resolvePathComponents(path, true);
959
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
960
+ const inode = this.readInode(idx);
961
+ const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
962
+ return { status: 0, data: encoder.encode(resolvedPath) };
963
+ }
964
+ // ---- CHMOD ----
965
+ chmod(path, mode) {
966
+ path = this.normalizePath(path);
967
+ const idx = this.resolvePathComponents(path, true);
968
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
969
+ const inode = this.readInode(idx);
970
+ inode.mode = inode.mode & S_IFMT | mode & 4095;
971
+ inode.ctime = Date.now();
972
+ this.writeInode(idx, inode);
973
+ return { status: 0 };
974
+ }
975
+ // ---- CHOWN ----
976
+ chown(path, uid, gid) {
977
+ path = this.normalizePath(path);
978
+ const idx = this.resolvePathComponents(path, true);
979
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
980
+ const inode = this.readInode(idx);
981
+ inode.uid = uid;
982
+ inode.gid = gid;
983
+ inode.ctime = Date.now();
984
+ this.writeInode(idx, inode);
985
+ return { status: 0 };
986
+ }
987
+ // ---- UTIMES ----
988
+ utimes(path, atime, mtime) {
989
+ path = this.normalizePath(path);
990
+ const idx = this.resolvePathComponents(path, true);
991
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
992
+ const inode = this.readInode(idx);
993
+ inode.atime = atime;
994
+ inode.mtime = mtime;
995
+ inode.ctime = Date.now();
996
+ this.writeInode(idx, inode);
997
+ return { status: 0 };
998
+ }
999
+ // ---- SYMLINK ----
1000
+ symlink(target, linkPath) {
1001
+ linkPath = this.normalizePath(linkPath);
1002
+ if (this.pathIndex.has(linkPath)) return { status: CODE_TO_STATUS.EEXIST };
1003
+ const parentStatus = this.ensureParent(linkPath);
1004
+ if (parentStatus !== 0) return { status: parentStatus };
1005
+ const targetBytes = encoder.encode(target);
1006
+ this.createInode(linkPath, INODE_TYPE.SYMLINK, DEFAULT_SYMLINK_MODE, targetBytes.byteLength, targetBytes);
1007
+ this.commitPending();
1008
+ return { status: 0 };
1009
+ }
1010
+ // ---- READLINK ----
1011
+ readlink(path) {
1012
+ path = this.normalizePath(path);
1013
+ const idx = this.pathIndex.get(path);
1014
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
1015
+ const inode = this.readInode(idx);
1016
+ if (inode.type !== INODE_TYPE.SYMLINK) return { status: CODE_TO_STATUS.EINVAL, data: null };
1017
+ const target = this.readData(inode.firstBlock, inode.blockCount, inode.size);
1018
+ return { status: 0, data: target };
1019
+ }
1020
+ // ---- LINK (hard link — copies the file) ----
1021
+ link(existingPath, newPath) {
1022
+ return this.copy(existingPath, newPath);
1023
+ }
1024
+ // ---- OPEN (file descriptor) ----
1025
+ open(path, flags, tabId2) {
1026
+ path = this.normalizePath(path);
1027
+ const hasCreate = (flags & 64) !== 0;
1028
+ const hasTrunc = (flags & 512) !== 0;
1029
+ const hasExcl = (flags & 128) !== 0;
1030
+ let idx = this.resolvePathComponents(path, true);
1031
+ if (idx === void 0) {
1032
+ if (!hasCreate) return { status: CODE_TO_STATUS.ENOENT, data: null };
1033
+ const mode = DEFAULT_FILE_MODE & ~(this.umask & 511);
1034
+ idx = this.createInode(path, INODE_TYPE.FILE, mode, 0);
1035
+ } else if (hasExcl && hasCreate) {
1036
+ return { status: CODE_TO_STATUS.EEXIST, data: null };
1037
+ }
1038
+ if (hasTrunc) {
1039
+ this.truncate(path, 0);
1040
+ }
1041
+ const fd = this.nextFd++;
1042
+ this.fdTable.set(fd, { tabId: tabId2, inodeIdx: idx, position: 0, flags });
1043
+ const buf = new Uint8Array(4);
1044
+ new DataView(buf.buffer).setUint32(0, fd, true);
1045
+ return { status: 0, data: buf };
1046
+ }
1047
+ // ---- CLOSE ----
1048
+ close(fd) {
1049
+ if (!this.fdTable.has(fd)) return { status: CODE_TO_STATUS.EBADF };
1050
+ this.fdTable.delete(fd);
1051
+ return { status: 0 };
1052
+ }
1053
+ // ---- FREAD ----
1054
+ fread(fd, length, position) {
1055
+ const entry = this.fdTable.get(fd);
1056
+ if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
1057
+ const inode = this.readInode(entry.inodeIdx);
1058
+ const pos = position ?? entry.position;
1059
+ const readLen = Math.min(length, inode.size - pos);
1060
+ if (readLen <= 0) return { status: 0, data: new Uint8Array(0) };
1061
+ const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
1062
+ const buf = new Uint8Array(readLen);
1063
+ this.handle.read(buf, { at: dataOffset });
1064
+ if (position === null) {
1065
+ entry.position += readLen;
1066
+ }
1067
+ return { status: 0, data: buf };
1068
+ }
1069
+ // ---- FWRITE ----
1070
+ fwrite(fd, data, position) {
1071
+ const entry = this.fdTable.get(fd);
1072
+ if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
1073
+ const inode = this.readInode(entry.inodeIdx);
1074
+ const isAppend = (entry.flags & 1024) !== 0;
1075
+ const pos = isAppend ? inode.size : position ?? entry.position;
1076
+ const endPos = pos + data.byteLength;
1077
+ if (endPos > inode.size) {
1078
+ const neededBlocks = Math.ceil(endPos / this.blockSize);
1079
+ if (neededBlocks > inode.blockCount) {
1080
+ const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1081
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
1082
+ const newFirst = this.allocateBlocks(neededBlocks);
1083
+ const newBuf = new Uint8Array(endPos);
1084
+ newBuf.set(oldData);
1085
+ newBuf.set(data, pos);
1086
+ this.writeData(newFirst, newBuf);
1087
+ inode.firstBlock = newFirst;
1088
+ inode.blockCount = neededBlocks;
1089
+ } else {
1090
+ const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
1091
+ this.handle.write(data, { at: dataOffset });
1092
+ }
1093
+ inode.size = endPos;
1094
+ } else {
1095
+ const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
1096
+ this.handle.write(data, { at: dataOffset });
1097
+ }
1098
+ inode.mtime = Date.now();
1099
+ this.writeInode(entry.inodeIdx, inode);
1100
+ if (position === null) {
1101
+ entry.position = endPos;
1102
+ }
1103
+ this.commitPending();
1104
+ const buf = new Uint8Array(4);
1105
+ new DataView(buf.buffer).setUint32(0, data.byteLength, true);
1106
+ return { status: 0, data: buf };
1107
+ }
1108
+ // ---- FSTAT ----
1109
+ fstat(fd) {
1110
+ const entry = this.fdTable.get(fd);
1111
+ if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
1112
+ return this.encodeStatResponse(entry.inodeIdx);
1113
+ }
1114
+ // ---- FTRUNCATE ----
1115
+ ftruncate(fd, len = 0) {
1116
+ const entry = this.fdTable.get(fd);
1117
+ if (!entry) return { status: CODE_TO_STATUS.EBADF };
1118
+ const inode = this.readInode(entry.inodeIdx);
1119
+ const path = this.readPath(inode.pathOffset, inode.pathLength);
1120
+ return this.truncate(path, len);
1121
+ }
1122
+ // ---- FSYNC ----
1123
+ fsync() {
1124
+ this.commitPending();
1125
+ this.handle.flush();
1126
+ return { status: 0 };
1127
+ }
1128
+ // ---- OPENDIR ----
1129
+ opendir(path, tabId2) {
1130
+ path = this.normalizePath(path);
1131
+ const idx = this.resolvePathComponents(path, true);
1132
+ if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
1133
+ const inode = this.readInode(idx);
1134
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
1135
+ const fd = this.nextFd++;
1136
+ this.fdTable.set(fd, { tabId: tabId2, inodeIdx: idx, position: 0, flags: 0 });
1137
+ const buf = new Uint8Array(4);
1138
+ new DataView(buf.buffer).setUint32(0, fd, true);
1139
+ return { status: 0, data: buf };
1140
+ }
1141
+ // ---- MKDTEMP ----
1142
+ mkdtemp(prefix) {
1143
+ const suffix = Math.random().toString(36).substring(2, 8);
1144
+ const path = this.normalizePath(prefix + suffix);
1145
+ const parentStatus = this.ensureParent(path);
1146
+ if (parentStatus !== 0) {
1147
+ const parentPath = path.substring(0, path.lastIndexOf("/"));
1148
+ if (parentPath) {
1149
+ this.mkdirRecursive(parentPath);
1150
+ }
1151
+ }
1152
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
1153
+ this.createInode(path, INODE_TYPE.DIRECTORY, mode, 0);
1154
+ this.commitPending();
1155
+ return { status: 0, data: encoder.encode(path) };
1156
+ }
1157
+ // ========== Helpers ==========
1158
+ getDirectChildren(dirPath) {
1159
+ const prefix = dirPath === "/" ? "/" : dirPath + "/";
1160
+ const children = [];
1161
+ for (const path of this.pathIndex.keys()) {
1162
+ if (path === dirPath) continue;
1163
+ if (!path.startsWith(prefix)) continue;
1164
+ const rest = path.substring(prefix.length);
1165
+ if (!rest.includes("/")) {
1166
+ children.push(path);
1167
+ }
1168
+ }
1169
+ return children.sort();
1170
+ }
1171
+ getAllDescendants(dirPath) {
1172
+ const prefix = dirPath === "/" ? "/" : dirPath + "/";
1173
+ const descendants = [];
1174
+ for (const path of this.pathIndex.keys()) {
1175
+ if (path.startsWith(prefix)) descendants.push(path);
1176
+ }
1177
+ return descendants.sort((a, b) => {
1178
+ const da = a.split("/").length;
1179
+ const db = b.split("/").length;
1180
+ return db - da;
1181
+ });
1182
+ }
1183
+ ensureParent(path) {
1184
+ const lastSlash = path.lastIndexOf("/");
1185
+ if (lastSlash <= 0) return 0;
1186
+ const parentPath = path.substring(0, lastSlash);
1187
+ const parentIdx = this.pathIndex.get(parentPath);
1188
+ if (parentIdx === void 0) return CODE_TO_STATUS.ENOENT;
1189
+ const parentInode = this.readInode(parentIdx);
1190
+ if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
1191
+ return 0;
1192
+ }
1193
+ /** Clean up all fds owned by a tab */
1194
+ cleanupTab(tabId2) {
1195
+ for (const [fd, entry] of this.fdTable) {
1196
+ if (entry.tabId === tabId2) {
1197
+ this.fdTable.delete(fd);
1198
+ }
1199
+ }
1200
+ }
1201
+ /** Get all file paths and their data for OPFS sync */
1202
+ getAllFiles() {
1203
+ const files = [];
1204
+ for (const [path, idx] of this.pathIndex) {
1205
+ files.push({ path, idx });
1206
+ }
1207
+ return files;
1208
+ }
1209
+ /** Get file path for a file descriptor (used by OPFS sync for FD-based ops) */
1210
+ getPathForFd(fd) {
1211
+ const entry = this.fdTable.get(fd);
1212
+ if (!entry) return null;
1213
+ const inode = this.readInode(entry.inodeIdx);
1214
+ return this.readPath(inode.pathOffset, inode.pathLength);
1215
+ }
1216
+ /** Get file data by inode index */
1217
+ getInodeData(idx) {
1218
+ const inode = this.readInode(idx);
1219
+ const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1220
+ return { type: inode.type, data, mtime: inode.mtime };
1221
+ }
1222
+ flush() {
1223
+ this.handle.flush();
1224
+ }
1225
+ };
1226
+
1227
+ // src/protocol/opcodes.ts
1228
+ var OP = {
1229
+ READ: 1,
1230
+ WRITE: 2,
1231
+ UNLINK: 3,
1232
+ STAT: 4,
1233
+ LSTAT: 5,
1234
+ MKDIR: 6,
1235
+ RMDIR: 7,
1236
+ READDIR: 8,
1237
+ RENAME: 9,
1238
+ EXISTS: 10,
1239
+ TRUNCATE: 11,
1240
+ APPEND: 12,
1241
+ COPY: 13,
1242
+ ACCESS: 14,
1243
+ REALPATH: 15,
1244
+ CHMOD: 16,
1245
+ CHOWN: 17,
1246
+ UTIMES: 18,
1247
+ SYMLINK: 19,
1248
+ READLINK: 20,
1249
+ LINK: 21,
1250
+ OPEN: 22,
1251
+ CLOSE: 23,
1252
+ FREAD: 24,
1253
+ FWRITE: 25,
1254
+ FSTAT: 26,
1255
+ FTRUNCATE: 27,
1256
+ FSYNC: 28,
1257
+ OPENDIR: 29,
1258
+ MKDTEMP: 30
1259
+ };
1260
+ var SAB_OFFSETS = {
1261
+ CONTROL: 0,
1262
+ // Int32 - signal (0=idle, 1=request, 2=response, 3=chunk, 4=ack)
1263
+ OPCODE: 4,
1264
+ // Int32 - operation code
1265
+ STATUS: 8,
1266
+ // Int32 - response status / error
1267
+ CHUNK_LEN: 12,
1268
+ // Int32 - bytes in this chunk
1269
+ TOTAL_LEN: 16,
1270
+ // BigUint64 - full data size across all chunks
1271
+ CHUNK_IDX: 24,
1272
+ // Int32 - 0-based chunk index
1273
+ RESERVED: 28,
1274
+ // Int32 - reserved
1275
+ HEADER_SIZE: 32
1276
+ // Data payload starts here
1277
+ };
1278
+ var SIGNAL = {
1279
+ IDLE: 0,
1280
+ REQUEST: 1,
1281
+ RESPONSE: 2,
1282
+ CHUNK: 3,
1283
+ CHUNK_ACK: 4
1284
+ };
1285
+ var encoder2 = new TextEncoder();
1286
+ var decoder2 = new TextDecoder();
1287
+ function decodeRequest(buf) {
1288
+ const view = new DataView(buf);
1289
+ const op = view.getUint32(0, true);
1290
+ const flags = view.getUint32(4, true);
1291
+ const pathLen = view.getUint32(8, true);
1292
+ const dataLen = view.getUint32(12, true);
1293
+ const bytes = new Uint8Array(buf);
1294
+ const path = decoder2.decode(bytes.subarray(16, 16 + pathLen));
1295
+ const data = dataLen > 0 ? bytes.subarray(16 + pathLen, 16 + pathLen + dataLen) : null;
1296
+ return { op, flags, path, data };
1297
+ }
1298
+ function encodeResponse(status, data) {
1299
+ const dataLen = data ? data.byteLength : 0;
1300
+ const buf = new ArrayBuffer(8 + dataLen);
1301
+ const view = new DataView(buf);
1302
+ view.setUint32(0, status, true);
1303
+ view.setUint32(4, dataLen, true);
1304
+ if (data) {
1305
+ new Uint8Array(buf).set(data, 8);
1306
+ }
1307
+ return buf;
1308
+ }
1309
+ function decodeSecondPath(data) {
1310
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1311
+ const pathLen = view.getUint32(0, true);
1312
+ return decoder2.decode(data.subarray(4, 4 + pathLen));
1313
+ }
1314
+
1315
+ // src/workers/sync-relay.worker.ts
1316
+ var engine = new VFSEngine();
1317
+ var leaderInitialized = false;
1318
+ var readySent = false;
1319
+ var debug = false;
1320
+ var leaderLoopRunning = false;
1321
+ var opfsSyncPort = null;
1322
+ var opfsSyncEnabled = false;
1323
+ var suppressPaths = /* @__PURE__ */ new Set();
1324
+ var sab;
1325
+ var ctrl;
1326
+ var readySab;
1327
+ var readySignal;
1328
+ var asyncSab = null;
1329
+ var asyncCtrl = null;
1330
+ var tabId = "";
1331
+ var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
1332
+ var clientPorts = /* @__PURE__ */ new Map();
1333
+ var portQueue = [];
1334
+ var yieldChannel = new MessageChannel();
1335
+ yieldChannel.port2.start();
1336
+ function yieldToEventLoop() {
1337
+ return new Promise((resolve) => {
1338
+ yieldChannel.port2.onmessage = () => resolve();
1339
+ yieldChannel.port1.postMessage(null);
1340
+ });
1341
+ }
1342
+ function registerClientPort(clientTabId, port) {
1343
+ port.onmessage = (e) => {
1344
+ if (e.data.buffer instanceof ArrayBuffer) {
1345
+ if (leaderLoopRunning) {
1346
+ portQueue.push({
1347
+ port,
1348
+ tabId: clientTabId,
1349
+ id: e.data.id,
1350
+ buffer: e.data.buffer
1351
+ });
1352
+ } else {
1353
+ const result = handleRequest(clientTabId, e.data.buffer);
1354
+ const response = encodeResponse(result.status, result.data);
1355
+ port.postMessage({ id: e.data.id, buffer: response }, [response]);
1356
+ if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
1357
+ }
1358
+ }
1359
+ };
1360
+ port.start();
1361
+ clientPorts.set(clientTabId, port);
1362
+ }
1363
+ function removeClientPort(clientTabId) {
1364
+ const port = clientPorts.get(clientTabId);
1365
+ if (port) {
1366
+ port.close();
1367
+ clientPorts.delete(clientTabId);
1368
+ }
1369
+ engine.cleanupTab(clientTabId);
1370
+ }
1371
+ function drainPortQueue() {
1372
+ while (portQueue.length > 0) {
1373
+ const msg = portQueue.shift();
1374
+ const result = handleRequest(msg.tabId, msg.buffer);
1375
+ const response = encodeResponse(result.status, result.data);
1376
+ msg.port.postMessage({ id: msg.id, buffer: response }, [response]);
1377
+ if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
1378
+ }
1379
+ }
1380
+ var leaderPort = null;
1381
+ var pendingResolve = null;
1382
+ var asyncRelayPort = null;
1383
+ function forwardToLeader(payload) {
1384
+ return new Promise((resolve) => {
1385
+ pendingResolve = resolve;
1386
+ const buf = payload.buffer.byteLength === payload.byteLength ? payload.buffer : payload.slice().buffer;
1387
+ leaderPort.postMessage(
1388
+ { id: tabId, tabId, buffer: buf },
1389
+ [buf]
1390
+ );
1391
+ });
1392
+ }
1393
+ function onLeaderMessage(e) {
1394
+ if (e.data.buffer instanceof ArrayBuffer) {
1395
+ if (pendingResolve) {
1396
+ const resolve = pendingResolve;
1397
+ pendingResolve = null;
1398
+ resolve(e.data.buffer);
1399
+ } else if (asyncRelayPort) {
1400
+ asyncRelayPort.postMessage({ id: e.data.id, buffer: e.data.buffer }, [e.data.buffer]);
1401
+ }
1402
+ }
1403
+ }
1404
+ var OP_NAMES = {
1405
+ 1: "READ",
1406
+ 2: "WRITE",
1407
+ 3: "UNLINK",
1408
+ 4: "STAT",
1409
+ 5: "LSTAT",
1410
+ 6: "MKDIR",
1411
+ 7: "RMDIR",
1412
+ 8: "READDIR",
1413
+ 9: "RENAME",
1414
+ 10: "EXISTS",
1415
+ 11: "TRUNCATE",
1416
+ 12: "APPEND",
1417
+ 13: "COPY",
1418
+ 14: "ACCESS",
1419
+ 15: "REALPATH",
1420
+ 16: "CHMOD",
1421
+ 17: "CHOWN",
1422
+ 18: "UTIMES",
1423
+ 19: "SYMLINK",
1424
+ 20: "READLINK",
1425
+ 21: "LINK",
1426
+ 22: "OPEN",
1427
+ 23: "CLOSE",
1428
+ 24: "FREAD",
1429
+ 25: "FWRITE",
1430
+ 26: "FSTAT",
1431
+ 27: "FTRUNCATE",
1432
+ 28: "FSYNC",
1433
+ 29: "OPENDIR",
1434
+ 30: "MKDTEMP"
1435
+ };
1436
+ function handleRequest(reqTabId, buffer) {
1437
+ const t0 = debug ? performance.now() : 0;
1438
+ const { op, flags, path, data } = decodeRequest(buffer);
1439
+ const t1 = debug ? performance.now() : 0;
1440
+ let result;
1441
+ let syncOp;
1442
+ let syncPath;
1443
+ let syncNewPath;
1444
+ switch (op) {
1445
+ case OP.READ:
1446
+ result = engine.read(path);
1447
+ break;
1448
+ case OP.WRITE:
1449
+ result = engine.write(path, data ?? new Uint8Array(0), flags);
1450
+ if (opfsSyncEnabled && result.status === 0) {
1451
+ syncOp = op;
1452
+ syncPath = path;
1453
+ }
1454
+ break;
1455
+ case OP.APPEND:
1456
+ result = engine.append(path, data ?? new Uint8Array(0));
1457
+ if (opfsSyncEnabled && result.status === 0) {
1458
+ syncOp = op;
1459
+ syncPath = path;
1460
+ }
1461
+ break;
1462
+ case OP.UNLINK:
1463
+ result = engine.unlink(path);
1464
+ if (opfsSyncEnabled && result.status === 0) {
1465
+ syncOp = op;
1466
+ syncPath = path;
1467
+ }
1468
+ break;
1469
+ case OP.STAT:
1470
+ result = engine.stat(path);
1471
+ break;
1472
+ case OP.LSTAT:
1473
+ result = engine.lstat(path);
1474
+ break;
1475
+ case OP.MKDIR:
1476
+ result = engine.mkdir(path, flags);
1477
+ if (opfsSyncEnabled && result.status === 0) {
1478
+ syncOp = op;
1479
+ syncPath = path;
1480
+ }
1481
+ break;
1482
+ case OP.RMDIR:
1483
+ result = engine.rmdir(path, flags);
1484
+ if (opfsSyncEnabled && result.status === 0) {
1485
+ syncOp = op;
1486
+ syncPath = path;
1487
+ }
1488
+ break;
1489
+ case OP.READDIR:
1490
+ result = engine.readdir(path, flags);
1491
+ break;
1492
+ case OP.RENAME: {
1493
+ const newPath = data ? decodeSecondPath(data) : "";
1494
+ result = engine.rename(path, newPath);
1495
+ if (opfsSyncEnabled && result.status === 0) {
1496
+ syncOp = op;
1497
+ syncPath = path;
1498
+ syncNewPath = newPath;
1499
+ }
1500
+ break;
1501
+ }
1502
+ case OP.EXISTS:
1503
+ result = engine.exists(path);
1504
+ break;
1505
+ case OP.TRUNCATE: {
1506
+ const len = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1507
+ result = engine.truncate(path, len);
1508
+ if (opfsSyncEnabled && result.status === 0) {
1509
+ syncOp = op;
1510
+ syncPath = path;
1511
+ }
1512
+ break;
1513
+ }
1514
+ case OP.COPY: {
1515
+ const destPath = data ? decodeSecondPath(data) : "";
1516
+ result = engine.copy(path, destPath, flags);
1517
+ if (opfsSyncEnabled && result.status === 0) {
1518
+ syncOp = op;
1519
+ syncPath = destPath;
1520
+ }
1521
+ break;
1522
+ }
1523
+ case OP.ACCESS:
1524
+ result = engine.access(path, flags);
1525
+ break;
1526
+ case OP.REALPATH:
1527
+ result = engine.realpath(path);
1528
+ break;
1529
+ case OP.CHMOD: {
1530
+ const chmodMode = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1531
+ result = engine.chmod(path, chmodMode);
1532
+ break;
1533
+ }
1534
+ case OP.CHOWN: {
1535
+ if (!data || data.byteLength < 8) {
1536
+ result = { status: 7 };
1537
+ break;
1538
+ }
1539
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1540
+ const uid = dv.getUint32(0, true);
1541
+ const gid = dv.getUint32(4, true);
1542
+ result = engine.chown(path, uid, gid);
1543
+ break;
1544
+ }
1545
+ case OP.UTIMES: {
1546
+ if (!data || data.byteLength < 16) {
1547
+ result = { status: 7 };
1548
+ break;
1549
+ }
1550
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1551
+ const atime = dv.getFloat64(0, true);
1552
+ const mtime = dv.getFloat64(8, true);
1553
+ result = engine.utimes(path, atime, mtime);
1554
+ break;
1555
+ }
1556
+ case OP.SYMLINK: {
1557
+ const target = data ? new TextDecoder().decode(data) : "";
1558
+ result = engine.symlink(target, path);
1559
+ break;
1560
+ }
1561
+ case OP.READLINK:
1562
+ result = engine.readlink(path);
1563
+ break;
1564
+ case OP.LINK: {
1565
+ const newPath = data ? decodeSecondPath(data) : "";
1566
+ result = engine.link(path, newPath);
1567
+ if (opfsSyncEnabled && result.status === 0) {
1568
+ syncOp = op;
1569
+ syncPath = newPath;
1570
+ }
1571
+ break;
1572
+ }
1573
+ case OP.OPEN:
1574
+ result = engine.open(path, flags, reqTabId);
1575
+ break;
1576
+ case OP.CLOSE: {
1577
+ const fd = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1578
+ result = engine.close(fd);
1579
+ break;
1580
+ }
1581
+ case OP.FREAD: {
1582
+ if (!data || data.byteLength < 12) {
1583
+ result = { status: 7 };
1584
+ break;
1585
+ }
1586
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1587
+ const fd = dv.getUint32(0, true);
1588
+ const length = dv.getUint32(4, true);
1589
+ const pos = dv.getInt32(8, true);
1590
+ result = engine.fread(fd, length, pos === -1 ? null : pos);
1591
+ break;
1592
+ }
1593
+ case OP.FWRITE: {
1594
+ if (!data || data.byteLength < 8) {
1595
+ result = { status: 7 };
1596
+ break;
1597
+ }
1598
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1599
+ const fd = dv.getUint32(0, true);
1600
+ const pos = dv.getInt32(4, true);
1601
+ const writeData = data.subarray(8);
1602
+ result = engine.fwrite(fd, writeData, pos === -1 ? null : pos);
1603
+ if (opfsSyncEnabled && result.status === 0) {
1604
+ syncOp = op;
1605
+ syncPath = engine.getPathForFd(fd) ?? void 0;
1606
+ }
1607
+ break;
1608
+ }
1609
+ case OP.FSTAT: {
1610
+ const fd = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1611
+ result = engine.fstat(fd);
1612
+ break;
1613
+ }
1614
+ case OP.FTRUNCATE: {
1615
+ if (!data || data.byteLength < 8) {
1616
+ result = { status: 7 };
1617
+ break;
1618
+ }
1619
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1620
+ const fd = dv.getUint32(0, true);
1621
+ const len = dv.getUint32(4, true);
1622
+ result = engine.ftruncate(fd, len);
1623
+ if (opfsSyncEnabled && result.status === 0) {
1624
+ syncOp = op;
1625
+ syncPath = engine.getPathForFd(fd) ?? void 0;
1626
+ }
1627
+ break;
1628
+ }
1629
+ case OP.FSYNC:
1630
+ result = engine.fsync();
1631
+ break;
1632
+ case OP.OPENDIR:
1633
+ result = engine.opendir(path, reqTabId);
1634
+ break;
1635
+ case OP.MKDTEMP:
1636
+ result = engine.mkdtemp(path);
1637
+ if (opfsSyncEnabled && result.status === 0 && result.data) {
1638
+ syncOp = op;
1639
+ syncPath = new TextDecoder().decode(result.data instanceof Uint8Array ? result.data : new Uint8Array(0));
1640
+ }
1641
+ break;
1642
+ default:
1643
+ result = { status: 7 };
1644
+ }
1645
+ if (debug) {
1646
+ const t2 = performance.now();
1647
+ console.log(`[sync-relay] op=${OP_NAMES[op] ?? op} path=${path} decode=${(t1 - t0).toFixed(3)}ms engine=${(t2 - t1).toFixed(3)}ms TOTAL=${(t2 - t0).toFixed(3)}ms`);
1648
+ }
1649
+ const ret = {
1650
+ status: result.status,
1651
+ data: result.data instanceof Uint8Array ? result.data : void 0
1652
+ };
1653
+ if (syncOp !== void 0 && syncPath) {
1654
+ ret._op = syncOp;
1655
+ ret._path = syncPath;
1656
+ ret._newPath = syncNewPath;
1657
+ }
1658
+ return ret;
1659
+ }
1660
+ function readPayload(targetSab, targetCtrl) {
1661
+ const totalLenView = new BigUint64Array(targetSab, SAB_OFFSETS.TOTAL_LEN, 1);
1662
+ const maxChunk = targetSab.byteLength - HEADER_SIZE;
1663
+ const chunkLen = Atomics.load(targetCtrl, 3);
1664
+ const totalLen = Number(Atomics.load(totalLenView, 0));
1665
+ if (totalLen <= maxChunk) {
1666
+ return new Uint8Array(targetSab, HEADER_SIZE, chunkLen).slice();
1667
+ }
1668
+ const fullBuffer = new Uint8Array(totalLen);
1669
+ let offset = 0;
1670
+ fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, chunkLen), offset);
1671
+ offset += chunkLen;
1672
+ while (offset < totalLen) {
1673
+ Atomics.store(targetCtrl, 0, SIGNAL.CHUNK_ACK);
1674
+ Atomics.notify(targetCtrl, 0);
1675
+ Atomics.wait(targetCtrl, 0, SIGNAL.CHUNK_ACK);
1676
+ const nextLen = Atomics.load(targetCtrl, 3);
1677
+ fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, nextLen), offset);
1678
+ offset += nextLen;
1679
+ }
1680
+ return fullBuffer;
1681
+ }
1682
+ function writeDirectResponse(targetSab, targetCtrl, status, data) {
1683
+ const dataLen = data ? data.byteLength : 0;
1684
+ const totalLen = 8 + dataLen;
1685
+ const maxChunk = targetSab.byteLength - HEADER_SIZE;
1686
+ if (totalLen <= maxChunk) {
1687
+ const hdr = new DataView(targetSab, HEADER_SIZE, 8);
1688
+ hdr.setUint32(0, status, true);
1689
+ hdr.setUint32(4, dataLen, true);
1690
+ if (data && dataLen > 0) {
1691
+ new Uint8Array(targetSab, HEADER_SIZE + 8, dataLen).set(data);
1692
+ }
1693
+ Atomics.store(targetCtrl, 3, totalLen);
1694
+ const totalView = new BigUint64Array(targetSab, SAB_OFFSETS.TOTAL_LEN, 1);
1695
+ Atomics.store(totalView, 0, BigInt(totalLen));
1696
+ Atomics.store(targetCtrl, 0, SIGNAL.RESPONSE);
1697
+ Atomics.notify(targetCtrl, 0);
1698
+ } else {
1699
+ const response = encodeResponse(status, data);
1700
+ writeResponse(targetSab, targetCtrl, new Uint8Array(response));
1701
+ }
1702
+ }
1703
+ function writeResponse(targetSab, targetCtrl, responseData) {
1704
+ const maxChunk = targetSab.byteLength - HEADER_SIZE;
1705
+ if (responseData.byteLength <= maxChunk) {
1706
+ new Uint8Array(targetSab, HEADER_SIZE, responseData.byteLength).set(responseData);
1707
+ Atomics.store(targetCtrl, 3, responseData.byteLength);
1708
+ const totalView = new BigUint64Array(targetSab, SAB_OFFSETS.TOTAL_LEN, 1);
1709
+ Atomics.store(totalView, 0, BigInt(responseData.byteLength));
1710
+ Atomics.store(targetCtrl, 0, SIGNAL.RESPONSE);
1711
+ Atomics.notify(targetCtrl, 0);
1712
+ } else {
1713
+ let sent = 0;
1714
+ while (sent < responseData.byteLength) {
1715
+ const chunkSize = Math.min(maxChunk, responseData.byteLength - sent);
1716
+ new Uint8Array(targetSab, HEADER_SIZE, chunkSize).set(
1717
+ responseData.subarray(sent, sent + chunkSize)
1718
+ );
1719
+ Atomics.store(targetCtrl, 3, chunkSize);
1720
+ Atomics.store(targetCtrl, 6, Math.floor(sent / maxChunk));
1721
+ const isLast = sent + chunkSize >= responseData.byteLength;
1722
+ Atomics.store(targetCtrl, 0, isLast ? SIGNAL.RESPONSE : SIGNAL.CHUNK);
1723
+ Atomics.notify(targetCtrl, 0);
1724
+ if (!isLast) {
1725
+ Atomics.wait(targetCtrl, 0, SIGNAL.CHUNK);
1726
+ }
1727
+ sent += chunkSize;
1728
+ }
1729
+ }
1730
+ }
1731
+ async function leaderLoop() {
1732
+ leaderLoopRunning = true;
1733
+ while (true) {
1734
+ let processed = true;
1735
+ let tightOps = 0;
1736
+ while (processed) {
1737
+ processed = false;
1738
+ if (++tightOps >= 100) {
1739
+ tightOps = 0;
1740
+ await yieldToEventLoop();
1741
+ }
1742
+ if (Atomics.load(ctrl, 0) === SIGNAL.REQUEST) {
1743
+ const lt0 = debug ? performance.now() : 0;
1744
+ const payload = readPayload(sab, ctrl);
1745
+ const lt1 = debug ? performance.now() : 0;
1746
+ const reqResult = handleRequest(tabId, payload.buffer);
1747
+ const lt2 = debug ? performance.now() : 0;
1748
+ writeDirectResponse(sab, ctrl, reqResult.status, reqResult.data);
1749
+ if (reqResult._op !== void 0) notifyOPFSSync(reqResult._op, reqResult._path, reqResult._newPath);
1750
+ const lt3 = debug ? performance.now() : 0;
1751
+ if (debug) {
1752
+ console.log(`[leaderLoop] readPayload=${(lt1 - lt0).toFixed(3)}ms handleRequest=${(lt2 - lt1).toFixed(3)}ms writeResponse=${(lt3 - lt2).toFixed(3)}ms TOTAL=${(lt3 - lt0).toFixed(3)}ms`);
1753
+ }
1754
+ const waitResult = Atomics.wait(ctrl, 0, SIGNAL.RESPONSE, 10);
1755
+ if (waitResult === "timed-out") {
1756
+ Atomics.store(ctrl, 0, SIGNAL.IDLE);
1757
+ }
1758
+ processed = true;
1759
+ continue;
1760
+ }
1761
+ if (asyncCtrl && Atomics.load(asyncCtrl, 0) === SIGNAL.REQUEST) {
1762
+ const payload = readPayload(asyncSab, asyncCtrl);
1763
+ const asyncResult = handleRequest(tabId, payload.buffer);
1764
+ writeDirectResponse(asyncSab, asyncCtrl, asyncResult.status, asyncResult.data);
1765
+ if (asyncResult._op !== void 0) notifyOPFSSync(asyncResult._op, asyncResult._path, asyncResult._newPath);
1766
+ const waitResult = Atomics.wait(asyncCtrl, 0, SIGNAL.RESPONSE, 10);
1767
+ if (waitResult === "timed-out") {
1768
+ Atomics.store(asyncCtrl, 0, SIGNAL.IDLE);
1769
+ }
1770
+ processed = true;
1771
+ continue;
1772
+ }
1773
+ if (portQueue.length > 0) {
1774
+ drainPortQueue();
1775
+ processed = true;
1776
+ continue;
1777
+ }
1778
+ }
1779
+ await yieldToEventLoop();
1780
+ if (clientPorts.size === 0 && !opfsSyncEnabled) {
1781
+ const currentSignal = Atomics.load(ctrl, 0);
1782
+ if (currentSignal !== SIGNAL.REQUEST) {
1783
+ Atomics.wait(ctrl, 0, currentSignal, 50);
1784
+ }
1785
+ }
1786
+ }
1787
+ }
1788
+ async function followerLoop() {
1789
+ while (true) {
1790
+ if (Atomics.load(ctrl, 0) === SIGNAL.REQUEST) {
1791
+ const payload = readPayload(sab, ctrl);
1792
+ const response = await forwardToLeader(payload);
1793
+ writeResponse(sab, ctrl, new Uint8Array(response));
1794
+ const result = Atomics.wait(ctrl, 0, SIGNAL.RESPONSE, 10);
1795
+ if (result === "timed-out") {
1796
+ Atomics.store(ctrl, 0, SIGNAL.IDLE);
1797
+ }
1798
+ continue;
1799
+ }
1800
+ if (asyncCtrl && Atomics.load(asyncCtrl, 0) === SIGNAL.REQUEST) {
1801
+ const payload = readPayload(asyncSab, asyncCtrl);
1802
+ const response = await forwardToLeader(payload);
1803
+ writeResponse(asyncSab, asyncCtrl, new Uint8Array(response));
1804
+ const result = Atomics.wait(asyncCtrl, 0, SIGNAL.RESPONSE, 10);
1805
+ if (result === "timed-out") {
1806
+ Atomics.store(asyncCtrl, 0, SIGNAL.IDLE);
1807
+ }
1808
+ continue;
1809
+ }
1810
+ const waitResult = Atomics.wait(ctrl, 0, SIGNAL.IDLE, 50);
1811
+ if (waitResult === "timed-out") {
1812
+ await yieldToEventLoop();
1813
+ }
1814
+ }
1815
+ }
1816
+ async function initEngine(config) {
1817
+ debug = config.debug ?? false;
1818
+ let rootDir = await navigator.storage.getDirectory();
1819
+ if (config.root && config.root !== "/") {
1820
+ const segments = config.root.split("/").filter(Boolean);
1821
+ for (const segment of segments) {
1822
+ rootDir = await rootDir.getDirectoryHandle(segment, { create: true });
1823
+ }
1824
+ }
1825
+ const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
1826
+ const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
1827
+ engine.init(vfsHandle, {
1828
+ uid: config.uid,
1829
+ gid: config.gid,
1830
+ umask: config.umask,
1831
+ strictPermissions: config.strictPermissions,
1832
+ debug: config.debug
1833
+ });
1834
+ if (config.opfsSync) {
1835
+ opfsSyncEnabled = true;
1836
+ const mc = new MessageChannel();
1837
+ opfsSyncPort = mc.port1;
1838
+ opfsSyncPort.onmessage = (e) => handleExternalChange(e.data);
1839
+ opfsSyncPort.start();
1840
+ const workerUrl = new URL("./opfs-sync.worker.js", import.meta.url);
1841
+ const syncWorker = new Worker(workerUrl, { type: "module" });
1842
+ syncWorker.postMessage(
1843
+ { type: "init", root: config.opfsSyncRoot ?? config.root },
1844
+ [mc.port2]
1845
+ );
1846
+ }
1847
+ }
1848
+ function notifyOPFSSync(op, path, newPath) {
1849
+ if (!opfsSyncPort) return;
1850
+ if (suppressPaths.has(path)) {
1851
+ suppressPaths.delete(path);
1852
+ return;
1853
+ }
1854
+ const ts = Date.now();
1855
+ switch (op) {
1856
+ case OP.WRITE:
1857
+ case OP.APPEND:
1858
+ case OP.TRUNCATE:
1859
+ case OP.FWRITE:
1860
+ case OP.FTRUNCATE:
1861
+ case OP.COPY:
1862
+ case OP.LINK: {
1863
+ const result = engine.read(path);
1864
+ if (result.status === 0) {
1865
+ if (result.data && result.data.byteLength > 0) {
1866
+ const buf = result.data.buffer.byteLength === result.data.byteLength ? result.data.buffer : result.data.slice().buffer;
1867
+ opfsSyncPort.postMessage({ op: "write", path, data: buf, ts }, [buf]);
1868
+ } else {
1869
+ opfsSyncPort.postMessage({ op: "write", path, data: new ArrayBuffer(0), ts });
1870
+ }
1871
+ }
1872
+ break;
1873
+ }
1874
+ case OP.UNLINK:
1875
+ case OP.RMDIR:
1876
+ opfsSyncPort.postMessage({ op: "delete", path, ts });
1877
+ break;
1878
+ case OP.MKDIR:
1879
+ case OP.MKDTEMP:
1880
+ opfsSyncPort.postMessage({ op: "mkdir", path, ts });
1881
+ break;
1882
+ case OP.RENAME:
1883
+ if (newPath) {
1884
+ opfsSyncPort.postMessage({ op: "rename", path, newPath, ts });
1885
+ }
1886
+ break;
1887
+ }
1888
+ }
1889
+ function handleExternalChange(msg) {
1890
+ switch (msg.op) {
1891
+ case "external-write": {
1892
+ suppressPaths.add(msg.path);
1893
+ const result = engine.write(msg.path, new Uint8Array(msg.data), 0);
1894
+ console.log("[sync-relay] external-write:", msg.path, `${msg.data?.byteLength ?? 0}B`, `status=${result.status}`);
1895
+ break;
1896
+ }
1897
+ case "external-delete": {
1898
+ suppressPaths.add(msg.path);
1899
+ const result = engine.unlink(msg.path);
1900
+ if (result.status !== 0) {
1901
+ const rmdirResult = engine.rmdir(msg.path, 1);
1902
+ console.log("[sync-relay] external-delete (rmdir):", msg.path, `status=${rmdirResult.status}`);
1903
+ } else {
1904
+ console.log("[sync-relay] external-delete:", msg.path, `status=${result.status}`);
1905
+ }
1906
+ break;
1907
+ }
1908
+ case "external-rename":
1909
+ suppressPaths.add(msg.path);
1910
+ if (msg.newPath) {
1911
+ suppressPaths.add(msg.newPath);
1912
+ const result = engine.rename(msg.path, msg.newPath);
1913
+ console.log("[sync-relay] external-rename:", msg.path, "\u2192", msg.newPath, `status=${result.status}`);
1914
+ }
1915
+ break;
1916
+ }
1917
+ }
1918
+ self.onmessage = async (e) => {
1919
+ const msg = e.data;
1920
+ if (msg.type === "async-port") {
1921
+ const port = msg.port ?? e.ports[0];
1922
+ if (port) {
1923
+ asyncRelayPort = port;
1924
+ port.onmessage = (ev) => {
1925
+ if (ev.data.buffer instanceof ArrayBuffer) {
1926
+ if (leaderInitialized) {
1927
+ const result = handleRequest(tabId || "nosab", ev.data.buffer);
1928
+ const response = encodeResponse(result.status, result.data);
1929
+ port.postMessage({ id: ev.data.id, buffer: response }, [response]);
1930
+ if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
1931
+ } else if (leaderPort) {
1932
+ const buf = ev.data.buffer;
1933
+ leaderPort.postMessage({ id: ev.data.id, tabId, buffer: buf }, [buf]);
1934
+ }
1935
+ }
1936
+ };
1937
+ port.start();
1938
+ }
1939
+ return;
1940
+ }
1941
+ if (msg.type === "init-leader") {
1942
+ if (leaderInitialized) return;
1943
+ leaderInitialized = true;
1944
+ tabId = msg.tabId;
1945
+ const hasSAB = msg.sab != null;
1946
+ if (hasSAB) {
1947
+ sab = msg.sab;
1948
+ readySab = msg.readySab;
1949
+ ctrl = new Int32Array(sab, 0, 8);
1950
+ readySignal = new Int32Array(readySab, 0, 1);
1951
+ }
1952
+ if (msg.asyncSab) {
1953
+ asyncSab = msg.asyncSab;
1954
+ asyncCtrl = new Int32Array(msg.asyncSab, 0, 8);
1955
+ }
1956
+ try {
1957
+ await initEngine(msg.config);
1958
+ } catch (err) {
1959
+ leaderInitialized = false;
1960
+ self.postMessage({
1961
+ type: "init-failed",
1962
+ error: err.message
1963
+ });
1964
+ return;
1965
+ }
1966
+ if (!readySent) {
1967
+ readySent = true;
1968
+ if (hasSAB) {
1969
+ Atomics.store(readySignal, 0, 1);
1970
+ Atomics.notify(readySignal, 0);
1971
+ }
1972
+ self.postMessage({ type: "ready" });
1973
+ }
1974
+ if (hasSAB) {
1975
+ leaderLoop();
1976
+ }
1977
+ return;
1978
+ }
1979
+ if (msg.type === "init-follower") {
1980
+ tabId = msg.tabId;
1981
+ const hasSAB = msg.sab != null;
1982
+ if (hasSAB) {
1983
+ sab = msg.sab;
1984
+ readySab = msg.readySab;
1985
+ ctrl = new Int32Array(sab, 0, 8);
1986
+ readySignal = new Int32Array(readySab, 0, 1);
1987
+ }
1988
+ if (msg.asyncSab) {
1989
+ asyncSab = msg.asyncSab;
1990
+ asyncCtrl = new Int32Array(msg.asyncSab, 0, 8);
1991
+ }
1992
+ return;
1993
+ }
1994
+ if (msg.type === "leader-port") {
1995
+ if (leaderInitialized) return;
1996
+ const newPort = msg.port ?? e.ports[0];
1997
+ if (!newPort) return;
1998
+ if (leaderPort) {
1999
+ leaderPort.close();
2000
+ if (pendingResolve) {
2001
+ const errorBuf = encodeResponse(5);
2002
+ pendingResolve(errorBuf);
2003
+ pendingResolve = null;
2004
+ }
2005
+ }
2006
+ leaderPort = newPort;
2007
+ newPort.onmessage = onLeaderMessage;
2008
+ newPort.start();
2009
+ if (!readySent) {
2010
+ readySent = true;
2011
+ if (readySignal) {
2012
+ Atomics.store(readySignal, 0, 1);
2013
+ Atomics.notify(readySignal, 0);
2014
+ }
2015
+ self.postMessage({ type: "ready" });
2016
+ if (ctrl) {
2017
+ followerLoop();
2018
+ }
2019
+ }
2020
+ return;
2021
+ }
2022
+ if (msg.type === "client-port") {
2023
+ registerClientPort(msg.tabId, msg.port ?? e.ports[0]);
2024
+ return;
2025
+ }
2026
+ if (msg.type === "client-lost") {
2027
+ removeClientPort(msg.tabId);
2028
+ return;
2029
+ }
2030
+ };
2031
+ //# sourceMappingURL=sync-relay.worker.js.map