@componentor/fs 3.0.9 → 3.0.11

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/dist/index.js CHANGED
@@ -1304,10 +1304,14 @@ var VFSFileSystem = class {
1304
1304
  // Ready promise for async callers
1305
1305
  readyPromise;
1306
1306
  resolveReady;
1307
+ rejectReady;
1308
+ initError = null;
1307
1309
  isReady = false;
1308
1310
  // Config (definite assignment — always set when constructor doesn't return singleton)
1309
1311
  config;
1310
1312
  tabId;
1313
+ _mode;
1314
+ corruptionError = null;
1311
1315
  /** Namespace string derived from root — used for lock names, BroadcastChannel, and SW scope
1312
1316
  * so multiple VFS instances with different roots don't collide. */
1313
1317
  ns;
@@ -1327,9 +1331,12 @@ var VFSFileSystem = class {
1327
1331
  const ns = `vfs-${root.replace(/[^a-zA-Z0-9]/g, "_")}`;
1328
1332
  const existing = instanceRegistry.get(ns);
1329
1333
  if (existing) return existing;
1334
+ const mode = config.mode ?? "hybrid";
1335
+ this._mode = mode;
1336
+ const opfsSync = config.opfsSync ?? mode === "hybrid";
1330
1337
  this.config = {
1331
1338
  root,
1332
- opfsSync: config.opfsSync ?? true,
1339
+ opfsSync,
1333
1340
  opfsSyncRoot: config.opfsSyncRoot,
1334
1341
  uid: config.uid ?? 0,
1335
1342
  gid: config.gid ?? 0,
@@ -1341,8 +1348,9 @@ var VFSFileSystem = class {
1341
1348
  };
1342
1349
  this.tabId = crypto.randomUUID();
1343
1350
  this.ns = ns;
1344
- this.readyPromise = new Promise((resolve2) => {
1351
+ this.readyPromise = new Promise((resolve2, reject) => {
1345
1352
  this.resolveReady = resolve2;
1353
+ this.rejectReady = reject;
1346
1354
  });
1347
1355
  this.promises = new VFSPromises(this._async, ns);
1348
1356
  instanceRegistry.set(ns, this);
@@ -1369,7 +1377,9 @@ var VFSFileSystem = class {
1369
1377
  this.initLeaderBroker();
1370
1378
  }
1371
1379
  } else if (msg.type === "init-failed") {
1372
- if (this.holdingLeaderLock) {
1380
+ if (msg.error?.startsWith("Corrupt VFS:")) {
1381
+ this.handleCorruptVFS(msg.error);
1382
+ } else if (this.holdingLeaderLock) {
1373
1383
  setTimeout(() => this.sendLeaderInit(), 500);
1374
1384
  } else if (!("locks" in navigator)) {
1375
1385
  this.startAsFollower();
@@ -1460,10 +1470,50 @@ var VFSFileSystem = class {
1460
1470
  }
1461
1471
  });
1462
1472
  }
1473
+ /** Send init-opfs message to sync-relay for OPFS-direct mode */
1474
+ sendOPFSInit() {
1475
+ this.syncWorker.postMessage({
1476
+ type: "init-opfs",
1477
+ sab: this.hasSAB ? this.sab : null,
1478
+ readySab: this.hasSAB ? this.readySab : null,
1479
+ asyncSab: this.hasSAB ? this.asyncSab : null,
1480
+ tabId: this.tabId,
1481
+ config: {
1482
+ root: this.config.root,
1483
+ ns: this.ns,
1484
+ uid: this.config.uid,
1485
+ gid: this.config.gid,
1486
+ debug: this.config.debug
1487
+ }
1488
+ });
1489
+ }
1490
+ /** Handle VFS corruption: log error, fall back to OPFS-direct mode.
1491
+ * The readyPromise will resolve once OPFS mode is ready, but init()
1492
+ * will reject with the corruption error to inform the caller. */
1493
+ handleCorruptVFS(errorMessage) {
1494
+ const err = new Error(`${errorMessage} \u2014 Falling back to OPFS mode`);
1495
+ this.corruptionError = err;
1496
+ console.error(`[VFS] ${err.message}`);
1497
+ if (this._mode === "vfs") {
1498
+ this.initError = err;
1499
+ this.rejectReady(err);
1500
+ if (this.hasSAB) {
1501
+ Atomics.store(this.readySignal, 0, -1);
1502
+ Atomics.notify(this.readySignal, 0);
1503
+ }
1504
+ return;
1505
+ }
1506
+ this._mode = "opfs";
1507
+ this.sendOPFSInit();
1508
+ }
1463
1509
  /** Start as leader — tell sync-relay to init VFS engine + OPFS handle */
1464
1510
  startAsLeader() {
1465
1511
  this.isFollower = false;
1466
- this.sendLeaderInit();
1512
+ if (this._mode === "opfs") {
1513
+ this.sendOPFSInit();
1514
+ } else {
1515
+ this.sendLeaderInit();
1516
+ }
1467
1517
  }
1468
1518
  /** Start as follower — connect to leader via service worker port brokering */
1469
1519
  startAsFollower() {
@@ -1564,8 +1614,9 @@ var VFSFileSystem = class {
1564
1614
  this.leaderChangeBc.close();
1565
1615
  this.leaderChangeBc = null;
1566
1616
  }
1567
- this.readyPromise = new Promise((resolve2) => {
1617
+ this.readyPromise = new Promise((resolve2, reject) => {
1568
1618
  this.resolveReady = resolve2;
1619
+ this.rejectReady = reject;
1569
1620
  });
1570
1621
  this.syncWorker.terminate();
1571
1622
  this.asyncWorker.terminate();
@@ -1586,8 +1637,12 @@ var VFSFileSystem = class {
1586
1637
  this.resolveReady();
1587
1638
  this.initLeaderBroker();
1588
1639
  } else if (msg.type === "init-failed") {
1589
- console.warn("[VFS] Promotion: OPFS handle still busy, retrying...");
1590
- setTimeout(() => this.sendLeaderInit(), 500);
1640
+ if (msg.error?.startsWith("Corrupt VFS:")) {
1641
+ this.handleCorruptVFS(msg.error);
1642
+ } else {
1643
+ console.warn("[VFS] Promotion: OPFS handle still busy, retrying...");
1644
+ setTimeout(() => this.sendLeaderInit(), 500);
1645
+ }
1591
1646
  }
1592
1647
  };
1593
1648
  this.asyncWorker.onmessage = (e) => {
@@ -1617,7 +1672,11 @@ var VFSFileSystem = class {
1617
1672
  [mc.port2]
1618
1673
  );
1619
1674
  }
1620
- this.sendLeaderInit();
1675
+ if (this._mode === "opfs") {
1676
+ this.sendOPFSInit();
1677
+ } else {
1678
+ this.sendLeaderInit();
1679
+ }
1621
1680
  }
1622
1681
  /** Spawn an inline worker from bundled code */
1623
1682
  spawnWorker(name) {
@@ -1628,14 +1687,23 @@ var VFSFileSystem = class {
1628
1687
  /** Block until workers are ready */
1629
1688
  ensureReady() {
1630
1689
  if (this.isReady) return;
1690
+ if (this.initError) throw this.initError;
1631
1691
  if (!this.hasSAB) {
1632
1692
  throw new Error("Sync API requires crossOriginIsolated (COOP/COEP headers). Use the promises API instead.");
1633
1693
  }
1634
- if (Atomics.load(this.readySignal, 0) === 1) {
1694
+ const signal = Atomics.load(this.readySignal, 0);
1695
+ if (signal === 1) {
1635
1696
  this.isReady = true;
1636
1697
  return;
1637
1698
  }
1699
+ if (signal === -1) {
1700
+ throw this.initError ?? new Error("VFS initialization failed");
1701
+ }
1638
1702
  spinWait(this.readySignal, 0, 0);
1703
+ const finalSignal = Atomics.load(this.readySignal, 0);
1704
+ if (finalSignal === -1) {
1705
+ throw this.initError ?? new Error("VFS initialization failed");
1706
+ }
1639
1707
  this.isReady = true;
1640
1708
  }
1641
1709
  /** Send a sync request via SAB and wait for response */
@@ -1886,8 +1954,101 @@ var VFSFileSystem = class {
1886
1954
  }
1887
1955
  purgeSync() {
1888
1956
  }
1889
- /** Async init helper avoid blocking main thread */
1957
+ /** The current filesystem mode. Changes to 'opfs' on corruption fallback. */
1958
+ get mode() {
1959
+ return this._mode;
1960
+ }
1961
+ /** Async init helper — avoid blocking main thread.
1962
+ * Rejects with corruption error if VFS was corrupt (but system falls back to OPFS mode).
1963
+ * Callers can catch and continue — the fs API works in OPFS mode after rejection. */
1890
1964
  init() {
1965
+ return this.readyPromise.then(() => {
1966
+ if (this.corruptionError) {
1967
+ throw this.corruptionError;
1968
+ }
1969
+ });
1970
+ }
1971
+ /** Switch the filesystem mode at runtime.
1972
+ *
1973
+ * Typical flow for IDE corruption recovery:
1974
+ * 1. `await fs.init()` throws with corruption error (auto-falls back to opfs)
1975
+ * 2. IDE shows warning, user clicks "Repair" → call `repairVFS(root, fs)`
1976
+ * 3. After repair: `await fs.setMode('hybrid')` to resume normal VFS+OPFS mode
1977
+ *
1978
+ * Returns a Promise that resolves when the new mode is ready. */
1979
+ async setMode(newMode) {
1980
+ if (newMode === this._mode && this.isReady && !this.corruptionError) {
1981
+ return;
1982
+ }
1983
+ this._mode = newMode;
1984
+ this.corruptionError = null;
1985
+ this.initError = null;
1986
+ this.isReady = false;
1987
+ this.config.opfsSync = newMode === "hybrid";
1988
+ this.readyPromise = new Promise((resolve2, reject) => {
1989
+ this.resolveReady = resolve2;
1990
+ this.rejectReady = reject;
1991
+ });
1992
+ this.syncWorker.terminate();
1993
+ this.asyncWorker.terminate();
1994
+ const sabSize = this.config.sabSize;
1995
+ if (this.hasSAB) {
1996
+ this.sab = new SharedArrayBuffer(sabSize);
1997
+ this.readySab = new SharedArrayBuffer(4);
1998
+ this.asyncSab = new SharedArrayBuffer(sabSize);
1999
+ this.ctrl = new Int32Array(this.sab, 0, 8);
2000
+ this.readySignal = new Int32Array(this.readySab, 0, 1);
2001
+ }
2002
+ this.syncWorker = this.spawnWorker("sync-relay");
2003
+ this.asyncWorker = this.spawnWorker("async-relay");
2004
+ this.syncWorker.onmessage = (e) => {
2005
+ const msg = e.data;
2006
+ if (msg.type === "ready") {
2007
+ this.isReady = true;
2008
+ this.resolveReady();
2009
+ if (!this.isFollower) {
2010
+ this.initLeaderBroker();
2011
+ }
2012
+ } else if (msg.type === "init-failed") {
2013
+ if (msg.error?.startsWith("Corrupt VFS:")) {
2014
+ this.handleCorruptVFS(msg.error);
2015
+ } else if (this.holdingLeaderLock) {
2016
+ setTimeout(() => this.sendLeaderInit(), 500);
2017
+ }
2018
+ }
2019
+ };
2020
+ this.asyncWorker.onmessage = (e) => {
2021
+ const msg = e.data;
2022
+ if (msg.type === "response") {
2023
+ const pending = this.asyncPending.get(msg.callId);
2024
+ if (pending) {
2025
+ this.asyncPending.delete(msg.callId);
2026
+ pending.resolve({ status: msg.status, data: msg.data });
2027
+ }
2028
+ }
2029
+ };
2030
+ if (this.hasSAB) {
2031
+ this.asyncWorker.postMessage({
2032
+ type: "init-leader",
2033
+ asyncSab: this.asyncSab,
2034
+ wakeSab: this.sab
2035
+ });
2036
+ } else {
2037
+ const mc = new MessageChannel();
2038
+ this.asyncWorker.postMessage(
2039
+ { type: "init-port", port: mc.port1 },
2040
+ [mc.port1]
2041
+ );
2042
+ this.syncWorker.postMessage(
2043
+ { type: "async-port", port: mc.port2 },
2044
+ [mc.port2]
2045
+ );
2046
+ }
2047
+ if (newMode === "opfs") {
2048
+ this.sendOPFSInit();
2049
+ } else {
2050
+ this.sendLeaderInit();
2051
+ }
1891
2052
  return this.readyPromise;
1892
2053
  }
1893
2054
  };
@@ -2066,6 +2227,7 @@ var VFSEngine = class {
2066
2227
  this.bitmap = new Uint8Array(layout.bitmapSize);
2067
2228
  this.handle.write(this.bitmap, { at: this.bitmapOffset });
2068
2229
  this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
2230
+ this.writeSuperblock();
2069
2231
  this.handle.flush();
2070
2232
  }
2071
2233
  /** Mount an existing VFS from disk — validates superblock integrity */
@@ -2225,14 +2387,33 @@ var VFSEngine = class {
2225
2387
  const off = i * INODE_SIZE;
2226
2388
  const type = inodeView.getUint8(off + INODE.TYPE);
2227
2389
  if (type === INODE_TYPE.FREE) continue;
2390
+ if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) {
2391
+ throw new Error(`Corrupt VFS: inode ${i} has invalid type ${type}`);
2392
+ }
2393
+ const pathOffset = inodeView.getUint32(off + INODE.PATH_OFFSET, true);
2394
+ const pathLength = inodeView.getUint16(off + INODE.PATH_LENGTH, true);
2395
+ const size = inodeView.getFloat64(off + INODE.SIZE, true);
2396
+ const firstBlock = inodeView.getUint32(off + INODE.FIRST_BLOCK, true);
2397
+ const blockCount = inodeView.getUint32(off + INODE.BLOCK_COUNT, true);
2398
+ if (pathLength === 0 || pathOffset + pathLength > this.pathTableUsed) {
2399
+ throw new Error(`Corrupt VFS: inode ${i} path out of bounds (offset=${pathOffset}, len=${pathLength}, tableUsed=${this.pathTableUsed})`);
2400
+ }
2401
+ if (type !== INODE_TYPE.DIRECTORY) {
2402
+ if (size < 0 || !isFinite(size)) {
2403
+ throw new Error(`Corrupt VFS: inode ${i} has invalid size ${size}`);
2404
+ }
2405
+ if (blockCount > 0 && firstBlock + blockCount > this.totalBlocks) {
2406
+ throw new Error(`Corrupt VFS: inode ${i} data blocks out of range (first=${firstBlock}, count=${blockCount}, total=${this.totalBlocks})`);
2407
+ }
2408
+ }
2228
2409
  const inode = {
2229
2410
  type,
2230
- pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
2231
- pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
2411
+ pathOffset,
2412
+ pathLength,
2232
2413
  mode: inodeView.getUint32(off + INODE.MODE, true),
2233
- size: inodeView.getFloat64(off + INODE.SIZE, true),
2234
- firstBlock: inodeView.getUint32(off + INODE.FIRST_BLOCK, true),
2235
- blockCount: inodeView.getUint32(off + INODE.BLOCK_COUNT, true),
2414
+ size,
2415
+ firstBlock,
2416
+ blockCount,
2236
2417
  mtime: inodeView.getFloat64(off + INODE.MTIME, true),
2237
2418
  ctime: inodeView.getFloat64(off + INODE.CTIME, true),
2238
2419
  atime: inodeView.getFloat64(off + INODE.ATIME, true),
@@ -2240,7 +2421,15 @@ var VFSEngine = class {
2240
2421
  gid: inodeView.getUint32(off + INODE.GID, true)
2241
2422
  };
2242
2423
  this.inodeCache.set(i, inode);
2243
- const path = pathBuf ? decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength)) : this.readPath(inode.pathOffset, inode.pathLength);
2424
+ let path;
2425
+ if (pathBuf) {
2426
+ path = decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength));
2427
+ } else {
2428
+ path = this.readPath(inode.pathOffset, inode.pathLength);
2429
+ }
2430
+ if (!path.startsWith("/") || path.includes("\0")) {
2431
+ throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
2432
+ }
2244
2433
  this.pathIndex.set(path, i);
2245
2434
  }
2246
2435
  }
@@ -3303,19 +3492,6 @@ async function openVFSHandle(fileHandle) {
3303
3492
  return { handle: new MemoryHandle(data), isMemory: true };
3304
3493
  }
3305
3494
  }
3306
- async function openFreshVFSHandle(fileHandle) {
3307
- try {
3308
- const handle = await fileHandle.createSyncAccessHandle();
3309
- return { handle, isMemory: false };
3310
- } catch {
3311
- return { handle: new MemoryHandle(), isMemory: true };
3312
- }
3313
- }
3314
- async function saveMemoryHandle(fileHandle, memHandle) {
3315
- const writable = await fileHandle.createWritable();
3316
- await writable.write(memHandle.getBuffer());
3317
- await writable.close();
3318
- }
3319
3495
  async function navigateToRoot(root) {
3320
3496
  let dir = await navigator.storage.getDirectory();
3321
3497
  if (root && root !== "/") {
@@ -3500,35 +3676,7 @@ async function loadFromOPFS(root = "/", fs) {
3500
3676
  }
3501
3677
  return { files, directories };
3502
3678
  }
3503
- try {
3504
- await rootDir.removeEntry(".vfs.bin");
3505
- } catch (_) {
3506
- }
3507
- const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
3508
- const { handle, isMemory } = await openFreshVFSHandle(vfsFileHandle);
3509
- try {
3510
- const engine = new VFSEngine();
3511
- engine.init(handle);
3512
- const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
3513
- let files = 0;
3514
- let directories = 0;
3515
- for (const dir of dirs) {
3516
- engine.mkdir(dir.path, 16877);
3517
- directories++;
3518
- }
3519
- const fileEntries = opfsEntries.filter((e) => e.type === "file");
3520
- for (const file of fileEntries) {
3521
- engine.write(file.path, new Uint8Array(file.data));
3522
- files++;
3523
- }
3524
- engine.flush();
3525
- if (isMemory) {
3526
- await saveMemoryHandle(vfsFileHandle, handle);
3527
- }
3528
- return { files, directories };
3529
- } finally {
3530
- handle.close();
3531
- }
3679
+ return spawnRepairWorker({ type: "load", root });
3532
3680
  }
3533
3681
  async function repairVFS(root = "/", fs) {
3534
3682
  if (fs) {
@@ -3542,137 +3690,28 @@ async function repairVFS(root = "/", fs) {
3542
3690
  // Detailed entries not available in fs-based path
3543
3691
  };
3544
3692
  }
3545
- return repairVFSRaw(root);
3693
+ return spawnRepairWorker({ type: "repair", root });
3546
3694
  }
3547
- async function repairVFSRaw(root) {
3548
- const rootDir = await navigateToRoot(root);
3549
- const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
3550
- const file = await vfsFileHandle.getFile();
3551
- const raw = new Uint8Array(await file.arrayBuffer());
3552
- const fileSize = raw.byteLength;
3553
- if (fileSize < SUPERBLOCK.SIZE) {
3554
- throw new Error(`VFS file too small to repair (${fileSize} bytes)`);
3555
- }
3556
- const view = new DataView(raw.buffer);
3557
- let inodeCount;
3558
- let blockSize;
3559
- let totalBlocks;
3560
- let inodeTableOffset;
3561
- let pathTableOffset;
3562
- let dataOffset;
3563
- const magic = view.getUint32(SUPERBLOCK.MAGIC, true);
3564
- const version = view.getUint32(SUPERBLOCK.VERSION, true);
3565
- const superblockValid = magic === VFS_MAGIC && version === VFS_VERSION;
3566
- if (superblockValid) {
3567
- inodeCount = view.getUint32(SUPERBLOCK.INODE_COUNT, true);
3568
- blockSize = view.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
3569
- totalBlocks = view.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
3570
- inodeTableOffset = view.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
3571
- pathTableOffset = view.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
3572
- view.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
3573
- dataOffset = view.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
3574
- view.getUint32(SUPERBLOCK.PATH_USED, true);
3575
- if (blockSize === 0 || (blockSize & blockSize - 1) !== 0 || inodeCount === 0 || inodeTableOffset >= fileSize || pathTableOffset >= fileSize || dataOffset >= fileSize) {
3576
- const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
3577
- inodeCount = DEFAULT_INODE_COUNT;
3578
- blockSize = DEFAULT_BLOCK_SIZE;
3579
- totalBlocks = INITIAL_DATA_BLOCKS;
3580
- inodeTableOffset = layout.inodeTableOffset;
3581
- pathTableOffset = layout.pathTableOffset;
3582
- dataOffset = layout.dataOffset;
3583
- }
3584
- } else {
3585
- const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
3586
- inodeCount = DEFAULT_INODE_COUNT;
3587
- blockSize = DEFAULT_BLOCK_SIZE;
3588
- totalBlocks = INITIAL_DATA_BLOCKS;
3589
- inodeTableOffset = layout.inodeTableOffset;
3590
- pathTableOffset = layout.pathTableOffset;
3591
- dataOffset = layout.dataOffset;
3592
- }
3593
- const decoder9 = new TextDecoder();
3594
- const recovered = [];
3595
- let lost = 0;
3596
- const maxInodes = Math.min(inodeCount, Math.floor((fileSize - inodeTableOffset) / INODE_SIZE));
3597
- for (let i = 0; i < maxInodes; i++) {
3598
- const off = inodeTableOffset + i * INODE_SIZE;
3599
- if (off + INODE_SIZE > fileSize) break;
3600
- const type = raw[off + INODE.TYPE];
3601
- if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) continue;
3602
- const inodeView = new DataView(raw.buffer, off, INODE_SIZE);
3603
- const pathOffset = inodeView.getUint32(INODE.PATH_OFFSET, true);
3604
- const pathLength = inodeView.getUint16(INODE.PATH_LENGTH, true);
3605
- const size = inodeView.getFloat64(INODE.SIZE, true);
3606
- const firstBlock = inodeView.getUint32(INODE.FIRST_BLOCK, true);
3607
- inodeView.getUint32(INODE.BLOCK_COUNT, true);
3608
- const absPathOffset = pathTableOffset + pathOffset;
3609
- if (pathLength === 0 || pathLength > 4096 || absPathOffset + pathLength > fileSize) {
3610
- lost++;
3611
- continue;
3612
- }
3613
- let path;
3614
- try {
3615
- path = decoder9.decode(raw.subarray(absPathOffset, absPathOffset + pathLength));
3616
- } catch {
3617
- lost++;
3618
- continue;
3619
- }
3620
- if (!path.startsWith("/") || path.includes("\0")) {
3621
- lost++;
3622
- continue;
3623
- }
3624
- if (type === INODE_TYPE.DIRECTORY) {
3625
- recovered.push({ path, type, data: new Uint8Array(0) });
3626
- continue;
3627
- }
3628
- if (size < 0 || size > fileSize || !isFinite(size)) {
3629
- lost++;
3630
- continue;
3631
- }
3632
- const dataStart = dataOffset + firstBlock * blockSize;
3633
- if (dataStart + size > fileSize || firstBlock >= totalBlocks) {
3634
- recovered.push({ path, type, data: new Uint8Array(0) });
3635
- lost++;
3636
- continue;
3637
- }
3638
- const data = raw.slice(dataStart, dataStart + size);
3639
- recovered.push({ path, type, data });
3640
- }
3641
- await rootDir.removeEntry(".vfs.bin");
3642
- const newFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
3643
- const { handle, isMemory } = await openFreshVFSHandle(newFileHandle);
3644
- try {
3645
- const engine = new VFSEngine();
3646
- engine.init(handle);
3647
- const dirs = recovered.filter((e) => e.type === INODE_TYPE.DIRECTORY && e.path !== "/").sort((a, b) => a.path.localeCompare(b.path));
3648
- const files = recovered.filter((e) => e.type === INODE_TYPE.FILE);
3649
- const symlinks = recovered.filter((e) => e.type === INODE_TYPE.SYMLINK);
3650
- for (const dir of dirs) {
3651
- const result = engine.mkdir(dir.path, 16877);
3652
- if (result.status !== 0) lost++;
3653
- }
3654
- for (const file2 of files) {
3655
- const result = engine.write(file2.path, file2.data);
3656
- if (result.status !== 0) lost++;
3657
- }
3658
- for (const sym of symlinks) {
3659
- const target = decoder9.decode(sym.data);
3660
- const result = engine.symlink(target, sym.path);
3661
- if (result.status !== 0) lost++;
3662
- }
3663
- engine.flush();
3664
- if (isMemory) {
3665
- await saveMemoryHandle(newFileHandle, handle);
3666
- }
3667
- } finally {
3668
- handle.close();
3669
- }
3670
- const entries = recovered.filter((e) => e.path !== "/").map((e) => ({
3671
- path: e.path,
3672
- type: e.type === INODE_TYPE.FILE ? "file" : e.type === INODE_TYPE.DIRECTORY ? "directory" : "symlink",
3673
- size: e.data.byteLength
3674
- }));
3675
- return { recovered: entries.length, lost, entries };
3695
+ function spawnRepairWorker(msg) {
3696
+ return new Promise((resolve2, reject) => {
3697
+ const worker = new Worker(
3698
+ new URL("./workers/repair.worker.js", import.meta.url),
3699
+ { type: "module" }
3700
+ );
3701
+ worker.onmessage = (event) => {
3702
+ worker.terminate();
3703
+ if (event.data.error) {
3704
+ reject(new Error(event.data.error));
3705
+ } else {
3706
+ resolve2(event.data);
3707
+ }
3708
+ };
3709
+ worker.onerror = (event) => {
3710
+ worker.terminate();
3711
+ reject(new Error(event.message || "Repair worker failed"));
3712
+ };
3713
+ worker.postMessage(msg);
3714
+ });
3676
3715
  }
3677
3716
 
3678
3717
  // src/index.ts