@rljson/fs-agent 0.0.16 → 0.0.18
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 +13 -0
- package/dist/fs-agent.js +101 -22
- package/dist/fs-scanner.d.ts +29 -0
- package/package.json +3 -3
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 */
|
|
@@ -126,6 +133,12 @@ export declare class FsAgent {
|
|
|
126
133
|
private _stopSync?;
|
|
127
134
|
private _stopSyncFromDb?;
|
|
128
135
|
private _lastSentRef?;
|
|
136
|
+
/**
|
|
137
|
+
* The incoming ref most recently applied. Retired from the connector's dedup
|
|
138
|
+
* sets when the next one supersedes it, so a peer returning the tree to that
|
|
139
|
+
* state can still reach this agent.
|
|
140
|
+
*/
|
|
141
|
+
private _lastAppliedRef?;
|
|
129
142
|
/** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
|
|
130
143
|
private _lastSentContentKey?;
|
|
131
144
|
private _timeouts;
|
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;
|
|
@@ -865,6 +933,12 @@ class FsAgent {
|
|
|
865
933
|
_stopSync;
|
|
866
934
|
_stopSyncFromDb;
|
|
867
935
|
_lastSentRef;
|
|
936
|
+
/**
|
|
937
|
+
* The incoming ref most recently applied. Retired from the connector's dedup
|
|
938
|
+
* sets when the next one supersedes it, so a peer returning the tree to that
|
|
939
|
+
* state can still reach this agent.
|
|
940
|
+
*/
|
|
941
|
+
_lastAppliedRef;
|
|
868
942
|
/** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
|
|
869
943
|
_lastSentContentKey;
|
|
870
944
|
_timeouts;
|
|
@@ -1051,6 +1125,7 @@ ${err.stack}` : String(err);
|
|
|
1051
1125
|
* via an explicit send, which pre-empts the Connector's db-observer path.
|
|
1052
1126
|
*/
|
|
1053
1127
|
async _sendRef(connector, ref, predecessorRefs) {
|
|
1128
|
+
connector.invalidateSent?.(ref);
|
|
1054
1129
|
if (this._resolveConflicts) {
|
|
1055
1130
|
connector.setPredecessors(predecessorRefs ?? []);
|
|
1056
1131
|
}
|
|
@@ -1802,6 +1877,10 @@ ${err.stack}` : String(err);
|
|
|
1802
1877
|
skipNotification: true,
|
|
1803
1878
|
previous
|
|
1804
1879
|
});
|
|
1880
|
+
if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
|
|
1881
|
+
connector.invalidateSent?.(this._lastAppliedRef);
|
|
1882
|
+
}
|
|
1883
|
+
this._lastAppliedRef = treeRef;
|
|
1805
1884
|
this._lastSentRef = postRestoreRef;
|
|
1806
1885
|
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1807
1886
|
this._currentRef = postRestoreRef;
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rljson/fs-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"updateGoldens": "cross-env UPDATE_GOLDENS=true pnpm test"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@rljson/server": "^0.0.
|
|
31
|
+
"@rljson/server": "^0.0.43",
|
|
32
32
|
"@types/node": "^25.3.1",
|
|
33
33
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
34
34
|
"@typescript-eslint/parser": "^8.56.1",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@rljson/bs": "^0.0.21",
|
|
54
|
-
"@rljson/db": "^0.0.
|
|
54
|
+
"@rljson/db": "^0.0.30",
|
|
55
55
|
"@rljson/hash": "^0.0.18",
|
|
56
56
|
"@rljson/io": "^0.0.66",
|
|
57
57
|
"@rljson/json": "^0.0.23",
|