@syncular/client 0.29.0 → 0.30.0
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 +31 -16
- package/dist/availability.d.ts +8 -1
- package/dist/availability.js +17 -5
- package/dist/multi-tab.d.ts +49 -8
- package/dist/multi-tab.js +137 -24
- package/dist/reactive-store.js +61 -48
- package/dist/worker-host.d.ts +6 -0
- package/dist/worker-host.js +67 -2
- package/package.json +3 -3
- package/src/availability.ts +27 -7
- package/src/multi-tab.ts +192 -40
- package/src/reactive-store.ts +67 -51
- package/src/worker-host.ts +74 -1
package/src/worker-host.ts
CHANGED
|
@@ -80,10 +80,13 @@ import {
|
|
|
80
80
|
broadcastChannelFactory,
|
|
81
81
|
type CrossTabChannel,
|
|
82
82
|
FollowerLink,
|
|
83
|
+
LEADER_INCOMPATIBLE_CODE,
|
|
83
84
|
LeaderBridge,
|
|
84
85
|
type LeadershipState,
|
|
86
|
+
MULTI_TAB_PROTOCOL_VERSION,
|
|
85
87
|
multiTabChannelName,
|
|
86
88
|
newTabId,
|
|
89
|
+
type TabIdentity,
|
|
87
90
|
} from './multi-tab';
|
|
88
91
|
import type { OutboxCommit } from './outbox';
|
|
89
92
|
import type {
|
|
@@ -397,6 +400,33 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
397
400
|
}
|
|
398
401
|
}
|
|
399
402
|
|
|
403
|
+
/**
|
|
404
|
+
* @internal — a newer tab asked for the database: close the core and
|
|
405
|
+
* release the lock so that tab promotes. This tab stays blocked until it
|
|
406
|
+
* reloads with the newer build.
|
|
407
|
+
*/
|
|
408
|
+
__yieldToNewerTab(): void {
|
|
409
|
+
const core = this.#core;
|
|
410
|
+
if (core === undefined) return;
|
|
411
|
+
this.#core = undefined;
|
|
412
|
+
this.#role = 'follower';
|
|
413
|
+
this.__setLeadership({
|
|
414
|
+
state: 'blocked',
|
|
415
|
+
reason: 'leader-incompatible',
|
|
416
|
+
code: LEADER_INCOMPATIBLE_CODE,
|
|
417
|
+
leader: 'newer',
|
|
418
|
+
retryable: true,
|
|
419
|
+
});
|
|
420
|
+
for (const listener of this.#roleListeners) {
|
|
421
|
+
try {
|
|
422
|
+
listener('follower');
|
|
423
|
+
} catch {
|
|
424
|
+
/* a UI listener must never break the handover */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
void core.close(true);
|
|
428
|
+
}
|
|
429
|
+
|
|
400
430
|
/** @internal — apply a follower reachability snapshot in place. */
|
|
401
431
|
__setLeadership(state: LeadershipState): void {
|
|
402
432
|
this.#leadership = state;
|
|
@@ -497,6 +527,18 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
497
527
|
new ClientSyncError(WORKER_FAILED_CODE, 'the handle is closed'),
|
|
498
528
|
);
|
|
499
529
|
}
|
|
530
|
+
if (
|
|
531
|
+
this.#leadership.state === 'blocked' &&
|
|
532
|
+
this.#leadership.reason === 'leader-incompatible'
|
|
533
|
+
) {
|
|
534
|
+
return Promise.reject(
|
|
535
|
+
new ClientSyncError(
|
|
536
|
+
LEADER_INCOMPATIBLE_CODE,
|
|
537
|
+
'the tab that owns the database runs a different protocol or schema version',
|
|
538
|
+
true,
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
500
542
|
if (this.#role === 'follower') {
|
|
501
543
|
if (this.#follower === undefined) {
|
|
502
544
|
return Promise.reject(
|
|
@@ -1017,6 +1059,8 @@ async function bootLeader(
|
|
|
1017
1059
|
fireConfigCallbacks(config, event);
|
|
1018
1060
|
handleRef.handle?.__dispatchEvent(event);
|
|
1019
1061
|
};
|
|
1062
|
+
// A newer tab may say hello before the handle exists; yield once it does.
|
|
1063
|
+
let newerTab = false;
|
|
1020
1064
|
const makeBridge =
|
|
1021
1065
|
config.multiTab !== false
|
|
1022
1066
|
? (
|
|
@@ -1032,7 +1076,12 @@ async function bootLeader(
|
|
|
1032
1076
|
channel,
|
|
1033
1077
|
epoch: parts.epoch ?? 0,
|
|
1034
1078
|
clientId,
|
|
1079
|
+
identity: tabIdentity(config),
|
|
1035
1080
|
invoke,
|
|
1081
|
+
onNewerTab: () => {
|
|
1082
|
+
newerTab = true;
|
|
1083
|
+
handleRef.handle?.__yieldToNewerTab();
|
|
1084
|
+
},
|
|
1036
1085
|
});
|
|
1037
1086
|
}
|
|
1038
1087
|
: undefined;
|
|
@@ -1058,9 +1107,17 @@ async function bootLeader(
|
|
|
1058
1107
|
leadershipListeners: parts.leadershipListeners,
|
|
1059
1108
|
});
|
|
1060
1109
|
handleRef.handle = handle;
|
|
1110
|
+
if (newerTab) handle.__yieldToNewerTab();
|
|
1061
1111
|
return handle;
|
|
1062
1112
|
}
|
|
1063
1113
|
|
|
1114
|
+
function tabIdentity(config: SyncClientHandleConfig): TabIdentity {
|
|
1115
|
+
return {
|
|
1116
|
+
protocol: MULTI_TAB_PROTOCOL_VERSION,
|
|
1117
|
+
schemaVersion: config.schema.version,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1064
1121
|
/**
|
|
1065
1122
|
* Boot a follower: open the channel, bind to the leader, and race the lock in
|
|
1066
1123
|
* the background so this tab promotes itself the instant the leader closes.
|
|
@@ -1080,6 +1137,7 @@ async function bootFollower(
|
|
|
1080
1137
|
const follower = new FollowerLink({
|
|
1081
1138
|
channel,
|
|
1082
1139
|
fromId: newTabId(),
|
|
1140
|
+
identity: tabIdentity(config),
|
|
1083
1141
|
onEvent: (event) => handleRef.handle?.__dispatchEvent(event),
|
|
1084
1142
|
onLeaderChange: (clientId) => {
|
|
1085
1143
|
// Learn the leader's shared client id (best-effort; the handle exposes
|
|
@@ -1115,6 +1173,8 @@ async function bootFollower(
|
|
|
1115
1173
|
const nextEpoch = follower.maxEpochSeen + 1;
|
|
1116
1174
|
// Unbind the link so any late leader traffic is ignored, then promote.
|
|
1117
1175
|
follower.unbind();
|
|
1176
|
+
// A newer tab may say hello before this core is installed.
|
|
1177
|
+
let newerTab = false;
|
|
1118
1178
|
try {
|
|
1119
1179
|
const core = await startWorkerCore({
|
|
1120
1180
|
config,
|
|
@@ -1138,7 +1198,12 @@ async function bootFollower(
|
|
|
1138
1198
|
channel: promoteChannel,
|
|
1139
1199
|
epoch: nextEpoch,
|
|
1140
1200
|
clientId,
|
|
1201
|
+
identity: tabIdentity(config),
|
|
1141
1202
|
invoke,
|
|
1203
|
+
onNewerTab: () => {
|
|
1204
|
+
newerTab = true;
|
|
1205
|
+
handle.__yieldToNewerTab();
|
|
1206
|
+
},
|
|
1142
1207
|
});
|
|
1143
1208
|
},
|
|
1144
1209
|
}
|
|
@@ -1152,6 +1217,7 @@ async function bootFollower(
|
|
|
1152
1217
|
return;
|
|
1153
1218
|
}
|
|
1154
1219
|
handle.__becomeLeader(core);
|
|
1220
|
+
if (newerTab) handle.__yieldToNewerTab();
|
|
1155
1221
|
} catch {
|
|
1156
1222
|
// Promotion failed to spawn a worker — release so the next tab tries.
|
|
1157
1223
|
await lease.release();
|
|
@@ -1193,7 +1259,14 @@ async function bootFollower(
|
|
|
1193
1259
|
// the shared origin replica behind the same barrier before it is returned;
|
|
1194
1260
|
// otherwise an already-running leader would silently ignore the request.
|
|
1195
1261
|
if (config.securityPreflight === true) {
|
|
1196
|
-
|
|
1262
|
+
try {
|
|
1263
|
+
await handle.beginSecurityPreflight();
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
// The caller never receives this handle, so it must not stay queued for
|
|
1266
|
+
// the lock and promote into a database nobody holds.
|
|
1267
|
+
await handle.close();
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1197
1270
|
}
|
|
1198
1271
|
return handle;
|
|
1199
1272
|
}
|