@rljson/fs-agent 0.0.19 → 0.0.20

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
@@ -441,6 +441,8 @@ class FsScanner {
441
441
  _nextBlobCache = /* @__PURE__ */ new Map();
442
442
  /** Ensures the persisted cache is read from disk only once. */
443
443
  _cacheLoaded = false;
444
+ /** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
445
+ _vanishedDuringScan = 0;
444
446
  constructor(rootPath, options = {}) {
445
447
  this._rootPath = rootPath;
446
448
  this._options = {
@@ -485,6 +487,7 @@ class FsScanner {
485
487
  this._nextBlobCache = /* @__PURE__ */ new Map();
486
488
  }
487
489
  const trees = /* @__PURE__ */ new Map();
490
+ this._vanishedDuringScan = 0;
488
491
  let rootTree;
489
492
  try {
490
493
  rootTree = await this._scanDirectory(this._rootPath, ".", 0, trees);
@@ -500,6 +503,11 @@ class FsScanner {
500
503
  "Failed to generate hash for root tree. Tree structure may be invalid."
501
504
  );
502
505
  }
506
+ if (this._vanishedDuringScan > 0) {
507
+ console.warn(
508
+ `[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} — skipped; the next scan picks up the settled state`
509
+ );
510
+ }
503
511
  trees.set(rootHashStr, rootTree);
