@rljson/fs-agent 0.0.25 → 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.
@@ -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: [...options.ignore || [], SYNC_ERROR_FILE, ATOMIC_TMP_PREFIX],
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, restoreOptions),
2240
+ this.restore(incomingTree, void 0, applyOptions),
2176
2241
  this._timeouts.restore,
2177
2242
  `syncFromDb → restore(${treeKey})`
2178
2243
  );
@@ -2191,6 +2256,7 @@ ${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
2262
  if (err instanceof MassDeleteRefusedError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
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",