@pylonsync/sync 0.3.36 → 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.
- package/package.json +1 -1
- package/src/index.ts +81 -3
package/package.json
CHANGED
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
|
|
|
@@ -1532,13 +1594,29 @@ export async function getServerData(
|
|
|
1532
1594
|
// Convenience factory
|
|
1533
1595
|
// ---------------------------------------------------------------------------
|
|
1534
1596
|
|
|
1535
|
-
/**
|
|
1597
|
+
/**
|
|
1598
|
+
* Create a sync engine connected to the pylon backend.
|
|
1599
|
+
*
|
|
1600
|
+
* Default `baseUrl` resolution order:
|
|
1601
|
+
* 1. Explicit `baseUrl` argument — wins always.
|
|
1602
|
+
* 2. `window.location.origin` when running in a browser — same-origin
|
|
1603
|
+
* deployments (Next.js + Vercel rewrites, embedded SPA, etc.) want
|
|
1604
|
+
* this and forgetting to pass it should NOT silently leak
|
|
1605
|
+
* `localhost:4321` requests in production.
|
|
1606
|
+
* 3. `http://localhost:4321` — the `pylon dev` default for SSR /
|
|
1607
|
+
* non-browser callers (Node scripts, tests).
|
|
1608
|
+
*/
|
|
1536
1609
|
export function createSyncEngine(
|
|
1537
|
-
baseUrl
|
|
1610
|
+
baseUrl?: string,
|
|
1538
1611
|
options?: Partial<SyncEngineConfig>,
|
|
1539
1612
|
): SyncEngine {
|
|
1613
|
+
const resolved =
|
|
1614
|
+
baseUrl ??
|
|
1615
|
+
(typeof window !== "undefined" && window.location?.origin
|
|
1616
|
+
? window.location.origin
|
|
1617
|
+
: "http://localhost:4321");
|
|
1540
1618
|
return new SyncEngine({
|
|
1541
1619
|
...(options ?? {}),
|
|
1542
|
-
baseUrl,
|
|
1620
|
+
baseUrl: resolved,
|
|
1543
1621
|
});
|
|
1544
1622
|
}
|