@syncular/client 0.30.0 → 0.30.1

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.md CHANGED
@@ -221,7 +221,8 @@ with `client.follower_timeout` when the link goes `blocked`, with
221
221
  `client.leader_handover` when another leader announces or this tab promotes
222
222
  before the answer arrives, and with `client.worker_failed` when the handle
223
223
  closes. Only calls queued while no leader is bound keep the
224
- `followerCallTimeoutMs` deadline.
224
+ `followerCallTimeoutMs` deadline; when this tab wins the lock itself, its own
225
+ core runs them.
225
226
 
226
227
  **Tabs of different builds never serve each other.** Every `hello`, `req`,
227
228
  and `announce` carries `MULTI_TAB_PROTOCOL_VERSION` and the schema version.
@@ -192,6 +192,11 @@ export declare class FollowerLink {
192
192
  * flushes the queued ones.
193
193
  */
194
194
  unbind(): void;
195
+ /**
196
+ * This tab won the lock and runs its own core: calls queued for the next
197
+ * leader run on `invoke` instead of failing, then the link closes.
198
+ */
199
+ handOver(invoke: (method: string, args: readonly unknown[]) => Promise<unknown>): void;
195
200
  close(): void;
196
201
  }
197
202
  export {};
package/dist/multi-tab.js CHANGED
@@ -502,6 +502,19 @@ export class FollowerLink {
502
502
  this.#armUnboundDeadline();
503
503
  this.#probe();
504
504
  }
