leafrelay 0.2.6 → 0.2.7

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.
@@ -2449,19 +2449,30 @@ var SyncEngine = class {
2449
2449
  stopRemoteReconnect;
2450
2450
  eventTimers = /* @__PURE__ */ new Map();
2451
2451
  stopped = false;
2452
+ failedPaths = /* @__PURE__ */ new Map();
2453
+ retryTimer;
2454
+ retryDelayMs = 3e4;
2455
+ pendingRemoteVersion;
2456
+ pendingInitialization = false;
2457
+ get syncError() {
2458
+ if (this.failedPaths.size === 0) {
2459
+ return void 0;
2460
+ }
2461
+ const [path, failure] = this.failedPaths.entries().next().value;
2462
+ return `${this.failedPaths.size} path(s) could not synchronize, including ${path}: ${failure.message}. Other paths remain active; failed paths will retry automatically.`;
2463
+ }
2452
2464
  async start() {
2453
2465
  this.stopped = false;
2454
- this.state = await this.options.stateStore.load();
2455
- const currentVersion = await this.options.remote.getCurrentVersion();
2456
- const localFiles = await this.options.local.listFiles();
2457
- const remoteFiles = this.remoteFilePaths();
2458
- this.options.log(`Checking replica at Overleaf version ${currentVersion}: ${localFiles.length} local file(s), ${remoteFiles.length} remote file(s).`);
2459
- if (!this.state || !this.state.initialized) {
2460
- await this.initializeState(currentVersion, localFiles, remoteFiles);
2461
- } else {
2462
- this.restoreConflicts();
2463
- await this.startupReconcile(currentVersion, localFiles, remoteFiles);
2466
+ const startup = this.enqueue(() => this.initialize());
2467
+ try {
2468
+ await startup;
2469
+ } catch (error2) {
2470
+ await this.stop();
2471
+ throw error2;
2464
2472
  }
2473
+ }
2474
+ async initialize() {
2475
+ this.state = await this.options.stateStore.load();
2465
2476
  this.stopLocalWatch = await this.options.local.watch((path) => this.schedule(path, "local"));
2466
2477
  this.stopRemoteChange = this.options.remote.onChange((change) => {
2467
2478
  if (this.stopped) {
@@ -2480,10 +2491,24 @@ var SyncEngine = class {
2480
2491
  this.options.log(`Failed to reconcile after reconnecting: ${error2 instanceof Error ? error2.message : String(error2)}`);
2481
2492
  });
2482
2493
  });
2483
- this.options.log(`Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
2494
+ const currentVersion = await this.options.remote.getCurrentVersion();
2495
+ const localFiles = await this.options.local.listFiles();
2496
+ const remoteFiles = this.remoteFilePaths();
2497
+ this.options.log(`Checking replica at Overleaf version ${currentVersion}: ${localFiles.length} local file(s), ${remoteFiles.length} remote file(s).`);
2498
+ if (!this.state || !this.state.initialized) {
2499
+ await this.initializeState(currentVersion, localFiles, remoteFiles);
2500
+ } else {
2501
+ this.restoreConflicts();
2502
+ await this.startupReconcile(currentVersion, localFiles, remoteFiles);
2503
+ }
2504
+ this.options.log(this.syncError ? `Synchronization active with pending retries. ${this.syncError}` : `Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
2484
2505
  }
2485
2506
  async stop() {
2486
2507
  this.stopped = true;
2508
+ if (this.retryTimer) {
2509
+ clearTimeout(this.retryTimer);
2510
+ this.retryTimer = void 0;
2511
+ }
2487
2512
  this.stopRemoteChange?.();
2488
2513
  this.stopRemoteChange = void 0;
2489
2514
  this.stopRemoteReconnect?.();
@@ -2557,21 +2582,25 @@ var SyncEngine = class {
2557
2582
  await this.options.stateStore.save(this.state);
2558
2583
  }
2559
2584
  this.restoreConflicts();
2585
+ this.pendingRemoteVersion = currentVersion;
2586
+ this.pendingInitialization = true;
2560
2587
  const paths = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles, ...Object.keys(this.state.files)]);
2561
2588
  for (const path of [...paths].sort()) {
2562
2589
  const normalized = normalizePath2(path);
2563
2590
  if (this.conflicts.has(normalized)) {
2564
2591
  continue;
2565
2592
  }
2566
- if (this.state.files[normalized] !== void 0) {
2567
- await this.reconcilePath(normalized, "local");
2568
- continue;
2593
+ try {
2594
+ if (this.state.files[normalized] !== void 0) {
2595
+ await this.synchronizePath(normalized, "local");
2596
+ } else {
2597
+ await this.initializePath(normalized);
2598
+ }
2599
+ } catch (error2) {
2600
+ this.recordFailure(normalized, "local", error2);
2569
2601
  }
2570
- await this.initializePath(normalized);
2571
2602
  }
2572
- this.state.remoteVersion = currentVersion;
2573
- this.state.initialized = true;
2574
- await this.options.stateStore.save(this.state);
2603
+ await this.finishRemoteBatch();
2575
2604
  if (this.conflicts.size !== 0) {
2576
2605
  this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
2577
2606
  }
@@ -2644,7 +2673,7 @@ var SyncEngine = class {
2644
2673
  throw new Error("Synchronization paused: Overleaf history is unavailable, so remote changes cannot be distinguished safely.");
2645
2674
  }
2646
2675
  }
2647
- const candidates = new Set(localChanged);
2676
+ const candidates = /* @__PURE__ */ new Set([...localChanged, ...this.failedPaths.keys()]);
2648
2677
  const knownFiles = /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles, ...remoteFiles]);
