@lunora/client 1.0.0-alpha.6 → 1.0.0-alpha.60

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 (88) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/auth/index.d.mts +11 -10
  3. package/dist/auth/index.d.ts +11 -10
  4. package/dist/auth/index.mjs +1 -60
  5. package/dist/index.d.mts +768 -230
  6. package/dist/index.d.ts +768 -230
  7. package/dist/index.mjs +1 -15
  8. package/dist/packem_shared/CONFLICT_ERROR_CODE-LjU7z0mB.mjs +1 -0
  9. package/dist/packem_shared/ClientServiceWorker-DXnBXeWK.mjs +1 -0
  10. package/dist/packem_shared/DEFAULT_MAX_BUFFER-D3QH2iaq.mjs +1 -0
  11. package/dist/packem_shared/LunoraClient-akyZf9CJ.mjs +1 -0
  12. package/dist/packem_shared/OfflineQueue-DG51qWl4.mjs +1 -0
  13. package/dist/packem_shared/RETIRE_AFTER_DURABLE_SEQ_ADVANCE-DoEHtuR8.mjs +1 -0
  14. package/dist/packem_shared/SKIP-d7LeP-sY.mjs +1 -0
  15. package/dist/packem_shared/SubscriptionRegistry-CdHSrDqu.mjs +1 -0
  16. package/dist/packem_shared/TabCoordinator-DVKG2RlZ.mjs +1 -0
  17. package/dist/packem_shared/anyApi-CBOws2ZA.mjs +1 -0
  18. package/dist/packem_shared/applyDelta-DGqMpi3N.mjs +1 -0
  19. package/dist/packem_shared/createAsyncStoragePersistence-CNdM5o1u.mjs +1 -0
  20. package/dist/packem_shared/createAsyncStorageQueryCache-Di-S52wy.mjs +1 -0
  21. package/dist/packem_shared/createCallRunner-Bwcrfh76.mjs +1 -0
  22. package/dist/packem_shared/createClientQuery-TKD_52cT.mjs +1 -0
  23. package/dist/packem_shared/createInMemoryBookmarkStorage-BooZhW0n.mjs +1 -0
  24. package/dist/packem_shared/createInMemoryPersistence-C0qm7c47.mjs +1 -0
  25. package/dist/packem_shared/createInMemoryQueryCache-D2uNHLfM.mjs +1 -0
  26. package/dist/packem_shared/createLocalStore-CZwiN9F4.mjs +1 -0
  27. package/dist/packem_shared/createMutatorRunner-CuMRJpUx.mjs +1 -0
  28. package/dist/packem_shared/createReconnect-CjTmjJDH.mjs +1 -0
  29. package/dist/packem_shared/createReply-CwFABEJs.mjs +1 -0
  30. package/dist/packem_shared/createServerClient-DXhPwQ8T.mjs +1 -0
  31. package/dist/packem_shared/createSnapshotPrecondition-BPMQbAbk.mjs +1 -0
  32. package/dist/packem_shared/delta-merge-BoVuM-rE.mjs +1 -0
  33. package/dist/packem_shared/deserializePreloaded-CHfOFpka.mjs +1 -0
  34. package/dist/packem_shared/getServerSession-BaOoo1p6.mjs +1 -0
  35. package/dist/packem_shared/httpStream-x6q-x4yK.mjs +11 -0
  36. package/dist/packem_shared/idb-utility-C8WS390w.mjs +1 -0
  37. package/dist/packem_shared/local-store-tpI9VFpO.mjs +1 -0
  38. package/dist/packem_shared/lunora-client.d-CEJvJAXn.d.mts +2295 -0
  39. package/dist/packem_shared/lunora-client.d-CU6S8XiA.d.ts +2295 -0
  40. package/dist/packem_shared/offline-queue-CzI2lYQ8.mjs +1 -0
  41. package/dist/packem_shared/preload.d-7lAyp-Ky.d.mts +21 -0
  42. package/dist/packem_shared/preload.d-Gmmtaq2-.d.ts +21 -0
  43. package/dist/packem_shared/preloadQuery-uFy24PCR.mjs +1 -0
  44. package/dist/packem_shared/single-blob-store-DrhzObic.mjs +1 -0
  45. package/dist/packem_shared/types.d-BOB755CV.d.mts +1015 -0
  46. package/dist/packem_shared/types.d-BOB755CV.d.ts +1015 -0
  47. package/dist/packem_shared/wire-codec-D4iww4NV.mjs +1 -0
  48. package/dist/packem_shared/wire-key-BOdKmpG3.mjs +1 -0
  49. package/dist/pagination/index.d.mts +42 -42
  50. package/dist/pagination/index.d.ts +42 -42
  51. package/dist/pagination/index.mjs +1 -61
  52. package/dist/query/index.d.mts +110 -43
  53. package/dist/query/index.d.ts +110 -43
  54. package/dist/query/index.mjs +1 -1
  55. package/dist/service.d.mts +49 -0
  56. package/dist/service.d.ts +49 -0
  57. package/dist/service.mjs +1 -0
  58. package/dist/ssr/index.d.mts +100 -79
  59. package/dist/ssr/index.d.ts +100 -79
  60. package/dist/ssr/index.mjs +1 -4
  61. package/dist/upload.d.mts +35 -0
  62. package/dist/upload.d.ts +35 -0
  63. package/dist/upload.mjs +1 -0
  64. package/package.json +13 -1
  65. package/dist/packem_shared/CONFLICT_ERROR_CODE-aUdVbEDw.mjs +0 -4
  66. package/dist/packem_shared/DEFAULT_MAX_BUFFER-BDkqO5PW.mjs +0 -107
  67. package/dist/packem_shared/LunoraClient-DhwA_5Kj.mjs +0 -2502
  68. package/dist/packem_shared/OfflineQueue-D-ASeqL7.mjs +0 -131
  69. package/dist/packem_shared/SKIP-vItZChkw.mjs +0 -50
  70. package/dist/packem_shared/SubscriptionRegistry-Dn-7k7eo.mjs +0 -1
  71. package/dist/packem_shared/applyDelta-4jFGTPA3.mjs +0 -61
  72. package/dist/packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs +0 -45
  73. package/dist/packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs +0 -11
  74. package/dist/packem_shared/createInMemoryPersistence-CW82inU5.mjs +0 -105
  75. package/dist/packem_shared/createInMemoryQueryCache-B1PQ9Twl.mjs +0 -138
  76. package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
  77. package/dist/packem_shared/createMutationRunner-BqsavzvG.mjs +0 -21
  78. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +0 -31
  79. package/dist/packem_shared/createReconnect-Di_-oHH7.mjs +0 -22
  80. package/dist/packem_shared/createServerClient-XahK8bPv.mjs +0 -11
  81. package/dist/packem_shared/deserializePreloaded-C0eJTY_W.mjs +0 -4
  82. package/dist/packem_shared/getServerSession-8jXewqxd.mjs +0 -13
  83. package/dist/packem_shared/lunora-client.d-DzEWBYeP.d.mts +0 -1948
  84. package/dist/packem_shared/lunora-client.d-DzEWBYeP.d.ts +0 -1948
  85. package/dist/packem_shared/preload.d-CmMMC-wm.d.ts +0 -20
  86. package/dist/packem_shared/preload.d-DuYqEQ5J.d.mts +0 -20
  87. package/dist/packem_shared/preloadQuery-lobFkD2Z.mjs +0 -13
  88. package/dist/packem_shared/subscription-C1Jy7HiF.mjs +0 -55
