@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 +2 -1
- package/dist/multi-tab.d.ts +5 -0
- package/dist/multi-tab.js +13 -0
- package/dist/reactive-store.js +14 -6
- package/dist/worker-host.js +2 -1
- package/package.json +3 -3
- package/src/multi-tab.ts +16 -0
- package/src/reactive-store.ts +15 -6
- package/src/worker-host.ts +2 -1
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.
|
package/dist/multi-tab.d.ts
CHANGED
|
@@ -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;
|
package/dist/reactive-store.js
CHANGED
|
@@ -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();
|
package/dist/worker-host.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
92
|
+
"@syncular/core": "0.30.1"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.30.
|
|
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;
|
package/src/reactive-store.ts
CHANGED
|
@@ -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();
|
package/src/worker-host.ts
CHANGED
|
@@ -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
|
|
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;
|