@rljson/fs-agent 0.0.9 → 0.0.11
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/README.public.md +10 -0
- package/dist/README.public.md +10 -0
- package/dist/fs-agent.d.ts +12 -0
- package/dist/fs-agent.js +81 -38
- package/dist/fs-scanner.d.ts +9 -1
- package/package.json +1 -1
package/README.public.md
CHANGED
|
@@ -747,10 +747,20 @@ const agent = new FsAgent('./my-project', bs, {
|
|
|
747
747
|
restore: 15_000, // Filesystem restore (default: 15s)
|
|
748
748
|
syncCallback: 25_000, // Overall syncFromDb callback (default: 25s)
|
|
749
749
|
debounceMs: 300, // Coalesce rapid FS events (default: 300ms)
|
|
750
|
+
processRefRetries: 3, // Retry failed refs before dropping (default: 3)
|
|
751
|
+
processRefRetryDelayMs: 5_000, // Base delay between retries (default: 5s)
|
|
750
752
|
},
|
|
751
753
|
});
|
|
752
754
|
```
|
|
753
755
|
|
|
756
|
+
### processRef Retry Behavior
|
|
757
|
+
|
|
758
|
+
When `syncFromDb` receives a tree ref and fails to process it (e.g. `db.get`
|
|
759
|
+
times out because the IoPeer transport hasn't connected yet), the ref is
|
|
760
|
+
retried up to `processRefRetries` times with increasing delay
|
|
761
|
+
(`attempt * processRefRetryDelayMs`). This prevents a single transient
|
|
762
|
+
timeout from permanently breaking the sync pipeline.
|
|
763
|
+
|
|
754
764
|
## Bounce-Back Prevention
|
|
755
765
|
|
|
756
766
|
Bidirectional sync can cause infinite loops when both clients detect each
|
package/dist/README.public.md
CHANGED
|
@@ -747,10 +747,20 @@ const agent = new FsAgent('./my-project', bs, {
|
|
|
747
747
|
restore: 15_000, // Filesystem restore (default: 15s)
|
|
748
748
|
syncCallback: 25_000, // Overall syncFromDb callback (default: 25s)
|
|
749
749
|
debounceMs: 300, // Coalesce rapid FS events (default: 300ms)
|
|
750
|
+
processRefRetries: 3, // Retry failed refs before dropping (default: 3)
|
|
751
|
+
processRefRetryDelayMs: 5_000, // Base delay between retries (default: 5s)
|
|
750
752
|
},
|
|
751
753
|
});
|
|
752
754
|
```
|
|
753
755
|
|
|
756
|
+
### processRef Retry Behavior
|
|
757
|
+
|
|
758
|
+
When `syncFromDb` receives a tree ref and fails to process it (e.g. `db.get`
|
|
759
|
+
times out because the IoPeer transport hasn't connected yet), the ref is
|
|
760
|
+
retried up to `processRefRetries` times with increasing delay
|
|
761
|
+
(`attempt * processRefRetryDelayMs`). This prevents a single transient
|
|
762
|
+
timeout from permanently breaking the sync pipeline.
|
|
763
|
+
|
|
754
764
|
## Bounce-Back Prevention
|
|
755
765
|
|
|
756
766
|
Bidirectional sync can cause infinite loops when both clients detect each
|
package/dist/fs-agent.d.ts
CHANGED
|
@@ -73,6 +73,18 @@ export interface TimeoutConfig {
|
|
|
73
73
|
* Also applies to incoming database refs in syncFromDb.
|
|
74
74
|
*/
|
|
75
75
|
debounceMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Number of retries for processRef in syncFromDb. Default: 3.
|
|
78
|
+
* When a ref fails to process (e.g. db.get timeout because the IoPeer
|
|
79
|
+
* transport hasn't connected yet), the ref is retried this many times
|
|
80
|
+
* with increasing delay before being dropped.
|
|
81
|
+
*/
|
|
82
|
+
processRefRetries?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Base delay between processRef retries (milliseconds). Default: 5 000 ms.
|
|
85
|
+
* Each retry waits `attempt * processRefRetryDelayMs` (i.e. 5s, 10s, 15s).
|
|
86
|
+
*/
|
|
87
|
+
processRefRetryDelayMs?: number;
|
|
76
88
|
}
|
|
77
89
|
/** Filename for sync error log written to the sync folder */
|
|
78
90
|
export declare const SYNC_ERROR_FILE = ".sync-errors.log";
|
package/dist/fs-agent.js
CHANGED
|
@@ -178,6 +178,7 @@ class FsScanner {
|
|
|
178
178
|
_options;
|
|
179
179
|
_bs;
|
|
180
180
|
_paused = false;
|
|
181
|
+
_missedChangesDuringPause = false;
|
|
181
182
|
constructor(rootPath, options = {}) {
|
|
182
183
|
this._rootPath = rootPath;
|
|
183
184
|
this._options = {
|
|
@@ -365,6 +366,7 @@ class FsScanner {
|
|
|
365
366
|
}
|
|
366
367
|
async _handleFileChange(_eventType, filename) {
|
|
367
368
|
if (this._paused) {
|
|
369
|
+
this._missedChangesDuringPause = true;
|
|
368
370
|
return;
|
|
369
371
|
}
|
|
370
372
|
const relativePath = filename.replace(/\\/g, "/");
|
|
@@ -442,12 +444,34 @@ class FsScanner {
|
|
|
442
444
|
*/
|
|
443
445
|
pauseWatch() {
|
|
444
446
|
this._paused = true;
|
|
447
|
+
this._missedChangesDuringPause = false;
|
|
445
448
|
}
|
|
446
449
|
/**
|
|
447
|
-
* Resume file change notifications
|
|
450
|
+
* Resume file change notifications.
|
|
451
|
+
* If any filesystem events were missed during the pause, triggers
|
|
452
|
+
* an asynchronous rescan so that syncToDb can detect and push the changes.
|
|
448
453
|
*/
|
|
449
454
|
resumeWatch() {
|
|
455
|
+
const missedChanges = this._missedChangesDuringPause;
|
|
450
456
|
this._paused = false;
|
|
457
|
+
this._missedChangesDuringPause = false;
|
|
458
|
+
if (missedChanges) {
|
|
459
|
+
void this._rescanAfterPause();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Re-scan the filesystem and fire onChange callbacks to catch modifications
|
|
464
|
+
* that were missed while watching was paused.
|
|
465
|
+
*/
|
|
466
|
+
async _rescanAfterPause() {
|
|
467
|
+
try {
|
|
468
|
+
await this.scan();
|
|
469
|
+
if (this._paused) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
await this._notifyChange({ type: "modified", path: "." });
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
451
475
|
}
|
|
452
476
|
getTreeByHash(treeHash) {
|
|
453
477
|
return this._tree?.trees.get(treeHash);
|
|
@@ -484,7 +508,9 @@ const DEFAULT_TIMEOUTS = {
|
|
|
484
508
|
extract: 15e3,
|
|
485
509
|
restore: 15e3,
|
|
486
510
|
syncCallback: 25e3,
|
|
487
|
-
debounceMs: 300
|
|
511
|
+
debounceMs: 300,
|
|
512
|
+
processRefRetries: 3,
|
|
513
|
+
processRefRetryDelayMs: 5e3
|
|
488
514
|
};
|
|
489
515
|
const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
490
516
|
class FsAgent {
|
|
@@ -1052,47 +1078,64 @@ ${err.stack}` : String(err);
|
|
|
1052
1078
|
let pendingRef = null;
|
|
1053
1079
|
let fromDbTimer = null;
|
|
1054
1080
|
const processRef = async (treeRef) => {
|
|
1055
|
-
this.
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
);
|
|
1066
|
-
const currentTree = await FsAgent._withTimeout(
|
|
1067
|
-
this.extract(),
|
|
1068
|
-
this._timeouts.extract,
|
|
1069
|
-
`syncFromDb → extract(${this._rootPath})`
|
|
1070
|
-
);
|
|
1071
|
-
if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
|
|
1072
|
-
const incomingFiles = this._getFileContentMap(incomingTree);
|
|
1073
|
-
const currentFiles = this._getFileContentMap(currentTree);
|
|
1081
|
+
const maxAttempts = this._timeouts.processRefRetries + 1;
|
|
1082
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1083
|
+
this._scanner.pauseWatch();
|
|
1084
|
+
try {
|
|
1085
|
+
const incomingTree = await FsAgent._withTimeout(
|
|
1086
|
+
this._fetchTreeFromDb(db, treeKey, treeRef),
|
|
1087
|
+
this._timeouts.fetchTree,
|
|
1088
|
+
`syncFromDb → fetchTree(${treeKey}@${treeRef.slice(0, 8)}…)`
|
|
1089
|
+
);
|
|
1090
|
+
const incomingNodeCount = incomingTree.trees.size;
|
|
1074
1091
|
console.log(
|
|
1075
|
-
`[FsAgent] syncFromDb:
|
|
1092
|
+
`[FsAgent] syncFromDb: fetched tree with ${incomingNodeCount} nodes for ref=${treeRef.slice(0, 8)}…`
|
|
1093
|
+
);
|
|
1094
|
+
const currentTree = await FsAgent._withTimeout(
|
|
1095
|
+
this.extract(),
|
|
1096
|
+
this._timeouts.extract,
|
|
1097
|
+
`syncFromDb → extract(${this._rootPath})`
|
|
1098
|
+
);
|
|
1099
|
+
if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
|
|
1100
|
+
const incomingFiles = this._getFileContentMap(incomingTree);
|
|
1101
|
+
const currentFiles = this._getFileContentMap(currentTree);
|
|
1102
|
+
console.log(
|
|
1103
|
+
`[FsAgent] syncFromDb: equivalent content, skipping restore (incoming=${incomingFiles.size} entries, current=${currentFiles.size} entries, ref=${treeRef.slice(0, 8)}…)`
|
|
1104
|
+
);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
await FsAgent._withTimeout(
|
|
1108
|
+
this.restore(incomingTree, void 0, restoreOptions),
|
|
1109
|
+
this._timeouts.restore,
|
|
1110
|
+
`syncFromDb → restore(${treeKey})`
|
|
1076
1111
|
);
|
|
1112
|
+
const postRestoreTree = await this._scanner.scan();
|
|
1113
|
+
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
1114
|
+
const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
|
|
1115
|
+
skipNotification: true
|
|
1116
|
+
});
|
|
1117
|
+
this._lastSentRef = postRestoreRef;
|
|
1118
|
+
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1077
1119
|
return;
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
if (attempt === maxAttempts) {
|
|
1122
|
+
console.error(
|
|
1123
|
+
`[FsAgent] syncFromDb processRef failed after ${maxAttempts} attempts:`,
|
|
1124
|
+
err
|
|
1125
|
+
);
|
|
1126
|
+
this._writeSyncError("syncFromDb/processRef", err);
|
|
1127
|
+
} else {
|
|
1128
|
+
const delaySec = attempt * this._timeouts.processRefRetryDelayMs / 1e3;
|
|
1129
|
+
console.warn(
|
|
1130
|
+
`[FsAgent] syncFromDb: attempt ${attempt}/${maxAttempts} failed for ref=${treeRef.slice(0, 8)}…, retrying in ${delaySec}s: ${err instanceof Error ? err.message : String(err)}`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
} finally {
|
|
1134
|
+
this._scanner.resumeWatch();
|
|
1078
1135
|
}
|
|
1079
|
-
await
|
|
1080
|
-
|
|
1081
|
-
this._timeouts.restore,
|
|
1082
|
-
`syncFromDb → restore(${treeKey})`
|
|
1136
|
+
await new Promise(
|
|
1137
|
+
(r) => setTimeout(r, attempt * this._timeouts.processRefRetryDelayMs)
|
|
1083
1138
|
);
|
|
1084
|
-
const postRestoreTree = await this._scanner.scan();
|
|
1085
|
-
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
1086
|
-
const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
|
|
1087
|
-
skipNotification: true
|
|
1088
|
-
});
|
|
1089
|
-
this._lastSentRef = postRestoreRef;
|
|
1090
|
-
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1091
|
-
} catch (err) {
|
|
1092
|
-
console.error("[FsAgent] syncFromDb processRef failed:", err);
|
|
1093
|
-
this._writeSyncError("syncFromDb/processRef", err);
|
|
1094
|
-
} finally {
|
|
1095
|
-
this._scanner.resumeWatch();
|
|
1096
1139
|
}
|
|
1097
1140
|
};
|
|
1098
1141
|
const syncCallback = async (treeRef) => {
|
package/dist/fs-scanner.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export declare class FsScanner {
|
|
|
72
72
|
private _options;
|
|
73
73
|
private _bs;
|
|
74
74
|
private _paused;
|
|
75
|
+
private _missedChangesDuringPause;
|
|
75
76
|
constructor(rootPath: string, options?: FsScanOptions);
|
|
76
77
|
get tree(): FsTree | null;
|
|
77
78
|
get rootPath(): string;
|
|
@@ -92,9 +93,16 @@ export declare class FsScanner {
|
|
|
92
93
|
*/
|
|
93
94
|
pauseWatch(): void;
|
|
94
95
|
/**
|
|
95
|
-
* Resume file change notifications
|
|
96
|
+
* Resume file change notifications.
|
|
97
|
+
* If any filesystem events were missed during the pause, triggers
|
|
98
|
+
* an asynchronous rescan so that syncToDb can detect and push the changes.
|
|
96
99
|
*/
|
|
97
100
|
resumeWatch(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Re-scan the filesystem and fire onChange callbacks to catch modifications
|
|
103
|
+
* that were missed while watching was paused.
|
|
104
|
+
*/
|
|
105
|
+
private _rescanAfterPause;
|
|
98
106
|
getTreeByHash(treeHash: TreeRef): Tree | undefined;
|
|
99
107
|
getTreeByPath(relativePath: string): Tree | undefined;
|
|
100
108
|
getAllTrees(): Tree[];
|