@@ -0,0 +1,2295 @@
1
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClientOptions, a as Unsubscribe, U as User, W as WsTokenProvider, S as ShardTrafficResult, b as ScheduleRecord, c as SchedulerStatus, d as WorkflowInstanceStatus, e as WorkflowInstancePage, f as WorkflowInstanceDetail, g as WorkflowInstanceAction, h as FunctionDescriptor, i as StorageListPage, G as GlobalTableInfo, j as GlobalFilterClause, k as GlobalTablePage, l as GlobalFacetResult, H as HttpStreamRef, m as HttpStreamArgsOf, n as HttpStreamChunkOf } from "./types.d-BOB755CV.mjs";
2
+ import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
3
+ /**
4
+ * Reactive key-value store for local-only client state.
5
+ *
6
+ * Unlike a server {@link SubscriptionState} (which tracks a live WS connection,
7
+ * an `acked` flag, `serverBase`, optimistic layers, and the full subscription
8
+ * machinery), a `ClientQueryRef` is purely local — no server round-trip, no
9
+ * WebSocket, no persistence. It exists so framework adapters can offer a
10
+ * `useClientQuery` hook whose values survive component remounts and are shared
11
+ * across every consumer of the same ref, with none of the ceremony or coupling
12
+ * of a dedicated context provider.
13
+ *
14
+ * The store lives inside `LunoraClient` (a private field) and is surfaced through
15
+ * `client.getClientQuery(ref)` / `setClientQuery(ref, value)` /
16
+ * `subscribeClientQuery(ref, callback)`.
17
+ */
18
+ /** Opaque handle for a typed client-local query slot. */
19
+ interface ClientQueryRef<T = unknown> {
20
+ /** Default value when no value has been set explicitly. */
21
+ readonly defaultValue: T;
22
+ /** Stable identity for the slot. Must be unique within a client instance. */
23
+ readonly key: string;
24
+ }
25
+ /**
26
+ * Create a typed {@link ClientQueryRef}. Call once per slot at module scope
27
+ * (or inside a component module) — the ref object is the stable identity.
28
+ * @example
29
+ * ```ts
30
+ * // lunora/client-queries.ts
31
+ * import { createClientQuery } from "@lunora/client";
32
+ *
33
+ * export const sidebarOpen = createClientQuery("sidebarOpen", true);
34
+ * export const selectedMessageId = createClientQuery("selectedMessageId", undefined as string | undefined);
35
+ * ```
36
+ */
37
+ declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
38
+ /**
39
+ * The machine-readable error codes a client can observe on a failed
40
+ * RPC/batch/subscription. Mirrors the server's `CODE_STATUS` keys
41
+ * (`@lunora/server`'s `error.ts`) by hand — the client is framework-neutral and
42
+ * must never import the server package (wrong dependency direction / would pull
43
+ * the server into the browser bundle). Keep this list in sync when a server code
44
+ * is added or removed (see the drift-guard note in the plan/maintenance docs).
45
+ */
46
+ declare const LUNORA_ERROR_CODES: readonly ["BAD_REQUEST", "CONFLICT", "COUNT_RLS_UNSUPPORTED", "FORBIDDEN", "INTERNAL_SERVER_ERROR", "MASK_UNSUPPORTED", "NOT_FOUND", "NOT_IMPLEMENTED", "RELATION_PREDICATE_UNSUPPORTED", "TOO_MANY_REQUESTS", "UNAUTHORIZED", "UNPROCESSABLE"];
47
+ /** A machine-readable error `code` the client may observe. Mirror of the server's `LunoraErrorCode`. */
48
+ type LunoraErrorCode = (typeof LUNORA_ERROR_CODES)[number];
49
+ /** Error code the server uses for optimistic-concurrency conflicts (HTTP 409). */
50
+ declare const CONFLICT_ERROR_CODE = "CONFLICT";
51
+ /**
52
+ * Whether an unknown rejection is an optimistic-concurrency conflict — the
53
+ * server lost a write race and the caller should refetch and retry (or surface
54
+ * the conflict). Structural check on the `code` property the client attaches
55
+ * when decoding the worker's `{ error: { code, message } }` envelope.
56
+ */
57
+ declare const isConflictError: (error: unknown) => error is Error & {
58
+ code: "CONFLICT";
59
+ };
60
+ /**
61
+ * Whether a rejection is an RLS/policy denial (`FORBIDDEN`, HTTP 403) — the
62
+ * caller is authenticated but not permitted to read/write the row. The most
63
+ * common per-call error a UI must handle in an RLS-first app.
64
+ */
65
+ declare const isForbiddenError: (error: unknown) => error is Error & {
66
+ code: "FORBIDDEN";
67
+ };
68
+ /** Whether a rejection is an authentication failure (`UNAUTHORIZED`, HTTP 401) — no/invalid identity. */
69
+ declare const isUnauthorizedError: (error: unknown) => error is Error & {
70
+ code: "UNAUTHORIZED";
71
+ };
72
+ /**
73
+ * Whether a rejection is a rate-limit denial (`TOO_MANY_REQUESTS`, HTTP 429).
74
+ * The retry hint (if the server sent one) is read with {@link getRetryAfterMs}.
75
+ */
76
+ declare const isRateLimitedError: (error: unknown) => error is Error & {
77
+ code: "TOO_MANY_REQUESTS";
78
+ };
79
+ /**
80
+ * Read the server's machine-readable `code` off a rejection, narrowed to the
81
+ * known {@link LunoraErrorCode} union. Returns `undefined` for a non-`Error`, a
82
+ * missing code, or an unrecognized code string (forward-compat server codes read
83
+ * as `undefined` here rather than being falsely narrowed).
84
+ */
85
+ declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
86
+ /**
87
+ * Read the rate-limit retry hint (`data.retryAfterMs`) off a
88
+ * `TOO_MANY_REQUESTS` rejection without hand-casting the `unknown` `data`
89
+ * payload. Returns the finite millisecond value the server sent, or `undefined`
90
+ * when absent/non-numeric. Pair with {@link isRateLimitedError}.
91
+ */
92
+ declare const getRetryAfterMs: (error: unknown) => number | undefined;
93
+ type SubscriptionCallback = (data: unknown) => void;
94
+ /** A subscription-scoped error the server pushed for this subscription id. */
95
+ interface SubscriptionError {
96
+ code?: string;
97
+ message: string;
98
+ }
99
+ type SubscriptionErrorCallback = (error: SubscriptionError) => void;
100
+ /**
101
+ * One active per-call optimistic transform layered onto a subscription. The
102
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
103
+ * folded through every layer's `transform`, in order — so an incoming server
104
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
105
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
106
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
107
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
108
+ * mutation's response, and stays `undefined` while the write is still queued/
109
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
110
+ */
111
+ interface OptimisticLayer {
112
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
113
+ commitCursor?: number;
114
+ readonly id: symbol;
115
+ readonly transform: (current: unknown) => unknown;
116
+ }
117
+ interface SubscriptionState {
118
+ /** True once the server has acked the subscription on the current socket. */
119
+ acked: boolean;
120
+ readonly args: Record<string, unknown>;
121
+ /**
122
+ * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
123
+ * time. Cached so the optimistic-update fan-out can compare against a
124
+ * mutation's args key without re-serializing every subscription's args on
125
+ * every mutation.
126
+ */
127
+ readonly argsKey: string;
128
+ readonly callbacks: Set<SubscriptionCallback>;
129
+ /**
130
+ * Notified when a `settled` frame advances this subscription's watermark — a
131
+ * write touched the subscription's tables but the result was byte-identical,
132
+ * so the server suppressed the data frame. A `@lunora/db` list collection
133
+ * uses this to drop the optimistic overlay for the confirmed write.
134
+ *
135
+ * A SET (not a single slot) because `SubscriptionState` is SHARED across
136
+ * every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
137
+ * collection may subscribe to a query a plain `useQuery` already opened, so
138
+ * each subscriber registers its own callback (mirroring `callbacks` /
139
+ * `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
140
+ * `useQuery` consumers register nothing, leaving the set empty.
141
+ */
142
+ readonly checkpointCallbacks: Set<(watermark: {
143
+ checkpoint?: number;
144
+ mutationId?: number;
145
+ }) => void>;
146
+ /** Notified when the server rejects this subscription (e.g. admin auth). */
147
+ readonly errorCallbacks: Set<SubscriptionErrorCallback>;
148
+ readonly fn: FunctionReference;
149
+ readonly id: string;
150
+ /**
151
+ * The highest custom-mutator `mutationId` from this client the server has
152
+ * applied, captured from the last `settled` frame (the suppressed-list-frame
153
+ * watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
154
+ * Absent until a `settled` frame arrives.
155
+ */
156
+ lastMutationId?: number;
157
+ /** Last known value, used to short-circuit `useQuery`-style consumers. */
158
+ lastValue: unknown;
159
+ /**
160
+ * Active per-call optimistic layers, in application order (see
161
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
162
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
163
+ * exactly and behaviour is identical to a plain server-value assignment.
164
+ */
165
+ optimisticLayers: OptimisticLayer[];
166
+ /**
167
+ * The authoritative server value the optimistic layers fold onto — the value
168
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
169
+ * are active; diverges only while a per-call optimistic write is pending. A
170
+ * server frame updates this (and re-folds the layers); the durable read cache
171
+ * persists this, never the optimistic overlay.
172
+ */
173
+ serverBase: unknown;
174
+ /**
175
+ * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
176
+ * captured from the last `data`/`delta`/`resume` frame. Persisted to the
177
+ * durable read cache and replayed as `sinceSeq` on reconnect so the server
178
+ * can resume instead of re-snapshotting (Pillar 1b/2). Absent until the
179
+ * first cursor-stamped frame arrives.
180
+ */
181
+ serverCursor?: number;
182
+ /**
183
+ * The CDC `epoch` token the `serverCursor` belongs to, captured from the
184
+ * same frame. Replayed as `sinceEpoch` on reconnect so the server resumes
185
+ * only when the client is still on the same changelog timeline — a reset or
186
+ * recycled shard advertises a new epoch, forcing a fresh snapshot. Absent
187
+ * until the first epoch-stamped frame arrives.
188
+ */
189
+ serverEpoch?: string;
190
+ readonly shardKey?: string;
191
+ }
192
+ /**
193
+ * Active subscription registry. The client keys subscriptions by
194
+ * `(functionPath, stableWireKey(args), shardKey)` so duplicate calls share a
195
+ * single server-side registration. Args are stably encoded (keys sorted at every
196
+ * depth) so two structurally-equal arg records constructed with a different key
197
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
198
+ * duplicate subscription. Encoding the args' **wire form** keeps the key
199
+ * byte-identical for pure-JSON args while giving wire-typed args (`bigint`,
200
+ * `Date`, bytes, …) distinct stable tokens instead of a throw.
201
+ */
202
+ declare class SubscriptionRegistry {
203
+ static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
204
+ private readonly byKey;
205
+ private readonly byId;
206
+ get(key: string): SubscriptionState | undefined;
207
+ getById(id: string): SubscriptionState | undefined;
208
+ add(state: SubscriptionState): void;
209
+ remove(state: SubscriptionState): void;
210
+ all(): SubscriptionState[];
211
+ }
212
+ /**
213
+ * Read/write handle over the client's live query cache, handed to a mutation's
214
+ * `withOptimisticUpdate` callback so a single mutation can optimistically patch
215
+ * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
216
+ *
217
+ * `getQuery` reads the current value (server value or any still-pending
218
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
219
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
220
+ * settles together — confirmed on the mutation's commit cursor, or rolled back
221
+ * on failure — the same per-subscription layer machinery the single-query
222
+ * per-call `optimistic` transform uses, generalized to N queries.
223
+ */
224
+ interface OptimisticLocalStore {
225
+ /**
226
+ * Every loaded subscription on `function_`, regardless of args, paired with
227
+ * the args it was subscribed under. Mirrors Convex's `getAllQueries` — handy
228
+ * when a write must patch every variant of a list query (all channels,
229
+ * all filters) without enumerating their args up front.
230
+ */
231
+ getAllQueries: <F extends FunctionReference>(function_: F) => {
232
+ args: ArgsOf<F>;
233
+ value: ReturnOf<F> | undefined;
234
+ }[];
235
+ /**
236
+ * Current cached value for the subscribed `(function_, args)` query, or
237
+ * `undefined` when nothing is subscribed/loaded for it. Reflects any
238
+ * optimistic override already written in this batch.
239
+ */
240
+ getQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>) => ReturnOf<F> | undefined;
241
+ /**
242
+ * Write an optimistic override for the subscribed `(function_, args)`
243
+ * query. A no-op (returns without effect) when no subscription matches —
244
+ * mirroring Convex, where you only patch queries the page is watching.
245
+ */
246
+ setQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, value: ReturnOf<F> | undefined) => void;
247
+ }
248
+ /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
249
+ type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
250
+ /**
251
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
252
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
253
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
254
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
255
+ * query — not merged) and drops gaplessly on the mutation's commit cursor, like
256
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
257
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
258
+ * so the caller settles the whole batch when the mutation does.
259
+ */
260
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined) => {
261
+ confirms: ((commitCursor: number | undefined) => void)[];
262
+ rollbacks: (() => void)[];
263
+ store: OptimisticLocalStore;
264
+ };
265
+ declare const DEFAULT_MAX_BUFFER = 1024;
266
+ interface StreamHandle<T = unknown> {
267
+ /** Mark the stream complete (no more chunks); resolves any pending consumer to `done:true`. */
268
+ readonly complete: () => void;
269
+ /** Surface an error to any pending consumer; subsequent pushes are dropped. */
270
+ readonly fail: (error: Error) => void;
271
+ /**
272
+ * Push one chunk. Silent no-op once the stream is `complete`, `fail`-ed,
273
+ * or `cancel`-ed. When the buffer is already at `maxBuffer`, the stream
274
+ * is failed with a `STREAM_BACKPRESSURE` error and the push is dropped —
275
+ * the producer never sees a thrown exception.
276
+ */
277
+ readonly push: (value: T) => void;
278
+ }
279
+ interface StreamIterable<T> extends AsyncIterable<T> {
280
+ /** Cancel the stream from the consumer side: closes the iterator and notifies the registered canceller. */
281
+ cancel: () => void;
282
+ }
283
+ /**
284
+ * Build a stream handle paired with an async-iterable. The handle is the
285
+ * server-driven side (the WS dispatcher pushes chunks / completes / errors);
286
+ * the iterable is what the user awaits. `onCancel` is invoked exactly once
287
+ * when the consumer calls `.cancel()` (or `.return()`) so the client can
288
+ * send a `{type:"unsubscribe"}` frame to the server.
289
+ */
290
+ declare const createStream: <T>(options: {
291
+ maxBuffer?: number;
292
+ onCancel: () => void;
293
+ }) => {
294
+ handle: StreamHandle<T>;
295
+ iterable: StreamIterable<T>;
296
+ };
297
+ type WSState = "idle" | "connecting" | "open" | "closed";
298
+ /**
299
+ * Aggregate live-socket health across every shard connection, for a UI status
300
+ * indicator. `idle` = no socket opened yet; `connecting` = at least one socket
301
+ * is (re)connecting and none is open; `connected` = at least one socket is open;
302
+ * `offline` = sockets exist but all are down (between reconnect attempts).
303
+ */
304
+ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
305
+ /** One shard's socket + watermark state in a {@link LunoraClient.debug} snapshot. */
306
+ interface ClientDebugShard {
307
+ /**
308
+ * Highest custom-mutator watermark the server has echoed for this client on this
309
+ * shard. A write whose `clientSeq` is above this has been sent but not confirmed
310
+ * — the first thing to check when an optimistic overlay won't clear.
311
+ */
312
+ confirmedMutationWatermark: number;
313
+ /** Whether a `WebSocket` object currently exists (distinct from it being open). */
314
+ hasSocket: boolean;
315
+ /** `undefined` for the default (unsharded) connection. */
316
+ shardKey: string | undefined;
317
+ /** Whether this shard's socket has ever completed a handshake — gates offline queueing. */
318
+ wasEverConnected: boolean;
319
+ wsState: WSState;
320
+ }
321
+ /** One live query or shape subscription in a {@link LunoraClient.debug} snapshot. */
322
+ interface ClientDebugSubscription {
323
+ /** Whether the server has acknowledged the subscription on the current socket. */
324
+ acked: boolean;
325
+ /** `namespace:fn` for a query, `shape:<name>` for a replication shape. */
326
+ functionPath: string;
327
+ id: string;
328
+ kind: "query" | "shape";
329
+ /** Highest custom-mutator watermark echoed on THIS subscription (absent until a `settled`/poke frame arrives). */
330
+ lastMutationId?: number;
331
+ /** Per-call optimistic layers still folded onto this subscription's value — a non-zero count with no pending write is a leak. */
332
+ pendingOptimisticLayers: number;
333
+ /** Replicated rowset size (shapes only). */
334
+ rowCount?: number;
335
+ /** The `__cdc_log` cursor the current value reflects. */
336
+ serverCursor?: number;
337
+ shardKey: string | undefined;
338
+ /** How many callers share this subscription (subscriptions are deduped by `(fn, args, shard)`). */
339
+ subscriberCount: number;
340
+ }
341
+ /**
342
+ * Everything the sync engine believes at one instant — see
343
+ * {@link LunoraClient.debug} for why this exists.
344
+ */
345
+ interface ClientDebugSnapshot {
346
+ /** The watermark key the server's custom-mutator protocol advances per `clientSeq`. */
347
+ clientId: string;
348
+ closed: boolean;
349
+ connectionStatus: ConnectionStatus;
350
+ /** Writes waiting in the built-in offline queue (not the `@lunora/db` outbox). */
351
+ pendingWrites: number;
352
+ shards: ClientDebugShard[];
353
+ subscriptions: ClientDebugSubscription[];
354
+ }
355
+ /**
356
+ * Terminal verdict for a mutation that passed through the offline queue,
357
+ * delivered to {@link LunoraClient.onMutationSettled}.
358
+ *
359
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
360
+ * original caller can await, and which no longer exists after a reload — this
361
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
362
+ * including writes restored from durable storage in a later session. It is the
363
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
364
+ * instead of silently dropping a rolled-back optimistic row.
365
+ *
366
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
367
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
368
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
369
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
370
+ * "the caller already saw this" apart from "nothing else will report this".
371
+ */
372
+ interface MutationSettledEvent {
373
+ /** The write's args, so a listener can describe or re-offer the change. */
374
+ readonly args: Record<string, unknown>;
375
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
376
+ readonly code?: string;
377
+ /** The rejection error on `status: "rejected"`. */
378
+ readonly error?: unknown;
379
+ /** The `<file>:<function>` reference of the mutation. */
380
+ readonly functionPath: string;
381
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
382
+ readonly hadAwaiter: boolean;
383
+ /** The write's stable id (idempotency key / queue id). */
384
+ readonly id: string;
385
+ /** Shard the write targeted, if any. */
386
+ readonly shardKey?: string;
387
+ /** Terminal outcome. */
388
+ readonly status: "committed" | "rejected";
389
+ }
390
+ /**
391
+ * Per-call options for {@link LunoraClient.action} — just `shardKey`, since an
392
+ * action is not a write and carries none of the optimistic machinery. Exported
393
+ * (at the end of this file) for the same reason {@link MutationCallOptions} is:
394
+ * so the framework adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`,
395
+ * `/angular`) type their `call(args, options?)` against one canonical
396
+ * definition. Add an option here and every adapter forwards it; re-declare it
397
+ * per adapter and they silently cannot.
398
+ */
399
+ interface ActionCallOptions {
400
+ /** Route the call to a specific shard. */
401
+ shardKey?: string;
402
+ }
403
+ /**
404
+ * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
405
+ * machinery plus `shardKey`. Exported (at the end of this file) so the framework
406
+ * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
407
+ * `mutate(args, options?)` against one canonical definition instead of
408
+ * re-declaring it.
409
+ */
410
+ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
411
+ /**
412
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
413
+ * durable outbox replay a committed-but-unacked write under its *original* key
414
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
415
+ * each then gets a fresh key.
416
+ */
417
+ mutationId?: string;
418
+ optimistic?: (current: TCurrent | undefined) => TValue;
419
+ /**
420
+ * Convex-parity multi-query optimistic update. Receives an
421
+ * `OptimisticLocalStore` over the live subscription cache plus the
422
+ * mutation's args, so one mutation can patch many subscribed queries at
423
+ * once; every write is rolled back atomically if the mutation fails.
424
+ */
425
+ optimisticUpdate?: OptimisticUpdate<TArgs>;
426
+ /**
427
+ * Sync predicate evaluated just before the offline queue replays this
428
+ * write on reconnect. When it returns `false` the mutation is dropped
429
+ * instead of replayed — use it to guard against replaying writes whose
430
+ * assumptions are no longer valid (e.g. the document it referred to was
431
+ * deleted by another client while this tab was offline).
432
+ */
433
+ precondition?: () => boolean;
434
+ shardKey?: string;
435
+ }
436
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
437
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
438
+ /**
439
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
440
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
441
+ * echoed for this client. A `@lunora/db` collection feeds these into its
442
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
443
+ * rows have landed.
444
+ */
445
+ interface SyncWatermark {
446
+ checkpoint?: number;
447
+ mutationId?: number;
448
+ }
449
+ /**
450
+ * An `Error` carrying the server's machine-readable `code` and (for a
451
+ * `LunoraError`) structured `data`, plus an optional actionable `hint` (Markdown)
452
+ * and `docsUrl` resolved from the central error catalog. The client's public
453
+ * error contract for RPC/batch failures — a UI can render `hint`/`docsUrl` to
454
+ * tell the user how to fix the error. The `(string & {})` arm keeps
455
+ * forward-compat/unknown server codes assignable without losing autocomplete on
456
+ * the known {@link LunoraErrorCode} union.
457
+ */
458
+ type LunoraClientError = Error & {
459
+ code?: LunoraErrorCode | (string & {});
460
+ data?: unknown;
461
+ docsUrl?: string;
462
+ hint?: string | string[];
463
+ };
464
+ /** One demuxed result slot of a {@link LunoraClient.batch} call (plan 088). */
465
+ type BatchSlot = {
466
+ error: LunoraClientError;
467
+ ok: false;
468
+ } | {
469
+ ok: true;
470
+ value: unknown;
471
+ };
472
+ /**
473
+ * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
474
+ * a single multiplexed WebSocket.
475
+ *
476
+ * Reconnect, offline queueing, and optimistic updates are all handled here;
477
+ * see the package README for the wire protocol.
478
+ */
479
+ declare class LunoraClient {
480
+ /** 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. */
481
+ private static readonly MAX_POKE_BUFFERS;
482
+ /**
483
+ * Create a typed {@link ClientQueryRef}. Convenience wrapper around
484
+ * {@link createClientQuery} so you don't need a separate import.
485
+ * @example
486
+ * ```ts
487
+ * const sidebarOpen = LunoraClient.createClientQuery("sidebarOpen", true);
488
+ * ```
489
+ */
490
+ static createClientQuery<T>(key: string, defaultValue: T): ClientQueryRef<T>;
491
+ readonly url: string;
492
+ readonly wsUrl: string;
493
+ /** Local reactive store for {@link ClientQueryRef} values — no server round-trip. Private; reach it via `getClientQuery` / `setClientQuery` / `subscribeClientQuery`. */
494
+ private readonly clientQueryStore;
495
+ private wsToken;
496
+ /** Better-auth base path (trailing slash stripped) for the `get-session` lookup. */
497
+ private readonly authBasePath;
498
+ private readonly fetchImpl;
499
+ private readonly WebSocketImpl;
500
+ private readonly bookmark;
501
+ private readonly reconnectOptions;
502
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
503
+ private readonly connectTimeoutMs;
504
+ /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
505
+ private readonly heartbeatIntervalMs;
506
+ private readonly offlineQueue;
507
+ /**
508
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
509
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
510
+ * is bypassed, so a db app has exactly one durable write path.
511
+ */
512
+ private readonly outbox;
513
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
514
+ private readonly clientId;
515
+ /**
516
+ * Highest CDC cursor this client has seen a write commit at, per shard key
517
+ * (`""` for the default shard) — the read-your-writes bookmark sent as
518
+ * `x-lunora-min-seq`.
519
+ *
520
+ * In memory only, and deliberately: it exists to keep THIS session's reads
521
+ * behind THIS session's writes. Persisting it across reloads would pin a
522
+ * fresh page to a cursor it has no reason to require and force needless
523
+ * fallbacks to the owner.
524
+ */
525
+ private readonly shardCursors;
526
+ /**
527
+ * The server's own name for the default shard (`defaultShardKey`), learned
528
+ * from the first response to a call that named no shard. `undefined` until
529
+ * then, which is why `cursorKeyFor` falls back to a placeholder entry that
530
+ * `learnDefaultShardKey` folds in once the name arrives.
531
+ */
532
+ private defaultShardKey;
533
+ /**
534
+ * `true` when the constructor's hydration microtask has finished loading the
535
+ * durable read cache (Pillar 2) into `hydratedQueryCache`. Signals that
536
+ * the cache is ready for synchronous `peekHydratedQuery` reads.
537
+ */
538
+ private readyResolved;
539
+ /** Resolvers for `whenReady()` — called once hydration completes. */
540
+ private readyResolve;
541
+ /**
542
+ * Promise that resolves once the durable read cache has been loaded. When
543
+ * `hydrateOnStart` is not set or no query cache is configured, resolves
544
+ * immediately (the constructor creates an already-resolved promise).
545
+ */
546
+ private readonly readyPromise;
547
+ /**
548
+ * Highest custom-mutator watermark the server has echoed for this client,
549
+ * nested by identity fingerprint (`identityFingerprint() ?? ""`) then shard
550
+ * bucket (`shardKey ?? ""`) — the DO tracks one `__client_watermark` per
551
+ * `(identity, clientId)` pair, not per bucket alone, so a bucket-only key
552
+ * would let a user switch claim the previous identity's sequence and wedge
553
+ * every push on `OUT_OF_ORDER` until a reload (plan 316). A nested map
554
+ * (rather than a single map composite-keyed by a joined string) is what a
555
+ * previous fix here tried and got wrong: an identity fingerprint or a
556
+ * `shardKey` is an arbitrary string, so any string delimiter — even one as
557
+ * exotic as U+FFFD — can appear in one operand and collide with the other
558
+ * (a fingerprint of `a<SEP>x` + bucket `y` joins to the same string as
559
+ * a fingerprint of `a` + bucket `x<SEP>y`), mixing two identities' watermarks.
560
+ * The nested map has no join step, so there is no delimiter to collide.
561
+ * `callMutator` bumps it from every ack; the `@lunora/db` mutator runtime
562
+ * seeds its `clientSeq` generator from it so a reload (which resets the
563
+ * in-memory counter) never reissues a stale sequence the server would
564
+ * silently swallow as a replay.
565
+ */
566
+ private readonly clientWatermarks;
567
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
568
+ private outboxMutationCounter;
569
+ private readonly onPersistenceError;
570
+ private readonly persistence;
571
+ /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
572
+ private readonly persistenceVersion;
573
+ /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
574
+ private outboxLeaderRelease;
575
+ /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
576
+ private readonly queryCache;
577
+ /**
578
+ * Values restored from the `queryCache` at construction, keyed by the
579
+ * read-cache key, awaiting the `subscribe()` that will consume them. A
580
+ * key is consumed (deleted) the first time its subscription is created, so
581
+ * the cache only ever seeds the initial value — live frames take over after.
582
+ */
583
+ private readonly hydratedQueryCache;
584
+ /**
585
+ * Coalesced read-cache writes: the latest value per key, flushed to
586
+ * the `queryCache` on a short debounce so a burst of deltas persists once.
587
+ */
588
+ private readonly pendingCacheWrites;
589
+ private cacheFlushTimer;
590
+ private readonly subscriptions;
591
+ /**
592
+ * Cross-tab coordinator; created only when `crossTabSync: true`. When the
593
+ * client is not the elected leader, all WebSocket operations are skipped.
594
+ * Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
595
+ */
596
+ private tabCoordinator;
597
+ /**
598
+ * The leader's last-broadcast aggregate {@link ConnectionStatus}, mirrored
599
+ * on a follower tab — which owns no `ShardConnection` of its own to
600
+ * compute a status from (see `computeStatus`). `undefined` until the
601
+ * leader's first broadcast (falls back to `"idle"`), and reset back to
602
+ * `undefined` whenever this tab stops being a follower of the CURRENT
603
+ * leader (becomes leader itself, or the leader changes), so a stale
604
+ * mirror from a previous leader never survives a leadership change.
605
+ */
606
+ private leaderStatus;
607
+ /**
608
+ * Sticky "has the mirrored leader status ever reported `connected`" flag —
609
+ * the follower's counterpart to {@link ShardConnection.wasEverConnected},
610
+ * since a follower has no `ShardConnection` of its own. Feeds the
611
+ * offline-queue gate (see `mutation`) exactly like the real per-shard flag
612
+ * does on the leader/single-tab path.
613
+ */
614
+ private leaderWasEverConnected;
615
+ /** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
616
+ private readonly connections;
617
+ /** Default `connect`-envelope context applied to a shard with no explicit override. */
618
+ private readonly defaultConnectionContext;
619
+ /**
620
+ * Per-shard `connect`-envelope context registered via `setConnectionContext`
621
+ * (keyed by `shardKey ?? ""`), overriding `defaultConnectionContext`. Sent
622
+ * on every socket open so it replays across reconnects, and forwarded to the
623
+ * server's `onConnect`/`onDisconnect` lifecycle hooks. This holds only the
624
+ * imperative (last-writer-wins) override; refcounted holders registered via
625
+ * `acquireConnectionContext` live in `connectionContextHolders` and take
626
+ * precedence — see `effectiveConnectionContext`.
627
+ */
628
+ private readonly connectionContexts;
629
+ /**
630
+ * Per-shard stack of refcounted connection-context holders (keyed by
631
+ * `shardKey ?? ""`), registered via `acquireConnectionContext`. Each holder
632
+ * is an opaque token carrying its `context`; the most-recently acquired
633
+ * holder wins (last-writer-wins among live holders), and the context is only
634
+ * cleared for a shard once its last holder releases — so two concurrently
635
+ * mounted presence hooks on the same shard can't stomp each other's context
636
+ * on cleanup. A holder is identified by reference identity so a release
637
+ * removes exactly the right one regardless of stack position.
638
+ */
639
+ private readonly connectionContextHolders;
640
+ private authToken;
641
+ /**
642
+ * Optional STABLE identity subject (a user id), the basis of the offline-queue
643
+ * identity stamp when supplied. Keeps a same-user token *refresh* from looking
644
+ * like an identity change (which would discard queued writes). `undefined` =
645
+ * not supplied, so identity falls back to a hash of the raw token. See
646
+ * `setAuthToken` / `identityFingerprint`.
647
+ */
648
+ private authSubject;
649
+ /**
650
+ * Identity stamp recorded against each queued offline mutation, keyed by
651
+ * the queue-assigned mutation id. Captured at enqueue from the auth token
652
+ * in effect at the time, and re-checked at flush so a queued write can
653
+ * never replay under a different identity than the one that issued it.
654
+ * See `identityFingerprint` for the fingerprint shape.
655
+ */
656
+ private readonly queuedIdentities;
657
+ /**
658
+ * Distinct shard keys with a mutation currently sitting in `offlineQueue`
659
+ * — fresh writes queued this session (`enqueueOfflineMutation`) or writes
660
+ * restored from durable storage (`hydratePersistedQueue`). A follower tab
661
+ * has no per-shard `ShardConnection` to iterate when its mirrored leader
662
+ * status turns `"connected"` (see the `onConnectionStatus` coordinator
663
+ * option), so this is what that flush walks instead. Entries are never
664
+ * removed — flushing an already-empty shard is a cheap no-op, and the set
665
+ * is bounded by the app's own distinct shard-key cardinality.
666
+ */
667
+ private readonly queuedOfflineShardKeys;
668
+ private closed;
669
+ /** Subscribers to auth-token changes (see `onAuthTokenChange`). */
670
+ private readonly authTokenListeners;
671
+ /** Subscribers to aggregate connection-status changes (see `onConnectionStatus`). */
672
+ private readonly statusListeners;
673
+ /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
674
+ private readonly tokenExpiredListeners;
675
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
676
+ private readonly mutationSettledListeners;
677
+ /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
678
+ private readonly pendingChangeListeners;
679
+ /**
680
+ * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
681
+ * of callbacks. Membership doubles as the resubscribe set replayed on every
682
+ * (re)connect so a topic survives a socket bounce.
683
+ */
684
+ private readonly whisperHandlers;
685
+ /** Last status broadcast, so we only notify listeners on an actual change. */
686
+ private lastStatus;
687
+ private nextSubId;
688
+ private nextStreamId;
689
+ /**
690
+ * In-flight client-side stream readers, keyed by the stream id sent on the
691
+ * wire. The handle drives the underlying iterator queue and `shardKey`
692
+ * tells us which socket to push the cancel frame onto when the consumer
693
+ * calls `.cancel()` or the iterator is garbage-collected.
694
+ */
695
+ private readonly streams;
696
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
697
+ private readonly shapeSubscriptions;
698
+ /**
699
+ * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
700
+ * `<connectionKey>\u0000<pokeId>`.
701
+ *
702
+ * The connection has to be in the key: `pokeId` is a per-DO counter that also
703
+ * resets on eviction, so a multi-shard client — one socket per shard, one map
704
+ * here — sees two shards mint `poke-1` concurrently. Keyed by `pokeId` alone
705
+ * their frames interleave into a single buffer: one shard applies the other's
706
+ * epoch (spurious fork → view wiped) and the other finds no buffer for its
707
+ * parts at all, silently dropping rows while the server's memo advances.
708
+ */
709
+ private readonly pokeBuffers;
710
+ private nextShapeId;
711
+ constructor(options: LunoraClientOptions);
712
+ /**
713
+ * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
714
+ * {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
715
+ * sync across all mounted instances.
716
+ *
717
+ * Pass a STABLE `subject` (the user id) to key the offline-queue identity on
718
+ * it instead of the token bytes, so a token *refresh* (same user, new JWT)
719
+ * doesn't read as an identity change and discard queued writes. The subject is
720
+ * **sticky**: a later call that omits it (or passes `undefined`) keeps the
721
+ * established subject — so `setAuthToken(refreshedToken)` after a prior
722
+ * `setAuthToken(token, user.id)` retains the identity. Pass `null` to clear it
723
+ * (an explicit sign-out). Establishing the subject for the first time on an
724
+ * UNCHANGED token (e.g. the user id resolves a tick after the token was set)
725
+ * re-stamps any in-flight queued writes rather than dropping them — same
726
+ * credential, just a more stable label. A real user switch (the token AND
727
+ * subject both change) still drops the previous user's writes.
728
+ *
729
+ * Does NOT update the WebSocket auth — the WS token is fixed at upgrade
730
+ * time and lives in the URL. To refresh live WS auth, call
731
+ * {@link setWsToken} explicitly, which closes existing shard sockets to
732
+ * force a reconnect with the new credential.
733
+ */
734
+ setAuthToken(token: string | null, subject?: string | null): void;
735
+ getAuthToken(): string | null;
736
+ /**
737
+ * The current identity fingerprint (the same stamp queued offline writes
738
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
739
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
740
+ * can drop a persisted write whose captured `identity` no longer matches the
741
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
742
+ */
743
+ currentIdentity(): string | null;
744
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
745
+ clientIdentifier(): string;
746
+ /**
747
+ * The highest custom-mutator watermark the server has echoed for this client
748
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
749
+ * its `clientSeq` generator from this so a reload never reissues a sequence
750
+ * the server has already applied (which it would swallow as a replay, silently
751
+ * dropping the write).
752
+ */
753
+ confirmedMutationWatermark(shardKey?: string): number;
754
+ /**
755
+ * Push a custom mutator to its authoritative server impl over the watermark
756
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
757
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
758
+ * client's `__client_watermark`.
759
+ *
760
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
761
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
762
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
763
+ * A `false` verdict tells the caller to reissue above the now-known watermark
764
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
765
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
766
+ *
767
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
768
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
769
+ */
770
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
771
+ clientSeq?: number;
772
+ shardKey?: string;
773
+ }): Promise<{
774
+ applied: boolean;
775
+ result: unknown;
776
+ }>;
777
+ /**
778
+ * Subscribe to auth-token changes. Returns an unsubscribe function. The
779
+ * listener is NOT invoked on registration — use {@link getAuthToken} for
780
+ * the current value.
781
+ */
782
+ onAuthTokenChange(listener: (token: string | null) => void): Unsubscribe;
783
+ /**
784
+ * Fetch the currently authenticated user from better-auth's `get-session`
785
+ * endpoint, returning the `user` record or `null` when signed out. Sends
786
+ * the stored bearer token (if any) and `credentials: "include"` so a
787
+ * cookie-session is also honoured. A network/parse failure or a non-OK
788
+ * response resolves to `null` rather than throwing — callers treat "couldn't
789
+ * resolve identity" as "signed out".
790
+ *
791
+ * Framework-agnostic: pair it with {@link onAuthTokenChange} to refetch when
792
+ * the token changes (that's what `@lunora/react`'s `useAuth` does).
793
+ */
794
+ getCurrentUser(): Promise<User | null>;
795
+ /**
796
+ * Replace the token appended to WS upgrade URLs as `?token=…` and close
797
+ * every open shard socket so the reconnect picks up the new value. Call
798
+ * this whenever the user's WS credential changes (rotating the admin token
799
+ * in the studio, switching workspaces, etc.). Accepts a static string or a
800
+ * {@link WsTokenProvider} resolved fresh at every (re)connect — the channel
801
+ * for short-lived credentials like the minted ephemeral admin sub-token.
802
+ * Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
803
+ */
804
+ setWsToken(token: string | undefined | WsTokenProvider): void;
805
+ /**
806
+ * Register (or clear, with `undefined`) the app context sent in the `connect`
807
+ * envelope for a shard's socket, overriding the client-wide
808
+ * {@link LunoraClientOptions.connectionContext}. The server forwards it to the
809
+ * `onConnect`/`onDisconnect` lifecycle hooks as `event.context` — e.g.
810
+ * `@lunora/react`'s `usePresence` registers `{ roomId, sessionId }` so the
811
+ * presence row is removed the instant the socket drops, with no TTL lag.
812
+ *
813
+ * Stored per shard and replayed on every (re)connect. When a socket for the
814
+ * shard is already open, a fresh `connect` envelope is sent immediately so the
815
+ * server sees the new context without waiting for a reconnect.
816
+ */
817
+ setConnectionContext(context: Record<string, unknown> | undefined, options?: {
818
+ shardKey?: string;
819
+ }): void;
820
+ /**
821
+ * Refcounted variant of {@link setConnectionContext}: register a connection
822
+ * `context` for a shard and get back a release function. Unlike the imperative
823
+ * setter, the context is only cleared once the *last* acquired holder releases
824
+ * it — so two components (e.g. two mounted `usePresence` hooks) on the same
825
+ * shard no longer clobber each other's context when one of them unmounts. The
826
+ * most-recently acquired live holder wins (last-writer-wins), and releasing
827
+ * the top holder falls back to the previous one rather than clearing.
828
+ *
829
+ * With a single holder the behaviour is identical to a
830
+ * `setConnectionContext(context)` / `setConnectionContext(undefined)` pair.
831
+ * Releasing more than once is a no-op (the holder is matched by reference, so
832
+ * a double release can't drop a different holder).
833
+ */
834
+ acquireConnectionContext(context: Record<string, unknown>, options?: {
835
+ shardKey?: string;
836
+ }): Unsubscribe;
837
+ /**
838
+ * Join a whisper `topic` and receive every ephemeral message other members
839
+ * broadcast to it on the same shard (typing indicators, live cursors,
840
+ * presence pings). Whispers never touch the server's durable state — there's
841
+ * no query, no row, no CDC entry. Returns an unsubscribe function; the topic
842
+ * is left on the server once its last local handler unsubscribes.
843
+ *
844
+ * `handler` receives the raw `data` and the sender's verified `from` user id
845
+ * (omitted for an anonymous sender). The topic is scoped to `options.shardKey`
846
+ * (the default shard when omitted) — use the same shard you target with the
847
+ * matching queries/mutations so members land on the same Durable Object.
848
+ *
849
+ * Security: whisper topics are NOT access-controlled beyond the shard
850
+ * boundary — any client that can open a socket to the shard can join, read,
851
+ * and inject on any topic name. `from` is server-stamped and unforgeable, but
852
+ * do not put data on a whisper topic that some shard members shouldn't see,
853
+ * and don't trust a whisper's `data` as authorization. Use a query/mutation
854
+ * (with RLS) for anything privileged; whispers are for transient awareness.
855
+ */
856
+ whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
857
+ shardKey?: string;
858
+ }): Unsubscribe;
859
+ /**
860
+ * Broadcast an ephemeral `data` payload to the other members of a whisper
861
+ * `topic` on `options.shardKey`'s shard. Fire-and-forget: the frame is
862
+ * dropped when the shard socket isn't open (whispers are transient, never
863
+ * queued), and the server silently drops it if the sender exceeds its
864
+ * whisper rate budget. The sender never receives its own whisper. Omitting
865
+ * `data` delivers JSON `null` to receivers (not `undefined`).
866
+ */
867
+ whisper(topic: string, data?: unknown, options?: {
868
+ shardKey?: string;
869
+ }): void;
870
+ /**
871
+ * Subscribe to token-expiry events: invoked whenever the server drops a
872
+ * shard socket because the connection's credential lapsed (close code
873
+ * `4001`). The client already reconnects automatically (re-resolving
874
+ * identity from the cookie/token in effect); use this to refresh a
875
+ * short-lived token first — e.g. call {@link setWsToken} / {@link setAuthToken}
876
+ * with a freshly minted one. Returns an unsubscribe function.
877
+ */
878
+ onTokenExpired(listener: () => void): Unsubscribe;
879
+ /**
880
+ * Current aggregate live-socket status across all shard connections. See
881
+ * {@link ConnectionStatus}.
882
+ */
883
+ connectionStatus(): ConnectionStatus;
884
+ /**
885
+ * Subscribe to aggregate connection-status changes. Invokes `listener`
886
+ * immediately with the current status, then on every transition. Returns an
887
+ * unsubscribe function.
888
+ */
889
+ onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
890
+ /**
891
+ * Number of offline writes waiting in the built-in queue to be sent — the
892
+ * depth for a "N changes waiting to sync" indicator. Counts writes that are
893
+ * queued (offline / mid-reconnect), not ones already in flight on the wire.
894
+ * A `@lunora/db` app whose writes ride the unified outbox should read
895
+ * `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
896
+ */
897
+ pendingCount(): number;
898
+ /**
899
+ * A point-in-time snapshot of everything the sync engine believes right now:
900
+ * per-shard sockets and watermarks, every live query and shape subscription with
901
+ * its cursor and ack state, and the offline-queue depth.
902
+ *
903
+ * This exists because the alternative is `console.log`. When an optimistic
904
+ * overlay doesn't clear, the questions are always the same — *is the socket open?
905
+ * what watermark has the server confirmed for this shard? has this shape been
906
+ * poked since my write? is anything stuck in the queue?* — and none of them were
907
+ * answerable from outside the client, so every adopter ends up instrumenting the
908
+ * library by hand or building a bespoke policy layer around a symptom.
909
+ *
910
+ * Read it from a devtools console, log it next to a bug report, or render it in a
911
+ * debug panel. Pull-only and allocation-cheap; nothing here is reactive, so poll
912
+ * it or read it on demand.
913
+ *
914
+ * ```ts
915
+ * const { shards, subscriptions } = client.debug();
916
+ * // shards: [{ shardKey: "user-1", wsState: "open", confirmedMutationWatermark: 42, … }]
917
+ * ```
918
+ */
919
+ debug(): ClientDebugSnapshot;
920
+ /**
921
+ * Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
922
+ * with the current count, then whenever the queue depth changes (a write is
923
+ * enqueued, flushed, or discarded). Returns an unsubscribe function.
924
+ */
925
+ onPendingChange(listener: (pending: number) => void): Unsubscribe;
926
+ /**
927
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
928
+ * fires once per queued write that commits or is rejected — including a write
929
+ * restored from durable storage after a reload, whose original `mutation()`
930
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
931
+ * evicts on overflow or discards on an identity change. This is the durable
932
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
933
+ * mutation that never queued still surfaces through the Promise `mutation()`
934
+ * returns. The listener is NOT invoked on registration. Returns an
935
+ * unsubscribe function. See {@link MutationSettledEvent}.
936
+ */
937
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
938
+ /**
939
+ * The `WebSocket` implementation this client was constructed with (an
940
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
941
+ * one) — `undefined` if neither is available. This is the seam a feature
942
+ * that opens its OWN socket outside the client's multiplexed connection
943
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
944
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
945
+ * constructor to inject the auth-headers factory's credential onto the
946
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
947
+ * `new globalThis.WebSocket(url)` would silently bypass.
948
+ */
949
+ getWebSocketImpl(): typeof WebSocket | undefined;
950
+ /**
951
+ * Read the current value for a {@link ClientQueryRef}. Returns
952
+ * `ref.defaultValue` when no value has been explicitly set.
953
+ */
954
+ getClientQuery<T>(ref: ClientQueryRef<T>): T;
955
+ /**
956
+ * Set a new value for `ref` and notify every subscriber. Pass `undefined`
957
+ * to reset the slot to `ref.defaultValue`.
958
+ */
959
+ setClientQuery<T>(ref: ClientQueryRef<T>, value: T): void;
960
+ /**
961
+ * Subscribe to changes for `ref`. The callback is NOT invoked on
962
+ * registration — call {@link getClientQuery} for the current value.
963
+ * Returns an unsubscribe function.
964
+ */
965
+ subscribeClientQuery(ref: ClientQueryRef, callback: (value: unknown) => void): Unsubscribe;
966
+ /**
967
+ * Reset a {@link ClientQueryRef} to its default value, notifying every
968
+ * subscriber. Equivalent to `setClientQuery(ref, ref.defaultValue)` but
969
+ * removes the stored entry so a future {@link getClientQuery} returns
970
+ * the default rather than an explicitly-set value.
971
+ */
972
+ resetClientQuery(ref: ClientQueryRef): void;
973
+ /**
974
+ * Capture a snapshot of the current live query value at call time and
975
+ * produce a `() => boolean` precondition that compares it against the
976
+ * value at replay time (on queue drain / reconnect).
977
+ *
978
+ * Delegates to {@link createSnapshotPrecondition} with this client bound —
979
+ * no need to pass `client` explicitly. The comparison semantics (including
980
+ * how an absent subscription is treated) live there, in one place.
981
+ * @example
982
+ * ```ts
983
+ * client.mutation(api.todos.update, { id, text }, {
984
+ * precondition: client.snapshotPrecondition(api.todos.list, { userId }),
985
+ * });
986
+ * ```
987
+ */
988
+ snapshotPrecondition(functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string): () => boolean;
989
+ /**
990
+ * Resolves once the durable read cache has been loaded into memory. When
991
+ * `hydrateOnStart` is not configured or no query cache adapter is active,
992
+ * returns an already-resolved promise so callers can always await it
993
+ * unconditionally.
994
+ *
995
+ * Framework adapters (React, Vue, etc.) use this to gate the first
996
+ * (enabled) render of a live query behind hydration, so the user sees
997
+ * cached data instead of an undefined flash before the socket round-trip.
998
+ */
999
+ whenReady(): Promise<void>;
1000
+ /**
1001
+ * Synchronously reports whether {@link whenReady} has already resolved (the
1002
+ * durable read cache is loaded, or none is configured). Framework adapters
1003
+ * read this to seed the hydration-gate state on the first render without
1004
+ * awaiting, then subscribe via {@link whenReady} for the pending case.
1005
+ */
1006
+ get isReady(): boolean;
1007
+ /**
1008
+ * Synchronously peek at a value the durable read cache loaded for the given
1009
+ * function path + args + shard key. Returns `undefined` when:
1010
+ *
1011
+ * - No query cache adapter is configured.
1012
+ * - Hydration hasn't completed yet (race — await {@link whenReady} first).
1013
+ * - The cached value's identity fingerprint doesn't match the current auth.
1014
+ *
1015
+ * Unlike the internal {@link takeHydratedCache}, this is a READ-ONLY peek:
1016
+ * the cached entry stays in `hydratedQueryCache` so the subscription created
1017
+ * later by {@link subscribe} consumes it normally.
1018
+ */
1019
+ peekHydratedQuery(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
1020
+ /**
1021
+ * Peek at the **current live value** of an active subscription, reporting
1022
+ * whether one exists at all rather than just its value. `present` is `false`
1023
+ * when no subscription is open for the given `(functionPath, args, shardKey)`;
1024
+ * `value` is the subscription's `lastValue`, which includes any optimistic
1025
+ * overlay.
1026
+ *
1027
+ * The two are separate because a caller like the snapshot precondition has to
1028
+ * tell "no subscription is active, so this read knows nothing" apart from
1029
+ * "the subscription is active and its value is `undefined`" — collapsing both
1030
+ * into a bare `undefined` return makes an unmounted component look like a
1031
+ * changed value, and drops the queued write.
1032
+ *
1033
+ * Unlike {@link peekHydratedQuery} (which reads from the durable read cache
1034
+ * and is independent of active subscriptions), this reflects the current
1035
+ * in-memory state of an already-opened subscription.
1036
+ */
1037
+ peekActiveQuerySnapshot(functionPath: string, args: Record<string, unknown>, shardKey?: string): {
1038
+ present: boolean;
1039
+ value: unknown;
1040
+ };
1041
+ query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
1042
+ shardKey?: string;
1043
+ }): Promise<ReturnOf<F>>;
1044
+ /**
1045
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
1046
+ * dispatched server-side exactly as an individual RPC — per-shard
1047
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
1048
+ * watermark ordering are all preserved — and the worker splits the batch by
1049
+ * shard so calls to different shards fan out to their own DOs. Results are
1050
+ * demuxed back in input order; a failing call does NOT fail the batch (its
1051
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
1052
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
1053
+ *
1054
+ * No promise pipelining and no capability passing — a call's args cannot
1055
+ * reference another call's result (see plan 088 §fence; capabilities are
1056
+ * incompatible with DO hibernation).
1057
+ */
1058
+ batch(calls: ReadonlyArray<{
1059
+ args?: Record<string, unknown>;
1060
+ fn: FunctionReference;
1061
+ shardKey?: string;
1062
+ }>): Promise<BatchSlot[]>;
1063
+ /**
1064
+ * Invoke a mutation. Errors propagate as rejections.
1065
+ *
1066
+ * Offline-queue semantics: a mutation is queued (and replayed on reconnect)
1067
+ * only when the targeted shard's socket was open at least once already
1068
+ * (`wasEverConnected`), so the registry / resubscribe handshake has run.
1069
+ * Mutations issued before the very first WS connect to a shard fail fast.
1070
+ * Opt into queueing-before-first-connect via
1071
+ * `OfflineQueueOptions.queueBeforeFirstConnect`.
1072
+ */
1073
+ mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
1074
+ action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
1075
+ /**
1076
+ * Bulk-import `rows` through a mutation that accepts a batch, chunked so a large
1077
+ * dataset lands in a bounded number of round-trips.
1078
+ *
1079
+ * **Offline caveat:** each chunk is sent with {@link LunoraClient.mutation}, which
1080
+ * resolves once the write is durably queued rather than once the server has applied
1081
+ * it. So an import run while offline resolves `{ chunks, imported }` with nothing
1082
+ * committed yet — the counts describe what was *handed over*, and the outbox
1083
+ * replays them on reconnect. Don't report "migration complete" on this alone.
1084
+ *
1085
+ * This is the one-shot migration / seed path: "I have 20k rows client-side and a
1086
+ * server mutation that inserts many at once". Doing it by hand goes wrong in two
1087
+ * predictable ways — a serial per-row loop pays one round-trip *and* one watermark
1088
+ * wait per row (a 200-row import becomes 200 sequential hops), while a single
1089
+ * giant call blows the DO's batch limit. So: chunk, send sequentially, and give
1090
+ * each chunk a stable idempotency key derived from `importId` + its index, so a
1091
+ * resumed or retried import doesn't double-insert the chunks that already landed.
1092
+ *
1093
+ * The server mutation is yours (Lunora can't guess the table or the row shape);
1094
+ * back it with `ctx.db.insertMany(...)`, or `insertManyUnsafe(...)` for data you
1095
+ * vouch for. `chunkSize` defaults to 500, matching the DO's default batch cap.
1096
+ *
1097
+ * ```ts
1098
+ * await client.importRows(api.migrate.importNodes, nodes, {
1099
+ * importId: `migrate-${userId}`,
1100
+ * onProgress: ({ done, total }) => setProgress(done / total),
1101
+ * shardKey: userId,
1102
+ * toArgs: (chunk) => ({ nodes: chunk }),
1103
+ * });
1104
+ * ```
1105
+ */
1106
+ importRows(function_: FunctionReference, rows: ReadonlyArray<unknown>, options?: {
1107
+ /** Rows per call. Defaults to 500 — the DO's default batch cap. */
1108
+ chunkSize?: number;
1109
+ /**
1110
+ * Stable id for this import run. Each chunk is sent under
1111
+ * `${importId}:${chunkIndex}` as its mutation id, so re-running an import
1112
+ * that uses the SAME `chunkSize` re-sends each chunk under its prior key
1113
+ * and the server dedupes it instead of inserting twice.
1114
+ *
1115
+ * CAVEAT — the key is POSITIONAL, not content-based: it pins on the chunk
1116
+ * INDEX, not on the rows inside it. Resume or retry with a DIFFERENT
1117
+ * `chunkSize` (or a changed row ordering) and index N now covers different
1118
+ * rows than the first run's index N; the server sees a duplicate key and
1119
+ * SILENTLY DROPS those rows. Keep `chunkSize` (and the row order) identical
1120
+ * across resumes of the same `importId`. Omit `importId` only for a
1121
+ * throwaway import where double-insertion is acceptable.
1122
+ */
1123
+ importId?: string;
1124
+ /**
1125
+ * Called after each chunk is accepted — for a progress bar.
1126
+ *
1127
+ * "Accepted" is not always "committed": while offline (or mid-reconnect) a
1128
+ * `mutation` resolves as soon as the write is durably **queued**, so a fully
1129
+ * offline import reports completion with nothing yet applied server-side.
1130
+ * Gate a migration's "done" state on connectivity, not just on this.
1131
+ */
1132
+ onProgress?: (progress: {
1133
+ done: number;
1134
+ total: number;
1135
+ }) => void;
1136
+ /** Routes every chunk to one shard's DO. */
1137
+ shardKey?: string;
1138
+ /** Build the mutation args for one chunk. Defaults to `{ rows: chunk }`. */
1139
+ toArgs?: (chunk: ReadonlyArray<unknown>) => Record<string, unknown>;
1140
+ }): Promise<{
1141
+ chunks: number;
1142
+ imported: number;
1143
+ }>;
1144
+ /**
1145
+ * Read the cross-shard request distribution for a `.shardBy(...)` table —
1146
+ * the feed the studio's `hot_shard` advisor lint consumes. Hits the
1147
+ * admin-gated `POST /_lunora/admin/shard-traffic` endpoint, which fans the
1148
+ * cheap per-shard `getMetrics` read out across every live shard and returns
1149
+ * each shard's `{ shardKey, requests }` total (a failed shard surfaces with
1150
+ * `requests: 0`). Requires the worker to be built with a `queryCoordinator`
1151
+ * and `adminToken`, and this client's auth token to match; defaults any
1152
+ * absent field so an older worker yields an empty-but-valid shape.
1153
+ */
1154
+ shardTraffic(table: string): Promise<ShardTrafficResult>;
1155
+ /**
1156
+ * List the functions queued via `runAfter` / `runAt`, soonest-due last
1157
+ * (the worker returns them in storage order). Hits the admin-gated
1158
+ * `/_lunora/admin/scheduled` endpoint, so the worker must be built with a
1159
+ * `schedulerDO` namespace and `adminToken`, and this client's auth token
1160
+ * must match. Powers `@lunora/studio`'s scheduled-jobs panel.
1161
+ */
1162
+ listScheduledJobs(): Promise<ScheduleRecord[]>;
1163
+ /**
1164
+ * Read the app-level workpool backlog that powers `@lunora/studio`'s SLO
1165
+ * view: per-pool `{ name, queued, inFlight, maxConcurrency }` plus the
1166
+ * app-wide `backlog` (total queued) and `inFlight` (total held slots) sums.
1167
+ * Hits the admin-gated `GET /_lunora/admin/scheduled/status` endpoint, so the
1168
+ * same preconditions as {@link listScheduledJobs} apply (a `schedulerDO`
1169
+ * namespace + `adminToken` on the worker and a matching auth token here).
1170
+ * Defaults any absent field so an older worker still yields a valid shape.
1171
+ */
1172
+ schedulerStatus(): Promise<SchedulerStatus>;
1173
+ /** Cancel a pending scheduled job by id. Returns whether a job was removed. */
1174
+ cancelScheduledJob(id: string): Promise<{
1175
+ cancelled: boolean;
1176
+ }>;
1177
+ /**
1178
+ * List the dead-letter jobs: schedules that exhausted their retry budget
1179
+ * and were parked instead of dropped. These never appear in
1180
+ * {@link listScheduledJobs} (their live header is gone), so this is the only
1181
+ * way the studio surfaces a permanently-failed job. Hits the admin-gated
1182
+ * `GET /_lunora/admin/scheduled/dead`; same preconditions as
1183
+ * {@link listScheduledJobs}. Powers `@lunora/studio`'s dead-letter panel.
1184
+ */
1185
+ listDeadJobs(): Promise<ScheduleRecord[]>;
1186
+ /**
1187
+ * Resurrect a dead-letter job by id: it re-enters the schedule with a fresh
1188
+ * retry budget and fires on the next drain. Returns whether a parked record
1189
+ * matched. Hits the admin-gated `POST /_lunora/admin/scheduled/dead/retry`.
1190
+ */
1191
+ retryDeadJob(id: string): Promise<{
1192
+ retried: boolean;
1193
+ }>;
1194
+ /**
1195
+ * Permanently drop a dead-letter job by id (the operator has decided not to
1196
+ * recover it). Returns whether a parked record was removed. Hits the
1197
+ * admin-gated `POST /_lunora/admin/scheduled/dead/cancel`.
1198
+ */
1199
+ removeDeadJob(id: string): Promise<{
1200
+ removed: boolean;
1201
+ }>;
1202
+ /**
1203
+ * List a workflow's instances via the admin Workflows proxy
1204
+ * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1205
+ * the `Workflow` binding can't expose. Requires the worker to be built with a
1206
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1207
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1208
+ * false }` sentinel, so the result resolves with `configured === false` and an
1209
+ * empty `instances` list — callers should branch on that flag rather than
1210
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1211
+ * `name` is the deployed workflow name.
1212
+ */
1213
+ listWorkflowInstances(options: {
1214
+ name: string;
1215
+ page?: number;
1216
+ perPage?: number;
1217
+ status?: WorkflowInstanceStatus;
1218
+ }): Promise<WorkflowInstancePage>;
1219
+ /** Read one workflow instance with its step timeline (`/_lunora/admin/workflows/instance`). */
1220
+ getWorkflowInstance(options: {
1221
+ id: string;
1222
+ name: string;
1223
+ }): Promise<WorkflowInstanceDetail>;
1224
+ /** Pause / resume / terminate a workflow instance (`/_lunora/admin/workflows/status`). Needs an Edit-scoped Cloudflare token. */
1225
+ setWorkflowInstanceStatus(options: {
1226
+ action: WorkflowInstanceAction;
1227
+ id: string;
1228
+ name: string;
1229
+ }): Promise<{
1230
+ status: WorkflowInstanceStatus;
1231
+ }>;
1232
+ /**
1233
+ * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
1234
+ * WebSocket. `onJobs` fires with the full list on connect and on every
1235
+ * change (schedule / cancel / alarm-fire). Reconnects with the client's
1236
+ * configured backoff. Requires `wsToken` to be set to an admin credential
1237
+ * (the browser can't send an `Authorization` header on a WS) — the master
1238
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
1239
+ * sub-token so the master credential stays out of the URL. Returns an
1240
+ * unsubscribe function that closes the socket and stops reconnecting.
1241
+ */
1242
+ subscribeScheduledJobs(onJobs: (jobs: ScheduleRecord[]) => void): Unsubscribe;
1243
+ /**
1244
+ * List the registered public functions (queries / mutations / actions) with
1245
+ * their kinds. Hits the admin-gated `GET /_lunora/admin/functions` endpoint —
1246
+ * the worker must be built with a `functions` registry and `adminToken`, and
1247
+ * this client's auth token must match. Powers `@lunora/studio`'s function
1248
+ * runner auto-discovery.
1249
+ */
1250
+ listFunctions(): Promise<FunctionDescriptor[]>;
1251
+ /**
1252
+ * List the code-defined cron triggers (the `cronJobs()` map injected on the
1253
+ * worker), each flattened to its firing `cron` expression. Hits the
1254
+ * admin-gated `GET /_lunora/admin/cron-jobs` endpoint — the worker must be
1255
+ * built with a `cronJobs` map and `adminToken`, and this client's auth token
1256
+ * must match. These are static (Cloudflare exposes no runtime cron
1257
+ * introspection), so the studio renders them read-only alongside the dynamic
1258
+ * scheduler jobs.
1259
+ */
1260
+ getCronJobs(): Promise<CronJobInfo[]>;
1261
+ /**
1262
+ * Manually fire one code-defined cron job by name — the same dispatch the
1263
+ * scheduled trigger runs (dispatch the function, or start the durable
1264
+ * workflow), on demand. Hits the admin-gated `POST /_lunora/admin/cron-jobs/run`
1265
+ * endpoint; the worker must be built with a `cronJobs` map and `adminToken`,
1266
+ * and this client's auth token must match. Resolves when the job has run (a
1267
+ * function job's shard response is 2xx, or the workflow instance was created)
1268
+ * and rejects with the dispatch error otherwise.
1269
+ */
1270
+ runCronJob(name: string): Promise<{
1271
+ name: string;
1272
+ ran: boolean;
1273
+ }>;
1274
+ /**
1275
+ * Fetch the generated OpenAPI 3.1 document. Hits the admin-gated
1276
+ * `GET /_lunora/admin/openapi` endpoint — the worker must be built with an
1277
+ * `openApiSpec` and `adminToken`, and this client's auth token must match.
1278
+ * Powers `@lunora/studio`'s API-reference (Scalar) view. When the worker has
1279
+ * no spec wired, the endpoint still resolves with an empty-but-valid OpenAPI
1280
+ * document (no `paths`), so callers can render a "not configured" state.
1281
+ */
1282
+ fetchOpenApi(): Promise<Record<string, unknown>>;
1283
+ /**
1284
+ * Fetch the generated OpenRPC 1.x document. Hits the admin-gated
1285
+ * `GET /_lunora/admin/openrpc` endpoint — the worker must be built with an
1286
+ * `openRpcSpec` and `adminToken`, and this client's auth token must match.
1287
+ * OpenRPC is the RPC-native spec (a `methods` array over the JSON-RPC-shaped
1288
+ * `POST /_lunora/rpc` transport); it documents the RPC functions only.
1289
+ * Powers `@lunora/studio`'s OpenRPC API-reference view. When the worker has
1290
+ * no spec wired, the endpoint still resolves with an empty-but-valid OpenRPC
1291
+ * document (no `methods`), so callers can render a "not configured" state.
1292
+ */
1293
+ fetchOpenRpc(): Promise<Record<string, unknown>>;
1294
+ /**
1295
+ * List objects in the storage bucket, optionally under a `prefix` and from a
1296
+ * pagination `cursor`. Hits the admin-gated `GET /_lunora/admin/storage`
1297
+ * endpoint — the worker must be built with a `storageList` function and
1298
+ * `adminToken`, and this client's auth token must match. Powers
1299
+ * `@lunora/studio`'s file browser.
1300
+ */
1301
+ listStorageObjects(options?: {
1302
+ bucket?: string;
1303
+ cursor?: string;
1304
+ limit?: number;
1305
+ prefix?: string;
1306
+ }): Promise<StorageListPage>;
1307
+ /**
1308
+ * Delete one object from the storage bucket by key. Hits the admin-gated
1309
+ * `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
1310
+ * with a `storageDelete` function and `adminToken`. Powers the studio file
1311
+ * browser's per-row delete; resolves `{ deleted, key }`.
1312
+ */
1313
+ deleteStorageObject(key: string, options?: {
1314
+ bucket?: string;
1315
+ }): Promise<{
1316
+ deleted: boolean;
1317
+ key: string;
1318
+ }>;
1319
+ /**
1320
+ * List the storage bucket names the worker exposes, for the studio file
1321
+ * browser's bucket picker. Hits the admin-gated
1322
+ * `GET /_lunora/admin/storage/buckets` endpoint — always resolves (an empty
1323
+ * array when the worker configures no `storageBuckets`, i.e. single-bucket).
1324
+ */
1325
+ listStorageBuckets(): Promise<string[]>;
1326
+ /**
1327
+ * Upload one object to the storage bucket. Hits the admin-gated
1328
+ * `PUT /_lunora/admin/storage?key=…` endpoint with the raw body and an
1329
+ * optional `contentType` header — the worker must be built with a
1330
+ * `storageUpload` function and `adminToken`. Powers the studio file
1331
+ * browser's upload control; resolves `{ etag?, key }`.
1332
+ */
1333
+ uploadStorageObject(options: {
1334
+ body: ArrayBuffer | Blob;
1335
+ bucket?: string;
1336
+ contentType?: string;
1337
+ key: string;
1338
+ }): Promise<{
1339
+ etag?: string;
1340
+ key: string;
1341
+ }>;
1342
+ /**
1343
+ * Build a (signed or public) URL for one object. Hits the admin-gated
1344
+ * `GET /_lunora/admin/storage/url?key=…` endpoint — the worker must be built
1345
+ * with a `storageSignedUrl` function and `adminToken`. Powers the studio
1346
+ * file browser's copy-URL action; resolves the URL string.
1347
+ *
1348
+ * `options.expiresInSeconds` requests a share-link lifetime, which is
1349
+ * validated/clamped server-side. The options object mirrors the worker's
1350
+ * `StorageSignedUrlFunction` options (a `password` / download-limit are noted
1351
+ * as future fields there).
1352
+ */
1353
+ signedStorageUrl(key: string, options?: {
1354
+ bucket?: string;
1355
+ expiresInSeconds?: number;
1356
+ }): Promise<string>;
1357
+ /**
1358
+ * List the `.global()` (D1-backed) tables with their row counts. Hits the
1359
+ * admin-gated `GET /_lunora/admin/global/tables` endpoint — the worker must
1360
+ * be built with a `globalIntrospector` and `adminToken`. Powers the data
1361
+ * browser's global mode.
1362
+ */
1363
+ listGlobalTables(): Promise<GlobalTableInfo[]>;
1364
+ /**
1365
+ * Read a page of rows from one `.global()` table. `filters` AND-narrows the
1366
+ * page to rows matching each `column = value` eq constraint — the drill-down a
1367
+ * facet-value click applies; the array is JSON-encoded into the `filters`
1368
+ * query param and the values are bound server-side.
1369
+ */
1370
+ readGlobalTablePage(options: {
1371
+ filters?: GlobalFilterClause[];
1372
+ limit?: number;
1373
+ offset?: number;
1374
+ table: string;
1375
+ }): Promise<GlobalTablePage>;
1376
+ /**
1377
+ * Summarise the distinct values of one column in a `.global()` table over the
1378
+ * active view (the same eq `filters` the browser is previewing) — the global
1379
+ * twin of the shard browser's facet. Hits the admin-gated
1380
+ * `GET /_lunora/admin/global/facet` endpoint; `column` is validated + bound
1381
+ * server-side. Powers the global data browser's facet sidebar.
1382
+ */
1383
+ facetGlobalColumn(options: {
1384
+ column: string;
1385
+ filters?: GlobalFilterClause[];
1386
+ limit?: number;
1387
+ table: string;
1388
+ }): Promise<GlobalFacetResult>;
1389
+ /**
1390
+ * List the schema's Vectorize indexes with their declared shape (table,
1391
+ * field, dimensions, metric, metadata) and live stats (vector count,
1392
+ * processing watermark) when the binding is reachable. Hits the admin-gated
1393
+ * `GET /_lunora/admin/vector/indexes` endpoint — the worker must be built
1394
+ * with a `vectorIntrospector` and `adminToken`. Powers the studio's vector
1395
+ * browser. Vectorize can't enumerate indexes at runtime, so this list comes
1396
+ * from the generated `LUNORA_VECTOR_INDEXES` registry.
1397
+ */
1398
+ listVectorIndexes(): Promise<VectorIndexSummary[]>;
1399
+ /**
1400
+ * Run a nearest-neighbour similarity query against one vector index: the
1401
+ * worker embeds `text` via the index's embedder and returns the top matches.
1402
+ * Hits the admin-gated `POST /_lunora/admin/vector/query` endpoint. Throws
1403
+ * `VECTOR_QUERY_UNSUPPORTED` when the worker's introspector has no embedder
1404
+ * wired (the index lists read-only).
1405
+ */
1406
+ queryVectorIndex(options: {
1407
+ name: string;
1408
+ text: string;
1409
+ topK?: number;
1410
+ }): Promise<VectorQueryMatch[]>;
1411
+ /**
1412
+ * Read one keyset-paginated page of the durable `ctx.log` archive that
1413
+ * `pipelineLogSink` writes to R2. Server-side only — the worker holds the R2
1414
+ * SQL credentials and runs the reader; the browser only sees the decoded
1415
+ * `{ rows, nextCursor }`. Pass the previous page's `nextCursor` as
1416
+ * `query.cursor` to page. Admin-gated. When the operator hasn't wired the
1417
+ * archive, `adminFetch` throws a `LunoraClientError` with `.code ===
1418
+ * "LOG_ARCHIVE_NOT_CONFIGURED"`, so a caller can render a "not configured"
1419
+ * state rather than an error.
1420
+ */
1421
+ queryLogArchive(query?: PipelineLogQuery): Promise<PipelineLogPage>;
1422
+ /**
1423
+ * List the worker's registered Workers KV namespaces (binding names). Hits
1424
+ * the admin-gated `GET /_lunora/admin/kv/namespaces` endpoint — the worker
1425
+ * must be built with a `kvIntrospector` and `adminToken`. Powers the
1426
+ * studio's KV browser.
1427
+ */
1428
+ listKvNamespaces(): Promise<KvNamespaceSummary[]>;
1429
+ /**
1430
+ * List keys in a KV namespace, optionally filtered by `prefix` and
1431
+ * paginated via `cursor`. Hits the admin-gated
1432
+ * `GET /_lunora/admin/kv/keys` endpoint.
1433
+ */
1434
+ listKvKeys(options: {
1435
+ cursor?: string;
1436
+ limit?: number;
1437
+ namespace: string;
1438
+ prefix?: string;
1439
+ }): Promise<KvKeyListResult>;
1440
+ /**
1441
+ * Read a KV value (as text) and its metadata. Hits the admin-gated
1442
+ * `GET /_lunora/admin/kv/value` endpoint. Returns `{ value: null, metadata: null }`
1443
+ * when the key is absent.
1444
+ */
1445
+ getKvValue(options: {
1446
+ key: string;
1447
+ namespace: string;
1448
+ }): Promise<KvValueResult>;
1449
+ /**
1450
+ * Write a string value to a KV namespace. Accepts an absolute `expiration`
1451
+ * (Unix seconds) or a relative `expirationTtl`, plus optional `metadata` —
1452
+ * re-send the loaded values on edit so a save preserves rather than clears
1453
+ * them. Hits the admin-gated `PUT /_lunora/admin/kv/value` endpoint.
1454
+ */
1455
+ putKvValue(options: {
1456
+ expiration?: number;
1457
+ expirationTtl?: number;
1458
+ key: string;
1459
+ metadata?: unknown;
1460
+ namespace: string;
1461
+ value: string;
1462
+ }): Promise<void>;
1463
+ /**
1464
+ * Delete a key from a KV namespace. No-op when the key is absent. Hits the
1465
+ * admin-gated `DELETE /_lunora/admin/kv/value` endpoint.
1466
+ */
1467
+ deleteKvKey(options: {
1468
+ key: string;
1469
+ namespace: string;
1470
+ }): Promise<void>;
1471
+ /**
1472
+ * List authenticated users, paged and optionally searched / filtered / sorted.
1473
+ * Hits the admin-gated `GET /_lunora/admin/auth/users` endpoint — the worker
1474
+ * must be built with an `authAdmin` and `adminToken`. Powers the studio's
1475
+ * users dashboard.
1476
+ */
1477
+ listAuthUsers(options?: {
1478
+ filterField?: string;
1479
+ filterValue?: string;
1480
+ limit?: number;
1481
+ offset?: number;
1482
+ search?: string;
1483
+ searchField?: string;
1484
+ sortBy?: string;
1485
+ sortDirection?: "asc" | "desc";
1486
+ }): Promise<AuthPage<AuthUser>>;
1487
+ /**
1488
+ * Create a user. Hits the admin-gated `POST /_lunora/admin/auth/users/create`
1489
+ * endpoint (requires the worker's `authAdmin` to implement `createUser`).
1490
+ * `data` carries any app-defined `user.additionalFields`.
1491
+ */
1492
+ createAuthUser(input: {
1493
+ data?: Record<string, unknown>;
1494
+ email: string;
1495
+ name: string;
1496
+ password?: string;
1497
+ role?: string | string[];
1498
+ }): Promise<AuthUser>;
1499
+ /** Set a user's role (string, or array joined comma-wise server-side). */
1500
+ setAuthUserRole(input: {
1501
+ role: string | string[];
1502
+ userId: string;
1503
+ }): Promise<AuthUser>;
1504
+ /** Ban a user. `expiresInSeconds` sets a temporary ban; omit it for a permanent one. Revokes the user's live sessions. */
1505
+ banAuthUser(input: {
1506
+ expiresInSeconds?: number;
1507
+ reason?: string;
1508
+ userId: string;
1509
+ }): Promise<AuthUser>;
1510
+ /** Lift a user's ban. */
1511
+ unbanAuthUser(input: {
1512
+ userId: string;
1513
+ }): Promise<AuthUser>;
1514
+ /** Set a user's password (admin override — no current-password challenge). */
1515
+ setAuthUserPassword(input: {
1516
+ newPassword: string;
1517
+ userId: string;
1518
+ }): Promise<void>;
1519
+ /** Permanently delete a user and revoke their sessions. */
1520
+ removeAuthUser(input: {
1521
+ userId: string;
1522
+ }): Promise<void>;
1523
+ /**
1524
+ * Mint an impersonation session for a user, returning its bearer `token`.
1525
+ * The caller is responsible for using the token (e.g. setting the session
1526
+ * cookie); the server performs no cookie round-trip.
1527
+ */
1528
+ impersonateAuthUser(input: {
1529
+ userId: string;
1530
+ }): Promise<AuthImpersonation>;
1531
+ /** Revoke a single session by its id (force sign-out of one device). */
1532
+ revokeAuthSession(input: {
1533
+ sessionId: string;
1534
+ }): Promise<void>;
1535
+ /** Revoke every session for a user (force sign-out everywhere). */
1536
+ revokeAuthUserSessions(input: {
1537
+ userId: string;
1538
+ }): Promise<void>;
1539
+ /**
1540
+ * Report which auth dashboard surfaces are available — derived server-side
1541
+ * from the enabled better-auth plugins. The studio renders only the panels
1542
+ * whose capability is `true`.
1543
+ */
1544
+ getAuthCapabilities(): Promise<AuthCapabilities>;
1545
+ /** Update a user's fields (name/email/app-defined `additionalFields`). */
1546
+ updateAuthUser(input: {
1547
+ data: Record<string, unknown>;
1548
+ userId: string;
1549
+ }): Promise<AuthUser>;
1550
+ /** List a user's linked accounts (credential / OAuth providers). Token material is stripped server-side. */
1551
+ listAuthAccounts(input: {
1552
+ userId: string;
1553
+ }): Promise<Record<string, unknown>[]>;
1554
+ /** Unlink a linked account from a user. */
1555
+ unlinkAuthAccount(input: {
1556
+ accountId: string;
1557
+ userId: string;
1558
+ }): Promise<void>;
1559
+ /** List a user's registered passkeys (requires the passkey plugin). */
1560
+ listAuthPasskeys(input: {
1561
+ userId: string;
1562
+ }): Promise<Record<string, unknown>[]>;
1563
+ /** Delete a passkey by id (requires the passkey plugin). */
1564
+ deleteAuthPasskey(input: {
1565
+ passkeyId: string;
1566
+ }): Promise<void>;
1567
+ /** Disable two-factor auth for a user (requires the two-factor plugin). */
1568
+ disableAuthTwoFactor(input: {
1569
+ userId: string;
1570
+ }): Promise<void>;
1571
+ /** List organizations, paged (requires the organization plugin). */
1572
+ listAuthOrganizations(options?: {
1573
+ limit?: number;
1574
+ offset?: number;
1575
+ }): Promise<AuthPage<Record<string, unknown>>>;
1576
+ /** List the members of an organization (requires the organization plugin). */
1577
+ listAuthOrgMembers(input: {
1578
+ limit?: number;
1579
+ offset?: number;
1580
+ organizationId: string;
1581
+ }): Promise<AuthPage<Record<string, unknown>>>;
1582
+ /** List an organization's pending invitations (requires the organization plugin). */
1583
+ listAuthOrgInvitations(input: {
1584
+ limit?: number;
1585
+ offset?: number;
1586
+ organizationId: string;
1587
+ }): Promise<AuthPage<Record<string, unknown>>>;
1588
+ /** Remove a member from an organization. */
1589
+ removeAuthOrgMember(input: {
1590
+ memberId: string;
1591
+ }): Promise<void>;
1592
+ /** Cancel a pending organization invitation. */
1593
+ cancelAuthOrgInvitation(input: {
1594
+ invitationId: string;
1595
+ }): Promise<void>;
1596
+ /**
1597
+ * Report the deployment's auth configuration — enabled plugins, sign-in
1598
+ * methods, user-settable create-user fields, organization sub-features
1599
+ * (teams / roles), and session / rate-limit policy. Drives the config panel
1600
+ * and the dynamic create-user form. Never carries a secret.
1601
+ */
1602
+ getAuthConfig(): Promise<AuthConfigInfo>;
1603
+ /** Create an organization; optionally seed an `owner` member for `ownerId`. */
1604
+ createAuthOrganization(input: {
1605
+ logo?: string;
1606
+ metadata?: Record<string, unknown>;
1607
+ name: string;
1608
+ ownerId?: string;
1609
+ slug?: string;
1610
+ }): Promise<Record<string, unknown>>;
1611
+ /** Update an organization's name/slug/logo/metadata. */
1612
+ updateAuthOrganization(input: {
1613
+ logo?: string;
1614
+ metadata?: Record<string, unknown>;
1615
+ name?: string;
1616
+ organizationId: string;
1617
+ slug?: string;
1618
+ }): Promise<Record<string, unknown>>;
1619
+ /** Delete an organization and cascade its members, invitations, teams, and custom roles. */
1620
+ deleteAuthOrganization(input: {
1621
+ organizationId: string;
1622
+ }): Promise<void>;
1623
+ /** Directly add an existing user to an organization (no invitation/acceptance). */
1624
+ addAuthOrgMember(input: {
1625
+ organizationId: string;
1626
+ role?: string;
1627
+ userId: string;
1628
+ }): Promise<Record<string, unknown>>;
1629
+ /** Create a pending email invitation to an organization. */
1630
+ inviteAuthOrgMember(input: {
1631
+ email: string;
1632
+ inviterId?: string;
1633
+ organizationId: string;
1634
+ role?: string;
1635
+ }): Promise<Record<string, unknown>>;
1636
+ /** Change a member's role. */
1637
+ setAuthOrgMemberRole(input: {
1638
+ memberId: string;
1639
+ role: string | string[];
1640
+ }): Promise<Record<string, unknown>>;
1641
+ /** List an organization's teams (requires the organization plugin with teams enabled). */
1642
+ listAuthOrgTeams(input: {
1643
+ limit?: number;
1644
+ offset?: number;
1645
+ organizationId: string;
1646
+ }): Promise<AuthPage<Record<string, unknown>>>;
1647
+ /** Create a team under an organization. */
1648
+ createAuthOrgTeam(input: {
1649
+ name: string;
1650
+ organizationId: string;
1651
+ }): Promise<Record<string, unknown>>;
1652
+ /** Rename a team. */
1653
+ updateAuthOrgTeam(input: {
1654
+ name: string;
1655
+ teamId: string;
1656
+ }): Promise<Record<string, unknown>>;
1657
+ /** Delete a team and its memberships. */
1658
+ removeAuthOrgTeam(input: {
1659
+ teamId: string;
1660
+ }): Promise<void>;
1661
+ /** List a team's members. */
1662
+ listAuthOrgTeamMembers(input: {
1663
+ limit?: number;
1664
+ offset?: number;
1665
+ teamId: string;
1666
+ }): Promise<AuthPage<Record<string, unknown>>>;
1667
+ /** Add a user to a team. */
1668
+ addAuthOrgTeamMember(input: {
1669
+ teamId: string;
1670
+ userId: string;
1671
+ }): Promise<Record<string, unknown>>;
1672
+ /** Remove a member from a team. */
1673
+ removeAuthOrgTeamMember(input: {
1674
+ teamMemberId: string;
1675
+ }): Promise<void>;
1676
+ /** List an organization's custom roles (requires the organization plugin with dynamic access control). */
1677
+ listAuthOrgRoles(input: {
1678
+ limit?: number;
1679
+ offset?: number;
1680
+ organizationId: string;
1681
+ }): Promise<AuthPage<Record<string, unknown>>>;
1682
+ /** Create a custom org role with a permission grant (a `resource -> actions[]` map). */
1683
+ createAuthOrgRole(input: {
1684
+ organizationId: string;
1685
+ permission: Record<string, string[]>;
1686
+ role: string;
1687
+ }): Promise<Record<string, unknown>>;
1688
+ /** Replace a custom org role's permission grant. */
1689
+ updateAuthOrgRole(input: {
1690
+ permission: Record<string, string[]>;
1691
+ roleId: string;
1692
+ }): Promise<Record<string, unknown>>;
1693
+ /** Delete a custom org role. */
1694
+ deleteAuthOrgRole(input: {
1695
+ roleId: string;
1696
+ }): Promise<void>;
1697
+ /** List auth sessions, paged and optionally filtered to one user. */
1698
+ listAuthSessions(options?: {
1699
+ limit?: number;
1700
+ offset?: number;
1701
+ userId?: string;
1702
+ }): Promise<AuthPage<AuthSession>>;
1703
+ subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1704
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1705
+ onError?: SubscriptionErrorCallback;
1706
+ shardKey?: string;
1707
+ }): Unsubscribe;
1708
+ /**
1709
+ * Subscribe to a declarative **shape** — server-side partial replication
1710
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
1711
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
1712
+ * validated `args` (never a `where` the client could forge), the server seeds
1713
+ * the current membership as an insert-poke and streams live membership diffs.
1714
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
1715
+ *
1716
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1717
+ * (name, args): the server resolves them under the socket's verified identity,
1718
+ * so every call gets its own id + view. The returned function unsubscribes.
1719
+ */
1720
+ subscribeShape(shape: {
1721
+ args?: Record<string, unknown>;
1722
+ name: string;
1723
+ }, callback: ShapeCallback, options?: {
1724
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1725
+ onError?: SubscriptionErrorCallback;
1726
+ shardKey?: string;
1727
+ }): Unsubscribe;
1728
+ /**
1729
+ * Open a streaming query. The function reference must be a
1730
+ * `kind:"stream"` registration (built with `c.query.input(...).stream(...)`);
1731
+ * the type constraint catches accidental use of a query/mutation/action
1732
+ * reference at compile time. The returned iterable yields one element per
1733
+ * chunk frame the server pushes, terminating when the server sends
1734
+ * `complete` or the consumer calls `.cancel()`. Errors arrive as a
1735
+ * rejection on the next `next()`.
1736
+ *
1737
+ * Streams ride the same WS as subscriptions and share the unsubscribe
1738
+ * channel: cancelling sends `{type:"unsubscribe", id}` with the stream id,
1739
+ * which the DO recognises as an abort signal for the in-flight iterator.
1740
+ *
1741
+ * Stream-start frames buffered while the socket is (re)connecting are
1742
+ * capped at {@link MAX_PENDING_STREAMS} per connection — overflowing the
1743
+ * cap drops the oldest queued frame (and fails its consumer) so a stuck
1744
+ * reconnect can't OOM the page.
1745
+ */
1746
+ stream<F extends FunctionReference<"stream">>(function_: F, args: ArgsOf<F>, options?: {
1747
+ durable?: boolean;
1748
+ maxBuffer?: number;
1749
+ shardKey?: string;
1750
+ }): StreamIterable<ReturnOf<F>>;
1751
+ /**
1752
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.<verb>(path).stream()`).
1753
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
1754
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
1755
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
1756
+ * chunks, a final `event: complete`, an `event: error` on throw).
1757
+ *
1758
+ * The reference comes from the generated `httpStreams.*` registry, so the
1759
+ * yielded chunk type is the route handler's yielded type. Cancelling the
1760
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
1761
+ * the server handler observes via its `signal`. The client's bearer token
1762
+ * (when set) rides as an `authorization` header.
1763
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
1764
+ */
1765
+ httpStream<Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: {
1766
+ headers?: Record<string, string>;
1767
+ maxBuffer?: number;
1768
+ signal?: AbortSignal;
1769
+ }): StreamIterable<HttpStreamChunkOf<Ref>>;
1770
+ close(): void;
1771
+ /** Guard shared by every public entry point: a closed client accepts no further calls. */
1772
+ private assertOpen;
1773
+ /**
1774
+ * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
1775
+ * connect timers, stop its heartbeat, and close its socket (if any).
1776
+ * Shared by `close()` (terminal) and the cross-tab `onStopBeingLeader`
1777
+ * handler (demoted, but still alive) so a demoted leader can't leak a
1778
+ * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
1779
+ * an inline `conn.socket?.close()` — which skips both — used to.
1780
+ */
1781
+ private teardownConnection;
1782
+ /**
1783
+ * Build (but do not start) this client's `TabCoordinator`. Extracted out of
1784
+ * the constructor so `setAuthToken` can rebuild it on an identity change —
1785
+ * the default channel name embeds the identity fingerprint (see below), so
1786
+ * a new identity needs a new coordinator on a new channel. The callback
1787
+ * bodies are the drift-sensitive region (a hand-merged identity guard on
1788
+ * the shard message listener sits ahead of an extracted `lastFrameAt`
1789
+ * stamp elsewhere in this file) — moved verbatim, not reflowed.
1790
+ */
1791
+ private createTabCoordinator;
1792
+ /**
1793
+ * Persist a mutation that can't go out on the wire right now (offline, or
1794
+ * mid-reconnect after a prior connect). The optimistic update has already
1795
+ * been applied by `mutation`; this only chooses the durable write path and
1796
+ * rolls the optimistic write back if persistence is rejected.
1797
+ *
1798
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
1799
+ * owns persistence + at-least-once replay, so we delegate and return
1800
+ * optimistically (confirmation rides the synced view). Otherwise the
1801
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
1802
+ */
1803
+ private enqueueOfflineMutation;
1804
+ /**
1805
+ * Restore offline mutations persisted in a prior session and open a socket
1806
+ * for each shard they target so they flush once the WS reconnects. Failures
1807
+ * are swallowed — a broken durable store must not stop the client booting.
1808
+ */
1809
+ private hydratePersistedQueue;
1810
+ /**
1811
+ * Re-queue the durable offline writes — but only as the multi-tab LEADER. The
1812
+ * persisted queue is shared across a profile's tabs; without coordination
1813
+ * every tab would re-queue and replay the same writes (correct only because
1814
+ * the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
1815
+ * exactly one tab hydrate; it holds the lock for its lifetime, so when it
1816
+ * closes another tab acquires the lock and takes over. Falls back to
1817
+ * unconditional hydration where Web Locks are unavailable (React Native, older
1818
+ * browsers, SSR) — single-context there, so no coordination is needed.
1819
+ */
1820
+ private hydrateAsOutboxLeader;
1821
+ /**
1822
+ * Load every cached query into {@link hydratedQueryCache} so the next
1823
+ * `subscribe()` for each key seeds its initial value off disk. A
1824
+ * subscription created before this resolves simply misses the cache (it
1825
+ * gets a live snapshot as before); the gate at seed time also drops any
1826
+ * entry whose stamped identity no longer matches the current one.
1827
+ */
1828
+ private hydrateQueryCache;
1829
+ /**
1830
+ * Consume the hydrated read-cache entry for a key (if any), gated on
1831
+ * identity. The entry is removed whether or not it matches — the cache only
1832
+ * ever seeds a subscription's first value. A mismatch (the cache was written
1833
+ * under a different identity) yields `undefined` so a signed-out cache never
1834
+ * leaks into a new session.
1835
+ */
1836
+ private takeHydratedCache;
1837
+ /**
1838
+ * Queue a coalesced read-cache write for a subscription's current value.
1839
+ * Latest-wins per key; flushed on a short debounce so a delta burst writes
1840
+ * once. No-op when the read cache is disabled or the value is undefined
1841
+ * (nothing to render offline).
1842
+ */
1843
+ private persistQueryValue;
1844
+ /** Drain {@link pendingCacheWrites} to the durable store. */
1845
+ private flushQueryCacheWrites;
1846
+ /** Derive the aggregate status from the per-shard socket states. */
1847
+ private computeStatus;
1848
+ /** Recompute the aggregate status and notify listeners if it changed. */
1849
+ private emitConnectionStatus;
1850
+ /**
1851
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
1852
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
1853
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
1854
+ * is unreachable — present only to satisfy the optional queue-id type.
1855
+ */
1856
+ private emitItemSettled;
1857
+ /**
1858
+ * Apply an optimistic update to the subscription that matches the mutation's
1859
+ * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
1860
+ * invoke if the mutation later fails.
1861
+ *
1862
+ * The registry is already indexed by exactly this triple via
1863
+ * `SubscriptionRegistry.key`, so at most one subscription can match. A direct
1864
+ * O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
1865
+ *
1866
+ * `shardKey` normalization: both `undefined` and `""` map to the empty string
1867
+ * inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
1868
+ * a shardKey correctly matches a subscription registered without one regardless
1869
+ * of whether the caller passed `undefined` or omitted the field.
1870
+ */
1871
+ private applyOptimisticUpdates;
1872
+ /**
1873
+ * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1874
+ * to the live subscription registry. Each `setQuery` registers a constant
1875
+ * optimistic LAYER on its target subscription (via the same engine the
1876
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
1877
+ * incoming deltas and drops gaplessly on its commit cursor — its `confirm` /
1878
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
1879
+ * callback unwinds its own partial writes — LIFO over just the rollbacks it
1880
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
1881
+ * mutation or leave a partial patch live.
1882
+ */
1883
+ private applyOptimisticUpdate;
1884
+ private getConnection;
1885
+ /**
1886
+ * Send an unsubscribe frame (tagged with its wire type) on the shard's
1887
+ * socket, or queue it for the next reconnect when the send can't go out.
1888
+ */
1889
+ private sendOrQueueUnsubscribe;
1890
+ /**
1891
+ * The `(wsState, hasSocket, wasEverConnected)` triple `mutation()`'s
1892
+ * offline-queue gate reads. On the leader/single-tab path this is exactly
1893
+ * the real `ShardConnection`'s state (byte-identical to the pre-cross-tab
1894
+ * behavior). A follower has no `ShardConnection` of its own (see
1895
+ * `ensureSocket`), so it derives the same triple from the mirrored
1896
+ * `leaderStatus`/`leaderWasEverConnected` instead: `"connected"` maps to
1897
+ * `"open"` (queue-eligible once `wasEverConnected`), `"connecting"` stays
1898
+ * `"connecting"` (the mid-reconnect queue branch), anything else is
1899
+ * `"idle"`. `hasSocket` is always `false` for a follower — it never has
1900
+ * one.
1901
+ */
1902
+ private connectionGateState;
1903
+ private getOrCreateConnection;
1904
+ private wsUrlFor;
1905
+ /**
1906
+ * Build the outbound RPC headers: JSON content type, optional bearer auth,
1907
+ * the optional mutation-replay idempotency key, and the D1 read-your-writes
1908
+ * bookmark when the caller opted into `attachBookmark`. The mutation id
1909
+ * rides both the direct send and any offline-queue replay of the same write,
1910
+ * so a mutation the server already committed returns its cached result
1911
+ * instead of running twice.
1912
+ */
1913
+ private rpcRequestHeaders;
1914
+ private rpc;
1915
+ /**
1916
+ * Authenticated request to a non-RPC admin endpoint (the scheduler list /
1917
+ * cancel routes). Attaches the bearer token, parses JSON, and surfaces the
1918
+ * worker's `{ error: { code, message } }` envelope as a coded `Error` —
1919
+ * mirroring {@link rpc} so callers see the same failure shape.
1920
+ */
1921
+ private adminFetch;
1922
+ /**
1923
+ * Resolve the effective connection context for a shard: the most-recently
1924
+ * acquired refcounted holder ({@link acquireConnectionContext}) wins, falling
1925
+ * back to the imperative {@link setConnectionContext} override, then the
1926
+ * client-wide default. Returns `undefined` when none apply.
1927
+ */
1928
+ private effectiveConnectionContext;
1929
+ /** Re-send the `connect` envelope for a shard whose effective context just changed (if its socket is open). */
1930
+ private refreshConnectionContext;
1931
+ /**
1932
+ * Send the one-shot `connect` envelope on an open shard socket. Always sent
1933
+ * once per socket open, so the server's `onConnect` hooks fire symmetrically
1934
+ * with `onDisconnect` (which the DO dispatches unconditionally at close for
1935
+ * every lifecycle-aware socket). The DO no-ops cheaply when no `onConnect`
1936
+ * hooks are registered, so the single frame costs nothing in the common case.
1937
+ *
1938
+ * The shard's registered context (or the client-wide default) rides along
1939
+ * when one is set — the DO records it on the attachment for replay to
1940
+ * `onDisconnect`. A socket with no registered context still announces itself;
1941
+ * the envelope simply omits `context`, which is optional on the wire.
1942
+ * Register a context — e.g. `setConnectionContext({})` — to attach app state
1943
+ * to the lifecycle dispatch.
1944
+ */
1945
+ private sendConnectEnvelope;
1946
+ /**
1947
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
1948
+ * socket. Each frame carries the shape's last applied checkpoint, so the
1949
+ * server resumes from it — or re-seeds when the cursor fell below CDC
1950
+ * retention or the epoch forked.
1951
+ */
1952
+ private resendShapeSubscriptions;
1953
+ private ensureSocket;
1954
+ /**
1955
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
1956
+ * minted token. The connection is already in the `connecting` state, so the
1957
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
1958
+ * competing connect that landed first all abandon this attempt. A provider
1959
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
1960
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
1961
+ * a silent tokenless socket the admin gate would reject.
1962
+ */
1963
+ private openSocketWithProvidedToken;
1964
+ /**
1965
+ * Construct one WebSocket connection attempt and wire the shared
1966
+ * lifecycle guarantees around it — the fail-fast connect-timeout, the
1967
+ * identity guard that stops a superseded attempt's late `open`/`message`/
1968
+ * `close`/`error` from touching a connection a newer attempt already
1969
+ * owns, and (once open) the keepalive heartbeat with its half-open
1970
+ * watchdog (plan 217). One call opens ONE attempt; the caller owns
1971
+ * reconnect scheduling from `onClose` — mirrors the shard's existing
1972
+ * `ensureSocket` / `handleDisconnect` split, now shared with
1973
+ * `subscribeScheduledJobs` so it stops re-living the bug that split
1974
+ * already fixed once (CLIENT-05).
1975
+ *
1976
+ * The identity guard is `conn.socket !== socket`, re-checked before every
1977
+ * action below. `conn.socket` is reassigned to a new attempt's socket
1978
+ * synchronously — right here, before `open` ever fires — so an older
1979
+ * attempt's guard trips the instant it's superseded, even if its
1980
+ * underlying socket only fires its real `close`/`error` much later. This
1981
+ * ordering is load-bearing: preserve it exactly.
1982
+ */
1983
+ private openManagedSocket;
1984
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
1985
+ private openSocket;
1986
+ private handleDisconnect;
1987
+ /**
1988
+ * Begin the keepalive heartbeat on an open connection attempt — the only
1989
+ * caller is {@link openManagedSocket}'s own `open` handler, so both the
1990
+ * shard socket and `subscribeScheduledJobs` share this one implementation
1991
+ * instead of each hand-rolling their own (plan 217, generalized).
1992
+ *
1993
+ * Each tick first checks the half-open watchdog (see
1994
+ * {@link ManagedSocketState.lastFrameAt}): if no frame at all has arrived
1995
+ * within `heartbeatIntervalMs * 2.5`, the far end has gone quiet without
1996
+ * the socket ever firing `close` — force it closed and report it through
1997
+ * `onWatchdogTrip` (the caller's `onClose`) so the normal reconnect/backoff
1998
+ * takes over instead of every live query on it silently staling forever.
1999
+ * Otherwise it sends a {@link WS_KEEPALIVE_PING} text frame the server
2000
+ * answers from its hibernation auto-response without waking the DO. A
2001
+ * no-op when the heartbeat is disabled (an interval of zero or less);
2002
+ * idempotent — any existing timer is cleared first so a reconnect can't
2003
+ * leak intervals.
2004
+ */
2005
+ private startHeartbeat;
2006
+ /** Clear a connection's keepalive timer, if any. Safe to call repeatedly. */
2007
+ private stopHeartbeat;
2008
+ /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
2009
+ private markShardPendingAck;
2010
+ private sendSubscribeIfOpen;
2011
+ private sendShapeSubscribeIfOpen;
2012
+ private handleServerMessage;
2013
+ private handleErrorMessage;
2014
+ /** Buffer key for an in-flight poke: `pokeId` is only unique per shard socket, so it is scoped by connection. */
2015
+ private pokeBufferKey;
2016
+ private handlePokeStart;
2017
+ private handlePokePart;
2018
+ private handlePokeEnd;
2019
+ /**
2020
+ * Commit one shape's slice of a poke, or refuse it and re-seed.
2021
+ *
2022
+ * Split out of {@link handlePokeEnd} because every decision here is PER SHAPE
2023
+ * — the reset flag, the base checkpoint, the watermark — while the poke
2024
+ * envelope around it is not.
2025
+ */
2026
+ private applyPokePart;
2027
+ /**
2028
+ * Force the server to re-send a full snapshot for `state`, leaving the
2029
+ * currently displayed value alone until it lands. Used when a delta frame
2030
+ * cannot be applied: dropping the resume cursor is what makes the resubscribe
2031
+ * a snapshot rather than a `resume`, and un-acking is what lets
2032
+ * `sendSubscribeIfOpen` put the frame on the wire at all. Mirrors the shape
2033
+ * path's re-seed on a diverged base.
2034
+ */
2035
+ private resnapshotSubscription;
2036
+ /** Materialize a shape's keyed view to an array and invoke its callbacks. */
2037
+ private emitShapeRows;
2038
+ private handleDataMessage;
2039
+ /**
2040
+ * Handle a `resume` frame (Pillar 1b): the server proved nothing the
2041
+ * subscription reads changed since our `sinceSeq`, so the cached value is
2042
+ * still current. We keep `lastValue` as-is, mark the sub acked, and advance
2043
+ * the cursor (re-persisting so the next reconnect resumes from the newer
2044
+ * watermark). No callback fires — the value didn't change, and `subscribe()`
2045
+ * already replayed the cached value to every consumer synchronously.
2046
+ */
2047
+ private handleResumeMessage;
2048
+ /**
2049
+ * Handle a `settled` frame: a write touched one of this subscription's read
2050
+ * tables but produced a byte-identical result, so the server suppressed the
2051
+ * data frame. Like {@link handleResumeMessage} the value didn't change — we
2052
+ * advance the resume position and re-persist — but we ALSO surface the echoed
2053
+ * custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
2054
+ * collection drops the optimistic overlay for the confirmed write (otherwise
2055
+ * its checkpoint gate, fed only by data frames, would hang forever). Sent
2056
+ * only to custom-mutator clients; plain `useQuery` subscribers leave
2057
+ * `onCheckpoint` unset and this is a near no-op.
2058
+ */
2059
+ private handleSettledMessage;
2060
+ /**
2061
+ * Mark `state` acked and, when the frame carries a newer cursor/epoch than
2062
+ * the cached position, advance the resume watermark and re-persist. Shared by
2063
+ * the `resume` and `settled` frame handlers — both acknowledge "nothing the
2064
+ * client must re-render changed, but the resume position may have moved".
2065
+ */
2066
+ private ackAndAdvanceCursor;
2067
+ /**
2068
+ * Resolve the value to publish for a `data`/`delta` frame.
2069
+ *
2070
+ * A `data` frame is an authoritative snapshot (the server re-execution path)
2071
+ * and always replaces the cached value wholesale. A `delta` frame carrying a
2072
+ * structured `MutationDelta` (the `broadcastDelta` row-change path) is
2073
+ * merged incrementally into the cached list — preserving order, no dup/loss —
2074
+ * so each subscription (including every paginated page) updates by delta
2075
+ * rather than a full re-send. We fall back to full replacement when the
2076
+ * delta isn't a recognisable row change, when there's no cached value yet,
2077
+ * or when it can't be applied cleanly against the current cached shape.
2078
+ */
2079
+ private resolveDataPayload;
2080
+ /** Route an inbound whisper to the topic's handlers on the originating shard. */
2081
+ private dispatchWhisper;
2082
+ /** Notify every {@link onTokenExpired} listener (best-effort, listener throws swallowed). */
2083
+ private notifyTokenExpired;
2084
+ /**
2085
+ * CLIENT-04: `type: "complete"` today is sent ONLY by `@lunora/do`'s
2086
+ * `handleStream` (see `shard-do.ts`), gated to the `stream` envelope type
2087
+ * and minting only `stream_*` ids — the `subscribe` path never sends it, so
2088
+ * a live SUBSCRIPTION provably never receives `complete` from the current
2089
+ * server. But `ServerCompleteMessage` is a generic `id`-keyed frame and
2090
+ * `ShardDO` is user-subclassable, so this stays defensive rather than
2091
+ * assuming a `sub_*` id can never reach here: unlike the historical
2092
+ * `subscriptions.remove(state)`, which dropped the state out of
2093
+ * `subscriptions.all()` — the set the reconnect resubscribe loop walks
2094
+ * (`ensureSocket`'s `open` handler) — and so froze the query forever across
2095
+ * every future reconnect, this fans a cancellation error to any listener
2096
+ * and marks the registration un-acked instead. Non-destructive: the state
2097
+ * stays in the registry, so the very next reconnect resubscribes it. The
2098
+ * two id-spaces don't overlap (`sub_*` vs `stream_*`), so the stream and
2099
+ * subscription lookups below are mutually exclusive.
2100
+ */
2101
+ private handleCompleteMessage;
2102
+ private unpersist;
2103
+ /**
2104
+ * Stable, non-reversible fingerprint of the current auth identity used to
2105
+ * stamp queued offline writes. `null` (signed out) is its own identity and
2106
+ * never matches a bearer-token fingerprint. The raw token is never stored;
2107
+ * a length-prefixed FNV-1a hash is enough to detect an identity *change*
2108
+ * without keeping the credential around in the queue map.
2109
+ */
2110
+ private identityFingerprint;
2111
+ /**
2112
+ * Stable token-hash fingerprint of a bearer token (the `<len>:<fnv>:<djb2>`
2113
+ * format a token-stamped queued write carries). Extracted so the replay gate
2114
+ * can recompute the hash of the current credential and recognise a write
2115
+ * stamped under it — even after the fingerprint was relabelled to a subject.
2116
+ *
2117
+ * Two independent 32-bit passes (FNV-1a + djb2) give a ~64-bit digest, so
2118
+ * two distinct equal-length tokens are astronomically unlikely to share a
2119
+ * fingerprint. A single 32-bit hash collides ~1-in-4e9 per equal-length
2120
+ * pair — enough that, on a shared device, user B could hydrate A's cached
2121
+ * reads. Different algorithms (not the same FNV with a different seed, which
2122
+ * would be affine-related) keep the two passes genuinely independent.
2123
+ * Still synchronous (no crypto) and stable across surrogate pairs.
2124
+ */
2125
+ private hashToken;
2126
+ /**
2127
+ * True when `stamped` is a token-hash of the SAME credential still held now,
2128
+ * even though the live identity has since been relabelled to a subject. Covers
2129
+ * `setAuthToken(token, userId)` where the subject resolved a tick after the
2130
+ * token was set: a write persisted (or requeued) under the token hash must
2131
+ * still replay — the credential never changed, only its label — instead of
2132
+ * being dropped as an identity mismatch. This is the durable counterpart to
2133
+ * {@link restampQueuedIdentity}, which only relabels the in-memory live stamp
2134
+ * (consumed on the first flush) and never touches `item.identity` or the
2135
+ * persisted record, so a reload or a transient-failure requeue would otherwise
2136
+ * fall back to the stale token-hash and wrongly reject the same user's write.
2137
+ */
2138
+ private isSameCredentialUnderTokenHash;
2139
+ /**
2140
+ * Drain every in-memory offline write and reject it because the auth
2141
+ * identity changed. Durable entries are also dropped from persistence so a
2142
+ * later `hydrate` can't resurrect another user's writes. Stamps are cleared
2143
+ * alongside. Persisted entries restored without a live awaiter still get
2144
+ * unpersisted here.
2145
+ */
2146
+ private rejectQueuedForIdentityChange;
2147
+ /**
2148
+ * Migrate every live identity stamp from `from` to `to` — used when the auth
2149
+ * identity label changes but the underlying credential (token) does NOT, e.g.
2150
+ * the user id resolves a tick after the token was set. The in-memory
2151
+ * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2152
+ * keeps the in-flight writes replayable under the new (more stable) identity
2153
+ * instead of the flush guard discarding them as a mismatch.
2154
+ */
2155
+ private restampQueuedIdentity;
2156
+ /**
2157
+ * Migrate the {@link clientWatermarks} bucket map nested under identity
2158
+ * `from` to identity `to` — the sibling of {@link restampQueuedIdentity},
2159
+ * for the same same-credential-subject-resolves case. Without this, a
2160
+ * bucket's watermark cached under the token-hash fingerprint would look
2161
+ * unset once the fingerprint relabels to `subj:…`, so the next push
2162
+ * re-derives `1` against a server watermark the DO already advanced — the
2163
+ * OUT_OF_ORDER wedge this cache-keying scheme exists to fix, reintroduced
2164
+ * by the fix itself. `to` may already hold a bucket map (switching back to
2165
+ * an identity that has its own cached watermarks); merge into it rather
2166
+ * than clobbering it, with `from`'s entries winning on a colliding bucket —
2167
+ * the same overwrite a plain `Map.set` would have done before this was a
2168
+ * nested map.
2169
+ */
2170
+ private restampWatermarks;
2171
+ /**
2172
+ * Drop the durable read cache on an identity change so a cached value stamped
2173
+ * under the previous identity can never hydrate into a new session. Clears
2174
+ * the in-flight write batch and the not-yet-consumed hydrated entries too;
2175
+ * the durable `clear()` is best-effort.
2176
+ */
2177
+ private clearQueryCacheForIdentityChange;
2178
+ /**
2179
+ * Flush every shard with a mutation currently queued in `offlineQueue`
2180
+ * (see `queuedOfflineShardKeys`). Used on a FOLLOWER tab when the
2181
+ * mirrored leader status transitions to `"connected"` — a follower has no
2182
+ * per-shard `ShardConnection` reconnect event to hang the usual
2183
+ * single-shard `flushOfflineQueue(shardKey)` call off of (see the
2184
+ * `handleConnect` call site), so this walks every shard that might have
2185
+ * something queued instead. Flushing an already-empty shard is a cheap
2186
+ * no-op (`flushOfflineQueue` returns immediately once `drain` yields
2187
+ * nothing), so over-inclusion here is harmless.
2188
+ */
2189
+ private flushAllOfflineQueues;
2190
+ private flushOfflineQueue;
2191
+ /**
2192
+ * Partition already-gated writes into the encodable ones (returned) and reject
2193
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
2194
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
2195
+ * is deterministic, not transient. Rejecting here is essential: otherwise
2196
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
2197
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
2198
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
2199
+ * the flush is the slow reconnect path.
2200
+ */
2201
+ private encodableOrSettleTerminal;
2202
+ /**
2203
+ * Identity guard for one queued write about to replay: a write stamped under
2204
+ * one identity must never replay under another. The live `queuedIdentities`
2205
+ * map is the source of truth for the current session; a hydrated write whose
2206
+ * id isn't in the map falls back to the stamp persisted with the record
2207
+ * (`item.identity`), so a reload can't replay another user's queued writes.
2208
+ * Only legacy records (persisted before stamps were durable —
2209
+ * `item.identity === undefined`) replay under whatever identity is current.
2210
+ *
2211
+ * `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
2212
+ * is `undefined` for legacy records; a persisted `null` (queued while signed
2213
+ * out) is a real value that must not collapse into `undefined` — hence the
2214
+ * explicit `=== undefined` check rather than `??`. Returns `true` when the
2215
+ * write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
2216
+ * `false`. Either way the live stamp is consumed.
2217
+ */
2218
+ private passesReplayIdentityGate;
2219
+ /** 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. */
2220
+ private settleReplaySuccess;
2221
+ /**
2222
+ * The entry a shard's cursor lives under.
2223
+ *
2224
+ * Only the server knows that an omitted `shardKey` and an explicit one
2225
+ * spelling out its configured default name are the same shard — the default
2226
+ * is server-side configuration the client never sees. Keying on what was
2227
+ * SENT would split one shard's cursor across two entries, so a write under
2228
+ * one spelling would stop constraining a read under the other. Resolving
2229
+ * `undefined` through the learned name is what keeps both spellings on one
2230
+ * entry.
2231
+ */
2232
+ private cursorKeyFor;
2233
+ /**
2234
+ * Learn the server's own name for the default shard from a response to a
2235
+ * call that named no shard.
2236
+ *
2237
+ * Any cursor recorded before the name was known sits under the placeholder
2238
+ * entry, so it is folded in rather than stranded — otherwise the requirement
2239
+ * from a client's first write would be lost exactly once, which is the kind
2240
+ * of gap that only shows up as a stale read under load.
2241
+ */
2242
+ private learnDefaultShardKey;
2243
+ /**
2244
+ * Record the cursor a write committed at as this shard's read-your-writes
2245
+ * requirement.
2246
+ *
2247
+ * Monotonic: responses can land out of order, and moving the requirement
2248
+ * BACKWARDS would let a later read be answered from a replica copy that
2249
+ * predates a write this client already saw.
2250
+ */
2251
+ private recordShardCursor;
2252
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
2253
+ private settleReplayTerminal;
2254
+ /**
2255
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
2256
+ * path, preserving FIFO order (parallel `.then()` chains would race the
2257
+ * ordering callers depend on). Each replays under its stable `mutationId` so
2258
+ * the server dedups a write it already committed (exactly-once). A coded error
2259
+ * is a server verdict (drop it); a codeless (transport/transient) failure stops
2260
+ * the flush and re-queues this write and every unreplayed one for the next
2261
+ * reconnect — their callers stay pending, and the identity guard re-applies on
2262
+ * retry via each record's persisted stamp.
2263
+ */
2264
+ private replaySequential;
2265
+ /**
2266
+ * Coalesce already-identity-gated writes for a single shard into ONE
2267
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
2268
+ * them to the shard DO, which replays each through its single-call dispatch, so
2269
+ * per-entry `mutationId` idempotency and in-order application are inherited from
2270
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
2271
+ * classification: success confirms the optimistic layer against the echoed
2272
+ * `commitCursor`; a coded application verdict is terminal; a transient shard
2273
+ * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
2274
+ * transport failure re-queues for the next reconnect (never dropping a durable
2275
+ * write). A whole-batch coded rejection (bad request / authorization denial the
2276
+ * server reached a verdict on) is terminal for every entry.
2277
+ *
2278
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
2279
+ * chunk failed at the transport level, so the caller leaves later chunks queued
2280
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
2281
+ * NOT done here.
2282
+ */
2283
+ private replayBatched;
2284
+ /**
2285
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
2286
+ * in input order. Each slot's envelope classifies its write the same way
2287
+ * {@link replaySequential} does: a success confirms the optimistic layer
2288
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
2289
+ * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
2290
+ * server never returned is returned for the caller to re-queue.
2291
+ * @returns the writes that must be re-queued (transient slots), in input order
2292
+ */
2293
+ private settleReplayBatchSlots;
2294
+ }
2295
+ export { ActionCallOptions as A, BatchSlot as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, LunoraClient as L, MutationCallOptions as M, OptimisticLocalStore as O, SubscriptionError as S, SubscriptionErrorCallback as a, StreamIterable as b, CONFLICT_ERROR_CODE as c, ClientDebugShard as d, ClientDebugSnapshot as e, ClientDebugSubscription as f, ClientQueryRef as g, LunoraClientError as h, LunoraErrorCode as i, MutationSettledEvent as j, OptimisticUpdate as k, StreamHandle as l, SubscriptionCallback as m, SubscriptionRegistry as n, SubscriptionState as o, SyncWatermark as p, createClientQuery as q, createLocalStore as r, createStream as s, getErrorCode as t, getRetryAfterMs as u, isConflictError as v, isForbiddenError as w, isRateLimitedError as x, isUnauthorizedError as y };