@rljson/fs-agent 0.0.24 → 0.0.26
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 +29 -0
- package/dist/fs-agent.js +73 -4
- 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,25 @@ 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
|
+
private _persistCurrentRef;
|
|
278
|
+
/**
|
|
279
|
+
* The ref this folder was last recorded at, from a previous run.
|
|
280
|
+
*
|
|
281
|
+
* Absent, unreadable and malformed all mean the same thing — this process
|
|
282
|
+
* cannot vouch for what it descends from — and all answer `undefined`, which
|
|
283
|
+
* the caller treats as "declare no ancestry".
|
|
284
|
+
* @returns The persisted ref, or `undefined`.
|
|
285
|
+
*/
|
|
286
|
+
private _loadPersistedRef;
|
|
258
287
|
/**
|
|
259
288
|
* Appends a sync error entry to the error log file in the sync folder.
|
|
260
289
|
* 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";
|
|
@@ -1022,6 +1022,7 @@ const DEFAULT_TIMEOUTS = {
|
|
|
1022
1022
|
const DISCONNECT_PAUSE_MAX_MS = 3e4;
|
|
1023
1023
|
const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
1024
1024
|
const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
1025
|
+
const AGENT_STATE_FILE = ".fsagent-state.json";
|
|
1025
1026
|
const MASS_DELETE_MIN_FILES = 100;
|
|
1026
1027
|
const MASS_DELETE_MAX_RATIO = 0.3;
|
|
1027
1028
|
class RestoreIncompleteError extends Error {
|
|
@@ -1105,7 +1106,12 @@ class FsAgent {
|
|
|
1105
1106
|
this._resolveConflicts = options.resolveConflicts ?? false;
|
|
1106
1107
|
this._scanner = new FsScanner(rootPath, {
|
|
1107
1108
|
...options,
|
|
1108
|
-
ignore: [
|
|
1109
|
+
ignore: [
|
|
1110
|
+
...options.ignore || [],
|
|
1111
|
+
SYNC_ERROR_FILE,
|
|
1112
|
+
ATOMIC_TMP_PREFIX,
|
|
1113
|
+
AGENT_STATE_FILE
|
|
1114
|
+
],
|
|
1109
1115
|
bs: this._bs
|
|
1110
1116
|
});
|
|
1111
1117
|
this._adapter = new FsBlobAdapter(this._bs);
|
|
@@ -1146,6 +1152,45 @@ class FsAgent {
|
|
|
1146
1152
|
get timeouts() {
|
|
1147
1153
|
return this._timeouts;
|
|
1148
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Records the ref this folder is now at, so a restart can still say what it
|
|
1157
|
+
* descends from.
|
|
1158
|
+
*
|
|
1159
|
+
* Best-effort on purpose: losing it costs ancestry on the next start, which
|
|
1160
|
+
* degrades to an additive-only apply rather than to anything unsafe, so it
|
|
1161
|
+
* must never be worth failing a sync over.
|
|
1162
|
+
* @param ref - The ref the folder is now at.
|
|
1163
|
+
*/
|
|
1164
|
+
_persistCurrentRef(ref) {
|
|
1165
|
+
try {
|
|
1166
|
+
writeFileSync(
|
|
1167
|
+
join(this._rootPath, AGENT_STATE_FILE),
|
|
1168
|
+
JSON.stringify({ currentRef: ref }),
|
|
1169
|
+
"utf-8"
|
|
1170
|
+
);
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* The ref this folder was last recorded at, from a previous run.
|
|
1176
|
+
*
|
|
1177
|
+
* Absent, unreadable and malformed all mean the same thing — this process
|
|
1178
|
+
* cannot vouch for what it descends from — and all answer `undefined`, which
|
|
1179
|
+
* the caller treats as "declare no ancestry".
|
|
1180
|
+
* @returns The persisted ref, or `undefined`.
|
|
1181
|
+
*/
|
|
1182
|
+
_loadPersistedRef() {
|
|
1183
|
+
try {
|
|
1184
|
+
const file = join(this._rootPath, AGENT_STATE_FILE);
|
|
1185
|
+
if (!existsSync(file)) return void 0;
|
|
1186
|
+
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
1187
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
1188
|
+
const ref = parsed.currentRef;
|
|
1189
|
+
return typeof ref === "string" && ref.length > 0 ? ref : void 0;
|
|
1190
|
+
} catch {
|
|
1191
|
+
return void 0;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1149
1194
|
/**
|
|
1150
1195
|
* Appends a sync error entry to the error log file in the sync folder.
|
|
1151
1196
|
* Uses synchronous I/O to guarantee the write completes even in catch blocks.
|
|
@@ -1791,6 +1836,15 @@ ${err.stack}` : String(err);
|
|
|
1791
1836
|
* @returns Function to stop watching
|
|
1792
1837
|
*/
|
|
1793
1838
|
async syncToDb(db, connector, treeKey, options) {
|
|
1839
|
+
if (this._currentRef === void 0) {
|
|
1840
|
+
const persisted = this._loadPersistedRef();
|
|
1841
|
+
if (persisted !== void 0) {
|
|
1842
|
+
this._currentRef = persisted;
|
|
1843
|
+
console.log(
|
|
1844
|
+
`[FsAgent] resuming from recorded ref ${persisted.slice(0, 8)}… — this folder can declare its ancestry`
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1794
1848
|
const initialParentRef = this._currentRef;
|
|
1795
1849
|
const initialTree = await FsAgent._withTimeout(
|
|
1796
1850
|
this.extract(),
|
|
@@ -1810,6 +1864,7 @@ ${err.stack}` : String(err);
|
|
|
1810
1864
|
if (initialRef) {
|
|
1811
1865
|
this._lastSentRef = initialRef;
|
|
1812
1866
|
this._currentRef = initialRef;
|
|
1867
|
+
this._persistCurrentRef(initialRef);
|
|
1813
1868
|
this._lastSentContentKey = this._contentKeyFromTree(initialTree);
|
|
1814
1869
|
await this._sendRef(
|
|
1815
1870
|
connector,
|
|
@@ -1850,6 +1905,8 @@ ${err.stack}` : String(err);
|
|
|
1850
1905
|
`syncToDb storeFsTree(${treeKey})`
|
|
1851
1906
|
);
|
|
1852
1907
|
this._currentRef = ref;
|
|
1908
|
+
this._persistCurrentRef(ref);
|
|
1909
|
+
this._persistCurrentRef(ref);
|
|
1853
1910
|
if (ref === this._lastSentRef) {
|
|
1854
1911
|
return;
|
|
1855
1912
|
}
|
|
@@ -2171,8 +2228,16 @@ ${err.stack}` : String(err);
|
|
|
2171
2228
|
return;
|
|
2172
2229
|
}
|
|
2173
2230
|
}
|
|
2231
|
+
const ancestryExpected = this._resolveConflicts;
|
|
2232
|
+
const declaresAncestry = (predecessorRefs?.length ?? 0) > 0;
|
|
2233
|
+
const applyOptions = restoreOptions?.cleanTarget && ancestryExpected && !declaresAncestry ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
|
|
2234
|
+
if (applyOptions !== restoreOptions) {
|
|
2235
|
+
console.warn(
|
|
2236
|
+
`[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.`
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2174
2239
|
await FsAgent._withTimeout(
|
|
2175
|
-
this.restore(incomingTree, void 0,
|
|
2240
|
+
this.restore(incomingTree, void 0, applyOptions),
|
|
2176
2241
|
this._timeouts.restore,
|
|
2177
2242
|
`syncFromDb → restore(${treeKey})`
|
|
2178
2243
|
);
|
|
@@ -2191,9 +2256,13 @@ ${err.stack}` : String(err);
|
|
|
2191
2256
|
this._lastSentRef = postRestoreRef;
|
|
2192
2257
|
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
2193
2258
|
this._currentRef = postRestoreRef;
|
|
2259
|
+
this._persistCurrentRef(postRestoreRef);
|
|
2194
2260
|
return;
|
|
2195
2261
|
} catch (err) {
|
|
2196
|
-
if (err instanceof
|
|
2262
|
+
if (err instanceof MassDeleteRefusedError) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (err instanceof PartialRestoreError) {
|
|
2197
2266
|
try {
|
|
2198
2267
|
const halfApplied = await this._scanner.scan();
|
|
2199
2268
|
this._lastSentContentKey = this._contentKeyFromTree(halfApplied);
|