@rljson/fs-agent 0.0.9 → 0.0.11

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/README.public.md CHANGED
@@ -747,10 +747,20 @@ const agent = new FsAgent('./my-project', bs, {
747
747
  restore: 15_000, // Filesystem restore (default: 15s)
748
748
  syncCallback: 25_000, // Overall syncFromDb callback (default: 25s)
749
749
  debounceMs: 300, // Coalesce rapid FS events (default: 300ms)
750
+ processRefRetries: 3, // Retry failed refs before dropping (default: 3)
751
+ processRefRetryDelayMs: 5_000, // Base delay between retries (default: 5s)
750
752
  },
751
753
  });
752
754
  ```
753
755
 
756
+ ### processRef Retry Behavior
757
+
758
+ When `syncFromDb` receives a tree ref and fails to process it (e.g. `db.get`
759
+ times out because the IoPeer transport hasn't connected yet), the ref is
760
+ retried up to `processRefRetries` times with increasing delay
761
+ (`attempt * processRefRetryDelayMs`). This prevents a single transient
762
+ timeout from permanently breaking the sync pipeline.
763
+
754
764
  ## Bounce-Back Prevention
755
765
 
756
766
  Bidirectional sync can cause infinite loops when both clients detect each
@@ -747,10 +747,20 @@ const agent = new FsAgent('./my-project', bs, {
747
747
  restore: 15_000, // Filesystem restore (default: 15s)
748
748
  syncCallback: 25_000, // Overall syncFromDb callback (default: 25s)
749
749
  debounceMs: 300, // Coalesce rapid FS events (default: 300ms)
750
+ processRefRetries: 3, // Retry failed refs before dropping (default: 3)
751
+ processRefRetryDelayMs: 5_000, // Base delay between retries (default: 5s)
750
752
  },
751
753
  });
752
754
  ```
753
755
 
