@rljson/fs-agent 0.0.31 → 0.0.33

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.
@@ -148,6 +148,15 @@ export declare const AGENT_STATE_FILE = ".fsagent-state.json";
148
148
  * three-file folder is an ordinary edit, and a guard that blocked it would
149
149
  * fire constantly on small trees and be turned off.
150
150
  */
151
+ /**
152
+ * How long an agent waits before answering another refusal.
153
+ *
154
+ * Two nodes can refuse each other — each holding files the other lacks — and an
155
+ * unthrottled answer to every refusal is a loop. Long enough that a genuine
156
+ * catch-up (a restore of the answered ref) completes inside it, short enough
157
+ * that a node joining an idle network is not left waiting.
158
+ */
159
+ export declare const REFUSAL_ANSWER_COOLDOWN_MS = 5000;
151
160
  export declare const MASS_DELETE_MIN_FILES = 100;
152
161
  /**
153
162
  * Above this share of the folder, a prune is treated as suspicious rather than
@@ -212,6 +221,8 @@ export declare class FsAgent {
212
221
  * state can still reach this agent.
213
222
  */
214
223
  private _lastAppliedRef?;
224
+ /** When this agent last answered a refusal, for the cooldown. */
225
+ private _lastRefusalAnswerMs;
215
226
  /** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
216
227
  private _lastSentContentKey?;
217
228
  /**
@@ -598,6 +609,19 @@ export declare class FsAgent {
598
609
  * @param connector - Connector whose dedup sets to retire from.
599
610
  * @param treeRef - The ref that now describes this folder.
600
611
  */
612
+ /**
613
+ * Re-announce this folder's current state after refusing an incoming tree.
614
+ *
615
+ * A refusal means the sender holds less than this node does, so it is the one
616
+ * that needs telling. Without this the network settles into a state that is
617
+ * stable and wrong: the sparse node cannot push, the full node has nothing
618
+ * new to push, and nothing moves until an unrelated edit happens somewhere.
619
+ *
620
+ * Rate-limited because two nodes can refuse each other — each holding files
621
+ * the other lacks — and an unthrottled answer to every refusal is a loop.
622
+ * @param connector - Connector to broadcast on.
623
+ */
624
+ private _readvertiseAfterRefusal;
601
625
  private _adoptAppliedRef;
602
626
  /**
603
627
  * Derives a deterministic content key from an FsTree.
package/dist/fs-agent.js CHANGED
@@ -1027,6 +1027,7 @@ const DISCONNECT_PAUSE_MAX_MS = 3e4;
1027
1027
  const SYNC_ERROR_FILE = ".sync-errors.log";
1028
1028
  const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
1029
1029
  const AGENT_STATE_FILE = ".fsagent-state.json";
1030
+ const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
1030
1031
  const MASS_DELETE_MIN_FILES = 100;
1031
1032
  const MASS_DELETE_MAX_RATIO = 0.3;
1032
1033
  class RestoreIncompleteError extends Error {
@@ -1069,6 +1070,8 @@ class FsAgent {
1069
1070
  * state can still reach this agent.
1070
1071
  */
1071
1072
  _lastAppliedRef;
1073
+ /** When this agent last answered a refusal, for the cooldown. */
1074
+ _lastRefusalAnswerMs = 0;
1072
1075
  /** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
1073
1076
  _lastSentContentKey;
1074
1077
  /**
@@ -2106,6 +2109,33 @@ ${err.stack}` : String(err);
2106
2109
  * @param connector - Connector whose dedup sets to retire from.
2107
2110
  * @param treeRef - The ref that now describes this folder.
2108
2111
  */
2112
+ /**
2113
+ * Re-announce this folder's current state after refusing an incoming tree.
2114
+ *
2115
+ * A refusal means the sender holds less than this node does, so it is the one
2116
+ * that needs telling. Without this the network settles into a state that is
2117
+ * stable and wrong: the sparse node cannot push, the full node has nothing
2118
+ * new to push, and nothing moves until an unrelated edit happens somewhere.
2119
+ *
2120
+ * Rate-limited because two nodes can refuse each other — each holding files
2121
+ * the other lacks — and an unthrottled answer to every refusal is a loop.
2122
+ * @param connector - Connector to broadcast on.
2123
+ */
2124
+ async _readvertiseAfterRefusal(connector) {
2125
+ const ref = this._currentRef;
2126
+ if (!ref) return;
2127
+ const now = Date.now();
2128
+ if (now - this._lastRefusalAnswerMs < REFUSAL_ANSWER_COOLDOWN_MS) return;
2129
+ this._lastRefusalAnswerMs = now;
2130
+ console.warn(
2131
+ `[FsAgent] refused an incoming tree — re-announcing ${ref.slice(0, 8)}… so the sender can catch up.`
2132
+ );
2133
+ try {
2134
+ await this._sendRef(connector, ref);
2135
+ } catch (err) {
2136
+ console.warn(`[FsAgent] re-announcement failed: ${String(err)}`);
2137
+ }
2138
+ }
2109
2139
  _adoptAppliedRef(connector, treeRef) {
2110
2140
  if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
2111
2141
  connector.invalidateSent?.(this._lastAppliedRef);
@@ -2293,6 +2323,8 @@ ${err.stack}` : String(err);
2293
2323
  return;
2294
2324
  } catch (err) {
2295
2325
  if (err instanceof MassDeleteRefusedError) {
2326
+ connector.invalidateReceived(treeRef);
2327
+ await this._readvertiseAfterRefusal(connector);
2296
2328
  return;
2297
2329
  }
2298
2330
  if (err instanceof PartialRestoreError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
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",