@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.
- package/README.md +102 -10
- package/dist/index.js +227 -188
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +1678 -0
- package/dist/workers/repair.worker.js.map +1 -0
- package/dist/workers/server.worker.js +34 -6
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +828 -14
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
360
|
-
pathLength
|
|
379
|
+
pathOffset,
|
|
380
|
+
pathLength,
|
|
361
381
|
mode: inodeView.getUint32(off + INODE.MODE, true),
|
|
362
|
-
size
|
|
363
|
-
firstBlock
|
|
364
|
-
blockCount
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|