@componentor/fs 3.0.9 → 3.0.10

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.
@@ -195,6 +195,7 @@ var VFSEngine = class {
195
195
  this.bitmap = new Uint8Array(layout.bitmapSize);
196
196
  this.handle.write(this.bitmap, { at: this.bitmapOffset });
197
197
  this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
198
+ this.writeSuperblock();
198
199
  this.handle.flush();
199
200
  }
200
201
  /** Mount an existing VFS from disk — validates superblock integrity */
@@ -354,14 +355,33 @@ var VFSEngine = class {
354
355
  const off = i * INODE_SIZE;
355
356
  const type = inodeView.getUint8(off + INODE.TYPE);
356
357
  if (type === INODE_TYPE.FREE) continue;
358
+ if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) {
359
+ throw new Error(`Corrupt VFS: inode ${i} has invalid type ${type}`);
360
+ }
361
+ const pathOffset = inodeView.getUint32(off + INODE.PATH_OFFSET, true);
362
+ const pathLength = inodeView.getUint16(off + INODE.PATH_LENGTH, true);
363
+ const size = inodeView.getFloat64(off + INODE.SIZE, true);
364
+ const firstBlock = inodeView.getUint32(off + INODE.FIRST_BLOCK, true);
365
+ const blockCount = inodeView.getUint32(off + INODE.BLOCK_COUNT, true);
366
+ if (pathLength === 0 || pathOffset + pathLength > this.pathTableUsed) {
367
+ throw new Error(`Corrupt VFS: inode ${i} path out of bounds (offset=${pathOffset}, len=${pathLength}, tableUsed=${this.pathTableUsed})`);
368
+ }
369
+ if (type !== INODE_TYPE.DIRECTORY) {
370
+ if (size < 0 || !isFinite(size)) {
371
+ throw new Error(`Corrupt VFS: inode ${i} has invalid size ${size}`);
372
+ }
373
+ if (blockCount > 0 && firstBlock + blockCount > this.totalBlocks) {
374
+ throw new Error(`Corrupt VFS: inode ${i} data blocks out of range (first=${firstBlock}, count=${blockCount}, total=${this.totalBlocks})`);
375
+ }
376
+ }
357
377
  const inode = {
358
378
  type,
359
- pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
360
- pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
379
+ pathOffset,
380
+ pathLength,
361
381
  mode: inodeView.getUint32(off + INODE.MODE, true),
362
- size: inodeView.getFloat64(off + INODE.SIZE, true),
363
- firstBlock: inodeView.getUint32(off + INODE.FIRST_BLOCK, true),
364
- blockCount: inodeView.getUint32(off + INODE.BLOCK_COUNT, true),
382
+ size,
383
+ firstBlock,
384
+ blockCount,
365
385
  mtime: inodeView.getFloat64(off + INODE.MTIME, true),
366
386
  ctime: inodeView.getFloat64(off + INODE.CTIME, true),
367
387
  atime: inodeView.getFloat64(off + INODE.ATIME, true),
@@ -369,7 +389,15 @@ var VFSEngine = class {
369
389
  gid: inodeView.getUint32(off + INODE.GID, true)
370
390
  };
371
391
  this.inodeCache.set(i, inode);
372
- const path = pathBuf ? decoder.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength)) : this.readPath(inode.pathOffset, inode.pathLength);
392
+ let path;
393
+ if (pathBuf) {
394
+ path = decoder.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength));
395
+ } else {
396
+ path = this.readPath(inode.pathOffset, inode.pathLength);
397
+ }
398
+ if (!path.startsWith("/") || path.includes("\0")) {
399
+ throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
400
+ }
373
401
  this.pathIndex.set(path, i);
374
402
  }
375
403
  }
@@ -1364,6 +1392,503 @@ var VFSEngine = class {
1364
1392
  }
1365
1393
  };
1366
1394
 