505
+ /**
506
+ * This tab won the lock and runs its own core: calls queued for the next
507
+ * leader run on `invoke` instead of failing, then the link closes.
508
+ */
509
+ handOver(invoke) {
510
+ const queued = this.#queue;
511
+ this.#queue = [];
512
+ for (const call of queued) {
513
+ call.cancelTimer?.();
514
+ invoke(call.method, call.args).then(call.resolve, call.reject);
515
+ }
516
+ this.close();
517
+ }
505
518
  close() {
506
519
  if (this.#closed)
507
520
  return;
@@ -233,6 +233,10 @@ class ObservationCache {
233
233
  for (const entry of this.#active)
234
234
  entry.onChange(batch);
235
235
  }
236
+ onLeaderServing() {
237
+ for (const entry of this.#active)
238
+ entry.onLeaderServing?.();
239
+ }
236
240
  clear() {
237
241
  for (const entry of this.#entries.values())
238
242
  entry.dispose();
@@ -386,6 +390,15 @@ class QueryEntry {
386
390
  this.store.releaseWindowClaims(this.#owner);
387
391
  this.reset();
388
392
  }
393
+ /** A claim rejected while no leader served this tab (blocked, handover,
394
+ * another build) is claimed again; its stored error would fail every read. */
395
+ onLeaderServing() {
396
+ if (this.#delegate !== undefined || this.#claimError === undefined)
397
+ return;
398
+ this.#claimError = undefined;
399
+ this.#claimCoverage();
400
+ this.#requestRead();
401
+ }
389
402
  onChange(batch) {
390
403
  if (!batchMatches(batch, this.spec))
391
404
  return;
@@ -446,12 +459,6 @@ class QueryEntry {
446
459
  return;
447
460
  }
448
461
  const wasBlocked = this.#state.phase === 'blocked';
449
- // A claim that failed while the leader link was blocked is retried with
450
- // the link: its stored error would otherwise fail every later read.
451
- if (wasBlocked && this.#claimError !== undefined) {
452
- this.#claimError = undefined;
453
- this.#claimCoverage();
454
- }
455
462
  this.#publish({
456
463
  ...this.#state,
457
464
  phase: wasBlocked
@@ -1002,6 +1009,7 @@ export class ReactiveClientStore {
1002
1009
  if (group.claims.size > 0)
1003
1010
  this.#scheduleWindow(group);
1004
1011
  }
1012
+ this.#queries.onLeaderServing();
1005
1013
  }
1006
1014
  });
1007
1015
  this.status.refresh();
@@ -128,7 +128,8 @@ export class SyncClientHandle {
128
128
  }
129
129
  /** @internal — swap this handle from follower to leader (promotion). */
130
130
  __becomeLeader(core) {
131
- this.#follower?.close();
131
+ // Calls queued for the next leader run on this tab's own core.
132
+ this.#follower?.handOver(core.invoke);
132
133
  this.#follower = undefined;
133
134
  this.#core = core;
134
135
  this.#clientId = core.clientId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
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",
@@ -89,10 +89,10 @@
89
89
  },
90
90
  "dependencies": {
91
91
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
92
- "@syncular/core": "0.30.0"
92
+ "@syncular/core": "0.30.1"
93
93
  },
94
94
  "devDependencies": {
95
- "@syncular/server": "0.30.0",
95
+ "@syncular/server": "0.30.1",
96
96
  "playwright": "1.63.0"
97
97
  }
98
98
  }
package/src/multi-tab.ts CHANGED
@@ -724,6 +724,22 @@ export class FollowerLink {
724
724
  this.#probe();
725
725
  }
726
726
 
727
+ /**
728
+ * This tab won the lock and runs its own core: calls queued for the next
729
+ * leader run on `invoke` instead of failing, then the link closes.
730
+ */
731
+ handOver(
732
+ invoke: (method: string, args: readonly unknown[]) => Promise<unknown>,
733
+ ): void {
734
+ const queued = this.#queue;
735
+ this.#queue = [];
736
+ for (const call of queued) {
737
+ call.cancelTimer?.();
738
+ invoke(call.method, call.args).then(call.resolve, call.reject);
739
+ }
740
+ this.close();
741
+ }
742
+
727
743
  close(): void {
728
744
  if (this.#closed) return;
729
745
  this.#closed = true;
@@ -315,6 +315,8 @@ function reconcileRows<Row>(
315
315
 
316
316
  interface CachedObservation {
317
317
  onChange(batch: ClientChangeBatch): void;
318
+ /** A leader serves this tab again (it leads or follows a bound leader). */
319
+ onLeaderServing?(): void;
318
320
  reset(): void;
319
321
  dispose(): void;
320
322
  }
@@ -356,6 +358,9 @@ class ObservationCache {
356
358
  onChange(batch: ClientChangeBatch): void {
357
359
  for (const entry of this.#active) entry.onChange(batch);
358
360
  }
361
+ onLeaderServing(): void {
362
+ for (const entry of this.#active) entry.onLeaderServing?.();
363
+ }
359
364
  clear(): void {
360
365
  for (const entry of this.#entries.values()) entry.dispose();
361
366
  this.#entries.clear();
@@ -523,6 +528,15 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
523
528
  this.reset();
524
529
  }
525
530
 
531
+ /** A claim rejected while no leader served this tab (blocked, handover,
532
+ * another build) is claimed again; its stored error would fail every read. */
533
+ onLeaderServing(): void {
534
+ if (this.#delegate !== undefined || this.#claimError === undefined) return;
535
+ this.#claimError = undefined;
536
+ this.#claimCoverage();
537
+ this.#requestRead();
538
+ }
539
+
526
540
  onChange(batch: ClientChangeBatch): void {
527
541
  if (!batchMatches(batch, this.spec)) return;
528
542
  if (batch.revision > this.#desiredRevision) {
@@ -584,12 +598,6 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
584
598
  return;
585
599
  }
586
600
  const wasBlocked = this.#state.phase === 'blocked';
587
- // A claim that failed while the leader link was blocked is retried with
588
- // the link: its stored error would otherwise fail every later read.
589
- if (wasBlocked && this.#claimError !== undefined) {
590
- this.#claimError = undefined;
591
- this.#claimCoverage();
592
- }
593
601
  this.#publish({
594
602
  ...this.#state,
595
603
  phase: wasBlocked
@@ -1188,6 +1196,7 @@ export class ReactiveClientStore {
1188
1196
  for (const group of this.#windowClaims.values()) {
1189
1197
  if (group.claims.size > 0) this.#scheduleWindow(group);
1190
1198
  }
1199
+ this.#queries.onLeaderServing();
1191
1200
  }
1192
1201
  });
1193
1202
  this.status.refresh();
@@ -385,7 +385,8 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
385
385
 
386
386
  /** @internal — swap this handle from follower to leader (promotion). */
387
387
  __becomeLeader(core: LeaderCore): void {
388
- this.#follower?.close();
388
+ // Calls queued for the next leader run on this tab's own core.
389
+ this.#follower?.handOver(core.invoke);
389
390
  this.#follower = undefined;
390
391
  this.#core = core;
391
392
  this.#clientId = core.clientId;