@componentor/fs 3.0.4 → 3.0.6
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 +21 -0
- package/dist/index.js +125 -44
- package/dist/index.js.map +1 -1
- package/dist/workers/server.worker.js +66 -4
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/service.worker.js +1 -2
- package/dist/workers/service.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +67 -5
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ const fs = new VFSFileSystem({
|
|
|
82
82
|
strictPermissions: false, // Enforce Unix permissions (default: false)
|
|
83
83
|
sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
|
|
84
84
|
debug: false, // Enable debug logging (default: false)
|
|
85
|
+
swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
|
|
85
86
|
});
|
|
86
87
|
```
|
|
87
88
|
|
|
@@ -508,6 +509,26 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
|
|
|
508
509
|
|
|
509
510
|
## Changelog
|
|
510
511
|
|
|
512
|
+
### v3.0.6 (2026)
|
|
513
|
+
|
|
514
|
+
**Performance:**
|
|
515
|
+
- Bulk-read inode + path tables during mount — 2 I/O calls instead of 10,000+, dramatically faster initialization for large VFS files
|
|
516
|
+
- All active inodes pre-populated in cache on mount (no cold-read penalty for first operations)
|
|
517
|
+
|
|
518
|
+
**Fixes:**
|
|
519
|
+
- `.vfs.bin` now auto-shrinks: trailing free blocks are trimmed on every commit, reclaiming disk space when files are deleted
|
|
520
|
+
- Minimum of 1024 data blocks (4MB) retained to avoid excessive re-growth on small create/delete cycles
|
|
521
|
+
|
|
522
|
+
### v3.0.5 (2026)
|
|
523
|
+
|
|
524
|
+
**Fixes:**
|
|
525
|
+
- Scope the internal service worker by default so it won't collide with the host application's own service worker
|
|
526
|
+
- Remove unnecessary `clients.claim()` from the service worker — it only acts as a MessagePort broker and never needs to control pages
|
|
527
|
+
- Namespace leader lock, BroadcastChannel, and SW scope by `root` so multiple `VFSFileSystem` instances with different roots don't collide
|
|
528
|
+
- Add `swScope` config option for custom service worker scope override
|
|
529
|
+
- Singleton registry: multiple `new VFSFileSystem()` calls with the same root return the same instance (no duplicate workers)
|
|
530
|
+
- Namespace `vfs-watch` BroadcastChannel by root so watch events don't leak between different roots
|
|
531
|
+
|
|
511
532
|
### v3.0.4 (2026)
|
|
512
533
|
|
|
513
534
|
**Features:**
|
package/dist/index.js
CHANGED
|
@@ -1038,22 +1038,23 @@ function format(obj) {
|
|
|
1038
1038
|
// src/methods/watch.ts
|
|
1039
1039
|
var watchers = /* @__PURE__ */ new Set();
|
|
1040
1040
|
var fileWatchers = /* @__PURE__ */ new Map();
|
|
1041
|
-
var
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1041
|
+
var bcMap = /* @__PURE__ */ new Map();
|
|
1042
|
+
function ensureBc(ns) {
|
|
1043
|
+
const entry = bcMap.get(ns);
|
|
1044
|
+
if (entry) {
|
|
1045
|
+
entry.refCount++;
|
|
1046
1046
|
return;
|
|
1047
1047
|
}
|
|
1048
|
-
bc = new BroadcastChannel(
|
|
1049
|
-
|
|
1048
|
+
const bc = new BroadcastChannel(`${ns}-watch`);
|
|
1049
|
+
bcMap.set(ns, { bc, refCount: 1 });
|
|
1050
1050
|
bc.onmessage = onBroadcast;
|
|
1051
1051
|
}
|
|
1052
|
-
function releaseBc() {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1052
|
+
function releaseBc(ns) {
|
|
1053
|
+
const entry = bcMap.get(ns);
|
|
1054
|
+
if (!entry) return;
|
|
1055
|
+
if (--entry.refCount <= 0) {
|
|
1056
|
+
entry.bc.close();
|
|
1057
|
+
bcMap.delete(ns);
|
|
1057
1058
|
}
|
|
1058
1059
|
}
|
|
1059
1060
|
function onBroadcast(event) {
|
|
@@ -1086,24 +1087,25 @@ function matchWatcher(entry, mutatedPath) {
|
|
|
1086
1087
|
if (recursive) return relativePath;
|
|
1087
1088
|
return relativePath.indexOf("/") === -1 ? relativePath : null;
|
|
1088
1089
|
}
|
|
1089
|
-
function watch(filePath, options, listener) {
|
|
1090
|
+
function watch(ns, filePath, options, listener) {
|
|
1090
1091
|
const opts = typeof options === "string" ? { } : options ?? {};
|
|
1091
1092
|
const cb = listener ?? (() => {
|
|
1092
1093
|
});
|
|
1093
1094
|
const absPath = resolve(filePath);
|
|
1094
1095
|
const signal = opts.signal;
|
|
1095
1096
|
const entry = {
|
|
1097
|
+
ns,
|
|
1096
1098
|
absPath,
|
|
1097
1099
|
recursive: opts.recursive ?? false,
|
|
1098
1100
|
listener: cb,
|
|
1099
1101
|
signal
|
|
1100
1102
|
};
|
|
1101
|
-
ensureBc();
|
|
1103
|
+
ensureBc(ns);
|
|
1102
1104
|
watchers.add(entry);
|
|
1103
1105
|
if (signal) {
|
|
1104
1106
|
const onAbort = () => {
|
|
1105
1107
|
watchers.delete(entry);
|
|
1106
|
-
releaseBc();
|
|
1108
|
+
releaseBc(ns);
|
|
1107
1109
|
signal.removeEventListener("abort", onAbort);
|
|
1108
1110
|
};
|
|
1109
1111
|
if (signal.aborted) {
|
|
@@ -1115,7 +1117,7 @@ function watch(filePath, options, listener) {
|
|
|
1115
1117
|
const watcher = {
|
|
1116
1118
|
close() {
|
|
1117
1119
|
watchers.delete(entry);
|
|
1118
|
-
releaseBc();
|
|
1120
|
+
releaseBc(ns);
|
|
1119
1121
|
},
|
|
1120
1122
|
ref() {
|
|
1121
1123
|
return watcher;
|
|
@@ -1126,7 +1128,7 @@ function watch(filePath, options, listener) {
|
|
|
1126
1128
|
};
|
|
1127
1129
|
return watcher;
|
|
1128
1130
|
}
|
|
1129
|
-
function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
1131
|
+
function watchFile(ns, syncRequest, filePath, optionsOrListener, listener) {
|
|
1130
1132
|
let opts;
|
|
1131
1133
|
let cb;
|
|
1132
1134
|
if (typeof optionsOrListener === "function") {
|
|
@@ -1145,6 +1147,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1145
1147
|
} catch {
|
|
1146
1148
|
}
|
|
1147
1149
|
const entry = {
|
|
1150
|
+
ns,
|
|
1148
1151
|
absPath,
|
|
1149
1152
|
listener: cb,
|
|
1150
1153
|
interval,
|
|
@@ -1152,7 +1155,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1152
1155
|
syncRequest,
|
|
1153
1156
|
timerId: null
|
|
1154
1157
|
};
|
|
1155
|
-
ensureBc();
|
|
1158
|
+
ensureBc(ns);
|
|
1156
1159
|
let set = fileWatchers.get(absPath);
|
|
1157
1160
|
if (!set) {
|
|
1158
1161
|
set = /* @__PURE__ */ new Set();
|
|
@@ -1161,7 +1164,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1161
1164
|
set.add(entry);
|
|
1162
1165
|
entry.timerId = setInterval(() => triggerWatchFile(entry), interval);
|
|
1163
1166
|
}
|
|
1164
|
-
function unwatchFile(filePath, listener) {
|
|
1167
|
+
function unwatchFile(ns, filePath, listener) {
|
|
1165
1168
|
const absPath = resolve(filePath);
|
|
1166
1169
|
const set = fileWatchers.get(absPath);
|
|
1167
1170
|
if (!set) return;
|
|
@@ -1170,7 +1173,7 @@ function unwatchFile(filePath, listener) {
|
|
|
1170
1173
|
if (entry.listener === listener) {
|
|
1171
1174
|
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1172
1175
|
set.delete(entry);
|
|
1173
|
-
releaseBc();
|
|
1176
|
+
releaseBc(ns);
|
|
1174
1177
|
break;
|
|
1175
1178
|
}
|
|
1176
1179
|
}
|
|
@@ -1178,7 +1181,7 @@ function unwatchFile(filePath, listener) {
|
|
|
1178
1181
|
} else {
|
|
1179
1182
|
for (const entry of set) {
|
|
1180
1183
|
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1181
|
-
releaseBc();
|
|
1184
|
+
releaseBc(ns);
|
|
1182
1185
|
}
|
|
1183
1186
|
fileWatchers.delete(absPath);
|
|
1184
1187
|
}
|
|
@@ -1229,13 +1232,14 @@ function emptyStats() {
|
|
|
1229
1232
|
birthtime: zero
|
|
1230
1233
|
};
|
|
1231
1234
|
}
|
|
1232
|
-
async function* watchAsync(_asyncRequest, filePath, options) {
|
|
1235
|
+
async function* watchAsync(ns, _asyncRequest, filePath, options) {
|
|
1233
1236
|
const absPath = resolve(filePath);
|
|
1234
1237
|
const recursive = options?.recursive ?? false;
|
|
1235
1238
|
const signal = options?.signal;
|
|
1236
1239
|
const queue = [];
|
|
1237
1240
|
let resolve2 = null;
|
|
1238
1241
|
const entry = {
|
|
1242
|
+
ns,
|
|
1239
1243
|
absPath,
|
|
1240
1244
|
recursive,
|
|
1241
1245
|
listener: (eventType, filename) => {
|
|
@@ -1247,7 +1251,7 @@ async function* watchAsync(_asyncRequest, filePath, options) {
|
|
|
1247
1251
|
},
|
|
1248
1252
|
signal
|
|
1249
1253
|
};
|
|
1250
|
-
ensureBc();
|
|
1254
|
+
ensureBc(ns);
|
|
1251
1255
|
watchers.add(entry);
|
|
1252
1256
|
try {
|
|
1253
1257
|
while (!signal?.aborted) {
|
|
@@ -1262,13 +1266,14 @@ async function* watchAsync(_asyncRequest, filePath, options) {
|
|
|
1262
1266
|
}
|
|
1263
1267
|
} finally {
|
|
1264
1268
|
watchers.delete(entry);
|
|
1265
|
-
releaseBc();
|
|
1269
|
+
releaseBc(ns);
|
|
1266
1270
|
}
|
|
1267
1271
|
}
|
|
1268
1272
|
|
|
1269
1273
|
// src/filesystem.ts
|
|
1270
1274
|
var encoder9 = new TextEncoder();
|
|
1271
1275
|
var DEFAULT_SAB_SIZE = 2 * 1024 * 1024;
|
|
1276
|
+
var instanceRegistry = /* @__PURE__ */ new Map();
|
|
1272
1277
|
var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
|
|
1273
1278
|
var _canAtomicsWait = typeof globalThis.WorkerGlobalScope !== "undefined";
|
|
1274
1279
|
function spinWait(arr, index, value) {
|
|
@@ -1299,9 +1304,12 @@ var VFSFileSystem = class {
|
|
|
1299
1304
|
readyPromise;
|
|
1300
1305
|
resolveReady;
|
|
1301
1306
|
isReady = false;
|
|
1302
|
-
// Config
|
|
1307
|
+
// Config (definite assignment — always set when constructor doesn't return singleton)
|
|
1303
1308
|
config;
|
|
1304
1309
|
tabId;
|
|
1310
|
+
/** Namespace string derived from root — used for lock names, BroadcastChannel, and SW scope
|
|
1311
|
+
* so multiple VFS instances with different roots don't collide. */
|
|
1312
|
+
ns;
|
|
1305
1313
|
// Service worker registration for multi-tab port transfer
|
|
1306
1314
|
swReg = null;
|
|
1307
1315
|
isFollower = false;
|
|
@@ -1314,8 +1322,12 @@ var VFSFileSystem = class {
|
|
|
1314
1322
|
// Promises API namespace
|
|
1315
1323
|
promises;
|
|
1316
1324
|
constructor(config = {}) {
|
|
1325
|
+
const root = config.root ?? "/";
|
|
1326
|
+
const ns = `vfs-${root.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
1327
|
+
const existing = instanceRegistry.get(ns);
|
|
1328
|
+
if (existing) return existing;
|
|
1317
1329
|
this.config = {
|
|
1318
|
-
root
|
|
1330
|
+
root,
|
|
1319
1331
|
opfsSync: config.opfsSync ?? true,
|
|
1320
1332
|
opfsSyncRoot: config.opfsSyncRoot,
|
|
1321
1333
|
uid: config.uid ?? 0,
|
|
@@ -1323,13 +1335,16 @@ var VFSFileSystem = class {
|
|
|
1323
1335
|
umask: config.umask ?? 18,
|
|
1324
1336
|
strictPermissions: config.strictPermissions ?? false,
|
|
1325
1337
|
sabSize: config.sabSize ?? DEFAULT_SAB_SIZE,
|
|
1326
|
-
debug: config.debug ?? false
|
|
1338
|
+
debug: config.debug ?? false,
|
|
1339
|
+
swScope: config.swScope
|
|
1327
1340
|
};
|
|
1328
1341
|
this.tabId = crypto.randomUUID();
|
|
1342
|
+
this.ns = ns;
|
|
1329
1343
|
this.readyPromise = new Promise((resolve2) => {
|
|
1330
1344
|
this.resolveReady = resolve2;
|
|
1331
1345
|
});
|
|
1332
|
-
this.promises = new VFSPromises(this._async);
|
|
1346
|
+
this.promises = new VFSPromises(this._async, ns);
|
|
1347
|
+
instanceRegistry.set(ns, this);
|
|
1333
1348
|
this.bootstrap();
|
|
1334
1349
|
}
|
|
1335
1350
|
/** Spawn workers and establish communication */
|
|
@@ -1398,7 +1413,7 @@ var VFSFileSystem = class {
|
|
|
1398
1413
|
return;
|
|
1399
1414
|
}
|
|
1400
1415
|
let decided = false;
|
|
1401
|
-
navigator.locks.request(
|
|
1416
|
+
navigator.locks.request(`${this.ns}-leader`, { ifAvailable: true }, async (lock) => {
|
|
1402
1417
|
if (decided) return;
|
|
1403
1418
|
decided = true;
|
|
1404
1419
|
if (lock) {
|
|
@@ -1415,7 +1430,7 @@ var VFSFileSystem = class {
|
|
|
1415
1430
|
/** Queue for leader takeover when the current leader's lock is released */
|
|
1416
1431
|
waitForLeaderLock() {
|
|
1417
1432
|
if (!("locks" in navigator)) return;
|
|
1418
|
-
navigator.locks.request(
|
|
1433
|
+
navigator.locks.request(`${this.ns}-leader`, async () => {
|
|
1419
1434
|
console.log("[VFS] Leader lock acquired \u2014 promoting to leader");
|
|
1420
1435
|
this.holdingLeaderLock = true;
|
|
1421
1436
|
this.promoteToLeader();
|
|
@@ -1433,6 +1448,7 @@ var VFSFileSystem = class {
|
|
|
1433
1448
|
tabId: this.tabId,
|
|
1434
1449
|
config: {
|
|
1435
1450
|
root: this.config.root,
|
|
1451
|
+
ns: this.ns,
|
|
1436
1452
|
opfsSync: this.config.opfsSync,
|
|
1437
1453
|
opfsSyncRoot: this.config.opfsSyncRoot,
|
|
1438
1454
|
uid: this.config.uid,
|
|
@@ -1459,7 +1475,7 @@ var VFSFileSystem = class {
|
|
|
1459
1475
|
tabId: this.tabId
|
|
1460
1476
|
});
|
|
1461
1477
|
this.connectToLeader();
|
|
1462
|
-
this.leaderChangeBc = new BroadcastChannel(
|
|
1478
|
+
this.leaderChangeBc = new BroadcastChannel(`${this.ns}-leader-change`);
|
|
1463
1479
|
this.leaderChangeBc.onmessage = () => {
|
|
1464
1480
|
if (this.isFollower) {
|
|
1465
1481
|
console.log("[VFS] Leader changed \u2014 reconnecting");
|
|
@@ -1485,7 +1501,8 @@ var VFSFileSystem = class {
|
|
|
1485
1501
|
async getServiceWorker() {
|
|
1486
1502
|
if (!this.swReg) {
|
|
1487
1503
|
const swUrl = new URL("./workers/service.worker.js", import.meta.url);
|
|
1488
|
-
|
|
1504
|
+
const scope = this.config.swScope ?? new URL(`./${this.ns}/`, swUrl).href;
|
|
1505
|
+
this.swReg = await navigator.serviceWorker.register(swUrl.href, { type: "module", scope });
|
|
1489
1506
|
}
|
|
1490
1507
|
const reg = this.swReg;
|
|
1491
1508
|
if (reg.active) return reg.active;
|
|
@@ -1530,9 +1547,9 @@ var VFSFileSystem = class {
|
|
|
1530
1547
|
}
|
|
1531
1548
|
};
|
|
1532
1549
|
mc.port1.start();
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1550
|
+
const bc = new BroadcastChannel(`${this.ns}-leader-change`);
|
|
1551
|
+
bc.postMessage({ type: "leader-changed" });
|
|
1552
|
+
bc.close();
|
|
1536
1553
|
}).catch((err) => {
|
|
1537
1554
|
console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
|
|
1538
1555
|
});
|
|
@@ -1798,13 +1815,13 @@ var VFSFileSystem = class {
|
|
|
1798
1815
|
}
|
|
1799
1816
|
// ---- Watch methods ----
|
|
1800
1817
|
watch(filePath, options, listener) {
|
|
1801
|
-
return watch(filePath, options, listener);
|
|
1818
|
+
return watch(this.ns, filePath, options, listener);
|
|
1802
1819
|
}
|
|
1803
1820
|
watchFile(filePath, optionsOrListener, listener) {
|
|
1804
|
-
watchFile(this._sync, filePath, optionsOrListener, listener);
|
|
1821
|
+
watchFile(this.ns, this._sync, filePath, optionsOrListener, listener);
|
|
1805
1822
|
}
|
|
1806
1823
|
unwatchFile(filePath, listener) {
|
|
1807
|
-
unwatchFile(filePath, listener);
|
|
1824
|
+
unwatchFile(this.ns, filePath, listener);
|
|
1808
1825
|
}
|
|
1809
1826
|
// ---- Stream methods ----
|
|
1810
1827
|
createReadStream(filePath, options) {
|
|
@@ -1875,8 +1892,10 @@ var VFSFileSystem = class {
|
|
|
1875
1892
|
};
|
|
1876
1893
|
var VFSPromises = class {
|
|
1877
1894
|
_async;
|
|
1878
|
-
|
|
1895
|
+
_ns;
|
|
1896
|
+
constructor(asyncRequest, ns) {
|
|
1879
1897
|
this._async = asyncRequest;
|
|
1898
|
+
this._ns = ns;
|
|
1880
1899
|
}
|
|
1881
1900
|
readFile(filePath, options) {
|
|
1882
1901
|
return readFile(this._async, filePath, options);
|
|
@@ -1954,7 +1973,7 @@ var VFSPromises = class {
|
|
|
1954
1973
|
return mkdtemp(this._async, prefix);
|
|
1955
1974
|
}
|
|
1956
1975
|
async *watch(filePath, options) {
|
|
1957
|
-
yield* watchAsync(this._async, filePath, options);
|
|
1976
|
+
yield* watchAsync(this._ns, this._async, filePath, options);
|
|
1958
1977
|
}
|
|
1959
1978
|
async flush() {
|
|
1960
1979
|
await this._async(OP.FSYNC, "");
|
|
@@ -2142,6 +2161,10 @@ var VFSEngine = class {
|
|
|
2142
2161
|
if (hi > this.bitmapDirtyHi) this.bitmapDirtyHi = hi;
|
|
2143
2162
|
}
|
|
2144
2163
|
commitPending() {
|
|
2164
|
+
if (this.blocksFreedsinceTrim) {
|
|
2165
|
+
this.trimTrailingBlocks();
|
|
2166
|
+
this.blocksFreedsinceTrim = false;
|
|
2167
|
+
}
|
|
2145
2168
|
if (this.bitmapDirtyHi >= 0) {
|
|
2146
2169
|
const lo = this.bitmapDirtyLo;
|
|
2147
2170
|
const hi = this.bitmapDirtyHi;
|
|
@@ -2154,13 +2177,69 @@ var VFSEngine = class {
|
|
|
2154
2177
|
this.superblockDirty = false;
|
|
2155
2178
|
}
|
|
2156
2179
|
}
|
|
2157
|
-
/**
|
|
2180
|
+
/** Shrink the OPFS file by removing trailing free blocks from the data region.
|
|
2181
|
+
* Scans bitmap from end to find the last used block, then truncates. */
|
|
2182
|
+
trimTrailingBlocks() {
|
|
2183
|
+
const bitmap = this.bitmap;
|
|
2184
|
+
let lastUsed = -1;
|
|
2185
|
+
for (let byteIdx = Math.ceil(this.totalBlocks / 8) - 1; byteIdx >= 0; byteIdx--) {
|
|
2186
|
+
if (bitmap[byteIdx] !== 0) {
|
|
2187
|
+
for (let bit = 7; bit >= 0; bit--) {
|
|
2188
|
+
const blockIdx = byteIdx * 8 + bit;
|
|
2189
|
+
if (blockIdx < this.totalBlocks && bitmap[byteIdx] & 1 << bit) {
|
|
2190
|
+
lastUsed = blockIdx;
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
break;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const newTotal = Math.max(lastUsed + 1, INITIAL_DATA_BLOCKS);
|
|
2198
|
+
if (newTotal >= this.totalBlocks) return;
|
|
2199
|
+
this.handle.truncate(this.dataOffset + newTotal * this.blockSize);
|
|
2200
|
+
const newBitmapSize = Math.ceil(newTotal / 8);
|
|
2201
|
+
this.bitmap = bitmap.slice(0, newBitmapSize);
|
|
2202
|
+
const trimmed = this.totalBlocks - newTotal;
|
|
2203
|
+
this.freeBlocks -= trimmed;
|
|
2204
|
+
this.totalBlocks = newTotal;
|
|
2205
|
+
this.superblockDirty = true;
|
|
2206
|
+
this.bitmapDirtyLo = 0;
|
|
2207
|
+
this.bitmapDirtyHi = newBitmapSize - 1;
|
|
2208
|
+
}
|
|
2209
|
+
/** Rebuild in-memory path→inode index from disk.
|
|
2210
|
+
* Bulk-reads the entire inode table + path table in 2 I/O calls,
|
|
2211
|
+
* then parses in memory (avoids 10k+ individual reads). */
|
|
2158
2212
|
rebuildIndex() {
|
|
2159
2213
|
this.pathIndex.clear();
|
|
2214
|
+
this.inodeCache.clear();
|
|
2215
|
+
const inodeTableSize = this.inodeCount * INODE_SIZE;
|
|
2216
|
+
const inodeBuf = new Uint8Array(inodeTableSize);
|
|
2217
|
+
this.handle.read(inodeBuf, { at: this.inodeTableOffset });
|
|
2218
|
+
const inodeView = new DataView(inodeBuf.buffer);
|
|
2219
|
+
const pathBuf = this.pathTableUsed > 0 ? new Uint8Array(this.pathTableUsed) : null;
|
|
2220
|
+
if (pathBuf) {
|
|
2221
|
+
this.handle.read(pathBuf, { at: this.pathTableOffset });
|
|
2222
|
+
}
|
|
2160
2223
|
for (let i = 0; i < this.inodeCount; i++) {
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2163
|
-
|
|
2224
|
+
const off = i * INODE_SIZE;
|
|
2225
|
+
const type = inodeView.getUint8(off + INODE.TYPE);
|
|
2226
|
+
if (type === INODE_TYPE.FREE) continue;
|
|
2227
|
+
const inode = {
|
|
2228
|
+
type,
|
|
2229
|
+
pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
|
|
2230
|
+
pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
|
|
2231
|
+
mode: inodeView.getUint32(off + INODE.MODE, true),
|
|
2232
|
+
size: inodeView.getFloat64(off + INODE.SIZE, true),
|
|
2233
|
+
firstBlock: inodeView.getUint32(off + INODE.FIRST_BLOCK, true),
|
|
2234
|
+
blockCount: inodeView.getUint32(off + INODE.BLOCK_COUNT, true),
|
|
2235
|
+
mtime: inodeView.getFloat64(off + INODE.MTIME, true),
|
|
2236
|
+
ctime: inodeView.getFloat64(off + INODE.CTIME, true),
|
|
2237
|
+
atime: inodeView.getFloat64(off + INODE.ATIME, true),
|
|
2238
|
+
uid: inodeView.getUint32(off + INODE.UID, true),
|
|
2239
|
+
gid: inodeView.getUint32(off + INODE.GID, true)
|
|
2240
|
+
};
|
|
2241
|
+
this.inodeCache.set(i, inode);
|
|
2242
|
+
const path = pathBuf ? decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength)) : this.readPath(inode.pathOffset, inode.pathLength);
|
|
2164
2243
|
this.pathIndex.set(path, i);
|
|
2165
2244
|
}
|
|
2166
2245
|
}
|
|
@@ -2301,6 +2380,7 @@ var VFSEngine = class {
|
|
|
2301
2380
|
this.superblockDirty = true;
|
|
2302
2381
|
return start;
|
|
2303
2382
|
}
|
|
2383
|
+
blocksFreedsinceTrim = false;
|
|
2304
2384
|
freeBlockRange(start, count) {
|
|
2305
2385
|
if (count === 0) return;
|
|
2306
2386
|
const bitmap = this.bitmap;
|
|
@@ -2312,6 +2392,7 @@ var VFSEngine = class {
|
|
|
2312
2392
|
this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
|
|
2313
2393
|
this.freeBlocks += count;
|
|
2314
2394
|
this.superblockDirty = true;
|
|
2395
|
+
this.blocksFreedsinceTrim = true;
|
|
2315
2396
|
}
|
|
2316
2397
|
// updateSuperblockFreeBlocks is no longer needed — superblock writes are coalesced via commitPending()
|
|
2317
2398
|
// ========== Inode allocation ==========
|