756
+ ### processRef Retry Behavior
757
+
758
+ When `syncFromDb` receives a tree ref and fails to process it (e.g. `db.get`
759
+ times out because the IoPeer transport hasn't connected yet), the ref is
760
+ retried up to `processRefRetries` times with increasing delay
761
+ (`attempt * processRefRetryDelayMs`). This prevents a single transient
762
+ timeout from permanently breaking the sync pipeline.
763
+
754
764
  ## Bounce-Back Prevention
755
765
 
756
766
  Bidirectional sync can cause infinite loops when both clients detect each
@@ -73,6 +73,18 @@ export interface TimeoutConfig {
73
73
  * Also applies to incoming database refs in syncFromDb.
74
74
  */
75
75
  debounceMs?: number;
76
+ /**
77
+ * Number of retries for processRef in syncFromDb. Default: 3.
78
+ * When a ref fails to process (e.g. db.get timeout because the IoPeer
79
+ * transport hasn't connected yet), the ref is retried this many times
80
+ * with increasing delay before being dropped.
81
+ */
82
+ processRefRetries?: number;
83
+ /**
84
+ * Base delay between processRef retries (milliseconds). Default: 5 000 ms.
85
+ * Each retry waits `attempt * processRefRetryDelayMs` (i.e. 5s, 10s, 15s).
86
+ */
87
+ processRefRetryDelayMs?: number;
76
88
  }
77
89
  /** Filename for sync error log written to the sync folder */
78
90
  export declare const SYNC_ERROR_FILE = ".sync-errors.log";
package/dist/fs-agent.js CHANGED
@@ -178,6 +178,7 @@ class FsScanner {
178
178
  _options;
179
179
  _bs;
180
180
  _paused = false;
181
+ _missedChangesDuringPause = false;
181
182
  constructor(rootPath, options = {}) {
182
183
  this._rootPath = rootPath;
183
184
  this._options = {
@@ -365,6 +366,7 @@ class FsScanner {
365
366
  }
366
367
  async _handleFileChange(_eventType, filename) {
367
368
  if (this._paused) {
369
+ this._missedChangesDuringPause = true;
368
370
  return;
369
371
  }
370
372
  const relativePath = filename.replace(/\\/g, "/");
@@ -442,12 +444,34 @@ class FsScanner {
442
444
  */
443
445
  pauseWatch() {
444
446
  this._paused = true;
447
+ this._missedChangesDuringPause = false;
445
448
  }
446
449
  /**
447
- * Resume file change notifications
450
+ * Resume file change notifications.
451
+ * If any filesystem events were missed during the pause, triggers
452
+ * an asynchronous rescan so that syncToDb can detect and push the changes.
448
453
  */
449
454
  resumeWatch() {
455
+ const missedChanges = this._missedChangesDuringPause;
450
456
  this._paused = false;
457
+ this._missedChangesDuringPause = false;
458
+ if (missedChanges) {
459
+ void this._rescanAfterPause();
460
+ }
461
+ }
462
+ /**
463
+ * Re-scan the filesystem and fire onChange callbacks to catch modifications
464
+ * that were missed while watching was paused.
465
+ */
466
+ async _rescanAfterPause() {
467
+ try {
468
+ await this.scan();
469
+ if (this._paused) {
470
+ return;
471
+ }
472
+ await this._notifyChange({ type: "modified", path: "." });
473
+ } catch {
474
+ }
451
475
  }
452
476
  getTreeByHash(treeHash) {
453
477
  return this._tree?.trees.get(treeHash);
@@ -484,7 +508,9 @@ const DEFAULT_TIMEOUTS = {
484
508
  extract: 15e3,
485
509
  restore: 15e3,
486
510
  syncCallback: 25e3,
487
- debounceMs: 300
511
+ debounceMs: 300,
512
+ processRefRetries: 3,
513
+ processRefRetryDelayMs: 5e3
488
514
  };
489
515
  const SYNC_ERROR_FILE = ".sync-errors.log";
490
516
  class FsAgent {
@@ -1052,47 +1078,64 @@ ${err.stack}` : String(err);
1052
1078
  let pendingRef = null;
1053
1079
  let fromDbTimer = null;
1054
1080
  const processRef = async (treeRef) => {
1055
- this._scanner.pauseWatch();
1056
- try {
1057
- const incomingTree = await FsAgent._withTimeout(
1058
- this._fetchTreeFromDb(db, treeKey, treeRef),
1059
- this._timeouts.fetchTree,
1060
- `syncFromDb → fetchTree(${treeKey}@${treeRef.slice(0, 8)}…)`
1061
- );
1062
- const incomingNodeCount = incomingTree.trees.size;
1063
- console.log(
1064
- `[FsAgent] syncFromDb: fetched tree with ${incomingNodeCount} nodes for ref=${treeRef.slice(0, 8)}…`
1065
- );
1066
- const currentTree = await FsAgent._withTimeout(
1067
- this.extract(),
1068
- this._timeouts.extract,
1069
- `syncFromDb → extract(${this._rootPath})`
1070
- );
1071
- if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
1072
- const incomingFiles = this._getFileContentMap(incomingTree);
1073
- const currentFiles = this._getFileContentMap(currentTree);
1081
+ const maxAttempts = this._timeouts.processRefRetries + 1;
1082
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1083
+ this._scanner.pauseWatch();
1084
+ try {
1085
+ const incomingTree = await FsAgent._withTimeout(
1086
+ this._fetchTreeFromDb(db, treeKey, treeRef),
1087
+ this._timeouts.fetchTree,
1088
+ `syncFromDb fetchTree(${treeKey}@${treeRef.slice(0, 8)}…)`
1089
+ );
1090
+ const incomingNodeCount = incomingTree.trees.size;
1074
1091
  console.log(
1075
- `[FsAgent] syncFromDb: equivalent content, skipping restore (incoming=${incomingFiles.size} entries, current=${currentFiles.size} entries, ref=${treeRef.slice(0, 8)}…)`
1092
+ `[FsAgent] syncFromDb: fetched tree with ${incomingNodeCount} nodes for ref=${treeRef.slice(0, 8)}…`
1093
+ );
1094
+ const currentTree = await FsAgent._withTimeout(
1095
+ this.extract(),
1096
+ this._timeouts.extract,
1097
+ `syncFromDb → extract(${this._rootPath})`
1098
+ );
1099
+ if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
1100
+ const incomingFiles = this._getFileContentMap(incomingTree);
1101
+ const currentFiles = this._getFileContentMap(currentTree);
1102
+ console.log(
1103
+ `[FsAgent] syncFromDb: equivalent content, skipping restore (incoming=${incomingFiles.size} entries, current=${currentFiles.size} entries, ref=${treeRef.slice(0, 8)}…)`
1104
+ );
1105
+ return;
1106
+ }
1107
+ await FsAgent._withTimeout(
1108
+ this.restore(incomingTree, void 0, restoreOptions),
1109
+ this._timeouts.restore,
1110
+ `syncFromDb → restore(${treeKey})`
1076
1111
  );
1112
+ const postRestoreTree = await this._scanner.scan();
1113
+ const dbAdapter = new FsDbAdapter(db, treeKey);
1114
+ const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
1115
+ skipNotification: true
1116
+ });
1117
+ this._lastSentRef = postRestoreRef;
1118
+ this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
1077
1119
  return;
1120
+ } catch (err) {
1121
+ if (attempt === maxAttempts) {
1122
+ console.error(
1123
+ `[FsAgent] syncFromDb processRef failed after ${maxAttempts} attempts:`,
1124
+ err
1125
+ );
1126
+ this._writeSyncError("syncFromDb/processRef", err);
1127
+ } else {
1128
+ const delaySec = attempt * this._timeouts.processRefRetryDelayMs / 1e3;
1129
+ console.warn(
1130
+ `[FsAgent] syncFromDb: attempt ${attempt}/${maxAttempts} failed for ref=${treeRef.slice(0, 8)}…, retrying in ${delaySec}s: ${err instanceof Error ? err.message : String(err)}`
1131
+ );
1132
+ }
1133
+ } finally {
1134
+ this._scanner.resumeWatch();
1078
1135
  }
1079
- await FsAgent._withTimeout(
1080
- this.restore(incomingTree, void 0, restoreOptions),
1081
- this._timeouts.restore,
1082
- `syncFromDb → restore(${treeKey})`
1136
+ await new Promise(
1137
+ (r) => setTimeout(r, attempt * this._timeouts.processRefRetryDelayMs)
1083
1138
  );
1084
- const postRestoreTree = await this._scanner.scan();
1085
- const dbAdapter = new FsDbAdapter(db, treeKey);
1086
- const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
1087
- skipNotification: true
1088
- });
1089
- this._lastSentRef = postRestoreRef;
1090
- this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
1091
- } catch (err) {
1092
- console.error("[FsAgent] syncFromDb processRef failed:", err);
1093
- this._writeSyncError("syncFromDb/processRef", err);
1094
- } finally {
1095
- this._scanner.resumeWatch();
1096
1139
  }
1097
1140
  };
1098
1141
  const syncCallback = async (treeRef) => {
@@ -72,6 +72,7 @@ export declare class FsScanner {
72
72
  private _options;
73
73
  private _bs;
74
74
  private _paused;
75
+ private _missedChangesDuringPause;
75
76
  constructor(rootPath: string, options?: FsScanOptions);
76
77
  get tree(): FsTree | null;
77
78
  get rootPath(): string;
@@ -92,9 +93,16 @@ export declare class FsScanner {
92
93
  */
93
94
  pauseWatch(): void;
94
95
  /**
95
- * Resume file change notifications
96
+ * Resume file change notifications.
97
+ * If any filesystem events were missed during the pause, triggers
98
+ * an asynchronous rescan so that syncToDb can detect and push the changes.
96
99
  */
97
100
  resumeWatch(): void;
101
+ /**
102
+ * Re-scan the filesystem and fire onChange callbacks to catch modifications
103
+ * that were missed while watching was paused.
104
+ */
105
+ private _rescanAfterPause;
98
106
  getTreeByHash(treeHash: TreeRef): Tree | undefined;
99
107
  getTreeByPath(relativePath: string): Tree | undefined;
100
108
  getAllTrees(): Tree[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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",