@rljson/fs-agent 0.0.58 → 0.0.60

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.
@@ -176,6 +176,30 @@ export declare const TREE_FETCH_CONCURRENCY = 64;
176
176
  * which is the cheap half of the trade.
177
177
  */
178
178
  export declare const RESTORED_BLOB_MEMORY_MAX = 50000;
179
+ /**
180
+ * How many tree children a restore works on at once.
181
+ *
182
+ * A restore's cost is dominated by `getBlob`, and a blob this node does not
183
+ * already hold is a socket round trip. Walking the tree strictly sequentially
184
+ * makes the whole restore `files × RTT` — invisible on localhost, and about
185
+ * sixteen files a second on the lab, where a 1 200-file folder then takes
186
+ * minutes and reads as a stalled node.
187
+ *
188
+ * Sixteen rather than sixty-four: blobs carry file CONTENT, so the ceiling is
189
+ * memory in flight rather than request count, and a folder of large files at
190
+ * high concurrency is how a client runs out of heap.
191
+ */
192
+ export declare const RESTORE_FETCH_CONCURRENCY = 16;
193
+ /**
194
+ * How many of its own past states a node remembers.
195
+ *
196
+ * Used to recognise a sender that is BEHIND this node — one that declares a
197
+ * state this node has held and left. A burst on a large folder produces a few
198
+ * dozen states, so this is generous; forgetting the oldest costs only the
199
+ * ability to spot a very stale sender, which the mass-delete guard still
200
+ * catches.
201
+ */
202
+ export declare const STATE_HISTORY_MAX = 1000;
179
203
  /**
180
204
  * What an agent concluded about an inbound ref.
181
205
  *
@@ -276,6 +300,15 @@ export declare class FsAgent {
276
300
  /** Files written vs left alone by the current {@link restore}. */
277
301
  private _restoreWritten;
278
302
  private _restoreSkipped;
303
+ /**
304
+ * The states this node has held, oldest first.
305
+ *
306
+ * A push declaring one of these — but not the current one — comes from a
307
+ * sender that is behind this node. Applying such a tree is what puts a
308
+ * deleted file back, so it is ignored outright rather than applied
309
+ * additively.
310
+ */
311
+ private readonly _stateHistory;
279
312
  /**
280
313
  * Absolute paths of the files this agent has actually TOLD anyone about.
281
314
  *
@@ -739,6 +772,17 @@ export declare class FsAgent {
739
772
  * its own pushes, and the states it adopts from theirs.
740
773
  * @param tree - The tree that just went out, or was adopted as ours.
741
774
  */
775
+ /**
776
+ * Records a state this node is now in.
777
+ * @param ref - The content ref of that state.
778
+ */
779
+ private _rememberState;
780
+ /**
781
+ * Whether a push comes from a sender this node has already moved past.
782
+ * @param predecessorRefs - What the push declares it descends from.
783
+ * @returns True when every declared parent is a state this node has left.
784
+ */
785
+ private _senderIsBehind;
742
786
  private _rememberAnnounced;
743
787
  private _adoptAppliedRef;
744
788
  /**
package/dist/fs-agent.js CHANGED
@@ -1079,6 +1079,8 @@ const AGENT_STATE_FILE = ".fsagent-state.json";
1079
1079
  const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
1080
1080
  const TREE_FETCH_CONCURRENCY = 64;
1081
1081
  const RESTORED_BLOB_MEMORY_MAX = 5e4;
1082
+ const RESTORE_FETCH_CONCURRENCY = 16;
1083
+ const STATE_HISTORY_MAX = 1e3;
1082
1084
  const VERDICT_REASON = {
1083
1085
  "own-echo": "is this agent's own last advertisement echoed back",
1084
1086
  stale: "is not the newest its sender has advertised"
@@ -1152,6 +1154,15 @@ class FsAgent {
1152
1154
  /** Files written vs left alone by the current {@link restore}. */
1153
1155
  _restoreWritten = 0;
1154
1156
  _restoreSkipped = 0;
