@lunora/client 1.0.0-alpha.22 → 1.0.0-alpha.221

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 (92) hide show
  1. package/README.md +16 -3
  2. package/dist/auth/index.d.mts +43 -12
  3. package/dist/auth/index.d.ts +43 -12
  4. package/dist/auth/index.mjs +1 -60
  5. package/dist/index.d.mts +768 -416
  6. package/dist/index.d.ts +768 -416
  7. package/dist/index.mjs +1 -20
  8. package/dist/packem_shared/CONFLICT_ERROR_CODE-B7qNZMMk.mjs +1 -0
  9. package/dist/packem_shared/DEFAULT_MAX_BUFFER-Bd-_iyiV.mjs +1 -0
  10. package/dist/packem_shared/LunoraClient-C7RQBwz0.mjs +1 -0
  11. package/dist/packem_shared/OfflineQueue-BWamzR6p.mjs +1 -0
  12. package/dist/packem_shared/RETIRE_AFTER_DURABLE_SEQ_ADVANCE-DoEHtuR8.mjs +1 -0
  13. package/dist/packem_shared/SKIP-d7LeP-sY.mjs +1 -0
  14. package/dist/packem_shared/SubscriptionRegistry-DKIXE7M7.mjs +1 -0
  15. package/dist/packem_shared/TabCoordinator-CXD7vulJ.mjs +1 -0
  16. package/dist/packem_shared/applyDelta-CWpwS3Bb.mjs +1 -0
  17. package/dist/packem_shared/createAsyncStoragePersistence-6FCvqc6C.mjs +1 -0
  18. package/dist/packem_shared/createAsyncStorageQueryCache-f5023fdB.mjs +1 -0
  19. package/dist/packem_shared/createCallRunner-Bwcrfh76.mjs +1 -0
  20. package/dist/packem_shared/createClientQuery-B8Nfj-7o.mjs +1 -0
  21. package/dist/packem_shared/createInMemoryBookmarkStorage-BooZhW0n.mjs +1 -0
  22. package/dist/packem_shared/createInMemoryPersistence-Cz2W6XVj.mjs +1 -0
  23. package/dist/packem_shared/createInMemoryQueryCache-D2uNHLfM.mjs +1 -0
  24. package/dist/packem_shared/createLocalStore-BT0QPKdf.mjs +1 -0
  25. package/dist/packem_shared/createMutatorRunner-CuMRJpUx.mjs +1 -0
  26. package/dist/packem_shared/createReconnect-CjTmjJDH.mjs +1 -0
  27. package/dist/packem_shared/createServerClient-C5GKWZsC.mjs +1 -0
  28. package/dist/packem_shared/createSnapshotPrecondition-CLkxVeN1.mjs +1 -0
  29. package/dist/packem_shared/delta-merge-Ls3DED4g.mjs +1 -0
  30. package/dist/packem_shared/deserializePreloaded-CyFK_cv6.mjs +1 -0
  31. package/dist/packem_shared/function-reference.d-Br_hsKje.d.mts +45 -0
  32. package/dist/packem_shared/function-reference.d-Br_hsKje.d.ts +45 -0
  33. package/dist/packem_shared/getServerSession-BaOoo1p6.mjs +1 -0
  34. package/dist/packem_shared/httpStream-BhSpxRWB.mjs +9 -0
  35. package/dist/packem_shared/idb-utility-C8WS390w.mjs +1 -0
  36. package/dist/packem_shared/local-store-fqWV2n_Z.mjs +1 -0
  37. package/dist/packem_shared/lunora-client.d-B22TAgVj.d.mts +4720 -0
  38. package/dist/packem_shared/lunora-client.d-Cq8NykWg.d.ts +4720 -0
  39. package/dist/packem_shared/offline-queue-D_fr-uQ3.mjs +1 -0
  40. package/dist/packem_shared/preload.d-3Sb8o5sD.d.mts +21 -0
  41. package/dist/packem_shared/preload.d-CPzCWpiu.d.ts +21 -0
  42. package/dist/packem_shared/preloadQuery-uFy24PCR.mjs +1 -0
  43. package/dist/packem_shared/single-blob-store-DpqSxsgi.mjs +1 -0
  44. package/dist/packem_shared/wire-errors-WYlbE1Nh.mjs +1 -0
  45. package/dist/pagination/index.d.mts +42 -42
  46. package/dist/pagination/index.d.ts +42 -42
  47. package/dist/pagination/index.mjs +1 -61
  48. package/dist/query/index.d.mts +111 -43
  49. package/dist/query/index.d.ts +111 -43
  50. package/dist/query/index.mjs +1 -1
  51. package/dist/service.d.mts +49 -0
  52. package/dist/service.d.ts +49 -0
  53. package/dist/service.mjs +1 -0
  54. package/dist/ssr/index.d.mts +109 -79
  55. package/dist/ssr/index.d.ts +109 -79
  56. package/dist/ssr/index.mjs +1 -4
  57. package/dist/upload.d.mts +35 -0
  58. package/dist/upload.d.ts +35 -0
  59. package/dist/upload.mjs +1 -0
  60. package/package.json +15 -2
  61. package/dist/packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs +0 -33
  62. package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +0 -100
  63. package/dist/packem_shared/DEFAULT_MAX_BUFFER-7hFnzNk9.mjs +0 -110
  64. package/dist/packem_shared/LunoraClient-Clb118SU.mjs +0 -3717
  65. package/dist/packem_shared/OfflineQueue-B4HUF7rt.mjs +0 -1
  66. package/dist/packem_shared/SKIP-vItZChkw.mjs +0 -50
  67. package/dist/packem_shared/SubscriptionRegistry-D4jfIzZu.mjs +0 -31
  68. package/dist/packem_shared/TabCoordinator-BwRR8H06.mjs +0 -222
  69. package/dist/packem_shared/applyDelta-CRKZ1PBt.mjs +0 -81
  70. package/dist/packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs +0 -45
  71. package/dist/packem_shared/createClientQuery-CQ51bWAE.mjs +0 -71
  72. package/dist/packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs +0 -11
  73. package/dist/packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs +0 -79
  74. package/dist/packem_shared/createInMemoryQueryCache-DiaGZkA2.mjs +0 -112
  75. package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +0 -2
  76. package/dist/packem_shared/createMutationRunner-BqsavzvG.mjs +0 -21
  77. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +0 -31
  78. package/dist/packem_shared/createReconnect-Di_-oHH7.mjs +0 -22
  79. package/dist/packem_shared/createReply-lI4tVS2w.mjs +0 -36
  80. package/dist/packem_shared/createServerClient-Dxemst5C.mjs +0 -11
  81. package/dist/packem_shared/createSnapshotPrecondition-CBwnVz6r.mjs +0 -18
  82. package/dist/packem_shared/deserializePreloaded-C0eJTY_W.mjs +0 -4
  83. package/dist/packem_shared/getServerSession-8jXewqxd.mjs +0 -13
  84. package/dist/packem_shared/idb-utility-DrSVX43Q.mjs +0 -48
  85. package/dist/packem_shared/local-store-DtcIW4c0.mjs +0 -106
  86. package/dist/packem_shared/lunora-client.d-pw-9sLl0.d.mts +0 -2722
  87. package/dist/packem_shared/lunora-client.d-pw-9sLl0.d.ts +0 -2722
  88. package/dist/packem_shared/offline-queue-CF4_Co5k.mjs +0 -199
  89. package/dist/packem_shared/preload.d-6ME5ubgq.d.mts +0 -20
  90. package/dist/packem_shared/preload.d-BkQr-3Vh.d.ts +0 -20
  91. package/dist/packem_shared/preloadQuery-lobFkD2Z.mjs +0 -13
  92. package/dist/packem_shared/stable-key-wv6eP48B.mjs +0 -40
