@rljson/fs-agent 0.0.21 → 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
@@ -1050,6 +1050,14 @@ class FsAgent {
1050
1050
  * view — and undo a deletion the peer just made.
1051
1051
  */
1052
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();
1053
1061
  _timeouts;
1054
1062
  /** Client-only: resolve DAG-branch conflicts into merge revisions. */
1055
1063
  _resolveConflicts;
@@ -1313,7 +1321,19 @@ ${err.stack}` : String(err);
1313
1321
  target
1314
1322
  );
1315
1323
  const preRestore = options?.cleanTarget ? await this._collectAllFiles(target) : /* @__PURE__ */ new Set();
1316
- 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
+ }
1317
1337
  if (options?.cleanTarget) {
1318
1338
  await this._pruneExtraneous(
1319
1339
  target,
@@ -1353,8 +1373,10 @@ ${err.stack}` : String(err);
1353
1373
  * @param treeHash - Hash of the tree node to restore
1354
1374
  * @param trees - Map of all tree nodes
1355
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
1356
1378
  */
1357
- async _restoreTree(treeHash, trees, targetPath) {
1379
+ async _restoreTree(treeHash, trees, targetPath, isOwnRoot) {
1358
1380
  const treeNode = trees.get(treeHash);
1359
1381
  if (!treeNode) {
1360
1382
  throw new Error(`Tree node not found: ${treeHash}`);
@@ -1366,6 +1388,10 @@ ${err.stack}` : String(err);
1366
1388
  if (meta.type === "file") {
1367
1389
  const filePath = join(targetPath, meta.relativePath);
1368
1390
  if (meta.blobId) {
1391
+ if (await this._alreadyOnDisk(filePath, meta, isOwnRoot)) {
1392
+ this._restoreSkipped++;
1393
+ return;
1394
+ }
1369
1395
  let fileBlob;
1370
1396
  try {
1371
1397
  fileBlob = await this._bs.getBlob(meta.blobId);
@@ -1381,21 +1407,93 @@ ${err.stack}` : String(err);
1381
1407
  }
1382
1408
  await mkdir(dirname(filePath), { recursive: true });
1383
1409
  await FsAgent._atomicWriteFile(filePath, fileBlob.content);
1410
+ this._restoreWritten++;
1384
1411
  if (meta.mtime) {
1385
1412
  const mtime = new Date(meta.mtime);
1386
1413
  await utimes(filePath, mtime, mtime);
1387
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
+ }
1388
1422
  }
1389
1423
  } else if (meta.type === "directory") {
1390
1424
  const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
1391
1425
  await mkdir(dirPath, { recursive: true });
1392
1426
  if (treeNode.children && Array.isArray(treeNode.children)) {
1393
1427
  for (const childHash of treeNode.children) {
1394
- await this._restoreTree(childHash, trees, targetPath);
1428
+ await this._restoreTree(childHash, trees, targetPath, isOwnRoot);
1395
1429
  }
1396
1430
  }
1397
1431
  }
1398
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
+ }
1399
1497
  /**
1400
1498
  * Gets the current tree structure
1401
1499
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.21",
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",