@syncular/client 0.2.0 → 0.3.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/dist/client.d.ts +19 -0
- package/dist/client.js +65 -2
- package/dist/devtools.d.ts +50 -0
- package/dist/devtools.js +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/naming.d.ts +10 -0
- package/dist/naming.js +26 -0
- package/dist/query-guard.d.ts +29 -0
- package/dist/query-guard.js +200 -0
- package/dist/schema.d.ts +27 -2
- package/dist/schema.js +87 -8
- package/dist/sql-tag.d.ts +35 -0
- package/dist/sql-tag.js +82 -0
- package/dist/worker-entry.js +7 -0
- package/dist/worker-host.d.ts +23 -17
- package/dist/worker-host.js +30 -8
- package/dist/worker-protocol.d.ts +4 -0
- package/package.json +8 -8
- package/src/client.ts +80 -1
- package/src/devtools.ts +119 -0
- package/src/index.ts +4 -0
- package/src/naming.ts +26 -0
- package/src/query-guard.ts +193 -0
- package/src/schema.ts +104 -10
- package/src/sql-tag.ts +128 -0
- package/src/worker-entry.ts +7 -0
- package/src/worker-host.ts +52 -22
- package/src/worker-protocol.ts +7 -0
package/src/worker-host.ts
CHANGED
|
@@ -8,17 +8,18 @@
|
|
|
8
8
|
* holding the lock). The returned {@link SyncClientHandle} is a thin, fully
|
|
9
9
|
* async proxy over the `worker-protocol` RPC.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* proxies every call to the leader tab over a
|
|
14
|
-
* `multi-tab.ts`). When the leader tab closes, its
|
|
15
|
-
* followers contest, the winner PROMOTES in place —
|
|
16
|
-
* the persisted OPFS database and re-announces — and
|
|
17
|
-
* flips to `'leader'` with `onRoleChange` firing. The
|
|
18
|
-
* kept across the transition so React bindings hold a
|
|
11
|
+
* Multi-tab is the DEFAULT (RFC 0002 §2.4 — the follower path is
|
|
12
|
+
* conformance-covered): a tab that LOSES the election becomes a FOLLOWER
|
|
13
|
+
* (`role === 'follower'`) that proxies every call to the leader tab over a
|
|
14
|
+
* BroadcastChannel (see `multi-tab.ts`). When the leader tab closes, its
|
|
15
|
+
* lock releases; the followers contest, the winner PROMOTES in place —
|
|
16
|
+
* spawns the worker over the persisted OPFS database and re-announces — and
|
|
17
|
+
* the handle's `role` flips to `'leader'` with `onRoleChange` firing. The
|
|
18
|
+
* same handle object is kept across the transition so React bindings hold a
|
|
19
|
+
* stable reference.
|
|
19
20
|
*
|
|
20
|
-
* With `multiTab
|
|
21
|
-
*
|
|
21
|
+
* With `multiTab: false`, the loser is an `isLeader === false` handle whose
|
|
22
|
+
* calls reject with `client.not_leader` (the single-tab contract).
|
|
22
23
|
*/
|
|
23
24
|
import type { WakeReason } from '@syncular/core';
|
|
24
25
|
import type { BlobRef, CachedBlob } from './blob';
|
|
@@ -35,6 +36,7 @@ import type {
|
|
|
35
36
|
WindowState,
|
|
36
37
|
} from './client';
|
|
37
38
|
import type { SqlRow, SqlValue } from './database';
|
|
39
|
+
import { registerDevtools } from './devtools';
|
|
38
40
|
import { ClientSyncError } from './errors';
|
|
39
41
|
import { InvalidationEmitter, type InvalidationListener } from './invalidation';
|
|
40
42
|
import {
|
|
@@ -92,10 +94,11 @@ export interface SyncClientHandleConfig {
|
|
|
92
94
|
readonly leaderLock?: LeaderLock;
|
|
93
95
|
readonly lockName?: string;
|
|
94
96
|
/**
|
|
95
|
-
* Multi-tab followers (TODO 3.2).
|
|
96
|
-
* election becomes a FOLLOWER that proxies to the leader over a
|
|
97
|
-
* BroadcastChannel, and contests + promotes when the leader closes.
|
|
98
|
-
* false
|
|
97
|
+
* Multi-tab followers (TODO 3.2). On by default: a tab that loses the
|
|
98
|
+
* leader election becomes a FOLLOWER that proxies to the leader over a
|
|
99
|
+
* BroadcastChannel, and contests + promotes when the leader closes. Set
|
|
100
|
+
* false for the single-tab contract — the loser is a dead
|
|
101
|
+
* `isLeader === false` handle rejecting with `client.not_leader`.
|
|
99
102
|
*/
|
|
100
103
|
readonly multiTab?: boolean;
|
|
101
104
|
/** Cross-tab channel factory (default `BroadcastChannel`); injectable for tests. */
|
|
@@ -170,6 +173,7 @@ export class SyncClientHandle {
|
|
|
170
173
|
readonly #invalidation: InvalidationEmitter;
|
|
171
174
|
readonly #presence: Set<(scopeKey: string) => void>;
|
|
172
175
|
readonly #roleListeners: Set<(role: HandleRole) => void>;
|
|
176
|
+
readonly #devtoolsUnregister: () => void;
|
|
173
177
|
#closed = false;
|
|
174
178
|
|
|
175
179
|
/** @internal — use {@link createSyncClientHandle}. */
|
|
@@ -189,6 +193,20 @@ export class SyncClientHandle {
|
|
|
189
193
|
this.#invalidation = internals.invalidation;
|
|
190
194
|
this.#presence = internals.presence;
|
|
191
195
|
this.#roleListeners = internals.roleListeners ?? new Set();
|
|
196
|
+
// RFC 0002 §3.2: console introspection — a no-op outside a dev page.
|
|
197
|
+
this.#devtoolsUnregister = registerDevtools({
|
|
198
|
+
kind: 'handle',
|
|
199
|
+
ref: this,
|
|
200
|
+
clientId: () => this.#clientId,
|
|
201
|
+
role: () => this.#role,
|
|
202
|
+
outbox: async () => (await this.pendingCommits()).length,
|
|
203
|
+
subscriptions: () => this.subscriptions(),
|
|
204
|
+
conflicts: async () => (await this.conflicts()).length,
|
|
205
|
+
rejections: async () => (await this.rejections()).length,
|
|
206
|
+
syncNeeded: () => this.syncNeeded(),
|
|
207
|
+
upgrading: () => this.upgrading(),
|
|
208
|
+
onInvalidate: (listener) => this.onInvalidate(listener),
|
|
209
|
+
});
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
/** @internal — swap this handle from follower to leader (promotion). */
|
|
@@ -266,7 +284,8 @@ export class SyncClientHandle {
|
|
|
266
284
|
new ClientSyncError(
|
|
267
285
|
NOT_LEADER_CODE,
|
|
268
286
|
'this tab is not the leader — another tab owns the syncular ' +
|
|
269
|
-
'core for this origin (
|
|
287
|
+
'core for this origin (this handle opted out of follower ' +
|
|
288
|
+
'proxying with multiTab: false)',
|
|
270
289
|
),
|
|
271
290
|
);
|
|
272
291
|
}
|
|
@@ -304,6 +323,16 @@ export class SyncClientHandle {
|
|
|
304
323
|
return this.#call('mutate', [mutations]);
|
|
305
324
|
}
|
|
306
325
|
|
|
326
|
+
/** Partial-update convenience: read-merge-write one full-row upsert. */
|
|
327
|
+
patch(
|
|
328
|
+
table: string,
|
|
329
|
+
rowId: string,
|
|
330
|
+
partial: Readonly<Record<string, unknown>>,
|
|
331
|
+
options?: { readonly baseVersion?: number },
|
|
332
|
+
): Promise<string> {
|
|
333
|
+
return this.#call('patch', [table, rowId, partial, options]);
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
sync(): Promise<SyncSummary> {
|
|
308
337
|
return this.#call('sync', []);
|
|
309
338
|
}
|
|
@@ -394,6 +423,7 @@ export class SyncClientHandle {
|
|
|
394
423
|
async close(): Promise<void> {
|
|
395
424
|
if (this.#closed) return;
|
|
396
425
|
this.#closed = true;
|
|
426
|
+
this.#devtoolsUnregister();
|
|
397
427
|
if (this.#follower !== undefined) {
|
|
398
428
|
this.#follower.close();
|
|
399
429
|
this.#follower = undefined;
|
|
@@ -574,9 +604,9 @@ function fireConfigCallbacks(
|
|
|
574
604
|
/**
|
|
575
605
|
* Acquire leadership, spawn the worker, initialize the core inside it.
|
|
576
606
|
*
|
|
577
|
-
* With `multiTab`
|
|
578
|
-
*
|
|
579
|
-
*
|
|
607
|
+
* With `multiTab` on (the default): a losing tab becomes a FOLLOWER proxying
|
|
608
|
+
* to the leader, and promotes itself if the leader later closes. With
|
|
609
|
+
* `multiTab: false`: a losing tab resolves to a dead not-leader handle.
|
|
580
610
|
*/
|
|
581
611
|
export async function createSyncClientHandle(
|
|
582
612
|
config: SyncClientHandleConfig,
|
|
@@ -609,8 +639,8 @@ export async function createSyncClientHandle(
|
|
|
609
639
|
}
|
|
610
640
|
|
|
611
641
|
// ---- Lost the election. ----
|
|
612
|
-
if (config.multiTab
|
|
613
|
-
//
|
|
642
|
+
if (config.multiTab === false) {
|
|
643
|
+
// Opted-out single-tab contract: a dead not-leader handle.
|
|
614
644
|
return new SyncClientHandle({
|
|
615
645
|
role: 'follower',
|
|
616
646
|
clientId: '',
|
|
@@ -650,7 +680,7 @@ async function bootLeader(
|
|
|
650
680
|
handleRef.handle?.__dispatchEvent(event);
|
|
651
681
|
};
|
|
652
682
|
const makeBridge =
|
|
653
|
-
config.multiTab
|
|
683
|
+
config.multiTab !== false
|
|
654
684
|
? (
|
|
655
685
|
clientId: string,
|
|
656
686
|
invoke: (
|
|
@@ -743,7 +773,7 @@ async function bootFollower(
|
|
|
743
773
|
fireConfigCallbacks(config, event);
|
|
744
774
|
handle.__dispatchEvent(event);
|
|
745
775
|
},
|
|
746
|
-
...(config.multiTab
|
|
776
|
+
...(config.multiTab !== false
|
|
747
777
|
? {
|
|
748
778
|
makeBridge: (
|
|
749
779
|
clientId: string,
|
package/src/worker-protocol.ts
CHANGED
|
@@ -113,6 +113,13 @@ export interface WorkerApi {
|
|
|
113
113
|
/** §4.8 completeness oracle (I3): the windowed-in units for a base. */
|
|
114
114
|
windowState(base: WindowBase): WindowState;
|
|
115
115
|
mutate(mutations: readonly MutationInput[]): string;
|
|
116
|
+
/** Partial-update convenience: read-merge-write one full-row upsert. */
|
|
117
|
+
patch(
|
|
118
|
+
table: string,
|
|
119
|
+
rowId: string,
|
|
120
|
+
partial: Readonly<Record<string, unknown>>,
|
|
121
|
+
options?: { readonly baseVersion?: number },
|
|
122
|
+
): string;
|
|
116
123
|
sync(): Promise<SyncSummary>;
|
|
117
124
|
syncUntilIdle(maxRounds?: number): Promise<SyncSummary>;
|
|
118
125
|
query(sql: string, params?: readonly SqlValue[]): SqlRow[];
|