@rljson/fs-agent 0.0.39 → 0.0.40

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,15 @@ 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
+ * What an agent concluded about an inbound ref.
181
+ *
182
+ * See `FsAgent._inboundRefVerdict` for why this is one decision rather than
183
+ * several conditions.
184
+ */
185
+ export type InboundRefVerdict = 'apply' | 'own-echo' | 'stale';
186
+ /** How each non-applying verdict reads in a log line. */
187
+ export declare const VERDICT_REASON: Record<Exclude<InboundRefVerdict, 'apply'>, string>;
179
188
  export declare const MASS_DELETE_MIN_FILES = 100;
180
189
  /**
181
190
  * Above this share of the folder, a prune is treated as suspicious rather than
@@ -653,6 +662,48 @@ export declare class FsAgent {
653
662
  * @returns `true` when the tree carries no entries at all.
654
663
  */
655
664
  private _treeIsEmpty;
665
+ /**
666
+ * Whether an inbound ref is news to this agent, and if not, why.
667
+ *
668
+ * "Is this ref news to me" is the question this subsystem keeps getting
669
+ * wrong. It has been answered in five separate places — the connector's sent
670
+ * and received dedup sets, `_lastSentContentKey`, `_lastSentRef`,
671
+ * `_lastAppliedRef` — and each has been wrong at least once:
672
+ *
673
+ * - an agent applied its OWN last advertisement over its own newer edit,
674
+ * destroying it and then declining to re-send (fixed 0.0.30);
675
+ * - a delete propagated only from the client that had created the file,
676
+ * because the send path never retired the state it left (0.0.31);
677
+ * - a refusal consumed the ref it refused, so the same emptiness arriving
678
+ * twice was silent the second time (0.0.33);
679
+ * - a restarted agent inherited its predecessor's conclusions (0.0.34);
680
+ * - a quiet join announced anyway, because the connector broadcasts on
681
+ * local insert and the gate was on the send (0.0.38).
682
+ *
683
+ * Collecting the pre-fetch gates here does not fix a sixth. It makes the
684
+ * question answerable in one place, in one order, with the reasoning
685
+ * attached — which is what the previous five each lacked.
686
+ *
687
+ * Order matters and is deliberate. The own-echo check comes first because it
688
+ * is the only one that holds regardless of what the sender believes: the two
689
+ * defences that ought to catch an echo both miss it. The origin filter
690
+ * compares the payload's origin to this connector's, and a bootstrap carries
691
+ * the SERVER as origin rather than the client the ref came from; the
692
+ * staleness check then measures the server's sequence, which did advance, so
693
+ * the echo reads as news.
694
+ *
695
+ * KNOWN LIMIT, stated because the next person will meet it: this compares
696
+ * against the LAST ref this agent sent, so an echo of an OLDER
697
+ * self-originated ref still gets through. Widening it to a set would also
698
+ * suppress a peer's legitimate revert to a state this agent once held. The
699
+ * real fix is for the bootstrap to carry the originating client, so the
700
+ * origin filter works and this check stops being needed at all.
701
+ * @param treeRef - The inbound ref.
702
+ * @param isNewestFromSender - Whether the connector judged it the newest
703
+ * thing its sender has advertised. Unknown answers `true`.
704
+ * @returns `apply`, or the reason it is not news.
705
+ */
706
+ private _inboundRefVerdict;
656
707
  private _adoptAppliedRef;