1157
+ /**
1158
+ * The states this node has held, oldest first.
1159
+ *
1160
+ * A push declaring one of these — but not the current one — comes from a
1161
+ * sender that is behind this node. Applying such a tree is what puts a
1162
+ * deleted file back, so it is ignored outright rather than applied
1163
+ * additively.
1164
+ */
1165
+ _stateHistory = [];
1155
1166
  /**
1156
1167
  * Absolute paths of the files this agent has actually TOLD anyone about.
1157
1168
  *
@@ -1267,6 +1278,7 @@ class FsAgent {
1267
1278
  }
1268
1279
  }
1269
1280
  _persistCurrentRef(ref) {
1281
+ this._rememberState(ref);
1270
1282
  try {
1271
1283
  writeFileSync(
1272
1284
  join(this._rootPath, AGENT_STATE_FILE),
@@ -1637,9 +1649,26 @@ ${err.stack}` : String(err);
1637
1649
  const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
1638
1650
  await mkdir(dirPath, { recursive: true });
1639
1651
  if (treeNode.children && Array.isArray(treeNode.children)) {
1640
- for (const childHash of treeNode.children) {
1641
- await this._restoreTree(childHash, trees, targetPath, isOwnRoot);
1642
- }
1652
+ const children = [...treeNode.children];
1653
+ let next = 0;
1654
+ const worker = async () => {
1655
+ for (; ; ) {
1656
+ const index = next++;
1657
+ if (index >= children.length) return;
1658
+ await this._restoreTree(
1659
+ children[index],
1660
+ trees,
1661
+ targetPath,
1662
+ isOwnRoot
1663
+ );
1664
+ }
1665
+ };
1666
+ await Promise.all(
1667
+ Array.from(
1668
+ { length: Math.min(RESTORE_FETCH_CONCURRENCY, children.length) },
1669
+ worker
1670
+ )
1671
+ );
1643
1672
  }
1644
1673
  }
1645
1674
  }
@@ -2360,6 +2389,28 @@ ${err.stack}` : String(err);
2360
2389
  * its own pushes, and the states it adopts from theirs.
2361
2390
  * @param tree - The tree that just went out, or was adopted as ours.
2362
2391
  */
2392
+ /**
2393
+ * Records a state this node is now in.
2394
+ * @param ref - The content ref of that state.
2395
+ */
2396
+ _rememberState(ref) {
2397
+ if (this._stateHistory.includes(ref)) return;
2398
+ this._stateHistory.push(ref);
2399
+ if (this._stateHistory.length > STATE_HISTORY_MAX) {
2400
+ this._stateHistory.shift();
2401
+ }
2402
+ }
2403
+ /**
2404
+ * Whether a push comes from a sender this node has already moved past.
2405
+ * @param predecessorRefs - What the push declares it descends from.
2406
+ * @returns True when every declared parent is a state this node has left.
2407
+ */
2408
+ _senderIsBehind(predecessorRefs) {
2409
+ if (predecessorRefs.length === 0) return false;
2410
+ return predecessorRefs.every(
2411
+ (ref) => ref !== this._currentRef && ref !== this._lastAppliedRef && this._stateHistory.includes(ref)
2412
+ );
2413
+ }
2363
2414
  _rememberAnnounced(tree) {
2364
2415
  this._announcedFiles.clear();
2365
2416
  for (const path of this._getFileContentMap(tree).keys()) {
@@ -2531,6 +2582,12 @@ ${err.stack}` : String(err);
2531
2582
  (r) => r !== void 0
2532
2583
  );
2533
2584
  const senderSawMyState = !ancestryIsCarried || !declaresAncestry || statesIAmIn.some((r) => predecessorRefs.includes(r));
2585
+ if (ancestryIsCarried && declaresAncestry && this._senderIsBehind(predecessorRefs)) {
2586
+ console.warn(
2587
+ `[FsAgent] ref=${treeRef.slice(0, 8)}… descends from a state this node has already left — ignoring, the sender will catch up and re-push.`
2588
+ );
2589
+ return;
2590
+ }
2534
2591
  const withholdPrune = restoreOptions?.cleanTarget && (ancestryExpected && !declaresAncestry || !senderSawMyState);
2535
2592
  const applyOptions = withholdPrune ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
2536
2593
  if (applyOptions !== restoreOptions) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
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",