@syncular/client 0.1.3 → 0.2.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 +227 -709
- package/dist/apply.d.ts +80 -0
- package/dist/apply.js +301 -0
- package/dist/blob.d.ts +127 -0
- package/dist/blob.js +201 -0
- package/dist/bun-database.d.ts +22 -0
- package/dist/bun-database.js +61 -0
- package/dist/client.d.ts +320 -69
- package/dist/client.js +1627 -163
- package/dist/content-type.d.ts +2 -0
- package/dist/content-type.js +2 -0
- package/dist/database.d.ts +35 -80
- package/dist/database.js +32 -947
- package/dist/encryption.d.ts +47 -0
- package/dist/encryption.js +75 -0
- package/dist/errors.d.ts +8 -22
- package/dist/errors.js +10 -207
- package/dist/http.d.ts +32 -0
- package/dist/http.js +196 -0
- package/dist/index.d.ts +28 -16
- package/dist/index.js +25 -12
- package/dist/invalidation.d.ts +69 -0
- package/dist/invalidation.js +84 -0
- package/dist/leader-lock.d.ts +28 -0
- package/dist/leader-lock.js +38 -0
- package/dist/multi-tab.d.ts +134 -0
- package/dist/multi-tab.js +399 -0
- package/dist/node-database.d.ts +41 -0
- package/dist/node-database.js +133 -0
- package/dist/outbox.d.ts +56 -0
- package/dist/outbox.js +140 -0
- package/dist/schema.d.ts +107 -0
- package/dist/schema.js +267 -0
- package/dist/state.d.ts +40 -0
- package/dist/state.js +77 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +7 -0
- package/dist/wasm-database.d.ts +30 -0
- package/dist/wasm-database.js +170 -0
- package/dist/window.d.ts +64 -0
- package/dist/window.js +0 -0
- package/dist/worker-entry.d.ts +16 -2
- package/dist/worker-entry.js +300 -456
- package/dist/worker-host.d.ts +174 -0
- package/dist/worker-host.js +536 -0
- package/dist/worker-protocol.d.ts +173 -61
- package/dist/worker-protocol.js +7 -16
- package/package.json +45 -112
- package/src/apply.ts +435 -0
- package/src/blob.ts +333 -0
- package/src/bun-database.ts +75 -0
- package/src/client.ts +2247 -277
- package/src/content-type.ts +2 -0
- package/src/database.ts +63 -1389
- package/src/encryption.ts +123 -0
- package/src/errors.ts +11 -265
- package/src/http.ts +254 -0
- package/src/index.ts +25 -20
- package/src/invalidation.ts +128 -0
- package/src/leader-lock.ts +68 -0
- package/src/multi-tab.ts +550 -0
- package/src/node-database.ts +180 -0
- package/src/outbox.ts +206 -0
- package/src/schema.ts +397 -0
- package/src/state.ts +126 -0
- package/src/transport.ts +52 -0
- package/src/wasm-database.ts +279 -0
- package/src/window.ts +0 -0
- package/src/worker-entry.ts +391 -545
- package/src/worker-host.ts +800 -0
- package/src/worker-protocol.ts +204 -99
- package/dist/auth-leases.d.ts +0 -11
- package/dist/auth-leases.d.ts.map +0 -1
- package/dist/auth-leases.js +0 -140
- package/dist/auth-leases.js.map +0 -1
- package/dist/blob-limits.d.ts +0 -15
- package/dist/blob-limits.d.ts.map +0 -1
- package/dist/blob-limits.js +0 -66
- package/dist/blob-limits.js.map +0 -1
- package/dist/bridge-client.d.ts +0 -69
- package/dist/bridge-client.d.ts.map +0 -1
- package/dist/bridge-client.js +0 -282
- package/dist/bridge-client.js.map +0 -1
- package/dist/client-config.d.ts +0 -6
- package/dist/client-config.d.ts.map +0 -1
- package/dist/client-config.js +0 -18
- package/dist/client-config.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/command-history.d.ts +0 -35
- package/dist/command-history.d.ts.map +0 -1
- package/dist/command-history.js +0 -378
- package/dist/command-history.js.map +0 -1
- package/dist/console-diagnostics.d.ts +0 -33
- package/dist/console-diagnostics.d.ts.map +0 -1
- package/dist/console-diagnostics.js +0 -496
- package/dist/console-diagnostics.js.map +0 -1
- package/dist/crdt-yjs/index.d.ts +0 -4
- package/dist/crdt-yjs/index.d.ts.map +0 -1
- package/dist/crdt-yjs/index.js +0 -4
- package/dist/crdt-yjs/index.js.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.js +0 -284
- package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/diagnostics.d.ts +0 -10
- package/dist/diagnostics.d.ts.map +0 -1
- package/dist/diagnostics.js +0 -83
- package/dist/diagnostics.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/generated-bridge.d.ts +0 -365
- package/dist/generated-bridge.d.ts.map +0 -1
- package/dist/generated-bridge.js +0 -250
- package/dist/generated-bridge.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mutations.d.ts +0 -72
- package/dist/mutations.d.ts.map +0 -1
- package/dist/mutations.js +0 -63
- package/dist/mutations.js.map +0 -1
- package/dist/network.d.ts +0 -3
- package/dist/network.d.ts.map +0 -1
- package/dist/network.js +0 -17
- package/dist/network.js.map +0 -1
- package/dist/react/index.d.ts +0 -169
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -628
- package/dist/react/index.js.map +0 -1
- package/dist/react-native/index.d.ts +0 -35
- package/dist/react-native/index.d.ts.map +0 -1
- package/dist/react-native/index.js +0 -49
- package/dist/react-native/index.js.map +0 -1
- package/dist/runtime-contract.d.ts +0 -13
- package/dist/runtime-contract.d.ts.map +0 -1
- package/dist/runtime-contract.js +0 -24
- package/dist/runtime-contract.js.map +0 -1
- package/dist/rust-client.d.ts +0 -106
- package/dist/rust-client.d.ts.map +0 -1
- package/dist/rust-client.js +0 -938
- package/dist/rust-client.js.map +0 -1
- package/dist/sentry.d.ts +0 -35
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -155
- package/dist/sentry.js.map +0 -1
- package/dist/sql-safety.d.ts +0 -3
- package/dist/sql-safety.d.ts.map +0 -1
- package/dist/sql-safety.js +0 -62
- package/dist/sql-safety.js.map +0 -1
- package/dist/syncular-runtime-artifacts.json +0 -61
- package/dist/tauri/index.d.ts +0 -35
- package/dist/tauri/index.d.ts.map +0 -1
- package/dist/tauri/index.js +0 -114
- package/dist/tauri/index.js.map +0 -1
- package/dist/types.d.ts +0 -1096
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/wasm/.syncular-wasm-profile +0 -1
- package/dist/wasm/syncular-runtime-artifact.json +0 -21
- package/dist/wasm/syncular.d.ts +0 -207
- package/dist/wasm/syncular.js +0 -2341
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
- package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
- package/dist/wasm-bindings/runtime-contract.js +0 -112
- package/dist/wasm-bindings/runtime-contract.js.map +0 -1
- package/dist/wasm-core/.syncular-wasm-profile +0 -1
- package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
- package/dist/wasm-core/syncular.d.ts +0 -162
- package/dist/wasm-core/syncular.js +0 -1847
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
- package/dist/wasm-perf/.syncular-wasm-profile +0 -1
- package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
- package/dist/wasm-perf/syncular.d.ts +0 -207
- package/dist/wasm-perf/syncular.js +0 -2341
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-runtime.d.ts +0 -23
- package/dist/wasm-runtime.d.ts.map +0 -1
- package/dist/wasm-runtime.js +0 -69
- package/dist/wasm-runtime.js.map +0 -1
- package/dist/worker-client.d.ts +0 -123
- package/dist/worker-client.d.ts.map +0 -1
- package/dist/worker-client.js +0 -1735
- package/dist/worker-client.js.map +0 -1
- package/dist/worker-entry.d.ts.map +0 -1
- package/dist/worker-entry.js.map +0 -1
- package/dist/worker-protocol.d.ts.map +0 -1
- package/dist/worker-protocol.js.map +0 -1
- package/dist/worker-realtime.d.ts +0 -39
- package/dist/worker-realtime.d.ts.map +0 -1
- package/dist/worker-realtime.js +0 -677
- package/dist/worker-realtime.js.map +0 -1
- package/src/auth-leases.ts +0 -251
- package/src/blob-limits.ts +0 -98
- package/src/bridge-client.ts +0 -512
- package/src/client-config.ts +0 -29
- package/src/command-history.ts +0 -623
- package/src/console-diagnostics.ts +0 -617
- package/src/crdt-yjs/index.ts +0 -3
- package/src/crdt-yjs/webview-host-facade.ts +0 -477
- package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
- package/src/diagnostics.ts +0 -116
- package/src/generated-bridge.ts +0 -741
- package/src/mutations.ts +0 -168
- package/src/network.ts +0 -32
- package/src/react/index.ts +0 -1036
- package/src/react-native/index.ts +0 -152
- package/src/runtime-contract.ts +0 -48
- package/src/rust-client.ts +0 -1491
- package/src/sentry.ts +0 -215
- package/src/sql-safety.ts +0 -61
- package/src/tauri/index.ts +0 -211
- package/src/types.ts +0 -1397
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
- package/src/wasm-bindings/runtime-contract.ts +0 -158
- package/src/wasm-runtime.ts +0 -145
- package/src/worker-client.ts +0 -2289
- package/src/worker-realtime.ts +0 -843
package/dist/index.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/client — the B3 TypeScript client protocol core
|
|
3
|
+
* (SPEC.md is normative; REVISE.md B3 is the architectural mandate).
|
|
4
|
+
*
|
|
5
|
+
* Browser-safe root: database backends live behind subpath exports
|
|
6
|
+
* (`./bun` for bun:sqlite tests, `./wasm` for sqlite-wasm + OPFS); the
|
|
7
|
+
* worker-side bootstrap lives behind `./worker`. The main-thread handle
|
|
8
|
+
* (`worker-host`) and the RPC protocol types are root exports — they
|
|
9
|
+
* import no SQLite.
|
|
10
|
+
*/
|
|
11
|
+
export * from './apply.js';
|
|
12
|
+
export * from './blob.js';
|
|
3
13
|
export * from './client.js';
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './command-history.js';
|
|
6
|
-
export * from './console-diagnostics.js';
|
|
14
|
+
export * from './content-type.js';
|
|
7
15
|
export * from './database.js';
|
|
16
|
+
export * from './encryption.js';
|
|
8
17
|
export * from './errors.js';
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
export
|
|
14
|
-
export * from './
|
|
15
|
-
|
|
18
|
+
export * from './http.js';
|
|
19
|
+
export * from './invalidation.js';
|
|
20
|
+
export * from './leader-lock.js';
|
|
21
|
+
export * from './multi-tab.js';
|
|
22
|
+
export * from './outbox.js';
|
|
23
|
+
export * from './schema.js';
|
|
24
|
+
export * from './state.js';
|
|
25
|
+
export * from './transport.js';
|
|
26
|
+
export * from './window.js';
|
|
27
|
+
export * from './worker-host.js';
|
|
28
|
+
export * from './worker-protocol.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE apply-path invalidation choke point (TODO 3.1 / DESIGN-eviction
|
|
3
|
+
* I1–I4). Every local mutation — `COMMIT` apply, segment apply (rows +
|
|
4
|
+
* sqlite images), optimistic overlay rebuild, revocation purge, schema-bump
|
|
5
|
+
* reset, and (future) window eviction — routes its touched keys through a
|
|
6
|
+
* single {@link Invalidation} accumulator, and the client emits exactly ONE
|
|
7
|
+
* {@link InvalidationEvent} per apply batch (never per row). Live queries
|
|
8
|
+
* subscribe via `SyncClient.onInvalidate` and re-run only when a table they
|
|
9
|
+
* depend on appears.
|
|
10
|
+
*
|
|
11
|
+
* Granularity truth (honest to the wire, §4.5 / §5.2):
|
|
12
|
+
* - `COMMIT` changes carry per-row `scopes` (variable → value), so their
|
|
13
|
+
* `prefix:value` scope keys (§3.1 vocabulary, I2) are emitted precisely.
|
|
14
|
+
* - Segments carry only a table + `scopeDigest`, NOT per-row scope keys, so
|
|
15
|
+
* a segment apply invalidates at the **table** granularity plus the
|
|
16
|
+
* subscription's requested/effective scope keys (the coarsest honest key
|
|
17
|
+
* the wire supports for bulk data).
|
|
18
|
+
* - Purge / reset / optimistic / eviction are keyed by table (and effective
|
|
19
|
+
* scope keys where a scope map is in hand).
|
|
20
|
+
*
|
|
21
|
+
* `tables` is therefore always the reliable floor; `scopeKeys` is a
|
|
22
|
+
* best-effort refinement present where the source carried it. A live query
|
|
23
|
+
* that cannot express its scope footprint keys off `tables` alone.
|
|
24
|
+
*/
|
|
25
|
+
import type { ScopeMap } from '@syncular/core';
|
|
26
|
+
import type { CompiledClientTable } from './schema.js';
|
|
27
|
+
/** One coalesced invalidation batch (I1). Empty batches are not emitted. */
|
|
28
|
+
export interface InvalidationEvent {
|
|
29
|
+
/** Tables whose local rows changed this batch — the reliable floor. */
|
|
30
|
+
readonly tables: ReadonlySet<string>;
|
|
31
|
+
/** `prefix:value` scope keys touched, where the source carried them (I2). */
|
|
32
|
+
readonly scopeKeys: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
export type InvalidationListener = (event: InvalidationEvent) => void;
|
|
35
|
+
/**
|
|
36
|
+
* A per-batch accumulator. One is created at the top of each apply batch,
|
|
37
|
+
* fed by the apply paths, and flushed once at the batch boundary. Mutable
|
|
38
|
+
* and cheap on purpose — no allocation until something is actually touched.
|
|
39
|
+
*/
|
|
40
|
+
export declare class Invalidation {
|
|
41
|
+
#private;
|
|
42
|
+
/** Mark a whole table dirty (the floor for any apply). */
|
|
43
|
+
table(name: string): void;
|
|
44
|
+
/** Add a raw `prefix:value` scope key (§3.1). */
|
|
45
|
+
scopeKey(key: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Add the `prefix:value` keys for an effective/requested scope map on
|
|
48
|
+
* `table` (variable → list(value)), skipping variables the table has no
|
|
49
|
+
* prefix for (never guesses a key).
|
|
50
|
+
*/
|
|
51
|
+
scopeMap(table: CompiledClientTable, scopes: ScopeMap): void;
|
|
52
|
+
/** A COMMIT change's stored scopes (variable → single value, §4.5). */
|
|
53
|
+
changeScopes(table: CompiledClientTable, scopes: Record<string, string>): void;
|
|
54
|
+
get touched(): boolean;
|
|
55
|
+
/** Freeze into an event, or undefined when nothing was touched. */
|
|
56
|
+
finish(): InvalidationEvent | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A tiny subscribable listener set. Notification is synchronous and
|
|
60
|
+
* exception-isolated (one throwing listener never starves the rest, and
|
|
61
|
+
* never corrupts the emitting apply path). Reused by the worker handle so
|
|
62
|
+
* both cores expose the identical `onInvalidate` surface.
|
|
63
|
+
*/
|
|
64
|
+
export declare class InvalidationEmitter {
|
|
65
|
+
#private;
|
|
66
|
+
on(listener: InvalidationListener): () => void;
|
|
67
|
+
emit(event: InvalidationEvent): void;
|
|
68
|
+
get size(): number;
|
|
69
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-batch accumulator. One is created at the top of each apply batch,
|
|
3
|
+
* fed by the apply paths, and flushed once at the batch boundary. Mutable
|
|
4
|
+
* and cheap on purpose — no allocation until something is actually touched.
|
|
5
|
+
*/
|
|
6
|
+
export class Invalidation {
|
|
7
|
+
#tables;
|
|
8
|
+
#scopeKeys;
|
|
9
|
+
/** Mark a whole table dirty (the floor for any apply). */
|
|
10
|
+
table(name) {
|
|
11
|
+
if (this.#tables === undefined)
|
|
12
|
+
this.#tables = new Set();
|
|
13
|
+
this.#tables.add(name);
|
|
14
|
+
}
|
|
15
|
+
/** Add a raw `prefix:value` scope key (§3.1). */
|
|
16
|
+
scopeKey(key) {
|
|
17
|
+
if (this.#scopeKeys === undefined)
|
|
18
|
+
this.#scopeKeys = new Set();
|
|
19
|
+
this.#scopeKeys.add(key);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Add the `prefix:value` keys for an effective/requested scope map on
|
|
23
|
+
* `table` (variable → list(value)), skipping variables the table has no
|
|
24
|
+
* prefix for (never guesses a key).
|
|
25
|
+
*/
|
|
26
|
+
scopeMap(table, scopes) {
|
|
27
|
+
for (const [variable, values] of Object.entries(scopes)) {
|
|
28
|
+
const prefix = table.scopePrefixByVariable.get(variable);
|
|
29
|
+
if (prefix === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
for (const v of values)
|
|
32
|
+
this.scopeKey(`${prefix}:${v}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** A COMMIT change's stored scopes (variable → single value, §4.5). */
|
|
36
|
+
changeScopes(table, scopes) {
|
|
37
|
+
for (const [variable, value] of Object.entries(scopes)) {
|
|
38
|
+
const prefix = table.scopePrefixByVariable.get(variable);
|
|
39
|
+
if (prefix !== undefined)
|
|
40
|
+
this.scopeKey(`${prefix}:${value}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
get touched() {
|
|
44
|
+
return this.#tables !== undefined || this.#scopeKeys !== undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Freeze into an event, or undefined when nothing was touched. */
|
|
47
|
+
finish() {
|
|
48
|
+
if (!this.touched)
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
tables: this.#tables ?? EMPTY,
|
|
52
|
+
scopeKeys: this.#scopeKeys ?? EMPTY,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const EMPTY = new Set();
|
|
57
|
+
/**
|
|
58
|
+
* A tiny subscribable listener set. Notification is synchronous and
|
|
59
|
+
* exception-isolated (one throwing listener never starves the rest, and
|
|
60
|
+
* never corrupts the emitting apply path). Reused by the worker handle so
|
|
61
|
+
* both cores expose the identical `onInvalidate` surface.
|
|
62
|
+
*/
|
|
63
|
+
export class InvalidationEmitter {
|
|
64
|
+
#listeners = new Set();
|
|
65
|
+
on(listener) {
|
|
66
|
+
this.#listeners.add(listener);
|
|
67
|
+
return () => {
|
|
68
|
+
this.#listeners.delete(listener);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
emit(event) {
|
|
72
|
+
for (const listener of this.#listeners) {
|
|
73
|
+
try {
|
|
74
|
+
listener(event);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// A UI listener must never break the apply path (I1).
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
get size() {
|
|
82
|
+
return this.#listeners.size;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-tab ownership seam (REVISE B3): exactly one core instance owns the
|
|
3
|
+
* local database. The interface is the whole B3 deliverable — cross-tab
|
|
4
|
+
* follower fanout is post-gate. Browsers use Web Locks; tests use the
|
|
5
|
+
* no-op single-owner lock.
|
|
6
|
+
*/
|
|
7
|
+
export interface LeaderLease {
|
|
8
|
+
release(): void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface LeaderLock {
|
|
11
|
+
/** Resolves when this instance holds leadership for `name`. */
|
|
12
|
+
acquire(name: string): Promise<LeaderLease>;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves immediately: the lease when leadership was free, `undefined`
|
|
15
|
+
* when another owner holds it. The worker handle uses this so a second
|
|
16
|
+
* tab gets a clear not-leader state instead of blocking forever
|
|
17
|
+
* (followers are post-gate, TODO 3.2).
|
|
18
|
+
*/
|
|
19
|
+
tryAcquire?(name: string): Promise<LeaderLease | undefined>;
|
|
20
|
+
}
|
|
21
|
+
/** Single-owner environments (tests, dedicated workers): always leader. */
|
|
22
|
+
export declare function singleOwnerLock(): LeaderLock;
|
|
23
|
+
/**
|
|
24
|
+
* Web Locks leader election: resolves once the exclusive lock is granted
|
|
25
|
+
* and holds it until the lease is released (tab close releases it
|
|
26
|
+
* implicitly, letting the next tab take over).
|
|
27
|
+
*/
|
|
28
|
+
export declare function webLocksLeaderLock(locks?: LockManager): LeaderLock;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Single-owner environments (tests, dedicated workers): always leader. */
|
|
2
|
+
export function singleOwnerLock() {
|
|
3
|
+
return {
|
|
4
|
+
acquire: () => Promise.resolve({ release: () => { } }),
|
|
5
|
+
tryAcquire: () => Promise.resolve({ release: () => { } }),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Web Locks leader election: resolves once the exclusive lock is granted
|
|
10
|
+
* and holds it until the lease is released (tab close releases it
|
|
11
|
+
* implicitly, letting the next tab take over).
|
|
12
|
+
*/
|
|
13
|
+
export function webLocksLeaderLock(locks) {
|
|
14
|
+
const manager = locks ?? navigator.locks;
|
|
15
|
+
return {
|
|
16
|
+
acquire: (name) => new Promise((resolveAcquire, rejectAcquire) => {
|
|
17
|
+
manager
|
|
18
|
+
.request(name, { mode: 'exclusive' }, () => new Promise((resolveHold) => {
|
|
19
|
+
resolveAcquire({ release: () => resolveHold() });
|
|
20
|
+
}))
|
|
21
|
+
.catch((error) => rejectAcquire(error));
|
|
22
|
+
}),
|
|
23
|
+
tryAcquire: (name) => new Promise((resolveTry, rejectTry) => {
|
|
24
|
+
manager
|
|
25
|
+
.request(name, { mode: 'exclusive', ifAvailable: true }, (lock) => {
|
|
26
|
+
// `ifAvailable` grants `null` instead of waiting (Web Locks).
|
|
27
|
+
if (lock === null) {
|
|
28
|
+
resolveTry(undefined);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return new Promise((resolveHold) => {
|
|
32
|
+
resolveTry({ release: () => resolveHold() });
|
|
33
|
+
});
|
|
34
|
+
})
|
|
35
|
+
.catch((error) => rejectTry(error));
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { SyncWorkerEvent } from './worker-protocol.js';
|
|
2
|
+
/** Client-local: a follower call could not reach a leader before its deadline. */
|
|
3
|
+
export declare const FOLLOWER_TIMEOUT_CODE = "client.follower_timeout";
|
|
4
|
+
/** Default deadline for a follower request (covers a leader-handover gap). */
|
|
5
|
+
export declare const DEFAULT_FOLLOWER_CALL_TIMEOUT_MS = 10000;
|
|
6
|
+
/** Max follower calls queued across a handover before we fail loudly. */
|
|
7
|
+
export declare const DEFAULT_FOLLOWER_QUEUE_LIMIT = 256;
|
|
8
|
+
interface HelloMessage {
|
|
9
|
+
readonly t: 'hello';
|
|
10
|
+
readonly fromId: string;
|
|
11
|
+
/** Highest epoch the sender has observed (helps a promoter monotonically
|
|
12
|
+
* advance even if it never saw the previous leader's announce). */
|
|
13
|
+
readonly epoch?: number;
|
|
14
|
+
}
|
|
15
|
+
interface ByeMessage {
|
|
16
|
+
readonly t: 'bye';
|
|
17
|
+
readonly fromId: string;
|
|
18
|
+
}
|
|
19
|
+
interface ReqMessage {
|
|
20
|
+
readonly t: 'req';
|
|
21
|
+
readonly epoch: number;
|
|
22
|
+
readonly fromId: string;
|
|
23
|
+
readonly reqId: number;
|
|
24
|
+
readonly method: string;
|
|
25
|
+
readonly args: readonly unknown[];
|
|
26
|
+
}
|
|
27
|
+
interface AnnounceMessage {
|
|
28
|
+
readonly t: 'announce';
|
|
29
|
+
readonly epoch: number;
|
|
30
|
+
readonly clientId: string;
|
|
31
|
+
}
|
|
32
|
+
interface ResMessage {
|
|
33
|
+
readonly t: 'res';
|
|
34
|
+
readonly epoch: number;
|
|
35
|
+
readonly reqId: number;
|
|
36
|
+
readonly ok: boolean;
|
|
37
|
+
readonly value?: unknown;
|
|
38
|
+
readonly error?: {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
retryable: boolean;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
interface EventMessage {
|
|
45
|
+
readonly t: 'event';
|
|
46
|
+
readonly epoch: number;
|
|
47
|
+
readonly event: SyncWorkerEvent;
|
|
48
|
+
}
|
|
49
|
+
export type MultiTabMessage = HelloMessage | ByeMessage | ReqMessage | AnnounceMessage | ResMessage | EventMessage;
|
|
50
|
+
/**
|
|
51
|
+
* The tiny cross-tab channel surface we depend on — the DOM `BroadcastChannel`
|
|
52
|
+
* satisfies it. Injectable so bun tests can pair two instances by name.
|
|
53
|
+
*/
|
|
54
|
+
export interface CrossTabChannel {
|
|
55
|
+
postMessage(message: MultiTabMessage): void;
|
|
56
|
+
addEventListener(type: 'message', listener: (event: {
|
|
57
|
+
data: MultiTabMessage;
|
|
58
|
+
}) => void): void;
|
|
59
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
60
|
+
data: MultiTabMessage;
|
|
61
|
+
}) => void): void;
|
|
62
|
+
close(): void;
|
|
63
|
+
}
|
|
64
|
+
/** Default channel factory: a real `BroadcastChannel` named per lock. */
|
|
65
|
+
export declare function broadcastChannelFactory(): (name: string) => CrossTabChannel;
|
|
66
|
+
/** The channel name a leader and its followers rendezvous on. */
|
|
67
|
+
export declare function multiTabChannelName(lockName: string): string;
|
|
68
|
+
/** A per-tab identity for addressing (not a presence identity). */
|
|
69
|
+
export declare function newTabId(): string;
|
|
70
|
+
/**
|
|
71
|
+
* Runs on the leader tab. Announces leadership, answers follower `req`s by
|
|
72
|
+
* invoking the (already-running) worker via `invoke`, and rebroadcasts every
|
|
73
|
+
* worker event to followers. Owns nothing about the worker lifecycle — the
|
|
74
|
+
* worker-host stays the single core owner; this is a relay.
|
|
75
|
+
*/
|
|
76
|
+
export declare class LeaderBridge {
|
|
77
|
+
#private;
|
|
78
|
+
constructor(options: {
|
|
79
|
+
channel: CrossTabChannel;
|
|
80
|
+
epoch: number;
|
|
81
|
+
clientId: string;
|
|
82
|
+
invoke: (method: string, args: readonly unknown[]) => Promise<unknown>;
|
|
83
|
+
});
|
|
84
|
+
/** Re-announce leadership (on promotion and on a follower `hello`). */
|
|
85
|
+
announce(): void;
|
|
86
|
+
/** Fan a worker event out to all followers on the current epoch. */
|
|
87
|
+
broadcastEvent(event: SyncWorkerEvent): void;
|
|
88
|
+
close(): void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Runs on a follower tab. Sends `req`s to the leader and settles them on the
|
|
92
|
+
* matching `res`; queues calls (bounded, timed) while no leader is bound (the
|
|
93
|
+
* handover gap) and flushes them once an `announce` binds a new leader.
|
|
94
|
+
* Feeds fanned-out events to `onEvent`. Learns leadership changes and hands
|
|
95
|
+
* the resolved leader `clientId` back through `onLeaderChange`.
|
|
96
|
+
*/
|
|
97
|
+
export declare class FollowerLink {
|
|
98
|
+
#private;
|
|
99
|
+
constructor(options: {
|
|
100
|
+
channel: CrossTabChannel;
|
|
101
|
+
fromId: string;
|
|
102
|
+
onEvent: (event: SyncWorkerEvent) => void;
|
|
103
|
+
onLeaderChange: (clientId: string) => void;
|
|
104
|
+
callTimeoutMs?: number;
|
|
105
|
+
queueLimit?: number;
|
|
106
|
+
});
|
|
107
|
+
get epoch(): number;
|
|
108
|
+
get maxEpochSeen(): number;
|
|
109
|
+
get leaderClientId(): string;
|
|
110
|
+
/** Whether a leader is currently bound (an announce has been heard). */
|
|
111
|
+
get bound(): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Resolve once the link is bound to a leader (the first `announce` arrived),
|
|
114
|
+
* or reject if `timeoutMs` elapses first. A caller awaits this before
|
|
115
|
+
* treating the follower as ready: until an announce is processed the link's
|
|
116
|
+
* epoch is -1, so any fanned-out `event` in that window is dropped (an
|
|
117
|
+
* unbound follower cannot match the leader's epoch). Awaiting binding closes
|
|
118
|
+
* that gap — a just-opened follower tab never misses an invalidation emitted
|
|
119
|
+
* between its `hello` and the leader's `announce`. Resolves synchronously
|
|
120
|
+
* when already bound.
|
|
121
|
+
*/
|
|
122
|
+
waitUntilBound(timeoutMs?: number): Promise<void>;
|
|
123
|
+
/** Forward one logical API call to the leader (queued if unbound). */
|
|
124
|
+
call(method: string, args: readonly unknown[]): Promise<unknown>;
|
|
125
|
+
/**
|
|
126
|
+
* The leader we were bound to went away (its lock released). Un-bind so new
|
|
127
|
+
* calls queue again; re-request an announce so the next leader finds us.
|
|
128
|
+
* In-flight calls stay pending — their per-call timeout is the backstop,
|
|
129
|
+
* and the winner's announce will flush any that were queued.
|
|
130
|
+
*/
|
|
131
|
+
unbind(): void;
|
|
132
|
+
close(): void;
|
|
133
|
+
}
|
|
134
|
+
export {};
|