657
708
  /**
658
709
  * Derives a deterministic content key from an FsTree.
package/dist/fs-agent.js CHANGED
@@ -1030,6 +1030,10 @@ const AGENT_STATE_FILE = ".fsagent-state.json";
1030
1030
  const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
1031
1031
  const TREE_FETCH_CONCURRENCY = 64;
1032
1032
  const RESTORED_BLOB_MEMORY_MAX = 5e4;
1033
+ const VERDICT_REASON = {
1034
+ "own-echo": "is this agent's own last advertisement echoed back",
1035
+ stale: "is not the newest its sender has advertised"
1036
+ };
1033
1037
  const MASS_DELETE_MIN_FILES = 100;
1034
1038
  const MASS_DELETE_MAX_RATIO = 0.3;
1035
1039
  class RestoreIncompleteError extends Error {
@@ -2183,6 +2187,52 @@ ${err.stack}` : String(err);
2183
2187
  _treeIsEmpty(tree) {
2184
2188
  return this._getFileContentMap(tree).size === 0;
2185
2189
  }
2190
+ /**
2191
+ * Whether an inbound ref is news to this agent, and if not, why.
2192
+ *
2193
+ * "Is this ref news to me" is the question this subsystem keeps getting
2194
+ * wrong. It has been answered in five separate places — the connector's sent
2195
+ * and received dedup sets, `_lastSentContentKey`, `_lastSentRef`,
2196
+ * `_lastAppliedRef` — and each has been wrong at least once:
2197
+ *
2198
+ * - an agent applied its OWN last advertisement over its own newer edit,
2199
+ * destroying it and then declining to re-send (fixed 0.0.30);
2200
+ * - a delete propagated only from the client that had created the file,
2201
+ * because the send path never retired the state it left (0.0.31);
2202
+ * - a refusal consumed the ref it refused, so the same emptiness arriving
2203
+ * twice was silent the second time (0.0.33);
2204
+ * - a restarted agent inherited its predecessor's conclusions (0.0.34);
2205
+ * - a quiet join announced anyway, because the connector broadcasts on
2206
+ * local insert and the gate was on the send (0.0.38).
2207
+ *
2208
+ * Collecting the pre-fetch gates here does not fix a sixth. It makes the
2209
+ * question answerable in one place, in one order, with the reasoning
2210
+ * attached — which is what the previous five each lacked.
2211
+ *
2212
+ * Order matters and is deliberate. The own-echo check comes first because it
2213
+ * is the only one that holds regardless of what the sender believes: the two
2214
+ * defences that ought to catch an echo both miss it. The origin filter
2215
+ * compares the payload's origin to this connector's, and a bootstrap carries
2216
+ * the SERVER as origin rather than the client the ref came from; the
2217
+ * staleness check then measures the server's sequence, which did advance, so
2218
+ * the echo reads as news.
2219
+ *
2220
+ * KNOWN LIMIT, stated because the next person will meet it: this compares
2221
+ * against the LAST ref this agent sent, so an echo of an OLDER
2222
+ * self-originated ref still gets through. Widening it to a set would also
2223
+ * suppress a peer's legitimate revert to a state this agent once held. The
2224
+ * real fix is for the bootstrap to carry the originating client, so the
2225
+ * origin filter works and this check stops being needed at all.
2226
+ * @param treeRef - The inbound ref.
2227
+ * @param isNewestFromSender - Whether the connector judged it the newest
2228
+ * thing its sender has advertised. Unknown answers `true`.
2229
+ * @returns `apply`, or the reason it is not news.
2230
+ */
2231
+ _inboundRefVerdict(treeRef, isNewestFromSender) {
2232
+ if (treeRef === this._lastSentRef) return "own-echo";
2233
+ if (!isNewestFromSender) return "stale";
2234
+ return "apply";
2235
+ }
2186
2236
  _adoptAppliedRef(connector, treeRef) {
2187
2237
  if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
2188
2238
  connector.invalidateSent?.(this._lastAppliedRef);
@@ -2278,17 +2328,14 @@ ${err.stack}` : String(err);
2278
2328
  const processRef = async (treeRef, recoveryAttempt = 0, predecessorRefs, isNewestFromSender = true) => {
2279
2329
  const maxAttempts = this._timeouts.processRefRetries + 1;
2280
2330
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2281
- if (treeRef === this._lastSentRef) {
2331
+ const verdict = this._inboundRefVerdict(treeRef, isNewestFromSender);
2332
+ if (verdict !== "apply") {
2282
2333
  console.warn(
2283
- `[FsAgent] ref=${treeRef.slice(0, 8)}… is this agent's own last advertisement echoed back — ignoring it.`
2334
+ `[FsAgent] ref=${treeRef.slice(0, 8)}… ${VERDICT_REASON[verdict]} — ignoring it.`
2284
2335
  );
2285
- return;
2286
- }
2287
- if (!isNewestFromSender) {
2288
- console.warn(
2289
- `[FsAgent] ref=${treeRef.slice(0, 8)}… is not the newest its sender has advertised — ignoring it.`
2290
- );
2291
- connector.invalidateReceived(treeRef);
2336
+ if (verdict === "stale") {
2337
+ connector.invalidateReceived(treeRef);
2338
+ }
2292
2339
  return;
2293
2340
  }
2294
2341
  this._scanner.pauseWatch();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
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",