@pylonsync/sync 0.3.41 → 0.3.42

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +62 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.41",
6
+ "version": "0.3.42",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -483,12 +483,39 @@ function newUuidLike(): string {
483
483
  return `cl_${t}_${rand}`;
484
484
  }
485
485
 
486
+ /**
487
+ * Coarse connection state for UI consumers.
488
+ *
489
+ * - `connecting` — engine is starting up; first WS handshake hasn't
490
+ * completed yet. Apps typically render their initial
491
+ * skeleton during this state.
492
+ * - `connected` — WS is open and we've stayed open long enough to
493
+ * consider it stable (5s on the wire). Live queries
494
+ * are receiving real-time updates.
495
+ * - `reconnecting` — WS dropped (network blip, Fly autostop) and the
496
+ * engine is backing off + retrying. Live queries
497
+ * keep returning the last-known data; mutations
498
+ * queue locally and replay on the next connect.
499
+ * - `offline` — engine has been stopped via `engine.stop()` or
500
+ * was never started. No retries pending.
501
+ *
502
+ * The `useSyncStatus` hook in `@pylonsync/react` subscribes to this
503
+ * via the existing store notify channel so re-renders happen
504
+ * automatically without a separate event bus.
505
+ */
506
+ export type SyncConnectionStatus =
507
+ | "connecting"
508
+ | "connected"
509
+ | "reconnecting"
510
+ | "offline";
511
+
486
512
  export class SyncEngine {
487
513
  private config: SyncEngineConfig;
488
514
  private cursor: SyncCursor = { last_seq: 0 };
489
515
  private running = false;
490
516
  private ws: WebSocket | null = null;
491
517
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
518
+ private _connectionStatus: SyncConnectionStatus = "offline";
492
519
  /** Monotonic attempt counter for exponential backoff. Reset to 0 on a
493
520
  * successful connection so the next reconnect starts fresh rather than
494
521
  * inheriting the previous storm's cooldown. */
@@ -598,6 +625,27 @@ export class SyncEngine {
598
625
  return this._resolvedSession;
599
626
  }
600
627
 
628
+ /**
629
+ * Coarse connection state (see `SyncConnectionStatus`). Updated as
630
+ * the WS opens/closes and reconnect attempts run; subscribers re-
631
+ * render via the same store notify channel as live queries, so
632
+ * `useSyncStatus` is just a thin reader.
633
+ */
634
+ connectionStatus(): SyncConnectionStatus {
635
+ return this._connectionStatus;
636
+ }
637
+
638
+ /**
639
+ * Mutate connection status + notify subscribers. Idempotent — same-
640
+ * status calls are a no-op so the WS onopen → connected transition
641
+ * doesn't spam re-renders during a stable connection.
642
+ */
643
+ private setConnectionStatus(next: SyncConnectionStatus): void {
644
+ if (this._connectionStatus === next) return;
645
+ this._connectionStatus = next;
646
+ this.store.notify();
647
+ }
648
+
601
649
  /** Sync key-value adapter for hot-path state (token, client_id). */
602
650
  readonly storage: import("./storage").Storage;
603
651
 
@@ -639,6 +687,7 @@ export class SyncEngine {
639
687
  async start(): Promise<void> {
640
688
  if (this.running) return;
641
689
  this.running = true;
690
+ this.setConnectionStatus("connecting");
642
691
 
643
692
  // Load persisted data if available.
644
693
  const shouldPersist = this.config.persist !== false && typeof indexedDB !== "undefined";
@@ -747,6 +796,7 @@ export class SyncEngine {
747
796
  clearInterval(this.pollTimer);
748
797
  this.pollTimer = null;
749
798
  }
799
+ this.setConnectionStatus("offline");
750
800
  }
751
801
 
752
802
  /** Connect to the WebSocket server for real-time updates. */
@@ -778,6 +828,12 @@ export class SyncEngine {
778
828
  // reconnect loop fire at ~2/sec forever. Only call the connection
779
829
  // "stable" after it's stayed up long enough to have been doing work.
780
830
  this.ws.onopen = () => {
831
+ // We only flip to "connected" once the socket actually opens.
832
+ // The 5s stable-window timer below decides when to RESET the
833
+ // backoff; status flips immediately because UI consumers want
834
+ // to clear the "reconnecting" indicator the moment data starts
835
+ // flowing again.
836
+ this.setConnectionStatus("connected");
781
837
  if (this.wsStableTimer) clearTimeout(this.wsStableTimer);
782
838
  this.wsStableTimer = setTimeout(() => {
783
839
  this.reconnectAttempts = 0;
@@ -856,6 +912,12 @@ export class SyncEngine {
856
912
  clearTimeout(this.wsStableTimer);
857
913
  this.wsStableTimer = null;
858
914
  }
915
+ // Surface the disconnect to UI consumers immediately. If
916
+ // `running` flipped to false (engine stopped), `stop()` already
917
+ // set "offline" — don't override that.
918
+ if (this.running) {
919
+ this.setConnectionStatus("reconnecting");
920
+ }
859
921
  this.scheduleReconnect();
860
922
  };
861
923