@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.
- package/dist/index.js +125 -25
- package/dist/index.js.map +1 -1
- package/dist/workers/opfs-sync.worker.js +50 -8
- package/dist/workers/opfs-sync.worker.js.map +1 -1
- package/dist/workers/repair.worker.js +125 -25
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +125 -25
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +206 -64
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +26 -0
|
@@ -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
|
|
853
|
-
const
|
|
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.
|
|
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 =
|
|
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
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
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
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
3020
|
-
|
|
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
|
-
|
|
3026
|
-
result.push(...await scanOPFSEntries(handle, fullPath));
|
|
3126
|
+
subdirs.push({ name, handle });
|
|
3027
3127
|
} else {
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|