@rljson/fs-agent 0.0.19 → 0.0.21

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,17 @@ 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} — this scan is a partial picture`
509
+ );
510
+ if (this._tree) {
511
+ return this._tree;
512
+ }
513
+ console.warn(
514
+ `[fs-scanner] no previous scan of ${this._rootPath} to fall back on — starting from the partial one`
515
+ );
516
+ }
503
517
  trees.set(rootHashStr, rootTree);
504
518
  this._tree = {
505
519
  rootHash: rootHashStr,
@@ -513,6 +527,7 @@ class FsScanner {
513
527
  }
514
528
  async _scanDirectory(absolutePath, relativePath, depth, trees) {
515
529
  const entries = await readdir(absolutePath, { withFileTypes: true });
530
+ const childTrees = [];
516
531
  const childRefs = [];
517
532
  for (const entry of entries) {
518
533
  if (this._shouldIgnore(entry.name)) {
@@ -526,78 +541,87 @@ class FsScanner {
526
541
  if (this._options.maxDepth !== void 0 && depth >= this._options.maxDepth) {
527
542
  continue;
528
543
  }
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
- );
544
+ try {
545
+ const childStats = await stat(childPath);
546
+ if (entry.isDirectory()) {
547
+ const childTree = await this._scanDirectory(
548
+ childPath,
549
+ childRelPath,
550
+ depth + 1,
551
+ trees
552
+ );
553
+ childTrees.push(childTree);
554
+ hip(childTree);
555
+ const childHashStr = childTree._hash;
556
+ trees.set(childHashStr, childTree);
557
+ childRefs.push(childHashStr);
558
+ } else if (entry.isFile()) {
559
+ const mtimeMs = childStats.mtime.getTime();
560
+ const cached = this._scanCachePath ? this._blobCache.get(childRelPath) : void 0;
561
+ let blobId;
562
+ if (cached && cached.mtime === mtimeMs && cached.size === childStats.size) {
563
+ blobId = cached.blobId;
564
+ } else {
565
+ let fileContent;
566
+ try {
567
+ fileContent = await readFile(childPath);
568
+ } catch (error) {
569
+ if (FsScanner._isVanished(error)) throw error;
570
+ throw new Error(
571
+ `Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
572
+ );
573
+ }
574
+ let blobProps;
575
+ try {
576
+ blobProps = await this._bs.setBlob(fileContent);
577
+ } catch (error) {
578
+ throw new Error(
579
+ `Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
580
+ );
581
+ }
582
+ if (!blobProps || !blobProps.blobId) {
583
+ throw new Error(
584
+ `Blob storage returned invalid blobId for file "${childRelPath}"`
585
+ );
586
+ }
587
+ blobId = blobProps.blobId;
563
588
  }
564
- if (!blobProps || !blobProps.blobId) {
565
- throw new Error(
566
- `Blob storage returned invalid blobId for file "${childRelPath}"`
567
- );
589
+ if (this._scanCachePath) {
590
+ this._nextBlobCache.set(childRelPath, {
591
+ mtime: mtimeMs,
592
+ size: childStats.size,
593
+ blobId
594
+ });
568
595
  }
569
- blobId = blobProps.blobId;
570
- }
571
- if (this._scanCachePath) {
572
- this._nextBlobCache.set(childRelPath, {
573
- mtime: mtimeMs,
596
+ const fileMeta = {
597
+ name: entry.name,
598
+ type: "file",
599
+ relativePath: childRelPath,
574
600
  size: childStats.size,
601
+ // mtime is kept for files (restore preserves it, so it round-trips to
602
+ // the same ref on every client) but NOT for directories (a folder's
603
+ // mtime is per-machine and does not round-trip). The absolute `path`
604
+ // is excluded everywhere — it is folder-specific.
605
+ mtime: mtimeMs,
575
606
  blobId
576
- });
607
+ // Link to content in Bs
608
+ };
609
+ const fileTree = {
610
+ id: entry.name,
611
+ isParent: false,
612
+ meta: fileMeta,
613
+ children: null
614
+ };
615
+ childTrees.push(fileTree);
616
+ hip(fileTree);
617
+ const fileTreeHashStr = fileTree._hash;
618
+ trees.set(fileTreeHashStr, fileTree);
619
+ childRefs.push(fileTreeHashStr);
577
620
  }
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);
621
+ } catch (error) {
622
+ if (!FsScanner._isVanished(error)) throw error;
623
+ this._vanishedDuringScan++;
624
+ continue;
601
625
  }
602
626
  }
603
627
  const dirName = relativePath === "." ? "." : (
@@ -766,6 +790,20 @@ class FsScanner {
766
790
  static _errMessage(err) {
767
791
  return err instanceof Error ? err.message : String(err);
768
792
  }
793
+ /**
794
+ * Whether a caught value is a "no longer there" filesystem error.
795
+ *
796
+ * A scan walks a directory that is still being written to. `readdir` returns
797
+ * a name, and by the time the entry is `stat`ed, read, or descended into it
798
+ * can be gone — a save-and-rename editor, a build tool, a peer applying a
799
+ * deletion. That is ordinary, not exceptional.
800
+ * @param err - The caught value.
801
+ * @returns `true` for ENOENT / ENOTDIR.
802
+ */
803
+ static _isVanished(err) {
804
+ const code = err?.code;
805
+ return code === "ENOENT" || code === "ENOTDIR";
806
+ }
769
807
  /** Whether the host is Windows — gates Windows-specific watcher hardening. */
770
808
  static get _isWindows() {
771
809
  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.21",
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",