@rljson/fs-agent 0.0.60 → 0.0.62

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.
@@ -300,6 +300,16 @@ export declare class FsAgent {
300
300
  /** Files written vs left alone by the current {@link restore}. */
301
301
  private _restoreWritten;
302
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;
303
313
  /**
304
314
  * The states this node has held, oldest first.
305
315
  *
@@ -783,6 +793,16 @@ export declare class FsAgent {
783
793
  * @returns True when every declared parent is a state this node has left.
784
794
  */
785
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;
786
806
  private _rememberAnnounced;
787
807
  private _adoptAppliedRef;
788
808
  /**
package/dist/fs-agent.js CHANGED
@@ -684,6 +684,32 @@ class FsScanner {
684
684
  } catch {
685
685
  }
686
686
  }
687
+ /**
688
+ * Whether a watcher event's path should be ignored.
689
+ *
690
+ * `fs.watch` in recursive mode reports a path RELATIVE TO THE ROOT
691
+ * (`sub/dir/.fsagent-tmp-abc`), while the ignore patterns are basenames.
692
+ * {@link _shouldIgnore} tests `startsWith`, so a nested match never fired:
693
+ * every atomic write the agent itself makes during a restore came back as a
694
+ * change event, each event triggered a scan, and the debounce that batches a
695
+ * push was reset before it could fire.
696
+ *
697
+ * Measured on the customer's folder: after a 3 642-file restore the watcher
698
+ * reported the SAME newly added file nine times and the agent never emitted a
699
+ * ref for it — `[fs] added: …/probe-….txt` nine times, no `sync:out`. The
700
+ * file did not fail to arrive; it was never sent.
701
+ *
702
+ * Every segment is tested, because the pattern may match a directory as
703
+ * easily as a file.
704
+ * @param relativePath - Path as the watcher reports it.
705
+ * @returns True when any segment matches an ignore pattern.
706
+ */
707
+ _shouldIgnorePath(relativePath) {
708
+ for (const segment of relativePath.split(/[\\/]/)) {
709
+ if (segment && this._shouldIgnore(segment)) return true;
710
+ }
711
+ return false;
712
+ }
687
713
  _shouldIgnore(name) {
688
714
  if (!this._options.ignore) {
689
715
  return false;
@@ -704,7 +730,7 @@ class FsScanner {
704
730
  }
705
731
  const onEvent = async (eventType, filename) => {
706
732
  if (!filename) return;
707
- if (this._shouldIgnore(filename)) {
733
+ if (this._shouldIgnorePath(filename)) {
708
734
  return;
709
735
  }
710
736
  await this._handleFileChange(eventType, filename);
@@ -1154,6 +1180,16 @@ class FsAgent {
1154
1180
  /** Files written vs left alone by the current {@link restore}. */
1155
1181
  _restoreWritten = 0;
1156
1182
  _restoreSkipped = 0;
1183
+ /**
1184
+ * Absolute paths deleted here since the last announcement.
1185
+ *
1186
+ * The mirror of {@link _announcedFiles}. Between a local unlink and the push
1187
+ * that announces it, this node's advertised state still CONTAINS the file —
1188
+ * so a peer pushing in that window descends from a state the file is in, and
1189
+ * an apply re-creates it. The local scan then sees the file present, and the
1190
+ * deletion is never announced at all: silently undone, everywhere.
1191
+ */
1192
+ _pendingDeletes = /* @__PURE__ */ new Set();
1157
1193
  /**
1158
1194
  * The states this node has held, oldest first.
1159
1195
  *
@@ -1605,6 +1641,10 @@ ${err.stack}` : String(err);
1605
1641
  this._restoreSkipped++;
1606
1642
  return;
1607
1643
  }
1644
+ if (this._pendingDeletes.has(filePath)) {
1645
+ this._restoreSkipped++;
1646
+ return;
1647
+ }
1608
1648
  let fileBlob;
1609
1649
  try {
1610
1650
  fileBlob = await this._bs.getBlob(meta.blobId);
@@ -2046,9 +2086,12 @@ ${err.stack}` : String(err);
2046
2086
  this._flushDeferredRescan = () => {
2047
2087
  if (!this._rescanDeferred) return;
2048
2088
  this._rescanDeferred = false;
2049
- debouncedSync({ type: "safety-rescan" });
2089
+ debouncedSync({ type: "safety-rescan", path: "." });
2050
2090
  };
2051
2091
  const debouncedSync = (change) => {
2092
+ if (change?.type === "deleted" && change.path !== ".") {
2093
+ this._pendingDeletes.add(join(this._rootPath, change.path));
2094
+ }
2052
2095
  if (change?.type === "safety-rescan" && this._remoteApplyInFlight) {
2053
2096
  this._rescanDeferred = true;
2054
2097
  return;
@@ -2411,11 +2454,26 @@ ${err.stack}` : String(err);
2411
2454
  (ref) => ref !== this._currentRef && ref !== this._lastAppliedRef && this._stateHistory.includes(ref)
2412
2455
  );
2413
2456
  }
2457
+ /**
2458
+ * Marks an incoming tree's files as known to the network.
2459
+ *
2460
+ * A union, not a replacement: {@link _rememberAnnounced} answers "what this
2461
+ * node has told the network", and this answers "what the network has told
2462
+ * this node". Both make a file prunable; neither alone is the whole set, and
2463
+ * treating the first as the whole set is what made deletions vanish.
2464
+ * @param tree - The tree just applied.
2465
+ */
2466
+ _rememberReceived(tree) {
2467
+ for (const path of this._getFileContentMap(tree).keys()) {
2468
+ this._announcedFiles.add(join(this._rootPath, path));
2469
+ }
2470
+ }
2414
2471
  _rememberAnnounced(tree) {
2415
2472
  this._announcedFiles.clear();
2416
2473
  for (const path of this._getFileContentMap(tree).keys()) {
2417
2474
  this._announcedFiles.add(join(this._rootPath, path));
2418
2475
  }
2476
+ this._pendingDeletes.clear();
2419
2477
  }
2420
2478
  _adoptAppliedRef(connector, treeRef) {
2421
2479
  if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
@@ -2619,6 +2677,7 @@ ${err.stack}` : String(err);
2619
2677
  this._adoptAppliedRef(connector, treeRef);
2620
2678
  this._currentRef = postRestoreRef;
2621
2679
  this._persistCurrentRef(postRestoreRef);
2680
+ this._rememberReceived(incomingTree);
2622
2681
  const postRestoreFiles = this._getFileContentMap(postRestoreTree);
2623
2682
  const incomingFiles = this._getFileContentMap(incomingTree);
2624
2683
  let hasNewsOfOurOwn = false;
@@ -141,6 +141,27 @@ export declare class FsScanner {
141
141
  * (temp + rename). Best-effort: a failed cache write never fails the scan.
142
142
  */
143
143
  private _persistCache;
144
+ /**
145
+ * Whether a watcher event's path should be ignored.
146
+ *
147
+ * `fs.watch` in recursive mode reports a path RELATIVE TO THE ROOT
148
+ * (`sub/dir/.fsagent-tmp-abc`), while the ignore patterns are basenames.
149
+ * {@link _shouldIgnore} tests `startsWith`, so a nested match never fired:
150
+ * every atomic write the agent itself makes during a restore came back as a
151
+ * change event, each event triggered a scan, and the debounce that batches a
152
+ * push was reset before it could fire.
153
+ *
154
+ * Measured on the customer's folder: after a 3 642-file restore the watcher
155
+ * reported the SAME newly added file nine times and the agent never emitted a
156
+ * ref for it — `[fs] added: …/probe-….txt` nine times, no `sync:out`. The
157
+ * file did not fail to arrive; it was never sent.
158
+ *
159
+ * Every segment is tested, because the pattern may match a directory as
160
+ * easily as a file.
161
+ * @param relativePath - Path as the watcher reports it.
162
+ * @returns True when any segment matches an ignore pattern.
163
+ */
164
+ private _shouldIgnorePath;
144
165
  private _shouldIgnore;
145
166
  watch(): Promise<void>;
146
167
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
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",