@rljson/fs-agent 0.0.20 → 0.0.22

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.
@@ -159,6 +159,14 @@ export declare class FsAgent {
159
159
  * view — and undo a deletion the peer just made.
160
160
  */
161
161
  private _remoteApplyInFlight;
162
+ /** Files written vs left alone by the current {@link restore}. */
163
+ private _restoreWritten;
164
+ private _restoreSkipped;
165
+ /**
166
+ * What this agent last wrote to each absolute path, so a repeat restore can
167
+ * recognise its own work without re-reading the file.
168
+ */
169
+ private _restoredBlobs;
162
170
  private _timeouts;
163
171
  /** Client-only: resolve DAG-branch conflicts into merge revisions. */
164
172
  private _resolveConflicts;
@@ -292,8 +300,48 @@ export declare class FsAgent {
292
300
  * @param treeHash - Hash of the tree node to restore
293
301
  * @param trees - Map of all tree nodes
294
302
  * @param targetPath - Target directory path
303
+ * @param isOwnRoot - Whether `targetPath` is this agent's own folder, which
304
+ * is the only case where the scanner's view describes these files
295
305
  */
296
306
  private _restoreTree;
307
+ /**
308
+ * The content identity this agent believes is on disk at `filePath`, or
309
+ * `undefined` when it has no basis for an opinion.
310
+ *
311
+ * Two sources, both anchored on a real blobId rather than a guess:
312
+ * what this agent last wrote there, and what the scanner hashed at its last
313
+ * scan of the folder (which survives a restart, so a fresh process still
314
+ * skips an unchanged 80 GB catalogue).
315
+ * @param filePath - Absolute path of the file.
316
+ * @param relativePath - Its path within the tree.
317
+ * @param isOwnRoot - Whether the restore target is this agent's own folder,
318
+ * which is the only case where the scanner's view describes this file.
319
+ * @returns The believed content identity, or `undefined`.
320
+ */
321
+ private _knownOnDisk;
322
+ /**
323
+ * Whether the file at `filePath` is already the content `meta` describes.
324
+ *
325
+ * The decision is anchored on the blobId: a different blobId is always
326
+ * rewritten, whatever the timestamps say. Hashing the file instead would
327
+ * mean reading 80 GB to avoid writing 80 GB, which saves nothing — so the
328
+ * known blobId is verified against a `stat`, which catches a file edited
329
+ * since this agent last had an opinion about it.
330
+ *
331
+ * Deliberately one-directional in its uncertainty: every unclear case
332
+ * answers `false` and the file is rewritten. A needless write costs time; a
333
+ * wrongly skipped write leaves the wrong bytes on disk indefinitely.
334
+ *
335
+ * Anchoring on the blobId is not belt-and-braces. Size and mtime alone
336
+ * cannot see a same-size edit made inside the same millisecond — the scan
337
+ * cache tolerates that, but a restore must not: there the cost is not a
338
+ * stale cache entry, it is the wrong file contents left in place.
339
+ * @param filePath - Absolute path of the file to check.
340
+ * @param meta - The metadata describing the content that should be there.
341
+ * @param isOwnRoot - Whether the target is this agent's own folder.
342
+ * @returns `true` only when the file is certainly already correct.
343
+ */
344
+ private _alreadyOnDisk;
297
345
  /**
298
346
  * Gets the current tree structure
299
347
  */
package/dist/fs-agent.js CHANGED
@@ -505,7 +505,13 @@ class FsScanner {
505
505
  }
506
506
  if (this._vanishedDuringScan > 0) {
507
507
  console.warn(
508
- `[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} — skipped; the next scan picks up the settled state`
508
+ `[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} — this scan is a partial picture`
509
+ );
510
+ if (this._tree) {
511
+ return this._tree;
512
+ }
513
+ console.warn(
514
+ `[fs-scanner] no previous scan of ${this._rootPath} to fall back on — starting from the partial one`
509
515
  );
510
516
  }
511
517
  trees.set(rootHashStr, rootTree);
@@ -1044,6 +1050,14 @@ class FsAgent {
1044
1050
  * view — and undo a deletion the peer just made.
1045
1051
  */
1046
1052
  _remoteApplyInFlight = false;
1053
+ /** Files written vs left alone by the current {@link restore}. */
1054
+ _restoreWritten = 0;
1055
+ _restoreSkipped = 0;
1056
+ /**
1057
+ * What this agent last wrote to each absolute path, so a repeat restore can
1058
+ * recognise its own work without re-reading the file.
1059
+ */
1060
+ _restoredBlobs = /* @__PURE__ */ new Map();
1047
1061
  _timeouts;
1048
1062
  /** Client-only: resolve DAG-branch conflicts into merge revisions. */
1049
1063
  _resolveConflicts;
@@ -1307,7 +1321,19 @@ ${err.stack}` : String(err);
1307
1321
  target
1308
1322
  );
1309
1323
  const preRestore = options?.cleanTarget ? await this._collectAllFiles(target) : /* @__PURE__ */ new Set();
1310
- await this._restoreTree(tree.rootHash, tree.trees, target);
1324
+ this._restoreWritten = 0;
1325
+ this._restoreSkipped = 0;
1326
+ await this._restoreTree(
1327
+ tree.rootHash,
1328
+ tree.trees,
1329
+ target,
1330
+ target === this._rootPath
1331
+ );
1332
+ if (this._restoreSkipped > 0) {
1333
+ console.log(
1334
+ `[FsAgent] restore: wrote ${this._restoreWritten}, left ${this._restoreSkipped} already-correct file${this._restoreSkipped === 1 ? "" : "s"} untouched`
1335
+ );
1336
+ }
1311
1337
  if (options?.cleanTarget) {
1312
1338
  await this._pruneExtraneous(
1313
1339
  target,
@@ -1347,8 +1373,10 @@ ${err.stack}` : String(err);
1347
1373
  * @param treeHash - Hash of the tree node to restore
1348
1374
  * @param trees - Map of all tree nodes
1349
1375
  * @param targetPath - Target directory path
1376
+ * @param isOwnRoot - Whether `targetPath` is this agent's own folder, which
1377
+ * is the only case where the scanner's view describes these files
1350
1378
  */
1351
- async _restoreTree(treeHash, trees, targetPath) {
1379
+ async _restoreTree(treeHash, trees, targetPath, isOwnRoot) {
1352
1380
  const treeNode = trees.get(treeHash);
1353
1381
  if (!treeNode) {
1354
1382
  throw new Error(`Tree node not found: ${treeHash}`);
@@ -1360,6 +1388,10 @@ ${err.stack}` : String(err);
1360
1388
  if (meta.type === "file") {
1361
1389
  const filePath = join(targetPath, meta.relativePath);
1362
1390
  if (meta.blobId) {
1391
+ if (await this._alreadyOnDisk(filePath, meta, isOwnRoot)) {
1392
+ this._restoreSkipped++;
1393
+ return;
1394
+ }
1363
1395
  let fileBlob;
1364
1396
  try {
1365
1397
  fileBlob = await this._bs.getBlob(meta.blobId);
@@ -1375,21 +1407,93 @@ ${err.stack}` : String(err);
1375
1407
  }
1376
1408
  await mkdir(dirname(filePath), { recursive: true });
1377
1409
  await FsAgent._atomicWriteFile(filePath, fileBlob.content);
1410
+ this._restoreWritten++;
1378
1411
  if (meta.mtime) {
1379
1412
  const mtime = new Date(meta.mtime);
1380
1413
  await utimes(filePath, mtime, mtime);
1381
1414
  }
1415
+ if (meta.size !== void 0 && meta.mtime !== void 0) {
1416
+ this._restoredBlobs.set(filePath, {
1417
+ blobId: meta.blobId,
1418
+ size: meta.size,
1419
+ mtime: meta.mtime
1420
+ });
1421
+ }
1382
1422
  }
1383
1423
  } else if (meta.type === "directory") {
1384
1424
  const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
1385
1425
  await mkdir(dirPath, { recursive: true });
1386
1426
  if (treeNode.children && Array.isArray(treeNode.children)) {
1387
1427
  for (const childHash of treeNode.children) {
1388
- await this._restoreTree(childHash, trees, targetPath);
1428
+ await this._restoreTree(childHash, trees, targetPath, isOwnRoot);
1389
1429
  }
1390
1430
  }
1391
1431
  }
1392
1432
  }
1433
+ /**
1434
+ * The content identity this agent believes is on disk at `filePath`, or
1435
+ * `undefined` when it has no basis for an opinion.
1436
+ *
1437
+ * Two sources, both anchored on a real blobId rather than a guess:
1438
+ * what this agent last wrote there, and what the scanner hashed at its last
1439
+ * scan of the folder (which survives a restart, so a fresh process still
1440
+ * skips an unchanged 80 GB catalogue).
1441
+ * @param filePath - Absolute path of the file.
1442
+ * @param relativePath - Its path within the tree.
1443
+ * @param isOwnRoot - Whether the restore target is this agent's own folder,
1444
+ * which is the only case where the scanner's view describes this file.
1445
+ * @returns The believed content identity, or `undefined`.
1446
+ */
1447
+ _knownOnDisk(filePath, relativePath, isOwnRoot) {
1448
+ const written = this._restoredBlobs.get(filePath);
1449
+ if (written) return written;
1450
+ if (!isOwnRoot) return void 0;
1451
+ const scanned = this._scanner.getTreeByPath(relativePath)?.meta;
1452
+ if (scanned?.blobId === void 0 || scanned.size === void 0 || scanned.mtime === void 0) {
1453
+ return void 0;
1454
+ }
1455
+ return {
1456
+ blobId: scanned.blobId,
1457
+ size: scanned.size,
1458
+ mtime: scanned.mtime
1459
+ };
1460
+ }
1461
+ /**
1462
+ * Whether the file at `filePath` is already the content `meta` describes.
1463
+ *
1464
+ * The decision is anchored on the blobId: a different blobId is always
1465
+ * rewritten, whatever the timestamps say. Hashing the file instead would
1466
+ * mean reading 80 GB to avoid writing 80 GB, which saves nothing — so the
1467
+ * known blobId is verified against a `stat`, which catches a file edited
1468
+ * since this agent last had an opinion about it.
1469
+ *
1470
+ * Deliberately one-directional in its uncertainty: every unclear case
1471
+ * answers `false` and the file is rewritten. A needless write costs time; a
1472
+ * wrongly skipped write leaves the wrong bytes on disk indefinitely.
1473
+ *
1474
+ * Anchoring on the blobId is not belt-and-braces. Size and mtime alone
1475
+ * cannot see a same-size edit made inside the same millisecond — the scan
1476
+ * cache tolerates that, but a restore must not: there the cost is not a
1477
+ * stale cache entry, it is the wrong file contents left in place.
1478
+ * @param filePath - Absolute path of the file to check.
1479
+ * @param meta - The metadata describing the content that should be there.
1480
+ * @param isOwnRoot - Whether the target is this agent's own folder.
1481
+ * @returns `true` only when the file is certainly already correct.
1482
+ */
1483
+ async _alreadyOnDisk(filePath, meta, isOwnRoot) {
1484
+ const known = this._knownOnDisk(
1485
+ filePath,
1486
+ meta.relativePath,
1487
+ isOwnRoot
1488
+ );
1489
+ if (!known || known.blobId !== meta.blobId) return false;
1490
+ try {
1491
+ const st = await stat(filePath);
1492
+ return st.size === known.size && Math.abs(st.mtimeMs - known.mtime) < 1;
1493
+ } catch {
1494
+ return false;
1495
+ }
1496
+ }
1393
1497
  /**
1394
1498
  * Gets the current tree structure
1395
1499
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Rljson fs-agent description",
5
5
  "homepage": "https://github.com/rljson/fs-agent",
6
6
  "bugs": "https://github.com/rljson/fs-agent/issues",