2649
2678
  for (const path of [...localFiles, ...remoteFiles]) {
2650
2679
  if (state.files[path] === void 0) {
@@ -2652,6 +2681,9 @@ var SyncEngine = class {
2652
2681
  }
2653
2682
  }
2654
2683
  for (const change of remoteDiff.diff) {
2684
+ if (!change.operation) {
2685
+ continue;
2686
+ }
2655
2687
  const paths = [change.pathname, change.newPathname].filter((path) => Boolean(path));
2656
2688
  for (const changedPath of paths.map(normalizePath2)) {
2657
2689
  let matched = false;
@@ -2666,21 +2698,19 @@ var SyncEngine = class {
2666
2698
  }
2667
2699
  }
2668
2700
  }
2669
- let failed = false;
2701
+ this.pendingRemoteVersion = currentVersion;
2670
2702
  this.options.log(`Reconciling ${candidates.size} changed path(s).`);
2671
2703
  for (const path of [...candidates].sort()) {
2704
+ if (this.stopped) {
2705
+ return;
2706
+ }
2672
2707
  try {
2673
- await this.reconcilePath(path, localChanged.has(path) ? "local" : "remote");
2708
+ await this.synchronizePath(path, localChanged.has(path) ? "local" : "remote");
2674
2709
  } catch (error2) {
2675
- failed = true;
2676
- this.options.log(`Failed to synchronize ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
2710
+ this.recordFailure(path, localChanged.has(path) ? "local" : "remote", error2);
2677
2711
  }
2678
2712
  }
2679
- if (failed) {
2680
- throw new Error("Synchronization paused for failed paths; successful paths were checkpointed.");
2681
- }
2682
- state.remoteVersion = currentVersion;
2683
- await this.options.stateStore.save(state);
2713
+ await this.finishRemoteBatch();
2684
2714
  if (this.conflicts.size !== 0) {
2685
2715
  this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
2686
2716
  }
@@ -2695,7 +2725,7 @@ var SyncEngine = class {
2695
2725
  await this.options.local.listFiles(),
2696
2726
  this.remoteFilePaths()
2697
2727
  );
2698
- this.options.log(`Realtime connection restored at Overleaf version ${currentVersion}.`);
2728
+ this.options.log(this.syncError ? `Realtime connection restored; ${this.syncError}` : `Realtime connection restored at Overleaf version ${currentVersion}.`);
2699
2729
  }
2700
2730
  schedule(path, source, actor) {
2701
2731
  const normalized = normalizePath2(path);
@@ -2725,13 +2755,72 @@ var SyncEngine = class {
2725
2755
  return;
2726
2756
  }
2727
2757
  const remoteActor = pending.unknownRemoteActor ? void 0 : pending.actors.size === 1 ? [...pending.actors][0] : pending.actors.size > 1 ? "multiple Overleaf collaborators" : void 0;
2728
- this.queue = this.queue.then(() => this.reconcilePath(normalized, pending.source, remoteActor)).catch((error2) => {
2729
- this.options.log(`Failed to synchronize ${normalized}: ${error2 instanceof Error ? error2.message : String(error2)}`);
2758
+ this.queue = this.queue.then(() => this.synchronizePath(normalized, pending.source, remoteActor)).catch((error2) => {
2759
+ this.recordFailure(normalized, pending.source, error2, remoteActor);
2730
2760
  });
2731
2761
  }, 250)
2732
2762
  };
2733
2763
  this.eventTimers.set(normalized, pending);
2734
2764
  }
2765
+ async synchronizePath(path, source, actor) {
2766
+ if (this.pendingInitialization && this.state.files[path] === void 0) {
2767
+ await this.initializePath(path);
2768
+ } else {
2769
+ await this.reconcilePath(path, source, actor);
2770
+ }
2771
+ if (this.failedPaths.delete(path)) {
2772
+ this.options.onSyncError?.(this.syncError);
2773
+ }
2774
+ }
2775
+ recordFailure(path, source, error2, actor) {
2776
+ const message = error2 instanceof Error ? error2.message : String(error2);
2777
+ this.failedPaths.set(path, { source, actor, message });
2778
+ this.options.log(`Failed to synchronize ${path}: ${message}; retrying without stopping other paths.`);
2779
+ this.options.onSyncError?.(this.syncError);
2780
+ this.scheduleRetry();
2781
+ }
2782
+ scheduleRetry() {
2783
+ if (this.stopped || this.retryTimer || this.failedPaths.size === 0) {
2784
+ return;
2785
+ }
2786
+ this.retryTimer = setTimeout(() => {
2787
+ this.retryTimer = void 0;
2788
+ void this.enqueue(async () => {
2789
+ if (this.stopped) {
2790
+ return;
2791
+ }
2792
+ for (const [path, failure] of [...this.failedPaths]) {
2793
+ if (this.stopped) {
2794
+ return;
2795
+ }
2796
+ try {
2797
+ await this.synchronizePath(path, failure.source, failure.actor);
2798
+ this.options.log(`Recovered synchronization for ${path}.`);
2799
+ } catch (error2) {
2800
+ const message = error2 instanceof Error ? error2.message : String(error2);
2801
+ this.failedPaths.set(path, { ...failure, message });
2802
+ this.options.log(`Retry failed for ${path}: ${message}`);
2803
+ }
2804
+ }
2805
+ await this.finishRemoteBatch();
2806
+ }).catch((error2) => this.options.log(`Could not checkpoint retry batch: ${String(error2)}`)).finally(() => {
2807
+ this.retryDelayMs = this.failedPaths.size ? Math.min(this.retryDelayMs * 2, 12e4) : 3e4;
2808
+ this.scheduleRetry();
2809
+ });
2810
+ }, this.retryDelayMs);
2811
+ }
2812
+ async finishRemoteBatch() {
2813
+ if (this.stopped || this.failedPaths.size || this.pendingRemoteVersion === void 0) {
2814
+ return;
2815
+ }
2816
+ this.state.remoteVersion = this.pendingRemoteVersion;
2817
+ if (this.pendingInitialization) {
2818
+ this.state.initialized = true;
2819
+ }
2820
+ await this.options.stateStore.save(this.state);
2821
+ this.pendingRemoteVersion = void 0;
2822
+ this.pendingInitialization = false;
2823
+ }
2735
2824
  enqueue(operation) {
2736
2825
  const result = this.queue.then(operation);
2737
2826
  this.queue = result.then(() => void 0, () => void 0);
@@ -3194,6 +3283,7 @@ async function startServe(directory = process.cwd(), options = {}) {
3194
3283
  remote,
3195
3284
  stateStore: new FileSyncStateStore(root, settings.uri),
3196
3285
  ignore,
3286
+ onSyncError: options.onSyncError,
3197
3287
  log: options.log ?? log,
3198
3288
  onConflict: options.onConflict ?? ((path, reason) => {
3199
3289
  (options.log ?? log)(`Conflict paused for ${path}: ${reason}`);
@@ -3211,6 +3301,7 @@ async function startServe(directory = process.cwd(), options = {}) {
3211
3301
  serverName: settings.serverName,
3212
3302
  projectId: settings.projectId,
3213
3303
  projectName: settings.projectName,
3304
+ syncError: () => engine.syncError,
3214
3305
  conflicts: () => engine.getConflicts(),
3215
3306
  resolveConflict: (path, winner) => engine.resolveConflict(path, winner),
3216
3307
  retry: (path) => engine.retry(path),
@@ -3239,4 +3330,4 @@ export {
3239
3330
  RemoteProject,
3240
3331
  startServe
3241
3332
  };
3242
- //# sourceMappingURL=chunk-FBT2UYMY.js.map
3333
+ //# sourceMappingURL=chunk-3TTRDNHP.js.map