@rljson/fs-agent 0.0.16 → 0.0.17
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.d.ts +7 -0
- package/dist/fs-agent.js +90 -22
- package/dist/fs-scanner.d.ts +29 -0
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface FsAgentOptions {
|
|
|
14
14
|
maxDepth?: number;
|
|
15
15
|
/** Follow symlinks (default: false) */
|
|
16
16
|
followSymlinks?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Persist a path→(mtime, size, blobId) scan cache at this file path so a RESTART
|
|
19
|
+
* does not re-read + re-hash the whole folder (a cold scan of an 80 GB catalog
|
|
20
|
+
* is ~48 min). Forwarded to the {@link FsScanner}. Requires a PERSISTENT blob
|
|
21
|
+
* store (e.g. `@rljson/bs-fs`). See {@link FsScanOptions.scanCachePath}.
|
|
22
|
+
*/
|
|
23
|
+
scanCachePath?: string;
|
|
17
24
|
/** Database instance for automatic syncing */
|
|
18
25
|
db?: Db;
|
|
19
26
|
/** Tree key for database storage */
|
package/dist/fs-agent.js
CHANGED
|
@@ -426,15 +426,25 @@ class FsScanner {
|
|
|
426
426
|
_safetyTimer = null;
|
|
427
427
|
/** Set by stopWatch() so a pending watcher reinstall / rescan bails out. */
|
|
428
428
|
_stopRequested = false;
|
|
429
|
+
/** Path→content cache backing {@link FsScanOptions.scanCachePath} (unused when unset). */
|
|
430
|
+
_scanCachePath;
|
|
431
|
+
/** Last-scan cache (loaded from disk); consulted to skip re-read/re-hash. */
|
|
432
|
+
_blobCache = /* @__PURE__ */ new Map();
|
|
433
|
+
/** Cache rebuilt during the CURRENT scan; becomes `_blobCache` at the end (self-pruning). */
|
|
434
|
+
_nextBlobCache = /* @__PURE__ */ new Map();
|
|
435
|
+
/** Ensures the persisted cache is read from disk only once. */
|
|
436
|
+
_cacheLoaded = false;
|
|
429
437
|
constructor(rootPath, options = {}) {
|
|
430
438
|
this._rootPath = rootPath;
|
|
431
439
|
this._options = {
|
|
432
440
|
ignore: options.ignore || ["node_modules", ".git", "dist", "coverage"],
|
|
433
441
|
maxDepth: options.maxDepth,
|
|
434
442
|
followSymlinks: options.followSymlinks ?? false,
|
|
435
|
-
bs: options.bs
|
|
443
|
+
bs: options.bs,
|
|
444
|
+
scanCachePath: options.scanCachePath
|
|
436
445
|
};
|
|
437
446
|
this._bs = options.bs || new BsMem();
|
|
447
|
+
this._scanCachePath = options.scanCachePath;
|
|
438
448
|
}
|
|
439
449
|
get tree() {
|
|
440
450
|
return this._tree;
|
|
@@ -463,6 +473,10 @@ class FsScanner {
|
|
|
463
473
|
`Cannot access root path "${this._rootPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
464
474
|
);
|
|
465
475
|
}
|
|
476
|
+
if (this._scanCachePath) {
|
|
477
|
+
await this._loadPersistedCache();
|
|
478
|
+
this._nextBlobCache = /* @__PURE__ */ new Map();
|
|
479
|
+
}
|
|
466
480
|
const trees = /* @__PURE__ */ new Map();
|
|
467
481
|
let rootTree;
|
|
468
482
|
try {
|
|
@@ -484,6 +498,10 @@ class FsScanner {
|
|
|
484
498
|
rootHash: rootHashStr,
|
|
485
499
|
trees
|
|
486
500
|
};
|
|
501
|
+
if (this._scanCachePath) {
|
|
502
|
+
this._blobCache = this._nextBlobCache;
|
|
503
|
+
await this._persistCache();
|
|
504
|
+
}
|
|
487
505
|
return this._tree;
|
|
488
506
|
}
|
|
489
507
|
async _scanDirectory(absolutePath, relativePath, depth, trees) {
|
|
@@ -514,26 +532,41 @@ class FsScanner {
|
|
|
514
532
|
trees.set(childHashStr, childTree);
|
|
515
533
|
childRefs.push(childHashStr);
|
|
516
534
|
} else if (entry.isFile()) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
535
|
+
const mtimeMs = childStats.mtime.getTime();
|
|
536
|
+
const cached = this._scanCachePath ? this._blobCache.get(childRelPath) : void 0;
|
|
537
|
+
let blobId;
|
|
538
|
+
if (cached && cached.mtime === mtimeMs && cached.size === childStats.size) {
|
|
539
|
+
blobId = cached.blobId;
|
|
540
|
+
} else {
|
|
541
|
+
let fileContent;
|
|
542
|
+
try {
|
|
543
|
+
fileContent = await readFile(childPath);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
let blobProps;
|
|
550
|
+
try {
|
|
551
|
+
blobProps = await this._bs.setBlob(fileContent);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (!blobProps || !blobProps.blobId) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`Blob storage returned invalid blobId for file "${childRelPath}"`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
blobId = blobProps.blobId;
|
|
532
563
|
}
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
564
|
+
if (this._scanCachePath) {
|
|
565
|
+
this._nextBlobCache.set(childRelPath, {
|
|
566
|
+
mtime: mtimeMs,
|
|
567
|
+
size: childStats.size,
|
|
568
|
+
blobId
|
|
569
|
+
});
|
|
537
570
|
}
|
|
538
571
|
const fileMeta = {
|
|
539
572
|
name: entry.name,
|
|
@@ -544,8 +577,8 @@ class FsScanner {
|
|
|
544
577
|
// the same ref on every client) but NOT for directories (a folder's
|
|
545
578
|
// mtime is per-machine and does not round-trip). The absolute `path`
|
|
546
579
|
// is excluded everywhere — it is folder-specific.
|
|
547
|
-
mtime:
|
|
548
|
-
blobId
|
|
580
|
+
mtime: mtimeMs,
|
|
581
|
+
blobId
|
|
549
582
|
// Link to content in Bs
|
|
550
583
|
};
|
|
551
584
|
const fileTree = {
|
|
@@ -577,6 +610,41 @@ class FsScanner {
|
|
|
577
610
|
};
|
|
578
611
|
return dirTree;
|
|
579
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Load the persisted scan cache from {@link FsScanOptions.scanCachePath} once.
|
|
615
|
+
* A missing or corrupt cache file is tolerated (the scan just starts cold).
|
|
616
|
+
*/
|
|
617
|
+
async _loadPersistedCache() {
|
|
618
|
+
if (this._cacheLoaded) return;
|
|
619
|
+
this._cacheLoaded = true;
|
|
620
|
+
if (!this._scanCachePath) return;
|
|
621
|
+
try {
|
|
622
|
+
const raw = await readFile(this._scanCachePath, "utf8");
|
|
623
|
+
const parsed = JSON.parse(raw);
|
|
624
|
+
if (Array.isArray(parsed.entries)) {
|
|
625
|
+
this._blobCache = new Map(parsed.entries);
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Write the current scan cache to {@link FsScanOptions.scanCachePath} atomically
|
|
632
|
+
* (temp + rename). Best-effort: a failed cache write never fails the scan.
|
|
633
|
+
*/
|
|
634
|
+
async _persistCache() {
|
|
635
|
+
if (!this._scanCachePath) return;
|
|
636
|
+
const body = JSON.stringify({
|
|
637
|
+
version: 1,
|
|
638
|
+
entries: [...this._blobCache.entries()]
|
|
639
|
+
});
|
|
640
|
+
const tmp = `${this._scanCachePath}.${Date.now().toString(36)}.tmp`;
|
|
641
|
+
try {
|
|
642
|
+
await mkdir(dirname(this._scanCachePath), { recursive: true });
|
|
643
|
+
await writeFile(tmp, body);
|
|
644
|
+
await rename(tmp, this._scanCachePath);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
580
648
|
_shouldIgnore(name) {
|
|
581
649
|
if (!this._options.ignore) {
|
|
582
650
|
return false;
|
package/dist/fs-scanner.d.ts
CHANGED
|
@@ -67,6 +67,17 @@ export interface FsScanOptions {
|
|
|
67
67
|
followSymlinks?: boolean;
|
|
68
68
|
/** Blob storage implementation (defaults to BsMem) */
|
|
69
69
|
bs?: Bs;
|
|
70
|
+
/**
|
|
71
|
+
* Persist a path→(mtime, size, blobId) scan cache at this file path. When set,
|
|
72
|
+
* a file whose mtime AND size are unchanged since the last scan is NOT re-read
|
|
73
|
+
* or re-hashed — its cached `blobId` is reused. The cache is loaded once on the
|
|
74
|
+
* first {@link FsScanner.scan} and re-written after every scan, so a RESTART
|
|
75
|
+
* does not re-read the whole folder (a cold scan of an 80 GB catalog is ~48
|
|
76
|
+
* min) and the periodic safety-rescan stays cheap. Requires a PERSISTENT blob
|
|
77
|
+
* store (e.g. `@rljson/bs-fs`) — with an in-RAM store the cached blob is gone
|
|
78
|
+
* after a restart. Omit to disable (default: full read+hash every scan).
|
|
79
|
+
*/
|
|
80
|
+
scanCachePath?: string;
|
|
70
81
|
}
|
|
71
82
|
/**
|
|
72
83
|
* Scans and watches file system changes, extracting RLJSON tree structure
|
|
@@ -84,12 +95,30 @@ export declare class FsScanner {
|
|
|
84
95
|
private _safetyTimer;
|
|
85
96
|
/** Set by stopWatch() so a pending watcher reinstall / rescan bails out. */
|
|
86
97
|
private _stopRequested;
|
|
98
|
+
/** Path→content cache backing {@link FsScanOptions.scanCachePath} (unused when unset). */
|
|
99
|
+
private _scanCachePath?;
|
|
100
|
+
/** Last-scan cache (loaded from disk); consulted to skip re-read/re-hash. */
|
|
101
|
+
private _blobCache;
|
|
102
|
+
/** Cache rebuilt during the CURRENT scan; becomes `_blobCache` at the end (self-pruning). */
|
|
103
|
+
private _nextBlobCache;
|
|
104
|
+
/** Ensures the persisted cache is read from disk only once. */
|
|
105
|
+
private _cacheLoaded;
|
|
87
106
|
constructor(rootPath: string, options?: FsScanOptions);
|
|
88
107
|
get tree(): FsTree | null;
|
|
89
108
|
get rootPath(): string;
|
|
90
109
|
get bs(): Bs;
|
|
91
110
|
scan(): Promise<FsTree>;
|
|
92
111
|
private _scanDirectory;
|
|
112
|
+
/**
|
|
113
|
+
* Load the persisted scan cache from {@link FsScanOptions.scanCachePath} once.
|
|
114
|
+
* A missing or corrupt cache file is tolerated (the scan just starts cold).
|
|
115
|
+
*/
|
|
116
|
+
private _loadPersistedCache;
|
|
117
|
+
/**
|
|
118
|
+
* Write the current scan cache to {@link FsScanOptions.scanCachePath} atomically
|
|
119
|
+
* (temp + rename). Best-effort: a failed cache write never fails the scan.
|
|
120
|
+
*/
|
|
121
|
+
private _persistCache;
|
|
93
122
|
private _shouldIgnore;
|
|
94
123
|
watch(): Promise<void>;
|
|
95
124
|
/**
|