@rljson/fs-agent 0.0.8 → 0.0.10
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 +21 -0
- package/dist/fs-agent.js +94 -31
- package/dist/index.d.ts +1 -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,7 +73,21 @@ 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
|
}
|
|
89
|
+
/** Filename for sync error log written to the sync folder */
|
|
90
|
+
export declare const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
77
91
|
/**
|
|
78
92
|
* Orchestrates filesystem operations with tree structures and blob storage
|
|
79
93
|
*/
|
|
@@ -111,6 +125,13 @@ export declare class FsAgent {
|
|
|
111
125
|
* Gets the current timeout configuration
|
|
112
126
|
*/
|
|
113
127
|
get timeouts(): Required<TimeoutConfig>;
|
|
128
|
+
/**
|
|
129
|
+
* Appends a sync error entry to the error log file in the sync folder.
|
|
130
|
+
* Uses synchronous I/O to guarantee the write completes even in catch blocks.
|
|
131
|
+
* @param context - Label identifying where the error occurred
|
|
132
|
+
* @param err - The error value caught
|
|
133
|
+
*/
|
|
134
|
+
_writeSyncError(context: string, err: unknown): void;
|
|
114
135
|
/**
|
|
115
136
|
* Wraps a promise with a timeout.
|
|
116
137
|
* Rejects with a descriptive error if the promise does not settle
|
package/dist/fs-agent.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BsMem } from "@rljson/bs";
|
|
2
2
|
import { Route, createTreesTableCfg } from "@rljson/rljson";
|
|
3
|
+
import { watch, appendFileSync } from "fs";
|
|
3
4
|
import { stat, readFile, mkdir, writeFile, readdir, utimes, rm } from "fs/promises";
|
|
4
5
|
import { dirname, join, sep } from "path";
|
|
5
6
|
import { hip } from "@rljson/hash";
|
|
6
|
-
import { watch } from "fs";
|
|
7
7
|
import { Db } from "@rljson/db";
|
|
8
8
|
import { IoMem, createSocketPair } from "@rljson/io";
|
|
9
9
|
import { Server, Client } from "@rljson/server";
|
|
@@ -484,8 +484,11 @@ const DEFAULT_TIMEOUTS = {
|
|
|
484
484
|
extract: 15e3,
|
|
485
485
|
restore: 15e3,
|
|
486
486
|
syncCallback: 25e3,
|
|
487
|
-
debounceMs: 300
|
|
487
|
+
debounceMs: 300,
|
|
488
|
+
processRefRetries: 3,
|
|
489
|
+
processRefRetryDelayMs: 5e3
|
|
488
490
|
};
|
|
491
|
+
const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
489
492
|
class FsAgent {
|
|
490
493
|
_scanner;
|
|
491
494
|
_adapter;
|
|
@@ -505,7 +508,11 @@ class FsAgent {
|
|
|
505
508
|
this._db = options.db;
|
|
506
509
|
this._treeKey = options.treeKey;
|
|
507
510
|
this._timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
|
|
508
|
-
this._scanner = new FsScanner(rootPath, {
|
|
511
|
+
this._scanner = new FsScanner(rootPath, {
|
|
512
|
+
...options,
|
|
513
|
+
ignore: [...options.ignore || [], SYNC_ERROR_FILE],
|
|
514
|
+
bs: this._bs
|
|
515
|
+
});
|
|
509
516
|
this._adapter = new FsBlobAdapter(this._bs);
|
|
510
517
|
if (this._db && this._treeKey) {
|
|
511
518
|
this._startAutoSync().catch(() => {
|
|
@@ -544,6 +551,23 @@ class FsAgent {
|
|
|
544
551
|
get timeouts() {
|
|
545
552
|
return this._timeouts;
|
|
546
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Appends a sync error entry to the error log file in the sync folder.
|
|
556
|
+
* Uses synchronous I/O to guarantee the write completes even in catch blocks.
|
|
557
|
+
* @param context - Label identifying where the error occurred
|
|
558
|
+
* @param err - The error value caught
|
|
559
|
+
*/
|
|
560
|
+
_writeSyncError(context, err) {
|
|
561
|
+
try {
|
|
562
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
563
|
+
const msg = err instanceof Error ? `${err.message}
|
|
564
|
+
${err.stack}` : String(err);
|
|
565
|
+
const entry = `[${ts}] ${context}: ${msg}
|
|
566
|
+
`;
|
|
567
|
+
appendFileSync(join(this._rootPath, SYNC_ERROR_FILE), entry);
|
|
568
|
+
} catch {
|
|
569
|
+
}
|
|
570
|
+
}
|
|
547
571
|
/**
|
|
548
572
|
* Wraps a promise with a timeout.
|
|
549
573
|
* Rejects with a descriptive error if the promise does not settle
|
|
@@ -772,6 +796,14 @@ class FsAgent {
|
|
|
772
796
|
if (error instanceof Error && error.message.startsWith("Timeout")) {
|
|
773
797
|
throw error;
|
|
774
798
|
}
|
|
799
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
800
|
+
console.warn(
|
|
801
|
+
`[FsAgent] _fetchTreeRecursively: db.get failed for hash=${currentHash.slice(0, 8)}…: ${errMsg}`
|
|
802
|
+
);
|
|
803
|
+
this._writeSyncError(
|
|
804
|
+
`fetchTree/db.get(${currentHash.slice(0, 8)}…)`,
|
|
805
|
+
error
|
|
806
|
+
);
|
|
775
807
|
continue;
|
|
776
808
|
}
|
|
777
809
|
const treeData = result?.rljson?.[treeKey];
|
|
@@ -938,7 +970,9 @@ class FsAgent {
|
|
|
938
970
|
if (ref) {
|
|
939
971
|
await this._sendRef(connector, ref);
|
|
940
972
|
}
|
|
941
|
-
} catch {
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.error("[FsAgent] syncToDb failed:", err);
|
|
975
|
+
this._writeSyncError("syncToDb", err);
|
|
942
976
|
}
|
|
943
977
|
}
|
|
944
978
|
}, this._timeouts.debounceMs);
|
|
@@ -1020,36 +1054,64 @@ class FsAgent {
|
|
|
1020
1054
|
let pendingRef = null;
|
|
1021
1055
|
let fromDbTimer = null;
|
|
1022
1056
|
const processRef = async (treeRef) => {
|
|
1023
|
-
this.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1057
|
+
const maxAttempts = this._timeouts.processRefRetries + 1;
|
|
1058
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1059
|
+
this._scanner.pauseWatch();
|
|
1060
|
+
try {
|
|
1061
|
+
const incomingTree = await FsAgent._withTimeout(
|
|
1062
|
+
this._fetchTreeFromDb(db, treeKey, treeRef),
|
|
1063
|
+
this._timeouts.fetchTree,
|
|
1064
|
+
`syncFromDb → fetchTree(${treeKey}@${treeRef.slice(0, 8)}…)`
|
|
1065
|
+
);
|
|
1066
|
+
const incomingNodeCount = incomingTree.trees.size;
|
|
1067
|
+
console.log(
|
|
1068
|
+
`[FsAgent] syncFromDb: fetched tree with ${incomingNodeCount} nodes for ref=${treeRef.slice(0, 8)}…`
|
|
1069
|
+
);
|
|
1070
|
+
const currentTree = await FsAgent._withTimeout(
|
|
1071
|
+
this.extract(),
|
|
1072
|
+
this._timeouts.extract,
|
|
1073
|
+
`syncFromDb → extract(${this._rootPath})`
|
|
1074
|
+
);
|
|
1075
|
+
if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
|
|
1076
|
+
const incomingFiles = this._getFileContentMap(incomingTree);
|
|
1077
|
+
const currentFiles = this._getFileContentMap(currentTree);
|
|
1078
|
+
console.log(
|
|
1079
|
+
`[FsAgent] syncFromDb: equivalent content, skipping restore (incoming=${incomingFiles.size} entries, current=${currentFiles.size} entries, ref=${treeRef.slice(0, 8)}…)`
|
|
1080
|
+
);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
await FsAgent._withTimeout(
|
|
1084
|
+
this.restore(incomingTree, void 0, restoreOptions),
|
|
1085
|
+
this._timeouts.restore,
|
|
1086
|
+
`syncFromDb → restore(${treeKey})`
|
|
1087
|
+
);
|
|
1088
|
+
const postRestoreTree = await this._scanner.scan();
|
|
1089
|
+
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
1090
|
+
const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
|
|
1091
|
+
skipNotification: true
|
|
1092
|
+
});
|
|
1093
|
+
this._lastSentRef = postRestoreRef;
|
|
1094
|
+
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1036
1095
|
return;
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
if (attempt === maxAttempts) {
|
|
1098
|
+
console.error(
|
|
1099
|
+
`[FsAgent] syncFromDb processRef failed after ${maxAttempts} attempts:`,
|
|
1100
|
+
err
|
|
1101
|
+
);
|
|
1102
|
+
this._writeSyncError("syncFromDb/processRef", err);
|
|
1103
|
+
} else {
|
|
1104
|
+
const delaySec = attempt * this._timeouts.processRefRetryDelayMs / 1e3;
|
|
1105
|
+
console.warn(
|
|
1106
|
+
`[FsAgent] syncFromDb: attempt ${attempt}/${maxAttempts} failed for ref=${treeRef.slice(0, 8)}…, retrying in ${delaySec}s: ${err instanceof Error ? err.message : String(err)}`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
} finally {
|
|
1110
|
+
this._scanner.resumeWatch();
|
|
1037
1111
|
}
|
|
1038
|
-
await
|
|
1039
|
-
|
|
1040
|
-
this._timeouts.restore,
|
|
1041
|
-
`syncFromDb → restore(${treeKey})`
|
|
1112
|
+
await new Promise(
|
|
1113
|
+
(r) => setTimeout(r, attempt * this._timeouts.processRefRetryDelayMs)
|
|
1042
1114
|
);
|
|
1043
|
-
const postRestoreTree = await this._scanner.scan();
|
|
1044
|
-
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
1045
|
-
const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
|
|
1046
|
-
skipNotification: true
|
|
1047
|
-
});
|
|
1048
|
-
this._lastSentRef = postRestoreRef;
|
|
1049
|
-
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1050
|
-
} catch {
|
|
1051
|
-
} finally {
|
|
1052
|
-
this._scanner.resumeWatch();
|
|
1053
1115
|
}
|
|
1054
1116
|
};
|
|
1055
1117
|
const syncCallback = async (treeRef) => {
|
|
@@ -1201,5 +1263,6 @@ export {
|
|
|
1201
1263
|
FsBlobAdapter,
|
|
1202
1264
|
FsDbAdapter,
|
|
1203
1265
|
FsScanner,
|
|
1266
|
+
SYNC_ERROR_FILE,
|
|
1204
1267
|
runClientServerSetup
|
|
1205
1268
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FsAgent, type FsAgentOptions, type RestoreOptions, type TimeoutConfig, } from './fs-agent.ts';
|
|
1
|
+
export { FsAgent, SYNC_ERROR_FILE, type FsAgentOptions, type RestoreOptions, type TimeoutConfig, } from './fs-agent.ts';
|
|
2
2
|
export { FsBlobAdapter, type BlobToFileOptions, type FileBlobMeta, type FileToBlobOptions, } from './fs-blob-adapter.ts';
|
|
3
3
|
export { FsDbAdapter, type StoreFsTreeOptions } from './fs-db-adapter.ts';
|
|
4
4
|
export { FsScanner, type FsChange, type FsChangeCallback, type FsChangeType, type FsNodeMeta, type FsScanOptions, type FsTree, } from './fs-scanner.ts';
|