1395
+ // src/opfs-engine.ts
1396
+ var encoder2 = new TextEncoder();
1397
+ var TYPE_FILE = 1;
1398
+ var TYPE_DIRECTORY = 2;
1399
+ var OK = 0;
1400
+ var ENOENT = 1;
1401
+ var EEXIST = 2;
1402
+ var ENOTEMPTY = 5;
1403
+ var EINVAL = 7;
1404
+ var EBADF = 8;
1405
+ var OPFSEngine = class {
1406
+ rootDir;
1407
+ fdTable = /* @__PURE__ */ new Map();
1408
+ nextFd = 3;
1409
+ nextIno = 1;
1410
+ processUid = 0;
1411
+ processGid = 0;
1412
+ async init(rootDir, opts) {
1413
+ this.rootDir = rootDir;
1414
+ this.processUid = opts?.uid ?? 0;
1415
+ this.processGid = opts?.gid ?? 0;
1416
+ }
1417
+ cleanupTab(_tabId) {
1418
+ for (const [fd, entry] of this.fdTable) {
1419
+ try {
1420
+ entry.handle.close();
1421
+ } catch {
1422
+ }
1423
+ this.fdTable.delete(fd);
1424
+ }
1425
+ }
1426
+ getPathForFd(fd) {
1427
+ return this.fdTable.get(fd)?.path ?? null;
1428
+ }
1429
+ // ========== Path helpers ==========
1430
+ normalizePath(path) {
1431
+ if (!path.startsWith("/")) path = "/" + path;
1432
+ while (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
1433
+ const parts = path.split("/");
1434
+ const resolved = [];
1435
+ for (const part of parts) {
1436
+ if (part === "" || part === ".") continue;
1437
+ if (part === "..") {
1438
+ resolved.pop();
1439
+ continue;
1440
+ }
1441
+ resolved.push(part);
1442
+ }
1443
+ return "/" + resolved.join("/");
1444
+ }
1445
+ /** Navigate to the parent directory of a path, returning the parent handle and child name. */
1446
+ async navigateToParent(path) {
1447
+ const parts = path.split("/").filter(Boolean);
1448
+ if (parts.length === 0) return null;
1449
+ const name = parts.pop();
1450
+ let dir = this.rootDir;
1451
+ for (const part of parts) {
1452
+ try {
1453
+ dir = await dir.getDirectoryHandle(part);
1454
+ } catch {
1455
+ return null;
1456
+ }
1457
+ }
1458
+ return { dir, name };
1459
+ }
1460
+ /** Navigate to a directory by path. */
1461
+ async navigateToDir(path) {
1462
+ if (path === "/") return this.rootDir;
1463
+ const parts = path.split("/").filter(Boolean);
1464
+ let dir = this.rootDir;
1465
+ for (const part of parts) {
1466
+ try {
1467
+ dir = await dir.getDirectoryHandle(part);
1468
+ } catch {
1469
+ return null;
1470
+ }
1471
+ }
1472
+ return dir;
1473
+ }
1474
+ /** Get a file or directory handle for a path. */
1475
+ async getEntry(path) {
1476
+ if (path === "/") return { handle: this.rootDir, kind: "directory" };
1477
+ const nav = await this.navigateToParent(path);
1478
+ if (!nav) return null;
1479
+ try {
1480
+ return { handle: await nav.dir.getFileHandle(nav.name), kind: "file" };
1481
+ } catch {
1482
+ try {
1483
+ return { handle: await nav.dir.getDirectoryHandle(nav.name), kind: "directory" };
1484
+ } catch {
1485
+ return null;
1486
+ }
1487
+ }
1488
+ }
1489
+ /** Ensure all parent directories exist (recursive mkdir for parents). */
1490
+ async ensureParent(path) {
1491
+ const parts = path.split("/").filter(Boolean);
1492
+ parts.pop();
1493
+ let dir = this.rootDir;
1494
+ for (const part of parts) {
1495
+ try {
1496
+ dir = await dir.getDirectoryHandle(part, { create: true });
1497
+ } catch {
1498
+ return null;
1499
+ }
1500
+ }
1501
+ return dir;
1502
+ }
1503
+ encodeStat(kind, size, mtime, ino) {
1504
+ const buf = new Uint8Array(49);
1505
+ const view = new DataView(buf.buffer);
1506
+ view.setUint8(0, kind === "file" ? TYPE_FILE : TYPE_DIRECTORY);
1507
+ view.setUint32(1, kind === "file" ? 33188 : 16877, true);
1508
+ view.setFloat64(5, size, true);
1509
+ view.setFloat64(13, mtime, true);
1510
+ view.setFloat64(21, mtime, true);
1511
+ view.setFloat64(29, mtime, true);
1512
+ view.setUint32(37, this.processUid, true);
1513
+ view.setUint32(41, this.processGid, true);
1514
+ view.setUint32(45, ino, true);
1515
+ return buf;
1516
+ }
1517
+ // ========== FS Operations ==========
1518
+ async read(path) {
1519
+ path = this.normalizePath(path);
1520
+ const nav = await this.navigateToParent(path);
1521
+ if (!nav) return { status: ENOENT, data: null };
1522
+ try {
1523
+ const fh = await nav.dir.getFileHandle(nav.name);
1524
+ const file = await fh.getFile();
1525
+ return { status: OK, data: new Uint8Array(await file.arrayBuffer()) };
1526
+ } catch {
1527
+ return { status: ENOENT, data: null };
1528
+ }
1529
+ }
1530
+ async write(path, data, _flags) {
1531
+ path = this.normalizePath(path);
1532
+ const parentDir = await this.ensureParent(path);
1533
+ if (!parentDir) return { status: ENOENT, data: null };
1534
+ const name = path.split("/").filter(Boolean).pop();
1535
+ try {
1536
+ const fh = await parentDir.getFileHandle(name, { create: true });
1537
+ const sh = await fh.createSyncAccessHandle();
1538
+ try {
1539
+ sh.truncate(0);
1540
+ if (data.byteLength > 0) sh.write(data, { at: 0 });
1541
+ sh.flush();
1542
+ } finally {
1543
+ sh.close();
1544
+ }
1545
+ return { status: OK, data: null };
1546
+ } catch {
1547
+ return { status: ENOENT, data: null };
1548
+ }
1549
+ }
1550
+ async append(path, data) {
1551
+ path = this.normalizePath(path);
1552
+ const parentDir = await this.ensureParent(path);
1553
+ if (!parentDir) return { status: ENOENT, data: null };
1554
+ const name = path.split("/").filter(Boolean).pop();
1555
+ try {
1556
+ const fh = await parentDir.getFileHandle(name, { create: true });
1557
+ const sh = await fh.createSyncAccessHandle();
1558
+ try {
1559
+ const size = sh.getSize();
1560
+ sh.write(data, { at: size });
1561
+ sh.flush();
1562
+ } finally {
1563
+ sh.close();
1564
+ }
1565
+ return { status: OK, data: null };
1566
+ } catch {
1567
+ return { status: ENOENT, data: null };
1568
+ }
1569
+ }
1570
+ async unlink(path) {
1571
+ path = this.normalizePath(path);
1572
+ const nav = await this.navigateToParent(path);
1573
+ if (!nav) return { status: ENOENT, data: null };
1574
+ try {
1575
+ await nav.dir.getFileHandle(nav.name);
1576
+ await nav.dir.removeEntry(nav.name);
1577
+ return { status: OK, data: null };
1578
+ } catch {
1579
+ return { status: ENOENT, data: null };
1580
+ }
1581
+ }
1582
+ async stat(path) {
1583
+ path = this.normalizePath(path);
1584
+ const entry = await this.getEntry(path);
1585
+ if (!entry) return { status: ENOENT, data: null };
1586
+ if (entry.kind === "file") {
1587
+ const file = await entry.handle.getFile();
1588
+ return { status: OK, data: this.encodeStat("file", file.size, file.lastModified, this.nextIno++) };
1589
+ }
1590
+ return { status: OK, data: this.encodeStat("directory", 0, Date.now(), this.nextIno++) };
1591
+ }
1592
+ async lstat(path) {
1593
+ return this.stat(path);
1594
+ }
1595
+ async mkdir(path, flags = 0) {
1596
+ path = this.normalizePath(path);
1597
+ const recursive = (flags & 1) !== 0;
1598
+ if (recursive) {
1599
+ const parts = path.split("/").filter(Boolean);
1600
+ let dir = this.rootDir;
1601
+ for (const part of parts) {
1602
+ dir = await dir.getDirectoryHandle(part, { create: true });
1603
+ }
1604
+ return { status: OK, data: encoder2.encode(path) };
1605
+ }
1606
+ const nav = await this.navigateToParent(path);
1607
+ if (!nav) return { status: ENOENT, data: null };
1608
+ try {
1609
+ try {
1610
+ await nav.dir.getDirectoryHandle(nav.name);
1611
+ return { status: EEXIST, data: null };
1612
+ } catch {
1613
+ }
1614
+ await nav.dir.getDirectoryHandle(nav.name, { create: true });
1615
+ return { status: OK, data: encoder2.encode(path) };
1616
+ } catch {
1617
+ return { status: ENOENT, data: null };
1618
+ }
1619
+ }
1620
+ async rmdir(path, flags = 0) {
1621
+ path = this.normalizePath(path);
1622
+ if (path === "/") return { status: EINVAL, data: null };
1623
+ const recursive = (flags & 1) !== 0;
1624
+ const nav = await this.navigateToParent(path);
1625
+ if (!nav) return { status: ENOENT, data: null };
1626
+ try {
1627
+ await nav.dir.getDirectoryHandle(nav.name);
1628
+ await nav.dir.removeEntry(nav.name, { recursive });
1629
+ return { status: OK, data: null };
1630
+ } catch (err) {
1631
+ if (err.name === "InvalidModificationError") return { status: ENOTEMPTY, data: null };
1632
+ return { status: ENOENT, data: null };
1633
+ }
1634
+ }
1635
+ async readdir(path, flags = 0) {
1636
+ path = this.normalizePath(path);
1637
+ const dir = await this.navigateToDir(path);
1638
+ if (!dir) return { status: ENOENT, data: null };
1639
+ const withFileTypes = (flags & 1) !== 0;
1640
+ const entries = [];
1641
+ for await (const [name, handle] of dir.entries()) {
1642
+ entries.push({ name, kind: handle.kind });
1643
+ }
1644
+ if (withFileTypes) {
1645
+ let totalSize2 = 4;
1646
+ const encoded = [];
1647
+ for (const e of entries) {
1648
+ const nameBytes = encoder2.encode(e.name);
1649
+ encoded.push({ nameBytes, type: e.kind === "file" ? TYPE_FILE : TYPE_DIRECTORY });
1650
+ totalSize2 += 2 + nameBytes.byteLength + 1;
1651
+ }
1652
+ const buf2 = new Uint8Array(totalSize2);
1653
+ const view2 = new DataView(buf2.buffer);
1654
+ view2.setUint32(0, encoded.length, true);
1655
+ let offset2 = 4;
1656
+ for (const e of encoded) {
1657
+ view2.setUint16(offset2, e.nameBytes.byteLength, true);
1658
+ offset2 += 2;
1659
+ buf2.set(e.nameBytes, offset2);
1660
+ offset2 += e.nameBytes.byteLength;
1661
+ buf2[offset2++] = e.type;
1662
+ }
1663
+ return { status: OK, data: buf2 };
1664
+ }
1665
+ let totalSize = 4;
1666
+ const nameEntries = [];
1667
+ for (const e of entries) {
1668
+ const nameBytes = encoder2.encode(e.name);
1669
+ nameEntries.push(nameBytes);
1670
+ totalSize += 2 + nameBytes.byteLength;
1671
+ }
1672
+ const buf = new Uint8Array(totalSize);
1673
+ const view = new DataView(buf.buffer);
1674
+ view.setUint32(0, nameEntries.length, true);
1675
+ let offset = 4;
1676
+ for (const nameBytes of nameEntries) {
1677
+ view.setUint16(offset, nameBytes.byteLength, true);
1678
+ offset += 2;
1679
+ buf.set(nameBytes, offset);
1680
+ offset += nameBytes.byteLength;
1681
+ }
1682
+ return { status: OK, data: buf };
1683
+ }
1684
+ async rename(oldPath, newPath) {
1685
+ oldPath = this.normalizePath(oldPath);
1686
+ newPath = this.normalizePath(newPath);
1687
+ const entry = await this.getEntry(oldPath);
1688
+ if (!entry) return { status: ENOENT, data: null };
1689
+ if (entry.kind === "file") {
1690
+ const fh = entry.handle;
1691
+ const file = await fh.getFile();
1692
+ const data = new Uint8Array(await file.arrayBuffer());
1693
+ const writeResult = await this.write(newPath, data);
1694
+ if (writeResult.status !== OK) return writeResult;
1695
+ await this.unlink(oldPath);
1696
+ } else {
1697
+ await this.mkdir(newPath, 1);
1698
+ await this.copyDirectoryContents(oldPath, newPath);
1699
+ await this.rmdir(oldPath, 1);
1700
+ }
1701
+ return { status: OK, data: null };
1702
+ }
1703
+ async copyDirectoryContents(srcPath, dstPath) {
1704
+ const srcDir = await this.navigateToDir(srcPath);
1705
+ if (!srcDir) return;
1706
+ for await (const [name, handle] of srcDir.entries()) {
1707
+ const srcChild = srcPath === "/" ? `/${name}` : `${srcPath}/${name}`;
1708
+ const dstChild = dstPath === "/" ? `/${name}` : `${dstPath}/${name}`;
1709
+ if (handle.kind === "directory") {
1710
+ await this.mkdir(dstChild, 1);
1711
+ await this.copyDirectoryContents(srcChild, dstChild);
1712
+ } else {
1713
+ const file = await handle.getFile();
1714
+ const data = new Uint8Array(await file.arrayBuffer());
1715
+ await this.write(dstChild, data);
1716
+ }
1717
+ }
1718
+ }
1719
+ async exists(path) {
1720
+ path = this.normalizePath(path);
1721
+ const entry = await this.getEntry(path);
1722
+ return { status: OK, data: new Uint8Array([entry ? 1 : 0]) };
1723
+ }
1724
+ async truncate(path, len) {
1725
+ path = this.normalizePath(path);
1726
+ const nav = await this.navigateToParent(path);
1727
+ if (!nav) return { status: ENOENT, data: null };
1728
+ try {
1729
+ const fh = await nav.dir.getFileHandle(nav.name);
1730
+ const sh = await fh.createSyncAccessHandle();
1731
+ try {
1732
+ sh.truncate(len);
1733
+ sh.flush();
1734
+ } finally {
1735
+ sh.close();
1736
+ }
1737
+ return { status: OK, data: null };
1738
+ } catch {
1739
+ return { status: ENOENT, data: null };
1740
+ }
1741
+ }
1742
+ async copy(src, dest, _flags) {
1743
+ src = this.normalizePath(src);
1744
+ dest = this.normalizePath(dest);
1745
+ const readResult = await this.read(src);
1746
+ if (readResult.status !== OK) return readResult;
1747
+ return this.write(dest, readResult.data ?? new Uint8Array(0));
1748
+ }
1749
+ async access(path, _mode) {
1750
+ path = this.normalizePath(path);
1751
+ const entry = await this.getEntry(path);
1752
+ if (!entry) return { status: ENOENT, data: null };
1753
+ return { status: OK, data: null };
1754
+ }
1755
+ async realpath(path) {
1756
+ path = this.normalizePath(path);
1757
+ const entry = await this.getEntry(path);
1758
+ if (!entry) return { status: ENOENT, data: null };
1759
+ return { status: OK, data: encoder2.encode(path) };
1760
+ }
1761
+ // OPFS doesn't support permissions — these are no-ops
1762
+ async chmod(path, _mode) {
1763
+ path = this.normalizePath(path);
1764
+ const entry = await this.getEntry(path);
1765
+ if (!entry) return { status: ENOENT, data: null };
1766
+ return { status: OK, data: null };
1767
+ }
1768
+ async chown(path, _uid, _gid) {
1769
+ path = this.normalizePath(path);
1770
+ const entry = await this.getEntry(path);
1771
+ if (!entry) return { status: ENOENT, data: null };
1772
+ return { status: OK, data: null };
1773
+ }
1774
+ async utimes(path, _atime, _mtime) {
1775
+ path = this.normalizePath(path);
1776
+ const entry = await this.getEntry(path);
1777
+ if (!entry) return { status: ENOENT, data: null };
1778
+ return { status: OK, data: null };
1779
+ }
1780
+ // OPFS has no symlinks or hard links
1781
+ async symlink(_target, _linkPath) {
1782
+ return { status: EINVAL, data: null };
1783
+ }
1784
+ async readlink(_path) {
1785
+ return { status: EINVAL, data: null };
1786
+ }
1787
+ async link(existingPath, newPath) {
1788
+ return this.copy(existingPath, newPath);
1789
+ }
1790
+ // ========== File descriptor operations ==========
1791
+ async open(path, flags, _tabId) {
1792
+ path = this.normalizePath(path);
1793
+ const hasCreate = (flags & 64) !== 0;
1794
+ const hasTrunc = (flags & 512) !== 0;
1795
+ const hasExcl = (flags & 128) !== 0;
1796
+ const parentDir = await this.ensureParent(path);
1797
+ if (!parentDir) return { status: ENOENT, data: null };
1798
+ const name = path.split("/").filter(Boolean).pop();
1799
+ try {
1800
+ let exists = true;
1801
+ try {
1802
+ await parentDir.getFileHandle(name);
1803
+ } catch {
1804
+ exists = false;
1805
+ }
1806
+ if (!exists && !hasCreate) return { status: ENOENT, data: null };
1807
+ if (exists && hasExcl && hasCreate) return { status: EEXIST, data: null };
1808
+ const fh = await parentDir.getFileHandle(name, { create: hasCreate });
1809
+ const sh = await fh.createSyncAccessHandle();
1810
+ if (hasTrunc) {
1811
+ sh.truncate(0);
1812
+ sh.flush();
1813
+ }
1814
+ const fd = this.nextFd++;
1815
+ this.fdTable.set(fd, { handle: sh, path, position: 0, flags });
1816
+ const buf = new Uint8Array(4);
1817
+ new DataView(buf.buffer).setUint32(0, fd, true);
1818
+ return { status: OK, data: buf };
1819
+ } catch {
1820
+ return { status: ENOENT, data: null };
1821
+ }
1822
+ }
1823
+ async close(fd) {
1824
+ const entry = this.fdTable.get(fd);
1825
+ if (!entry) return { status: EBADF, data: null };
1826
+ try {
1827
+ entry.handle.close();
1828
+ } catch {
1829
+ }
1830
+ this.fdTable.delete(fd);
1831
+ return { status: OK, data: null };
1832
+ }
1833
+ async fread(fd, length, position) {
1834
+ const entry = this.fdTable.get(fd);
1835
+ if (!entry) return { status: EBADF, data: null };
1836
+ const pos = position ?? entry.position;
1837
+ const size = entry.handle.getSize();
1838
+ const readLen = Math.min(length, size - pos);
1839
+ if (readLen <= 0) return { status: OK, data: new Uint8Array(0) };
1840
+ const buf = new Uint8Array(readLen);
1841
+ entry.handle.read(buf, { at: pos });
1842
+ if (position === null) {
1843
+ entry.position += readLen;
1844
+ }
1845
+ return { status: OK, data: buf };
1846
+ }
1847
+ async fwrite(fd, data, position) {
1848
+ const entry = this.fdTable.get(fd);
1849
+ if (!entry) return { status: EBADF, data: null };
1850
+ const isAppend = (entry.flags & 1024) !== 0;
1851
+ const pos = isAppend ? entry.handle.getSize() : position ?? entry.position;
1852
+ entry.handle.write(data, { at: pos });
1853
+ if (position === null) {
1854
+ entry.position = pos + data.byteLength;
1855
+ }
1856
+ const buf = new Uint8Array(4);
1857
+ new DataView(buf.buffer).setUint32(0, data.byteLength, true);
1858
+ return { status: OK, data: buf };
1859
+ }
1860
+ async fstat(fd) {
1861
+ const entry = this.fdTable.get(fd);
1862
+ if (!entry) return { status: EBADF, data: null };
1863
+ const size = entry.handle.getSize();
1864
+ return { status: OK, data: this.encodeStat("file", size, Date.now(), fd) };
1865
+ }
1866
+ async ftruncate(fd, len = 0) {
1867
+ const entry = this.fdTable.get(fd);
1868
+ if (!entry) return { status: EBADF, data: null };
1869
+ entry.handle.truncate(len);
1870
+ entry.handle.flush();
1871
+ return { status: OK, data: null };
1872
+ }
1873
+ async fsync() {
1874
+ for (const [, entry] of this.fdTable) {
1875
+ try {
1876
+ entry.handle.flush();
1877
+ } catch {
1878
+ }
1879
+ }
1880
+ return { status: OK, data: null };
1881
+ }
1882
+ async opendir(path, _tabId) {
1883
+ return this.readdir(path, 1);
1884
+ }
1885
+ async mkdtemp(prefix) {
1886
+ const random = Math.random().toString(36).substring(2, 8);
1887
+ const path = this.normalizePath(prefix + random);
1888
+ return this.mkdir(path, 1);
1889
+ }
1890
+ };
1891
+
1367
1892
  // src/protocol/opcodes.ts
1368
1893
  var OP = {
1369
1894
  READ: 1,
@@ -1422,7 +1947,7 @@ var SIGNAL = {
1422
1947
  CHUNK: 3,
1423
1948
  CHUNK_ACK: 4
1424
1949
  };
1425
- var encoder2 = new TextEncoder();
1950
+ var encoder3 = new TextEncoder();
1426
1951
  var decoder2 = new TextDecoder();
1427
1952
  function decodeRequest(buf) {
1428
1953
  const view = new DataView(buf);
@@ -1454,6 +1979,8 @@ function decodeSecondPath(data) {
1454
1979
 
1455
1980
  // src/workers/sync-relay.worker.ts
1456
1981
  var engine = new VFSEngine();
1982
+ var opfsEngine = null;
1983
+ var opfsMode = false;
1457
1984
  var leaderInitialized = false;
1458
1985
  var readySent = false;
1459
1986
  var debug = false;
@@ -1481,7 +2008,7 @@ function yieldToEventLoop() {
1481
2008
  });
1482
2009
  }
1483
2010
  function registerClientPort(clientTabId, port) {
1484
- port.onmessage = (e) => {
2011
+ port.onmessage = async (e) => {
1485
2012
  if (e.data.buffer instanceof ArrayBuffer) {
1486
2013
  if (leaderLoopRunning) {
1487
2014
  portQueue.push({
@@ -1491,10 +2018,10 @@ function registerClientPort(clientTabId, port) {
1491
2018
  buffer: e.data.buffer
1492
2019
  });
1493
2020
  } else {
1494
- const result = handleRequest(clientTabId, e.data.buffer);
2021
+ const result = opfsMode ? await handleRequestOPFS(clientTabId, e.data.buffer) : handleRequest(clientTabId, e.data.buffer);
1495
2022
  const response = encodeResponse(result.status, result.data);
1496
2023
  port.postMessage({ id: e.data.id, buffer: response }, [response]);
1497
- if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
2024
+ if (!opfsMode && result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
1498
2025
  }
1499
2026
  }
1500
2027
  };
@@ -1507,7 +2034,11 @@ function removeClientPort(clientTabId) {
1507
2034
  port.close();
1508
2035
  clientPorts.delete(clientTabId);
1509
2036
  }
1510
- engine.cleanupTab(clientTabId);
2037
+ if (opfsMode) {
2038
+ opfsEngine?.cleanupTab(clientTabId);
2039
+ } else {
2040
+ engine.cleanupTab(clientTabId);
2041
+ }
1511
2042
  }
1512
2043
  function drainPortQueue() {
1513
2044
  while (portQueue.length > 0) {
@@ -1518,6 +2049,14 @@ function drainPortQueue() {
1518
2049
  if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
1519
2050
  }
1520
2051
  }
2052
+ async function drainPortQueueAsync() {
2053
+ while (portQueue.length > 0) {
2054
+ const msg = portQueue.shift();
2055
+ const result = await handleRequestOPFS(msg.tabId, msg.buffer);
2056
+ const response = encodeResponse(result.status, result.data);
2057
+ msg.port.postMessage({ id: msg.id, buffer: response }, [response]);
2058
+ }
2059
+ }
1521
2060
  var leaderPort = null;
1522
2061
  var pendingResolve = null;
1523
2062
  var asyncRelayPort = null;
@@ -1815,6 +2354,178 @@ function handleRequest(reqTabId, buffer) {
1815
2354
  }
1816
2355
  return ret;
1817
2356
  }
2357
+ async function handleRequestOPFS(reqTabId, buffer) {
2358
+ const oe = opfsEngine;
2359
+ const { op, flags, path, data } = decodeRequest(buffer);
2360
+ let result;
2361
+ let syncPath;
2362
+ let syncNewPath;
2363
+ switch (op) {
2364
+ case OP.READ:
2365
+ result = await oe.read(path);
2366
+ break;
2367
+ case OP.WRITE:
2368
+ result = await oe.write(path, data ?? new Uint8Array(0), flags);
2369
+ syncPath = path;
2370
+ break;
2371
+ case OP.APPEND:
2372
+ result = await oe.append(path, data ?? new Uint8Array(0));
2373
+ syncPath = path;
2374
+ break;
2375
+ case OP.UNLINK:
2376
+ result = await oe.unlink(path);
2377
+ syncPath = path;
2378
+ break;
2379
+ case OP.STAT:
2380
+ result = await oe.stat(path);
2381
+ break;
2382
+ case OP.LSTAT:
2383
+ result = await oe.lstat(path);
2384
+ break;
2385
+ case OP.MKDIR:
2386
+ result = await oe.mkdir(path, flags);
2387
+ syncPath = path;
2388
+ break;
2389
+ case OP.RMDIR:
2390
+ result = await oe.rmdir(path, flags);
2391
+ syncPath = path;
2392
+ break;
2393
+ case OP.READDIR:
2394
+ result = await oe.readdir(path, flags);
2395
+ break;
2396
+ case OP.RENAME: {
2397
+ const newPath = data ? decodeSecondPath(data) : "";
2398
+ result = await oe.rename(path, newPath);
2399
+ syncPath = path;
2400
+ syncNewPath = newPath;
2401
+ break;
2402
+ }
2403
+ case OP.EXISTS:
2404
+ result = await oe.exists(path);
2405
+ break;
2406
+ case OP.TRUNCATE: {
2407
+ const len = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
2408
+ result = await oe.truncate(path, len);
2409
+ syncPath = path;
2410
+ break;
2411
+ }
2412
+ case OP.COPY: {
2413
+ const destPath = data ? decodeSecondPath(data) : "";
2414
+ result = await oe.copy(path, destPath, flags);
2415
+ syncPath = destPath;
2416
+ break;
2417
+ }
2418
+ case OP.ACCESS:
2419
+ result = await oe.access(path, flags);
2420
+ break;
2421
+ case OP.REALPATH:
2422
+ result = await oe.realpath(path);
2423
+ break;
2424
+ case OP.CHMOD: {
2425
+ const chmodMode = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
2426
+ result = await oe.chmod(path, chmodMode);
2427
+ break;
2428
+ }
2429
+ case OP.CHOWN: {
2430
+ if (!data || data.byteLength < 8) {
2431
+ result = { status: 7 };
2432
+ break;
2433
+ }
2434
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
2435
+ result = await oe.chown(path, dv.getUint32(0, true), dv.getUint32(4, true));
2436
+ break;
2437
+ }
2438
+ case OP.UTIMES: {
2439
+ if (!data || data.byteLength < 16) {
2440
+ result = { status: 7 };
2441
+ break;
2442
+ }
2443
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
2444
+ result = await oe.utimes(path, dv.getFloat64(0, true), dv.getFloat64(8, true));
2445
+ break;
2446
+ }
2447
+ case OP.SYMLINK: {
2448
+ const target = data ? new TextDecoder().decode(data) : "";
2449
+ result = await oe.symlink(target, path);
2450
+ break;
2451
+ }
2452
+ case OP.READLINK:
2453
+ result = await oe.readlink(path);
2454
+ break;
2455
+ case OP.LINK: {
2456
+ const newPath = data ? decodeSecondPath(data) : "";
2457
+ result = await oe.link(path, newPath);
2458
+ syncPath = newPath;
2459
+ break;
2460
+ }
2461
+ case OP.OPEN:
2462
+ result = await oe.open(path, flags, reqTabId);
2463
+ break;
2464
+ case OP.CLOSE: {
2465
+ const fd = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
2466
+ result = await oe.close(fd);
2467
+ break;
2468
+ }
2469
+ case OP.FREAD: {
2470
+ if (!data || data.byteLength < 12) {
2471
+ result = { status: 7 };
2472
+ break;
2473
+ }
2474
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
2475
+ result = await oe.fread(dv.getUint32(0, true), dv.getUint32(4, true), dv.getInt32(8, true) === -1 ? null : dv.getInt32(8, true));
2476
+ break;
2477
+ }
2478
+ case OP.FWRITE: {
2479
+ if (!data || data.byteLength < 8) {
2480
+ result = { status: 7 };
2481
+ break;
2482
+ }
2483
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
2484
+ const fd = dv.getUint32(0, true);
2485
+ const pos = dv.getInt32(4, true);
2486
+ result = await oe.fwrite(fd, data.subarray(8), pos === -1 ? null : pos);
2487
+ syncPath = oe.getPathForFd(fd) ?? void 0;
2488
+ break;
2489
+ }
2490
+ case OP.FSTAT: {
2491
+ const fd = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
2492
+ result = await oe.fstat(fd);
2493
+ break;
2494
+ }
2495
+ case OP.FTRUNCATE: {
2496
+ if (!data || data.byteLength < 8) {
2497
+ result = { status: 7 };
2498
+ break;
2499
+ }
2500
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
2501
+ result = await oe.ftruncate(dv.getUint32(0, true), dv.getUint32(4, true));
2502
+ syncPath = oe.getPathForFd(dv.getUint32(0, true)) ?? void 0;
2503
+ break;
2504
+ }
2505
+ case OP.FSYNC:
2506
+ result = await oe.fsync();
2507
+ break;
2508
+ case OP.OPENDIR:
2509
+ result = await oe.opendir(path, reqTabId);
2510
+ break;
2511
+ case OP.MKDTEMP:
2512
+ result = await oe.mkdtemp(path);
2513
+ if (result.status === 0 && result.data) {
2514
+ syncPath = new TextDecoder().decode(result.data instanceof Uint8Array ? result.data : new Uint8Array(0));
2515
+ }
2516
+ break;
2517
+ default:
2518
+ result = { status: 7 };
2519
+ }
2520
+ const ret = {
2521
+ status: result.status,
2522
+ data: result.data instanceof Uint8Array ? result.data : void 0
2523
+ };
2524
+ if (result.status === 0 && syncPath) {
2525
+ broadcastWatch(op, syncPath, syncNewPath);
2526
+ }
2527
+ return ret;
2528
+ }
1818
2529
  function readPayload(targetSab, targetCtrl) {
1819
2530
  const totalLenView = new BigUint64Array(targetSab, SAB_OFFSETS.TOTAL_LEN, 1);
1820
2531
  const maxChunk = targetSab.byteLength - HEADER_SIZE;
@@ -1943,6 +2654,54 @@ async function leaderLoop() {
1943
2654
  }
1944
2655
  }
1945
2656
  }
2657
+ async function leaderLoopOPFS() {
2658
+ leaderLoopRunning = true;
2659
+ while (true) {
2660
+ let processed = true;
2661
+ let tightOps = 0;
2662
+ while (processed) {
2663
+ processed = false;
2664
+ if (++tightOps >= 100) {
2665
+ tightOps = 0;
2666
+ await yieldToEventLoop();
2667
+ }
2668
+ if (Atomics.load(ctrl, 0) === SIGNAL.REQUEST) {
2669
+ const payload = readPayload(sab, ctrl);
2670
+ const reqResult = await handleRequestOPFS(tabId, payload.buffer);
2671
+ writeDirectResponse(sab, ctrl, reqResult.status, reqResult.data);
2672
+ const waitResult = Atomics.wait(ctrl, 0, SIGNAL.RESPONSE, 10);
2673
+ if (waitResult === "timed-out") {
2674
+ Atomics.store(ctrl, 0, SIGNAL.IDLE);
2675
+ }
2676
+ processed = true;
2677
+ continue;
2678
+ }
2679
+ if (asyncCtrl && Atomics.load(asyncCtrl, 0) === SIGNAL.REQUEST) {
2680
+ const payload = readPayload(asyncSab, asyncCtrl);
2681
+ const asyncResult = await handleRequestOPFS(tabId, payload.buffer);
2682
+ writeDirectResponse(asyncSab, asyncCtrl, asyncResult.status, asyncResult.data);
2683
+ const waitResult = Atomics.wait(asyncCtrl, 0, SIGNAL.RESPONSE, 10);
2684
+ if (waitResult === "timed-out") {
2685
+ Atomics.store(asyncCtrl, 0, SIGNAL.IDLE);
2686
+ }
2687
+ processed = true;
2688
+ continue;
2689
+ }
2690
+ if (portQueue.length > 0) {
2691
+ await drainPortQueueAsync();
2692
+ processed = true;
2693
+ continue;
2694
+ }
2695
+ }
2696
+ await yieldToEventLoop();
2697
+ if (clientPorts.size === 0) {
2698
+ const currentSignal = Atomics.load(ctrl, 0);
2699
+ if (currentSignal !== SIGNAL.REQUEST) {
2700
+ Atomics.wait(ctrl, 0, currentSignal, 50);
2701
+ }
2702
+ }
2703
+ }
2704
+ }
1946
2705
  async function followerLoop() {
1947
2706
  while (true) {
1948
2707
  if (Atomics.load(ctrl, 0) === SIGNAL.REQUEST) {
@@ -2012,6 +2771,23 @@ async function initEngine(config) {
2012
2771
  }
2013
2772
  watchBc = new BroadcastChannel(`${config.ns}-watch`);
2014
2773
  }
2774
+ async function initOPFSEngine(config) {
2775
+ debug = config.debug ?? false;
2776
+ opfsMode = true;
2777
+ let rootDir = await navigator.storage.getDirectory();
2778
+ if (config.root && config.root !== "/") {
2779
+ const segments = config.root.split("/").filter(Boolean);
2780
+ for (const segment of segments) {
2781
+ rootDir = await rootDir.getDirectoryHandle(segment, { create: true });
2782
+ }
2783
+ }
2784
+ opfsEngine = new OPFSEngine();
2785
+ await opfsEngine.init(rootDir, {
2786
+ uid: config.uid,
2787
+ gid: config.gid
2788
+ });
2789
+ watchBc = new BroadcastChannel(`${config.ns}-watch`);
2790
+ }
2015
2791
  function broadcastWatch(op, path, newPath) {
2016
2792
  if (!watchBc) return;
2017
2793
  let eventType;
@@ -2136,13 +2912,13 @@ self.onmessage = async (e) => {
2136
2912
  const port = msg.port ?? e.ports[0];
2137
2913
  if (port) {
2138
2914
  asyncRelayPort = port;
2139
- port.onmessage = (ev) => {
2915
+ port.onmessage = async (ev) => {
2140
2916
  if (ev.data.buffer instanceof ArrayBuffer) {
2141
2917
  if (leaderInitialized) {
2142
- const result = handleRequest(tabId || "nosab", ev.data.buffer);
2918
+ const result = opfsMode ? await handleRequestOPFS(tabId || "nosab", ev.data.buffer) : handleRequest(tabId || "nosab", ev.data.buffer);
2143
2919
  const response = encodeResponse(result.status, result.data);
2144
2920
  port.postMessage({ id: ev.data.id, buffer: response }, [response]);
2145
- if (result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
2921
+ if (!opfsMode && result._op !== void 0) notifyOPFSSync(result._op, result._path, result._newPath);
2146
2922
  } else if (leaderPort) {
2147
2923
  const buf = ev.data.buffer;
2148
2924
  leaderPort.postMessage({ id: ev.data.id, tabId, buffer: buf }, [buf]);
@@ -2191,6 +2967,44 @@ self.onmessage = async (e) => {
2191
2967
  }
2192
2968
  return;
2193
2969
  }
2970
+ if (msg.type === "init-opfs") {
2971
+ leaderInitialized = true;
2972
+ readySent = false;
2973
+ tabId = msg.tabId;
2974
+ const hasSAB = msg.sab != null;
2975
+ if (hasSAB) {
2976
+ sab = msg.sab;
2977
+ readySab = msg.readySab;
2978
+ ctrl = new Int32Array(sab, 0, 8);
2979
+ readySignal = new Int32Array(readySab, 0, 1);
2980
+ }
2981
+ if (msg.asyncSab) {
2982
+ asyncSab = msg.asyncSab;
2983
+ asyncCtrl = new Int32Array(msg.asyncSab, 0, 8);
2984
+ }
2985
+ try {
2986
+ await initOPFSEngine(msg.config);
2987
+ } catch (err) {
2988
+ leaderInitialized = false;
2989
+ self.postMessage({
2990
+ type: "init-failed",
2991
+ error: err.message
2992
+ });
2993
+ return;
2994
+ }
2995
+ if (!readySent) {
2996
+ readySent = true;
2997
+ if (hasSAB) {
2998
+ Atomics.store(readySignal, 0, 1);
2999
+ Atomics.notify(readySignal, 0);
3000
+ }
3001
+ self.postMessage({ type: "ready", mode: "opfs" });
3002
+ }
3003
+ if (hasSAB) {
3004
+ leaderLoopOPFS();
3005
+ }
3006
+ return;
3007
+ }
2194
3008
  if (msg.type === "init-follower") {
2195
3009
  tabId = msg.tabId;
2196
3010
  const hasSAB = msg.sab != null;