@rljson/fs-agent 0.0.38 → 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.
- package/dist/fs-agent.d.ts +62 -1
- package/dist/fs-agent.js +65 -18
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -166,6 +166,25 @@ export declare const REFUSAL_ANSWER_COOLDOWN_MS = 5000;
|
|
|
166
166
|
* which trades a latency problem for a queueing one.
|
|
167
167
|
*/
|
|
168
168
|
export declare const TREE_FETCH_CONCURRENCY = 64;
|
|
169
|
+
/**
|
|
170
|
+
* How many restored paths an agent remembers writing.
|
|
171
|
+
*
|
|
172
|
+
* The memory exists so a repeat restore recognises this agent's own work
|
|
173
|
+
* without re-reading the file. Nothing ever removed an entry, so a long-lived
|
|
174
|
+
* agent over a large catalogue held one per file it had ever written. Dropping
|
|
175
|
+
* the oldest costs a stat on a file that has not been touched in a long time,
|
|
176
|
+
* which is the cheap half of the trade.
|
|
177
|
+
*/
|
|
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>;
|
|
169
188
|
export declare const MASS_DELETE_MIN_FILES = 100;
|
|
170
189
|
/**
|
|
171
190
|
* Above this share of the folder, a prune is treated as suspicious rather than
|
|
@@ -501,7 +520,7 @@ export declare class FsAgent {
|
|
|
501
520
|
* @param route - Route to tree table
|
|
502
521
|
* @param treeKey - Tree table key
|
|
503
522
|
* @param rootHash - Hash of the root node to start fetching from
|
|
504
|
-
* @returns
|
|
523
|
+
* @returns Every node in the tree, keyed by its content hash
|
|
505
524
|
*/
|
|
506
525
|
private _fetchTreeRecursively;
|
|
507
526
|
/**
|
|
@@ -643,6 +662,48 @@ export declare class FsAgent {
|
|
|
643
662
|
* @returns `true` when the tree carries no entries at all.
|
|
644
663
|
*/
|
|
645
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;
|
|
646
707
|
private _adoptAppliedRef;
|
|
647
708
|
/**
|
|
648
709
|
* Derives a deterministic content key from an FsTree.
|
package/dist/fs-agent.js
CHANGED
|
@@ -1029,6 +1029,11 @@ const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
|
1029
1029
|
const AGENT_STATE_FILE = ".fsagent-state.json";
|
|
1030
1030
|
const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
|
|
1031
1031
|
const TREE_FETCH_CONCURRENCY = 64;
|
|
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
|
+
};
|
|
1032
1037
|
const MASS_DELETE_MIN_FILES = 100;
|
|
1033
1038
|
const MASS_DELETE_MAX_RATIO = 0.3;
|
|
1034
1039
|
class RestoreIncompleteError extends Error {
|
|
@@ -1534,6 +1539,10 @@ ${err.stack}` : String(err);
|
|
|
1534
1539
|
await utimes(filePath, mtime, mtime);
|
|
1535
1540
|
}
|
|
1536
1541
|
if (meta.size !== void 0 && meta.mtime !== void 0) {
|
|
1542
|
+
if (this._restoredBlobs.size >= RESTORED_BLOB_MEMORY_MAX) {
|
|
1543
|
+
const oldest = this._restoredBlobs.keys().next().value;
|
|
1544
|
+
this._restoredBlobs.delete(oldest);
|
|
1545
|
+
}
|
|
1537
1546
|
this._restoredBlobs.set(filePath, {
|
|
1538
1547
|
blobId: meta.blobId,
|
|
1539
1548
|
size: meta.size,
|
|
@@ -1687,7 +1696,7 @@ ${err.stack}` : String(err);
|
|
|
1687
1696
|
* @param route - Route to tree table
|
|
1688
1697
|
* @param treeKey - Tree table key
|
|
1689
1698
|
* @param rootHash - Hash of the root node to start fetching from
|
|
1690
|
-
* @returns
|
|
1699
|
+
* @returns Every node in the tree, keyed by its content hash
|
|
1691
1700
|
*/
|
|
1692
1701
|
async _fetchTreeRecursively(db, route, treeKey, rootHash) {
|
|
1693
1702
|
const fetchedNodes = /* @__PURE__ */ new Map();
|
|
@@ -1760,7 +1769,7 @@ ${err.stack}` : String(err);
|
|
|
1760
1769
|
}
|
|
1761
1770
|
frontier = next;
|
|
1762
1771
|
}
|
|
1763
|
-
return
|
|
1772
|
+
return fetchedNodes;
|
|
1764
1773
|
}
|
|
1765
1774
|
/**
|
|
1766
1775
|
* Fetches tree from database without restoring to filesystem.
|
|
@@ -1780,17 +1789,12 @@ ${err.stack}` : String(err);
|
|
|
1780
1789
|
this._timeouts.fetchTree,
|
|
1781
1790
|
`fetchTree(${treeKey}@${rootRef.slice(0, 8)}…)`
|
|
1782
1791
|
);
|
|
1783
|
-
if (allNodes.
|
|
1792
|
+
if (allNodes.size === 0) {
|
|
1784
1793
|
throw new Error(
|
|
1785
1794
|
`No tree nodes found for ${treeKey}@${rootRef}. The tree may have been deleted or the reference is invalid.`
|
|
1786
1795
|
);
|
|
1787
1796
|
}
|
|
1788
|
-
const trees =
|
|
1789
|
-
for (const treeNode of allNodes) {
|
|
1790
|
-
if (treeNode._hash) {
|
|
1791
|
-
trees.set(treeNode._hash, treeNode);
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1797
|
+
const trees = allNodes;
|
|
1794
1798
|
if (!trees.has(rootRef)) {
|
|
1795
1799
|
throw new Error(
|
|
1796
1800
|
`Root tree node "${rootRef}" not found in tree data. Available hashes: ${Array.from(trees.keys()).slice(0, 5).join(", ")}${trees.size > 5 ? "..." : ""}`
|
|
@@ -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
|
-
|
|
2331
|
+
const verdict = this._inboundRefVerdict(treeRef, isNewestFromSender);
|
|
2332
|
+
if (verdict !== "apply") {
|
|
2282
2333
|
console.warn(
|
|
2283
|
-
`[FsAgent] ref=${treeRef.slice(0, 8)}…
|
|
2334
|
+
`[FsAgent] ref=${treeRef.slice(0, 8)}… ${VERDICT_REASON[verdict]} — ignoring it.`
|
|
2284
2335
|
);
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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();
|