@rljson/fs-agent 0.0.59 → 0.0.61
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 +50 -0
- package/dist/fs-agent.js +73 -1
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -190,6 +190,16 @@ export declare const RESTORED_BLOB_MEMORY_MAX = 50000;
|
|
|
190
190
|
* high concurrency is how a client runs out of heap.
|
|
191
191
|
*/
|
|
192
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;
|
|
193
203
|
/**
|
|
194
204
|
* What an agent concluded about an inbound ref.
|
|
195
205
|
*
|
|
@@ -290,6 +300,25 @@ export declare class FsAgent {
|
|
|
290
300
|
/** Files written vs left alone by the current {@link restore}. */
|
|
291
301
|
private _restoreWritten;
|
|
292
302
|
private _restoreSkipped;
|
|
303
|
+
/**
|
|
304
|
+
* Absolute paths deleted here since the last announcement.
|
|
305
|
+
*
|
|
306
|
+
* The mirror of {@link _announcedFiles}. Between a local unlink and the push
|
|
307
|
+
* that announces it, this node's advertised state still CONTAINS the file —
|
|
308
|
+
* so a peer pushing in that window descends from a state the file is in, and
|
|
309
|
+
* an apply re-creates it. The local scan then sees the file present, and the
|
|
310
|
+
* deletion is never announced at all: silently undone, everywhere.
|
|
311
|
+
*/
|
|
312
|
+
private readonly _pendingDeletes;
|
|
313
|
+
/**
|
|
314
|
+
* The states this node has held, oldest first.
|
|
315
|
+
*
|
|
316
|
+
* A push declaring one of these — but not the current one — comes from a
|
|
317
|
+
* sender that is behind this node. Applying such a tree is what puts a
|
|
318
|
+
* deleted file back, so it is ignored outright rather than applied
|
|
319
|
+
* additively.
|
|
320
|
+
*/
|
|
321
|
+
private readonly _stateHistory;
|
|
293
322
|
/**
|
|
294
323
|
* Absolute paths of the files this agent has actually TOLD anyone about.
|
|
295
324
|
*
|
|
@@ -753,6 +782,27 @@ export declare class FsAgent {
|
|
|
753
782
|
* its own pushes, and the states it adopts from theirs.
|
|
754
783
|
* @param tree - The tree that just went out, or was adopted as ours.
|
|
755
784
|
*/
|
|
785
|
+
/**
|
|
786
|
+
* Records a state this node is now in.
|
|
787
|
+
* @param ref - The content ref of that state.
|
|
788
|
+
*/
|
|
789
|
+
private _rememberState;
|
|
790
|
+
/**
|
|
791
|
+
* Whether a push comes from a sender this node has already moved past.
|
|
792
|
+
* @param predecessorRefs - What the push declares it descends from.
|
|
793
|
+
* @returns True when every declared parent is a state this node has left.
|
|
794
|
+
*/
|
|
795
|
+
private _senderIsBehind;
|
|
796
|
+
/**
|
|
797
|
+
* Marks an incoming tree's files as known to the network.
|
|
798
|
+
*
|
|
799
|
+
* A union, not a replacement: {@link _rememberAnnounced} answers "what this
|
|
800
|
+
* node has told the network", and this answers "what the network has told
|
|
801
|
+
* this node". Both make a file prunable; neither alone is the whole set, and
|
|
802
|
+
* treating the first as the whole set is what made deletions vanish.
|
|
803
|
+
* @param tree - The tree just applied.
|
|
804
|
+
*/
|
|
805
|
+
private _rememberReceived;
|
|
756
806
|
private _rememberAnnounced;
|
|
757
807
|
private _adoptAppliedRef;
|
|
758
808
|
/**
|
package/dist/fs-agent.js
CHANGED
|
@@ -1080,6 +1080,7 @@ const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
|
|
|
1080
1080
|
const TREE_FETCH_CONCURRENCY = 64;
|
|
1081
1081
|
const RESTORED_BLOB_MEMORY_MAX = 5e4;
|
|
1082
1082
|
const RESTORE_FETCH_CONCURRENCY = 16;
|
|
1083
|
+
const STATE_HISTORY_MAX = 1e3;
|
|
1083
1084
|
const VERDICT_REASON = {
|
|
1084
1085
|
"own-echo": "is this agent's own last advertisement echoed back",
|
|
1085
1086
|
stale: "is not the newest its sender has advertised"
|
|
@@ -1153,6 +1154,25 @@ class FsAgent {
|
|
|
1153
1154
|
/** Files written vs left alone by the current {@link restore}. */
|
|
1154
1155
|
_restoreWritten = 0;
|
|
1155
1156
|
_restoreSkipped = 0;
|
|
1157
|
+
/**
|
|
1158
|
+
* Absolute paths deleted here since the last announcement.
|
|
1159
|
+
*
|
|
1160
|
+
* The mirror of {@link _announcedFiles}. Between a local unlink and the push
|
|
1161
|
+
* that announces it, this node's advertised state still CONTAINS the file —
|
|
1162
|
+
* so a peer pushing in that window descends from a state the file is in, and
|
|
1163
|
+
* an apply re-creates it. The local scan then sees the file present, and the
|
|
1164
|
+
* deletion is never announced at all: silently undone, everywhere.
|
|
1165
|
+
*/
|
|
1166
|
+
_pendingDeletes = /* @__PURE__ */ new Set();
|
|
1167
|
+
/**
|
|
1168
|
+
* The states this node has held, oldest first.
|
|
1169
|
+
*
|
|
1170
|
+
* A push declaring one of these — but not the current one — comes from a
|
|
1171
|
+
* sender that is behind this node. Applying such a tree is what puts a
|
|
1172
|
+
* deleted file back, so it is ignored outright rather than applied
|
|
1173
|
+
* additively.
|
|
1174
|
+
*/
|
|
1175
|
+
_stateHistory = [];
|
|
1156
1176
|
/**
|
|
1157
1177
|
* Absolute paths of the files this agent has actually TOLD anyone about.
|
|
1158
1178
|
*
|
|
@@ -1268,6 +1288,7 @@ class FsAgent {
|
|
|
1268
1288
|
}
|
|
1269
1289
|
}
|
|
1270
1290
|
_persistCurrentRef(ref) {
|
|
1291
|
+
this._rememberState(ref);
|
|
1271
1292
|
try {
|
|
1272
1293
|
writeFileSync(
|
|
1273
1294
|
join(this._rootPath, AGENT_STATE_FILE),
|
|
@@ -1594,6 +1615,10 @@ ${err.stack}` : String(err);
|
|
|
1594
1615
|
this._restoreSkipped++;
|
|
1595
1616
|
return;
|
|
1596
1617
|
}
|
|
1618
|
+
if (this._pendingDeletes.has(filePath)) {
|
|
1619
|
+
this._restoreSkipped++;
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1597
1622
|
let fileBlob;
|
|
1598
1623
|
try {
|
|
1599
1624
|
fileBlob = await this._bs.getBlob(meta.blobId);
|
|
@@ -2035,9 +2060,12 @@ ${err.stack}` : String(err);
|
|
|
2035
2060
|
this._flushDeferredRescan = () => {
|
|
2036
2061
|
if (!this._rescanDeferred) return;
|
|
2037
2062
|
this._rescanDeferred = false;
|
|
2038
|
-
debouncedSync({ type: "safety-rescan" });
|
|
2063
|
+
debouncedSync({ type: "safety-rescan", path: "." });
|
|
2039
2064
|
};
|
|
2040
2065
|
const debouncedSync = (change) => {
|
|
2066
|
+
if (change?.type === "deleted" && change.path !== ".") {
|
|
2067
|
+
this._pendingDeletes.add(join(this._rootPath, change.path));
|
|
2068
|
+
}
|
|
2041
2069
|
if (change?.type === "safety-rescan" && this._remoteApplyInFlight) {
|
|
2042
2070
|
this._rescanDeferred = true;
|
|
2043
2071
|
return;
|
|
@@ -2378,11 +2406,48 @@ ${err.stack}` : String(err);
|
|
|
2378
2406
|
* its own pushes, and the states it adopts from theirs.
|
|
2379
2407
|
* @param tree - The tree that just went out, or was adopted as ours.
|
|
2380
2408
|
*/
|
|
2409
|
+
/**
|
|
2410
|
+
* Records a state this node is now in.
|
|
2411
|
+
* @param ref - The content ref of that state.
|
|
2412
|
+
*/
|
|
2413
|
+
_rememberState(ref) {
|
|
2414
|
+
if (this._stateHistory.includes(ref)) return;
|
|
2415
|
+
this._stateHistory.push(ref);
|
|
2416
|
+
if (this._stateHistory.length > STATE_HISTORY_MAX) {
|
|
2417
|
+
this._stateHistory.shift();
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Whether a push comes from a sender this node has already moved past.
|
|
2422
|
+
* @param predecessorRefs - What the push declares it descends from.
|
|
2423
|
+
* @returns True when every declared parent is a state this node has left.
|
|
2424
|
+
*/
|
|
2425
|
+
_senderIsBehind(predecessorRefs) {
|
|
2426
|
+
if (predecessorRefs.length === 0) return false;
|
|
2427
|
+
return predecessorRefs.every(
|
|
2428
|
+
(ref) => ref !== this._currentRef && ref !== this._lastAppliedRef && this._stateHistory.includes(ref)
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Marks an incoming tree's files as known to the network.
|
|
2433
|
+
*
|
|
2434
|
+
* A union, not a replacement: {@link _rememberAnnounced} answers "what this
|
|
2435
|
+
* node has told the network", and this answers "what the network has told
|
|
2436
|
+
* this node". Both make a file prunable; neither alone is the whole set, and
|
|
2437
|
+
* treating the first as the whole set is what made deletions vanish.
|
|
2438
|
+
* @param tree - The tree just applied.
|
|
2439
|
+
*/
|
|
2440
|
+
_rememberReceived(tree) {
|
|
2441
|
+
for (const path of this._getFileContentMap(tree).keys()) {
|
|
2442
|
+
this._announcedFiles.add(join(this._rootPath, path));
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2381
2445
|
_rememberAnnounced(tree) {
|
|
2382
2446
|
this._announcedFiles.clear();
|
|
2383
2447
|
for (const path of this._getFileContentMap(tree).keys()) {
|
|
2384
2448
|
this._announcedFiles.add(join(this._rootPath, path));
|
|
2385
2449
|
}
|
|
2450
|
+
this._pendingDeletes.clear();
|
|
2386
2451
|
}
|
|
2387
2452
|
_adoptAppliedRef(connector, treeRef) {
|
|
2388
2453
|
if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
|
|
@@ -2549,6 +2614,12 @@ ${err.stack}` : String(err);
|
|
|
2549
2614
|
(r) => r !== void 0
|
|
2550
2615
|
);
|
|
2551
2616
|
const senderSawMyState = !ancestryIsCarried || !declaresAncestry || statesIAmIn.some((r) => predecessorRefs.includes(r));
|
|
2617
|
+
if (ancestryIsCarried && declaresAncestry && this._senderIsBehind(predecessorRefs)) {
|
|
2618
|
+
console.warn(
|
|
2619
|
+
`[FsAgent] ref=${treeRef.slice(0, 8)}… descends from a state this node has already left — ignoring, the sender will catch up and re-push.`
|
|
2620
|
+
);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2552
2623
|
const withholdPrune = restoreOptions?.cleanTarget && (ancestryExpected && !declaresAncestry || !senderSawMyState);
|
|
2553
2624
|
const applyOptions = withholdPrune ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
|
|
2554
2625
|
if (applyOptions !== restoreOptions) {
|
|
@@ -2580,6 +2651,7 @@ ${err.stack}` : String(err);
|
|
|
2580
2651
|
this._adoptAppliedRef(connector, treeRef);
|
|
2581
2652
|
this._currentRef = postRestoreRef;
|
|
2582
2653
|
this._persistCurrentRef(postRestoreRef);
|
|
2654
|
+
this._rememberReceived(incomingTree);
|
|
2583
2655
|
const postRestoreFiles = this._getFileContentMap(postRestoreTree);
|
|
2584
2656
|
const incomingFiles = this._getFileContentMap(incomingTree);
|
|
2585
2657
|
let hasNewsOfOurOwn = false;
|