504
512
  this._tree = {
505
513
  rootHash: rootHashStr,
@@ -513,6 +521,7 @@ class FsScanner {
513
521
  }
514
522
  async _scanDirectory(absolutePath, relativePath, depth, trees) {
515
523
  const entries = await readdir(absolutePath, { withFileTypes: true });
524
+ const childTrees = [];
516
525
  const childRefs = [];
517
526
  for (const entry of entries) {
518
527
  if (this._shouldIgnore(entry.name)) {
@@ -526,78 +535,87 @@ class FsScanner {
526
535
  if (this._options.maxDepth !== void 0 && depth >= this._options.maxDepth) {
527
536
  continue;
528
537
  }
529
- const childStats = await stat(childPath);
530
- if (entry.isDirectory()) {
531
- const childTree = await this._scanDirectory(
532
- childPath,
533
- childRelPath,
534
- depth + 1,
535
- trees
536
- );
537
- hip(childTree);
538
- const childHashStr = childTree._hash;
539
- trees.set(childHashStr, childTree);
540
- childRefs.push(childHashStr);
541
- } else if (entry.isFile()) {
542
- const mtimeMs = childStats.mtime.getTime();
543
- const cached = this._scanCachePath ? this._blobCache.get(childRelPath) : void 0;
544
- let blobId;
545
- if (cached && cached.mtime === mtimeMs && cached.size === childStats.size) {
546
- blobId = cached.blobId;
547
- } else {
548
- let fileContent;
549
- try {
550
- fileContent = await readFile(childPath);
551
- } catch (error) {
552
- throw new Error(
553
- `Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
554
- );
555
- }
556
- let blobProps;
557
- try {
558
- blobProps = await this._bs.setBlob(fileContent);
559
- } catch (error) {
560
- throw new Error(
561
- `Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
562
- );
538
+ try {
539
+ const childStats = await stat(childPath);
540
+ if (entry.isDirectory()) {
541
+ const childTree = await this._scanDirectory(
542
+ childPath,
543
+ childRelPath,
544
+ depth + 1,
545
+ trees
546
+ );
547
+ childTrees.push(childTree);
548
+ hip(childTree);
549
+ const childHashStr = childTree._hash;
550
+ trees.set(childHashStr, childTree);
551
+ childRefs.push(childHashStr);
552
+ } else if (entry.isFile()) {
553
+ const mtimeMs = childStats.mtime.getTime();
554
+ const cached = this._scanCachePath ? this._blobCache.get(childRelPath) : void 0;
555
+ let blobId;
556
+ if (cached && cached.mtime === mtimeMs && cached.size === childStats.size) {
557
+ blobId = cached.blobId;
558
+ } else {
559
+ let fileContent;
560
+ try {
561
+ fileContent = await readFile(childPath);
562
+ } catch (error) {
563
+ if (FsScanner._isVanished(error)) throw error;
564
+ throw new Error(
565
+ `Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
566
+ );
567
+ }
568
+ let blobProps;
569
+ try {
570
+ blobProps = await this._bs.setBlob(fileContent);
571
+ } catch (error) {
572
+ throw new Error(
573
+ `Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
574
+ );
575
+ }
576
+ if (!blobProps || !blobProps.blobId) {
577
+ throw new Error(
578
+ `Blob storage returned invalid blobId for file "${childRelPath}"`
579
+ );
580
+ }
581
+ blobId = blobProps.blobId;
563
582
  }
564
- if (!blobProps || !blobProps.blobId) {
565
- throw new Error(
566
- `Blob storage returned invalid blobId for file "${childRelPath}"`
567
- );
583
+ if (this._scanCachePath) {
584
+ this._nextBlobCache.set(childRelPath, {
585
+ mtime: mtimeMs,
586
+ size: childStats.size,
587
+ blobId
588
+ });
568
589
  }
569
- blobId = blobProps.blobId;
570
- }
571
- if (this._scanCachePath) {
572
- this._nextBlobCache.set(childRelPath, {
573
- mtime: mtimeMs,
590
+ const fileMeta = {
591
+ name: entry.name,
592
+ type: "file",
593
+ relativePath: childRelPath,
574
594
  size: childStats.size,
595
+ // mtime is kept for files (restore preserves it, so it round-trips to
596
+ // the same ref on every client) but NOT for directories (a folder's
597
+ // mtime is per-machine and does not round-trip). The absolute `path`
598
+ // is excluded everywhere — it is folder-specific.
599
+ mtime: mtimeMs,
575
600
  blobId
576
- });
601
+ // Link to content in Bs
602
+ };
603
+ const fileTree = {
604
+ id: entry.name,
605
+ isParent: false,
606
+ meta: fileMeta,
607
+ children: null
608
+ };
609
+ childTrees.push(fileTree);
610
+ hip(fileTree);
611
+ const fileTreeHashStr = fileTree._hash;
612
+ trees.set(fileTreeHashStr, fileTree);
613
+ childRefs.push(fileTreeHashStr);
577
614
  }
578
- const fileMeta = {
579
- name: entry.name,
580
- type: "file",
581
- relativePath: childRelPath,
582
- size: childStats.size,
583
- // mtime is kept for files (restore preserves it, so it round-trips to
584
- // the same ref on every client) but NOT for directories (a folder's
585
- // mtime is per-machine and does not round-trip). The absolute `path`
586
- // is excluded everywhere — it is folder-specific.
587
- mtime: mtimeMs,
588
- blobId
589
- // Link to content in Bs
590
- };
591
- const fileTree = {
592
- id: entry.name,
593
- isParent: false,
594
- meta: fileMeta,
595
- children: null
596
- };
597
- hip(fileTree);
598
- const fileTreeHashStr = fileTree._hash;
599
- trees.set(fileTreeHashStr, fileTree);
600
- childRefs.push(fileTreeHashStr);
615
+ } catch (error) {
616
+ if (!FsScanner._isVanished(error)) throw error;
617
+ this._vanishedDuringScan++;
618
+ continue;
601
619
  }
602
620
  }
603
621
  const dirName = relativePath === "." ? "." : (
@@ -766,6 +784,20 @@ class FsScanner {
766
784
  static _errMessage(err) {
767
785
  return err instanceof Error ? err.message : String(err);
768
786
  }
787
+ /**
788
+ * Whether a caught value is a "no longer there" filesystem error.
789
+ *
790
+ * A scan walks a directory that is still being written to. `readdir` returns
791
+ * a name, and by the time the entry is `stat`ed, read, or descended into it
792
+ * can be gone — a save-and-rename editor, a build tool, a peer applying a
793
+ * deletion. That is ordinary, not exceptional.
794
+ * @param err - The caught value.
795
+ * @returns `true` for ENOENT / ENOTDIR.
796
+ */
797
+ static _isVanished(err) {
798
+ const code = err?.code;
799
+ return code === "ENOENT" || code === "ENOTDIR";
800
+ }
769
801
  /** Whether the host is Windows — gates Windows-specific watcher hardening. */
770
802
  static get _isWindows() {
771
803
  return process.platform === "win32";
@@ -117,6 +117,8 @@ export declare class FsScanner {
117
117
  private _nextBlobCache;
118
118
  /** Ensures the persisted cache is read from disk only once. */
119
119
  private _cacheLoaded;
120
+ /** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
121
+ private _vanishedDuringScan;
120
122
  constructor(rootPath: string, options?: FsScanOptions);
121
123
  get tree(): FsTree | null;
122
124
  get rootPath(): string;
@@ -156,6 +158,17 @@ export declare class FsScanner {
156
158
  * @returns A message string
157
159
  */
158
160
  private static _errMessage;
161
+ /**
162
+ * Whether a caught value is a "no longer there" filesystem error.
163
+ *
164
+ * A scan walks a directory that is still being written to. `readdir` returns
165
+ * a name, and by the time the entry is `stat`ed, read, or descended into it
166
+ * can be gone — a save-and-rename editor, a build tool, a peer applying a
167
+ * deletion. That is ordinary, not exceptional.
168
+ * @param err - The caught value.
169
+ * @returns `true` for ENOENT / ENOTDIR.
170
+ */
171
+ private static _isVanished;
159
172
  /** Whether the host is Windows — gates Windows-specific watcher hardening. */
160
173
  private static get _isWindows();
161
174
  private _handleFileChange;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
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",