@@ -0,0 +1,4720 @@
1
+ import { LunoraErrorCodeInput, LunoraErrorCode } from '@lunora/errors';
2
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.js";
3
+ import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
4
+ /**
5
+ * Typed reference to an HTTP-SSE stream route (`httpRoute.<verb>(path).stream()`)
6
+ * emitted by `@lunora/codegen` as `httpStreams.<namespace>.<name>`.
7
+ *
8
+ * Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
9
+ * (opened with `fetch` + `ReadableStream` against the route's own URL), not the
10
+ * WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
11
+ * and the route path; the phantom marker carries the chunk / searchParams /
12
+ * params types so `httpStream` (and the framework hooks over it) infer the
13
+ * chunk type end-to-end.
14
+ */
15
+ /**
16
+ * A typed reference to a `defineRoom` room, as the generated `rooms` object carries
17
+ * it. At runtime it is the room's name; the phantom marker carries its message type,
18
+ * so `connectRoom(rooms.chat, …)` types `onMessage` and `send` from the room itself.
19
+ */
20
+ interface RoomRef<Message = unknown> {
21
+ /**
22
+ * Phantom marker carrying the room's message type for inference. Never present
23
+ * at runtime.
24
+ */
25
+ readonly __lunoraRoomMessage?: Message;
26
+ /** The room's name, which the shard routes on (`room:<name>`). */
27
+ readonly name: string;
28
+ }
29
+ interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
30
+ /**
31
+ * Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
32
+ * parameters for inference. Never present at runtime; declared in a
33
+ * covariant (output) position so a concrete reference stays assignable to
34
+ * a widened one.
35
+ */
36
+ readonly __lunoraHttpStream?: {
37
+ chunk: Chunk;
38
+ params: Params;
39
+ searchParams: SearchParams;
40
+ };
41
+ /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
42
+ readonly method: string;
43
+ /** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
44
+ readonly path: string;
45
+ }
46
+ /**
47
+ * The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
48
+ */
49
+ interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
50
+ /** Values for the route path's `:name` segments. */
51
+ params?: Params;
52
+ /** URL query params, appended to the request URL (undefined entries are skipped). */
53
+ searchParams?: SearchParams;
54
+ }
55
+ /**
56
+ * Extract the chunk type from a {@link HttpStreamRef}.
57
+ */
58
+ type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
59
+ /**
60
+ * Extract the call-side args type from a {@link HttpStreamRef}.
61
+ */
62
+ type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
63
+ type Unsubscribe = () => void;
64
+ /**
65
+ * Serializable result of `preloadQuery`. Produced on the server during SSR,
66
+ * embedded in the rendered HTML, then handed to `usePreloadedQuery` on the
67
+ * client so the first render shows the server value with no loading flash
68
+ * before a live subscription attaches. Every field survives `JSON.stringify`.
69
+ */
70
+ interface Preloaded<T = unknown> {
71
+ readonly __lunoraPreloaded: true;
72
+ readonly args: Record<string, unknown>;
73
+ readonly functionPath: string;
74
+ readonly shardKey?: string;
75
+ readonly value: T;
76
+ }
77
+ /**
78
+ * Pluggable storage for the `x-d1-bookmark` value used to provide
79
+ * read-your-writes between a mutation and subsequent queries.
80
+ */
81
+ interface BookmarkStorage {
82
+ get: () => string | null;
83
+ set: (value: string | null) => void;
84
+ }
85
+ interface ReconnectOptions {
86
+ initialDelayMs?: number;
87
+ jitter?: boolean;
88
+ maxDelayMs?: number;
89
+ }
90
+ /** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
91
+ type PersistenceOperation = "append" | "clear" | "load" | "remove" | "replace";
92
+ /** Context handed to a persistence-error handler. */
93
+ interface PersistenceErrorContext {
94
+ readonly error: unknown;
95
+ /** The mutation id involved, when the failing op was scoped to one (`append`/`remove`). */
96
+ readonly mutationId?: string;
97
+ readonly operation: PersistenceOperation;
98
+ }
99
+ interface OfflineQueueOptions {
100
+ maxItems?: number;
101
+ /**
102
+ * Invoked when a {@link PersistenceAdapter} call rejects (e.g. IndexedDB quota
103
+ * exceeded). Without a handler, failures are logged via `console.warn` so they
104
+ * are never fully silent. Note: a failed `append` means the write is queued in
105
+ * memory but NOT durable — it will not survive a reload.
106
+ */
107
+ onPersistenceError?: (context: PersistenceErrorContext) => void;
108
+ /**
109
+ * Queue mutations issued before a shard's first successful WebSocket
110
+ * connect (defaults to `false`). The standard behaviour (`LunoraClient`'s
111
+ * `mutation()`) queues only when the targeted shard has been connected at
112
+ * least once (`wasEverConnected`), so the registry / resubscribe handshake
113
+ * has run. Set this to `true` for offline-first apps that want to enqueue
114
+ * writes on the very first session before the WS is up.
115
+ */
116
+ queueBeforeFirstConnect?: boolean;
117
+ }
118
+ /**
119
+ * Serializable shape of an offline mutation, durably stored by a
120
+ * {@link PersistenceAdapter} so queued writes survive a reload/crash. The live
121
+ * `resolve`/`reject` callbacks of an in-flight `QueuedMutation` are *not*
122
+ * persisted — a restored mutation is replayed with no original awaiter.
123
+ */
124
+ interface PersistedMutation {
125
+ args: Record<string, unknown>;
126
+ /**
127
+ * The CDC cursor this write was composed against, persisted so a replay —
128
+ * including one after a reload, days later — is still judged against what its
129
+ * author could actually see. Consumed only by `.dropStalePatches()` tables.
130
+ *
131
+ * Persisting it is the entire point of the feature. Re-deriving a baseline at
132
+ * replay time would read the cursor the client has ADVANCED to while the write
133
+ * sat in the queue, which is the newer state the write must be compared
134
+ * against — so the stale write would always look fresh and always clobber.
135
+ *
136
+ * Absent on records written by older client versions, and on a client with no
137
+ * live subscription to take a cursor from; both replay with no baseline, which
138
+ * applies the write unchanged.
139
+ */
140
+ baselineSeq?: number;
141
+ /**
142
+ * The client id that queued this write, persisted so a replay after a reload
143
+ * lands in the SAME server-side dedup namespace it was issued under. The
144
+ * standalone client's own `clientId` is minted per session, so replaying under
145
+ * the live one would miss the `__idempotency` row for an anonymous caller and
146
+ * re-run a write the server already committed. Absent on records written by
147
+ * older client versions, which replay under the live id.
148
+ */
149
+ clientId?: string;
150
+ functionPath: string;
151
+ id: string;
152
+ /**
153
+ * Issuing identity fingerprint, persisted so a hydrated write replays only
154
+ * under the identity that queued it (`null` = queued while signed out).
155
+ * Absent on records written by older client versions, which replay under
156
+ * the ambient identity for back-compat.
157
+ */
158
+ identity?: string | null;
159
+ shardKey?: string;
160
+ /**
161
+ * App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
162
+ * On hydrate, a record whose `version` doesn't match the current one is dropped
163
+ * and purged rather than replayed — so a write persisted by an older deploy
164
+ * (with a now-changed function signature) can't replay against the new schema.
165
+ * Absent when no `persistenceVersion` is configured (no version gating).
166
+ */
167
+ version?: string;
168
+ }
169
+ /**
170
+ * Durable store for the offline mutation queue. The default client keeps the
171
+ * queue in memory; supplying an adapter (e.g. `createIndexedDbPersistence`)
172
+ * makes queued writes survive a page reload. Implementations must preserve FIFO
173
+ * (enqueue) order in `PersistenceAdapter.load`.
174
+ *
175
+ * Replay semantics are at-least-once: a mutation is removed only after the
176
+ * server confirms (or rejects) it, so a crash between commit and `remove` can
177
+ * replay it again on the next load.
178
+ */
179
+ interface PersistenceAdapter {
180
+ /** Append a mutation to durable storage (called on enqueue). */
181
+ append: (mutation: PersistedMutation) => Promise<void>;
182
+ /** Drop every persisted mutation (e.g. on logout). */
183
+ clear: () => Promise<void>;
184
+ /** Load all persisted mutations in FIFO order — called once at startup. */
185
+ load: () => Promise<PersistedMutation[]>;
186
+ /** Remove a mutation by id once it has been replayed (resolved or rejected). */
187
+ remove: (id: string) => Promise<void>;
188
+ /**
189
+ * Overwrite an already-persisted mutation IN PLACE, keeping its position in
190
+ * FIFO order. Used when a queued write's identity stamp is rewritten after a
191
+ * sign-in / sign-out.
192
+ *
193
+ * Must be atomic: a `remove` + `append` pair has a window where a process
194
+ * stop leaves the mutation in no durable store at all, and the in-memory
195
+ * entry has already advanced, so a reload loses the write outright. It also
196
+ * moved the record to the BACK of the queue, replaying it out of the order
197
+ * it was issued in. Implementations do the whole swap under one transaction
198
+ * (or one serialized blob write).
199
+ *
200
+ * A mutation whose id is not present is left alone — the record was drained
201
+ * concurrently and re-inserting it would replay a settled write.
202
+ */
203
+ replace: (mutation: PersistedMutation) => Promise<void>;
204
+ }
205
+ /**
206
+ * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
207
+ * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
208
+ * the durable outbox needs to dedupe and watermark replays.
209
+ */
210
+ interface OutboxMutation {
211
+ args: Record<string, unknown>;
212
+ /**
213
+ * The CDC cursor this write was composed against — see
214
+ * {@link PersistedMutation.baselineSeq}, whose contract this mirrors.
215
+ *
216
+ * A sink MUST persist it and hand it back unchanged on replay. This is the
217
+ * canonical statement of that rule for the durable path; every other site
218
+ * that carries the field points here. Re-deriving one at replay time reads
219
+ * the cursor the client has since advanced to — the newer state the write is
220
+ * supposed to be judged against — so a stale write always looks fresh and
221
+ * always clobbers.
222
+ *
223
+ * `undefined` when the client has no live subscription to take a cursor from,
224
+ * which replays the write unchanged.
225
+ */
226
+ baselineSeq?: number;
227
+ /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
228
+ clientId: string;
229
+ functionPath: string;
230
+ /**
231
+ * `${clientId}:${mutationId}`, or the caller's own `mutationId` when it passed
232
+ * one (an `importRows` chunk) — sent as `x-lunora-mutation-id` so a replay is
233
+ * server-idempotent.
234
+ */
235
+ idempotencyKey: string;
236
+ /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
237
+ identity: string | null;
238
+ /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
239
+ mutationId: number;
240
+ /**
241
+ * Roll this write's optimistic patch back. The sink's owner invokes it when
242
+ * the replay reaches a PERMANENT verdict (a coded rejection, an identity
243
+ * drop) — never on a transient failure it will retry, and never on success.
244
+ *
245
+ * Without it a rejected replay leaves its predicted value on screen until an
246
+ * unrelated frame or a reload: the client drops the layer when it hands the
247
+ * write over (it cannot cursor-confirm through this path) and has no other
248
+ * signal that the write died. Absent when the write carried no optimistic
249
+ * update, and safe to ignore — a sink that never calls it behaves as before.
250
+ */
251
+ onRejected?: () => void;
252
+ shardKey?: string;
253
+ }
254
+ /**
255
+ * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
256
+ * the client delegates offline write durability + at-least-once replay to this
257
+ * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
258
+ * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
259
+ * backed by the TanStack `OfflineExecutor`); the interface itself is
260
+ * dependency-free so `@lunora/client` stays TanStack-free.
261
+ */
262
+ interface OutboxSink {
263
+ /**
264
+ * Persist and schedule a write for replay. Rejects with an
265
+ * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
266
+ * the caller can surface back-pressure to the issuing mutation.
267
+ */
268
+ enqueue: (mutation: OutboxMutation) => Promise<void>;
269
+ /**
270
+ * Whether the sink still holds writes that have not replayed. The client
271
+ * consults this before sending a fresh mutation live, so a new write can
272
+ * never overtake an older one the sink is still holding (a write deferred
273
+ * because its identity isn't re-confirmed yet is held indefinitely, with
274
+ * nothing for the client's flush barrier to wait on).
275
+ *
276
+ * Optional: a sink that cannot answer never engages the ordering gate, so
277
+ * such a sink behaves exactly as before.
278
+ */
279
+ pending?: () => boolean;
280
+ }
281
+ /**
282
+ * One persisted query result in the durable read cache (Pillar 2). Keyed in the
283
+ * store by `shardKey + functionPath + argsKey`; the record carries everything
284
+ * needed to render offline on reload and to resume the live subscription.
285
+ */
286
+ interface CachedQuery {
287
+ /**
288
+ * Token-hash fingerprint of the bearer the value was cached under, when the
289
+ * entry was written by the identity the client currently advertises.
290
+ *
291
+ * The second half of the identity gate, and the half that makes the cache
292
+ * usable at all for a bearer-token app. `identity` settles on the resolved
293
+ * subject (`subj:<id>`) once the session resolves, but on the NEXT reload
294
+ * every adapter can only offer the stored token first — the subject arrives
295
+ * a round trip later, and offline it never arrives at all. Matching the
296
+ * credential the entry was written under is what lets the seed happen before
297
+ * (or without) that round trip. Absent when signed out, or when the value
298
+ * arrived over a socket authenticated as someone else.
299
+ */
300
+ credential?: string;
301
+ /**
302
+ * Issuing identity fingerprint (same shape the offline queue stamps). A
303
+ * cached value only hydrates when it matches the current identity, so a
304
+ * signed-out cache never leaks into a new session.
305
+ *
306
+ * `null` = cached by a client that had no subject to name — an app with no
307
+ * auth at all, or one the server answered "no session" to. It is never
308
+ * written while a session resolve is in flight, because a `null` written
309
+ * there belongs to a user the client was about to name, and every
310
+ * unidentified session would match it.
311
+ */
312
+ identity: string | null;
313
+ /**
314
+ * The `cursor` high-watermark this value reflects, replayed as `sinceSeq`
315
+ * on reconnect so the server can resume instead of re-snapshotting. Absent
316
+ * when the value predates CDC / no cursor was advertised.
317
+ */
318
+ serverCursor?: number;
319
+ /**
320
+ * The CDC `epoch` the `serverCursor` belongs to, replayed as `sinceEpoch`
321
+ * on reconnect so the server only resumes when the client is still on the
322
+ * same changelog timeline. Absent when no epoch was advertised.
323
+ */
324
+ serverEpoch?: string;
325
+ /** Wall-clock millis the value was written — drives LRU eviction. */
326
+ ts: number;
327
+ /** The full query result last seen from the server. */
328
+ value: unknown;
329
+ /**
330
+ * App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
331
+ * A cached value whose `version` doesn't match the current one is not hydrated —
332
+ * so a result of a now-changed shape from an older deploy can't render. Absent
333
+ * when no `persistenceVersion` is configured (no version gating).
334
+ */
335
+ version?: string;
336
+ }
337
+ /** A stored read-cache row: the {@link CachedQuery} plus the key it is stored under. */
338
+ interface StoredQuery extends CachedQuery {
339
+ key: string;
340
+ }
341
+ /**
342
+ * Durable store for the client read cache (Pillar 2): query results survive a
343
+ * reload so reads hydrate from disk and render immediately while the socket
344
+ * reconnects. Opt-in via {@link LunoraClientOptions.queryCache}; omit to keep
345
+ * reads in memory only (today's behaviour). Mirrors {@link PersistenceAdapter}'s
346
+ * shape over the same IndexedDB plumbing.
347
+ */
348
+ interface QueryCacheAdapter {
349
+ /** Drop every cached query (e.g. on logout / identity change). */
350
+ clear: () => Promise<void>;
351
+ /** Load every cached query — called once at startup to hydrate reads. */
352
+ load: () => Promise<StoredQuery[]>;
353
+ /** Upsert one cached query by key (called when a subscription value advances). */
354
+ put: (key: string, entry: CachedQuery) => Promise<void>;
355
+ /** Remove one cached query by key. */
356
+ remove: (key: string) => Promise<void>;
357
+ }
358
+ /**
359
+ * Resolves the WS `?token=` credential fresh at every (re)connect — the channel
360
+ * for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
361
+ * at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
362
+ * May return the token synchronously or as a Promise; returning `undefined`
363
+ * connects without a token. A thrown error / rejected Promise fails that
364
+ * connect attempt, and the client retries with its normal reconnect backoff.
365
+ */
366
+ type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
367
+ interface LunoraClientOptions {
368
+ /**
369
+ * Base path the worker mounts better-auth at, used by the client's
370
+ * `getCurrentUser()` to reach the `get-session` route. Defaults to
371
+ * `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
372
+ */
373
+ authBasePath?: string;
374
+ bookmarkStorage?: BookmarkStorage;
375
+ /**
376
+ * Stable per-client id backing the custom-mutator watermark. Sent on the
377
+ * `connect` envelope (so the server can scope this client's
378
+ * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
379
+ * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
380
+ * monotonic mutation id to form the idempotency key. The `@lunora/db` path
381
+ * persists a stable id alongside the outbox and passes it here; omit for the
382
+ * standalone client, which generates an ephemeral per-session id.
383
+ */
384
+ clientId?: string;
385
+ /**
386
+ * Default app context sent in the `connect` envelope right after each socket
387
+ * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
388
+ * as `event.context`. A per-shard context registered via
389
+ * `setConnectionContext` overrides this for that shard. Omit when no lifecycle
390
+ * hook needs connection context.
391
+ */
392
+ connectionContext?: Record<string, unknown>;
393
+ /**
394
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
395
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
396
+ * worker that never upgrades — the client force-closes the socket and routes
397
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
398
+ * of leaving the live channel silently stuck on the browser's much longer
399
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
400
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
401
+ */
402
+ connectTimeoutMs?: number;
403
+ /**
404
+ * When `true`, tabs sharing the same origin (and the same signed-in identity)
405
+ * coordinate via BroadcastChannel so only one tab — the "leader" — opens
406
+ * WebSocket connections to the server. Reduces simultaneous WS connections,
407
+ * bandwidth, and cross-tab state drift. Requires `BroadcastChannel`
408
+ * (browser-only); silently ignored otherwise. Defaults to `false`.
409
+ *
410
+ * **The channel is one-directional: leader → follower.** The leader
411
+ * broadcasts the values, errors, checkpoints and connection status of the
412
+ * subscriptions *it* holds; there is no frame with which a follower can ask
413
+ * the leader for anything.
414
+ *
415
+ * `subscribe` works on a follower and is how the relay delivers: the
416
+ * registration is what the leader's broadcast key is matched against, so a
417
+ * follower sees a value while the leader independently holds the same
418
+ * `(fn, args, shardKey)`.
419
+ *
420
+ * Nothing else is served, because the leader broadcasts nothing for it and a
421
+ * follower cannot ask. `subscribeShape` and `acquireConnectionContext` are
422
+ * inert on a follower — framework code (`@lunora/db`'s shape sync, every
423
+ * `usePresence` adapter) calls them from an effect the app cannot opt out
424
+ * of, so throwing would unwind the tab rather than degrade one feature.
425
+ * `whisper`, `whisperSubscribe`, `setConnectionContext` and `stream` are
426
+ * only ever called by app code, which can handle a failure, so those throw
427
+ * `NOT_IMPLEMENTED` rather than returning a handle that never fires. (The
428
+ * brief window every tab spends claiming leadership at startup is not a
429
+ * follower state: a lone tab self-promotes and its registered subscriptions
430
+ * are sent then.) HTTP surfaces — `query`, `mutation`, `action`, the offline
431
+ * queue's replay — are unaffected on every tab.
432
+ *
433
+ * So: enable this when your tabs run the SAME app views over plain
434
+ * `subscribe`, and leave it off if tabs can sit on different routes or you
435
+ * use shapes, whispers, streams, or connection context.
436
+ */
437
+ crossTabSync?: boolean;
438
+ fetch?: typeof fetch;
439
+ /**
440
+ * Interval (ms) between keepalive pings sent on each open subscription
441
+ * socket. The server answers them via the Durable Object's hibernation
442
+ * auto-response WITHOUT waking the DO, so an idle socket stays alive across
443
+ * hibernation without a billable wakeup. Defaults to 30000 (30s); set to
444
+ * `0` (or a negative value) to disable the heartbeat entirely.
445
+ */
446
+ heartbeatIntervalMs?: number;
447
+ /**
448
+ * When `true` and a `queryCache` is active, React's `useQuery` holds its
449
+ * TanStack query disabled until the durable cache has finished loading
450
+ * (`whenReady()`), so its first enabled render can seed the cached value
451
+ * instead of issuing an HTTP read that the cache would immediately
452
+ * overwrite. Defaults to `false`.
453
+ *
454
+ * It is ONLY React's `useQuery` that defers — the Vue, Svelte, Solid and
455
+ * Angular hooks subscribe at mount regardless of this flag, and so does
456
+ * React's own subscription registry. They do not need the gate: a
457
+ * subscription opened before the load completes is seeded by the load
458
+ * itself, so a cached value reaches the first subscriber either way.
459
+ *
460
+ * Requires `queryCache` to be set (not `false`); silently ignored otherwise.
461
+ */
462
+ hydrateOnStart?: boolean;
463
+ offlineQueue?: OfflineQueueOptions;
464
+ /**
465
+ * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
466
+ * path wires `createExecutorOutboxSink`), offline mutations are delegated to
467
+ * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
468
+ * is bypassed, so a db app has exactly one durable write path. Omit for the
469
+ * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
470
+ */
471
+ outbox?: OutboxSink;
472
+ /**
473
+ * Durable store for the offline mutation queue. Tri-state — an explicit
474
+ * {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
475
+ * in memory, lost on reload); omitted (the default) auto-probes a durable
476
+ * IndexedDB store when the `indexedDB` global is present (browsers), otherwise
477
+ * in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
478
+ * environments that can persist do. Pass `createAsyncStoragePersistence()` on
479
+ * React Native.
480
+ */
481
+ persistence?: false | PersistenceAdapter;
482
+ /**
483
+ * App/schema version stamped onto every persisted queued write and cached
484
+ * read. Bump it on a breaking change to a function signature or query shape:
485
+ * on the next boot, persisted writes / cached reads stamped with a different
486
+ * version are dropped (and purged) rather than replayed / hydrated against the
487
+ * new schema. Omit to disable version gating (records are never invalidated by
488
+ * version).
489
+ *
490
+ * **Adoption is itself an invalidation event:** records written before you set
491
+ * `persistenceVersion` carry no version, so the first boot after enabling it
492
+ * purges all currently-queued offline writes (and cached reads) as stale. Adopt
493
+ * it on a build where that clean slate is acceptable — typically the same
494
+ * breaking deploy you're protecting against — not purely speculatively.
495
+ */
496
+ persistenceVersion?: string;
497
+ /**
498
+ * HTTP polling fallback for live queries on a network that refuses WebSocket
499
+ * upgrades (a corporate proxy, a captive portal).
500
+ *
501
+ * One-shot `query`/`mutation`/`action` calls already ride HTTP POST, so such a
502
+ * network does not break them — it breaks reactivity, and only that. After a
503
+ * run of connect attempts that never reach `open`, the client re-runs each
504
+ * subscribed query over the batch-RPC endpoint on an interval and reports
505
+ * `"polling"` from the client's `connectionStatus()`.
506
+ *
507
+ * It is a degradation, not a second transport: shapes (`@lunora/db`
508
+ * collections), durable streams and whispers have no request/response form to
509
+ * re-run and stay unavailable until a socket opens. Freshness is bounded by
510
+ * the interval, and each tick costs a full re-run of every subscribed query.
511
+ *
512
+ * Defaults to polling every 5s after 3 consecutive failed opens. Set
513
+ * `intervalMs: 0` to disable it and keep the historical behaviour (live
514
+ * queries simply stop moving).
515
+ */
516
+ pollingFallback?: {
517
+ /** Consecutive connect attempts that must fail to reach `open` first. Default 3; `0` disables. */
518
+ afterFailedAttempts?: number;
519
+ /** Poll cadence in ms. Default 5000; `0` disables. */
520
+ intervalMs?: number;
521
+ };
522
+ /**
523
+ * Durable store for the read cache (Pillar 2). When active, query results
524
+ * are persisted as their subscriptions advance and hydrated on construction
525
+ * so a reload renders cached data before the socket reconnects, then resumes
526
+ * the live subscription from the persisted cursor. Tri-state — an explicit
527
+ * {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
528
+ * memory only); omitted (the default) auto-probes IndexedDB exactly like
529
+ * {@link LunoraClientOptions.persistence}.
530
+ */
531
+ queryCache?: QueryCacheAdapter | false;
532
+ reconnect?: ReconnectOptions;
533
+ url: string;
534
+ WebSocket?: typeof WebSocket;
535
+ /**
536
+ * Credential appended to the WebSocket URL as `?token=…`. The server matches
537
+ * it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
538
+ * `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
539
+ * what the studio supplies). Browsers can't set headers on the `WebSocket`
540
+ * constructor, so the query parameter is the only channel; it ends up in
541
+ * server logs and history, so prefer a short-lived rotating token in
542
+ * production over a static secret.
543
+ *
544
+ * Pass a {@link WsTokenProvider} function to resolve the token fresh at
545
+ * every (re)connect — the channel for short-lived credentials such as the
546
+ * ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
547
+ * provider re-mints on each reconnect, including the one following a `4001`
548
+ * token-expired drop, so a static master token never has to ride the URL.
549
+ */
550
+ wsToken?: string | WsTokenProvider;
551
+ wsUrl?: string;
552
+ }
553
+ /** Wire envelope sent on `POST /_lunora/rpc`. */
554
+ interface RpcEnvelope {
555
+ args?: Record<string, unknown>;
556
+ /**
557
+ * Stable per-client identifier (custom-mutator push path). Pairs with
558
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
559
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
560
+ */
561
+ clientId?: string;
562
+ functionPath: string;
563
+ /**
564
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
565
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
566
+ * `client.mutation` calls.
567
+ */
568
+ idempotencyKey?: string;
569
+ /**
570
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
571
+ * server-side per-client watermark: `id <= watermark` is a replay (skipped),
572
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
573
+ * batch so the client resends from `watermark + 1`. Absent on plain
574
+ * `client.mutation` calls.
575
+ */
576
+ mutationId?: number;
577
+ shardKey?: string;
578
+ }
579
+ /**
580
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
581
+ * watermarked custom-mutator push additionally carries `lastMutationId` — the
582
+ * highest per-client sequence the DO has applied — which the client uses to keep
583
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
584
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
585
+ * committed at — which gates the drop of a per-call optimistic layer.
586
+ */
587
+ type RpcResponseBody = {
588
+ error: {
589
+ code: string;
590
+ data?: unknown;
591
+ message: string;
592
+ };
593
+ } | {
594
+ commitCursor?: number;
595
+ lastMutationId?: number;
596
+ result: unknown;
597
+ };
598
+ /** Subscription protocol — client → server. */
599
+ interface ClientSubscribeMessage {
600
+ id: string;
601
+ /**
602
+ * `sinceSeq` is the persisted `cursor` high-watermark the client last saw
603
+ * for this shard (Pillar 1b resume). Present only when a durable
604
+ * {@link QueryCacheAdapter} restored a cached value with a cursor; the
605
+ * server replies with a lightweight `resume` frame instead of a full
606
+ * snapshot when nothing the query reads changed since it. Absent on a
607
+ * first-time subscribe.
608
+ */
609
+ query: {
610
+ args?: Record<string, unknown>;
611
+ functionPath?: string;
612
+ sinceEpoch?: string;
613
+ sinceSeq?: number;
614
+ table?: string;
615
+ };
616
+ type: "subscribe";
617
+ }
618
+ interface ClientUnsubscribeMessage {
619
+ id: string;
620
+ type: "unsubscribe";
621
+ }
622
+ /**
623
+ * One-shot control frame sent right after the socket opens. Registers the
624
+ * connection's app `context` (e.g. `{ roomId, sessionId }`) with the server and
625
+ * fires the `onConnect` lifecycle hooks; the same context is replayed to
626
+ * `onDisconnect` when the socket drops.
627
+ */
628
+ interface ClientConnectMessage {
629
+ /**
630
+ * Wire behaviours this client can handle that an older one cannot, so the
631
+ * server can use them without breaking clients that can't. Currently just
632
+ * `"pageDelta"`; see `shared/page-result.ts` for what it promises and why it
633
+ * must be announced rather than assumed. Omitting it is always safe.
634
+ */
635
+ caps?: ReadonlyArray<string>;
636
+ /**
637
+ * Stable per-client id (persisted alongside the outbox). Lets the server
638
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
639
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
640
+ * use custom mutators.
641
+ */
642
+ clientId?: string;
643
+ context?: Record<string, unknown>;
644
+ id: string;
645
+ type: "connect";
646
+ }
647
+ /**
648
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
649
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
650
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
651
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
652
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
653
+ * echoed as `shapeId` on every poke part.
654
+ */
655
+ interface ClientShapeSubscribeMessage {
656
+ id: string;
657
+ shape: {
658
+ args?: Record<string, unknown>;
659
+ name: string;
660
+ };
661
+ /**
662
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
663
+ * applied for this shape). When absent or below the server's retained floor
664
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
665
+ * delta.
666
+ */
667
+ sinceCheckpoint?: number;
668
+ /**
669
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
670
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
671
+ * the cursor is numerically in range.
672
+ */
673
+ sinceEpoch?: string;
674
+ type: "shape_subscribe";
675
+ }
676
+ /** Cancel a shape subscription started with the same `id`. */
677
+ interface ClientShapeUnsubscribeMessage {
678
+ id: string;
679
+ type: "shape_unsubscribe";
680
+ }
681
+ interface ClientAckMessage {
682
+ id: string;
683
+ type: "ack";
684
+ }
685
+ /**
686
+ * Start a streaming query. The id namespaces a fresh stream and is echoed on
687
+ * every {@link ServerChunkMessage} the server pushes back. Cancel a running
688
+ * stream by sending a {@link ClientUnsubscribeMessage} with the same id —
689
+ * subscription and stream id-spaces share the cancel channel; the prefix
690
+ * (`sub_*` vs `stream_*`) keeps the local registries searchable.
691
+ */
692
+ interface ClientStreamMessage {
693
+ /**
694
+ * Run generation the {@link ClientStreamMessage.sinceChunk} watermark
695
+ * belongs to: the `generation` stamp carried by the chunk frames this
696
+ * client already received, echoed back on a resume. The server refuses to
697
+ * splice a different run's tail onto the held prefix — a mismatch fails
698
+ * with `STREAM_INTERRUPTED` instead. Omitted on a first attach.
699
+ */
700
+ generation?: number;
701
+ id: string;
702
+ query: {
703
+ args?: Record<string, unknown>;
704
+ functionPath: string;
705
+ shardKey?: string;
706
+ };
707
+ /**
708
+ * Resume watermark: the highest chunk `seq` this client already received.
709
+ * Only meaningful for a stream the server declared `durable` — the run
710
+ * replays everything after it and then continues live, which is what turns
711
+ * a reconnect into a resume instead of a lost generation. Omitted on a
712
+ * first attach.
713
+ *
714
+ * Named `sinceChunk`, not `sinceSeq`, because a subscribe envelope already
715
+ * carries a `query.sinceSeq` meaning the CDC cursor.
716
+ */
717
+ sinceChunk?: number;
718
+ type: "stream";
719
+ }
720
+ /**
721
+ * Join or leave a whisper `topic` — an app-chosen ephemeral channel scoped to a
722
+ * shard. While joined, the client receives every {@link ServerWhisperMessage}
723
+ * other members broadcast to the topic.
724
+ */
725
+ interface ClientWhisperSubscribeMessage {
726
+ topic: string;
727
+ type: "whisper_subscribe" | "whisper_unsubscribe";
728
+ }
729
+ /**
730
+ * Broadcast ephemeral `data` to the topic's other members on the shard. The
731
+ * payload is relayed verbatim with no server-side persistence (no SQLite/CDC
732
+ * write) — for typing indicators, live cursors, presence pings. The sender does
733
+ * not receive its own whisper.
734
+ */
735
+ interface ClientWhisperMessage {
736
+ data?: unknown;
737
+ topic: string;
738
+ type: "whisper";
739
+ }
740
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
741
+ /** Subscription protocol — server → client. */
742
+ interface ServerDataMessage {
743
+ /**
744
+ * The `__cdc_log` high-watermark covered by this frame (Pillar 1b). The
745
+ * client persists it as the query's `serverCursor` and replays it as
746
+ * `sinceSeq` on the next reconnect. Absent on shards that never enabled CDC.
747
+ */
748
+ cursor?: number;
749
+ data?: unknown;
750
+ delta?: unknown;
751
+ /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
752
+ epoch?: string;
753
+ id: string;
754
+ /**
755
+ * The highest custom-mutator `mutationId` from this client the server has
756
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
757
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
758
+ * the matching optimistic overlay. Absent on shards without custom mutators.
759
+ */
760
+ lastMutationId?: number;
761
+ type: "data" | "delta";
762
+ }
763
+ /**
764
+ * Lightweight resume acknowledgement (Pillar 1b): the server determined that
765
+ * nothing the subscription reads changed since the client's `sinceSeq`, so it
766
+ * skips re-sending the snapshot. The client keeps its cached value and only
767
+ * advances `serverCursor` to `cursor`.
768
+ */
769
+ interface ServerResumeMessage {
770
+ cursor?: number;
771
+ /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
772
+ epoch?: string;
773
+ id: string;
774
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
775
+ lastMutationId?: number;
776
+ type: "resume";
777
+ }
778
+ /**
779
+ * Settled acknowledgement for a **list** subscription: a write touched one of
780
+ * the subscription's read tables but produced a byte-identical result, so the
781
+ * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
782
+ * client (one that announced a `clientId`, hence has a server-side
783
+ * `__client_watermark`) so its optimistic list overlay drops even when no data
784
+ * frame arrives. Plain `useQuery` subscribers never receive it, and an older
785
+ * client safely ignores the unknown frame.
786
+ */
787
+ interface ServerSettledMessage {
788
+ cursor?: number;
789
+ /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
790
+ epoch?: string;
791
+ id: string;
792
+ /**
793
+ * The highest custom-mutator `mutationId` from this client the server has
794
+ * now applied (the per-client `__client_watermark`). Forwarded to a
795
+ * collection's `onCheckpoint` so it can drop the overlay for the confirmed
796
+ * write whose result didn't change this list.
797
+ */
798
+ lastMutationId?: number;
799
+ type: "settled";
800
+ }
801
+ interface ServerErrorMessage {
802
+ error?: unknown;
803
+ id?: string;
804
+ message?: string;
805
+ type: "error";
806
+ }
807
+ interface ServerAckMessage {
808
+ id: string;
809
+ type: "ack";
810
+ }
811
+ interface ServerCompleteMessage {
812
+ id: string;
813
+ type: "complete";
814
+ }
815
+ /** One frame of a streaming query — `data` carries the user-yielded chunk. */
816
+ interface ServerChunkMessage {
817
+ data: unknown;
818
+ /**
819
+ * Generation stamp of the **durable** run this chunk belongs to. The client
820
+ * stores it beside {@link ServerChunkMessage.seq} and echoes it as
821
+ * {@link ClientStreamMessage.generation} on a resume, so the server can
822
+ * tell a genuine resume from an attempt to splice onto a different run
823
+ * under the same key. Absent on an ephemeral stream.
824
+ */
825
+ generation?: number;
826
+ id: string;
827
+ /**
828
+ * Monotonic position of this chunk within a **durable** run, starting at 1.
829
+ * The client stores the last one it saw and replays it as
830
+ * {@link ClientStreamMessage.sinceChunk} when the socket comes back. Absent
831
+ * on an ephemeral stream, which has nothing to resume from.
832
+ */
833
+ seq?: number;
834
+ type: "chunk";
835
+ }
836
+ /**
837
+ * An ephemeral whisper relayed from another member of `topic` on the same shard
838
+ * (AnyCable-style whispering). `data` is the sender's payload verbatim; `from`
839
+ * is the sender's verified user id when known (absent for an anonymous sender).
840
+ * Never persisted server-side.
841
+ */
842
+ interface ServerWhisperMessage {
843
+ data: unknown;
844
+ from?: string;
845
+ topic: string;
846
+ type: "whisper";
847
+ }
848
+ /**
849
+ * The user the shard authenticated this socket as, sent in reply to every
850
+ * `connect` frame. `subject` is the resolved user id, or `null` when the socket
851
+ * is anonymous. A cookie-session client reads it to learn who it is — a
852
+ * sign-out or another user's sign-in changes nothing else it can see.
853
+ */
854
+ interface ServerIdentityMessage {
855
+ subject: null | string;
856
+ type: "identity";
857
+ }
858
+ /**
859
+ * One row-level change in a shape's replication stream — the wire form of the
860
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
861
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
862
+ * the removed row by `key` alone. The client applies these to its local
863
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
864
+ * never had in this shape).
865
+ */
866
+ interface RowOp {
867
+ /** Row primary key (`_id`). */
868
+ key: string;
869
+ op: "delete" | "insert" | "update";
870
+ /** Logical table the row belongs to. */
871
+ table: string;
872
+ /** Post-image document for insert/update; absent on delete. */
873
+ value?: Record<string, unknown>;
874
+ }
875
+ /**
876
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
877
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
878
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
879
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
880
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
881
+ */
882
+ interface ServerPokeStartMessage {
883
+ /**
884
+ * Poke-level fallback base, stamped by single-part senders. Per-shape
885
+ * {@link ServerPokePartMessage.baseCheckpoint} takes precedence; this is what
886
+ * a part without its own base falls back to.
887
+ */
888
+ baseCheckpoint?: number;
889
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
890
+ epoch?: string;
891
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
892
+ pokeId: string;
893
+ type: "pokeStart";
894
+ }
895
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
896
+ interface ServerPokePartMessage {
897
+ /**
898
+ * The checkpoint this shape's view must be at for `rowsPatch` to splice on
899
+ * cleanly. Per shape, because every shape on a socket has its own
900
+ * delivered-through cursor. Absent when the server cannot name a base — the
901
+ * gap check is then disarmed for this part, never guessed at.
902
+ */
903
+ baseCheckpoint?: number;
904
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
905
+ lastMutationId?: number;
906
+ pokeId: string;
907
+ /**
908
+ * `true` when `rowsPatch` is the shape's COMPLETE membership, not a diff (a
909
+ * full seed or re-seed). The client MUST drop its current view for this shape
910
+ * before applying: a seed is inserts-only, so merging it leaves any row that
911
+ * left the shape while the client was disconnected on screen forever.
912
+ *
913
+ * Never inferred from an absent {@link ServerPokePartMessage.baseCheckpoint} —
914
+ * most live poke paths legitimately carry no base.
915
+ */
916
+ reset?: boolean;
917
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
918
+ rowsPatch: RowOp[];
919
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
920
+ shapeId: string;
921
+ type: "pokePart";
922
+ }
923
+ /**
924
+ * Closes a poke: the client commits the buffered parts atomically and advances
925
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
926
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
927
+ * the next reconnect.
928
+ */
929
+ interface ServerPokeEndMessage {
930
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
931
+ checkpoint?: number;
932
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
933
+ epoch?: string;
934
+ pokeId: string;
935
+ type: "pokeEnd";
936
+ }
937
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerIdentityMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
938
+ /**
939
+ * The authenticated user as exposed client-side, mirroring better-auth's
940
+ * `user` row (the `user` field of the `get-session` response). Kept minimal
941
+ * and structural — only `id` is guaranteed; the rest are the common better-auth
942
+ * fields, and the index signature carries any plugin-contributed extras.
943
+ */
944
+ interface User {
945
+ readonly createdAt?: NullableTimestamp;
946
+ readonly email?: null | string;
947
+ readonly emailVerified?: boolean | null;
948
+ readonly id: string;
949
+ readonly image?: null | string;
950
+ readonly name?: null | string;
951
+ readonly [key: string]: unknown;
952
+ readonly updatedAt?: NullableTimestamp;
953
+ }
954
+ /**
955
+ * Per-job retry policy carried on a {@link ScheduleRecord}. Mirrors
956
+ * `@lunora/scheduler`'s `RetryPolicy`; absent means the scheduler's defaults.
957
+ */
958
+ interface ScheduleRetryPolicy {
959
+ /** Backoff growth across attempts. Default `"exponential"`. */
960
+ backoff?: "exponential" | "linear";
961
+ /** Base delay in milliseconds for the first retry. Default `30_000`. */
962
+ baseMs?: number;
963
+ /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
964
+ maxAttempts?: number;
965
+ /** Optional ceiling clamping the computed backoff delay. */
966
+ maxMs?: number;
967
+ }
968
+ /**
969
+ * One pending scheduled function, as returned by the worker's
970
+ * `GET /_lunora/admin/scheduled` endpoint. The route is a byte-for-byte proxy of
971
+ * the SchedulerDO's own `/list`, so this mirrors `@lunora/scheduler`'s
972
+ * `ScheduleRecord` field-for-field — structurally, so the client carries no
973
+ * dependency on it. `packages/client/__tests__/structural-mirrors.test.ts` fails
974
+ * when the two drift.
975
+ */
976
+ interface ScheduleRecord {
977
+ args: Record<string, unknown>;
978
+ /**
979
+ * Dispatch attempts already made. Absent (treated as 0) until the first
980
+ * failure; on a dead-letter record it is the exhausted count (> the retry
981
+ * budget). Surfaced so the studio can show how hard a job tried before it
982
+ * was parked.
983
+ */
984
+ attempts?: number;
985
+ enqueuedAt: number;
986
+ /**
987
+ * The `ns:fn` path dispatched on fire. Absent when the job targets a durable
988
+ * workflow/agent instead — exactly one of `functionPath` /
989
+ * {@link ScheduleRecord.workflow} is set, so a view rendering a job's target
990
+ * must fall back to `workflow` rather than assuming a path.
991
+ */
992
+ functionPath?: string;
993
+ id: string;
994
+ /** Scheduler/workpool instance the job was enqueued through. Absent for the default instance. */
995
+ instanceName?: string;
996
+ /** Logical workpool the job is routed to (concurrency-gated), when any. */
997
+ pool?: string;
998
+ /** Per-job retry policy; absent means the scheduler's built-in defaults. */
999
+ retry?: ScheduleRetryPolicy;
1000
+ scheduledFor: number;
1001
+ shardKey?: string;
1002
+ /**
1003
+ * The workflow/agent class name (its `ctx.exports` key) a fresh durable instance is started from
1004
+ * on fire (the {@link ScheduleRecord.args} become its `params`). Set instead
1005
+ * of {@link ScheduleRecord.functionPath}.
1006
+ */
1007
+ workflow?: string;
1008
+ }
1009
+ /**
1010
+ * One workpool's live backlog, as returned by the worker's
1011
+ * `GET /_lunora/admin/scheduled/status` endpoint. Mirrors `@lunora/scheduler`'s
1012
+ * `SchedulerPoolStatus` structurally so the client carries no dependency on it.
1013
+ */
1014
+ interface SchedulerPoolStatus {
1015
+ /** Jobs currently dispatched-but-not-yet-completed (the held concurrency slots). */
1016
+ inFlight: number;
1017
+ /** The pool's concurrency cap. */
1018
+ maxConcurrency: number;
1019
+ /** The logical workpool name. */
1020
+ name: string;
1021
+ /** Pending jobs routed to this pool but not yet dispatched. */
1022
+ queued: number;
1023
+ }
1024
+ /**
1025
+ * The app-level scheduler backlog, as returned by the worker's
1026
+ * `GET /_lunora/admin/scheduled/status` endpoint. `pools` is the per-pool
1027
+ * breakdown; `backlog` and `inFlight` are the app-wide sums of `queued` and
1028
+ * `inFlight` across every pool — the headline numbers for the studio SLO
1029
+ * view. Mirrors `@lunora/scheduler`'s `SchedulerStatus` structurally.
1030
+ */
1031
+ interface SchedulerStatus {
1032
+ /** Sum of every pool's `queued` count — the total pending backlog. */
1033
+ backlog: number;
1034
+ /** Sum of every pool's `inFlight` count — the total held concurrency slots. */
1035
+ inFlight: number;
1036
+ /** Per-pool backlog breakdown. */
1037
+ pools: SchedulerPoolStatus[];
1038
+ }
1039
+ /**
1040
+ * One shard's request volume, as returned by the worker's
1041
+ * `POST /_lunora/admin/shard-traffic` endpoint. The cross-shard traffic feed
1042
+ * the studio's `hot_shard` advisor lint consumes: `requests` is the shard's
1043
+ * lifetime dispatch total, `shardKey` the DO id name (`""` for the root shard).
1044
+ */
1045
+ interface ShardTrafficEntry {
1046
+ requests: number;
1047
+ shardKey: string;
1048
+ }
1049
+ /**
1050
+ * The whole-shard-set traffic distribution returned by the worker's
1051
+ * `POST /_lunora/admin/shard-traffic` endpoint. `shards` is one entry per live
1052
+ * shard (a failed shard surfaces with `requests: 0`); `ok`/`failed` count the
1053
+ * shards that returned vs. errored. Shaped to feed the advisor's `hot_shard`
1054
+ * lint after the studio tags each entry with its sharded function `group`.
1055
+ */
1056
+ interface ShardTrafficResult {
1057
+ failed: number;
1058
+ ok: number;
1059
+ shards: ShardTrafficEntry[];
1060
+ }
1061
+ /**
1062
+ * One object in the storage bucket, as returned by the worker's
1063
+ * `GET /_lunora/admin/storage` endpoint. Mirrors `@lunora/storage`'s
1064
+ * `R2ObjectLike` structurally.
1065
+ */
1066
+ interface StorageObject {
1067
+ customMetadata?: Record<string, string>;
1068
+ etag: string;
1069
+ httpMetadata?: {
1070
+ contentType?: string;
1071
+ };
1072
+ key: string;
1073
+ size: number;
1074
+ /**
1075
+ * When the object was stored. R2 emits a `Date`, which JSON-serializes to an
1076
+ * ISO string over the wire; a mock may supply epoch ms — so consumers should
1077
+ * normalise via `new Date(uploaded)`. Absent if the backend didn't report it.
1078
+ */
1079
+ uploaded?: number | string;
1080
+ }
1081
+ /** One page of {@link StorageObject}s plus the cursor to fetch the next, if any. */
1082
+ interface StorageListPage {
1083
+ cursor?: string;
1084
+ objects: StorageObject[];
1085
+ }
1086
+ /**
1087
+ * One argument of a registered function, derived from its `v.*` validator by the
1088
+ * worker. A compact signature shape — enough to render a function's API without
1089
+ * the build-time codegen types.
1090
+ */
1091
+ interface FunctionArgumentDescriptor {
1092
+ /** Element validator kind for an `array` arg (one level), e.g. `string`. */
1093
+ element?: string;
1094
+ /** The (optional-unwrapped) validator kind, e.g. `string`, `id`, `object`. */
1095
+ kind: string;
1096
+ /** The argument name. */
1097
+ name: string;
1098
+ /** True when the arg is wrapped in `v.optional(...)`. */
1099
+ optional: boolean;
1100
+ /** Target table for an `id` arg (`v.id("table")`). */
1101
+ table?: string;
1102
+ }
1103
+ /**
1104
+ * One registered function, as returned by the worker's
1105
+ * `GET /_lunora/admin/functions` endpoint: its `<file>:<function>` path, which
1106
+ * client method (`query` / `mutation` / `action`) invokes it, and its argument
1107
+ * signature. `args` is absent on responses from an older worker.
1108
+ */
1109
+ interface FunctionDescriptor {
1110
+ args?: FunctionArgumentDescriptor[];
1111
+ kind: "action" | "mutation" | "query";
1112
+ path: string;
1113
+ }
1114
+ /** A `.global()` (D1-backed) table plus its row count, from `/_lunora/admin/global/tables`. */
1115
+ interface GlobalTableInfo {
1116
+ name: string;
1117
+ rowCount: number;
1118
+ }
1119
+ /** A window of rows from one global table, from `/_lunora/admin/global/table`. */
1120
+ interface GlobalTablePage {
1121
+ columns: string[];
1122
+ /** FK columns (local column → referenced table) for external tables with real `REFERENCES` constraints, from `PRAGMA foreign_key_list`. */
1123
+ refs?: Record<string, string>;
1124
+ rows: Record<string, unknown>[];
1125
+ total: number;
1126
+ }
1127
+ /**
1128
+ * One equality constraint a facet-value click adds to the global browser's view
1129
+ * (`column = value`). `value` is the raw stored scalar the facet returned, sent
1130
+ * as-is and bound server-side, so it never injects SQL.
1131
+ */
1132
+ interface GlobalFilterClause {
1133
+ column: string;
1134
+ value: unknown;
1135
+ }
1136
+ /** One distinct value of a faceted global column with its row count, from `/_lunora/admin/global/facet`. */
1137
+ interface GlobalFacetValue {
1138
+ count: number;
1139
+ value: unknown;
1140
+ }
1141
+ /** Per-column distinct-value summary for the global browser, from `/_lunora/admin/global/facet`. */
1142
+ interface GlobalFacetResult {
1143
+ truncated: boolean;
1144
+ values: GlobalFacetValue[];
1145
+ }
1146
+ /** A nullable timestamp field as better-auth serializes it: epoch-ms, ISO string, or null. */
1147
+ type NullableTimestamp = null | number | string;
1148
+ /** A workflow instance's lifecycle status. Mirrors Cloudflare's `InstanceStatus`. */
1149
+ type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
1150
+ /** The lifecycle mutations the status endpoint accepts. */
1151
+ type WorkflowInstanceAction = "pause" | "resume" | "terminate";
1152
+ /** One row of the workflow-instances list. */
1153
+ interface WorkflowInstanceSummary {
1154
+ createdOn?: string;
1155
+ endedOn?: string;
1156
+ id: string;
1157
+ startedOn?: string;
1158
+ status: WorkflowInstanceStatus;
1159
+ }
1160
+ /** One durable step of an instance's execution timeline. */
1161
+ interface WorkflowStepDetail {
1162
+ /** 1-based attempt count (`> 1` means the step retried). */
1163
+ attempts?: number;
1164
+ end?: string;
1165
+ error?: unknown;
1166
+ name: string;
1167
+ output?: unknown;
1168
+ start?: string;
1169
+ success?: boolean;
1170
+ /** `step` / `sleep` / `waitForEvent` / … (Cloudflare's step `type`). */
1171
+ type?: string;
1172
+ }
1173
+ /** A workflow instance's full detail: summary plus params/output/error and the step timeline. */
1174
+ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
1175
+ error?: unknown;
1176
+ output?: unknown;
1177
+ params?: unknown;
1178
+ steps: WorkflowStepDetail[];
1179
+ }
1180
+ /** A page of workflow instances. */
1181
+ interface WorkflowInstancePage {
1182
+ /**
1183
+ * Whether workflow inspection is configured on the worker (a Cloudflare
1184
+ * account id + API token). `false` when the admin proxy reports it can't
1185
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
1186
+ * caller render a "set credentials" state without a failed request.
1187
+ */
1188
+ configured?: boolean;
1189
+ instances: WorkflowInstanceSummary[];
1190
+ page: number;
1191
+ perPage: number;
1192
+ totalCount?: number;
1193
+ }
1194
+ type SubscriptionCallback = (data: unknown) => void;
1195
+ /**
1196
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
1197
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
1198
+ * echoed for this client. A `@lunora/db` collection feeds these into its
1199
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
1200
+ * rows have landed.
1201
+ */
1202
+ interface SyncWatermark {
1203
+ checkpoint?: number;
1204
+ mutationId?: number;
1205
+ /**
1206
+ * `true` only on the checkpoint a `data`/`delta` frame fires immediately
1207
+ * before that same frame's rowset callback — i.e. the one checkpoint whose
1208
+ * `mutationId` describes rows that are about to arrive.
1209
+ *
1210
+ * A `settled` frame (and a cross-tab `subscription-settled` relay) fires a
1211
+ * checkpoint with NO matching rowset, because the server suppressed a data
1212
+ * frame whose value didn't change. A consumer that stashes `mutationId` for
1213
+ * the next rowset to consume — `@lunora/db`'s `pendingFrameWatermark` — must
1214
+ * not stash those, or the value sits until some later unstamped frame eats
1215
+ * it and the checkpoint gate resolves at a stale watermark instead of
1216
+ * falling back to the RPC-ack compensator.
1217
+ */
1218
+ rowsFollow?: boolean;
1219
+ }
1220
+ /** A subscription-scoped error the server pushed for this subscription id. */
1221
+ interface SubscriptionError {
1222
+ /**
1223
+ * The coded reason, when the frame carried one. `LunoraErrorCodeInput`, not
1224
+ * `LunoraErrorCode`: the catalog codes autocomplete for a consumer branching
1225
+ * on this (the shard sends `BAD_SUBSCRIPTION_ARGS`, `TOO_MANY_SUBSCRIPTIONS`,
1226
+ * `SUBSCRIPTION_PERSIST_FAILED`, …; the client itself adds
1227
+ * `WIRE_DECODE_FAILED` for a frame `decodeWire` refuses), but this value is
1228
+ * read verbatim off the wire and nothing validates it against the catalog,
1229
+ * so narrowing it to `LunoraErrorCode` would be a lie a newer server tells.
1230
+ */
1231
+ code?: LunoraErrorCodeInput;
1232
+ message: string;
1233
+ }
1234
+ type SubscriptionErrorCallback = (error: SubscriptionError) => void;
1235
+ /**
1236
+ * One active per-call optimistic transform layered onto a subscription. The
1237
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
1238
+ * folded through every layer's `transform`, in order — so an incoming server
1239
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
1240
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
1241
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
1242
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
1243
+ * mutation's response, and stays `undefined` while the write is still queued/
1244
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
1245
+ */
1246
+ interface OptimisticLayer {
1247
+ /**
1248
+ * Where this write's RPC resolved in the process-wide acknowledgement order
1249
+ * (see `acknowledgementMark`); `undefined` until confirmed. A polled snapshot
1250
+ * carries no cursor, so this is what lets it drop the layer instead.
1251
+ */
1252
+ acknowledgedAt?: number;
1253
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
1254
+ commitCursor?: number;
1255
+ readonly id: symbol;
1256
+ readonly transform: (current: unknown) => unknown;
1257
+ }
1258
+ interface SubscriptionState {
1259
+ /** True once the server has acked the subscription on the current socket. */
1260
+ acked: boolean;
1261
+ readonly args: Record<string, unknown>;
1262
+ /**
1263
+ * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
1264
+ * time. Cached so the optimistic-update fan-out can compare against a
1265
+ * mutation's args key without re-serializing every subscription's args on
1266
+ * every mutation.
1267
+ */
1268
+ readonly argsKey: string;
1269
+ readonly callbacks: Set<SubscriptionCallback>;
1270
+ /**
1271
+ * Notified when a `settled` frame advances this subscription's watermark — a
1272
+ * write touched the subscription's tables but the result was byte-identical,
1273
+ * so the server suppressed the data frame. A `@lunora/db` list collection
1274
+ * uses this to drop the optimistic overlay for the confirmed write.
1275
+ *
1276
+ * A SET (not a single slot) because `SubscriptionState` is SHARED across
1277
+ * every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
1278
+ * collection may subscribe to a query a plain `useQuery` already opened, so
1279
+ * each subscriber registers its own callback (mirroring `callbacks` /
1280
+ * `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
1281
+ * `useQuery` consumers register nothing, leaving the set empty.
1282
+ */
1283
+ readonly checkpointCallbacks: Set<(watermark: SyncWatermark) => void>;
1284
+ /** Notified when the server rejects this subscription (e.g. admin auth). */
1285
+ readonly errorCallbacks: Set<SubscriptionErrorCallback>;
1286
+ readonly fn: FunctionReference;
1287
+ readonly id: string;
1288
+ /**
1289
+ * The highest custom-mutator `mutationId` from this client the server has
1290
+ * applied, captured from the last `settled` frame (the suppressed-list-frame
1291
+ * watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
1292
+ * Absent until a `settled` frame arrives.
1293
+ */
1294
+ lastMutationId?: number;
1295
+ /** Last known value, used to short-circuit `useQuery`-style consumers. */
1296
+ lastValue: unknown;
1297
+ /**
1298
+ * Active per-call optimistic layers, in application order (see
1299
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
1300
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
1301
+ * exactly and behaviour is identical to a plain server-value assignment.
1302
+ */
1303
+ optimisticLayers: OptimisticLayer[];
1304
+ /**
1305
+ * The authoritative server value the optimistic layers fold onto — the value
1306
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
1307
+ * are active; diverges only while a per-call optimistic write is pending. A
1308
+ * server frame updates this (and re-folds the layers); the durable read cache
1309
+ * persists this, never the optimistic overlay.
1310
+ */
1311
+ serverBase: unknown;
1312
+ /**
1313
+ * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
1314
+ * captured from the last `data`/`delta`/`resume` frame. Persisted to the
1315
+ * durable read cache and replayed as `sinceSeq` on reconnect so the server
1316
+ * can resume instead of re-snapshotting (Pillar 1b/2). Absent until the
1317
+ * first cursor-stamped frame arrives.
1318
+ */
1319
+ serverCursor?: number;
1320
+ /**
1321
+ * The CDC `epoch` token the `serverCursor` belongs to, captured from the
1322
+ * same frame. Replayed as `sinceEpoch` on reconnect so the server resumes
1323
+ * only when the client is still on the same changelog timeline — a reset or
1324
+ * recycled shard advertises a new epoch, forcing a fresh snapshot. Absent
1325
+ * until the first epoch-stamped frame arrives.
1326
+ */
1327
+ serverEpoch?: string;
1328
+ readonly shardKey?: string;
1329
+ /**
1330
+ * The wire-encoded form of `args`, computed once at `subscribe` time (so an
1331
+ * unsupported value fails loud at the call site, not inside a reconnect's
1332
+ * open handler). Sent on every `subscribe` frame — identical to `args` for
1333
+ * pure JSON, tagged tokens for `bigint`/`Date`/bytes/… (the shard
1334
+ * `decodeWire`s them at its subscribe entry point).
1335
+ *
1336
+ * A SNAPSHOT, not a view: `args` is the caller's own object, retained by
1337
+ * reference and never copied, so a caller that mutates it after subscribing
1338
+ * would otherwise poison every later resubscribe. `encodeWire` rebuilds
1339
+ * every container, so this copy is immune to that.
1340
+ */
1341
+ readonly wireArgs: Record<string, unknown>;
1342
+ }
1343
+ /**
1344
+ * Active subscription registry. The client keys subscriptions by
1345
+ * `(functionPath, stableWireKey(args), shardKey)` so duplicate calls share a
1346
+ * single server-side registration. Args are stably encoded (keys sorted at every
1347
+ * depth) so two structurally-equal arg records constructed with a different key
1348
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
1349
+ * duplicate subscription. Encoding the args' **wire form** keeps the key
1350
+ * byte-identical for pure-JSON args while giving wire-typed args (`bigint`,
1351
+ * `Date`, bytes, …) distinct stable tokens instead of a throw.
1352
+ */
1353
+ declare class SubscriptionRegistry {
1354
+ static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
1355
+ /**
1356
+ * The registry key of an already-registered state, from its cached
1357
+ * {@link SubscriptionState.argsKey}. Re-deriving it from `state.args` would
1358
+ * re-read the caller's own (mutable) args object, so a caller that mutated
1359
+ * its args after subscribing would compute a DIFFERENT key on unsubscribe
1360
+ * and leak the registration forever.
1361
+ */
1362
+ static keyOf(state: SubscriptionState): string;
1363
+ private readonly byKey;
1364
+ private readonly byId;
1365
+ get(key: string): SubscriptionState | undefined;
1366
+ getById(id: string): SubscriptionState | undefined;
1367
+ add(state: SubscriptionState): void;
1368
+ remove(state: SubscriptionState): void;
1369
+ all(): SubscriptionState[];
1370
+ /**
1371
+ * Drop every registration. Terminal — used by `LunoraClient.close()`, whose
1372
+ * whole point is to release the callback closures each {@link SubscriptionState}
1373
+ * holds (`callbacks`, `errorCallbacks`, `checkpointCallbacks` — React state
1374
+ * setters and `@lunora/db` collection closures), which otherwise outlive the
1375
+ * closed client for as long as the client object is reachable.
1376
+ */
1377
+ clear(): void;
1378
+ }
1379
+ /**
1380
+ * An `Error` carrying the server's machine-readable `code` and (for a
1381
+ * `LunoraError`) structured `data`, plus an optional actionable `hint` (Markdown)
1382
+ * and `docsUrl` resolved from the central error catalog. The client's public
1383
+ * error contract for RPC/batch failures — a UI can render `hint`/`docsUrl` to
1384
+ * tell the user how to fix the error. The `(string & {})` arm keeps
1385
+ * forward-compat/unknown server codes assignable without losing autocomplete on
1386
+ * the known `LunoraErrorCode` union.
1387
+ */
1388
+ type LunoraClientError = Error & {
1389
+ code?: LunoraErrorCode | (string & {});
1390
+ data?: unknown;
1391
+ docsUrl?: string;
1392
+ hint?: string | string[];
1393
+ };
1394
+ /** One demuxed result slot of a `LunoraClient.batch` call. */
1395
+ type BatchSlot = {
1396
+ error: LunoraClientError;
1397
+ ok: false;
1398
+ } | {
1399
+ ok: true;
1400
+ value: unknown;
1401
+ };
1402
+ /**
1403
+ * Reactive key-value store for local-only client state.
1404
+ *
1405
+ * Unlike a server {@link SubscriptionState} (which tracks a live WS connection,
1406
+ * an `acked` flag, `serverBase`, optimistic layers, and the full subscription
1407
+ * machinery), a `ClientQueryRef` is purely local — no server round-trip, no
1408
+ * WebSocket, no persistence. It exists so framework adapters can offer a
1409
+ * `useClientQuery` hook whose values survive component remounts and are shared
1410
+ * across every consumer of the same ref, with none of the ceremony or coupling
1411
+ * of a dedicated context provider.
1412
+ *
1413
+ * The store lives inside `LunoraClient` (a private field) and is surfaced through
1414
+ * `client.getClientQuery(ref)` / `setClientQuery(ref, value)` /
1415
+ * `subscribeClientQuery(ref, callback)`.
1416
+ */
1417
+ /** Opaque handle for a typed client-local query slot. */
1418
+ interface ClientQueryRef<T = unknown> {
1419
+ /** Default value when no value has been set explicitly. */
1420
+ readonly defaultValue: T;
1421
+ /** Stable identity for the slot. Must be unique within a client instance. */
1422
+ readonly key: string;
1423
+ }
1424
+ /**
1425
+ * Create a typed {@link ClientQueryRef}. Call once per slot at module scope
1426
+ * (or inside a component module) — the ref object is the stable identity.
1427
+ * @example
1428
+ * ```ts
1429
+ * // lunora/client-queries.ts
1430
+ * import { createClientQuery } from "@lunora/client";
1431
+ *
1432
+ * export const sidebarOpen = createClientQuery("sidebarOpen", true);
1433
+ * export const selectedMessageId = createClientQuery("selectedMessageId", undefined as string | undefined);
1434
+ * ```
1435
+ */
1436
+ declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
1437
+ type WSState = "idle" | "connecting" | "open" | "closed";
1438
+ /**
1439
+ * Aggregate live-socket health across every shard connection, for a UI status
1440
+ * indicator. `idle` = no socket opened yet; `connecting` = at least one socket
1441
+ * is (re)connecting and none is open; `connected` = at least one socket is open;
1442
+ * `polling` = no socket would open, so live queries are being refreshed over HTTP
1443
+ * instead (see `polling-fallback.ts` — live but slower, and shapes
1444
+ * / streams / whispers are dark; mutations go over HTTP as they do while
1445
+ * connected); `offline` = sockets exist but all are down (between reconnect
1446
+ * attempts), and a poll could not reach the origin either.
1447
+ *
1448
+ * `polling` outranks `connecting`: while the fallback is running a reconnect is
1449
+ * still armed in the background, and reporting that attempt would flicker the
1450
+ * indicator between two states while data is in fact arriving on the slow path.
1451
+ */
1452
+ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline" | "polling";
1453
+ /**
1454
+ * Read/write handle over the client's live query cache, handed to a mutation's
1455
+ * `withOptimisticUpdate` callback so a single mutation can optimistically patch
1456
+ * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
1457
+ *
1458
+ * `getQuery` reads the current value (server value or any still-pending
1459
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
1460
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
1461
+ * settles together — confirmed on the mutation's commit cursor, or rolled back
1462
+ * on failure — the same per-subscription layer machinery the single-query
1463
+ * per-call `optimistic` transform uses, generalized to N queries.
1464
+ */
1465
+ interface OptimisticLocalStore {
1466
+ /**
1467
+ * Every loaded subscription on `function_`, regardless of args, paired with
1468
+ * the args it was subscribed under. Mirrors Convex's `getAllQueries` — handy
1469
+ * when a write must patch every variant of a list query (all channels,
1470
+ * all filters) without enumerating their args up front.
1471
+ */
1472
+ getAllQueries: <F extends FunctionReference>(function_: F) => {
1473
+ args: ArgsOf<F>;
1474
+ value: ReturnOf<F> | undefined;
1475
+ }[];
1476
+ /**
1477
+ * Current cached value for the subscribed `(function_, args)` query, or
1478
+ * `undefined` when nothing is subscribed/loaded for it. Reflects any
1479
+ * optimistic override already written in this batch.
1480
+ */
1481
+ getQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>) => ReturnOf<F> | undefined;
1482
+ /**
1483
+ * Write an optimistic override for the subscribed `(function_, args)`
1484
+ * query. A no-op (returns without effect) when no subscription matches —
1485
+ * mirroring Convex, where you only patch queries the page is watching.
1486
+ */
1487
+ setQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, value: ReturnOf<F> | undefined) => void;
1488
+ }
1489
+ /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
1490
+ type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
1491
+ /**
1492
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
1493
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
1494
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
1495
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
1496
+ * query — not merged) and drops gaplessly on the mutation's commit cursor, like
1497
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
1498
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
1499
+ * so the caller settles the whole batch when the mutation does.
1500
+ */
1501
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined) => {
1502
+ confirms: ((commitCursor: number | undefined) => void)[];
1503
+ rollbacks: (() => void)[];
1504
+ store: OptimisticLocalStore;
1505
+ };
1506
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
1507
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
1508
+ declare const DEFAULT_MAX_BUFFER = 1024;
1509
+ interface StreamHandle<T = unknown> {
1510
+ /** Mark the stream complete (no more chunks); resolves any pending consumer to `done:true`. */
1511
+ readonly complete: () => void;
1512
+ /** Surface an error to any pending consumer; subsequent pushes are dropped. */
1513
+ readonly fail: (error: Error) => void;
1514
+ /**
1515
+ * Push one chunk. Silent no-op once the stream is `complete`, `fail`-ed,
1516
+ * or `cancel`-ed. When the buffer is already at `maxBuffer`, the stream
1517
+ * is failed with a `STREAM_BACKPRESSURE` error and the push is dropped —
1518
+ * the producer never sees a thrown exception.
1519
+ */
1520
+ readonly push: (value: T) => void;
1521
+ }
1522
+ interface StreamIterable<T> extends AsyncIterable<T> {
1523
+ /** Cancel the stream from the consumer side: closes the iterator and notifies the registered canceller. */
1524
+ cancel: () => void;
1525
+ }
1526
+ /**
1527
+ * Build a stream handle paired with an async-iterable. The handle is the
1528
+ * server-driven side (the WS dispatcher pushes chunks / completes / errors);
1529
+ * the iterable is what the user awaits. `onCancel` is invoked exactly once
1530
+ * when the consumer calls `.cancel()` (or `.return()`) so the client can
1531
+ * send a `{type:"unsubscribe"}` frame to the server.
1532
+ */
1533
+ declare const createStream: <T>(options: {
1534
+ maxBuffer?: number;
1535
+ onCancel: () => void;
1536
+ /** Invoked once the stream is failed, so a producer can stop feeding it. */
1537
+ onFail?: () => void;
1538
+ }) => {
1539
+ handle: StreamHandle<T>;
1540
+ iterable: StreamIterable<T>;
1541
+ };
1542
+ /** One shard's socket + watermark state in a {@link LunoraClient.debug} snapshot. */
1543
+ interface ClientDebugShard {
1544
+ /**
1545
+ * Highest custom-mutator watermark the server has echoed for this client on this
1546
+ * shard. A write whose `clientSeq` is above this has been sent but not confirmed
1547
+ * — the first thing to check when an optimistic overlay won't clear.
1548
+ */
1549
+ confirmedMutationWatermark: number;
1550
+ /** Whether a `WebSocket` object currently exists (distinct from it being open). */
1551
+ hasSocket: boolean;
1552
+ /** `undefined` for the default (unsharded) connection. */
1553
+ shardKey: string | undefined;
1554
+ /** Whether this shard's socket has ever completed a handshake — gates offline queueing. */
1555
+ wasEverConnected: boolean;
1556
+ wsState: WSState;
1557
+ }
1558
+ /** One live query or shape subscription in a {@link LunoraClient.debug} snapshot. */
1559
+ interface ClientDebugSubscription {
1560
+ /** Whether the server has acknowledged the subscription on the current socket. */
1561
+ acked: boolean;
1562
+ /** `namespace:fn` for a query, `shape:<name>` for a replication shape. */
1563
+ functionPath: string;
1564
+ id: string;
1565
+ kind: "query" | "shape";
1566
+ /** Highest custom-mutator watermark echoed on THIS subscription (absent until a `settled`/poke frame arrives). */
1567
+ lastMutationId?: number;
1568
+ /** Per-call optimistic layers still folded onto this subscription's value — a non-zero count with no pending write is a leak. */
1569
+ pendingOptimisticLayers: number;
1570
+ /** Replicated rowset size (shapes only). */
1571
+ rowCount?: number;
1572
+ /** The `__cdc_log` cursor the current value reflects. */
1573
+ serverCursor?: number;
1574
+ shardKey: string | undefined;
1575
+ /** How many callers share this subscription (subscriptions are deduped by `(fn, args, shard)`). */
1576
+ subscriberCount: number;
1577
+ }
1578
+ /**
1579
+ * Everything the sync engine believes at one instant — see
1580
+ * {@link LunoraClient.debug} for why this exists.
1581
+ */
1582
+ interface ClientDebugSnapshot {
1583
+ /** The watermark key the server's custom-mutator protocol advances per `clientSeq`. */
1584
+ clientId: string;
1585
+ closed: boolean;
1586
+ connectionStatus: ConnectionStatus;
1587
+ /** Writes waiting in the built-in offline queue (not the `@lunora/db` outbox). */
1588
+ pendingWrites: number;
1589
+ shards: ClientDebugShard[];
1590
+ subscriptions: ClientDebugSubscription[];
1591
+ }
1592
+ /**
1593
+ * Terminal verdict for a mutation that passed through the offline queue,
1594
+ * delivered to {@link LunoraClient.onMutationSettled}.
1595
+ *
1596
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1597
+ * original caller can await, and which no longer exists after a reload — this
1598
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1599
+ * including writes restored from durable storage in a later session. It is the
1600
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1601
+ * instead of silently dropping a rolled-back optimistic row.
1602
+ *
1603
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1604
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1605
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1606
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1607
+ * "the caller already saw this" apart from "nothing else will report this".
1608
+ */
1609
+ interface MutationSettledEvent {
1610
+ /** The write's args, so a listener can describe or re-offer the change. */
1611
+ readonly args: Record<string, unknown>;
1612
+ /**
1613
+ * Server/queue error code on `rejected` (e.g. `CONFLICT`), when present —
1614
+ * or `WIRE_DECODE_FAILED` on a `committed` write whose result could not be
1615
+ * decoded.
1616
+ */
1617
+ readonly code?: string;
1618
+ /** The rejection error on `status: "rejected"`, or the decode error of a `committed` write whose result could not be read. */
1619
+ readonly error?: unknown;
1620
+ /** The `<file>:<function>` reference of the mutation. */
1621
+ readonly functionPath: string;
1622
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1623
+ readonly hadAwaiter: boolean;
1624
+ /** The write's stable id (idempotency key / queue id). */
1625
+ readonly id: string;
1626
+ /** Shard the write targeted, if any. */
1627
+ readonly shardKey?: string;
1628
+ /** Terminal outcome. */
1629
+ readonly status: "committed" | "rejected";
1630
+ }
1631
+ /**
1632
+ * Per-call options for {@link LunoraClient.action} — just `shardKey`, since an
1633
+ * action is not a write and carries none of the optimistic machinery. Exported
1634
+ * (at the end of this file) for the same reason {@link MutationCallOptions} is:
1635
+ * so the framework adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`,
1636
+ * `/angular`) type their `call(args, options?)` against one canonical
1637
+ * definition. Add an option here and every adapter forwards it; re-declare it
1638
+ * per adapter and they silently cannot.
1639
+ */
1640
+ interface ActionCallOptions {
1641
+ /** Route the call to a specific shard. */
1642
+ shardKey?: string;
1643
+ }
1644
+ /**
1645
+ * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
1646
+ * machinery plus `shardKey`. Exported (at the end of this file) so the framework
1647
+ * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
1648
+ * `mutate(args, options?)` against one canonical definition instead of
1649
+ * re-declaring it.
1650
+ */
1651
+ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
1652
+ /**
1653
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
1654
+ * durable outbox replay a committed-but-unacked write under its *original* key
1655
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
1656
+ * each then gets a fresh key.
1657
+ */
1658
+ mutationId?: string;
1659
+ /**
1660
+ * Single-query shortcut: the transform is layered onto the subscription
1661
+ * registered under **this write's own** `(functionPath, args, shardKey)`, and
1662
+ * nothing else. A client cannot know which queries a write affects, so the
1663
+ * targeting rule is "same reference, same args" — which makes this the
1664
+ * shorthand for a query and a mutation that share a path (a counter, a
1665
+ * document by id), and a silent no-op for anything else.
1666
+ *
1667
+ * **The general case — a `messages:send` mutation patching a `messages:list`
1668
+ * query — is {@link MutationCallOptions.optimisticUpdate}**, whose store names
1669
+ * its targets. Reach for that whenever the mutation and the query are
1670
+ * different functions, which is nearly always.
1671
+ *
1672
+ * The transform must be pure: it re-runs on every server frame while the write
1673
+ * is pending, so derive from `current` rather than closing over a value.
1674
+ */
1675
+ optimistic?: (current: TCurrent | undefined) => TValue;
1676
+ /**
1677
+ * Convex-parity multi-query optimistic update. Receives an
1678
+ * `OptimisticLocalStore` over the live subscription cache plus the
1679
+ * mutation's args, so one mutation can patch many subscribed queries at
1680
+ * once; every write is rolled back atomically if the mutation fails.
1681
+ */
1682
+ optimisticUpdate?: OptimisticUpdate<TArgs>;
1683
+ /**
1684
+ * Sync predicate evaluated just before the offline queue replays this
1685
+ * write on reconnect. When it returns `false` the mutation is dropped
1686
+ * instead of replayed — use it to guard against replaying writes whose
1687
+ * assumptions are no longer valid (e.g. the document it referred to was
1688
+ * deleted by another client while this tab was offline).
1689
+ */
1690
+ precondition?: () => boolean;
1691
+ /**
1692
+ * Pin this call's `.dropStalePatches()` baseline instead of sampling the
1693
+ * client's current view — the durable-replay counterpart to
1694
+ * {@link MutationCallOptions.mutationId}. See
1695
+ * {@link import("./types").OutboxMutation.baselineSeq} for why a replay must
1696
+ * hand back the cursor it was composed at.
1697
+ *
1698
+ * Tri-state, the same shape (and for the same reason) as the `identity` stamp
1699
+ * a queued write carries: `undefined` samples the current cursor, `null` pins
1700
+ * "composed with no baseline", and a number pins that cursor. A plain
1701
+ * `number | undefined` could not express the middle case, so every write
1702
+ * queued without a live subscription would have re-derived.
1703
+ *
1704
+ * Omit for normal calls.
1705
+ */
1706
+ replayBaseline?: null | number;
1707
+ /**
1708
+ * The credential a durable replay was judged under — the one
1709
+ * {@link LunoraClient.replayIdentityVerdict} returns with a `"match"`. The
1710
+ * request is sent with exactly the bearer that verdict was judged against,
1711
+ * never the live one: a `setAuthToken` landing between the verdict and the
1712
+ * send would otherwise put a write judged as one user's on the next user's
1713
+ * bearer, and a bearer request carries no subject for the worker to refuse it
1714
+ * on. Under a cookie session the request also names the user the write was
1715
+ * queued by, and the worker refuses it with `IDENTITY_MISMATCH` when the
1716
+ * cookie now belongs to someone else — the check the client cannot make,
1717
+ * because it cannot see the cookie.
1718
+ *
1719
+ * Single use, and only valid on the client that issued it. A replay refused
1720
+ * for its credential (`UNAUTHENTICATED` / `TOKEN_EXPIRED`) fires {@link LunoraClient.onTokenExpired} once per credential, and only
1721
+ * while that credential is still the current one. Omit for normal calls,
1722
+ * which always send the live token.
1723
+ */
1724
+ replayCredential?: ReplayCredential;
1725
+ shardKey?: string;
1726
+ }
1727
+ declare const replayCredentialBrand: unique symbol;
1728
+ /**
1729
+ * Opaque proof that a durable write was judged replayable, bound to the
1730
+ * credential it was judged under. Issued only by
1731
+ * {@link LunoraClient.replayIdentityVerdict}; pass it to
1732
+ * {@link MutationCallOptions.replayCredential}.
1733
+ */
1734
+ interface ReplayCredential {
1735
+ readonly [replayCredentialBrand]: true;
1736
+ }
1737
+ /** {@link LunoraClient.replayIdentityVerdict}'s answer: a `"match"` carries the credential the replay must be sent with. */
1738
+ type ReplayIdentityVerdict = {
1739
+ credential: ReplayCredential;
1740
+ verdict: "match";
1741
+ } | {
1742
+ verdict: "mismatch";
1743
+ } | {
1744
+ verdict: "unknown";
1745
+ };
1746
+ /**
1747
+ * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
1748
+ * a single multiplexed WebSocket.
1749
+ *
1750
+ * Reconnect, offline queueing, and optimistic updates are all handled here;
1751
+ * see the package README for the wire protocol.
1752
+ */
1753
+ declare class LunoraClient {
1754
+ /** Hard cap on concurrently-buffered pokes — a backstop that reclaims buffers abandoned by a mid-poke disconnect (no `pokeEnd`). Far above any real concurrent-in-flight count. */
1755
+ private static readonly MAX_POKE_BUFFERS;
1756
+ /**
1757
+ * Create a typed {@link ClientQueryRef}. Convenience wrapper around
1758
+ * {@link createClientQuery} so you don't need a separate import.
1759
+ * @example
1760
+ * ```ts
1761
+ * const sidebarOpen = LunoraClient.createClientQuery("sidebarOpen", true);
1762
+ * ```
1763
+ */
1764
+ static createClientQuery<T>(key: string, defaultValue: T): ClientQueryRef<T>;
1765
+ readonly url: string;
1766
+ readonly wsUrl: string;
1767
+ /** The admin modules reach the client only through this host. The arrows read `this` when called, so spies and overrides installed after construction still take effect. */
1768
+ private readonly adminHost;
1769
+ /** Local reactive store for {@link ClientQueryRef} values — no server round-trip. Private; reach it via `getClientQuery` / `setClientQuery` / `subscribeClientQuery`. */
1770
+ private readonly clientQueryStore;
1771
+ private wsToken;
1772
+ /** Bumped on every `setWsToken` change: a provider request started under an older value is superseded. */
1773
+ private wsTokenGeneration;
1774
+ /** Better-auth base path (trailing slash stripped) for the `get-session` lookup. */
1775
+ private readonly authBasePath;
1776
+ private readonly fetchImpl;
1777
+ private readonly WebSocketImpl;
1778
+ private readonly bookmark;
1779
+ private readonly reconnectOptions;
1780
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
1781
+ private readonly connectTimeoutMs;
1782
+ /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
1783
+ private readonly heartbeatIntervalMs;
1784
+ /** The socket-lifecycle inputs, built once after the constructor assigns them (none is reassigned later). */
1785
+ private readonly managedSocketOptions;
1786
+ /** Failed-open run that engages the HTTP polling fallback; `0` disables it. */
1787
+ private readonly pollingFallbackAfterFailedAttempts;
1788
+ /** Polling-fallback cadence (ms); `0` disables it. See {@link LunoraClientOptions.pollingFallback}. */
1789
+ private readonly pollingFallbackIntervalMs;
1790
+ private readonly offlineQueue;
1791
+ /**
1792
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
1793
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
1794
+ * is bypassed, so a db app has exactly one durable write path.
1795
+ */
1796
+ private readonly outbox;
1797
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
1798
+ private readonly clientId;
1799
+ /**
1800
+ * Highest CDC cursor this client has seen a write commit at, per shard key
1801
+ * (`""` for the default shard) — the read-your-writes bookmark sent as
1802
+ * `x-lunora-min-seq`.
1803
+ *
1804
+ * In memory only, and deliberately: it exists to keep THIS session's reads
1805
+ * behind THIS session's writes. Persisting it across reloads would pin a
1806
+ * fresh page to a cursor it has no reason to require and force needless
1807
+ * fallbacks to the owner.
1808
+ */
1809
+ private readonly shardCursors;
1810
+ /**
1811
+ * The server's own name for the default shard (`defaultShardKey`), learned
1812
+ * from the first response to a call that named no shard. `undefined` until
1813
+ * then, which is why `cursorKeyFor` falls back to a placeholder entry that
1814
+ * `learnDefaultShardKey` folds in once the name arrives.
1815
+ */
1816
+ private defaultShardKey;
1817
+ /**
1818
+ * `true` when the constructor's hydration microtask has finished loading the
1819
+ * durable read cache (Pillar 2) into `hydratedQueryCache`. Signals that
1820
+ * the cache is ready for synchronous `peekHydratedQuery` reads.
1821
+ */
1822
+ private readyResolved;
1823
+ /** Resolvers for `whenReady()` — called once hydration completes. */
1824
+ private readyResolve;
1825
+ /**
1826
+ * Promise that resolves once the durable read cache has been loaded. When
1827
+ * `hydrateOnStart` is not set or no query cache is configured, resolves
1828
+ * immediately (the constructor creates an already-resolved promise).
1829
+ */
1830
+ private readonly readyPromise;
1831
+ /**
1832
+ * Highest custom-mutator watermark the server has echoed for this client,
1833
+ * nested by identity fingerprint (`identityFingerprint() ?? ""`) then shard
1834
+ * bucket (`shardKey ?? ""`) — the DO tracks one `__client_watermark` per
1835
+ * `(identity, clientId)` pair, not per bucket alone, so a bucket-only key
1836
+ * would let a user switch claim the previous identity's sequence and wedge
1837
+ * every push on `OUT_OF_ORDER` until a reload (plan 316). A nested map
1838
+ * (rather than a single map composite-keyed by a joined string) is what a
1839
+ * previous fix here tried and got wrong: an identity fingerprint or a
1840
+ * `shardKey` is an arbitrary string, so any string delimiter — even one as
1841
+ * exotic as U+FFFD — can appear in one operand and collide with the other
1842
+ * (a fingerprint of `a<SEP>x` + bucket `y` joins to the same string as
1843
+ * a fingerprint of `a` + bucket `x<SEP>y`), mixing two identities' watermarks.
1844
+ * The nested map has no join step, so there is no delimiter to collide.
1845
+ * `callMutator` bumps it from every ack; the `@lunora/db` mutator runtime
1846
+ * seeds its `clientSeq` generator from it so a reload (which resets the
1847
+ * in-memory counter) never reissues a stale sequence the server would
1848
+ * silently swallow as a replay.
1849
+ */
1850
+ private readonly clientWatermarks;
1851
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1852
+ private outboxMutationCounter;
1853
+ private readonly onPersistenceError;
1854
+ private readonly persistence;
1855
+ /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
1856
+ private readonly persistenceVersion;
1857
+ /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
1858
+ private outboxLeaderRelease;
1859
+ /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
1860
+ private readonly queryCache;
1861
+ /**
1862
+ * Values restored from the `queryCache` at construction, keyed by the
1863
+ * read-cache key, awaiting the `subscribe()` that will consume them. A
1864
+ * key is consumed (deleted) the first time its subscription is created, so
1865
+ * the cache only ever seeds the initial value — live frames take over after.
1866
+ */
1867
+ private readonly hydratedQueryCache;
1868
+ /**
1869
+ * The read-cache entries currently ON DISPLAY in a live subscription, by the
1870
+ * same key — every entry consumed out of `hydratedQueryCache` and not yet
1871
+ * overwritten by a server frame.
1872
+ *
1873
+ * Gating the cache at read time only protects a value not handed over YET.
1874
+ * A credential can change while a cached value is already on screen (the
1875
+ * account-switch shape: an established subject, a token it was never checked
1876
+ * against), and the fingerprint does not move with it — so nothing else
1877
+ * would notice. `revokeCacheSeededValues` takes those values back off
1878
+ * screen and returns the entries to `hydratedQueryCache`, where the ordinary
1879
+ * identity gate decides whether they may ever be shown again.
1880
+ */
1881
+ private readonly cacheSeededQueries;
1882
+ /**
1883
+ * Coalesced read-cache writes: the latest value per key, flushed to
1884
+ * the `queryCache` on a short debounce so a burst of deltas persists once.
1885
+ */
1886
+ private readonly pendingCacheWrites;
1887
+ private cacheFlushTimer;
1888
+ private readonly subscriptions;
1889
+ /**
1890
+ * Cross-tab coordinator; created only when `crossTabSync: true`. When the
1891
+ * client is not the elected leader, all WebSocket operations are skipped.
1892
+ * Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
1893
+ */
1894
+ private tabCoordinator;
1895
+ /**
1896
+ * The leader's last-broadcast aggregate {@link ConnectionStatus}, mirrored
1897
+ * on a follower tab — which owns no `ShardConnection` of its own to
1898
+ * compute a status from (see `computeStatus`). `undefined` until the
1899
+ * leader's first broadcast (falls back to `"idle"`), and reset back to
1900
+ * `undefined` whenever this tab stops being a follower of the CURRENT
1901
+ * leader (becomes leader itself, or the leader changes), so a stale
1902
+ * mirror from a previous leader never survives a leadership change.
1903
+ */
1904
+ private readonly leader;
1905
+ /** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
1906
+ private readonly connections;
1907
+ /**
1908
+ * Shards whose socket was closed for being idle after it had connected (see
1909
+ * {@link LunoraClient.releaseIdleShard}). The connection record is gone, but the write
1910
+ * gate still needs to know the shard was reached — see `connectionGateState`.
1911
+ * Consumed when the shard's connection is recreated.
1912
+ */
1913
+ private readonly idleClosedShards;
1914
+ /** Default `connect`-envelope context applied to a shard with no explicit override. */
1915
+ private readonly defaultConnectionContext;
1916
+ /**
1917
+ * Per-shard `connect`-envelope context registered via `setConnectionContext`
1918
+ * (keyed by `shardKey ?? ""`), overriding `defaultConnectionContext`. Sent
1919
+ * on every socket open so it replays across reconnects, and forwarded to the
1920
+ * server's `onConnect`/`onDisconnect` lifecycle hooks. This holds only the
1921
+ * imperative (last-writer-wins) override; refcounted holders registered via
1922
+ * `acquireConnectionContext` live in `connectionContextHolders` and take
1923
+ * precedence — see `effectiveConnectionContext`.
1924
+ */
1925
+ private readonly connectionContexts;
1926
+ /**
1927
+ * Per-shard stack of refcounted connection-context holders (keyed by
1928
+ * `shardKey ?? ""`), registered via `acquireConnectionContext`. Each holder
1929
+ * is an opaque token carrying its `context`; the most-recently acquired
1930
+ * holder wins (last-writer-wins among live holders), and the context is only
1931
+ * cleared for a shard once its last holder releases — so two concurrently
1932
+ * mounted presence hooks on the same shard can't stomp each other's context
1933
+ * on cleanup. A holder is identified by reference identity so a release
1934
+ * removes exactly the right one regardless of stack position.
1935
+ */
1936
+ private readonly connectionContextHolders;
1937
+ private authToken;
1938
+ /**
1939
+ * Optional STABLE identity subject (a user id), the basis of the offline-queue
1940
+ * identity stamp when supplied. Keeps a same-user token *refresh* from looking
1941
+ * like an identity change (which would discard queued writes). `undefined` =
1942
+ * not supplied, so identity falls back to a hash of the raw token. See
1943
+ * `setAuthToken` / `identityFingerprint`.
1944
+ */
1945
+ private authSubject;
1946
+ /**
1947
+ * The bearer token `authSubject` was last asserted against. A subject
1948
+ * is a claim about WHO a specific credential belongs to; once the token
1949
+ * rotates, the sticky label is carried forward on the assumption it is the
1950
+ * same user — but nothing has confirmed that yet, and the alternative (an
1951
+ * account switch) would replay one user's queued writes as another. While
1952
+ * this is out of step with `authToken` the identity counts as unconfirmed
1953
+ * and every replay verdict is `"unknown"` (hold), until the next session
1954
+ * resolve re-asserts the subject. Only used when a subject is established;
1955
+ * a token-hash identity is self-confirming.
1956
+ */
1957
+ private subjectToken;
1958
+ /**
1959
+ * How many `getCurrentUser()` round trips are in flight.
1960
+ *
1961
+ * The identity gates need to tell two `null` fingerprints apart. For an app
1962
+ * with no auth at all, and for a client the server has answered "no
1963
+ * session" to, `null` is a real and stable identity — nobody else holds it,
1964
+ * so a `null`-stamped queued write or cached read is this client's own and
1965
+ * may be used. While a resolve is IN FLIGHT it is neither: the fingerprint
1966
+ * may be one round trip away from `subj:<id>`, which is the window a cookie
1967
+ * app spends every page load in, and where `null === null` handed one
1968
+ * browser user's durable state to the next. Counted rather than flagged so
1969
+ * two overlapping resolves (a token change during a probe) both have to
1970
+ * finish before the gates reopen.
1971
+ */
1972
+ private sessionProbesInFlight;
1973
+ /**
1974
+ * Monotonic id stamped on each `/get-session` round trip, so only the
1975
+ * NEWEST one may write the shared identity.
1976
+ *
1977
+ * `adoptResolvedSubject`'s token check asks whether the credential the
1978
+ * answer is about is still the one in hand. On a cookie app there is no
1979
+ * credential to ask about: every probe captures `requestToken === null`, so
1980
+ * two overlapping probes both pass it and whichever lands LAST wins —
1981
+ * including an older one, which then replaces the subject a newer answer
1982
+ * already established. Comparing the generation instead asks the question
1983
+ * the token cannot: is this still the current question?
1984
+ */
1985
+ private sessionProbeGeneration;
1986
+ /**
1987
+ * Monotonic id of every question this client asks the server about who it
1988
+ * is: each `/get-session` probe, and each cookie-authenticated socket upgrade
1989
+ * (answered by the `identity` frame the shard sends on `connect`). Probes
1990
+ * take their `sessionProbeGeneration` from it, so both kinds of
1991
+ * answer share one order.
1992
+ */
1993
+ private identityQuestions;
1994
+ /**
1995
+ * The `identityQuestions` id of the newest answer adopted so far. A
1996
+ * cookie-session answer older than it describes an older cookie — a socket
1997
+ * upgraded before a probe that has since answered — and may not move the
1998
+ * identity back. See `adoptCookieSubject`.
1999
+ */
2000
+ private identityAnsweredAt;
2001
+ /** Unregisters this client from the page's session-change signal (`shared/session-change.ts`); set in the constructor. */
2002
+ private releaseSessionChangeListener;
2003
+ /** Whether the one-time `IDENTITY_MISMATCH` configuration warning has been logged (see `noteIdentityMismatch`). */
2004
+ private warnedIdentityDisagreement;
2005
+ /**
2006
+ * Whether anything resolves this client's identity at all.
2007
+ *
2008
+ * The gates need to tell an app with NO AUTH — whose `null` fingerprint is
2009
+ * settled forever and shared with nobody — from a cookie app that simply
2010
+ * has not asked yet. Nothing in the client's own configuration says which
2011
+ * it is (`authBasePath` has a default), so the answer is declared: a
2012
+ * `getCurrentUser()` call, or `@lunora/client/auth`'s identity store
2013
+ * attaching itself, is what makes `null` provisional. An app that does
2014
+ * neither keeps its read cache and its offline queue exactly as before.
2015
+ */
2016
+ private identityResolutionExpected;
2017
+ /**
2018
+ * Whether a `/get-session` round trip has ever produced an ANSWER — a user,
2019
+ * or an explicit "no session". Distinct from `sessionProbesInFlight`, which
2020
+ * only covers the request's own lifetime and so reopened the gates both
2021
+ * before the first probe started and after one failed to answer, leaving a
2022
+ * `null` fingerprint free to match the previous cookie user's cached rows
2023
+ * and queued writes in exactly the window the probe count was added to
2024
+ * close. A transport or parse failure never sets this: it learned nothing.
2025
+ */
2026
+ private identitySettled;
2027
+ /**
2028
+ * Identity stamp recorded against each queued offline mutation, keyed by
2029
+ * the queue-assigned mutation id. Captured at enqueue from the auth token
2030
+ * in effect at the time, and re-checked at flush so a queued write can
2031
+ * never replay under a different identity than the one that issued it.
2032
+ * See `identityFingerprint` for the fingerprint shape.
2033
+ */
2034
+ private readonly queuedIdentities;
2035
+ /**
2036
+ * Distinct shard keys with a mutation currently sitting in `offlineQueue`
2037
+ * — fresh writes queued this session (`enqueueOfflineMutation`) or writes
2038
+ * restored from durable storage (`hydratePersistedQueue`). A follower tab
2039
+ * has no per-shard `ShardConnection` to iterate when its mirrored leader
2040
+ * status turns `"connected"` (see the `onConnectionStatus` coordinator
2041
+ * option), so this is what that flush walks instead. Entries are never
2042
+ * removed — flushing an already-empty shard is a cheap no-op, and the set
2043
+ * is bounded by the app's own distinct shard-key cardinality.
2044
+ */
2045
+ private readonly queuedOfflineShardKeys;
2046
+ private closed;
2047
+ /** Subscribers to auth-token changes (see `onAuthTokenChange`). */
2048
+ private readonly authTokenListeners;
2049
+ /** Subscribers to aggregate connection-status changes (see `onConnectionStatus`). */
2050
+ private readonly statusListeners;
2051
+ /** Room handles' held-send flushers, keyed by the shard connection they send on. */
2052
+ private readonly roomFlushes;
2053
+ /** Room handles' pending-send resets, run when the identity changes so queued sends never cross identities. */
2054
+ private readonly roomIdentityResets;
2055
+ /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
2056
+ private readonly tokenExpiredListeners;
2057
+ /** Subscribers notified when the previous identity's session is retired (see `onIdentityChange`). */
2058
+ private readonly identityChangeListeners;
2059
+ /** How many identities this client has retired (see `identityEpoch`). */
2060
+ private retiredIdentities;
2061
+ /** Token hash the last durable-replay auth refusal fired `onTokenExpired` for — see `noteReplayAuthRefusal`. */
2062
+ private authRefusalNotifiedFor;
2063
+ /** What each outstanding {@link ReplayCredential} was judged against — see `replayIdentityVerdict`. */
2064
+ private readonly replayCredentials;
2065
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
2066
+ private readonly mutationSettledListeners;
2067
+ /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
2068
+ private readonly pendingChangeListeners;
2069
+ /**
2070
+ * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
2071
+ * of callbacks. Membership doubles as the resubscribe set replayed on every
2072
+ * (re)connect so a topic survives a socket bounce.
2073
+ */
2074
+ private readonly whisperHandlers;
2075
+ /** Last status broadcast, so we only notify listeners on an actual change. */
2076
+ private lastStatus;
2077
+ private nextSubId;
2078
+ private nextStreamId;
2079
+ /**
2080
+ * In-flight client-side stream readers, keyed by the stream id sent on the
2081
+ * wire. The handle drives the underlying iterator queue and `shardKey`
2082
+ * tells us which socket to push the cancel frame onto when the consumer
2083
+ * calls `.cancel()` or the iterator is garbage-collected.
2084
+ */
2085
+ private readonly streams;
2086
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
2087
+ /**
2088
+ * Teardown callbacks for the admin sockets {@link LunoraClient.subscribeScheduledJobs}
2089
+ * opens. Those run their own reconnect loop off a closure-local `closed`
2090
+ * flag rather than `this.closed` (they predate `ensureSocket`'s guard), so
2091
+ * without this registry a `close()` left every one of them reconnecting on
2092
+ * its backoff forever — re-minting an ephemeral admin sub-token on each
2093
+ * attempt when a `WsTokenProvider` is wired.
2094
+ */
2095
+ private readonly adminSocketTeardowns;
2096
+ /**
2097
+ * The in-flight offline-queue replay per shard (`connectionKey`), while one
2098
+ * is running. Two jobs:
2099
+ *
2100
+ * 1. It serializes overlapping flushes for the same shard — two reconnect
2101
+ * events in quick succession used to drain and replay concurrently.
2102
+ * 2. It is the barrier {@link LunoraClient.mutation} waits on before sending a FRESH
2103
+ * write directly. The socket's `open` handler flips `wsState` to `"open"`
2104
+ * first and calls the flush last, so from that instant `mutation()`'s
2105
+ * offline gate is false and a new write raced straight to `/rpc` against
2106
+ * the replay of the older, queued write for the same document — the newer
2107
+ * one could land first and then be overwritten by the older. Ordering
2108
+ * inside the replay (`replaySequential`) never covered this, because the
2109
+ * racing write was never in the queue.
2110
+ */
2111
+ private readonly offlineFlushes;
2112
+ /**
2113
+ * Per-shard replay backoff, keyed by {@link connectionKey} exactly as the
2114
+ * flushes and their timers are: `delayMs` is the longest hint the CURRENT
2115
+ * flush of that key was given (written by
2116
+ * {@link LunoraClient.noteReplayRetryDelay}, consumed once by
2117
+ * {@link LunoraClient.scheduleRateLimitedRetry} at the end of the drain), and
2118
+ * `attempts` counts its consecutive failed flushes, which is what the
2119
+ * hintless backoff ramps on.
2120
+ *
2121
+ * Keyed, not a single field: flushes are per shard key and run concurrently,
2122
+ * so one field means one limited shard sets the wait for every other shard
2123
+ * and two flushes overwrite — then consume — each other's delay, leaving one
2124
+ * of them with nothing scheduled at all. Evicted by
2125
+ * {@link LunoraClient.scheduleRateLimitedRetry} the moment a key has nothing
2126
+ * left to retry, so an app that shards per document does not accumulate an
2127
+ * entry per document it ever wrote.
2128
+ */
2129
+ private readonly replayRetryState;
2130
+ /** Pending rate-limit retry flushes, keyed by {@link connectionKey}, so `close()` can cancel them. */
2131
+ private readonly replayRetryTimers;
2132
+ private readonly shapeSubscriptions;
2133
+ /** In-flight pokes, keyed by connection and `pokeId`. The key rule is in shape-state.ts. */
2134
+ private readonly pokeAssembler;
2135
+ private nextShapeId;
2136
+ constructor(options: LunoraClientOptions);
2137
+ /**
2138
+ * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
2139
+ * {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
2140
+ * sync across all mounted instances.
2141
+ *
2142
+ * Pass a STABLE `subject` (the user id) to key the offline-queue identity on
2143
+ * it instead of the token bytes, so a token *refresh* (same user, new JWT)
2144
+ * doesn't read as an identity change and discard queued writes. The subject is
2145
+ * **sticky**: a later call that omits it (or passes `undefined`) keeps the
2146
+ * established subject — so `setAuthToken(refreshedToken)` after a prior
2147
+ * `setAuthToken(token, user.id)` retains the identity. Clearing the token
2148
+ * clears it too — a credential-less subject would let the NEXT sign-in
2149
+ * inherit the previous user's identity (and their queue and read cache).
2150
+ * Establishing the subject on an UNCHANGED token (e.g. the user id resolves a
2151
+ * tick after the token was set — see {@link getCurrentUser}, which does this
2152
+ * for every adapter) re-stamps any in-flight queued writes rather than
2153
+ * dropping them: same credential, just a more stable label. A real user
2154
+ * switch still drops the previous user's writes.
2155
+ *
2156
+ * A sticky subject carried across a token change is not re-confirmed until
2157
+ * the next session resolve says which user the NEW credential belongs to.
2158
+ * Queued writes are HELD (never sent, never dropped) for that window — see
2159
+ * {@link replayIdentityVerdict} — so a refresh can't replay them and an
2160
+ * account switch can't replay the previous user's writes as the new one.
2161
+ *
2162
+ * Does NOT update the WebSocket auth — the WS token is fixed at upgrade
2163
+ * time and lives in the URL. To refresh live WS auth, call
2164
+ * {@link setWsToken} explicitly, which closes existing shard sockets to
2165
+ * force a reconnect with the new credential.
2166
+ */
2167
+ setAuthToken(token: string | null, subject?: string | null): void;
2168
+ getAuthToken(): string | null;
2169
+ /**
2170
+ * The current identity fingerprint (the same stamp queued offline writes
2171
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
2172
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
2173
+ * can drop a persisted write whose captured `identity` no longer matches the
2174
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
2175
+ */
2176
+ currentIdentity(): string | null;
2177
+ /**
2178
+ * Declare that this client's identity is resolved by something — call it
2179
+ * before the first `getCurrentUser()`, not after.
2180
+ *
2181
+ * `@lunora/client/auth`'s identity store calls this when it attaches, which
2182
+ * is the moment an app becomes one that HAS auth. Until something says so,
2183
+ * a `null` fingerprint is taken to be the settled, unshared identity of an
2184
+ * app with no auth at all, because nothing else distinguishes the two: both
2185
+ * hold no token, both have never been answered, and `authBasePath` has a
2186
+ * default. That reading is right for the no-auth app and wrong for a cookie
2187
+ * app whose first `/get-session` has not started — the window where
2188
+ * `null === null` matched the PREVIOUS browser user's cached rows and
2189
+ * queued writes, before any probe was in flight to mark the fingerprint
2190
+ * provisional.
2191
+ *
2192
+ * Idempotent, and one-way: a client that resolves an identity keeps its
2193
+ * gates armed until a probe actually answers.
2194
+ */
2195
+ expectIdentityResolution(): void;
2196
+ /**
2197
+ * This client's current CDC baseline for `shardKey` — the cursor its live
2198
+ * queries have reached, which is what a write composed right now should be
2199
+ * judged against.
2200
+ *
2201
+ * Exposed for the same reason {@link currentIdentity} is: a durable
2202
+ * {@link OutboxSink} owns its own at-least-once replay outside the built-in
2203
+ * `OfflineQueue`, so it has to capture this at COMPOSE time and hand it back
2204
+ * through {@link MutationCallOptions.replayBaseline}. Sampling it inside the
2205
+ * replay instead reads the cursor this client has advanced to in the
2206
+ * meantime, which is the newer state the write must be judged against.
2207
+ *
2208
+ * `undefined` when no subscription on the shard carries a cursor yet.
2209
+ */
2210
+ currentBaseline(shardKey?: string): number | undefined;
2211
+ /**
2212
+ * Verdict on whether a durable write stamped with `stamped` may be replayed
2213
+ * now. The comparison a replay handler must NOT hand-roll.
2214
+ *
2215
+ * `"match"` is the same identity, or the same credential under a token hash
2216
+ * — which a late-resolving subject would otherwise make look different.
2217
+ *
2218
+ * `"unknown"` is nobody signed in *yet*. A durable replay starts when the
2219
+ * executor is constructed, before the app has resolved its session and
2220
+ * called {@link setAuthToken}, so this is the normal state on every reload.
2221
+ * There is no other identity to replay as, so such a write must be HELD,
2222
+ * never dropped — dropping here destroys the queuing user's own offline
2223
+ * writes. It is indistinguishable from an explicit sign-out (the fingerprint
2224
+ * is `null` for both), which is the safe conflation: holding a write for a
2225
+ * signed-out app costs a retry, dropping it costs the write.
2226
+ *
2227
+ * It also covers a `null` stamp against a `null` fingerprint — but only
2228
+ * while {@link identityUnresolved} says a session resolve is in flight.
2229
+ * Two `null`s are the same identity for an app with no auth at all (nobody
2230
+ * else shares that fingerprint, because nobody else exists) and for a
2231
+ * client the server has told there is no session; they are NOT the same
2232
+ * identity in the window where the fingerprint is about to become
2233
+ * `subj:<id>`, which is where one browser user's queued write went out on
2234
+ * the next one's cookie.
2235
+ *
2236
+ * A held write is not stranded: the hold ends when the resolve settles, and
2237
+ * both {@link setAuthToken} and {@link getCurrentUser} re-flush there. If
2238
+ * the identity that arrives is not the one that queued the write, the
2239
+ * verdict is then `"mismatch"` and the write is settled terminally.
2240
+ *
2241
+ * A sticky subject carried across a token change is `"unknown"` for the same
2242
+ * reason: the label says user A, the credential in hand has not been checked
2243
+ * against it, and the two answers (a refresh vs an account switch) call for
2244
+ * opposite verdicts. Hold until the next session resolve settles it.
2245
+ *
2246
+ * `"mismatch"` is a different identity signed in, and is the one that must be
2247
+ * terminal: replaying would attribute one user's write to another and pass
2248
+ * THEIR row-level security.
2249
+ *
2250
+ * A `"match"` carries the {@link ReplayCredential} the replay must be sent
2251
+ * with ({@link MutationCallOptions.replayCredential}): the bearer held right
2252
+ * now, which is the one this verdict judged. The replay goes out with it even
2253
+ * if the token changes before the send, and the outbox's next attempt judges
2254
+ * again against whatever is current then.
2255
+ */
2256
+ replayIdentityVerdict(stamped: null | string | undefined): ReplayIdentityVerdict;
2257
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
2258
+ clientIdentifier(): string;
2259
+ /**
2260
+ * The highest custom-mutator watermark the server has echoed for this client
2261
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
2262
+ * its `clientSeq` generator from this so a reload never reissues a sequence
2263
+ * the server has already applied (which it would swallow as a replay, silently
2264
+ * dropping the write).
2265
+ */
2266
+ confirmedMutationWatermark(shardKey?: string): number;
2267
+ /**
2268
+ * Push a custom mutator to its authoritative server impl over the watermark
2269
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
2270
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
2271
+ * client's `__client_watermark`.
2272
+ *
2273
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
2274
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
2275
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
2276
+ * A `false` verdict tells the caller to reissue above the now-known watermark
2277
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
2278
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
2279
+ *
2280
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
2281
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
2282
+ */
2283
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
2284
+ clientSeq?: number;
2285
+ shardKey?: string;
2286
+ }): Promise<{
2287
+ applied: boolean;
2288
+ result: unknown;
2289
+ }>;
2290
+ /**
2291
+ * Subscribe to auth-token changes. Returns an unsubscribe function. The
2292
+ * listener is NOT invoked on registration — use {@link getAuthToken} for
2293
+ * the current value.
2294
+ */
2295
+ onAuthTokenChange(listener: (token: string | null) => void): Unsubscribe;
2296
+ /**
2297
+ * Subscribe to a user switch or sign-out: fired after {@link setAuthToken}
2298
+ * retires the previous identity's session, i.e. every live subscription has
2299
+ * just been blanked to `undefined` and its socket closed. Not fired when a
2300
+ * bearer identity is first established from signed-out (nothing is
2301
+ * retired), nor when a subject is attached to the same credential, nor on
2302
+ * a cookie session's first answer after page load.
2303
+ *
2304
+ * A cookie session fires it on every change the server reports — sign-out,
2305
+ * a sign-in after a known sign-out, a different user — whether the answer
2306
+ * came from {@link getCurrentUser} or from a socket's `identity` frame. Its
2307
+ * sign-out changes nothing the client can observe on its own; in a browser
2308
+ * the client also re-resolves whenever `@lunora/auth-ui` or the
2309
+ * `lunoraSessionSync()` better-auth plugin reports a session change.
2310
+ *
2311
+ * For caches layered on top of the client. A subscriber only hears the
2312
+ * blank for queries it still subscribes to, so a cache that keeps values for
2313
+ * unmounted queries must drop them here, or the next user can read the
2314
+ * previous user's rows from it.
2315
+ */
2316
+ onIdentityChange(listener: () => void): Unsubscribe;
2317
+ /**
2318
+ * How many identities this client has retired since it was constructed —
2319
+ * the number of times {@link onIdentityChange} has fired. Already moved when
2320
+ * those listeners run.
2321
+ *
2322
+ * A value rendered on the server (a `Preloaded` token) was read for whoever
2323
+ * was signed in when the page loaded, so it is only safe to show while this
2324
+ * is `0`: once a sign-out or user switch has retired that identity, a
2325
+ * component mounting later must not seed the previous user's rows.
2326
+ */
2327
+ identityEpoch(): number;
2328
+ /**
2329
+ * Fetch the currently authenticated user from better-auth's `get-session`
2330
+ * endpoint, returning the `user` record or `null` when signed out. Sends
2331
+ * the stored bearer token (if any) and `credentials: "include"` so a
2332
+ * cookie-session is also honoured.
2333
+ *
2334
+ * `null` means the **server answered** that there is no session — an empty
2335
+ * body, no `user` field, or a non-OK status such as 401. A network or parse
2336
+ * failure **rejects** instead, because "the endpoint was unreachable" is not
2337
+ * the same answer as "you are signed out": collapsing the two made an
2338
+ * offline reload with a valid stored token read as a sign-out in every
2339
+ * adapter's auth gate. Callers that cannot act on the difference can still
2340
+ * `.catch(() => null)`; `@lunora/client/auth`'s identity store maps the
2341
+ * rejection to the `"unreachable"` status instead.
2342
+ *
2343
+ * Framework-agnostic: pair it with {@link onAuthTokenChange} to refetch when
2344
+ * the token changes (that's what `@lunora/react`'s `useAuth` does).
2345
+ *
2346
+ * A resolved user also ESTABLISHES the identity subject for the credential
2347
+ * that resolved it ({@link setAuthToken}'s second argument). This is the one
2348
+ * call every adapter's identity store already makes, and the only place that
2349
+ * knows the answer — leaving it to each adapter is what left the sticky-subject
2350
+ * contract unreached in every shipped one, so a routine JWT refresh read as a
2351
+ * user switch and discarded the user's own queued writes and read cache.
2352
+ *
2353
+ * Under a cookie session (no token held) the answer is adopted whatever it
2354
+ * is, "no session" included, and a change from the identity already known
2355
+ * retires the previous session — see {@link onIdentityChange}.
2356
+ */
2357
+ getCurrentUser(): Promise<User | null>;
2358
+ /**
2359
+ * Replace the token appended to WS upgrade URLs as `?token=…` and close
2360
+ * every open shard socket so the reconnect picks up the new value. Call
2361
+ * this whenever the user's WS credential changes (rotating the admin token
2362
+ * in the studio, switching workspaces, etc.). Accepts a static string or a
2363
+ * {@link WsTokenProvider} resolved fresh at every (re)connect — the channel
2364
+ * for short-lived credentials like the minted ephemeral admin sub-token.
2365
+ * Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
2366
+ */
2367
+ setWsToken(token: string | undefined | WsTokenProvider): void;
2368
+ /**
2369
+ * Register (or clear, with `undefined`) the app context sent in the `connect`
2370
+ * envelope for a shard's socket, overriding the client-wide
2371
+ * {@link LunoraClientOptions.connectionContext}. The server forwards it to the
2372
+ * `onConnect`/`onDisconnect` lifecycle hooks as `event.context` — e.g.
2373
+ * `@lunora/react`'s `usePresence` registers `{ roomId, sessionId }` so the
2374
+ * presence row is removed the instant the socket drops, with no TTL lag.
2375
+ *
2376
+ * Stored per shard and replayed on every (re)connect. When a socket for the
2377
+ * shard is already open, a fresh `connect` envelope is sent immediately so the
2378
+ * server sees the new context without waiting for a reconnect.
2379
+ *
2380
+ * Not available on a `crossTabSync` FOLLOWER tab — the context rides the
2381
+ * `connect` envelope of a socket a follower does not own, so it would be
2382
+ * stored and never sent. Throws `NOT_IMPLEMENTED` there.
2383
+ */
2384
+ setConnectionContext(context: Record<string, unknown> | undefined, options?: {
2385
+ shardKey?: string;
2386
+ }): void;
2387
+ /**
2388
+ * Refcounted variant of {@link setConnectionContext}: register a connection
2389
+ * `context` for a shard and get back a release function. Unlike the imperative
2390
+ * setter, the context is only cleared once the *last* acquired holder releases
2391
+ * it — so two components (e.g. two mounted `usePresence` hooks) on the same
2392
+ * shard no longer clobber each other's context when one of them unmounts. The
2393
+ * most-recently acquired live holder wins (last-writer-wins), and releasing
2394
+ * the top holder falls back to the previous one rather than clearing.
2395
+ *
2396
+ * With a single holder the behaviour is identical to a
2397
+ * `setConnectionContext(context)` / `setConnectionContext(undefined)` pair.
2398
+ * Releasing more than once is a no-op (the holder is matched by reference, so
2399
+ * a double release can't drop a different holder).
2400
+ */
2401
+ acquireConnectionContext(context: Record<string, unknown>, options?: {
2402
+ shardKey?: string;
2403
+ }): Unsubscribe;
2404
+ /**
2405
+ * Join a whisper `topic` and receive every ephemeral message other members
2406
+ * broadcast to it on the same shard (typing indicators, live cursors,
2407
+ * presence pings). Whispers never touch the server's durable state — there's
2408
+ * no query, no row, no CDC entry. Returns an unsubscribe function; the topic
2409
+ * is left on the server once its last local handler unsubscribes.
2410
+ *
2411
+ * `handler` receives the raw `data` and the sender's verified `from` user id
2412
+ * (omitted for an anonymous sender). The topic is scoped to `options.shardKey`
2413
+ * (the default shard when omitted) — use the same shard you target with the
2414
+ * matching queries/mutations so members land on the same Durable Object.
2415
+ *
2416
+ * Security: a whisper topic is access-controlled only if the app declares an
2417
+ * `onWhisper` authorizer server-side (`@lunora/server`). Without one, the
2418
+ * topic's only boundary is the shard — any client that can open a socket to it
2419
+ * can join, read, and inject on any topic name. `from` is server-stamped and
2420
+ * unforgeable either way, but never trust a whisper's `data` as authorization,
2421
+ * and remember that even an authorized topic is transient awareness with no
2422
+ * durable trace: anything privileged belongs behind a query/mutation with RLS.
2423
+ *
2424
+ * A denied join is silent — whispers are never acked, so nothing distinguishes
2425
+ * "denied" from "nobody is whispering". Gate the UI on a query you can read a
2426
+ * verdict from, not on whether whispers arrive.
2427
+ *
2428
+ * Not available on a `crossTabSync` FOLLOWER tab — whisper frames are not
2429
+ * relayed over the cross-tab channel, so this throws `NOT_IMPLEMENTED`
2430
+ * there rather than registering a handler nothing can ever reach. See
2431
+ * {@link LunoraClientOptions.crossTabSync}.
2432
+ */
2433
+ whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
2434
+ shardKey?: string;
2435
+ }): Unsubscribe;
2436
+ /**
2437
+ * Broadcast an ephemeral `data` payload to the other members of a whisper
2438
+ * `topic` on `options.shardKey`'s shard. Fire-and-forget: the frame is
2439
+ * dropped when the shard socket isn't open (whispers are transient, never
2440
+ * queued), and the server silently drops it if the sender exceeds its
2441
+ * whisper rate budget. The sender never receives its own whisper. Omitting
2442
+ * `data` delivers JSON `null` to receivers (not `undefined`).
2443
+ *
2444
+ * Returns `true` when the frame was handed to an open socket, `false` when it
2445
+ * was dropped, so a caller that must not lose a message (one sent right after
2446
+ * connecting, before the socket opens) can detect the drop and retry.
2447
+ *
2448
+ * That best-effort drop is for a socket that is momentarily down. A
2449
+ * `crossTabSync` FOLLOWER tab has no socket and never will (see
2450
+ * {@link LunoraClientOptions.crossTabSync}), so every whisper from it would
2451
+ * be dropped forever — it throws `NOT_IMPLEMENTED` instead.
2452
+ *
2453
+ * A send the app's `onWhisper` authorizer denies is dropped the same silent
2454
+ * way, for the same reason: there is no ack frame to report it on. See
2455
+ * {@link whisperSubscribe} for the security model.
2456
+ */
2457
+ whisper(topic: string, data?: unknown, options?: {
2458
+ shardKey?: string;
2459
+ }): boolean;
2460
+ /**
2461
+ * Join a `defineRoom` room from the client: subscribe to the room's server
2462
+ * dispatches and send typed messages to it. A thin, typed wrapper over
2463
+ * {@link whisperSubscribe} / {@link whisper} on the room's topic
2464
+ * (`room:<name>`, the topic the shard resolves to the room's handler).
2465
+ *
2466
+ * The room is passed as its generated reference, so `onMessage` and `send` are
2467
+ * typed from the room's own message validator:
2468
+ *
2469
+ * ```ts
2470
+ * import { rooms } from "./_generated/api";
2471
+ *
2472
+ * const chat = client.connectRoom(rooms.chat, {
2473
+ * onMessage: (message) => append(message.text),
2474
+ * });
2475
+ * chat.send({ text: "hello" });
2476
+ * chat.close();
2477
+ * ```
2478
+ *
2479
+ * `onMessage` receives what the room's server handler broadcasts (via its
2480
+ * `socket.broadcast`), NOT the sender's own raw send — the handler is
2481
+ * authoritative. Returns a handle with `send` and `close`.
2482
+ */
2483
+ connectRoom<Message>(room: RoomRef<Message>, handlers: {
2484
+ onMessage: (message: Message) => void;
2485
+ }, options?: {
2486
+ shardKey?: string;
2487
+ }): {
2488
+ close: () => void;
2489
+ send: (message: Message) => boolean;
2490
+ };
2491
+ /**
2492
+ * Subscribe to token-expiry events: invoked whenever the server drops a
2493
+ * shard socket because the connection's credential lapsed (close code
2494
+ * `4001`). The client already reconnects automatically (re-resolving
2495
+ * identity from the cookie/token in effect); use this to refresh a
2496
+ * short-lived token first — e.g. call {@link setWsToken} / {@link setAuthToken}
2497
+ * with a freshly minted one. Returns an unsubscribe function.
2498
+ *
2499
+ * Also fired, once per credential, when a queued write's replay is refused
2500
+ * `TOKEN_EXPIRED` / `UNAUTHENTICATED`. The write is held for the refresh, and
2501
+ * survives it only if the identity is keyed on a `subject`: pass the same user
2502
+ * id with the new token, `setAuthToken(freshToken, user.id)` (or have passed
2503
+ * it before — the subject is sticky). With no subject the identity is the
2504
+ * token hash, so a refresh cannot be told from an account switch and the held
2505
+ * writes are rejected `OFFLINE_IDENTITY_CHANGED`. A cookie session has no
2506
+ * token to replace: renew the cookie, and the held write is re-sent on a
2507
+ * backoff with it.
2508
+ */
2509
+ onTokenExpired(listener: () => void): Unsubscribe;
2510
+ /**
2511
+ * Current aggregate live-socket status across all shard connections. See
2512
+ * {@link ConnectionStatus}.
2513
+ */
2514
+ connectionStatus(): ConnectionStatus;
2515
+ /**
2516
+ * Subscribe to aggregate connection-status changes. Invokes `listener`
2517
+ * immediately with the current status, then on every transition. Returns an
2518
+ * unsubscribe function.
2519
+ */
2520
+ onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
2521
+ /**
2522
+ * Number of offline writes waiting in the built-in queue to be sent — the
2523
+ * depth for a "N changes waiting to sync" indicator. Counts writes that are
2524
+ * queued (offline / mid-reconnect), not ones already in flight on the wire.
2525
+ * A `@lunora/db` app whose writes ride the unified outbox should read
2526
+ * `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
2527
+ */
2528
+ pendingCount(): number;
2529
+ /**
2530
+ * A point-in-time snapshot of everything the sync engine believes right now:
2531
+ * per-shard sockets and watermarks, every live query and shape subscription with
2532
+ * its cursor and ack state, and the offline-queue depth.
2533
+ *
2534
+ * This exists because the alternative is `console.log`. When an optimistic
2535
+ * overlay doesn't clear, the questions are always the same — *is the socket open?
2536
+ * what watermark has the server confirmed for this shard? has this shape been
2537
+ * poked since my write? is anything stuck in the queue?* — and none of them were
2538
+ * answerable from outside the client, so every adopter ends up instrumenting the
2539
+ * library by hand or building a bespoke policy layer around a symptom.
2540
+ *
2541
+ * Read it from a devtools console, log it next to a bug report, or render it in a
2542
+ * debug panel. Pull-only and allocation-cheap; nothing here is reactive, so poll
2543
+ * it or read it on demand.
2544
+ *
2545
+ * ```ts
2546
+ * const { shards, subscriptions } = client.debug();
2547
+ * // shards: [{ shardKey: "user-1", wsState: "open", confirmedMutationWatermark: 42, … }]
2548
+ * ```
2549
+ */
2550
+ debug(): ClientDebugSnapshot;
2551
+ /**
2552
+ * Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
2553
+ * with the current count, then whenever the queue depth changes (a write is
2554
+ * enqueued, flushed, or discarded). Returns an unsubscribe function.
2555
+ */
2556
+ onPendingChange(listener: (pending: number) => void): Unsubscribe;
2557
+ /**
2558
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
2559
+ * fires once per queued write that commits or is rejected — including a write
2560
+ * restored from durable storage after a reload, whose original `mutation()`
2561
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
2562
+ * evicts on overflow or discards on an identity change. This is the durable
2563
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
2564
+ * mutation that never queued still surfaces through the Promise `mutation()`
2565
+ * returns. The listener is NOT invoked on registration. Returns an
2566
+ * unsubscribe function. See {@link MutationSettledEvent}.
2567
+ */
2568
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
2569
+ /**
2570
+ * The `WebSocket` implementation this client was constructed with (an
2571
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
2572
+ * one) — `undefined` if neither is available. This is the seam a feature
2573
+ * that opens its OWN socket outside the client's multiplexed connection
2574
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
2575
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
2576
+ * constructor to inject the auth-headers factory's credential onto the
2577
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
2578
+ * `new globalThis.WebSocket(url)` would silently bypass.
2579
+ */
2580
+ getWebSocketImpl(): typeof WebSocket | undefined;
2581
+ /**
2582
+ * Read the current value for a {@link ClientQueryRef}. Returns
2583
+ * `ref.defaultValue` when no value has been explicitly set.
2584
+ */
2585
+ getClientQuery<T>(ref: ClientQueryRef<T>): T;
2586
+ /**
2587
+ * Set a new value for `ref` and notify every subscriber. Pass `undefined`
2588
+ * to reset the slot to `ref.defaultValue`.
2589
+ */
2590
+ setClientQuery<T>(ref: ClientQueryRef<T>, value: T): void;
2591
+ /**
2592
+ * Subscribe to changes for `ref`. The callback is NOT invoked on
2593
+ * registration — call {@link getClientQuery} for the current value.
2594
+ * Returns an unsubscribe function.
2595
+ */
2596
+ subscribeClientQuery(ref: ClientQueryRef, callback: (value: unknown) => void): Unsubscribe;
2597
+ /**
2598
+ * Reset a {@link ClientQueryRef} to its default value, notifying every
2599
+ * subscriber. Equivalent to `setClientQuery(ref, ref.defaultValue)` but
2600
+ * removes the stored entry so a future {@link getClientQuery} returns
2601
+ * the default rather than an explicitly-set value.
2602
+ */
2603
+ resetClientQuery(ref: ClientQueryRef): void;
2604
+ /**
2605
+ * Capture a snapshot of the current live query value at call time and
2606
+ * produce a `() => boolean` precondition that compares it against the
2607
+ * value at replay time (on queue drain / reconnect).
2608
+ *
2609
+ * Delegates to {@link createSnapshotPrecondition} with this client bound —
2610
+ * no need to pass `client` explicitly. The comparison semantics (including
2611
+ * how an absent subscription is treated) live there, in one place.
2612
+ * @example
2613
+ * ```ts
2614
+ * client.mutation(api.todos.update, { id, text }, {
2615
+ * precondition: client.snapshotPrecondition(api.todos.list, { userId }),
2616
+ * });
2617
+ * ```
2618
+ */
2619
+ snapshotPrecondition(functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string): () => boolean;
2620
+ /**
2621
+ * Resolves once the durable read cache has been loaded into memory. When
2622
+ * `hydrateOnStart` is not configured or no query cache adapter is active,
2623
+ * returns an already-resolved promise so callers can always await it
2624
+ * unconditionally.
2625
+ *
2626
+ * Framework adapters (React, Vue, etc.) use this to gate the first
2627
+ * (enabled) render of a live query behind hydration, so the user sees
2628
+ * cached data instead of an undefined flash before the socket round-trip.
2629
+ */
2630
+ whenReady(): Promise<void>;
2631
+ /**
2632
+ * Synchronously reports whether {@link whenReady} has already resolved (the
2633
+ * durable read cache is loaded, or none is configured). Framework adapters
2634
+ * read this to seed the hydration-gate state on the first render without
2635
+ * awaiting, then subscribe via {@link whenReady} for the pending case.
2636
+ */
2637
+ get isReady(): boolean;
2638
+ /**
2639
+ * Synchronously peek at a value the durable read cache loaded for the given
2640
+ * function path + args + shard key. Returns `undefined` when:
2641
+ *
2642
+ * - No query cache adapter is configured.
2643
+ * - Hydration hasn't completed yet (race — await {@link whenReady} first).
2644
+ * - The cached value's identity fingerprint doesn't match the current auth.
2645
+ *
2646
+ * Unlike the internal {@link takeHydratedCache}, this is a READ-ONLY peek:
2647
+ * the cached entry stays in `hydratedQueryCache` so the subscription created
2648
+ * later by {@link subscribe} consumes it normally.
2649
+ */
2650
+ peekHydratedQuery(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
2651
+ /**
2652
+ * Same lookup as {@link peekHydratedQuery} for a caller that already holds the
2653
+ * `stableWireKey`-encoded args string, so the args are not canonicalised again.
2654
+ */
2655
+ peekHydratedQueryByKey(functionPath: string, argsKey: string, shardKey?: string): unknown;
2656
+ /**
2657
+ * Peek at the **current live value** of an active subscription, reporting
2658
+ * whether one exists at all rather than just its value. `present` is `false`
2659
+ * when no subscription is open for the given `(functionPath, args, shardKey)`;
2660
+ * `value` is the subscription's `lastValue`, which includes any optimistic
2661
+ * overlay.
2662
+ *
2663
+ * The two are separate because a caller like the snapshot precondition has to
2664
+ * tell "no subscription is active, so this read knows nothing" apart from
2665
+ * "the subscription is active and its value is `undefined`" — collapsing both
2666
+ * into a bare `undefined` return makes an unmounted component look like a
2667
+ * changed value, and drops the queued write.
2668
+ *
2669
+ * Unlike {@link peekHydratedQuery} (which reads from the durable read cache
2670
+ * and is independent of active subscriptions), this reflects the current
2671
+ * in-memory state of an already-opened subscription.
2672
+ */
2673
+ peekActiveQuerySnapshot(functionPath: string, args: Record<string, unknown>, shardKey?: string): {
2674
+ present: boolean;
2675
+ value: unknown;
2676
+ };
2677
+ query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
2678
+ shardKey?: string;
2679
+ }): Promise<ReturnOf<F>>;
2680
+ /**
2681
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
2682
+ * dispatched server-side exactly as an individual RPC — per-shard
2683
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
2684
+ * watermark ordering are all preserved — and the worker splits the batch by
2685
+ * shard so calls to different shards fan out to their own DOs. Results are
2686
+ * demuxed back in input order; a failing call does NOT fail the batch (its
2687
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
2688
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
2689
+ *
2690
+ * No promise pipelining and no capability passing — a call's args cannot
2691
+ * reference another call's result (see plan 088 §fence; capabilities are
2692
+ * incompatible with DO hibernation).
2693
+ */
2694
+ batch(calls: ReadonlyArray<{
2695
+ args?: Record<string, unknown>;
2696
+ fn: FunctionReference;
2697
+ shardKey?: string;
2698
+ }>): Promise<BatchSlot[]>;
2699
+ /**
2700
+ * Invoke a mutation. Errors propagate as rejections.
2701
+ *
2702
+ * Offline-queue semantics: a mutation is queued (and replayed on reconnect)
2703
+ * only when the targeted shard's socket was open at least once already
2704
+ * (`wasEverConnected`), so the registry / resubscribe handshake has run.
2705
+ * Mutations issued before the very first WS connect to a shard fail fast.
2706
+ * Opt into queueing-before-first-connect via
2707
+ * `OfflineQueueOptions.queueBeforeFirstConnect`.
2708
+ *
2709
+ * **Return-value caveat — the queued paths do not carry the server's result.**
2710
+ * The declared `Promise<ReturnOf<F>>` only holds when the write goes straight
2711
+ * to the server. Once a write is queued:
2712
+ *
2713
+ * - with a durable `outbox` configured, this resolves **immediately with
2714
+ * `undefined`** (typed as `ReturnOf<F>`) the moment the write is handed to
2715
+ * the outbox — the replay happens later, out of band, with no awaiter;
2716
+ * - with the built-in offline queue, it stays pending until the replay lands
2717
+ * and then resolves with the replayed call's value.
2718
+ *
2719
+ * So `const id = await client.mutation(api.todos.create, …)` is `undefined`
2720
+ * for every write issued while offline under an outbox. Generate ids
2721
+ * client-side (or read them back from a subscription) rather than depending
2722
+ * on a mutation's return value in an offline-capable app —
2723
+ * {@link LunoraClient.importRows} documents the same caveat for its counts.
2724
+ */
2725
+ mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
2726
+ /**
2727
+ * Whether a {@link mutation} on `shardKey` would be queued (offline queue or
2728
+ * durable outbox) rather than fail when the network is down. `false` before
2729
+ * the shard's first connect (unless `queueBeforeFirstConnect` is set) and on
2730
+ * a client with no WebSocket. A caller that wants to hold a write until the
2731
+ * network returns must do so itself when this is `false`.
2732
+ */
2733
+ canQueueOffline(shardKey?: string): boolean;
2734
+ action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
2735
+ /**
2736
+ * Bulk-import `rows` through a mutation that accepts a batch, chunked so a large
2737
+ * dataset lands in a bounded number of round-trips.
2738
+ *
2739
+ * **Queued chunks:** `imported` counts only rows the server committed. A chunk a
2740
+ * durable outbox took instead (offline, or behind older queued writes) is counted
2741
+ * in `queued`; the outbox replays it later under the same key. The built-in
2742
+ * offline queue holds the call until each chunk commits, so its rows land in
2743
+ * `imported`. Report "migration complete" only when `queued` is `0`.
2744
+ *
2745
+ * This is the one-shot migration / seed path: "I have 20k rows client-side and a
2746
+ * server mutation that inserts many at once". Doing it by hand goes wrong in two
2747
+ * predictable ways — a serial per-row loop pays one round-trip *and* one watermark
2748
+ * wait per row (a 200-row import becomes 200 sequential hops), while a single
2749
+ * giant call blows the DO's batch limit. So: chunk, send sequentially, and give
2750
+ * each chunk a stable idempotency key derived from `importId` + its index, so a
2751
+ * resumed or retried import doesn't double-insert the chunks that already landed
2752
+ * (for an anonymous caller only while the `clientId` is the same; see `importId`).
2753
+ *
2754
+ * The server mutation is yours (Lunora can't guess the table or the row shape);
2755
+ * back it with `ctx.db.insertMany(...)`, or `insertManyUnsafe(...)` for data you
2756
+ * vouch for. `chunkSize` defaults to 500, matching the DO's default batch cap.
2757
+ *
2758
+ * ```ts
2759
+ * await client.importRows(api.migrate.importNodes, nodes, {
2760
+ * importId: `migrate-${userId}`,
2761
+ * onProgress: ({ done, total }) => setProgress(done / total),
2762
+ * shardKey: userId,
2763
+ * toArgs: (chunk) => ({ nodes: chunk }),
2764
+ * });
2765
+ * ```
2766
+ */
2767
+ importRows(function_: FunctionReference, rows: ReadonlyArray<unknown>, options?: {
2768
+ /** Rows per call. Defaults to 500 — the DO's default batch cap. */
2769
+ chunkSize?: number;
2770
+ /**
2771
+ * Stable id for this import run. Each chunk is sent under
2772
+ * `${importId}:${chunkIndex}` as its mutation id, so re-running an import
2773
+ * that uses the SAME `chunkSize` re-sends each chunk under its prior key
2774
+ * and the server dedupes it instead of inserting twice.
2775
+ *
2776
+ * CAVEAT — the key is POSITIONAL, not content-based: it pins on the chunk
2777
+ * INDEX, not on the rows inside it. Resume or retry with a DIFFERENT
2778
+ * `chunkSize` (or a changed row ordering) and index N now covers different
2779
+ * rows than the first run's index N; the server sees a duplicate key and
2780
+ * SILENTLY DROPS those rows. Keep `chunkSize` (and the row order) identical
2781
+ * across resumes of the same `importId`. Omit `importId` only for a
2782
+ * throwaway import where double-insertion is acceptable.
2783
+ *
2784
+ * CAVEAT — anonymous callers: the server scopes the key to the signed-in
2785
+ * user, or to the client's `clientId` when nobody is signed in. A
2786
+ * `LunoraClient` mints a fresh `clientId` on every construction, so an
2787
+ * anonymous import resumed after a reload is not deduped and inserts the
2788
+ * landed chunks again. Pass a persisted `clientId` to the constructor to
2789
+ * resume anonymously, or run the import signed in.
2790
+ */
2791
+ importId?: string;
2792
+ /**
2793
+ * Called after each chunk is accepted — for a progress bar. `done` counts
2794
+ * committed and queued rows alike; the result separates them.
2795
+ */
2796
+ onProgress?: (progress: {
2797
+ done: number;
2798
+ total: number;
2799
+ }) => void;
2800
+ /** Routes every chunk to one shard's DO. */
2801
+ shardKey?: string;
2802
+ /** Build the mutation args for one chunk. Defaults to `{ rows: chunk }`. */
2803
+ toArgs?: (chunk: ReadonlyArray<unknown>) => Record<string, unknown>;
2804
+ }): Promise<{
2805
+ chunks: number;
2806
+ imported: number;
2807
+ queued: number;
2808
+ }>;
2809
+ /**
2810
+ * Read the cross-shard request distribution for a `.shardBy(...)` table —
2811
+ * the feed the studio's `hot_shard` advisor lint consumes. Hits the
2812
+ * admin-gated `POST /_lunora/admin/shard-traffic` endpoint, which fans the
2813
+ * cheap per-shard `getMetrics` read out across every live shard and returns
2814
+ * each shard's `{ shardKey, requests }` total (a failed shard surfaces with
2815
+ * `requests: 0`). Requires the worker to be built with a `queryCoordinator`
2816
+ * and `adminToken`, and this client's auth token to match; defaults any
2817
+ * absent field so an older worker yields an empty-but-valid shape.
2818
+ */
2819
+ shardTraffic(table: string): Promise<ShardTrafficResult>;
2820
+ /**
2821
+ * List the functions queued via `runAfter` / `runAt`, soonest-due last
2822
+ * (the worker returns them in storage order). Hits the admin-gated
2823
+ * `/_lunora/admin/scheduled` endpoint, so the worker must be built with a
2824
+ * `schedulerDO` namespace and `adminToken`, and this client's auth token
2825
+ * must match. Powers `@lunora/studio`'s scheduled-jobs panel.
2826
+ */
2827
+ listScheduledJobs(): Promise<ScheduleRecord[]>;
2828
+ /**
2829
+ * Read the app-level workpool backlog that powers `@lunora/studio`'s SLO
2830
+ * view: per-pool `{ name, queued, inFlight, maxConcurrency }` plus the
2831
+ * app-wide `backlog` (total queued) and `inFlight` (total held slots) sums.
2832
+ * Hits the admin-gated `GET /_lunora/admin/scheduled/status` endpoint, so the
2833
+ * same preconditions as {@link listScheduledJobs} apply (a `schedulerDO`
2834
+ * namespace + `adminToken` on the worker and a matching auth token here).
2835
+ * Defaults any absent field so an older worker still yields a valid shape.
2836
+ */
2837
+ schedulerStatus(): Promise<SchedulerStatus>;
2838
+ /** Cancel a pending scheduled job by id. Returns whether a job was removed. */
2839
+ cancelScheduledJob(id: string): Promise<{
2840
+ cancelled: boolean;
2841
+ }>;
2842
+ /**
2843
+ * List the dead-letter jobs: schedules that exhausted their retry budget
2844
+ * and were parked instead of dropped. These never appear in
2845
+ * {@link listScheduledJobs} (their live header is gone), so this is the only
2846
+ * way the studio surfaces a permanently-failed job. Hits the admin-gated
2847
+ * `GET /_lunora/admin/scheduled/dead`; same preconditions as
2848
+ * {@link listScheduledJobs}. Powers `@lunora/studio`'s dead-letter panel.
2849
+ */
2850
+ listDeadJobs(): Promise<ScheduleRecord[]>;
2851
+ /**
2852
+ * Resurrect a dead-letter job by id: it re-enters the schedule with a fresh
2853
+ * retry budget and fires on the next drain. Returns whether a parked record
2854
+ * matched. Hits the admin-gated `POST /_lunora/admin/scheduled/dead/retry`.
2855
+ */
2856
+ retryDeadJob(id: string): Promise<{
2857
+ retried: boolean;
2858
+ }>;
2859
+ /**
2860
+ * Permanently drop a dead-letter job by id (the operator has decided not to
2861
+ * recover it). Returns whether a parked record was removed. Hits the
2862
+ * admin-gated `POST /_lunora/admin/scheduled/dead/cancel`.
2863
+ */
2864
+ removeDeadJob(id: string): Promise<{
2865
+ removed: boolean;
2866
+ }>;
2867
+ /**
2868
+ * List a workflow's instances via the admin Workflows proxy
2869
+ * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
2870
+ * the `Workflow` binding can't expose. Requires the worker to be built with a
2871
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
2872
+ * configured this does NOT reject: the proxy returns a `200 { configured:
2873
+ * false }` sentinel, so the result resolves with `configured === false` and an
2874
+ * empty `instances` list — callers should branch on that flag rather than
2875
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
2876
+ * `name` is the deployed workflow name.
2877
+ */
2878
+ listWorkflowInstances(options: {
2879
+ name: string;
2880
+ page?: number;
2881
+ perPage?: number;
2882
+ status?: WorkflowInstanceStatus;
2883
+ }): Promise<WorkflowInstancePage>;
2884
+ /** Read one workflow instance with its step timeline (`/_lunora/admin/workflows/instance`). */
2885
+ getWorkflowInstance(options: {
2886
+ id: string;
2887
+ name: string;
2888
+ }): Promise<WorkflowInstanceDetail>;
2889
+ /** Pause / resume / terminate a workflow instance (`/_lunora/admin/workflows/status`). Needs an Edit-scoped Cloudflare token. */
2890
+ setWorkflowInstanceStatus(options: {
2891
+ action: WorkflowInstanceAction;
2892
+ id: string;
2893
+ name: string;
2894
+ }): Promise<{
2895
+ status: WorkflowInstanceStatus;
2896
+ }>;
2897
+ /**
2898
+ * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
2899
+ * WebSocket. `onJobs` fires with the full list on connect and on every
2900
+ * change (schedule / cancel / alarm-fire). Reconnects with the client's
2901
+ * configured backoff. Requires `wsToken` to be set to an admin credential
2902
+ * (the browser can't send an `Authorization` header on a WS) — the master
2903
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
2904
+ * sub-token so the master credential stays out of the URL. Returns an
2905
+ * unsubscribe function that closes the socket and stops reconnecting.
2906
+ */
2907
+ subscribeScheduledJobs(onJobs: (jobs: ScheduleRecord[]) => void): Unsubscribe;
2908
+ /**
2909
+ * List the registered public functions (queries / mutations / actions) with
2910
+ * their kinds. Hits the admin-gated `GET /_lunora/admin/functions` endpoint —
2911
+ * the worker must be built with a `functions` registry and `adminToken`, and
2912
+ * this client's auth token must match. Powers `@lunora/studio`'s function
2913
+ * runner auto-discovery.
2914
+ */
2915
+ listFunctions(): Promise<FunctionDescriptor[]>;
2916
+ /**
2917
+ * List the code-defined cron triggers (the `cronJobs()` map injected on the
2918
+ * worker), each flattened to its firing `cron` expression. Hits the
2919
+ * admin-gated `GET /_lunora/admin/cron-jobs` endpoint — the worker must be
2920
+ * built with a `cronJobs` map and `adminToken`, and this client's auth token
2921
+ * must match. These are static (Cloudflare exposes no runtime cron
2922
+ * introspection), so the studio renders them read-only alongside the dynamic
2923
+ * scheduler jobs.
2924
+ */
2925
+ getCronJobs(): Promise<CronJobInfo[]>;
2926
+ /**
2927
+ * Manually fire one code-defined cron job by name — the same dispatch the
2928
+ * scheduled trigger runs (dispatch the function, or start the durable
2929
+ * workflow), on demand. Hits the admin-gated `POST /_lunora/admin/cron-jobs/run`
2930
+ * endpoint; the worker must be built with a `cronJobs` map and `adminToken`,
2931
+ * and this client's auth token must match. Resolves when the job has run (a
2932
+ * function job's shard response is 2xx, or the workflow instance was created)
2933
+ * and rejects with the dispatch error otherwise.
2934
+ */
2935
+ runCronJob(name: string): Promise<{
2936
+ name: string;
2937
+ ran: boolean;
2938
+ }>;
2939
+ /**
2940
+ * Fetch the generated OpenAPI 3.1 document. Hits the admin-gated
2941
+ * `GET /_lunora/admin/openapi` endpoint — the worker must be built with an
2942
+ * `openApiSpec` and `adminToken`, and this client's auth token must match.
2943
+ * Powers `@lunora/studio`'s API-reference (Scalar) view. When the worker has
2944
+ * no spec wired, the endpoint still resolves with an empty-but-valid OpenAPI
2945
+ * document (no `paths`), so callers can render a "not configured" state.
2946
+ */
2947
+ fetchOpenApi(): Promise<Record<string, unknown>>;
2948
+ /**
2949
+ * Fetch the generated architecture manifest (module catalog + static call
2950
+ * graph). Hits the admin-gated `GET /_lunora/admin/architecture` endpoint.
2951
+ * Codegen emits the manifest once the app declares a module
2952
+ * (`lunora/<dir>/module.ts`); until then the endpoint answers an empty one
2953
+ * (no modules, nodes or edges). Powers `@lunora/studio`'s Architecture view.
2954
+ */
2955
+ fetchArchitecture(): Promise<Record<string, unknown>>;
2956
+ /**
2957
+ * Fetch the generated OpenRPC 1.x document. Hits the admin-gated
2958
+ * `GET /_lunora/admin/openrpc` endpoint — the worker must be built with an
2959
+ * `openRpcSpec` and `adminToken`, and this client's auth token must match.
2960
+ * OpenRPC is the RPC-native spec (a `methods` array over the JSON-RPC-shaped
2961
+ * `POST /_lunora/rpc` transport); it documents the RPC functions only.
2962
+ * Powers `@lunora/studio`'s OpenRPC API-reference view. When the worker has
2963
+ * no spec wired, the endpoint still resolves with an empty-but-valid OpenRPC
2964
+ * document (no `methods`), so callers can render a "not configured" state.
2965
+ */
2966
+ fetchOpenRpc(): Promise<Record<string, unknown>>;
2967
+ /**
2968
+ * List objects in the storage bucket, optionally under a `prefix` and from a
2969
+ * pagination `cursor`. Hits the admin-gated `GET /_lunora/admin/storage`
2970
+ * endpoint — the worker must be built with a `storageList` function and
2971
+ * `adminToken`, and this client's auth token must match. Powers
2972
+ * `@lunora/studio`'s file browser.
2973
+ */
2974
+ listStorageObjects(options?: {
2975
+ bucket?: string;
2976
+ cursor?: string;
2977
+ limit?: number;
2978
+ prefix?: string;
2979
+ }): Promise<StorageListPage>;
2980
+ /**
2981
+ * Delete one object from the storage bucket by key. Hits the admin-gated
2982
+ * `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
2983
+ * with a `storageDelete` function and `adminToken`. Powers the studio file
2984
+ * browser's per-row delete; resolves `{ deleted, key }`.
2985
+ *
2986
+ * An absent `deleted` field reads as `false`, matching every sibling admin
2987
+ * verb (`runCronJob`'s `ran`, …): the studio renders this value as the row's
2988
+ * outcome, so defaulting a missing field to success would report a delete
2989
+ * that a mismatched/older worker never performed.
2990
+ */
2991
+ deleteStorageObject(key: string, options?: {
2992
+ bucket?: string;
2993
+ }): Promise<{
2994
+ deleted: boolean;
2995
+ key: string;
2996
+ }>;
2997
+ /**
2998
+ * List the storage bucket names the worker exposes, for the studio file
2999
+ * browser's bucket picker. Hits the admin-gated
3000
+ * `GET /_lunora/admin/storage/buckets` endpoint — always resolves (an empty
3001
+ * array when the worker configures no `storageBuckets`, i.e. single-bucket).
3002
+ */
3003
+ listStorageBuckets(): Promise<string[]>;
3004
+ /**
3005
+ * Upload one object to the storage bucket. Hits the admin-gated
3006
+ * `PUT /_lunora/admin/storage?key=…` endpoint with the raw body and an
3007
+ * optional `contentType` header — the worker must be built with a
3008
+ * `storageUpload` function and `adminToken`. Powers the studio file
3009
+ * browser's upload control; resolves `{ etag?, key }`.
3010
+ */
3011
+ uploadStorageObject(options: {
3012
+ body: ArrayBuffer | Blob;
3013
+ bucket?: string;
3014
+ contentType?: string;
3015
+ key: string;
3016
+ }): Promise<{
3017
+ etag?: string;
3018
+ key: string;
3019
+ }>;
3020
+ /**
3021
+ * Build a (signed or public) URL for one object. Hits the admin-gated
3022
+ * `GET /_lunora/admin/storage/url?key=…` endpoint — the worker must be built
3023
+ * with a `storageSignedUrl` function and `adminToken`. Powers the studio
3024
+ * file browser's copy-URL action; resolves the URL string.
3025
+ *
3026
+ * `options.expiresInSeconds` requests a share-link lifetime, which is
3027
+ * validated/clamped server-side. The options object mirrors the worker's
3028
+ * `StorageSignedUrlFunction` options (a `password` / download-limit are noted
3029
+ * as future fields there).
3030
+ */
3031
+ signedStorageUrl(key: string, options?: {
3032
+ bucket?: string;
3033
+ expiresInSeconds?: number;
3034
+ }): Promise<string>;
3035
+ /**
3036
+ * List the `.global()` (D1-backed) tables with their row counts. Hits the
3037
+ * admin-gated `GET /_lunora/admin/global/tables` endpoint — the worker must
3038
+ * be built with a `globalIntrospector` and `adminToken`. Powers the data
3039
+ * browser's global mode.
3040
+ */
3041
+ listGlobalTables(): Promise<GlobalTableInfo[]>;
3042
+ /**
3043
+ * Read a page of rows from one `.global()` table. `filters` AND-narrows the
3044
+ * page to rows matching each `column = value` eq constraint — the drill-down a
3045
+ * facet-value click applies; the array is wire-encoded, JSON-encoded into the
3046
+ * `filters` query param, and the values are bound server-side.
3047
+ *
3048
+ * The response is `decodeWire`d. The worker encodes it (`readGlobalTablePage`
3049
+ * in `@lunora/d1`) because JSON cannot carry a `v.bigint()` column at all and
3050
+ * silently flattens a `v.bytes()` one to `{}` — so without the decode here the
3051
+ * grid renders the raw 3-element tagged array instead of the value. The shard
3052
+ * browser's twin already pairs the same way through `rpc`; this is the global
3053
+ * half of that symmetry.
3054
+ */
3055
+ readGlobalTablePage(options: {
3056
+ filters?: GlobalFilterClause[];
3057
+ limit?: number;
3058
+ offset?: number;
3059
+ table: string;
3060
+ }): Promise<GlobalTablePage>;
3061
+ /**
3062
+ * Summarise the distinct values of one column in a `.global()` table over the
3063
+ * active view (the same eq `filters` the browser is previewing) — the global
3064
+ * twin of the shard browser's facet. Hits the admin-gated
3065
+ * `GET /_lunora/admin/global/facet` endpoint; `column` is validated + bound
3066
+ * server-side. Powers the global data browser's facet sidebar.
3067
+ *
3068
+ * Wire-encoded/decoded on both legs for the same reason
3069
+ * {@link LunoraClient.readGlobalTablePage} is: a facet over a BLOB column
3070
+ * returns bytes, which `Response.json` flattens to `{}` — and since a facet
3071
+ * value is exactly what a click sends back as a `filters` clause, that is a
3072
+ * broken drill-down rather than a display glitch.
3073
+ */
3074
+ facetGlobalColumn(options: {
3075
+ column: string;
3076
+ filters?: GlobalFilterClause[];
3077
+ limit?: number;
3078
+ table: string;
3079
+ }): Promise<GlobalFacetResult>;
3080
+ /**
3081
+ * List the schema's Vectorize indexes with their declared shape (table,
3082
+ * field, dimensions, metric, metadata) and live stats (vector count,
3083
+ * processing watermark) when the binding is reachable. Hits the admin-gated
3084
+ * `GET /_lunora/admin/vector/indexes` endpoint — the worker must be built
3085
+ * with a `vectorIntrospector` and `adminToken`. Powers the studio's vector
3086
+ * browser. Vectorize can't enumerate indexes at runtime, so this list comes
3087
+ * from the generated `LUNORA_VECTOR_INDEXES` registry.
3088
+ */
3089
+ listVectorIndexes(): Promise<VectorIndexSummary[]>;
3090
+ /**
3091
+ * Run a nearest-neighbour similarity query against one vector index: the
3092
+ * worker embeds `text` via the index's embedder and returns the top matches.
3093
+ * Hits the admin-gated `POST /_lunora/admin/vector/query` endpoint. Throws
3094
+ * `VECTOR_QUERY_UNSUPPORTED` when the worker's introspector has no embedder
3095
+ * wired (the index lists read-only).
3096
+ */
3097
+ queryVectorIndex(options: {
3098
+ name: string;
3099
+ text: string;
3100
+ topK?: number;
3101
+ }): Promise<VectorQueryMatch[]>;
3102
+ /**
3103
+ * Read one keyset-paginated page of the durable `ctx.log` archive that
3104
+ * `pipelineLogSink` writes to R2. Server-side only — the worker holds the R2
3105
+ * SQL credentials and runs the reader; the browser only sees the decoded
3106
+ * `{ rows, nextCursor }`. Pass the previous page's `nextCursor` as
3107
+ * `query.cursor` to page. Admin-gated. When the operator hasn't wired the
3108
+ * archive, `adminFetch` throws a `LunoraClientError` with `.code ===
3109
+ * "LOG_ARCHIVE_NOT_CONFIGURED"`, so a caller can render a "not configured"
3110
+ * state rather than an error.
3111
+ */
3112
+ queryLogArchive(query?: PipelineLogQuery): Promise<PipelineLogPage>;
3113
+ /**
3114
+ * List the worker's registered Workers KV namespaces (binding names). Hits
3115
+ * the admin-gated `GET /_lunora/admin/kv/namespaces` endpoint — the worker
3116
+ * must be built with a `kvIntrospector` and `adminToken`. Powers the
3117
+ * studio's KV browser.
3118
+ */
3119
+ listKvNamespaces(): Promise<KvNamespaceSummary[]>;
3120
+ /**
3121
+ * List keys in a KV namespace, optionally filtered by `prefix` and
3122
+ * paginated via `cursor`. Hits the admin-gated
3123
+ * `GET /_lunora/admin/kv/keys` endpoint.
3124
+ */
3125
+ listKvKeys(options: {
3126
+ cursor?: string;
3127
+ limit?: number;
3128
+ namespace: string;
3129
+ prefix?: string;
3130
+ }): Promise<KvKeyListResult>;
3131
+ /**
3132
+ * Read a KV value (as text) and its metadata. Hits the admin-gated
3133
+ * `GET /_lunora/admin/kv/value` endpoint. Returns `{ value: null, metadata: null }`
3134
+ * when the key is absent.
3135
+ */
3136
+ getKvValue(options: {
3137
+ key: string;
3138
+ namespace: string;
3139
+ }): Promise<KvValueResult>;
3140
+ /**
3141
+ * Write a string value to a KV namespace. Accepts an absolute `expiration`
3142
+ * (Unix seconds) or a relative `expirationTtl`, plus optional `metadata` —
3143
+ * re-send the loaded values on edit so a save preserves rather than clears
3144
+ * them. Hits the admin-gated `PUT /_lunora/admin/kv/value` endpoint.
3145
+ */
3146
+ putKvValue(options: {
3147
+ expiration?: number;
3148
+ expirationTtl?: number;
3149
+ key: string;
3150
+ metadata?: unknown;
3151
+ namespace: string;
3152
+ value: string;
3153
+ }): Promise<void>;
3154
+ /**
3155
+ * Delete a key from a KV namespace. No-op when the key is absent. Hits the
3156
+ * admin-gated `DELETE /_lunora/admin/kv/value` endpoint.
3157
+ */
3158
+ deleteKvKey(options: {
3159
+ key: string;
3160
+ namespace: string;
3161
+ }): Promise<void>;
3162
+ /**
3163
+ * List authenticated users, paged and optionally searched / filtered / sorted.
3164
+ * Hits the admin-gated `GET /_lunora/admin/auth/users` endpoint — the worker
3165
+ * must be built with an `authAdmin` and `adminToken`. Powers the studio's
3166
+ * users dashboard.
3167
+ */
3168
+ listAuthUsers(options?: {
3169
+ filterField?: string;
3170
+ filterValue?: string;
3171
+ limit?: number;
3172
+ offset?: number;
3173
+ search?: string;
3174
+ searchField?: string;
3175
+ sortBy?: string;
3176
+ sortDirection?: "asc" | "desc";
3177
+ }): Promise<AuthPage<AuthUser>>;
3178
+ /**
3179
+ * Create a user. Hits the admin-gated `POST /_lunora/admin/auth/users/create`
3180
+ * endpoint (requires the worker's `authAdmin` to implement `createUser`).
3181
+ * `data` carries any app-defined `user.additionalFields`.
3182
+ */
3183
+ createAuthUser(input: {
3184
+ data?: Record<string, unknown>;
3185
+ email: string;
3186
+ name: string;
3187
+ password?: string;
3188
+ role?: string | string[];
3189
+ }): Promise<AuthUser>;
3190
+ /** Set a user's role (string, or array joined comma-wise server-side). */
3191
+ setAuthUserRole(input: {
3192
+ role: string | string[];
3193
+ userId: string;
3194
+ }): Promise<AuthUser>;
3195
+ /** Ban a user. `expiresInSeconds` sets a temporary ban; omit it for a permanent one. Revokes the user's live sessions. */
3196
+ banAuthUser(input: {
3197
+ expiresInSeconds?: number;
3198
+ reason?: string;
3199
+ userId: string;
3200
+ }): Promise<AuthUser>;
3201
+ /** Lift a user's ban. */
3202
+ unbanAuthUser(input: {
3203
+ userId: string;
3204
+ }): Promise<AuthUser>;
3205
+ /** Set a user's password (admin override — no current-password challenge). */
3206
+ setAuthUserPassword(input: {
3207
+ newPassword: string;
3208
+ userId: string;
3209
+ }): Promise<void>;
3210
+ /** Permanently delete a user and revoke their sessions. */
3211
+ removeAuthUser(input: {
3212
+ userId: string;
3213
+ }): Promise<void>;
3214
+ /**
3215
+ * Mint an impersonation session for a user, returning its bearer `token`.
3216
+ * The caller is responsible for using the token (e.g. setting the session
3217
+ * cookie); the server performs no cookie round-trip.
3218
+ */
3219
+ impersonateAuthUser(input: {
3220
+ userId: string;
3221
+ }): Promise<AuthImpersonation>;
3222
+ /** Revoke a single session by its id (force sign-out of one device). */
3223
+ revokeAuthSession(input: {
3224
+ sessionId: string;
3225
+ }): Promise<void>;
3226
+ /** Revoke every session for a user (force sign-out everywhere). */
3227
+ revokeAuthUserSessions(input: {
3228
+ userId: string;
3229
+ }): Promise<void>;
3230
+ /**
3231
+ * Report which auth dashboard surfaces are available — derived server-side
3232
+ * from the enabled better-auth plugins. The studio renders only the panels
3233
+ * whose capability is `true`.
3234
+ */
3235
+ getAuthCapabilities(): Promise<AuthCapabilities>;
3236
+ /** Update a user's fields (name/email/app-defined `additionalFields`). */
3237
+ updateAuthUser(input: {
3238
+ data: Record<string, unknown>;
3239
+ userId: string;
3240
+ }): Promise<AuthUser>;
3241
+ /** List a user's linked accounts (credential / OAuth providers). Token material is stripped server-side. */
3242
+ listAuthAccounts(input: {
3243
+ userId: string;
3244
+ }): Promise<Record<string, unknown>[]>;
3245
+ /** Unlink a linked account from a user. */
3246
+ unlinkAuthAccount(input: {
3247
+ accountId: string;
3248
+ userId: string;
3249
+ }): Promise<void>;
3250
+ /** List a user's registered passkeys (requires the passkey plugin). */
3251
+ listAuthPasskeys(input: {
3252
+ userId: string;
3253
+ }): Promise<Record<string, unknown>[]>;
3254
+ /** Delete a passkey by id (requires the passkey plugin). */
3255
+ deleteAuthPasskey(input: {
3256
+ passkeyId: string;
3257
+ }): Promise<void>;
3258
+ /** Disable two-factor auth for a user (requires the two-factor plugin). */
3259
+ disableAuthTwoFactor(input: {
3260
+ userId: string;
3261
+ }): Promise<void>;
3262
+ /** List organizations, paged (requires the organization plugin). */
3263
+ listAuthOrganizations(options?: {
3264
+ limit?: number;
3265
+ offset?: number;
3266
+ }): Promise<AuthPage<Record<string, unknown>>>;
3267
+ /** List the members of an organization (requires the organization plugin). */
3268
+ listAuthOrgMembers(input: {
3269
+ limit?: number;
3270
+ offset?: number;
3271
+ organizationId: string;
3272
+ }): Promise<AuthPage<Record<string, unknown>>>;
3273
+ /** List an organization's pending invitations (requires the organization plugin). */
3274
+ listAuthOrgInvitations(input: {
3275
+ limit?: number;
3276
+ offset?: number;
3277
+ organizationId: string;
3278
+ }): Promise<AuthPage<Record<string, unknown>>>;
3279
+ /**
3280
+ * List sign-up invitations, newest first (requires the `inviteOnly` plugin).
3281
+ * Unfiltered: a row is pending when `acceptedAt` is null and `expiresAt` is in
3282
+ * the future, and the caller labels it — filtering server-side after the page
3283
+ * would let page 1 come back empty while pending rows sat on page 2.
3284
+ */
3285
+ listAuthSignUpInvitations(options?: {
3286
+ limit?: number;
3287
+ offset?: number;
3288
+ }): Promise<AuthPage<Record<string, unknown>>>;
3289
+ /** Invite an address to sign up, or refresh an existing invitation for it. */
3290
+ createAuthSignUpInvitation(input: {
3291
+ email: string;
3292
+ expiresInSeconds?: number;
3293
+ invitedBy?: string;
3294
+ }): Promise<Record<string, unknown>>;
3295
+ /** Withdraw a sign-up invitation. Not retroactive — an account already created keeps existing. */
3296
+ revokeAuthSignUpInvitation(input: {
3297
+ email: string;
3298
+ }): Promise<void>;
3299
+ /** Remove a member from an organization. */
3300
+ removeAuthOrgMember(input: {
3301
+ memberId: string;
3302
+ }): Promise<void>;
3303
+ /** Cancel a pending organization invitation. */
3304
+ cancelAuthOrgInvitation(input: {
3305
+ invitationId: string;
3306
+ }): Promise<void>;
3307
+ /**
3308
+ * Report the deployment's auth configuration — enabled plugins, sign-in
3309
+ * methods, user-settable create-user fields, organization sub-features
3310
+ * (teams / roles), and session / rate-limit policy. Drives the config panel
3311
+ * and the dynamic create-user form. Never carries a secret.
3312
+ */
3313
+ getAuthConfig(): Promise<AuthConfigInfo>;
3314
+ /** Create an organization; optionally seed an `owner` member for `ownerId`. */
3315
+ createAuthOrganization(input: {
3316
+ logo?: string;
3317
+ metadata?: Record<string, unknown>;
3318
+ name: string;
3319
+ ownerId?: string;
3320
+ slug?: string;
3321
+ }): Promise<Record<string, unknown>>;
3322
+ /** Update an organization's name/slug/logo/metadata. */
3323
+ updateAuthOrganization(input: {
3324
+ logo?: string;
3325
+ metadata?: Record<string, unknown>;
3326
+ name?: string;
3327
+ organizationId: string;
3328
+ slug?: string;
3329
+ }): Promise<Record<string, unknown>>;
3330
+ /** Delete an organization and cascade its members, invitations, teams, and custom roles. */
3331
+ deleteAuthOrganization(input: {
3332
+ organizationId: string;
3333
+ }): Promise<void>;
3334
+ /** Directly add an existing user to an organization (no invitation/acceptance). */
3335
+ addAuthOrgMember(input: {
3336
+ organizationId: string;
3337
+ role?: string;
3338
+ userId: string;
3339
+ }): Promise<Record<string, unknown>>;
3340
+ /** Create a pending email invitation to an organization. */
3341
+ inviteAuthOrgMember(input: {
3342
+ email: string;
3343
+ inviterId?: string;
3344
+ organizationId: string;
3345
+ role?: string;
3346
+ }): Promise<Record<string, unknown>>;
3347
+ /** Change a member's role. */
3348
+ setAuthOrgMemberRole(input: {
3349
+ memberId: string;
3350
+ role: string | string[];
3351
+ }): Promise<Record<string, unknown>>;
3352
+ /** List an organization's teams (requires the organization plugin with teams enabled). */
3353
+ listAuthOrgTeams(input: {
3354
+ limit?: number;
3355
+ offset?: number;
3356
+ organizationId: string;
3357
+ }): Promise<AuthPage<Record<string, unknown>>>;
3358
+ /** Create a team under an organization. */
3359
+ createAuthOrgTeam(input: {
3360
+ name: string;
3361
+ organizationId: string;
3362
+ }): Promise<Record<string, unknown>>;
3363
+ /** Rename a team. */
3364
+ updateAuthOrgTeam(input: {
3365
+ name: string;
3366
+ teamId: string;
3367
+ }): Promise<Record<string, unknown>>;
3368
+ /** Delete a team and its memberships. */
3369
+ removeAuthOrgTeam(input: {
3370
+ teamId: string;
3371
+ }): Promise<void>;
3372
+ /** List a team's members. */
3373
+ listAuthOrgTeamMembers(input: {
3374
+ limit?: number;
3375
+ offset?: number;
3376
+ teamId: string;
3377
+ }): Promise<AuthPage<Record<string, unknown>>>;
3378
+ /** Add a user to a team. */
3379
+ addAuthOrgTeamMember(input: {
3380
+ teamId: string;
3381
+ userId: string;
3382
+ }): Promise<Record<string, unknown>>;
3383
+ /** Remove a member from a team. */
3384
+ removeAuthOrgTeamMember(input: {
3385
+ teamMemberId: string;
3386
+ }): Promise<void>;
3387
+ /** List an organization's custom roles (requires the organization plugin with dynamic access control). */
3388
+ listAuthOrgRoles(input: {
3389
+ limit?: number;
3390
+ offset?: number;
3391
+ organizationId: string;
3392
+ }): Promise<AuthPage<Record<string, unknown>>>;
3393
+ /** Create a custom org role with a permission grant (a `resource -> actions[]` map). */
3394
+ createAuthOrgRole(input: {
3395
+ organizationId: string;
3396
+ permission: Record<string, string[]>;
3397
+ role: string;
3398
+ }): Promise<Record<string, unknown>>;
3399
+ /** Replace a custom org role's permission grant. */
3400
+ updateAuthOrgRole(input: {
3401
+ permission: Record<string, string[]>;
3402
+ roleId: string;
3403
+ }): Promise<Record<string, unknown>>;
3404
+ /** Delete a custom org role. */
3405
+ deleteAuthOrgRole(input: {
3406
+ roleId: string;
3407
+ }): Promise<void>;
3408
+ /** List auth sessions, paged and optionally filtered to one user. */
3409
+ listAuthSessions(options?: {
3410
+ limit?: number;
3411
+ offset?: number;
3412
+ userId?: string;
3413
+ }): Promise<AuthPage<AuthSession>>;
3414
+ /**
3415
+ * Subscribe to a live query. The callback fires with the current value (from
3416
+ * the durable read cache, when one is hydrated) and again on every server
3417
+ * frame; the returned function unsubscribes.
3418
+ *
3419
+ * Subscriptions are deduped by `(functionPath, args, shardKey)` — a second
3420
+ * `subscribe` for the same triple joins the existing registration and shares
3421
+ * its value, cursor and optimistic layers.
3422
+ *
3423
+ * Available on a `crossTabSync` FOLLOWER tab, unlike the other socket-backed
3424
+ * surfaces: registering here is exactly what lets the cross-tab relay deliver
3425
+ * the leader's broadcasts (see the note in the body). The caveat is that the
3426
+ * channel only carries the LEADER's own subscriptions outward, so a follower
3427
+ * sees this query's frames only while the leader holds it too — see
3428
+ * {@link LunoraClientOptions.crossTabSync}.
3429
+ */
3430
+ subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
3431
+ onCheckpoint?: (watermark: SyncWatermark) => void;
3432
+ onError?: SubscriptionErrorCallback;
3433
+ shardKey?: string;
3434
+ }): Unsubscribe;
3435
+ /**
3436
+ * Subscribe to a declarative **shape** — server-side partial replication
3437
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
3438
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
3439
+ * validated `args` (never a `where` the client could forge), the server seeds
3440
+ * the current membership as an insert-poke and streams live membership diffs.
3441
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
3442
+ *
3443
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
3444
+ * (name, args): the server resolves them under the socket's verified identity,
3445
+ * so every call gets its own id + view. The returned function unsubscribes.
3446
+ *
3447
+ * Not available on a `crossTabSync` FOLLOWER tab: shape pokes are not part of
3448
+ * the leader→follower broadcast set, so a follower's shape could never
3449
+ * resolve. The returned handle is inert there, and `options.onError` is
3450
+ * invoked with `NOT_IMPLEMENTED` — see
3451
+ * {@link LunoraClientOptions.crossTabSync}. Reporting rather than throwing is
3452
+ * deliberate: `@lunora/db`'s shape-backed `createCollection` calls this from
3453
+ * its sync path, where a throw takes the collection out entirely, while
3454
+ * `onError` is the seam it already routes to `markReady()`.
3455
+ */
3456
+ subscribeShape(shape: {
3457
+ args?: Record<string, unknown>;
3458
+ name: string;
3459
+ }, callback: ShapeCallback, options?: {
3460
+ onCheckpoint?: (watermark: SyncWatermark) => void;
3461
+ onError?: SubscriptionErrorCallback;
3462
+ shardKey?: string;
3463
+ }): Unsubscribe;
3464
+ /**
3465
+ * Open a streaming query. The function reference must be a
3466
+ * `kind:"stream"` registration (built with `c.query.input(...).stream(...)`);
3467
+ * the type constraint catches accidental use of a query/mutation/action
3468
+ * reference at compile time. The returned iterable yields one element per
3469
+ * chunk frame the server pushes, terminating when the server sends
3470
+ * `complete` or the consumer calls `.cancel()`. Errors arrive as a
3471
+ * rejection on the next `next()`.
3472
+ *
3473
+ * Streams ride the same WS as subscriptions and share the unsubscribe
3474
+ * channel: cancelling sends `{type:"unsubscribe", id}` with the stream id on
3475
+ * the socket the stream runs over. For an ephemeral stream the DO aborts the
3476
+ * in-flight iterator. For a `durable` stream, cancelling detaches this
3477
+ * consumer and the run keeps going: it finishes and stays readable for the
3478
+ * next attach. A cancel made while the socket is down is dropped rather
3479
+ * than queued, because the server already detached every consumer on that
3480
+ * socket when it closed.
3481
+ *
3482
+ * Stream-start frames buffered while the socket is (re)connecting are
3483
+ * capped at {@link MAX_PENDING_STREAMS} per connection — overflowing the
3484
+ * cap drops the oldest queued frame (and fails its consumer) so a stuck
3485
+ * reconnect can't OOM the page.
3486
+ */
3487
+ stream<F extends FunctionReference<"stream">>(function_: F, args: ArgsOf<F>, options?: {
3488
+ durable?: boolean;
3489
+ maxBuffer?: number;
3490
+ shardKey?: string;
3491
+ }): StreamIterable<ReturnOf<F>>;
3492
+ /**
3493
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.<verb>(path).stream()`).
3494
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
3495
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
3496
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
3497
+ * chunks, a final `event: complete`, an `event: error` on throw).
3498
+ *
3499
+ * The reference comes from the generated `httpStreams.*` registry, so the
3500
+ * yielded chunk type is the route handler's yielded type. Cancelling the
3501
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
3502
+ * the server handler observes via its `signal`. The client's bearer token
3503
+ * (when set) rides as an `authorization` header.
3504
+ */
3505
+ httpStream<Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: {
3506
+ headers?: Record<string, string>;
3507
+ maxBuffer?: number;
3508
+ signal?: AbortSignal;
3509
+ }): StreamIterable<HttpStreamChunkOf<Ref>>;
3510
+ close(): void;
3511
+ /** Guard shared by every public entry point: a closed client accepts no further calls. */
3512
+ private assertOpen;
3513
+ /**
3514
+ * `true` when this tab is a cross-tab FOLLOWER of a live leader — i.e. it
3515
+ * will not open a socket of its own and another tab is known to hold one.
3516
+ *
3517
+ * Deliberately NOT just `!isLeader()`. Every `crossTabSync` client is a
3518
+ * non-leader for the first `leaderTimeout` of its life, while its
3519
+ * claim-leadership probe is outstanding; a lone tab self-promotes at the end
3520
+ * of that window and `onBecomeLeader` opens the sockets and replays every
3521
+ * registered subscription. That window is a legitimate, self-healing defer,
3522
+ * not a failure. A KNOWN leader on another tab is the state that never heals.
3523
+ */
3524
+ private followsAnotherTab;
3525
+ /**
3526
+ * Reject a call that needs a socket this tab will never have.
3527
+ *
3528
+ * The cross-tab protocol is one-directional: a leader broadcasts
3529
+ * `subscription-data` / `-error` / `-settled` / `connection-status` to
3530
+ * followers, and a follower has no frame with which to tell the leader what
3531
+ * it needs (see `cross-tab.ts`'s `WsFollowerMessage`, which is exactly
3532
+ * heartbeat / claim-leadership / yield-leadership). `subscribeShape` /
3533
+ * `whisper*` / `setConnectionContext` / `acquireConnectionContext` — none of
3534
+ * which the leader broadcasts at all — therefore never worked on a follower
3535
+ * under any circumstances: each returned a handle that looked live, fired no
3536
+ * callback, raised no error, and reported `connectionStatus() ===
3537
+ * "connected"` (mirrored from the leader).
3538
+ *
3539
+ * `subscribe` is deliberately NOT in that set. A follower's `subscribe`
3540
+ * registers the key the leader's broadcast is matched against, so it is the
3541
+ * mechanism the relay is built on rather than a surface that silently fails.
3542
+ * A follower sees a query only while the leader holds the same
3543
+ * `(fn, args, shardKey)` — that is the documented shape of the option, not a
3544
+ * defect.
3545
+ *
3546
+ * `subscribeShape` and `stream()` reach the same outcome by a different
3547
+ * route: they fail the handle they return rather than throwing at the call,
3548
+ * because both are driven from a sync/render path where a throw destroys the
3549
+ * caller instead of degrading it. See
3550
+ * {@link LunoraClientOptions.crossTabSync} for what the option does and does
3551
+ * not cover.
3552
+ */
3553
+ private assertLeaderOwnedSurface;
3554
+ /**
3555
+ * Clear every timer a {@link ShardConnection} can have armed.
3556
+ *
3557
+ * One function because both teardown paths must clear all three and a fourth
3558
+ * timer would otherwise have to be remembered in two places — which is how a
3559
+ * leak gets added rather than written.
3560
+ */
3561
+ private clearConnectionTimers;
3562
+ /**
3563
+ * Whether anything still needs `key`'s socket: a live or shape
3564
+ * subscription, a stream, a whisper topic, a connection context (presence
3565
+ * rides its `connect` envelope), or a write queued for or flushing to it.
3566
+ */
3567
+ private shardInUse;
3568
+ /**
3569
+ * Close a non-default shard's socket once nothing uses it, after
3570
+ * {@link IDLE_SHARD_CLOSE_MS} so a quick leave-and-return reuses it. The
3571
+ * connection record is dropped; the shard is remembered in
3572
+ * {@link idleClosedShards}, so writes to it keep the gating a connected shard
3573
+ * gets — sent over HTTP, and queued if the network is down (a queued write
3574
+ * reopens the socket to flush). A later subscription reconnects it.
3575
+ * Every release path calls this; the timer re-checks, so a shard picked up
3576
+ * again in the meantime stays open.
3577
+ */
3578
+ private releaseIdleShard;
3579
+ /** Forget a finished or cancelled stream; it may have been what held its shard open. */
3580
+ private forgetStream;
3581
+ /**
3582
+ * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
3583
+ * connect timers, stop its heartbeat, and close its socket (if any).
3584
+ * Shared by `close()` (terminal) and the cross-tab `onStopBeingLeader`
3585
+ * handler (demoted, but still alive) so a demoted leader can't leak a
3586
+ * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
3587
+ * an inline `conn.socket?.close()` — which skips both — used to.
3588
+ *
3589
+ * Settles this shard's in-flight streams first. The teardown clears
3590
+ * `conn.socket` BEFORE the real `close` event fires, so that event trips
3591
+ * `openManagedSocket`'s identity guard (`conn.socket !== socket`) and
3592
+ * returns — meaning `handleDisconnect`, the only other place that settles a
3593
+ * shard's streams, never runs for this connection again. `close()` already
3594
+ * failed and cleared `this.streams` before it gets here, so this is a no-op
3595
+ * on that path; the cross-tab demotion path is the one where a consumer's
3596
+ * `for await` used to block forever with no error and no completion.
3597
+ */
3598
+ private teardownConnection;
3599
+ /**
3600
+ * Build (but do not start) this client's `TabCoordinator`. Extracted out of
3601
+ * the constructor so `setAuthToken` can rebuild it on an identity change —
3602
+ * the default channel name embeds the identity fingerprint (see below), so
3603
+ * a new identity needs a new coordinator on a new channel. The callback
3604
+ * bodies are the drift-sensitive region (a hand-merged identity guard on
3605
+ * the shard message listener sits ahead of an extracted `lastFrameAt`
3606
+ * stamp elsewhere in this file) — moved verbatim, not reflowed.
3607
+ */
3608
+ private createTabCoordinator;
3609
+ /** The body of {@link batch}, optionally abortable (the polling fallback's timeout). */
3610
+ private sendBatch;
3611
+ /**
3612
+ * The body of {@link mutation}, also saying whether the write committed. `committed` is
3613
+ * `false` only when a durable outbox took the write, which resolves at once
3614
+ * with no server result.
3615
+ */
3616
+ private runMutation;
3617
+ /**
3618
+ * Unwind a direct write's optimistic layers after its RPC threw — or keep
3619
+ * them, when it threw only because a COMMITTED result did not decode: the
3620
+ * write happened, so its predicted value stays until the confirming frame
3621
+ * supersedes it.
3622
+ */
3623
+ private settleFailedDirectWrite;
3624
+ /**
3625
+ * Persist a mutation that can't go out on the wire right now (offline, or
3626
+ * mid-reconnect after a prior connect). The optimistic update has already
3627
+ * been applied by `mutation`; this only chooses the durable write path and
3628
+ * rolls the optimistic write back if persistence is rejected.
3629
+ *
3630
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
3631
+ * owns persistence + at-least-once replay, so we delegate and return
3632
+ * optimistically (confirmation rides the synced view). Otherwise the
3633
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
3634
+ */
3635
+ private enqueueOfflineMutation;
3636
+ /**
3637
+ * Is a write for this shard still sitting in the durable path, waiting to
3638
+ * replay? Drives the FIFO gate in {@link mutation}: a live write must never
3639
+ * overtake one that is queued (or held) ahead of it.
3640
+ *
3641
+ * Whichever durable path is wired answers: the built-in {@link OfflineQueue}
3642
+ * knows its entries' shard keys, while an {@link OutboxSink} reports only a
3643
+ * process-wide depth (the executor's pending count is not shard-scoped) —
3644
+ * over-inclusion there costs a write a trip through the outbox it could have
3645
+ * skipped, which is the same trip the writes ahead of it are taking anyway.
3646
+ * A sink that reports nothing keeps the previous behaviour.
3647
+ */
3648
+ private hasPendingWriteAhead;
3649
+ /**
3650
+ * Restore offline mutations persisted in a prior session and open a socket
3651
+ * for each shard they target so they flush once the WS reconnects. Failures
3652
+ * are swallowed — a broken durable store must not stop the client booting.
3653
+ */
3654
+ private hydratePersistedQueue;
3655
+ /**
3656
+ * Re-queue the durable offline writes — but only as the multi-tab LEADER. The
3657
+ * persisted queue is shared across a profile's tabs; without coordination
3658
+ * every tab would re-queue and replay the same writes (correct only because
3659
+ * the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
3660
+ * exactly one tab hydrate; it holds the lock for its lifetime, so when it
3661
+ * closes another tab acquires the lock and takes over. Falls back to
3662
+ * unconditional hydration where Web Locks are unavailable (React Native, older
3663
+ * browsers, SSR) — single-context there, so no coordination is needed.
3664
+ */
3665
+ private hydrateAsOutboxLeader;
3666
+ /**
3667
+ * Load every cached query so a `subscribe()` for the key seeds its initial
3668
+ * value off disk.
3669
+ *
3670
+ * The load is asynchronous but every framework adapter subscribes
3671
+ * SYNCHRONOUSLY at mount, so the subscriptions that most want the cache
3672
+ * already exist by the time it lands. Those are seeded here and their entry
3673
+ * is dropped: an entry that stayed in {@link hydratedQueryCache} behind a
3674
+ * live subscription would be consumed by the NEXT subscribe of the same key
3675
+ * (a remount after navigating away) and replay the previous session's value
3676
+ * over whatever the socket had since delivered. A cache entry therefore
3677
+ * never outlives a live subscription for its key — it is either handed to
3678
+ * that subscription or discarded.
3679
+ *
3680
+ * Only keys with no live subscription are held for a later `subscribe()`;
3681
+ * the identity gate applies to both paths, so a signed-out cache never leaks
3682
+ * into a new session.
3683
+ */
3684
+ private hydrateQueryCache;
3685
+ /**
3686
+ * Hand a loaded read-cache entry to a subscription that was opened before
3687
+ * the load resolved — the same value, cursor and epoch {@link subscribe}
3688
+ * would have taken from {@link takeHydratedCache} had the load finished
3689
+ * first, so the cursor still rides the `subscribe` frame as `sinceSeq` (that
3690
+ * frame only goes out once the socket opens, well after this microtask).
3691
+ *
3692
+ * A no-op once the socket has delivered anything for this key: a live value
3693
+ * always beats the cache. Identity-gated exactly like `takeHydratedCache`.
3694
+ */
3695
+ private seedSubscriptionFromCache;
3696
+ /**
3697
+ * Move the durable read cache in or out of view as the CREDENTIAL changes —
3698
+ * which the identity fingerprint does not track: a sticky subject rides
3699
+ * across a token change (the account-switch shape), leaving the label
3700
+ * unmoved, so `setAuthToken`'s identity-change block never runs for it.
3701
+ * Revoke while that pairing is unchecked, and hand the values back once the
3702
+ * next session resolve confirms the credential really is that subject's.
3703
+ */
3704
+ private syncReadCacheToCredential;
3705
+ /**
3706
+ * A server frame owns this key now, so the read cache may neither take its
3707
+ * seeded value back ({@link revokeCacheSeededValues}) nor hold an entry for
3708
+ * a later `subscribe()` of the same key to replay a stale session over.
3709
+ */
3710
+ private dropCacheSeed;
3711
+ /**
3712
+ * Hand a cache-seeded entry back to {@link hydratedQueryCache} when its last
3713
+ * subscriber detaches, so the NEXT `subscribe()` of the same key can seed
3714
+ * from it again.
3715
+ *
3716
+ * Without this the hydrated cache was one-shot per key: {@link takeHydratedCache}
3717
+ * consumes the entry, the final unsubscribe drops the state that held its
3718
+ * value, and nothing re-seeds. Navigating away from a route and back while
3719
+ * offline therefore rendered `undefined` for the rest of the offline
3720
+ * session even though the durable store still held the rows. React hid it
3721
+ * behind TanStack's `gcTime`; every other adapter showed it immediately.
3722
+ *
3723
+ * Only a seed still listed in {@link cacheSeededQueries} moves — a server
3724
+ * frame calls {@link dropCacheSeed}, which clears both maps, so an entry
3725
+ * that is still here has not been superseded and is the freshest value this
3726
+ * client has for the key. That is the same map-to-map move
3727
+ * {@link revokeCacheSeededValues} makes, and it keeps the two maps disjoint:
3728
+ * an entry is either on display or waiting, never both.
3729
+ */
3730
+ private returnCacheSeed;
3731
+ /**
3732
+ * Take every value the durable read cache is currently displaying back off
3733
+ * screen, and return its entry to {@link hydratedQueryCache} so the identity
3734
+ * gate — not this call — decides whether it is ever shown again. A
3735
+ * same-credential refresh gets its offline-first value back the moment the
3736
+ * next session resolve re-confirms the subject; a genuine account switch
3737
+ * never does, and {@link clearQueryCacheForIdentityChange} wipes it.
3738
+ *
3739
+ * Only cache-seeded values: a server frame has been delivered under a socket
3740
+ * whose own identity is pinned and checked ({@link persistQueryValue}), and
3741
+ * dropping those would blank a live query on every token refresh.
3742
+ */
3743
+ private revokeCacheSeededValues;
3744
+ /**
3745
+ * Consume the hydrated read-cache entry for a key (if any), gated on
3746
+ * identity ({@link cachedQueryMatchesIdentity}). A mismatch yields
3747
+ * `undefined`, so a cache written under a different identity never leaks
3748
+ * into a new session.
3749
+ *
3750
+ * Only a MATCH is removed. The cache seeds a subscription's first value
3751
+ * once, so consuming a match is right — but destroying a mismatch is what
3752
+ * made a late-resolving identity unrecoverable: the subject typically lands
3753
+ * a `/get-session` after the first `subscribe()`, and by then the entry that
3754
+ * would have matched was gone. Left in place, {@link reseedFromHydratedCache}
3755
+ * can still hand it over when the identity settles.
3756
+ */
3757
+ private takeHydratedCache;
3758
+ /**
3759
+ * Hand every still-held read-cache entry to the subscription that is open on
3760
+ * its key, now that the identity has settled onto a resolved subject.
3761
+ *
3762
+ * `subscribe()` runs long before `/get-session` answers, so on a reload the
3763
+ * gate is asked its question with only a raw token in hand. When the token
3764
+ * itself matches, {@link cachedQueryMatchesIdentity} already says yes there
3765
+ * and then; when it has been refreshed since the value was cached, the only
3766
+ * honest answer at that moment is "unknown" — and this is where it becomes
3767
+ * knowable. {@link seedSubscriptionFromCache} is a no-op for any subscription
3768
+ * the socket has already fed, so a live value always wins.
3769
+ */
3770
+ private reseedFromHydratedCache;
3771
+ /**
3772
+ * Queue a coalesced read-cache write for a subscription's current value.
3773
+ * Latest-wins per key; flushed on a short debounce so a delta burst writes
3774
+ * once. No-op when the read cache is disabled or the value is undefined
3775
+ * (nothing to render offline).
3776
+ */
3777
+ private persistQueryValue;
3778
+ /** Drain {@link pendingCacheWrites} to the durable store. */
3779
+ private flushQueryCacheWrites;
3780
+ /** Derive the aggregate status from the per-shard socket states. */
3781
+ private computeStatus;
3782
+ /**
3783
+ * The CDC cursor this client's view of `shardKey` is at — the highest
3784
+ * `serverCursor` any live subscription on that shard has advanced to.
3785
+ *
3786
+ * This is the baseline a `.dropStalePatches()` table judges a write against,
3787
+ * and the "highest" is what makes it sound: a cursor the client has reached on
3788
+ * ANY subscription means the shard's changelog up to that point has been
3789
+ * delivered here, so anything later is by definition something this client had
3790
+ * not seen when it composed the write.
3791
+ *
3792
+ * `undefined` when no subscription on the shard carries a cursor yet — a
3793
+ * client that has only ever issued one-shot RPCs, or a server with CDC off.
3794
+ * That is reported honestly rather than defaulted to `0`: a `0` baseline would
3795
+ * claim the client had seen NOTHING, which makes every field look changed and
3796
+ * would have the shard discard every patch it sends.
3797
+ */
3798
+ private baselineCursorFor;
3799
+ /**
3800
+ * One polling-fallback pass for a shard: re-run every live query bound to it
3801
+ * over the batch-RPC endpoint and feed each result into the same frame-apply
3802
+ * path a server `data` frame takes.
3803
+ *
3804
+ * Reusing {@link LunoraClient.batch} and {@link LunoraClient.handleDataMessage}
3805
+ * rather than forking either is the whole point. The apply path is where
3806
+ * optimistic layers rebase, the durable read cache is seeded, and subscriber
3807
+ * callbacks fan out; a second implementation of that would drift, and it would
3808
+ * drift into exactly the bugs the live path has already had fixed.
3809
+ *
3810
+ * The result is re-encoded on the way in because `batch` hands back a decoded
3811
+ * value while the frame path expects the wire form. `encodeWire` is identity
3812
+ * for JSON-safe data and lossless for the rest (that is the codec's contract),
3813
+ * so the round trip costs an allocation and changes nothing.
3814
+ *
3815
+ * **No cursor rides this path.** A poll is a full snapshot, so the frames it
3816
+ * synthesizes carry no `cursor`/`epoch` and cannot advance a subscription's
3817
+ * resume watermark — which is correct: the watermark describes what the CDC
3818
+ * log has delivered, and this delivered none of it. When the socket comes
3819
+ * back, the resubscribe resumes from the last cursor the SOCKET saw, and the
3820
+ * server re-snapshots whatever it needs to.
3821
+ *
3822
+ * A slot that failed is fanned to the subscription's error callbacks rather
3823
+ * than being swallowed: the whole point of the fallback is that the app keeps
3824
+ * working, and a query that is now failing (a permission change, a bad arg)
3825
+ * has to be visible.
3826
+ *
3827
+ * Resolves `false` when the request never reached the origin (`fetch` itself
3828
+ * rejected), which is how the fallback tells a device with no network from a
3829
+ * network that only refuses the upgrade. Any answer, an error envelope
3830
+ * included, resolves `true`.
3831
+ *
3832
+ * Optimistic layers need the same care without a cursor. The snapshot is taken
3833
+ * after the request is sent, so every write whose RPC had resolved before that
3834
+ * moment is already in it. Those layers are dropped against the acknowledgement
3835
+ * mark sampled before the send. A write acknowledged after the send may be
3836
+ * missing from the snapshot, so its layer stays.
3837
+ */
3838
+ private pollSubscriptions;
3839
+ /**
3840
+ * Whether a request reaches the origin at all: an empty batch, whose answer
3841
+ * (even a refusal) is the proof. `false` only when `fetch` itself rejects.
3842
+ */
3843
+ private probeOrigin;
3844
+ /** Recompute the aggregate status and notify listeners if it changed. */
3845
+ private emitConnectionStatus;
3846
+ /**
3847
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
3848
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
3849
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
3850
+ * is unreachable — present only to satisfy the optional queue-id type.
3851
+ */
3852
+ private emitItemSettled;
3853
+ /**
3854
+ * Apply an optimistic update to the subscription that matches the mutation's
3855
+ * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
3856
+ * invoke if the mutation later fails.
3857
+ *
3858
+ * The registry is already indexed by exactly this triple via
3859
+ * `SubscriptionRegistry.key`, so at most one subscription can match. A direct
3860
+ * O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
3861
+ *
3862
+ * `shardKey` normalization: both `undefined` and `""` map to the empty string
3863
+ * inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
3864
+ * a shardKey correctly matches a subscription registered without one regardless
3865
+ * of whether the caller passed `undefined` or omitted the field.
3866
+ */
3867
+ private applyOptimisticUpdates;
3868
+ /**
3869
+ * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
3870
+ * to the live subscription registry. Each `setQuery` registers a constant
3871
+ * optimistic LAYER on its target subscription (via the same engine the
3872
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
3873
+ * incoming deltas and drops gaplessly on its commit cursor — its `confirm` /
3874
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
3875
+ * callback unwinds its own partial writes — LIFO over just the rollbacks it
3876
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
3877
+ * mutation or leave a partial patch live.
3878
+ */
3879
+ private applyOptimisticUpdate;
3880
+ private getConnection;
3881
+ /**
3882
+ * Send an unsubscribe frame (tagged with its wire type) on the shard's
3883
+ * socket, or queue it for the next reconnect when the send can't go out.
3884
+ */
3885
+ private sendOrQueueUnsubscribe;
3886
+ /**
3887
+ * The `(wsState, hasSocket, wasEverConnected, polling)` state `mutation()`'s
3888
+ * offline-queue gate reads. On the leader/single-tab path this is exactly
3889
+ * the real `ShardConnection`'s state (byte-identical to the pre-cross-tab
3890
+ * behavior). A follower has no `ShardConnection` of its own (see
3891
+ * `ensureSocket`), so it derives the same triple from the mirrored
3892
+ * `leader.status`/`leader.wasEverConnected` instead: `"connected"` maps to
3893
+ * `"open"` (queue-eligible once `wasEverConnected`), `"connecting"` stays
3894
+ * `"connecting"` (the mid-reconnect queue branch), anything else is
3895
+ * `"idle"`. `hasSocket` is always `false` for a follower — it never has
3896
+ * one.
3897
+ *
3898
+ * `polling` is `true` while the HTTP polling fallback is reaching the origin
3899
+ * (the leader's mirrored `"polling"` on a follower). Writes then go straight
3900
+ * over HTTP, as they do while connected, instead of queueing for a socket
3901
+ * that may never open.
3902
+ *
3903
+ * `idleClosed` is `true` for a shard whose socket this client closed because
3904
+ * nothing used it (see {@link releaseIdleShard}). It was connected, so it
3905
+ * stays queue-eligible (`wasEverConnected`), but its socket is not "down":
3906
+ * a write is tried over HTTP first, as it went while the socket was open,
3907
+ * and queued only if the network turns out to be unreachable.
3908
+ */
3909
+ private connectionGateState;
3910
+ private getOrCreateConnection;
3911
+ private wsUrlFor;
3912
+ /**
3913
+ * Build the outbound RPC headers: JSON content type, optional bearer auth,
3914
+ * the optional mutation-replay idempotency key, and the D1 read-your-writes
3915
+ * bookmark when the caller opted into `attachBookmark`. The mutation id
3916
+ * rides both the direct send and any offline-queue replay of the same write,
3917
+ * so a mutation the server already committed returns its cached result
3918
+ * instead of running twice.
3919
+ */
3920
+ private rpcRequestHeaders;
3921
+ private rpc;
3922
+ /**
3923
+ * Authenticated request to a non-RPC admin endpoint (the scheduler list /
3924
+ * cancel routes). Attaches the bearer token, parses JSON, and surfaces the
3925
+ * worker's `{ error: { code, message } }` envelope as a coded `Error` —
3926
+ * mirroring {@link rpc} so callers see the same failure shape.
3927
+ */
3928
+ private adminFetch;
3929
+ /**
3930
+ * Resolve the effective connection context for a shard: the most-recently
3931
+ * acquired refcounted holder ({@link acquireConnectionContext}) wins, falling
3932
+ * back to the imperative {@link setConnectionContext} override, then the
3933
+ * client-wide default. Returns `undefined` when none apply.
3934
+ */
3935
+ private effectiveConnectionContext;
3936
+ /** Re-send the `connect` envelope for a shard whose effective context just changed (if its socket is open). */
3937
+ private refreshConnectionContext;
3938
+ /**
3939
+ * Send the one-shot `connect` envelope on an open shard socket. Always sent
3940
+ * once per socket open, so the server's `onConnect` hooks fire symmetrically
3941
+ * with `onDisconnect` (which the DO dispatches unconditionally at close for
3942
+ * every lifecycle-aware socket). The DO no-ops cheaply when no `onConnect`
3943
+ * hooks are registered, and answers with the socket's `identity` frame
3944
+ * (see `handleIdentityFrame`).
3945
+ *
3946
+ * The shard's registered context (or the client-wide default) rides along
3947
+ * when one is set — the DO records it on the attachment for replay to
3948
+ * `onDisconnect`. A socket with no registered context still announces itself;
3949
+ * the envelope simply omits `context`, which is optional on the wire.
3950
+ * Register a context — e.g. `setConnectionContext({})` — to attach app state
3951
+ * to the lifecycle dispatch.
3952
+ */
3953
+ private sendConnectEnvelope;
3954
+ /**
3955
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
3956
+ * socket. Each frame carries the shape's last applied checkpoint, so the
3957
+ * server resumes from it — or re-seeds when the cursor fell below CDC
3958
+ * retention or the epoch forked.
3959
+ */
3960
+ private resendShapeSubscriptions;
3961
+ private ensureSocket;
3962
+ /**
3963
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
3964
+ * minted token. The connection is already in the `connecting` state, so the
3965
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
3966
+ * competing connect that landed first all abandon this attempt. A provider
3967
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
3968
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
3969
+ * a silent tokenless socket the admin gate would reject.
3970
+ */
3971
+ private openSocketWithProvidedToken;
3972
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
3973
+ private openSocket;
3974
+ /**
3975
+ * Tear down a stream the consumer cancelled, telling the server when its
3976
+ * socket is up.
3977
+ */
3978
+ private cancelStream;
3979
+ private handleDisconnect;
3980
+ /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
3981
+ private markShardPendingAck;
3982
+ /**
3983
+ * Re-send `state`'s `subscribe` frame, but only once fewer than
3984
+ * {@link RESUBSCRIBE_CONCURRENCY} frames on this shard are still awaiting a
3985
+ * reply. Used by every path that resends MANY subscriptions at once — the
3986
+ * socket-open resubscribe and the cross-tab leader handover, which replays
3987
+ * every tab's subscriptions combined. A first subscribe (one frame, caller
3988
+ * paced) still goes straight out through {@link sendSubscribeIfOpen}.
3989
+ */
3990
+ private queueResubscribe;
3991
+ /**
3992
+ * Send queued `subscribe` frames until the in-flight window is full.
3993
+ *
3994
+ * Cannot wedge: a queued subscription whose frame does not actually go out
3995
+ * (socket closed, already acked, unsubscribed while queued) takes no slot,
3996
+ * every sent frame is released by the first server frame carrying its id
3997
+ * (see {@link handleServerMessage}) or by its own watchdog, and a disconnect
3998
+ * drops the queue wholesale — the next `open` re-queues every subscription
3999
+ * on the shard, because `markShardPendingAck` un-acked them all.
4000
+ */
4001
+ private drainResubscribeQueue;
4002
+ /** Free the in-flight slot `id` holds on `conn` (if any) and let the next queued subscribe go out. */
4003
+ private releaseResubscribeSlot;
4004
+ /**
4005
+ * Abandon this connection's paced resubscribe: clear every watchdog and
4006
+ * forget both the in-flight and the waiting entries. Called wherever the
4007
+ * socket they were sent on goes away — the server drops that socket's
4008
+ * subscriptions anyway, and the next `open` re-queues all of them.
4009
+ */
4010
+ private clearResubscribeQueue;
4011
+ /** Send `state`'s `subscribe` frame when the shard's socket can carry it; `true` when a frame actually went out. */
4012
+ private sendSubscribeIfOpen;
4013
+ private sendShapeSubscribeIfOpen;
4014
+ private handleServerMessage;
4015
+ private handleErrorMessage;
4016
+ private handlePokeEnd;
4017
+ /**
4018
+ * Commit one shape's slice of a poke. The decision and the state change live in
4019
+ * `commitPokeSlice`; this method only performs the I/O that follows it.
4020
+ */
4021
+ private applyPokePart;
4022
+ /**
4023
+ * Force the server to re-send a full snapshot for `state`, leaving the
4024
+ * currently displayed value alone until it lands. Used when a delta frame
4025
+ * cannot be applied: dropping the resume cursor is what makes the resubscribe
4026
+ * a snapshot rather than a `resume`, and un-acking is what lets
4027
+ * `sendSubscribeIfOpen` put the frame on the wire at all. Mirrors the shape
4028
+ * path's re-seed on a diverged base.
4029
+ */
4030
+ private resnapshotSubscription;
4031
+ /**
4032
+ * Apply one `data` frame. `snapshotMark` is set only for a frame synthesized
4033
+ * by a poll, which carries no cursor: see {@link pollSubscriptions}.
4034
+ */
4035
+ private handleDataMessage;
4036
+ /**
4037
+ * Handle a `resume` frame (Pillar 1b): the server proved nothing the
4038
+ * subscription reads changed since our `sinceSeq`, so the cached value is
4039
+ * still current. We keep `lastValue` as-is, mark the sub acked, and advance
4040
+ * the cursor (re-persisting so the next reconnect resumes from the newer
4041
+ * watermark). No callback fires — the value didn't change, and `subscribe()`
4042
+ * already replayed the cached value to every consumer synchronously.
4043
+ */
4044
+ private handleResumeMessage;
4045
+ /**
4046
+ * Handle a `settled` frame: a write touched one of this subscription's read
4047
+ * tables but produced a byte-identical result, so the server suppressed the
4048
+ * data frame. Like {@link handleResumeMessage} the value didn't change — we
4049
+ * advance the resume position and re-persist — but we ALSO surface the echoed
4050
+ * custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
4051
+ * collection drops the optimistic overlay for the confirmed write (otherwise
4052
+ * its checkpoint gate, fed only by data frames, would hang forever). Sent
4053
+ * only to custom-mutator clients; plain `useQuery` subscribers leave
4054
+ * `onCheckpoint` unset and this is a near no-op.
4055
+ */
4056
+ private handleSettledMessage;
4057
+ /**
4058
+ * Mark `state` acked and, when the frame carries a newer cursor/epoch than
4059
+ * the cached position, advance the resume watermark and re-persist. Shared by
4060
+ * the `resume` and `settled` frame handlers — both acknowledge "nothing the
4061
+ * client must re-render changed, but the resume position may have moved".
4062
+ */
4063
+ private ackAndAdvanceCursor;
4064
+ /** Route an inbound whisper to the topic's handlers on the originating shard. */
4065
+ private dispatchWhisper;
4066
+ /** Notify every {@link onTokenExpired} listener (best-effort, listener throws swallowed). */
4067
+ private notifyTokenExpired;
4068
+ /**
4069
+ * CLIENT-04: `type: "complete"` today is sent ONLY by `@lunora/do`'s
4070
+ * `handleStream` (see `shard-do.ts`), gated to the `stream` envelope type
4071
+ * and minting only `stream_*` ids — the `subscribe` path never sends it, so
4072
+ * a live SUBSCRIPTION provably never receives `complete` from the current
4073
+ * server. But `ServerCompleteMessage` is a generic `id`-keyed frame and
4074
+ * `ShardDO` is user-subclassable, so this stays defensive rather than
4075
+ * assuming a `sub_*` id can never reach here: unlike the historical
4076
+ * `subscriptions.remove(state)`, which dropped the state out of
4077
+ * `subscriptions.all()` — the set the reconnect resubscribe loop walks
4078
+ * (`ensureSocket`'s `open` handler) — and so froze the query forever across
4079
+ * every future reconnect, this fans a cancellation error to any listener
4080
+ * and marks the registration un-acked instead. Non-destructive: the state
4081
+ * stays in the registry, so the very next reconnect resubscribes it. The
4082
+ * two id-spaces don't overlap (`sub_*` vs `stream_*`), so the stream and
4083
+ * subscription lookups below are mutually exclusive.
4084
+ */
4085
+ private handleCompleteMessage;
4086
+ private unpersist;
4087
+ /**
4088
+ * Stable, non-reversible fingerprint of the current auth identity used to
4089
+ * stamp queued offline writes.
4090
+ *
4091
+ * `null` never matches a bearer-token fingerprint — but it is only an
4092
+ * IDENTITY of its own once this client knows no subject is coming. An app
4093
+ * with no auth at all, and a client the server has answered "no session"
4094
+ * to, both hold a `null` nobody else shares. A client mid-`/get-session`
4095
+ * holds a `null` that may be one round trip from `subj:<id>`, which is
4096
+ * every cookie app's page load: ask {@link identityUnresolved} before
4097
+ * treating it as an identity, or `null === null` matches two different
4098
+ * people. The raw token is never stored;
4099
+ * a length-prefixed FNV-1a hash is enough to detect an identity *change*
4100
+ * without keeping the credential around in the queue map.
4101
+ */
4102
+ private identityFingerprint;
4103
+ /**
4104
+ * Stable token-hash fingerprint of a bearer token (the `<len>:<fnv>:<djb2>`
4105
+ * format a token-stamped queued write carries). Extracted so the replay gate
4106
+ * can recompute the hash of the current credential and recognise a write
4107
+ * stamped under it — even after the fingerprint was relabelled to a subject.
4108
+ *
4109
+ * Two independent 32-bit passes (FNV-1a + djb2) give a ~64-bit digest, so
4110
+ * two distinct equal-length tokens are astronomically unlikely to share a
4111
+ * fingerprint. A single 32-bit hash collides ~1-in-4e9 per equal-length
4112
+ * pair — enough that, on a shared device, user B could hydrate A's cached
4113
+ * reads. Different algorithms (not the same FNV with a different seed, which
4114
+ * would be affine-related) keep the two passes genuinely independent.
4115
+ * Still synchronous (no crypto) and stable across surrogate pairs.
4116
+ */
4117
+ private hashToken;
4118
+ /**
4119
+ * The `credential` field a read-cache write carries: the token hash of the
4120
+ * bearer currently held, or nothing at all when signed out (there is no
4121
+ * credential to match against, and the `null` identity already covers it).
4122
+ */
4123
+ private credentialStamp;
4124
+ /**
4125
+ * Whether a read-cache entry belongs to the identity in effect right now —
4126
+ * the ONE gate the hydrated peek, the subscribe-time take and the late seed
4127
+ * all share.
4128
+ *
4129
+ * Three ways to be the same identity, because the label and the credential
4130
+ * resolve at different times:
4131
+ *
4132
+ * 1. The fingerprints are equal — the plain case.
4133
+ * 2. The entry is stamped under a token hash and the live identity has since
4134
+ * been relabelled to the subject that credential resolved to
4135
+ * ({@link isSameCredentialUnderTokenHash}).
4136
+ * 3. The entry is stamped under a subject and THIS session has only the raw
4137
+ * token so far, but it is byte-for-byte the credential the entry was written
4138
+ * under. This is the direction the strict comparison missed entirely, and it
4139
+ * is the normal shape of every reload: every adapter calls
4140
+ * `setAuthToken(token)` from storage and only learns the subject a
4141
+ * `/get-session` round trip later — which offline never completes at all.
4142
+ * Without it the durable read cache never seeded for a bearer-token app.
4143
+ *
4144
+ * All three want a credential this client actually HOLDS, which is why a
4145
+ * **cookie session seeds nothing on an offline cold start** — a documented
4146
+ * limitation, not a gap. The cookie is `HttpOnly`: the client can neither
4147
+ * read it nor prove it still has it, and offline the `/get-session` that
4148
+ * would resolve the subject never answers, so such entries (stamped
4149
+ * `subj:<id>`, no credential) have nothing to match. Seeding them on the
4150
+ * remembered subject LABEL would hand the rows to whoever opens the browser
4151
+ * profile with nothing evidencing they are that subject, and revoking on a
4152
+ * later mismatch does not repair it: that check can only run once
4153
+ * connectivity returns, which is exactly the state where the offline seed
4154
+ * was not needed. Offline-first reads require a bearer token.
4155
+ *
4156
+ * Case 1 is suspended while the subject awaits re-confirmation
4157
+ * ({@link subjectAwaitingReconfirm}): the fingerprint then labels a
4158
+ * credential nothing has checked it against, which is precisely the
4159
+ * account-switch shape — `setAuthToken(otherAccountsToken)` with no subject,
4160
+ * what every adapter does on a reload, still reading `subj:<previous user>`.
4161
+ * Matching on that label alone hands the new account the previous account's
4162
+ * cached rows. Cases 2 and 3 both PROVE the credential, so the reload this
4163
+ * gate exists for still seeds.
4164
+ */
4165
+ private cachedQueryMatchesIdentity;
4166
+ /**
4167
+ * True when `stamped` is a token-hash of the SAME credential still held now,
4168
+ * even though the live identity has since been relabelled to a subject. Covers
4169
+ * `setAuthToken(token, userId)` where the subject resolved a tick after the
4170
+ * token was set: a write persisted (or requeued) under the token hash must
4171
+ * still replay — the credential never changed, only its label — instead of
4172
+ * being dropped as an identity mismatch. This is the durable counterpart to
4173
+ * {@link restampQueuedIdentity}, which only relabels the in-memory live stamp
4174
+ * (consumed on the first flush) and never touches `item.identity` or the
4175
+ * persisted record, so a reload or a transient-failure requeue would otherwise
4176
+ * fall back to the stale token-hash and wrongly reject the same user's write.
4177
+ */
4178
+ private isSameCredentialUnderTokenHash;
4179
+ /**
4180
+ * Release one {@link sessionProbesInFlight} hold and re-flush the writes
4181
+ * that hold alone was holding.
4182
+ *
4183
+ * A resolve that names a user re-flushes through {@link setAuthToken} (the
4184
+ * identity changed). One that does not — signed out, or unreachable —
4185
+ * changes no identity and so fires no listener, and a write held only
4186
+ * because {@link identityUnresolved} said so would sit queued until the
4187
+ * next reconnect. Same connected-only gate as `setAuthToken`'s, so this
4188
+ * stays a re-flush of a live connection rather than a reason to replay an
4189
+ * offline queue.
4190
+ *
4191
+ * Only for a `null` fingerprint, which is the ONLY hold this probe owns:
4192
+ * {@link replayIdentityVerdict}'s unresolved-identity hold tests
4193
+ * `current === null`, while its other hold — a sticky subject awaiting
4194
+ * re-confirmation — requires an established subject and so a
4195
+ * `subj:<id>` fingerprint. That one is already on the `noteHeldRetryDelay`
4196
+ * backoff and must stay there.
4197
+ *
4198
+ * Re-flushing it from here instead was an unbounded loop, not a slow
4199
+ * backoff: `drainOfflineQueue` answers a subject-awaiting hold by probing
4200
+ * `getCurrentUser()` itself, whose release re-flushed, which drained, which
4201
+ * probed again — hundreds of round trips without a millisecond passing,
4202
+ * until the heap gave out. Narrowing to the `null` fingerprint makes the
4203
+ * cycle unreachable rather than merely rare: the branch that starts a probe
4204
+ * cannot be entered by a flush this guard lets through.
4205
+ */
4206
+ private releaseSessionProbe;
4207
+ /**
4208
+ * Whether this client's `null` identity fingerprint is still provisional —
4209
+ * a {@link getCurrentUser} resolve is in flight and may be about to name a
4210
+ * subject. The one question every identity gate asks before it is willing
4211
+ * to treat `null` as an identity rather than as an absence of one.
4212
+ */
4213
+ private identityUnresolved;
4214
+ /**
4215
+ * Key the offline-queue identity on the resolved user id rather than the
4216
+ * token bytes, so the NEXT token refresh keeps the same identity.
4217
+ *
4218
+ * Under a bearer token only a resolved user labels anything. A 401 or an
4219
+ * empty session resolves `null` for reasons that say nothing about who holds
4220
+ * the token, so the established label is left alone rather than cleared
4221
+ * (which would look like an identity change and drop the queue). A token
4222
+ * that rotated mid-flight belongs to a session this answer predates and is
4223
+ * ignored for the same reason.
4224
+ *
4225
+ * Under a cookie session (no token) the answer is the identity, `null`
4226
+ * included: nothing on the client changes when the user signs out, so "the
4227
+ * server found no session" is the only sign-out this client will ever see.
4228
+ * See {@link adoptCookieSubject}.
4229
+ *
4230
+ * A superseded probe is ignored too, and the token check cannot see it: on a
4231
+ * cookie app both `requestToken`s are `null`, so a slow answer from an OLDER
4232
+ * probe passed that check and overwrote the subject a newer one had already
4233
+ * established. `requestGeneration` is the question's id — it must still be
4234
+ * the current one at the moment of the write.
4235
+ */
4236
+ private adoptResolvedSubject;
4237
+ /**
4238
+ * Adopt the user a cookie session resolved to — `null` for nobody — as this
4239
+ * client's identity, from either source the server answers through: a
4240
+ * `/get-session` probe, or a socket's `identity` frame.
4241
+ *
4242
+ * Three states, told apart by `authSubject`: `undefined` is not known yet
4243
+ * (every page load until the first answer), `null` is known to be nobody,
4244
+ * and a string is that user. {@link setAuthToken} retires the previous
4245
+ * session on every change between two KNOWN states — `A → nobody`,
4246
+ * `nobody → B`, `A → B` — and on none out of `undefined`, so the first
4247
+ * answer after a page load never evicts what the page is already showing
4248
+ * for the session it was loaded under.
4249
+ *
4250
+ * `question` orders the answers: one older than the answer in force
4251
+ * describes an older cookie and is dropped. Returns whether it was adopted.
4252
+ */
4253
+ private adoptCookieSubject;
4254
+ /**
4255
+ * Frame an already-encoded whisper body onto the topic's shard socket. Returns `false`
4256
+ * when the socket is not open, so the caller can hold the body and send it later.
4257
+ */
4258
+ private sendEncodedWhisper;
4259
+ /**
4260
+ * Whether room sends on `shardKey` may go straight to its socket: the socket is open,
4261
+ * the server has confirmed its identity, and its room rejoins are on the wire.
4262
+ */
4263
+ private roomSendsAdmitted;
4264
+ /** Flush the held room sends on `shardKey`, once that socket is admitted. */
4265
+ private releaseRoomSends;
4266
+ /** The server answered this socket's identity: record it, and release the sends that waited on it. */
4267
+ private confirmShardIdentity;
4268
+ /**
4269
+ * A cookie socket's `identity` frame: the user the shard authenticated this
4270
+ * socket as, sent in reply to every `connect`.
4271
+ *
4272
+ * Adopted like a probe's answer when it is the newest one
4273
+ * ({@link adoptCookieSubject}). An OLDER one that disagrees with the
4274
+ * identity in force means this socket was upgraded on a cookie that has
4275
+ * since changed hands, and is still delivering that user's rows: the
4276
+ * session it belongs to is retired, which replaces the socket.
4277
+ *
4278
+ * Ignored for a socket that authenticated with a token rather than the
4279
+ * cookie (see `ShardConnection.identityQuestion`) — its answer is about that
4280
+ * token, not about the cookie session this client's identity tracks.
4281
+ */
4282
+ private handleIdentityFrame;
4283
+ /**
4284
+ * The subject half of {@link setAuthToken}.
4285
+ *
4286
+ * Sticky: only an explicit value (incl. `null` = signed out) changes the
4287
+ * subject; omitting it keeps the established one. Two exceptions, both about
4288
+ * a subject that must not outlive the credential it described:
4289
+ *
4290
+ * - Clearing the token clears a user subject — see `setAuthToken`'s
4291
+ * docblock. A `null` one stays: a cookie session known to be nobody's is
4292
+ * still nobody's.
4293
+ * - A token arriving on a session known to be nobody's drops the `null`,
4294
+ * so the identity falls back to the token itself rather than staying
4295
+ * "nobody" while a credential is held.
4296
+ */
4297
+ private applySubject;
4298
+ /**
4299
+ * A cookie session the server has answered "nobody" for — see
4300
+ * {@link adoptCookieSubject}. Distinct from a subject not known YET
4301
+ * (`undefined`).
4302
+ */
4303
+ private isKnownNobody;
4304
+ /**
4305
+ * True while an established subject labels a credential it was never checked
4306
+ * against — see the `subjectToken` field. Every replay verdict is
4307
+ * `"unknown"` (hold) until the next session resolve re-asserts the subject.
4308
+ */
4309
+ private subjectAwaitingReconfirm;
4310
+ /**
4311
+ * The identity a queued write is stamped with. The live `queuedIdentities`
4312
+ * map is the source of truth for this session; a hydrated write whose id
4313
+ * isn't in it falls back to the stamp persisted with the record. `Map.get`
4314
+ * returns `undefined` for unstamped/hydrated ids and `item.identity` is
4315
+ * `undefined` for legacy records (persisted before stamps were durable),
4316
+ * while a persisted `null` (queued while signed out) is a real value that
4317
+ * must not collapse into `undefined` — hence `=== undefined`, not `??`.
4318
+ */
4319
+ private stampOf;
4320
+ /**
4321
+ * Warn when a token refused for a queued write is replaced with no subject
4322
+ * keyed: the likely refresh reads as a user switch, so the writes it was
4323
+ * meant to rescue were just rejected `OFFLINE_IDENTITY_CHANGED`. Dropping is
4324
+ * the safe default — a refresh and an account switch look the same without
4325
+ * a subject — so this only says how to keep them.
4326
+ */
4327
+ private warnRefreshWithoutSubject;
4328
+ /**
4329
+ * Reject the in-memory offline writes that can no longer replay under the
4330
+ * identity now signed in, dropping their durable records so a later
4331
+ * `hydrate` can't resurrect another user's writes.
4332
+ *
4333
+ * Per item, not wholesale: this runs on a plain sign-in too (`null` → a
4334
+ * signed-in identity is a credential change like any other), where the
4335
+ * queue is the *signing-in user's own* restored writes. Every stamp goes
4336
+ * through {@link replayIdentityVerdict}, the one comparison, and only
4337
+ * `"mismatch"` is terminal — `"match"` and `"unknown"` stay queued.
4338
+ *
4339
+ * The durable read cache is cleared only when there WAS a previous identity
4340
+ * to protect. Wiping it on a sign-in from signed-out would destroy the
4341
+ * offline-first cache on every cold boot, and buys nothing: cache entries
4342
+ * are identity-stamped and gated at every read.
4343
+ */
4344
+ private rejectQueuedForIdentityChange;
4345
+ /**
4346
+ * Move the identity stamp of every open shard socket from `from` to `to` —
4347
+ * the sibling of {@link restampQueuedIdentity} for the read cache. A socket
4348
+ * pins the identity it was upgraded under (see `ShardConnection.identity`)
4349
+ * and {@link persistQueryValue} stamps cached reads with it, deliberately,
4350
+ * so a still-open previous-user socket can't file frames under the new
4351
+ * user's stamp. A same-credential relabel is the one case where that pin is
4352
+ * stale rather than protective: without this, every read cached for the rest
4353
+ * of the session is filed under a fingerprint the next session's identity
4354
+ * gate rejects, and the durable read cache silently yields nothing.
4355
+ */
4356
+ private restampConnectionIdentity;
4357
+ /**
4358
+ * Migrate every identity stamp from `from` to `to` — used when the auth
4359
+ * identity label changes but the underlying credential (token) does NOT, e.g.
4360
+ * the user id resolves a tick after the token was set. The in-memory
4361
+ * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
4362
+ * keeps the in-flight writes replayable under the new (more stable) identity
4363
+ * instead of the flush guard discarding them as a mismatch.
4364
+ *
4365
+ * That map alone was not enough: it is consumed and DELETED on the first
4366
+ * flush attempt (`replayIdentityVerdict`), while the queue entry and its
4367
+ * persisted record keep the original stamp. So a reload, or a requeue after a
4368
+ * transient failure, fell back to the old token hash — and once the token had
4369
+ * been refreshed, `isSameCredentialUnderTokenHash` no longer recognised it
4370
+ * and the write was rejected `OFFLINE_IDENTITY_CHANGED` for the very user
4371
+ * `setAuthToken`'s sticky-`subject` contract promises to protect. The queue's
4372
+ * own re-stamp covers both the entry and its durable record.
4373
+ */
4374
+ private restampQueuedIdentity;
4375
+ /**
4376
+ * Migrate the {@link clientWatermarks} bucket map nested under identity
4377
+ * `from` to identity `to` — the sibling of {@link restampQueuedIdentity},
4378
+ * for the same same-credential-subject-resolves case. Without this, a
4379
+ * bucket's watermark cached under the token-hash fingerprint would look
4380
+ * unset once the fingerprint relabels to `subj:…`, so the next push
4381
+ * re-derives `1` against a server watermark the DO already advanced — the
4382
+ * OUT_OF_ORDER wedge this cache-keying scheme exists to fix, reintroduced
4383
+ * by the fix itself. `to` may already hold a bucket map (switching back to
4384
+ * an identity that has its own cached watermarks); merge into it rather
4385
+ * than clobbering it, with `from`'s entries winning on a colliding bucket —
4386
+ * the same overwrite a plain `Map.set` would have done before this was a
4387
+ * nested map.
4388
+ */
4389
+ private restampWatermarks;
4390
+ /**
4391
+ * Drop the durable read cache on an identity change so a cached value stamped
4392
+ * under the previous identity can never hydrate into a new session. Clears
4393
+ * the in-flight write batch and the not-yet-consumed hydrated entries too;
4394
+ * the durable `clear()` is best-effort.
4395
+ */
4396
+ private clearQueryCacheForIdentityChange;
4397
+ /**
4398
+ * Retire everything the PREVIOUS identity left live on a genuine identity
4399
+ * change: the sockets it is authenticated on, and the rows they delivered.
4400
+ *
4401
+ * A WebSocket credential is pinned in its upgrade URL and cannot be rotated
4402
+ * in place, so `setAuthToken` alone left the previous user's socket open and
4403
+ * delivering — the consequence `ShardConnection.identity`'s docblock already
4404
+ * described, with "nothing" as the thing that closes it on a client without
4405
+ * `crossTabSync`. And each `SubscriptionState` keeps the last value that
4406
+ * socket delivered, which `subscribe()` replays synchronously to every new
4407
+ * subscriber, so the incoming user's first render was the outgoing user's
4408
+ * rows even after the socket did go.
4409
+ *
4410
+ * Both halves are needed: closing the socket without clearing the values
4411
+ * leaves them on screen until a frame replaces them (which never comes for a
4412
+ * query the new user cannot read), and clearing the values without closing
4413
+ * the socket lets the next frame put them straight back.
4414
+ */
4415
+ private evictPreviousIdentitySession;
4416
+ /**
4417
+ * Move this tab's cross-tab coordinator onto the channel the new identity
4418
+ * derives, after an identity change.
4419
+ *
4420
+ * The channel name embeds the identity fingerprint (see
4421
+ * {@link createTabCoordinator}), so without this the tab keeps
4422
+ * leading/following the PREVIOUS identity's group. BroadcastChannel names
4423
+ * are immutable, so a stop-old+construct-new is the only way to move
4424
+ * channels — but that means the fresh coordinator would otherwise sit
4425
+ * through the full claim-then-`leaderTimeout` dance (3s default) before any
4426
+ * tab opens a socket again, freezing every live query for that long on
4427
+ * EVERY identity change (including a routine JWT refresh for an app that
4428
+ * doesn't pass a stable `subject` — the documented reason to pass one). If
4429
+ * this tab was already the leader, it's overwhelmingly likely to remain the
4430
+ * sole tab on the new channel too, so promote it immediately instead of
4431
+ * waiting — `promoteImmediately`'s docblock covers the (self-healing) rare
4432
+ * case where another tab does the same at once.
4433
+ */
4434
+ private restartTabCoordinatorForIdentity;
4435
+ /** Drop every room send held before the socket opened: they were made under the identity that is now going away. */
4436
+ private resetRoomSends;
4437
+ /**
4438
+ * Close every open shard socket so each reconnects carrying the credential
4439
+ * in effect now.
4440
+ *
4441
+ * Non-terminal, unlike {@link close}: the `ShardConnection` records and the
4442
+ * subscriptions riding them survive, and the registered `close` handler
4443
+ * (`handleDisconnect`) schedules the retry and replays the subscribes. This
4444
+ * is the only way to move a WS credential — it lives in the upgrade URL.
4445
+ */
4446
+ private bounceShardSockets;
4447
+ /**
4448
+ * Flush every shard with a mutation currently queued in `offlineQueue`
4449
+ * (see `queuedOfflineShardKeys`). Used on a FOLLOWER tab when the
4450
+ * mirrored leader status transitions to `"connected"` — a follower has no
4451
+ * per-shard `ShardConnection` reconnect event to hang the usual
4452
+ * single-shard `flushOfflineQueue(shardKey)` call off of (see the `onOpen`
4453
+ * callback that calls `flushOfflineQueue(shardKey)`), so this walks every
4454
+ * shard that might have something queued instead. Flushing an already-empty
4455
+ * shard is a cheap no-op (`flushOfflineQueue` returns immediately once
4456
+ * `drain` yields nothing), so over-inclusion here is harmless.
4457
+ */
4458
+ private flushAllOfflineQueues;
4459
+ /**
4460
+ * Replay a shard's queued writes, serialized per shard and published as
4461
+ * {@link offlineFlushes} so a concurrent `mutation()` can wait behind it.
4462
+ * Never rejects: every entry's outcome is settled individually inside
4463
+ * {@link drainOfflineQueue}, and a poisoned chain would strand every later
4464
+ * flush AND every write waiting on the barrier.
4465
+ */
4466
+ private flushOfflineQueue;
4467
+ private drainOfflineQueue;
4468
+ /**
4469
+ * Ask for another flush of this shard after a backoff, because every entry it
4470
+ * drained was HELD for an identity that is not re-confirmed yet.
4471
+ *
4472
+ * Same state and same timer as a rate-limited replay
4473
+ * ({@link LunoraClient.noteReplayRetryDelay}) — one pending flush per shard
4474
+ * key either way — only the reason differs: there is no error to read a
4475
+ * `Retry-After` off, so the hintless {@link defaultReplayRetryDelayMs} ramp
4476
+ * (1s doubling to a 60s ceiling, jittered) is the whole policy. It is a
4477
+ * ceiling on the rate, not a budget of attempts: a durable write is never
4478
+ * dropped for having waited too long, so a subject that never resolves leaves
4479
+ * the queue re-probing at ≤ 60s intervals with the writes intact and
4480
+ * {@link LunoraClient.pendingCount} non-zero for the app to surface. The
4481
+ * retries stop when the hold lifts (the queue drains), when the shard has
4482
+ * nothing queued, or at `close()`.
4483
+ *
4484
+ * Identity is re-read by the flush, never carried across the timer: the
4485
+ * retry re-enters {@link LunoraClient.drainOfflineQueue}, which re-runs
4486
+ * {@link LunoraClient.replayGateVerdict} per entry against the identity in
4487
+ * effect when it runs. A retry therefore cannot replay a write under an
4488
+ * identity that changed while the timer was pending — it re-holds it, or
4489
+ * rejects it as a mismatch, exactly as a reconnect-driven flush would.
4490
+ */
4491
+ private noteHeldRetryDelay;
4492
+ /**
4493
+ * Remember the longest delay this shard's flush was told (or worked out) to
4494
+ * wait, so the drain can honour it before trying again
4495
+ * ({@link LunoraClient.replayRetryState}). Counts the attempt either way:
4496
+ * that is what a hintless refusal backs off on.
4497
+ *
4498
+ * A credential refused under a cookie session (`authToken === null`) backs
4499
+ * off on the hintless ramp too. A bearer refusal waits for `setAuthToken`,
4500
+ * which re-flushes; a cookie is renewed in the browser, invisibly to this
4501
+ * client, so the only way to learn it was is to send again.
4502
+ */
4503
+ private noteReplayRetryDelay;
4504
+ /**
4505
+ * Consume this shard's retry delay and re-flush it once the delay has
4506
+ * elapsed.
4507
+ *
4508
+ * Two things record a delay for it to consume: a failure the server or an
4509
+ * edge ANSWERED (see {@link replayRetryDelayMs}) and a flush that HELD every
4510
+ * entry it drained (see {@link LunoraClient.noteHeldRetryDelay}). Both happen
4511
+ * over a socket that stays open, so without this the writes sit queued
4512
+ * indefinitely; a `fetch` that never landed records nothing, because the
4513
+ * reconnect that follows flushes the queue anyway. One pending timer per
4514
+ * shard; a second delay replaces it rather than stacking flushes.
4515
+ *
4516
+ * Nothing left to retry on this key (drained, closed, or no delay) drops its
4517
+ * backoff state, which is both the reset after progress and what bounds the
4518
+ * map.
4519
+ */
4520
+ private scheduleRateLimitedRetry;
4521
+ /**
4522
+ * Partition already-gated writes into the encodable ones (returned) and reject
4523
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
4524
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
4525
+ * is deterministic, not transient. Rejecting here is essential: otherwise
4526
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
4527
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
4528
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
4529
+ * the flush is the slow reconnect path.
4530
+ */
4531
+ private encodableOrSettleTerminal;
4532
+ /**
4533
+ * The three-way comparison behind {@link replayIdentityVerdict} (documented
4534
+ * there), without a credential: the built-in queue sends with its own pinned token.
4535
+ */
4536
+ private identityVerdict;
4537
+ /**
4538
+ * Identity gate for one queued write about to replay: a write stamped under
4539
+ * one identity must never replay under another, and must never be DESTROYED
4540
+ * because the identity isn't known yet.
4541
+ *
4542
+ * The three-way verdict is {@link replayIdentityVerdict}'s, not a second
4543
+ * hand-rolled comparison — `@lunora/db`'s durable outbox holds on `"unknown"`
4544
+ * through the same call, and this path (the default for the standalone
4545
+ * client) used to drop there instead, purging the queuing user's own write
4546
+ * on every reload that opened its socket before the session resolved.
4547
+ *
4548
+ * `"send"` replays it and consumes the live stamp. `"hold"` leaves it queued
4549
+ * and persisted, stamp intact, for a later flush once an identity is
4550
+ * established (see `setAuthToken`). `"reject"` means a different identity is
4551
+ * signed in: settle it terminally `OFFLINE_IDENTITY_CHANGED` and purge the
4552
+ * durable record.
4553
+ */
4554
+ private replayGateVerdict;
4555
+ /** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
4556
+ private settleReplaySuccess;
4557
+ /**
4558
+ * The entry a shard's cursor lives under.
4559
+ *
4560
+ * Only the server knows that an omitted `shardKey` and an explicit one
4561
+ * spelling out its configured default name are the same shard — the default
4562
+ * is server-side configuration the client never sees. Keying on what was
4563
+ * SENT would split one shard's cursor across two entries, so a write under
4564
+ * one spelling would stop constraining a read under the other. Resolving
4565
+ * `undefined` through the learned name is what keeps both spellings on one
4566
+ * entry.
4567
+ */
4568
+ private cursorKeyFor;
4569
+ /**
4570
+ * Learn the server's own name for the default shard from a response to a
4571
+ * call that named no shard.
4572
+ *
4573
+ * Any cursor recorded before the name was known sits under the placeholder
4574
+ * entry, so it is folded in rather than stranded — otherwise the requirement
4575
+ * from a client's first write would be lost exactly once, which is the kind
4576
+ * of gap that only shows up as a stale read under load.
4577
+ */
4578
+ private learnDefaultShardKey;
4579
+ /**
4580
+ * Record the cursor a write committed at as this shard's read-your-writes
4581
+ * requirement.
4582
+ *
4583
+ * Monotonic: responses can land out of order, and moving the requirement
4584
+ * BACKWARDS would let a later read be answered from a replica copy that
4585
+ * predates a write this client already saw.
4586
+ */
4587
+ private recordShardCursor;
4588
+ /**
4589
+ * Whether a replay failure leaves the durable write queued rather than
4590
+ * settling it terminally — the ONE classification the single-call, per-slot
4591
+ * and whole-batch paths share, so a write's fate never depends on how many
4592
+ * siblings rode along.
4593
+ *
4594
+ * {@link isTransientReplayFailure} answers the "no verdict was reached" half.
4595
+ * The other half is a refused CREDENTIAL ({@link isAuthReplayFailure}): the
4596
+ * queue flushes on the shard's `open` handler with whatever bearer survived
4597
+ * the offline window, so an expired token is the expected outcome of a long
4598
+ * disconnect, not a verdict on the write. Notifying {@link onTokenExpired}
4599
+ * here is what closes the loop — the HTTP replay path has no equivalent of
4600
+ * the WS `4001` close frame, so without this nothing tells the app to
4601
+ * refresh, and `setAuthToken` (which re-flushes) is never called.
4602
+ *
4603
+ * Notified once per credential, not once per write: a flush of a hundred
4604
+ * queued writes earns a hundred identical refusals, and firing the hook for
4605
+ * each would put a hundred token refreshes on the app. The stamp re-arms as
4606
+ * soon as the credential moves, which is the only thing that can change the
4607
+ * answer.
4608
+ */
4609
+ private shouldRequeueReplayFailure;
4610
+ /**
4611
+ * Whether `error` refused a replay's CREDENTIAL (`authToken`, the bearer it
4612
+ * was sent with), and if so tell {@link onTokenExpired} — once per
4613
+ * credential, and only while it is still the current one. A refusal of a
4614
+ * token the app has already replaced says nothing about the current one:
4615
+ * the next pass re-sends under it, so asking for another refresh would be
4616
+ * spurious.
4617
+ */
4618
+ private noteReplayAuthRefusal;
4619
+ /**
4620
+ * A request under `authToken` succeeded: if a refusal of that credential was
4621
+ * reported, its next refusal is news again. A bearer re-arms by changing; a
4622
+ * cookie session's `null` never does, so without this the app could be asked
4623
+ * to refresh a cookie only once per page.
4624
+ */
4625
+ private noteCredentialAccepted;
4626
+ /**
4627
+ * Redeem a {@link ReplayCredential} (single use) into the request flags it
4628
+ * pins (the judged bearer, and the subject a cookie replay names) and the
4629
+ * identity stamp it was judged for. Throws for a credential this client did
4630
+ * not issue or already redeemed.
4631
+ */
4632
+ private takeReplayCredential;
4633
+ /**
4634
+ * Whether `failure` is the worker's `IDENTITY_MISMATCH` refusal, and if so,
4635
+ * ask who is signed in now: the answer is what re-runs the replay gate
4636
+ * ({@link setAuthToken}), and nothing else would on a cookie session.
4637
+ */
4638
+ private noteIdentityMismatch;
4639
+ /**
4640
+ * Settle a write the server COMMITTED whose result does not decode. It is
4641
+ * `committed` — its optimistic layer confirms against the echoed cursor like
4642
+ * any success — but there is no value to hand the caller, so the awaiter is
4643
+ * rejected with the decode error and the settled event carries it.
4644
+ */
4645
+ private settleReplayUndecodable;
4646
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
4647
+ private settleReplayTerminal;
4648
+ /**
4649
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
4650
+ * path, preserving FIFO order (parallel `.then()` chains would race the
4651
+ * ordering callers depend on). Each replays under its stable `mutationId` so
4652
+ * the server dedups a write it already committed (exactly-once). A server verdict
4653
+ * drops the write; a transient failure ({@link isTransientReplayFailure} — a
4654
+ * transport error, a shard the worker couldn't reach, a rate-limit refusal, or a
4655
+ * non-2xx carrying no `{ error }` envelope) stops the flush and re-queues this
4656
+ * write and every unreplayed one for the next reconnect — their callers stay
4657
+ * pending, and the identity guard re-applies on retry via each record's persisted
4658
+ * stamp. The batch path classifies a slot by the same rule, so a durable write's
4659
+ * fate never depends on how many siblings happened to be queued alongside it.
4660
+ */
4661
+ private replaySequential;
4662
+ /**
4663
+ * Coalesce already-identity-gated writes for a single shard into ONE
4664
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
4665
+ * them to the shard DO, which replays each through its single-call dispatch, so
4666
+ * per-entry `mutationId` idempotency and in-order application are inherited from
4667
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
4668
+ * classification: success confirms the optimistic layer against the echoed
4669
+ * `commitCursor`; a coded application verdict is terminal; a failure
4670
+ * {@link shouldRequeueReplayFailure} keeps, a missing slot, or a whole-batch
4671
+ * transport failure re-queues for the next reconnect (never dropping a durable
4672
+ * write). A whole-batch coded rejection (a bad request the server reached a
4673
+ * verdict on) is terminal for every entry.
4674
+ *
4675
+ * The body is also held under {@link MAX_BATCH_BODY_BYTES}: the worker caps a
4676
+ * batch body at 1 MiB and answers `413`, which is ONE refusal covering every
4677
+ * write in the chunk. An over-budget chunk is halved before it is sent, and a
4678
+ * `413` for a chunk of more than one write halves it and retries rather than
4679
+ * settling the whole chunk terminally — so a backlog of large writes still
4680
+ * commits, one bisection deeper.
4681
+ *
4682
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
4683
+ * chunk failed at the transport level, so the caller leaves later chunks queued
4684
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
4685
+ * NOT done here.
4686
+ */
4687
+ private replayBatched;
4688
+ /**
4689
+ * Classify a `/_lunora/rpc-batch` reply that carried no per-slot results — one
4690
+ * outcome covering every entry in the chunk. A code
4691
+ * {@link shouldRequeueReplayFailure} keeps (an unreachable shard, a
4692
+ * rate-limit refusal, a refused credential) or a {@link TransportError} (an
4693
+ * edge reply with no verdict in it) leaves every write durable for the next
4694
+ * attempt; anything else is a verdict reached on the request itself, and
4695
+ * settles all of them.
4696
+ */
4697
+ private settleWholeBatchError;
4698
+ /**
4699
+ * Split an over-large batch chunk in half and replay each half, preserving
4700
+ * FIFO order. Recurses through {@link replayBatched}, so a chunk keeps halving
4701
+ * until it fits (or reaches one write, which is then the server's verdict to
4702
+ * give). A `stop` on the first half leaves the second unsent and queued.
4703
+ */
4704
+ private replayBatchedHalves;
4705
+ /**
4706
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
4707
+ * in input order. Each slot's envelope classifies its write the same way
4708
+ * {@link replaySequential} does: a success confirms the optimistic layer
4709
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
4710
+ * a failure {@link shouldRequeueReplayFailure} keeps, or a slot the server
4711
+ * never returned, is returned for the caller to re-queue.
4712
+ *
4713
+ * A slot whose `error` key holds no readable envelope is in the same position
4714
+ * as one the server never returned — nothing came back about that entry that
4715
+ * can be read as a verdict — so it is kept, not settled.
4716
+ * @returns the writes that must be re-queued (kept slots), in input order
4717
+ */
4718
+ private settleReplayBatchSlots;
4719
+ }
4720
+ export { SchedulerStatus as $, ActionCallOptions as A, BookmarkStorage as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, OutboxMutation as E, FunctionArgumentDescriptor as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxSink as I, PersistedMutation as J, ReplayCredential as K, LunoraClient as L, MutationCallOptions as M, ReplayIdentityVerdict as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReconnectOptions as R, SubscriptionError as S, RoomRef as T, User as U, RowOp as V, RpcEnvelope as W, RpcResponseBody as X, ScheduleRecord as Y, ScheduleRetryPolicy as Z, SchedulerPoolStatus as _, Unsubscribe as a, ServerMessage as a0, ServerPokeEndMessage as a1, ServerPokePartMessage as a2, ServerPokeStartMessage as a3, ShardTrafficEntry as a4, ShardTrafficResult as a5, StorageListPage as a6, StorageObject as a7, StoredQuery as a8, StreamHandle as a9, SubscriptionCallback as aa, SubscriptionRegistry as ab, SubscriptionState as ac, SyncWatermark as ad, WorkflowInstanceAction as ae, WorkflowInstanceDetail as af, WorkflowInstancePage as ag, WorkflowInstanceStatus as ah, WorkflowInstanceSummary as ai, WorkflowStepDetail as aj, WsTokenProvider as ak, createClientQuery as al, createLocalStore as am, createStream as an, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, BatchSlot as g, CachedQuery as h, ClientDebugShard as i, ClientDebugSnapshot as j, ClientDebugSubscription as k, ClientMessage as l, ClientQueryRef as m, ClientShapeSubscribeMessage as n, ClientShapeUnsubscribeMessage as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, MutationSettledEvent as x, OptimisticLocalStore as y, OptimisticUpdate as z };