@componentor/fs 3.0.5 → 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.
@@ -291,6 +291,10 @@ var VFSEngine = class {
291
291
  if (hi > this.bitmapDirtyHi) this.bitmapDirtyHi = hi;
292
292
  }
293
293
  commitPending() {
294
+ if (this.blocksFreedsinceTrim) {
295
+ this.trimTrailingBlocks();
296
+ this.blocksFreedsinceTrim = false;
297
+ }
294
298
  if (this.bitmapDirtyHi >= 0) {
295
299
  const lo = this.bitmapDirtyLo;
296
300
  const hi = this.bitmapDirtyHi;
@@ -303,13 +307,69 @@ var VFSEngine = class {
303
307
  this.superblockDirty = false;
304
308
  }
305
309
  }
306
- /** Rebuild in-memory path→inode index from disk */
310
+ /** Shrink the OPFS file by removing trailing free blocks from the data region.
311
+ * Scans bitmap from end to find the last used block, then truncates. */
312
+ trimTrailingBlocks() {
313
+ const bitmap = this.bitmap;
314
+ let lastUsed = -1;
315
+ for (let byteIdx = Math.ceil(this.totalBlocks / 8) - 1; byteIdx >= 0; byteIdx--) {
316
+ if (bitmap[byteIdx] !== 0) {
317
+ for (let bit = 7; bit >= 0; bit--) {
318
+ const blockIdx = byteIdx * 8 + bit;
319
+ if (blockIdx < this.totalBlocks && bitmap[byteIdx] & 1 << bit) {
320
+ lastUsed = blockIdx;
321
+ break;
322
+ }
323
+ }
324
+ break;
325
+ }
326
+ }
327
+ const newTotal = Math.max(lastUsed + 1, INITIAL_DATA_BLOCKS);
328
+ if (newTotal >= this.totalBlocks) return;
329
+ this.handle.truncate(this.dataOffset + newTotal * this.blockSize);
330
+ const newBitmapSize = Math.ceil(newTotal / 8);
331
+ this.bitmap = bitmap.slice(0, newBitmapSize);
332
+ const trimmed = this.totalBlocks - newTotal;
333
+ this.freeBlocks -= trimmed;
334
+ this.totalBlocks = newTotal;
335
+ this.superblockDirty = true;
336
+ this.bitmapDirtyLo = 0;
337
+ this.bitmapDirtyHi = newBitmapSize - 1;
338
+ }
339
+ /** Rebuild in-memory path→inode index from disk.
340
+ * Bulk-reads the entire inode table + path table in 2 I/O calls,
341
+ * then parses in memory (avoids 10k+ individual reads). */
307
342
  rebuildIndex() {
308
343
  this.pathIndex.clear();
344
+ this.inodeCache.clear();
345
+ const inodeTableSize = this.inodeCount * INODE_SIZE;
346
+ const inodeBuf = new Uint8Array(inodeTableSize);
347
+ this.handle.read(inodeBuf, { at: this.inodeTableOffset });
348
+ const inodeView = new DataView(inodeBuf.buffer);
349
+ const pathBuf = this.pathTableUsed > 0 ? new Uint8Array(this.pathTableUsed) : null;
350
+ if (pathBuf) {
351
+ this.handle.read(pathBuf, { at: this.pathTableOffset });
352
+ }
309
353
  for (let i = 0; i < this.inodeCount; i++) {
310
- const inode = this.readInode(i);
311
- if (inode.type === INODE_TYPE.FREE) continue;
312
- const path = this.readPath(inode.pathOffset, inode.pathLength);
354
+ const off = i * INODE_SIZE;
355
+ const type = inodeView.getUint8(off + INODE.TYPE);
356
+ if (type === INODE_TYPE.FREE) continue;
357
+ const inode = {
358
+ type,
359
+ pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
360
+ pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
361
+ mode: inodeView.getUint32(off + INODE.MODE, true),
362
+ size: inodeView.getFloat64(off + INODE.SIZE, true),
363
+ firstBlock: inodeView.getUint32(off + INODE.FIRST_BLOCK, true),
364
+ blockCount: inodeView.getUint32(off + INODE.BLOCK_COUNT, true),
365
+ mtime: inodeView.getFloat64(off + INODE.MTIME, true),
366
+ ctime: inodeView.getFloat64(off + INODE.CTIME, true),
367
+ atime: inodeView.getFloat64(off + INODE.ATIME, true),
368
+ uid: inodeView.getUint32(off + INODE.UID, true),
369
+ gid: inodeView.getUint32(off + INODE.GID, true)
370
+ };
371
+ this.inodeCache.set(i, inode);
372
+ const path = pathBuf ? decoder.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength)) : this.readPath(inode.pathOffset, inode.pathLength);
313
373
  this.pathIndex.set(path, i);
314
374
  }
315
375
  }
@@ -450,6 +510,7 @@ var VFSEngine = class {
450
510
  this.superblockDirty = true;
451
511
  return start;
452
512
  }
513
+ blocksFreedsinceTrim = false;
453
514
  freeBlockRange(start, count) {
454
515
  if (count === 0) return;
455
516
  const bitmap = this.bitmap;
@@ -461,6 +522,7 @@ var VFSEngine = class {
461
522
  this.markBitmapDirty(start >>> 3, start + count - 1 >>> 3);
462
523
  this.freeBlocks += count;
463
524
  this.superblockDirty = true;
525
+ this.blocksFreedsinceTrim = true;
464
526
  }
465
527
  // updateSuperblockFreeBlocks is no longer needed — superblock writes are coalesced via commitPending()
466
528
  // ========== Inode allocation ==========
@@ -1948,7 +2010,7 @@ async function initEngine(config) {
1948
2010
  [mc.port2]
1949
2011
  );
1950
2012
  }
1951
- watchBc = new BroadcastChannel("vfs-watch");
2013
+ watchBc = new BroadcastChannel(`${config.ns}-watch`);
1952
2014
  }
1953
2015
  function broadcastWatch(op, path, newPath) {
1954
2016
  if (!watchBc) return;