@syncular/client 0.15.36 → 0.15.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.15.36",
3
+ "version": "0.15.37",
4
4
  "description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -81,7 +81,7 @@
81
81
  },
82
82
  "dependencies": {
83
83
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
84
- "@syncular/core": "0.15.36"
84
+ "@syncular/core": "0.15.37"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "better-sqlite3": ">=11"
@@ -92,7 +92,7 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@syncular/server": "0.15.36",
95
+ "@syncular/server": "0.15.37",
96
96
  "@types/better-sqlite3": "^7.6.13",
97
97
  "better-sqlite3": "^12.11.1"
98
98
  }
package/src/client.ts CHANGED
@@ -545,6 +545,8 @@ export class SyncClient {
545
545
  #conflicts: ConflictRecord[] = [];
546
546
  #rejections: RejectionRecord[] = [];
547
547
  #socket: RealtimeSocket | undefined;
548
+ #realtimeConnectPromise: Promise<void> | undefined;
549
+ #realtimeGeneration = 0;
548
550
  #pendingRound: PendingRound | undefined;
549
551
  #needsPull = false;
550
552
  #syncing = false;
@@ -793,8 +795,7 @@ export class SyncClient {
793
795
  async close(): Promise<void> {
794
796
  this.#devtoolsUnregister?.();
795
797
  this.#devtoolsUnregister = undefined;
796
- this.#socket?.close();
797
- this.#socket = undefined;
798
+ this.disconnectRealtime();
798
799
  this.#abortPendingRound('client closed mid-round');
799
800
  await this.#lease?.release();
800
801
  this.#lease = undefined;
@@ -2758,10 +2759,32 @@ export class SyncClient {
2758
2759
  // -- realtime (§8 client side) ----------------------------------------------
2759
2760
 
2760
2761
  connectRealtime(): Promise<void> {
2761
- return this.#runProtectedAsync(() => this.#connectRealtime());
2762
+ this.#requireActive();
2763
+ if (this.#socket !== undefined) return Promise.resolve();
2764
+ if (this.#realtimeConnectPromise !== undefined) {
2765
+ return this.#realtimeConnectPromise;
2766
+ }
2767
+ const generation = this.#realtimeGeneration;
2768
+ const task = this.#runProtectedAsync(() =>
2769
+ this.#connectRealtime(generation),
2770
+ );
2771
+ this.#realtimeConnectPromise = task;
2772
+ void task.then(
2773
+ () => {
2774
+ if (this.#realtimeConnectPromise === task) {
2775
+ this.#realtimeConnectPromise = undefined;
2776
+ }
2777
+ },
2778
+ () => {
2779
+ if (this.#realtimeConnectPromise === task) {
2780
+ this.#realtimeConnectPromise = undefined;
2781
+ }
2782
+ },
2783
+ );
2784
+ return task;
2762
2785
  }
2763
2786
 
2764
- async #connectRealtime(): Promise<void> {
2787
+ async #connectRealtime(generation: number): Promise<void> {
2765
2788
  const connector = this.#config.realtime;
2766
2789
  if (connector === undefined) {
2767
2790
  throw new ClientSyncError(
@@ -2769,16 +2792,19 @@ export class SyncClient {
2769
2792
  'no realtime connector configured',
2770
2793
  );
2771
2794
  }
2795
+ let openedSocket: RealtimeSocket | undefined;
2772
2796
  const socket = await connector({
2773
2797
  onText: (text) => this.#handleRealtimeText(text),
2774
2798
  onBinary: (bytes) => this.#routeRealtimeBinary(bytes),
2775
2799
  onClose: () => {
2800
+ if (openedSocket === undefined || this.#socket !== openedSocket) return;
2776
2801
  this.#socket = undefined;
2777
2802
  this.#presence.clear(); // §8.6.1: presence is per-connection
2778
2803
  this.#abortPendingRound('realtime socket closed mid-round (§8.7)');
2779
2804
  this.#emitDiagnostics();
2780
2805
  },
2781
2806
  });
2807
+ openedSocket = socket;
2782
2808
  if (this.#securityLifecycle === 'preflight') {
2783
2809
  socket.close();
2784
2810
  throw new ClientSyncError(
@@ -2786,11 +2812,20 @@ export class SyncClient {
2786
2812
  'realtime connected after the client entered security preflight',
2787
2813
  );
2788
2814
  }
2815
+ if (generation !== this.#realtimeGeneration || !this.#started) {
2816
+ socket.close();
2817
+ throw new ClientSyncError(
2818
+ 'client.realtime_cancelled',
2819
+ 'realtime connection was cancelled before activation',
2820
+ );
2821
+ }
2789
2822
  this.#socket = socket;
2790
2823
  this.#emitDiagnostics();
2791
2824
  }
2792
2825
 
2793
2826
  disconnectRealtime(): void {
2827
+ this.#realtimeGeneration += 1;
2828
+ this.#realtimeConnectPromise = undefined;
2794
2829
  this.#socket?.close();
2795
2830
  this.#socket = undefined;
2796
2831
  this.#presence.clear(); // §8.6.1: presence is per-connection
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export * from './outbox';
29
29
  export * from './outcomes';
30
30
  export * from './query-guard';
31
31
  export * from './reactive-store';
32
+ export * from './realtime-supervisor';
32
33
  export * from './schema';
33
34
  export * from './sql-tag';
34
35
  export * from './state';