@rljson/fs-agent 0.0.42 → 0.0.43

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.js CHANGED
@@ -443,6 +443,10 @@ class FsScanner {
443
443
  _cacheLoaded = false;
444
444
  /** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
445
445
  _vanishedDuringScan = 0;
446
+ /** The scan pass running right now, if any. See {@link _scanAfterNow}. */
447
+ _activeScan = null;
448
+ /** The single follow-up pass everyone who arrived mid-scan is waiting on. */
449
+ _pendingScan = null;
446
450
  constructor(rootPath, options = {}) {
447
451
  this._rootPath = rootPath;
448
452
  this._options = {
@@ -749,7 +753,7 @@ class FsScanner {
749
753
  }
750
754
  const prevKey = this._tree ? this._safetyContentKey(this._tree) : null;
751
755
  try {
752
- await this.scan();
756
+ await this._scanAfterNow();
753
757
  } catch (err) {
754
758
  console.warn(
755
759
  `[fs-scanner] safety rescan failed: ${FsScanner._errMessage(err)}`
@@ -840,7 +844,7 @@ class FsScanner {
840
844
  try {
841
845
  if (exists) {
842
846
  const existingTree = this._findTreeByPath(relativePath);
843
- await this.scan();
847
+ await this._scanAfterNow();
844
848
  await this._notifyChange({
845
849
  type: existingTree ? "modified" : "added",
846
850
  path: relativePath
@@ -855,12 +859,57 @@ class FsScanner {
855
859
  this.stopWatch();
856
860
  }
857
861
  if (rootExists) {
858
- await this.scan();
862
+ await this._scanAfterNow();
859
863
  await this._notifyChange({ type: "deleted", path: relativePath });
860
864
  }
861
865
  } catch {
862
866
  }
863
867
  }
868
+ /**
869
+ * A scan that is guaranteed to have STARTED after this call, sharing one
870
+ * pass with everyone else who asked while it was running.
871
+ *
872
+ * `scan()` is a whole-tree operation: it walks every directory, stats every
873
+ * file and writes a blob for anything new. Calling it once per watcher event
874
+ * therefore costs O(files²) on a burst — and fs.watch does not await its
875
+ * callback, so those scans all run AT ONCE. Copying 1 200 tiny files into a
876
+ * watched folder took 1 200 concurrent full scans: the CPU sat 82% idle
877
+ * waiting on the filesystem, RSS climbed from 263 MB to 785 MB, and after two
878
+ * minutes the peer had received nothing. On the customer's 3 702-file folder
879
+ * the same burst wedged the client outright.
880
+ *
881
+ * Started-after is the part that cannot be traded away. Joining a scan that
882
+ * began BEFORE the event would let a push carry a tree that predates the file
883
+ * that triggered it — which is precisely the partial trees the lab saw
884
+ * advertised: 64 nodes, then 103, then 204, each a stale snapshot of a folder
885
+ * that already held twelve hundred files. So a caller either starts a scan
886
+ * now, or waits for the one that begins when the current pass ends. A burst
887
+ * of any size collapses to at most two scans, and no change is missed.
888
+ */
889
+ _scanAfterNow() {
890
+ if (!this._activeScan) {
891
+ const started = (async () => {
892
+ try {
893
+ return await this.scan();
894
+ } finally {
895
+ this._activeScan = null;
896
+ }
897
+ })();
898
+ this._activeScan = started;
899
+ return started;
900
+ }
901
+ if (this._pendingScan) return this._pendingScan;
902
+ const queued = (async () => {
903
+ try {
904
+ await this._activeScan;
905
+ } catch {
906
+ }
907
+ this._pendingScan = null;
908
+ return this._scanAfterNow();
909
+ })();
910
+ this._pendingScan = queued;
911
+ return queued;
912
+ }
864
913
  _findTreeByPath(relativePath) {
865
914
  if (!this._tree) return void 0;
866
915
  for (const tree of this._tree.trees.values()) {
@@ -119,6 +119,10 @@ export declare class FsScanner {
119
119
  private _cacheLoaded;
120
120
  /** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
121
121
  private _vanishedDuringScan;
122
+ /** The scan pass running right now, if any. See {@link _scanAfterNow}. */
123
+ private _activeScan;
124
+ /** The single follow-up pass everyone who arrived mid-scan is waiting on. */
125
+ private _pendingScan;
122
126
  constructor(rootPath: string, options?: FsScanOptions);
123
127
  get tree(): FsTree | null;
124
128
  /** Whether a watcher is currently installed. */
@@ -174,6 +178,28 @@ export declare class FsScanner {
174
178
  /** Whether the host is Windows — gates Windows-specific watcher hardening. */
175
179
  private static get _isWindows();
176
180
  private _handleFileChange;
181
+ /**
182
+ * A scan that is guaranteed to have STARTED after this call, sharing one
183
+ * pass with everyone else who asked while it was running.
184
+ *
185
+ * `scan()` is a whole-tree operation: it walks every directory, stats every
186
+ * file and writes a blob for anything new. Calling it once per watcher event
187
+ * therefore costs O(files²) on a burst — and fs.watch does not await its
188
+ * callback, so those scans all run AT ONCE. Copying 1 200 tiny files into a
189
+ * watched folder took 1 200 concurrent full scans: the CPU sat 82% idle
190
+ * waiting on the filesystem, RSS climbed from 263 MB to 785 MB, and after two
191
+ * minutes the peer had received nothing. On the customer's 3 702-file folder
192
+ * the same burst wedged the client outright.
193
+ *
194
+ * Started-after is the part that cannot be traded away. Joining a scan that
195
+ * began BEFORE the event would let a push carry a tree that predates the file
196
+ * that triggered it — which is precisely the partial trees the lab saw
197
+ * advertised: 64 nodes, then 103, then 204, each a stale snapshot of a folder
198
+ * that already held twelve hundred files. So a caller either starts a scan
199
+ * now, or waits for the one that begins when the current pass ends. A burst
200
+ * of any size collapses to at most two scans, and no change is missed.
201
+ */
202
+ private _scanAfterNow;
177
203
  private _findTreeByPath;
178
204
  /**
179
205
  * Whether this notification should escape the pause.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
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",