@componentor/fs 3.0.5 → 3.0.7
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 +17 -0
- package/dist/index.js +118 -42
- 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/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
|
@@ -509,6 +509,21 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
|
|
|
509
509
|
|
|
510
510
|
## Changelog
|
|
511
511
|
|
|
512
|
+
### v3.0.7 (2026)
|
|
513
|
+
|
|
514
|
+
**Fixes:**
|
|
515
|
+
- Fix `fs.watch()` path matching for root `/` watchers — watching `/` now correctly matches all child paths instead of missing them due to an off-by-one boundary check
|
|
516
|
+
|
|
517
|
+
### v3.0.6 (2026)
|
|
518
|
+
|
|
519
|
+
**Performance:**
|
|
520
|
+
- Bulk-read inode + path tables during mount — 2 I/O calls instead of 10,000+, dramatically faster initialization for large VFS files
|
|
521
|
+
- All active inodes pre-populated in cache on mount (no cold-read penalty for first operations)
|
|
522
|
+
|
|
523
|
+
**Fixes:**
|
|
524
|
+
- `.vfs.bin` now auto-shrinks: trailing free blocks are trimmed on every commit, reclaiming disk space when files are deleted
|
|
525
|
+
- Minimum of 1024 data blocks (4MB) retained to avoid excessive re-growth on small create/delete cycles
|
|
526
|
+
|
|
512
527
|
### v3.0.5 (2026)
|
|
513
528
|
|
|
514
529
|
**Fixes:**
|
|
@@ -516,6 +531,8 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
|
|
|
516
531
|
- Remove unnecessary `clients.claim()` from the service worker — it only acts as a MessagePort broker and never needs to control pages
|
|
517
532
|
- Namespace leader lock, BroadcastChannel, and SW scope by `root` so multiple `VFSFileSystem` instances with different roots don't collide
|
|
518
533
|
- Add `swScope` config option for custom service worker scope override
|
|
534
|
+
- Singleton registry: multiple `new VFSFileSystem()` calls with the same root return the same instance (no duplicate workers)
|
|
535
|
+
- Namespace `vfs-watch` BroadcastChannel by root so watch events don't leak between different roots
|
|
519
536
|
|
|
520
537
|
### v3.0.4 (2026)
|
|
521
538
|
|
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) {
|
|
@@ -1079,31 +1080,33 @@ function matchWatcher(entry, mutatedPath) {
|
|
|
1079
1080
|
if (mutatedPath === absPath) {
|
|
1080
1081
|
return basename(mutatedPath);
|
|
1081
1082
|
}
|
|
1082
|
-
|
|
1083
|
+
const prefix = absPath.endsWith("/") ? absPath : absPath + "/";
|
|
1084
|
+
if (!mutatedPath.startsWith(prefix)) {
|
|
1083
1085
|
return null;
|
|
1084
1086
|
}
|
|
1085
|
-
const relativePath = mutatedPath.substring(
|
|
1087
|
+
const relativePath = mutatedPath.substring(prefix.length);
|
|
1086
1088
|
if (recursive) return relativePath;
|
|
1087
1089
|
return relativePath.indexOf("/") === -1 ? relativePath : null;
|
|
1088
1090
|
}
|
|
1089
|
-
function watch(filePath, options, listener) {
|
|
1091
|
+
function watch(ns, filePath, options, listener) {
|
|
1090
1092
|
const opts = typeof options === "string" ? { } : options ?? {};
|
|
1091
1093
|
const cb = listener ?? (() => {
|
|
1092
1094
|
});
|
|
1093
1095
|
const absPath = resolve(filePath);
|
|
1094
1096
|
const signal = opts.signal;
|
|
1095
1097
|
const entry = {
|
|
1098
|
+
ns,
|
|
1096
1099
|
absPath,
|
|
1097
1100
|
recursive: opts.recursive ?? false,
|
|
1098
1101
|
listener: cb,
|
|
1099
1102
|
signal
|
|
1100
1103
|
};
|
|
1101
|
-
ensureBc();
|
|
1104
|
+
ensureBc(ns);
|
|
1102
1105
|
watchers.add(entry);
|
|
1103
1106
|
if (signal) {
|
|
1104
1107
|
const onAbort = () => {
|
|
1105
1108
|
watchers.delete(entry);
|
|
1106
|
-
releaseBc();
|
|
1109
|
+
releaseBc(ns);
|
|
1107
1110
|
signal.removeEventListener("abort", onAbort);
|
|
1108
1111
|
};
|
|
1109
1112
|
if (signal.aborted) {
|
|
@@ -1115,7 +1118,7 @@ function watch(filePath, options, listener) {
|
|
|
1115
1118
|
const watcher = {
|
|
1116
1119
|
close() {
|
|
1117
1120
|
watchers.delete(entry);
|
|
1118
|
-
releaseBc();
|
|
1121
|
+
releaseBc(ns);
|
|
1119
1122
|
},
|
|
1120
1123
|
ref() {
|
|
1121
1124
|
return watcher;
|
|
@@ -1126,7 +1129,7 @@ function watch(filePath, options, listener) {
|
|
|
1126
1129
|
};
|
|
1127
1130
|
return watcher;
|
|
1128
1131
|
}
|
|
1129
|
-
function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
1132
|
+
function watchFile(ns, syncRequest, filePath, optionsOrListener, listener) {
|
|
1130
1133
|
let opts;
|
|
1131
1134
|
let cb;
|
|
1132
1135
|
if (typeof optionsOrListener === "function") {
|
|
@@ -1145,6 +1148,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1145
1148
|
} catch {
|
|
1146
1149
|
}
|
|
1147
1150
|
const entry = {
|
|
1151
|
+
ns,
|
|
1148
1152
|
absPath,
|
|
1149
1153
|
listener: cb,
|
|
1150
1154
|
interval,
|
|
@@ -1152,7 +1156,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1152
1156
|
syncRequest,
|
|
1153
1157
|
timerId: null
|
|
1154
1158
|
};
|
|
1155
|
-
ensureBc();
|
|
1159
|
+
ensureBc(ns);
|
|
1156
1160
|
let set = fileWatchers.get(absPath);
|
|
1157
1161
|
if (!set) {
|
|
1158
1162
|
set = /* @__PURE__ */ new Set();
|
|
@@ -1161,7 +1165,7 @@ function watchFile(syncRequest, filePath, optionsOrListener, listener) {
|
|
|
1161
1165
|
set.add(entry);
|
|
1162
1166
|
entry.timerId = setInterval(() => triggerWatchFile(entry), interval);
|
|
1163
1167
|
}
|
|
1164
|
-
function unwatchFile(filePath, listener) {
|
|
1168
|
+
function unwatchFile(ns, filePath, listener) {
|
|
1165
1169
|
const absPath = resolve(filePath);
|
|
1166
1170
|
const set = fileWatchers.get(absPath);
|
|
1167
1171
|
if (!set) return;
|
|
@@ -1170,7 +1174,7 @@ function unwatchFile(filePath, listener) {
|
|
|
1170
1174
|
if (entry.listener === listener) {
|
|
1171
1175
|
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1172
1176
|
set.delete(entry);
|
|
1173
|
-
releaseBc();
|
|
1177
|
+
releaseBc(ns);
|
|
1174
1178
|
break;
|
|
1175
1179
|
}
|
|
1176
1180
|
}
|
|
@@ -1178,7 +1182,7 @@ function unwatchFile(filePath, listener) {
|
|
|
1178
1182
|
} else {
|
|
1179
1183
|
for (const entry of set) {
|
|
1180
1184
|
if (entry.timerId !== null) clearInterval(entry.timerId);
|
|
1181
|
-
releaseBc();
|
|
1185
|
+
releaseBc(ns);
|
|
1182
1186
|
}
|
|
1183
1187
|
fileWatchers.delete(absPath);
|
|
1184
1188
|
}
|
|
@@ -1229,13 +1233,14 @@ function emptyStats() {
|
|
|
1229
1233
|
birthtime: zero
|
|
1230
1234
|
};
|
|
1231
1235
|
}
|
|
1232
|
-
async function* watchAsync(_asyncRequest, filePath, options) {
|
|
1236
|
+
async function* watchAsync(ns, _asyncRequest, filePath, options) {
|
|
1233
1237
|
const absPath = resolve(filePath);
|
|
1234
1238
|
const recursive = options?.recursive ?? false;
|
|
1235
1239
|
const signal = options?.signal;
|
|
1236
1240
|
const queue = [];
|
|
1237
1241
|
let resolve2 = null;
|
|
1238
1242
|
const entry = {
|
|
1243
|
+
ns,
|
|
1239
1244
|
absPath,
|
|
1240
1245
|
recursive,
|
|
1241
1246
|
listener: (eventType, filename) => {
|
|
@@ -1247,7 +1252,7 @@ async function* watchAsync(_asyncRequest, filePath, options) {
|
|
|
1247
1252
|
},
|
|
1248
1253
|
signal
|
|
1249
1254
|
};
|
|
1250
|
-
ensureBc();
|
|
1255
|
+
ensureBc(ns);
|
|
1251
1256
|
watchers.add(entry);
|
|
1252
1257
|
try {
|
|
1253
1258
|
while (!signal?.aborted) {
|
|
@@ -1262,13 +1267,14 @@ async function* watchAsync(_asyncRequest, filePath, options) {
|
|
|
1262
1267
|
}
|
|
1263
1268
|
} finally {
|
|
1264
1269
|
watchers.delete(entry);
|
|
1265
|
-
releaseBc();
|
|
1270
|
+
releaseBc(ns);
|
|
1266
1271
|
}
|
|
1267
1272
|
}
|
|
1268
1273
|
|
|
1269
1274
|
// src/filesystem.ts
|
|
1270
1275
|
var encoder9 = new TextEncoder();
|
|
1271
1276
|
var DEFAULT_SAB_SIZE = 2 * 1024 * 1024;
|
|
1277
|
+
var instanceRegistry = /* @__PURE__ */ new Map();
|
|
1272
1278
|
var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
|
|
1273
1279
|
var _canAtomicsWait = typeof globalThis.WorkerGlobalScope !== "undefined";
|
|
1274
1280
|
function spinWait(arr, index, value) {
|
|
@@ -1299,7 +1305,7 @@ var VFSFileSystem = class {
|
|
|
1299
1305
|
readyPromise;
|
|
1300
1306
|
resolveReady;
|
|
1301
1307
|
isReady = false;
|
|
1302
|
-
// Config
|
|
1308
|
+
// Config (definite assignment — always set when constructor doesn't return singleton)
|
|
1303
1309
|
config;
|
|
1304
1310
|
tabId;
|
|
1305
1311
|
/** Namespace string derived from root — used for lock names, BroadcastChannel, and SW scope
|
|
@@ -1317,8 +1323,12 @@ var VFSFileSystem = class {
|
|
|
1317
1323
|
// Promises API namespace
|
|
1318
1324
|
promises;
|
|
1319
1325
|
constructor(config = {}) {
|
|
1326
|
+
const root = config.root ?? "/";
|
|
1327
|
+
const ns = `vfs-${root.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
1328
|
+
const existing = instanceRegistry.get(ns);
|
|
1329
|
+
if (existing) return existing;
|
|
1320
1330
|
this.config = {
|
|
1321
|
-
root
|
|
1331
|
+
root,
|
|
1322
1332
|
opfsSync: config.opfsSync ?? true,
|
|
1323
1333
|
opfsSyncRoot: config.opfsSyncRoot,
|
|
1324
1334
|
uid: config.uid ?? 0,
|
|
@@ -1330,11 +1340,12 @@ var VFSFileSystem = class {
|
|
|
1330
1340
|
swScope: config.swScope
|
|
1331
1341
|
};
|
|
1332
1342
|
this.tabId = crypto.randomUUID();
|
|
1333
|
-
this.ns =
|
|
1343
|
+
this.ns = ns;
|
|
1334
1344
|
this.readyPromise = new Promise((resolve2) => {
|
|
1335
1345
|
this.resolveReady = resolve2;
|
|
1336
1346
|
});
|
|
1337
|
-
this.promises = new VFSPromises(this._async);
|
|
1347
|
+
this.promises = new VFSPromises(this._async, ns);
|
|
1348
|
+
instanceRegistry.set(ns, this);
|
|
1338
1349
|
this.bootstrap();
|
|
1339
1350
|
}
|
|
1340
1351
|
/** Spawn workers and establish communication */
|
|
@@ -1438,6 +1449,7 @@ var VFSFileSystem = class {
|
|
|
1438
1449
|
tabId: this.tabId,
|
|
1439
1450
|
config: {
|
|
1440
1451
|
root: this.config.root,
|
|
1452
|
+
ns: this.ns,
|
|
1441
1453
|
opfsSync: this.config.opfsSync,
|
|
1442
1454
|
opfsSyncRoot: this.config.opfsSyncRoot,
|
|
1443
1455
|
uid: this.config.uid,
|
|
@@ -1536,9 +1548,9 @@ var VFSFileSystem = class {
|
|
|
1536
1548
|
}
|
|
1537
1549
|
};
|
|
1538
1550
|
mc.port1.start();
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1551
|
+
const bc = new BroadcastChannel(`${this.ns}-leader-change`);
|
|
1552
|
+
bc.postMessage({ type: "leader-changed" });
|
|
1553
|
+
bc.close();
|
|
1542
1554
|
}).catch((err) => {
|
|
1543
1555
|
console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
|
|
1544
1556
|
});
|
|
@@ -1804,13 +1816,13 @@ var VFSFileSystem = class {
|
|
|
1804
1816
|
}
|
|
1805
1817
|
// ---- Watch methods ----
|
|
1806
1818
|
watch(filePath, options, listener) {
|
|
1807
|
-
return watch(filePath, options, listener);
|
|
1819
|
+
return watch(this.ns, filePath, options, listener);
|
|
1808
1820
|
}
|
|
1809
1821
|
watchFile(filePath, optionsOrListener, listener) {
|
|
1810
|
-
watchFile(this._sync, filePath, optionsOrListener, listener);
|
|
1822
|
+
watchFile(this.ns, this._sync, filePath, optionsOrListener, listener);
|
|
1811
1823
|
}
|
|
1812
1824
|
unwatchFile(filePath, listener) {
|
|
1813
|
-
unwatchFile(filePath, listener);
|
|
1825
|
+
unwatchFile(this.ns, filePath, listener);
|
|
1814
1826
|
}
|
|
1815
1827
|
// ---- Stream methods ----
|
|
1816
1828
|
createReadStream(filePath, options) {
|
|
@@ -1881,8 +1893,10 @@ var VFSFileSystem = class {
|
|
|
1881
1893
|
};
|
|
1882
1894
|
var VFSPromises = class {
|
|
1883
1895
|
_async;
|
|
1884
|
-
|
|
1896
|
+
_ns;
|
|
1897
|
+
constructor(asyncRequest, ns) {
|
|
1885
1898
|
this._async = asyncRequest;
|
|
1899
|
+
this._ns = ns;
|
|
1886
1900
|
}
|
|
1887
1901
|
readFile(filePath, options) {
|
|
1888
1902
|
return readFile(this._async, filePath, options);
|
|
@@ -1960,7 +1974,7 @@ var VFSPromises = class {
|
|
|
1960
1974
|
return mkdtemp(this._async, prefix);
|
|
1961
1975
|
}
|
|
1962
1976
|
async *watch(filePath, options) {
|
|
1963
|
-
yield* watchAsync(this._async, filePath, options);
|
|
1977
|
+
yield* watchAsync(this._ns, this._async, filePath, options);
|
|
1964
1978
|
}
|
|
1965
1979
|
async flush() {
|
|
1966
1980
|
await this._async(OP.FSYNC, "");
|
|
@@ -2148,6 +2162,10 @@ var VFSEngine = class {
|
|
|
2148
2162
|
if (hi > this.bitmapDirtyHi) this.bitmapDirtyHi = hi;
|
|
2149
2163
|
}
|
|
2150
2164
|
commitPending() {
|
|
2165
|
+
if (this.blocksFreedsinceTrim) {
|
|
2166
|
+
this.trimTrailingBlocks();
|
|
2167
|
+
this.blocksFreedsinceTrim = false;
|
|
2168
|
+
}
|
|
2151
2169
|
if (this.bitmapDirtyHi >= 0) {
|
|
2152
2170
|
const lo = this.bitmapDirtyLo;
|
|
2153
2171
|
const hi = this.bitmapDirtyHi;
|
|
@@ -2160,13 +2178,69 @@ var VFSEngine = class {
|
|
|
2160
2178
|
this.superblockDirty = false;
|
|
2161
2179
|
}
|
|
2162
2180
|
}
|
|
2163
|
-
/**
|
|
2181
|
+
/** Shrink the OPFS file by removing trailing free blocks from the data region.
|
|
2182
|
+
* Scans bitmap from end to find the last used block, then truncates. */
|
|
2183
|
+
trimTrailingBlocks() {
|
|
2184
|
+
const bitmap = this.bitmap;
|
|
2185
|
+
let lastUsed = -1;
|
|
2186
|
+
for (let byteIdx = Math.ceil(this.totalBlocks / 8) - 1; byteIdx >= 0; byteIdx--) {
|
|
2187
|
+
if (bitmap[byteIdx] !== 0) {
|
|
2188
|
+
for (let bit = 7; bit >= 0; bit--) {
|
|
2189
|
+
const blockIdx = byteIdx * 8 + bit;
|
|
2190
|
+
if (blockIdx < this.totalBlocks && bitmap[byteIdx] & 1 << bit) {
|
|
2191
|
+
lastUsed = blockIdx;
|
|
2192
|
+
break;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
const newTotal = Math.max(lastUsed + 1, INITIAL_DATA_BLOCKS);
|
|
2199
|
+
if (newTotal >= this.totalBlocks) return;
|
|
2200
|
+
this.handle.truncate(this.dataOffset + newTotal * this.blockSize);
|
|
2201
|
+
const newBitmapSize = Math.ceil(newTotal / 8);
|
|
2202
|
+
this.bitmap = bitmap.slice(0, newBitmapSize);
|
|
2203
|
+
const trimmed = this.totalBlocks - newTotal;
|
|
2204
|
+
this.freeBlocks -= trimmed;
|
|
2205
|
+
this.totalBlocks = newTotal;
|
|
2206
|
+
this.superblockDirty = true;
|
|
2207
|
+
this.bitmapDirtyLo = 0;
|
|
2208
|
+
this.bitmapDirtyHi = newBitmapSize - 1;
|
|
2209
|
+
}
|
|
2210
|
+
/** Rebuild in-memory path→inode index from disk.
|
|
2211
|
+
* Bulk-reads the entire inode table + path table in 2 I/O calls,
|
|
2212
|
+
* then parses in memory (avoids 10k+ individual reads). */
|
|
2164
2213
|
rebuildIndex() {
|
|
2165
2214
|
this.pathIndex.clear();
|
|
2215
|
+
this.inodeCache.clear();
|
|
2216
|
+
const inodeTableSize = this.inodeCount * INODE_SIZE;
|
|
2217
|
+
const inodeBuf = new Uint8Array(inodeTableSize);
|
|
2218
|
+
this.handle.read(inodeBuf, { at: this.inodeTableOffset });
|
|
2219
|
+
const inodeView = new DataView(inodeBuf.buffer);
|
|
2220
|
+
const pathBuf = this.pathTableUsed > 0 ? new Uint8Array(this.pathTableUsed) : null;
|
|
2221
|
+
if (pathBuf) {
|
|
2222
|
+
this.handle.read(pathBuf, { at: this.pathTableOffset });
|
|
2223
|
+
}
|
|
2166
2224
|
for (let i = 0; i < this.inodeCount; i++) {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2225
|
+
const off = i * INODE_SIZE;
|
|
2226
|
+
const type = inodeView.getUint8(off + INODE.TYPE);
|
|
2227
|
+
if (type === INODE_TYPE.FREE) continue;
|
|
2228
|
+
const inode = {
|
|
2229
|
+
type,
|
|
2230
|
+
pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
|
|
2231
|
+
pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
|
|
2232
|
+
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),
|
|
2236
|
+
mtime: inodeView.getFloat64(off + INODE.MTIME, true),
|
|
2237
|
+
ctime: inodeView.getFloat64(off + INODE.CTIME, true),
|
|
2238
|
+
atime: inodeView.getFloat64(off + INODE.ATIME, true),
|
|
2239
|
+
uid: inodeView.getUint32(off + INODE.UID, true),
|
|
2240
|
+
gid: inodeView.getUint32(off + INODE.GID, true)
|
|
2241
|
+
};
|
|
2242
|
+
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);
|
|
2170
2244
|
this.pathIndex.set(path, i);
|
|
2171
2245
|
}
|
|
2172
2246
|
}
|
|
@@ -2307,6 +2381,7 @@ var VFSEngine = class {
|
|
|
2307
2381
|
this.superblockDirty = true;
|
|
2308
2382
|
return start;
|
|
2309
2383
|
}
|
|
2384
|
+
blocksFreedsinceTrim = false;
|
|
2310
2385
|
freeBlockRange(start, count) {
|
|
2311
2386
|
if (count === 0) return;
|
|
2312
2387
|
const bitmap = this.bitmap;
|
|
@@ -2318,6 +2393,7 @@ var VFSEngine = class {
|
|
|
2318
2393
|
this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
|
|
2319
2394
|
this.freeBlocks += count;
|
|
2320
2395
|
this.superblockDirty = true;
|
|
2396
|
+
this.blocksFreedsinceTrim = true;
|
|
2321
2397
|
}
|
|
2322
2398
|
// updateSuperblockFreeBlocks is no longer needed — superblock writes are coalesced via commitPending()
|
|
2323
2399
|
// ========== Inode allocation ==========
|