@componentor/fs 3.0.43 → 3.0.45

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.
@@ -510,14 +510,23 @@ var VFSEngine = class {
510
510
  growPathTable(needed) {
511
511
  const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
512
512
  const growth = newSize - this.pathTableSize;
513
- const dataSize = this.totalBlocks * this.blockSize;
514
- const dataBuf = new Uint8Array(dataSize);
515
- this.handle.read(dataBuf, { at: this.dataOffset });
516
513
  const newTotalSize = this.handle.getSize() + growth;
517
514
  this.handle.truncate(newTotalSize);
515
+ const dataSize = this.totalBlocks * this.blockSize;
516
+ const CHUNK = 4 * 1024 * 1024;
517
+ const scratch = new Uint8Array(Math.min(CHUNK, Math.max(dataSize, 1)));
518
+ let remaining = dataSize;
519
+ while (remaining > 0) {
520
+ const chunk = Math.min(remaining, CHUNK);
521
+ const srcAt = this.dataOffset + (remaining - chunk);
522
+ const dstAt = this.dataOffset + growth + (remaining - chunk);
523
+ const slice = chunk < scratch.length ? scratch.subarray(0, chunk) : scratch;
524
+ this.handle.read(slice, { at: srcAt });
525
+ this.handle.write(slice, { at: dstAt });
526
+ remaining -= chunk;
527
+ }
518
528
  const newBitmapOffset = this.bitmapOffset + growth;
519
529
  const newDataOffset = this.dataOffset + growth;
520
- this.handle.write(dataBuf, { at: newDataOffset });
521
530
  this.handle.write(this.bitmap, { at: newBitmapOffset });
522
531
  this.pathTableSize = newSize;
523
532
  this.bitmapOffset = newBitmapOffset;
@@ -525,6 +534,23 @@ var VFSEngine = class {
525
534
  this.superblockDirty = true;
526
535
  }
527
536
  // ========== Bitmap I/O ==========
537
+ // Write `length` zero bytes at absolute file offset `at` via a small
538
+ // reusable scratch buffer. Used to materialize POSIX "holes" when a
539
+ // write starts past the current file size — those bytes must read as
540
+ // zeros rather than whatever stale data happened to live in the
541
+ // underlying storage blocks.
542
+ zeroFileRange(at, length) {
543
+ if (length <= 0) return;
544
+ const CHUNK = 4 * 1024 * 1024;
545
+ const zeros = new Uint8Array(Math.min(length, CHUNK));
546
+ let written = 0;
547
+ while (written < length) {
548
+ const n = Math.min(CHUNK, length - written);
549
+ const slice = n < zeros.length ? zeros.subarray(0, n) : zeros;
550
+ this.handle.write(slice, { at: at + written });
551
+ written += n;
552
+ }
553
+ }
528
554
  allocateBlocks(count) {
529
555
  if (count === 0) return 0;
530
556
  const bitmap = this.bitmap;
@@ -849,17 +875,28 @@ var VFSEngine = class {
849
875
  }
850
876
  const inode = this.readInode(existingIdx);
851
877
  if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
852
- const existing = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
853
- const combined = new Uint8Array(existing.byteLength + data.byteLength);
854
- combined.set(existing);
855
- combined.set(data, existing.byteLength);
856
- const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
857
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
878
+ const combinedSize = inode.size + data.byteLength;
879
+ const neededBlocks = Math.ceil(combinedSize / this.blockSize);
858
880
  const newFirst = this.allocateBlocks(neededBlocks);
859
- this.writeData(newFirst, combined);
881
+ const newBase = this.dataOffset + newFirst * this.blockSize;
882
+ if (inode.size > 0) {
883
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
884
+ const CHUNK = 4 * 1024 * 1024;
885
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
886
+ let copied = 0;
887
+ while (copied < inode.size) {
888
+ const n = Math.min(CHUNK, inode.size - copied);
889
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
890
+ this.handle.read(slice, { at: oldBase + copied });
891
+ this.handle.write(slice, { at: newBase + copied });
892
+ copied += n;
893
+ }
894
+ }
895
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
896
+ this.handle.write(data, { at: newBase + inode.size });
860
897
  inode.firstBlock = newFirst;
861
898
  inode.blockCount = neededBlocks;
862
- inode.size = combined.byteLength;
899
+ inode.size = combinedSize;
863
900
  inode.mtime = Date.now();
864
901
  this.writeInode(existingIdx, inode);
865
902
  this.commitPending();
@@ -1122,13 +1159,29 @@ var VFSEngine = class {
1122
1159
  } else if (len > inode.size) {
1123
1160
  const neededBlocks = Math.ceil(len / this.blockSize);
1124
1161
  if (neededBlocks > inode.blockCount) {
1125
- const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
1126
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
1127
1162
  const newFirst = this.allocateBlocks(neededBlocks);
1128
- const newData = new Uint8Array(len);
1129
- newData.set(oldData);
1130
- this.writeData(newFirst, newData);
1163
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1164
+ if (inode.size > 0) {
1165
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
1166
+ const CHUNK = 4 * 1024 * 1024;
1167
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
1168
+ let copied = 0;
1169
+ while (copied < inode.size) {
1170
+ const n = Math.min(CHUNK, inode.size - copied);
1171
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1172
+ this.handle.read(slice, { at: oldBase + copied });
1173
+ this.handle.write(slice, { at: newBase + copied });
1174
+ copied += n;
1175
+ }
1176
+ }
1177
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
1178
+ this.zeroFileRange(newBase + inode.size, len - inode.size);
1131
1179
  inode.firstBlock = newFirst;
1180
+ } else {
1181
+ this.zeroFileRange(
1182
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
1183
+ len - inode.size
1184
+ );
1132
1185
  }
1133
1186
  inode.blockCount = neededBlocks;
1134
1187
  inode.size = len;
@@ -1149,8 +1202,36 @@ var VFSEngine = class {
1149
1202
  if (flags & 1 && this.pathIndex.has(destPath)) {
1150
1203
  return { status: CODE_TO_STATUS.EEXIST };
1151
1204
  }
1152
- const data = srcInode.size > 0 ? this.readData(srcInode.firstBlock, srcInode.blockCount, srcInode.size) : new Uint8Array(0);
1153
- return this.write(destPath, data);
1205
+ if (srcPath === destPath) return { status: 0 };
1206
+ const srcSize = srcInode.size;
1207
+ const srcFirstBlock = srcInode.firstBlock;
1208
+ const emptyStatus = this.write(destPath, new Uint8Array(0));
1209
+ if (emptyStatus.status !== 0) return emptyStatus;
1210
+ if (srcSize === 0) return { status: 0 };
1211
+ const destIdx = this.resolvePathComponents(destPath, true);
1212
+ if (destIdx === void 0) return { status: CODE_TO_STATUS.EIO };
1213
+ const destInode = this.readInode(destIdx);
1214
+ const neededBlocks = Math.ceil(srcSize / this.blockSize);
1215
+ const newFirst = this.allocateBlocks(neededBlocks);
1216
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1217
+ const srcBase = this.dataOffset + srcFirstBlock * this.blockSize;
1218
+ const CHUNK = 4 * 1024 * 1024;
1219
+ const scratch = new Uint8Array(Math.min(CHUNK, srcSize));
1220
+ let copied = 0;
1221
+ while (copied < srcSize) {
1222
+ const n = Math.min(CHUNK, srcSize - copied);
1223
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1224
+ this.handle.read(slice, { at: srcBase + copied });
1225
+ this.handle.write(slice, { at: newBase + copied });
1226
+ copied += n;
1227
+ }
1228
+ destInode.firstBlock = newFirst;
1229
+ destInode.blockCount = neededBlocks;
1230
+ destInode.size = srcSize;
1231
+ destInode.mtime = Date.now();
1232
+ this.writeInode(destIdx, destInode);
1233
+ this.commitPending();
1234
+ return { status: 0 };
1154
1235
  }
1155
1236
  // ---- ACCESS ----
1156
1237
  access(path, mode = 0) {
@@ -1314,16 +1395,35 @@ var VFSEngine = class {
1314
1395
  if (endPos > inode.size) {
1315
1396
  const neededBlocks = Math.ceil(endPos / this.blockSize);
1316
1397
  if (neededBlocks > inode.blockCount) {
1317
- const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1318
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
1319
1398
  const newFirst = this.allocateBlocks(neededBlocks);
1320
- const newBuf = new Uint8Array(endPos);
1321
- newBuf.set(oldData);
1322
- newBuf.set(data, pos);
1323
- this.writeData(newFirst, newBuf);
1399
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1400
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
1401
+ if (inode.size > 0) {
1402
+ const CHUNK = 4 * 1024 * 1024;
1403
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
1404
+ let copied = 0;
1405
+ while (copied < inode.size) {
1406
+ const n = Math.min(CHUNK, inode.size - copied);
1407
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1408
+ this.handle.read(slice, { at: oldBase + copied });
1409
+ this.handle.write(slice, { at: newBase + copied });
1410
+ copied += n;
1411
+ }
1412
+ }
1413
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
1414
+ if (pos > inode.size) {
1415
+ this.zeroFileRange(newBase + inode.size, pos - inode.size);
1416
+ }
1417
+ this.handle.write(data, { at: newBase + pos });
1324
1418
  inode.firstBlock = newFirst;
1325
1419
  inode.blockCount = neededBlocks;
1326
1420
  } else {
1421
+ if (pos > inode.size) {
1422
+ this.zeroFileRange(
1423
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
1424
+ pos - inode.size
1425
+ );
1426
+ }
1327
1427
  const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
1328
1428
  this.handle.write(data, { at: dataOffset });
1329
1429
  }
@@ -3016,21 +3116,53 @@ async function followerLoop() {
3016
3116
  }
3017
3117
  }
3018
3118
  var OPFS_SKIP = /* @__PURE__ */ new Set([".vfs.bin", ".vfs.bin.tmp"]);
3019
- async function scanOPFSEntries(dir, prefix) {
3020
- const result = [];
3119
+ var OPFS_POPULATE_CHUNK = 2 * 1024 * 1024;
3120
+ async function populateVFSFromOPFS(dir, prefix) {
3121
+ const subdirs = [];
3122
+ const files = [];
3021
3123
  for await (const [name, handle] of dir.entries()) {
3022
3124
  if (prefix === "" && OPFS_SKIP.has(name)) continue;
3023
- const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
3024
3125
  if (handle.kind === "directory") {
3025
- result.push({ path: fullPath, type: "directory" });
3026
- result.push(...await scanOPFSEntries(handle, fullPath));
3126
+ subdirs.push({ name, handle });
3027
3127
  } else {
3028
- const file = await handle.getFile();
3029
- const buf = await file.arrayBuffer();
3030
- result.push({ path: fullPath, type: "file", data: new Uint8Array(buf) });
3128
+ files.push({ name, handle });
3129
+ }
3130
+ }
3131
+ for (const { name } of subdirs) {
3132
+ const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
3133
+ engine.mkdir(fullPath, 16877);
3134
+ }
3135
+ for (const { name, handle } of files) {
3136
+ const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
3137
+ let access = null;
3138
+ try {
3139
+ access = await handle.createSyncAccessHandle();
3140
+ const size = access.getSize();
3141
+ engine.write(fullPath, new Uint8Array(0));
3142
+ if (size > 0) {
3143
+ const chunk = new Uint8Array(Math.min(size, OPFS_POPULATE_CHUNK));
3144
+ let offset = 0;
3145
+ while (offset < size) {
3146
+ const len = Math.min(chunk.length, size - offset);
3147
+ const view = len === chunk.length ? chunk : chunk.subarray(0, len);
3148
+ access.read(view, { at: offset });
3149
+ engine.append(fullPath, view);
3150
+ offset += len;
3151
+ }
3152
+ }
3153
+ } finally {
3154
+ if (access) {
3155
+ try {
3156
+ access.close();
3157
+ } catch {
3158
+ }
3159
+ }
3031
3160
  }
3032
3161
  }
3033
- return result;
3162
+ for (const { name, handle } of subdirs) {
3163
+ const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
3164
+ await populateVFSFromOPFS(handle, fullPath);
3165
+ }
3034
3166
  }
3035
3167
  var DEFAULT_LIMITS = {
3036
3168
  maxInodes: 4e6,
@@ -3121,17 +3253,8 @@ async function initEngine(config) {
3121
3253
  throw err;
3122
3254
  }
3123
3255
  if (wasFresh) {
3124
- const opfsEntries = await scanOPFSEntries(rootDir, "");
3125
- if (opfsEntries.length > 0) {
3126
- const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
3127
- for (const dir of dirs) {
3128
- engine.mkdir(dir.path, 16877);
3129
- }
3130
- for (const file of opfsEntries.filter((e) => e.type === "file")) {
3131
- engine.write(file.path, file.data);
3132
- }
3133
- engine.flush();
3134
- }
3256
+ await populateVFSFromOPFS(rootDir, "");
3257
+ engine.flush();
3135
3258
  }
3136
3259
  if (config.opfsSync) {
3137
3260
  opfsSyncEnabled = true;
@@ -3197,6 +3320,29 @@ function broadcastWatch(op, path, newPath) {
3197
3320
  watchBc.postMessage({ eventType: "rename", path: newPath });
3198
3321
  }
3199
3322
  }
3323
+ var pendingPathSyncs = /* @__PURE__ */ new Map();
3324
+ var SYNC_DEBOUNCE_MS = 50;
3325
+ function flushPathSync(path) {
3326
+ pendingPathSyncs.delete(path);
3327
+ if (!opfsSyncPort) return;
3328
+ try {
3329
+ const result = engine.read(path);
3330
+ if (result.status !== 0) return;
3331
+ const ts = Date.now();
3332
+ if (result.data && result.data.byteLength > 0) {
3333
+ const buf = result.data.buffer.byteLength === result.data.byteLength ? result.data.buffer : result.data.slice().buffer;
3334
+ opfsSyncPort.postMessage({ op: "write", path, data: buf, ts }, [buf]);
3335
+ } else {
3336
+ opfsSyncPort.postMessage({ op: "write", path, data: new ArrayBuffer(0), ts });
3337
+ }
3338
+ } catch {
3339
+ }
3340
+ }
3341
+ function schedulePathSync(path) {
3342
+ const prev = pendingPathSyncs.get(path);
3343
+ if (prev) clearTimeout(prev);
3344
+ pendingPathSyncs.set(path, setTimeout(() => flushPathSync(path), SYNC_DEBOUNCE_MS));
3345
+ }
3200
3346
  function notifyOPFSSync(op, path, newPath) {
3201
3347
  if (!opfsSyncPort) return;
3202
3348
  if (suppressPaths.has(path)) {
@@ -3212,39 +3358,35 @@ function notifyOPFSSync(op, path, newPath) {
3212
3358
  case OP.FTRUNCATE:
3213
3359
  case OP.COPY:
3214
3360
  case OP.LINK: {
3215
- const result = engine.read(path);
3216
- if (result.status === 0) {
3217
- if (result.data && result.data.byteLength > 0) {
3218
- const buf = result.data.buffer.byteLength === result.data.byteLength ? result.data.buffer : result.data.slice().buffer;
3219
- opfsSyncPort.postMessage({ op: "write", path, data: buf, ts }, [buf]);
3220
- } else {
3221
- opfsSyncPort.postMessage({ op: "write", path, data: new ArrayBuffer(0), ts });
3222
- }
3223
- }
3361
+ schedulePathSync(path);
3224
3362
  break;
3225
3363
  }
3226
3364
  case OP.SYMLINK: {
3227
- const result = engine.read(path);
3228
- if (result.status === 0) {
3229
- if (result.data && result.data.byteLength > 0) {
3230
- const buf = result.data.buffer.byteLength === result.data.byteLength ? result.data.buffer : result.data.slice().buffer;
3231
- opfsSyncPort.postMessage({ op: "write", path, data: buf, ts }, [buf]);
3232
- } else {
3233
- opfsSyncPort.postMessage({ op: "write", path, data: new ArrayBuffer(0), ts });
3234
- }
3235
- }
3365
+ schedulePathSync(path);
3236
3366
  break;
3237
3367
  }
3238
3368
  case OP.UNLINK:
3239
- case OP.RMDIR:
3369
+ case OP.RMDIR: {
3370
+ const pending = pendingPathSyncs.get(path);
3371
+ if (pending) {
3372
+ clearTimeout(pending);
3373
+ pendingPathSyncs.delete(path);
3374
+ }
3240
3375
  opfsSyncPort.postMessage({ op: "delete", path, ts });
3241
3376
  break;
3377
+ }
3242
3378
  case OP.MKDIR:
3243
3379
  case OP.MKDTEMP:
3244
3380
  opfsSyncPort.postMessage({ op: "mkdir", path, ts });
3245
3381
  break;
3246
3382
  case OP.RENAME:
3247
3383
  if (newPath) {
3384
+ const pending = pendingPathSyncs.get(path);
3385
+ if (pending) {
3386
+ clearTimeout(pending);
3387
+ pendingPathSyncs.delete(path);
3388
+ schedulePathSync(newPath);
3389
+ }
3248
3390
  opfsSyncPort.postMessage({ op: "rename", path, newPath, ts });
3249
3391
  }
3250
3392
  break;