@rljson/fs-agent 0.0.12 → 0.0.15

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.
@@ -44,6 +44,14 @@ export interface FsAgentOptions {
44
44
  * auto-generates its own identity.
45
45
  */
46
46
  clientIdentity?: ClientId;
47
+ /**
48
+ * Enable Nextcloud-style conflict resolution. When true, {@link syncFromDb}
49
+ * registers a DAG-branch conflict observer that resolves forks into a single
50
+ * merge revision (winner keeps the path, loser is renamed). This is a
51
+ * **client-only** behaviour — hubs are dumb relays and must leave it off
52
+ * (the default). See `doc/conflict-resolution-design.md`.
53
+ */
54
+ resolveConflicts?: boolean;
47
55
  }
48
56
  /** Restore options */
49
57
  export interface RestoreOptions {
@@ -85,9 +93,26 @@ export interface TimeoutConfig {
85
93
  * Each retry waits `attempt * processRefRetryDelayMs` (i.e. 5s, 10s, 15s).
86
94
  */
87
95
  processRefRetryDelayMs?: number;
96
+ /**
97
+ * Number of **recovery re-queues** for a ref whose per-cycle retries were
98
+ * all exhausted (e.g. `db.get` kept timing out because the transport was
99
+ * disconnected/contended during a hub crash, reconnect or restart). Instead
100
+ * of permanently dropping the ref — which loses the file written in that
101
+ * window — it is re-queued up to this many times so it is eventually applied
102
+ * once the transport recovers. A newer incoming ref supersedes a pending
103
+ * recovery; `tearDown()` stops it. Default: 10. Set 0 to restore the old
104
+ * drop-on-exhaustion behaviour.
105
+ */
106
+ recoveryRetries?: number;
88
107
  }
89
108
  /** Filename for sync error log written to the sync folder */
90
109
  export declare const SYNC_ERROR_FILE = ".sync-errors.log";
110
+ /**
111
+ * Filename prefix for the staging files used by atomic writes. The scanner
112
+ * ignores anything starting with this so the transient temp + rename never
113
+ * pollutes the tree or churns the watcher.
114
+ */
115
+ export declare const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
91
116
  /**
92
117
  * Orchestrates filesystem operations with tree structures and blob storage
93
118
  */
@@ -104,6 +129,15 @@ export declare class FsAgent {
104
129
  /** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
105
130
  private _lastSentContentKey?;
106
131
  private _timeouts;
132
+ /** Client-only: resolve DAG-branch conflicts into merge revisions. */
133
+ private _resolveConflicts;
134
+ /**
135
+ * Ancestry head: the content ref of the revision currently representing the
136
+ * filesystem state. New local revisions descend from it; received revisions
137
+ * advance it. Only tracked when `resolveConflicts` is enabled, so the
138
+ * InsertHistory predecessor DAG forms only where conflict resolution is on.
139
+ */
140
+ private _currentRef?;
107
141
  constructor(rootPath: string, bs?: Bs, options?: FsAgentOptions);
108
142
  /**
109
143
  * Gets the root path
@@ -132,6 +166,38 @@ export declare class FsAgent {
132
166
  * @param err - The error value caught
133
167
  */
134
168
  _writeSyncError(context: string, err: unknown): void;
169
+ /**
170
+ * Extracts a human-readable message from a thrown value. The non-`Error`
171
+ * branch is defensive (the DB/transport always throw `Error`s).
172
+ * @param err - The caught value.
173
+ * @returns A message string.
174
+ */
175
+ private static _errMessage;
176
+ /**
177
+ * Retries an async operation up to `attempts` times with exponential backoff
178
+ * (each delay doubles from `baseDelayMs`). For transient failures — a file
179
+ * briefly locked by antivirus or a save-and-rename editor, a peer briefly
180
+ * unreachable. Non-final failures are logged once at warn level so retry
181
+ * pressure is visible without log-spam.
182
+ * @param fn - The operation to run
183
+ * @param attempts - Maximum number of attempts
184
+ * @param baseDelayMs - Initial backoff delay (doubles each retry)
185
+ * @param label - Human-readable label for log messages
186
+ * @returns The operation's resolved value
187
+ */
188
+ private static _withRetry;
189
+ /**
190
+ * Atomically writes a file: stages the content in a sibling `.<rand>.tmp`,
191
+ * then renames over the target. The rename is atomic, so a crash mid-write
192
+ * leaves only the temp behind — never a half-written target file. (We do not
193
+ * `fsync` the temp: it adds significant per-file latency under bursty
194
+ * restores, and durability-on-power-loss is secondary here since the content
195
+ * is replicated and re-synced.) The random suffix keeps concurrent restores
196
+ * of the same path from trampling each other.
197
+ * @param filePath - Destination path
198
+ * @param content - Bytes to write
199
+ */
200
+ private static _atomicWriteFile;
135
201
  /**
136
202
  * Wraps a promise with a timeout.
137
203
  * Rejects with a descriptive error if the promise does not settle
@@ -147,6 +213,9 @@ export declare class FsAgent {
147
213
  * otherwise falls back to fire-and-forget `send()`.
148
214
  * @param connector - The Connector to send through
149
215
  * @param ref - The ref to broadcast
216
+ * @param predecessorRefs - Causal predecessor content refs to attach (for
217
+ * conflict ancestry); set explicitly here because the FsAgent broadcasts
218
+ * via an explicit send, which pre-empts the Connector's db-observer path.
150
219
  */
151
220
  private _sendRef;
152
221
  /**
@@ -179,6 +248,14 @@ export declare class FsAgent {
179
248
  * @param options - Restore options
180
249
  */
181
250
  restore(tree: FsTree, targetPath?: string, options?: RestoreOptions): Promise<void>;
251
+ /**
252
+ * Recursively collects the absolute paths of all files under `currentDir`.
253
+ * Used to snapshot the pre-restore file set for prune race-protection.
254
+ * @param currentDir - Directory to walk
255
+ * @param out - Accumulator set (created if omitted)
256
+ * @returns The set of absolute file paths
257
+ */
258
+ private _collectAllFiles;
182
259
  /**
183
260
  * Recursively restores a tree node and its children
184
261
  * @param treeHash - Hash of the tree node to restore
@@ -246,10 +323,13 @@ export declare class FsAgent {
246
323
  */
247
324
  private _collectExpectedPaths;
248
325
  /**
249
- * Remove files/dirs not present in the expected sets
326
+ * Remove files/dirs not present in the expected sets, preserving any file
327
+ * that appeared *during* the restore (not in `preRestore`) — a fresh user
328
+ * write that must not be clobbered.
250
329
  * @param currentDir - Directory currently being inspected
251
330
  * @param expectedDirs - Allowed directory paths
252
331
  * @param expectedFiles - Allowed file paths
332
+ * @param preRestore - Files present before the restore (prune candidates)
253
333
  */
254
334
  private _pruneExtraneous;
255
335
  /**
@@ -262,6 +342,44 @@ export declare class FsAgent {
262
342
  * @returns Function to stop watching
263
343
  */
264
344
  syncToDb(db: Db, connector: Connector, treeKey: string, options?: StoreFsTreeOptions): Promise<() => void>;
345
+ /**
346
+ * Resolves the `previous` (InsertHistory predecessor timeIds) for a new
347
+ * revision from the parent's shared content refs. timeIds are per-db, so we
348
+ * map each shared parent ref to *this* db's local timeId(s). Returns undefined
349
+ * when ancestry tracking is off (default) or no parent is known — in which
350
+ * case the store behaves exactly as before.
351
+ * @param db - Database instance
352
+ * @param treeKey - Tree table key
353
+ * @param parentRefs - Parent content refs (local head, or received predecessors)
354
+ */
355
+ private _ancestryPrevious;
356
+ /**
357
+ * Classifies an incoming revision relative to our current head using the
358
+ * local InsertHistory DAG (keyed on shared content refs):
359
+ * - `behind` → incoming descends from our head → fast-forward (restore).
360
+ * - `ahead` → our head descends from incoming (e.g. a reconnect bootstrap
361
+ * re-sending an older ancestor) → ignore; we are newer.
362
+ * - `diverged` → siblings produced by concurrent edits → resolve the fork.
363
+ * @param db - Database instance
364
+ * @param treeKey - Tree table key
365
+ * @param currentRef - Our current head's content ref
366
+ * @param incomingRef - The incoming revision's content ref
367
+ * @param incomingPredecessorRefs - The incoming revision's predecessor refs
368
+ */
369
+ private _ancestryRelation;
370
+ /**
371
+ * Resolves a divergent incoming revision inline (called from `processRef`
372
+ * with the watcher paused, so resolution cannot race the sync loop). Records
373
+ * the incoming revision as a fork tip without clobbering local content, then
374
+ * merges our head and the incoming tip into a single merge revision D that is
375
+ * materialised to disk and broadcast.
376
+ * @param db - Database instance
377
+ * @param treeKey - Tree table key
378
+ * @param incomingRef - The incoming revision's content ref
379
+ * @param incomingTree - The fetched incoming tree
380
+ * @param predecessorRefs - The incoming revision's predecessor content refs
381
+ */
382
+ private _resolveConflictInline;
265
383
  /**
266
384
  * Builds a map of relativePath → blobId for all files in a tree.
267
385
  * Used to compare trees by content rather than by hash (which includes mtime).
@@ -289,6 +407,17 @@ export declare class FsAgent {
289
407
  * @param b - Second tree to compare
290
408
  */
291
409
  private _treesHaveEquivalentContent;
410
+ /**
411
+ * Builds the dependency surface a {@link FsConflictResolver} needs, wiring it
412
+ * to this agent's db, blob store, scanner, and working directory.
413
+ *
414
+ * The merge store records the merged ref/content key as the last-sent state,
415
+ * so the watcher-driven re-scan that follows the on-disk materialisation
416
+ * settles to a no-op instead of re-broadcasting.
417
+ * @param db - Database instance
418
+ * @param treeKey - Tree table key
419
+ */
420
+ private _buildConflictResolverDeps;
292
421
  /**
293
422
  * Watches database for tree changes and syncs to filesystem
294
423
  * Uses Connector for socket-based notifications