@rljson/fs-agent 0.0.26 → 0.0.28

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.
@@ -274,6 +274,17 @@ export declare class FsAgent {
274
274
  * must never be worth failing a sync over.
275
275
  * @param ref - The ref the folder is now at.
276
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;
277
288
  private _persistCurrentRef;
278
289
  /**
279
290
  * The ref this folder was last recorded at, from a previous run.
package/dist/fs-agent.js CHANGED
@@ -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
  }
@@ -1161,6 +1165,21 @@ class FsAgent {
1161
1165
  * must never be worth failing a sync over.
1162
1166
  * @param ref - The ref the folder is now at.
1163
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
+ }
1164
1183
  _persistCurrentRef(ref) {
1165
1184
  try {
1166
1185
  writeFileSync(
@@ -1927,7 +1946,7 @@ ${err.stack}` : String(err);
1927
1946
  }, this._timeouts.debounceMs);
1928
1947
  };
1929
1948
  this._scanner.onChange(debouncedSync);
1930
- await this._scanner.watch();
1949
+ await this._ensureWatching();
1931
1950
  return () => {
1932
1951
  if (debounceTimer) clearTimeout(debounceTimer);
1933
1952
  this._scanner.offChange(debouncedSync);
@@ -2170,14 +2189,13 @@ ${err.stack}` : String(err);
2170
2189
  * @returns Function to stop watching
2171
2190
  */
2172
2191
  async syncFromDb(db, connector, treeKey, restoreOptions) {
2173
- if (!this._scanner["_watcher"]) {
2174
- await this._scanner.watch();
2175
- }
2192
+ await this._ensureWatching();
2176
2193
  let pendingRef = null;
2177
2194
  let fromDbTimer = null;
2178
2195
  let pendingRecoveryAttempt = 0;
2179
2196
  let pendingPredecessorRefs;
2180
- const processRef = async (treeRef, recoveryAttempt = 0, predecessorRefs) => {
2197
+ let pendingIsNewest = true;
2198
+ const processRef = async (treeRef, recoveryAttempt = 0, predecessorRefs, isNewestFromSender = true) => {
2181
2199
  const maxAttempts = this._timeouts.processRefRetries + 1;
2182
2200
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2183
2201
  this._scanner.pauseWatch();
@@ -2230,10 +2248,12 @@ ${err.stack}` : String(err);
2230
2248
  }
2231
2249
  const ancestryExpected = this._resolveConflicts;
2232
2250
  const declaresAncestry = (predecessorRefs?.length ?? 0) > 0;
2233
- const applyOptions = restoreOptions?.cleanTarget && ancestryExpected && !declaresAncestry ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
2251
+ const staleAdvertisement = !isNewestFromSender;
2252
+ const withholdPrune = ancestryExpected && !declaresAncestry || staleAdvertisement;
2253
+ const applyOptions = restoreOptions?.cleanTarget && withholdPrune ? { ...restoreOptions, cleanTarget: false } : restoreOptions;
2234
2254
  if (applyOptions !== restoreOptions) {
2235
2255
  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.`
2256
+ `[FsAgent] ref=${treeRef.slice(0, 8)}… ` + (staleAdvertisement ? "is not the newest its sender has advertised" : "declares no ancestry") + ` — applying additively, not pruning.`
2237
2257
  );
2238
2258
  }
2239
2259
  await FsAgent._withTimeout(
@@ -2305,30 +2325,38 @@ ${err.stack}` : String(err);
2305
2325
  );
2306
2326
  }
2307
2327
  };
2308
- const scheduleProcess = (ref, delayMs, recoveryAttempt, predecessorRefs) => {
2328
+ const scheduleProcess = (ref, delayMs, recoveryAttempt, predecessorRefs, isNewestFromSender = true) => {
2309
2329
  if (pendingRef && pendingRef !== ref) {
2310
2330
  connector.invalidateReceived(pendingRef);
2311
2331
  }
2312
2332
  pendingRef = ref;
2313
2333
  pendingRecoveryAttempt = recoveryAttempt;
2314
2334
  pendingPredecessorRefs = predecessorRefs;
2335
+ pendingIsNewest = isNewestFromSender;
2315
2336
  if (fromDbTimer) clearTimeout(fromDbTimer);
2316
2337
  fromDbTimer = setTimeout(async () => {
2317
2338
  fromDbTimer = null;
2318
2339
  const r = pendingRef;
2319
2340
  const ra = pendingRecoveryAttempt;
2320
2341
  const pr = pendingPredecessorRefs;
2342
+ const newest = pendingIsNewest;
2321
2343
  pendingRef = null;
2322
2344
  if (r) {
2323
- await processRef(r, ra, pr);
2345
+ await processRef(r, ra, pr, newest);
2324
2346
  }
2325
2347
  }, delayMs);
2326
2348
  };
2327
- const syncCallback = (treeRef, predecessorRefs) => {
2349
+ const syncCallback = (treeRef, predecessorRefs, info) => {
2328
2350
  if (!treeRef || typeof treeRef !== "string") {
2329
2351
  return Promise.resolve();
2330
2352
  }
2331
- scheduleProcess(treeRef, this._timeouts.debounceMs, 0, predecessorRefs);
2353
+ scheduleProcess(
2354
+ treeRef,
2355
+ this._timeouts.debounceMs,
2356
+ 0,
2357
+ predecessorRefs,
2358
+ info?.isNewestFromSender ?? true
2359
+ );
2332
2360
  return Promise.resolve();
2333
2361
  };
2334
2362
  connector.listen(syncCallback);
@@ -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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
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",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@rljson/bs": "^0.0.21",
54
- "@rljson/db": "^0.0.31",
54
+ "@rljson/db": "^0.0.32",
55
55
  "@rljson/hash": "^0.0.18",
56
56
  "@rljson/io": "^0.0.66",
57
57
  "@rljson/json": "^0.0.23",