@syncular/client 0.1.3 → 0.2.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.
Files changed (233) hide show
  1. package/README.md +227 -709
  2. package/dist/apply.d.ts +80 -0
  3. package/dist/apply.js +301 -0
  4. package/dist/blob.d.ts +127 -0
  5. package/dist/blob.js +201 -0
  6. package/dist/bun-database.d.ts +22 -0
  7. package/dist/bun-database.js +61 -0
  8. package/dist/client.d.ts +320 -69
  9. package/dist/client.js +1627 -163
  10. package/dist/content-type.d.ts +2 -0
  11. package/dist/content-type.js +2 -0
  12. package/dist/database.d.ts +35 -80
  13. package/dist/database.js +32 -947
  14. package/dist/encryption.d.ts +47 -0
  15. package/dist/encryption.js +75 -0
  16. package/dist/errors.d.ts +8 -22
  17. package/dist/errors.js +10 -207
  18. package/dist/http.d.ts +32 -0
  19. package/dist/http.js +196 -0
  20. package/dist/index.d.ts +28 -16
  21. package/dist/index.js +25 -12
  22. package/dist/invalidation.d.ts +69 -0
  23. package/dist/invalidation.js +84 -0
  24. package/dist/leader-lock.d.ts +28 -0
  25. package/dist/leader-lock.js +38 -0
  26. package/dist/multi-tab.d.ts +134 -0
  27. package/dist/multi-tab.js +399 -0
  28. package/dist/node-database.d.ts +41 -0
  29. package/dist/node-database.js +133 -0
  30. package/dist/outbox.d.ts +56 -0
  31. package/dist/outbox.js +140 -0
  32. package/dist/schema.d.ts +107 -0
  33. package/dist/schema.js +267 -0
  34. package/dist/state.d.ts +40 -0
  35. package/dist/state.js +77 -0
  36. package/dist/transport.d.ts +44 -0
  37. package/dist/transport.js +7 -0
  38. package/dist/wasm-database.d.ts +30 -0
  39. package/dist/wasm-database.js +170 -0
  40. package/dist/window.d.ts +64 -0
  41. package/dist/window.js +0 -0
  42. package/dist/worker-entry.d.ts +16 -2
  43. package/dist/worker-entry.js +300 -456
  44. package/dist/worker-host.d.ts +174 -0
  45. package/dist/worker-host.js +536 -0
  46. package/dist/worker-protocol.d.ts +173 -61
  47. package/dist/worker-protocol.js +7 -16
  48. package/package.json +45 -112
  49. package/src/apply.ts +435 -0
  50. package/src/blob.ts +333 -0
  51. package/src/bun-database.ts +75 -0
  52. package/src/client.ts +2247 -277
  53. package/src/content-type.ts +2 -0
  54. package/src/database.ts +63 -1389
  55. package/src/encryption.ts +123 -0
  56. package/src/errors.ts +11 -265
  57. package/src/http.ts +254 -0
  58. package/src/index.ts +25 -20
  59. package/src/invalidation.ts +128 -0
  60. package/src/leader-lock.ts +68 -0
  61. package/src/multi-tab.ts +550 -0
  62. package/src/node-database.ts +180 -0
  63. package/src/outbox.ts +206 -0
  64. package/src/schema.ts +397 -0
  65. package/src/state.ts +126 -0
  66. package/src/transport.ts +52 -0
  67. package/src/wasm-database.ts +279 -0
  68. package/src/window.ts +0 -0
  69. package/src/worker-entry.ts +391 -545
  70. package/src/worker-host.ts +800 -0
  71. package/src/worker-protocol.ts +204 -99
  72. package/dist/auth-leases.d.ts +0 -11
  73. package/dist/auth-leases.d.ts.map +0 -1
  74. package/dist/auth-leases.js +0 -140
  75. package/dist/auth-leases.js.map +0 -1
  76. package/dist/blob-limits.d.ts +0 -15
  77. package/dist/blob-limits.d.ts.map +0 -1
  78. package/dist/blob-limits.js +0 -66
  79. package/dist/blob-limits.js.map +0 -1
  80. package/dist/bridge-client.d.ts +0 -69
  81. package/dist/bridge-client.d.ts.map +0 -1
  82. package/dist/bridge-client.js +0 -282
  83. package/dist/bridge-client.js.map +0 -1
  84. package/dist/client-config.d.ts +0 -6
  85. package/dist/client-config.d.ts.map +0 -1
  86. package/dist/client-config.js +0 -18
  87. package/dist/client-config.js.map +0 -1
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js.map +0 -1
  90. package/dist/command-history.d.ts +0 -35
  91. package/dist/command-history.d.ts.map +0 -1
  92. package/dist/command-history.js +0 -378
  93. package/dist/command-history.js.map +0 -1
  94. package/dist/console-diagnostics.d.ts +0 -33
  95. package/dist/console-diagnostics.d.ts.map +0 -1
  96. package/dist/console-diagnostics.js +0 -496
  97. package/dist/console-diagnostics.js.map +0 -1
  98. package/dist/crdt-yjs/index.d.ts +0 -4
  99. package/dist/crdt-yjs/index.d.ts.map +0 -1
  100. package/dist/crdt-yjs/index.js +0 -4
  101. package/dist/crdt-yjs/index.js.map +0 -1
  102. package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
  103. package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
  104. package/dist/crdt-yjs/webview-host-facade.js +0 -284
  105. package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
  106. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
  107. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
  108. package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
  109. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
  110. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
  111. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
  112. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
  113. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
  114. package/dist/database.d.ts.map +0 -1
  115. package/dist/database.js.map +0 -1
  116. package/dist/diagnostics.d.ts +0 -10
  117. package/dist/diagnostics.d.ts.map +0 -1
  118. package/dist/diagnostics.js +0 -83
  119. package/dist/diagnostics.js.map +0 -1
  120. package/dist/errors.d.ts.map +0 -1
  121. package/dist/errors.js.map +0 -1
  122. package/dist/generated-bridge.d.ts +0 -365
  123. package/dist/generated-bridge.d.ts.map +0 -1
  124. package/dist/generated-bridge.js +0 -250
  125. package/dist/generated-bridge.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/mutations.d.ts +0 -72
  129. package/dist/mutations.d.ts.map +0 -1
  130. package/dist/mutations.js +0 -63
  131. package/dist/mutations.js.map +0 -1
  132. package/dist/network.d.ts +0 -3
  133. package/dist/network.d.ts.map +0 -1
  134. package/dist/network.js +0 -17
  135. package/dist/network.js.map +0 -1
  136. package/dist/react/index.d.ts +0 -169
  137. package/dist/react/index.d.ts.map +0 -1
  138. package/dist/react/index.js +0 -628
  139. package/dist/react/index.js.map +0 -1
  140. package/dist/react-native/index.d.ts +0 -35
  141. package/dist/react-native/index.d.ts.map +0 -1
  142. package/dist/react-native/index.js +0 -49
  143. package/dist/react-native/index.js.map +0 -1
  144. package/dist/runtime-contract.d.ts +0 -13
  145. package/dist/runtime-contract.d.ts.map +0 -1
  146. package/dist/runtime-contract.js +0 -24
  147. package/dist/runtime-contract.js.map +0 -1
  148. package/dist/rust-client.d.ts +0 -106
  149. package/dist/rust-client.d.ts.map +0 -1
  150. package/dist/rust-client.js +0 -938
  151. package/dist/rust-client.js.map +0 -1
  152. package/dist/sentry.d.ts +0 -35
  153. package/dist/sentry.d.ts.map +0 -1
  154. package/dist/sentry.js +0 -155
  155. package/dist/sentry.js.map +0 -1
  156. package/dist/sql-safety.d.ts +0 -3
  157. package/dist/sql-safety.d.ts.map +0 -1
  158. package/dist/sql-safety.js +0 -62
  159. package/dist/sql-safety.js.map +0 -1
  160. package/dist/syncular-runtime-artifacts.json +0 -61
  161. package/dist/tauri/index.d.ts +0 -35
  162. package/dist/tauri/index.d.ts.map +0 -1
  163. package/dist/tauri/index.js +0 -114
  164. package/dist/tauri/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -1096
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -2
  168. package/dist/types.js.map +0 -1
  169. package/dist/wasm/.syncular-wasm-profile +0 -1
  170. package/dist/wasm/syncular-runtime-artifact.json +0 -21
  171. package/dist/wasm/syncular.d.ts +0 -207
  172. package/dist/wasm/syncular.js +0 -2341
  173. package/dist/wasm/syncular_bg.wasm +0 -0
  174. package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
  175. package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
  176. package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
  177. package/dist/wasm-bindings/runtime-contract.js +0 -112
  178. package/dist/wasm-bindings/runtime-contract.js.map +0 -1
  179. package/dist/wasm-core/.syncular-wasm-profile +0 -1
  180. package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
  181. package/dist/wasm-core/syncular.d.ts +0 -162
  182. package/dist/wasm-core/syncular.js +0 -1847
  183. package/dist/wasm-core/syncular_bg.wasm +0 -0
  184. package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
  185. package/dist/wasm-perf/.syncular-wasm-profile +0 -1
  186. package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
  187. package/dist/wasm-perf/syncular.d.ts +0 -207
  188. package/dist/wasm-perf/syncular.js +0 -2341
  189. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  190. package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
  191. package/dist/wasm-runtime.d.ts +0 -23
  192. package/dist/wasm-runtime.d.ts.map +0 -1
  193. package/dist/wasm-runtime.js +0 -69
  194. package/dist/wasm-runtime.js.map +0 -1
  195. package/dist/worker-client.d.ts +0 -123
  196. package/dist/worker-client.d.ts.map +0 -1
  197. package/dist/worker-client.js +0 -1735
  198. package/dist/worker-client.js.map +0 -1
  199. package/dist/worker-entry.d.ts.map +0 -1
  200. package/dist/worker-entry.js.map +0 -1
  201. package/dist/worker-protocol.d.ts.map +0 -1
  202. package/dist/worker-protocol.js.map +0 -1
  203. package/dist/worker-realtime.d.ts +0 -39
  204. package/dist/worker-realtime.d.ts.map +0 -1
  205. package/dist/worker-realtime.js +0 -677
  206. package/dist/worker-realtime.js.map +0 -1
  207. package/src/auth-leases.ts +0 -251
  208. package/src/blob-limits.ts +0 -98
  209. package/src/bridge-client.ts +0 -512
  210. package/src/client-config.ts +0 -29
  211. package/src/command-history.ts +0 -623
  212. package/src/console-diagnostics.ts +0 -617
  213. package/src/crdt-yjs/index.ts +0 -3
  214. package/src/crdt-yjs/webview-host-facade.ts +0 -477
  215. package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
  216. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
  217. package/src/diagnostics.ts +0 -116
  218. package/src/generated-bridge.ts +0 -741
  219. package/src/mutations.ts +0 -168
  220. package/src/network.ts +0 -32
  221. package/src/react/index.ts +0 -1036
  222. package/src/react-native/index.ts +0 -152
  223. package/src/runtime-contract.ts +0 -48
  224. package/src/rust-client.ts +0 -1491
  225. package/src/sentry.ts +0 -215
  226. package/src/sql-safety.ts +0 -61
  227. package/src/tauri/index.ts +0 -211
  228. package/src/types.ts +0 -1397
  229. package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
  230. package/src/wasm-bindings/runtime-contract.ts +0 -158
  231. package/src/wasm-runtime.ts +0 -145
  232. package/src/worker-client.ts +0 -2289
  233. package/src/worker-realtime.ts +0 -843
package/dist/index.js CHANGED
@@ -1,15 +1,28 @@
1
- export * from './blob-limits.js';
2
- export * from './bridge-client.js';
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 './client-config.js';
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 './mutations.js';
10
- export * from './network.js';
11
- export * from './runtime-contract.js';
12
- export * from './types.js';
13
- export { getSyncularPackagedRuntimeArtifacts, getSyncularRuntimeArtifact, getSyncularRuntimeArtifactCatalogUrl, getSyncularWasmGlueUrl, getSyncularWasmUrl, resolveSyncularRuntimeArtifactCatalog, selectSyncularRuntimeArtifact, } from './wasm-runtime.js';
14
- export * from './worker-client.js';
15
- //# sourceMappingURL=index.js.map
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 {};