@rljson/fs-agent 0.0.56 → 0.0.58

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.
@@ -276,6 +276,14 @@ export declare class FsAgent {
276
276
  /** Files written vs left alone by the current {@link restore}. */
277
277
  private _restoreWritten;
278
278
  private _restoreSkipped;
279
+ /**
280
+ * Absolute paths of the files this agent has actually TOLD anyone about.
281
+ *
282
+ * A prune may only remove a file that peers could know exists. Anything
283
+ * written since the last announcement is invisible to every sender, so a
284
+ * tree that lacks it is not deleting it — it simply predates it.
285
+ */
286
+ private readonly _announcedFiles;
279
287
  /**
280
288
  * Files this restore DELETED.
281
289
  *
@@ -724,6 +732,14 @@ export declare class FsAgent {
724
732
  * @returns `apply`, or the reason it is not news.
725
733
  */
726
734
  private _inboundRefVerdict;
735
+ /**
736
+ * Records the files an announcement carried.
737
+ *
738
+ * Called wherever this agent commits to a state as "what peers know" —
739
+ * its own pushes, and the states it adopts from theirs.
740
+ * @param tree - The tree that just went out, or was adopted as ours.
741
+ */
742
+ private _rememberAnnounced;
727
743
  private _adoptAppliedRef;
728
744
  /**
729
745
  * Derives a deterministic content key from an FsTree.
package/dist/fs-agent.js CHANGED
@@ -1152,6 +1152,14 @@ class FsAgent {
1152
1152
  /** Files written vs left alone by the current {@link restore}. */
1153
1153
  _restoreWritten = 0;
1154
1154
  _restoreSkipped = 0;
1155
+ /**
1156
+ * Absolute paths of the files this agent has actually TOLD anyone about.
1157
+ *
1158
+ * A prune may only remove a file that peers could know exists. Anything
1159
+ * written since the last announcement is invisible to every sender, so a
1160
+ * tree that lacks it is not deleting it — it simply predates it.
1161
+ */
1162
+ _announcedFiles = /* @__PURE__ */ new Set();
1155
1163
  /**
1156
1164
  * Files this restore DELETED.
1157
1165
  *
@@ -1939,6 +1947,9 @@ ${err.stack}` : String(err);
1939
1947
  }
1940
1948
  }
1941
1949
  } else if (!expectedFiles.has(fullPath) && preRestore.has(fullPath)) {
1950
+ if (this._announcedFiles.size > 0 && !this._announcedFiles.has(fullPath)) {
1951
+ continue;
1952
+ }
1942
1953
  await rm(fullPath, { force: true });
1943
1954
  this._restorePruned++;
1944
1955
  }
@@ -1988,12 +1999,14 @@ ${err.stack}` : String(err);
1988
1999
  this._currentRef = initialRef;
1989
2000
  this._persistCurrentRef(initialRef);
1990
2001
  this._lastSentContentKey = this._contentKeyFromTree(initialTree);
2002
+ this._rememberAnnounced(initialTree);
1991
2003
  }
1992
2004
  if (initialRef && !isSilentJoiner) {
1993
2005
  this._lastSentRef = initialRef;
1994
2006
  this._currentRef = initialRef;
1995
2007
  this._persistCurrentRef(initialRef);
1996
2008
  this._lastSentContentKey = this._contentKeyFromTree(initialTree);
2009
+ this._rememberAnnounced(initialTree);
1997
2010
  await this._sendRef(
1998
2011
  connector,
1999
2012
  initialRef,
@@ -2030,7 +2043,35 @@ ${err.stack}` : String(err);
2030
2043
  );
2031
2044
  const ref = await FsAgent._withRetry(
2032
2045
  () => FsAgent._withTimeout(
2033
- dbAdapter.storeFsTree(tree, { ...options, previous }),
2046
+ // skipNotification, because THIS AGENT broadcasts the ref
2047
+ // itself a few lines below, with the ancestry attached.
2048
+ //
2049
+ // Letting the insert notify sends the ref twice, and the
2050
+ // first copy is the wrong one: the connector attaches
2051
+ // whatever predecessors it currently holds, and this push has
2052
+ // not set its own yet — so the ref goes out carrying the
2053
+ // PREVIOUS push's parent. `_sendRef` then sets the right
2054
+ // ancestry and sends again, and every receiver drops that
2055
+ // second copy as already-received.
2056
+ //
2057
+ // Measured on the lab, sender against receivers:
2058
+ //
2059
+ // sent 6guj63Ox parent yNAJN-wC | seen parent CtAgdd1w
2060
+ // sent UBl35ZQQ parent 6guj63Ox | seen parent yNAJN-wC
2061
+ //
2062
+ // Every push arriving one parent behind. Harmless while
2063
+ // nothing read the ancestry; now that a receiver prunes only
2064
+ // for a sender that names a state it is in, it means a
2065
+ // correctly-formed deletion is refused by everyone — which is
2066
+ // exactly what the large-folder recipe has been reporting.
2067
+ //
2068
+ // The doubled `[sync:out]` line in every log was this, in
2069
+ // plain sight, for the whole investigation.
2070
+ dbAdapter.storeFsTree(tree, {
2071
+ ...options,
2072
+ previous,
2073
+ skipNotification: true
2074
+ }),
2034
2075
  this._timeouts.fetchTree,
2035
2076
  `syncToDb → storeFsTree(${treeKey})`
2036
2077
  ),
@@ -2045,6 +2086,7 @@ ${err.stack}` : String(err);
2045
2086
  }
2046
2087
  this._lastSentRef = ref;
2047
2088
  this._lastSentContentKey = contentKey;
2089
+ this._rememberAnnounced(tree);
2048
2090
  if (parentRef && parentRef !== ref) {
2049
2091
  connector.invalidateReceived(parentRef);
2050
2092
  }
@@ -2311,6 +2353,19 @@ ${err.stack}` : String(err);
2311
2353
  if (!isNewestFromSender) return "stale";
2312
2354
  return "apply";
2313
2355
  }
2356
+ /**
2357
+ * Records the files an announcement carried.
2358
+ *
2359
+ * Called wherever this agent commits to a state as "what peers know" —
2360
+ * its own pushes, and the states it adopts from theirs.
2361
+ * @param tree - The tree that just went out, or was adopted as ours.
2362
+ */
2363
+ _rememberAnnounced(tree) {
2364
+ this._announcedFiles.clear();
2365
+ for (const path of this._getFileContentMap(tree).keys()) {
2366
+ this._announcedFiles.add(join(this._rootPath, path));
2367
+ }
2368
+ }
2314
2369
  _adoptAppliedRef(connector, treeRef) {
2315
2370
  if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
2316
2371
  connector.invalidateSent?.(this._lastAppliedRef);
@@ -2380,6 +2435,7 @@ ${err.stack}` : String(err);
2380
2435
  const ref = await dbAdapter.storeFsTree(tree, { previous });
2381
2436
  this._lastSentRef = ref;
2382
2437
  this._lastSentContentKey = this._contentKeyFromTree(tree);
2438
+ this._rememberAnnounced(tree);
2383
2439
  this._currentRef = ref;
2384
2440
  return ref;
2385
2441
  }
@@ -2443,6 +2499,7 @@ ${err.stack}` : String(err);
2443
2499
  this._currentRef = treeRef;
2444
2500
  this._persistCurrentRef(treeRef);
2445
2501
  this._lastSentContentKey = this._contentKeyFromTree(currentTree);
2502
+ this._rememberAnnounced(currentTree);
2446
2503
  return;
2447
2504
  }
2448
2505
  if (this._resolveConflicts && this._currentRef && predecessorRefs && predecessorRefs.length > 0) {
@@ -2517,6 +2574,7 @@ ${err.stack}` : String(err);
2517
2574
  if (!hasNewsOfOurOwn) {
2518
2575
  this._lastSentRef = postRestoreRef;
2519
2576
  this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
2577
+ this._rememberAnnounced(postRestoreTree);
2520
2578
  if (postRestoreFiles.size < incomingFiles.size) {
2521
2579
  console.log(
2522
2580
  `[FsAgent] ref=${treeRef.slice(0, 8)}… left this node behind (${postRestoreFiles.size} of ${incomingFiles.size} files) — not announcing a state that is catching up.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
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",