@rljson/fs-agent 0.0.25 → 0.0.27
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 +40 -0
- package/dist/fs-agent.js +90 -7
- package/dist/fs-scanner.d.ts +2 -0
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -128,6 +128,16 @@ export declare const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
|
128
128
|
* pollutes the tree or churns the watcher.
|
|
129
129
|
*/
|
|
130
130
|
export declare const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
131
|
+
/**
|
|
132
|
+
* Filename for the agent's own state, kept beside the synced folder's content
|
|
133
|
+
* and ignored by the scanner like the other two above.
|
|
134
|
+
*
|
|
135
|
+
* It holds one thing: the ref this folder was last known to be at. That
|
|
136
|
+
* survives a restart, which is the whole point — a process that comes back
|
|
137
|
+
* with no idea what it descends from cannot declare ancestry, and a push with
|
|
138
|
+
* no ancestry is one every peer has to treat as untrustworthy for deletion.
|
|
139
|
+
*/
|
|
140
|
+
export declare const AGENT_STATE_FILE = ".fsagent-state.json";
|
|
131
141
|
/**
|
|
132
142
|
* Orchestrates filesystem operations with tree structures and blob storage
|
|
133
143
|
*/
|
|
@@ -255,6 +265,36 @@ export declare class FsAgent {
|
|
|
255
265
|
* Gets the current timeout configuration
|
|
256
266
|
*/
|
|
257
267
|
get timeouts(): Required<TimeoutConfig>;
|
|
268
|
+
/**
|
|
269
|
+
* Records the ref this folder is now at, so a restart can still say what it
|
|
270
|
+
* descends from.
|
|
271
|
+
*
|
|
272
|
+
* Best-effort on purpose: losing it costs ancestry on the next start, which
|
|
273
|
+
* degrades to an additive-only apply rather than to anything unsafe, so it
|
|
274
|
+
* must never be worth failing a sync over.
|
|
275
|
+
* @param ref - The ref the folder is now at.
|
|
276
|
+
*/
|
|
277
|
+
/**
|
|
278
|
+
* Starts the watcher unless it is already running.
|
|
279
|
+
*
|
|
280
|
+
* Both `syncToDb` and `syncFromDb` need a live watcher and either may be
|
|
281
|
+
* started first. `syncToDb` used to call `watch()` unconditionally, so
|
|
282
|
+
* starting them in the order `syncFromDb` then `syncToDb` threw "Already
|
|
283
|
+
* watching" — which quietly forced every caller into push-first, the order
|
|
284
|
+
* that lets a reconnecting client overwrite the network with a stale tree.
|
|
285
|
+
* A crash is a poor reason to choose an unsafe order.
|
|
286
|
+
*/
|
|
287
|
+
private _ensureWatching;
|
|
288
|
+
private _persistCurrentRef;
|
|
289
|
+
/**
|
|
290
|
+
* The ref this folder was last recorded at, from a previous run.
|
|
291
|
+
*
|
|
292
|
+
* Absent, unreadable and malformed all mean the same thing — this process
|
|
293
|
+
* cannot vouch for what it descends from — and all answer `undefined`, which
|
|
294
|
+
* the caller treats as "declare no ancestry".
|
|
295
|
+
* @returns The persisted ref, or `undefined`.
|
|
296
|
+
*/
|
|
297
|
+
private _loadPersistedRef;
|
|
258
298
|
/**
|
|
259
299
|
* Appends a sync error entry to the error log file in the sync folder.
|
|
260
300
|
* Uses synchronous I/O to guarantee the write completes even in catch blocks.
|
package/dist/fs-agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BsMem } from "@rljson/bs";
|
|
2
2
|
import { Route, createTreesTableCfg } from "@rljson/rljson";
|
|
3
|
-
import { watch, appendFileSync } from "fs";
|
|
3
|
+
import { watch, writeFileSync, existsSync, readFileSync, appendFileSync } from "fs";
|
|
4
4
|
import { stat, readFile, mkdir, writeFile, readdir, rename, unlink, utimes, rm } from "fs/promises";
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
import { hip } from "@rljson/hash";
|
|
@@ -458,6 +458,10 @@ class FsScanner {
|
|
|
458
458
|
get tree() {
|
|
459
459
|
return this._tree;
|
|
460
460
|
}
|
|
461
|
+
/** Whether a watcher is currently installed. */
|
|
462
|
+
get isWatching() {
|
|
463
|
+
return this._watcher !== null;
|
|
464
|
+
}
|
|
461
465
|
get rootPath() {
|
|
462
466
|
return this._rootPath;
|
|
463
467
|
}
|
|
@@ -1022,6 +1026,7 @@ const DEFAULT_TIMEOUTS = {
|
|
|
1022
1026
|
const DISCONNECT_PAUSE_MAX_MS = 3e4;
|
|
1023
1027
|
const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
1024
1028
|
const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
1029
|
+
const AGENT_STATE_FILE = ".fsagent-state.json";
|
|
1025
1030
|
const MASS_DELETE_MIN_FILES = 100;
|
|
1026
1031
|
const MASS_DELETE_MAX_RATIO = 0.3;
|
|
1027
1032
|
class RestoreIncompleteError extends Error {
|
|
@@ -1105,7 +1110,12 @@ class FsAgent {
|
|
|
1105
1110
|
this._resolveConflicts = options.resolveConflicts ?? false;
|
|
1106
1111
|
this._scanner = new FsScanner(rootPath, {
|
|
1107
1112
|
...options,
|
|
1108
|
-
ignore: [
|
|
1113
|
+
ignore: [
|
|
1114
|
+
...options.ignore || [],
|
|
1115
|
+
SYNC_ERROR_FILE,
|
|
1116
|
+
ATOMIC_TMP_PREFIX,
|
|
1117
|
+
AGENT_STATE_FILE
|
|
1118
|
+
],
|
|
1109
1119
|
bs: this._bs
|
|
1110
1120
|
});
|
|
1111
1121
|
this._adapter = new FsBlobAdapter(this._bs);
|
|
@@ -1146,6 +1156,60 @@ class FsAgent {
|
|
|
1146
1156
|
get timeouts() {
|
|
1147
1157
|
return this._timeouts;
|
|
1148
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Records the ref this folder is now at, so a restart can still say what it
|
|
1161
|
+
* descends from.
|
|
1162
|
+
*
|
|
1163
|
+
* Best-effort on purpose: losing it costs ancestry on the next start, which
|
|
1164
|
+
* degrades to an additive-only apply rather than to anything unsafe, so it
|
|
1165
|
+
* must never be worth failing a sync over.
|
|
1166
|
+
* @param ref - The ref the folder is now at.
|
|
1167
|
+
*/
|
|
1168
|
+
/**
|
|
1169
|
+
* Starts the watcher unless it is already running.
|
|
1170
|
+
*
|
|
1171
|
+
* Both `syncToDb` and `syncFromDb` need a live watcher and either may be
|
|
1172
|
+
* started first. `syncToDb` used to call `watch()` unconditionally, so
|
|
1173
|
+
* starting them in the order `syncFromDb` then `syncToDb` threw "Already
|
|
1174
|
+
* watching" — which quietly forced every caller into push-first, the order
|
|
1175
|
+
* that lets a reconnecting client overwrite the network with a stale tree.
|
|
1176
|
+
* A crash is a poor reason to choose an unsafe order.
|
|
1177
|
+
*/
|
|
1178
|
+
async _ensureWatching() {
|
|
1179
|
+
if (!this._scanner.isWatching) {
|
|
1180
|
+
await this._scanner.watch();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
_persistCurrentRef(ref) {
|
|
1184
|
+
try {
|
|
1185
|
+
writeFileSync(
|
|
1186
|
+
join(this._rootPath, AGENT_STATE_FILE),
|
|
1187
|
+
JSON.stringify({ currentRef: ref }),
|
|
1188
|
+
"utf-8"
|
|
1189
|
+
);
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* The ref this folder was last recorded at, from a previous run.
|
|
1195
|
+
*
|
|
1196
|
+
* Absent, unreadable and malformed all mean the same thing — this process
|
|
1197
|
+
* cannot vouch for what it descends from — and all answer `undefined`, which
|
|
1198
|
+
* the caller treats as "declare no ancestry".
|
|
1199
|
+
* @returns The persisted ref, or `undefined`.
|
|
1200
|
+
*/
|
|
1201
|
+
_loadPersistedRef() {
|
|
1202
|
+
try {
|
|
1203
|
+
const file = join(this._rootPath, AGENT_STATE_FILE);
|
|
1204
|
+
if (!existsSync(file)) return void 0;
|
|
1205
|
+
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
1206
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
1207
|
+
const ref = parsed.currentRef;
|
|
1208
|
+
return typeof ref === "string" && ref.length > 0 ? ref : void 0;
|
|
1209
|
+
} catch {
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1149
1213
|
/**
|
|
1150
1214
|
* Appends a sync error entry to the error log file in the sync folder.
|
|
1151
1215
|
* Uses synchronous I/O to guarantee the write completes even in catch blocks.
|
|
@@ -1791,6 +1855,15 @@ ${err.stack}` : String(err);
|
|
|
1791
1855
|
* @returns Function to stop watching
|
|
1792
1856
|
*/
|
|
1793
1857
|
async syncToDb(db, connector, treeKey, options) {
|
|
1858
|
+
if (this._currentRef === void 0) {
|
|
1859
|
+
const persisted = this._loadPersistedRef();
|
|
1860
|
+
if (persisted !== void 0) {
|
|
1861
|
+
this._currentRef = persisted;
|
|
1862
|
+
console.log(
|
|
1863
|
+
`[FsAgent] resuming from recorded ref ${persisted.slice(0, 8)}… — this folder can declare its ancestry`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1794
1867
|
const initialParentRef = this._currentRef;
|
|
1795
1868
|
const initialTree = await FsAgent._withTimeout(
|
|
1796
1869
|
this.extract(),
|
|
@@ -1810,6 +1883,7 @@ ${err.stack}` : String(err);
|
|
|
1810
1883
|
if (initialRef) {
|
|
1811
1884
|
this._lastSentRef = initialRef;
|
|
1812
1885
|
this._currentRef = initialRef;
|
|
1886
|
+
this._persistCurrentRef(initialRef);
|
|
1813
1887
|
this._lastSentContentKey = this._contentKeyFromTree(initialTree);
|
|
1814
1888
|
await this._sendRef(
|
|
1815
1889
|
connector,
|
|
@@ -1850,6 +1924,8 @@ ${err.stack}` : String(err);
|
|
|
1850
1924
|
`syncToDb storeFsTree(${treeKey})`
|
|
1851
1925
|
);
|
|
1852
1926
|
this._currentRef = ref;
|
|
1927
|
+
this._persistCurrentRef(ref);
|
|
1928
|
+
this._persistCurrentRef(ref);
|
|
1853
1929
|
if (ref === this._lastSentRef) {
|
|
1854
1930
|
return;
|
|
1855
1931
|
}
|
|
@@ -1870,7 +1946,7 @@ ${err.stack}` : String(err);
|
|
|
1870
1946
|
}, this._timeouts.debounceMs);
|
|
1871
1947
|
};
|
|
1872
1948
|
this._scanner.onChange(debouncedSync);
|
|
1873
|
-
await this.
|
|
1949
|
+
await this._ensureWatching();
|
|
1874
1950
|
return () => {
|
|
1875
1951
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1876
1952
|
this._scanner.offChange(debouncedSync);
|
|
@@ -2113,9 +2189,7 @@ ${err.stack}` : String(err);
|
|
|
2113
2189
|
* @returns Function to stop watching
|
|
2114
2190
|
*/
|
|
2115
2191
|
async syncFromDb(db, connector, treeKey, restoreOptions) {
|
|
2116
|
-
|
|
2117
|
-
await this._scanner.watch();
|
|
2118
|
-
}
|
|
2192
|
+
await this._ensureWatching();
|
|
2119
2193
|
let pendingRef = null;
|
|
2120
2194
|
let fromDbTimer = null;
|
|
2121
2195
|
let pendingRecoveryAttempt = 0;
|
|
@@ -2171,8 +2245,16 @@ ${err.stack}` : String(err);
|
|
|
2171
2245
|
return;
|
|
2172
2246
|
}
|
|
2173
2247
|
}
|
|
2248
|
+
const ancestryExpected = this._resolveConflicts;
|
|
2249
|
+
const declaresAncestry = (predecessorRefs?.length ?? 0) > 0;
|
|
2250
|
+
const applyOptions = restoreOptions?.cleanTarget && ancestryExpected && !declaresAncestry ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
|
|
2251
|
+
if (applyOptions !== restoreOptions) {
|
|
2252
|
+
console.warn(
|
|
2253
|
+
`[FsAgent] ref=${treeRef.slice(0, 8)}… declares no ancestry — applying additively, not pruning. A sender that cannot say what it descends from must not delete.`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2174
2256
|
await FsAgent._withTimeout(
|
|
2175
|
-
this.restore(incomingTree, void 0,
|
|
2257
|
+
this.restore(incomingTree, void 0, applyOptions),
|
|
2176
2258
|
this._timeouts.restore,
|
|
2177
2259
|
`syncFromDb → restore(${treeKey})`
|
|
2178
2260
|
);
|
|
@@ -2191,6 +2273,7 @@ ${err.stack}` : String(err);
|
|
|
2191
2273
|
this._lastSentRef = postRestoreRef;
|
|
2192
2274
|
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
2193
2275
|
this._currentRef = postRestoreRef;
|
|
2276
|
+
this._persistCurrentRef(postRestoreRef);
|
|
2194
2277
|
return;
|
|
2195
2278
|
} catch (err) {
|
|
2196
2279
|
if (err instanceof MassDeleteRefusedError) {
|
package/dist/fs-scanner.d.ts
CHANGED
|
@@ -121,6 +121,8 @@ export declare class FsScanner {
|
|
|
121
121
|
private _vanishedDuringScan;
|
|
122
122
|
constructor(rootPath: string, options?: FsScanOptions);
|
|
123
123
|
get tree(): FsTree | null;
|
|
124
|
+
/** Whether a watcher is currently installed. */
|
|
125
|
+
get isWatching(): boolean;
|
|
124
126
|
get rootPath(): string;
|
|
125
127
|
get bs(): Bs;
|
|
126
128
|
scan(): Promise<FsTree>;
|