@lunora/client 1.0.0-alpha.67 → 1.0.0-alpha.69

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 (44) hide show
  1. package/dist/auth/index.d.mts +3 -2
  2. package/dist/auth/index.d.ts +3 -2
  3. package/dist/index.d.mts +80 -7
  4. package/dist/index.d.ts +80 -7
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/CONFLICT_ERROR_CODE-CDDneB-G.mjs +1 -0
  7. package/dist/packem_shared/LunoraClient-NP7mgVkC.mjs +1 -0
  8. package/dist/packem_shared/OfflineQueue-DIhrKdlU.mjs +1 -0
  9. package/dist/packem_shared/{SubscriptionRegistry-CdHSrDqu.mjs → SubscriptionRegistry-D7n5ZE5F.mjs} +1 -1
  10. package/dist/packem_shared/applyDelta-DJmlwFnT.mjs +1 -0
  11. package/dist/packem_shared/{createAsyncStorageQueryCache-Di-S52wy.mjs → createAsyncStorageQueryCache-C0mJLuZK.mjs} +1 -1
  12. package/dist/packem_shared/createClientQuery-B8Nfj-7o.mjs +1 -0
  13. package/dist/packem_shared/createLocalStore-DtsP-CpS.mjs +1 -0
  14. package/dist/packem_shared/{createServerClient-BhfYV6Je.mjs → createServerClient-DwpNFoKP.mjs} +1 -1
  15. package/dist/packem_shared/{createSnapshotPrecondition-BPMQbAbk.mjs → createSnapshotPrecondition-ZyQDet2v.mjs} +1 -1
  16. package/dist/packem_shared/{delta-merge-BoVuM-rE.mjs → delta-merge-CVSN-uoC.mjs} +1 -1
  17. package/dist/packem_shared/function-reference.d-Br_hsKje.d.mts +45 -0
  18. package/dist/packem_shared/function-reference.d-Br_hsKje.d.ts +45 -0
  19. package/dist/packem_shared/{local-store-tpI9VFpO.mjs → local-store-BQuKm9n6.mjs} +1 -1
  20. package/dist/packem_shared/{lunora-client.d-DAbmOzH_.d.mts → lunora-client.d-CLwpIj61.d.ts} +1212 -47
  21. package/dist/packem_shared/{lunora-client.d-Dz6Yx4By.d.ts → lunora-client.d-DWDMVBKE.d.mts} +1212 -47
  22. package/dist/packem_shared/offline-queue-Bmeyg5fy.mjs +1 -0
  23. package/dist/packem_shared/{preload.d-CN5mOiAj.d.ts → preload.d-BAazUwhn.d.mts} +2 -2
  24. package/dist/packem_shared/{preload.d-BmPMaxYW.d.mts → preload.d-ClHaOfOQ.d.ts} +2 -2
  25. package/dist/packem_shared/{wire-codec-D4iww4NV.mjs → wire-codec-PBOTh_2d.mjs} +1 -1
  26. package/dist/packem_shared/{wire-key-BOdKmpG3.mjs → wire-key-Dl7EFWSD.mjs} +1 -1
  27. package/dist/query/index.d.mts +4 -3
  28. package/dist/query/index.d.ts +4 -3
  29. package/dist/service.d.mts +1 -1
  30. package/dist/service.d.ts +1 -1
  31. package/dist/service.mjs +1 -1
  32. package/dist/ssr/index.d.mts +4 -4
  33. package/dist/ssr/index.d.ts +4 -4
  34. package/dist/ssr/index.mjs +1 -1
  35. package/package.json +2 -2
  36. package/dist/packem_shared/CONFLICT_ERROR_CODE-LjU7z0mB.mjs +0 -1
  37. package/dist/packem_shared/LunoraClient-BKmMPd4k.mjs +0 -1
  38. package/dist/packem_shared/OfflineQueue-DG51qWl4.mjs +0 -1
  39. package/dist/packem_shared/applyDelta-DGqMpi3N.mjs +0 -1
  40. package/dist/packem_shared/createClientQuery-TKD_52cT.mjs +0 -1
  41. package/dist/packem_shared/createLocalStore-CZwiN9F4.mjs +0 -1
  42. package/dist/packem_shared/offline-queue-CzI2lYQ8.mjs +0 -1
  43. package/dist/packem_shared/types.d-BOB755CV.d.mts +0 -1015
  44. package/dist/packem_shared/types.d-BOB755CV.d.ts +0 -1015
@@ -1,4 +1,5 @@
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";
1
+ import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.js";
2
+ import { LunoraErrorCode } from '@lunora/errors';
2
3
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
3
4
  /**
4
5
  * Reactive key-value store for local-only client state.
@@ -36,60 +37,1051 @@ interface ClientQueryRef<T = unknown> {
36
37
  */
37
38
  declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
38
39
  /**
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).
40
+ * Typed reference to an HTTP-SSE stream route (`httpRoute.<verb>(path).stream()`)
41
+ * emitted by `@lunora/codegen` as `httpStreams.<namespace>.<name>`.
42
+ *
43
+ * Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
44
+ * (opened with `fetch` + `ReadableStream` against the route's own URL), not the
45
+ * WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
46
+ * and the route path; the phantom marker carries the chunk / searchParams /
47
+ * params types so `httpStream` (and the framework hooks over it) infer the
48
+ * chunk type end-to-end.
49
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
45
50
  */
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
+ interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
52
+ /**
53
+ * Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
54
+ * parameters for inference. Never present at runtime; declared in a
55
+ * covariant (output) position so a concrete reference stays assignable to
56
+ * a widened one.
57
+ */
58
+ readonly __lunoraHttpStream?: {
59
+ chunk: Chunk;
60
+ params: Params;
61
+ searchParams: SearchParams;
62
+ };
63
+ /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
64
+ readonly method: string;
65
+ /** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
66
+ readonly path: string;
67
+ }
51
68
  /**
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.
69
+ * The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
70
+ * @experimental Part of the HTTP-SSE stream surface.
56
71
  */
57
- declare const isConflictError: (error: unknown) => error is Error & {
58
- code: "CONFLICT";
59
- };
72
+ interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
73
+ /** Values for the route path's `:name` segments. */
74
+ params?: Params;
75
+ /** URL query params, appended to the request URL (undefined entries are skipped). */
76
+ searchParams?: SearchParams;
77
+ }
60
78
  /**
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.
79
+ * Extract the chunk type from a {@link HttpStreamRef}.
80
+ * @experimental Part of the HTTP-SSE stream surface.
64
81
  */
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
- };
82
+ type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
83
+ /**
84
+ * Extract the call-side args type from a {@link HttpStreamRef}.
85
+ * @experimental Part of the HTTP-SSE stream surface.
86
+ */
87
+ type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
88
+ type Unsubscribe = () => void;
89
+ /**
90
+ * Serializable result of `preloadQuery`. Produced on the server during SSR,
91
+ * embedded in the rendered HTML, then handed to `usePreloadedQuery` on the
92
+ * client so the first render shows the server value with no loading flash
93
+ * before a live subscription attaches. Every field survives `JSON.stringify`.
94
+ */
95
+ interface Preloaded<T = unknown> {
96
+ readonly __lunoraPreloaded: true;
97
+ readonly args: Record<string, unknown>;
98
+ readonly functionPath: string;
99
+ readonly shardKey?: string;
100
+ readonly value: T;
101
+ }
102
+ /**
103
+ * Pluggable storage for the `x-d1-bookmark` value used to provide
104
+ * read-your-writes between a mutation and subsequent queries.
105
+ */
106
+ interface BookmarkStorage {
107
+ get: () => string | null;
108
+ set: (value: string | null) => void;
109
+ }
110
+ interface ReconnectOptions {
111
+ initialDelayMs?: number;
112
+ jitter?: boolean;
113
+ maxDelayMs?: number;
114
+ }
115
+ /** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
116
+ type PersistenceOperation = "append" | "clear" | "load" | "remove";
117
+ /** Context handed to a persistence-error handler. */
118
+ interface PersistenceErrorContext {
119
+ readonly error: unknown;
120
+ /** The mutation id involved, when the failing op was scoped to one (`append`/`remove`). */
121
+ readonly mutationId?: string;
122
+ readonly operation: PersistenceOperation;
123
+ }
124
+ interface OfflineQueueOptions {
125
+ maxItems?: number;
126
+ /**
127
+ * Invoked when a {@link PersistenceAdapter} call rejects (e.g. IndexedDB quota
128
+ * exceeded). Without a handler, failures are logged via `console.warn` so they
129
+ * are never fully silent. Note: a failed `append` means the write is queued in
130
+ * memory but NOT durable — it will not survive a reload.
131
+ */
132
+ onPersistenceError?: (context: PersistenceErrorContext) => void;
133
+ /**
134
+ * Queue mutations issued before a shard's first successful WebSocket
135
+ * connect (defaults to `false`). The standard behaviour (`LunoraClient`'s
136
+ * `mutation()`) queues only when the targeted shard has been connected at
137
+ * least once (`wasEverConnected`), so the registry / resubscribe handshake
138
+ * has run. Set this to `true` for offline-first apps that want to enqueue
139
+ * writes on the very first session before the WS is up.
140
+ */
141
+ queueBeforeFirstConnect?: boolean;
142
+ }
143
+ /**
144
+ * Serializable shape of an offline mutation, durably stored by a
145
+ * {@link PersistenceAdapter} so queued writes survive a reload/crash. The live
146
+ * `resolve`/`reject` callbacks of an in-flight `QueuedMutation` are *not*
147
+ * persisted — a restored mutation is replayed with no original awaiter.
148
+ */
149
+ interface PersistedMutation {
150
+ args: Record<string, unknown>;
151
+ /**
152
+ * The client id that queued this write, persisted so a replay after a reload
153
+ * lands in the SAME server-side dedup namespace it was issued under. The
154
+ * standalone client's own `clientId` is minted per session, so replaying under
155
+ * the live one would miss the `__idempotency` row for an anonymous caller and
156
+ * re-run a write the server already committed. Absent on records written by
157
+ * older client versions, which replay under the live id.
158
+ */
159
+ clientId?: string;
160
+ functionPath: string;
161
+ id: string;
162
+ /**
163
+ * Issuing identity fingerprint, persisted so a hydrated write replays only
164
+ * under the identity that queued it (`null` = queued while signed out).
165
+ * Absent on records written by older client versions, which replay under
166
+ * the ambient identity for back-compat.
167
+ */
168
+ identity?: string | null;
169
+ shardKey?: string;
170
+ /**
171
+ * App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
172
+ * On hydrate, a record whose `version` doesn't match the current one is dropped
173
+ * and purged rather than replayed — so a write persisted by an older deploy
174
+ * (with a now-changed function signature) can't replay against the new schema.
175
+ * Absent when no `persistenceVersion` is configured (no version gating).
176
+ */
177
+ version?: string;
178
+ }
179
+ /**
180
+ * Durable store for the offline mutation queue. The default client keeps the
181
+ * queue in memory; supplying an adapter (e.g. `createIndexedDbPersistence`)
182
+ * makes queued writes survive a page reload. Implementations must preserve FIFO
183
+ * (enqueue) order in `PersistenceAdapter.load`.
184
+ *
185
+ * Replay semantics are at-least-once: a mutation is removed only after the
186
+ * server confirms (or rejects) it, so a crash between commit and `remove` can
187
+ * replay it again on the next load.
188
+ */
189
+ interface PersistenceAdapter {
190
+ /** Append a mutation to durable storage (called on enqueue). */
191
+ append: (mutation: PersistedMutation) => Promise<void>;
192
+ /** Drop every persisted mutation (e.g. on logout). */
193
+ clear: () => Promise<void>;
194
+ /** Load all persisted mutations in FIFO order — called once at startup. */
195
+ load: () => Promise<PersistedMutation[]>;
196
+ /** Remove a mutation by id once it has been replayed (resolved or rejected). */
197
+ remove: (id: string) => Promise<void>;
198
+ }
199
+ /**
200
+ * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
201
+ * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
202
+ * the durable outbox needs to dedupe and watermark replays.
203
+ */
204
+ interface OutboxMutation {
205
+ args: Record<string, unknown>;
206
+ /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
207
+ clientId: string;
208
+ functionPath: string;
209
+ /** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
210
+ idempotencyKey: string;
211
+ /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
212
+ identity: string | null;
213
+ /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
214
+ mutationId: number;
215
+ shardKey?: string;
216
+ }
217
+ /**
218
+ * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
219
+ * the client delegates offline write durability + at-least-once replay to this
220
+ * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
221
+ * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
222
+ * backed by the TanStack `OfflineExecutor`); the interface itself is
223
+ * dependency-free so `@lunora/client` stays TanStack-free.
224
+ */
225
+ interface OutboxSink {
226
+ /**
227
+ * Persist and schedule a write for replay. Rejects with an
228
+ * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
229
+ * the caller can surface back-pressure to the issuing mutation.
230
+ */
231
+ enqueue: (mutation: OutboxMutation) => Promise<void>;
232
+ }
233
+ /**
234
+ * One persisted query result in the durable read cache (Pillar 2). Keyed in the
235
+ * store by `shardKey + functionPath + argsKey`; the record carries everything
236
+ * needed to render offline on reload and to resume the live subscription.
237
+ */
238
+ interface CachedQuery {
239
+ /**
240
+ * Issuing identity fingerprint (same shape the offline queue stamps). A
241
+ * cached value only hydrates when it matches the current identity, so a
242
+ * signed-out cache never leaks into a new session. `null` = cached while
243
+ * signed out.
244
+ */
245
+ identity: string | null;
246
+ /**
247
+ * The `cursor` high-watermark this value reflects, replayed as `sinceSeq`
248
+ * on reconnect so the server can resume instead of re-snapshotting. Absent
249
+ * when the value predates CDC / no cursor was advertised.
250
+ */
251
+ serverCursor?: number;
252
+ /**
253
+ * The CDC `epoch` the `serverCursor` belongs to, replayed as `sinceEpoch`
254
+ * on reconnect so the server only resumes when the client is still on the
255
+ * same changelog timeline. Absent when no epoch was advertised.
256
+ */
257
+ serverEpoch?: string;
258
+ /** Wall-clock millis the value was written — drives LRU eviction. */
259
+ ts: number;
260
+ /** The full query result last seen from the server. */
261
+ value: unknown;
262
+ /**
263
+ * App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
264
+ * A cached value whose `version` doesn't match the current one is not hydrated —
265
+ * so a result of a now-changed shape from an older deploy can't render. Absent
266
+ * when no `persistenceVersion` is configured (no version gating).
267
+ */
268
+ version?: string;
269
+ }
270
+ /** A stored read-cache row: the {@link CachedQuery} plus the key it is stored under. */
271
+ interface StoredQuery extends CachedQuery {
272
+ key: string;
273
+ }
274
+ /**
275
+ * Durable store for the client read cache (Pillar 2): query results survive a
276
+ * reload so reads hydrate from disk and render immediately while the socket
277
+ * reconnects. Opt-in via {@link LunoraClientOptions.queryCache}; omit to keep
278
+ * reads in memory only (today's behaviour). Mirrors {@link PersistenceAdapter}'s
279
+ * shape over the same IndexedDB plumbing.
280
+ */
281
+ interface QueryCacheAdapter {
282
+ /** Drop every cached query (e.g. on logout / identity change). */
283
+ clear: () => Promise<void>;
284
+ /** Load every cached query — called once at startup to hydrate reads. */
285
+ load: () => Promise<StoredQuery[]>;
286
+ /** Upsert one cached query by key (called when a subscription value advances). */
287
+ put: (key: string, entry: CachedQuery) => Promise<void>;
288
+ /** Remove one cached query by key. */
289
+ remove: (key: string) => Promise<void>;
290
+ }
291
+ /**
292
+ * Resolves the WS `?token=` credential fresh at every (re)connect — the channel
293
+ * for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
294
+ * at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
295
+ * May return the token synchronously or as a Promise; returning `undefined`
296
+ * connects without a token. A thrown error / rejected Promise fails that
297
+ * connect attempt, and the client retries with its normal reconnect backoff.
298
+ */
299
+ type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
300
+ interface LunoraClientOptions {
301
+ /**
302
+ * Base path the worker mounts better-auth at, used by the client's
303
+ * `getCurrentUser()` to reach the `get-session` route. Defaults to
304
+ * `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
305
+ */
306
+ authBasePath?: string;
307
+ bookmarkStorage?: BookmarkStorage;
308
+ /**
309
+ * Stable per-client id backing the custom-mutator watermark. Sent on the
310
+ * `connect` envelope (so the server can scope this client's
311
+ * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
312
+ * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
313
+ * monotonic mutation id to form the idempotency key. The `@lunora/db` path
314
+ * persists a stable id alongside the outbox and passes it here; omit for the
315
+ * standalone client, which generates an ephemeral per-session id.
316
+ */
317
+ clientId?: string;
318
+ /**
319
+ * Default app context sent in the `connect` envelope right after each socket
320
+ * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
321
+ * as `event.context`. A per-shard context registered via
322
+ * `setConnectionContext` overrides this for that shard. Omit when no lifecycle
323
+ * hook needs connection context.
324
+ */
325
+ connectionContext?: Record<string, unknown>;
326
+ /**
327
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
328
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
329
+ * worker that never upgrades — the client force-closes the socket and routes
330
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
331
+ * of leaving the live channel silently stuck on the browser's much longer
332
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
333
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
334
+ */
335
+ connectTimeoutMs?: number;
336
+ /**
337
+ * When `true`, tabs sharing the same origin (and the same signed-in identity)
338
+ * coordinate via BroadcastChannel so only one tab — the "leader" — opens
339
+ * WebSocket connections to the server. Reduces simultaneous WS connections,
340
+ * bandwidth, and cross-tab state drift. Requires `BroadcastChannel`
341
+ * (browser-only); silently ignored otherwise. Defaults to `false`.
342
+ *
343
+ * **The channel is one-directional: leader → follower.** The leader
344
+ * broadcasts the values, errors, checkpoints and connection status of the
345
+ * subscriptions *it* holds; there is no frame with which a follower can ask
346
+ * the leader for anything.
347
+ *
348
+ * `subscribe` works on a follower and is how the relay delivers: the
349
+ * registration is what the leader's broadcast key is matched against, so a
350
+ * follower sees a value while the leader independently holds the same
351
+ * `(fn, args, shardKey)`.
352
+ *
353
+ * Nothing else is served, because the leader broadcasts nothing for it and a
354
+ * follower cannot ask. `subscribeShape` and `acquireConnectionContext` are
355
+ * inert on a follower — framework code (`@lunora/db`'s shape sync, every
356
+ * `usePresence` adapter) calls them from an effect the app cannot opt out
357
+ * of, so throwing would unwind the tab rather than degrade one feature.
358
+ * `whisper`, `whisperSubscribe`, `setConnectionContext` and `stream` are
359
+ * only ever called by app code, which can handle a failure, so those throw
360
+ * `NOT_IMPLEMENTED` rather than returning a handle that never fires. (The
361
+ * brief window every tab spends claiming leadership at startup is not a
362
+ * follower state: a lone tab self-promotes and its registered subscriptions
363
+ * are sent then.) HTTP surfaces — `query`, `mutation`, `action`, the offline
364
+ * queue's replay — are unaffected on every tab.
365
+ *
366
+ * So: enable this when your tabs run the SAME app views over plain
367
+ * `subscribe`, and leave it off if tabs can sit on different routes or you
368
+ * use shapes, whispers, streams, or connection context.
369
+ */
370
+ crossTabSync?: boolean;
371
+ fetch?: typeof fetch;
372
+ /**
373
+ * Interval (ms) between keepalive pings sent on each open subscription
374
+ * socket. The server answers them via the Durable Object's hibernation
375
+ * auto-response WITHOUT waking the DO, so an idle socket stays alive across
376
+ * hibernation without a billable wakeup. Defaults to 30000 (30s); set to
377
+ * `0` (or a negative value) to disable the heartbeat entirely.
378
+ */
379
+ heartbeatIntervalMs?: number;
380
+ /**
381
+ * When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
382
+ * wait for the durable cache to finish hydrating before their first render
383
+ * with an enabled subscription, so users see cached data instead of an
384
+ * undefined flash before the socket round-trip. Defaults to `false`.
385
+ *
386
+ * Requires `queryCache` to be set (not `false`); silently ignored otherwise.
387
+ */
388
+ hydrateOnStart?: boolean;
389
+ offlineQueue?: OfflineQueueOptions;
390
+ /**
391
+ * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
392
+ * path wires `createExecutorOutboxSink`), offline mutations are delegated to
393
+ * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
394
+ * is bypassed, so a db app has exactly one durable write path. Omit for the
395
+ * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
396
+ */
397
+ outbox?: OutboxSink;
398
+ /**
399
+ * Durable store for the offline mutation queue. Tri-state — an explicit
400
+ * {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
401
+ * in memory, lost on reload); omitted (the default) auto-probes a durable
402
+ * IndexedDB store when the `indexedDB` global is present (browsers), otherwise
403
+ * in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
404
+ * environments that can persist do. Pass `createAsyncStoragePersistence()` on
405
+ * React Native.
406
+ */
407
+ persistence?: false | PersistenceAdapter;
408
+ /**
409
+ * App/schema version stamped onto every persisted queued write and cached
410
+ * read. Bump it on a breaking change to a function signature or query shape:
411
+ * on the next boot, persisted writes / cached reads stamped with a different
412
+ * version are dropped (and purged) rather than replayed / hydrated against the
413
+ * new schema. Omit to disable version gating (records are never invalidated by
414
+ * version).
415
+ *
416
+ * **Adoption is itself an invalidation event:** records written before you set
417
+ * `persistenceVersion` carry no version, so the first boot after enabling it
418
+ * purges all currently-queued offline writes (and cached reads) as stale. Adopt
419
+ * it on a build where that clean slate is acceptable — typically the same
420
+ * breaking deploy you're protecting against — not purely speculatively.
421
+ */
422
+ persistenceVersion?: string;
423
+ /**
424
+ * Durable store for the read cache (Pillar 2). When active, query results
425
+ * are persisted as their subscriptions advance and hydrated on construction
426
+ * so a reload renders cached data before the socket reconnects, then resumes
427
+ * the live subscription from the persisted cursor. Tri-state — an explicit
428
+ * {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
429
+ * memory only); omitted (the default) auto-probes IndexedDB exactly like
430
+ * {@link LunoraClientOptions.persistence}.
431
+ */
432
+ queryCache?: QueryCacheAdapter | false;
433
+ reconnect?: ReconnectOptions;
434
+ url: string;
435
+ WebSocket?: typeof WebSocket;
436
+ /**
437
+ * Credential appended to the WebSocket URL as `?token=…`. The server matches
438
+ * it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
439
+ * `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
440
+ * what the studio supplies). Browsers can't set headers on the `WebSocket`
441
+ * constructor, so the query parameter is the only channel; it ends up in
442
+ * server logs and history, so prefer a short-lived rotating token in
443
+ * production over a static secret.
444
+ *
445
+ * Pass a {@link WsTokenProvider} function to resolve the token fresh at
446
+ * every (re)connect — the channel for short-lived credentials such as the
447
+ * ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
448
+ * provider re-mints on each reconnect, including the one following a `4001`
449
+ * token-expired drop, so a static master token never has to ride the URL.
450
+ */
451
+ wsToken?: string | WsTokenProvider;
452
+ wsUrl?: string;
453
+ }
454
+ /** Wire envelope sent on `POST /_lunora/rpc`. */
455
+ interface RpcEnvelope {
456
+ args?: Record<string, unknown>;
457
+ /**
458
+ * Stable per-client identifier (custom-mutator push path). Pairs with
459
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
460
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
461
+ */
462
+ clientId?: string;
463
+ functionPath: string;
464
+ /**
465
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
466
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
467
+ * `client.mutation` calls.
468
+ */
469
+ idempotencyKey?: string;
470
+ /**
471
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
472
+ * server-side per-client watermark: `id <= watermark` is a replay (skipped),
473
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
474
+ * batch so the client resends from `watermark + 1`. Absent on plain
475
+ * `client.mutation` calls.
476
+ */
477
+ mutationId?: number;
478
+ shardKey?: string;
479
+ }
72
480
  /**
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}.
481
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
482
+ * watermarked custom-mutator push additionally carries `lastMutationId` the
483
+ * highest per-client sequence the DO has applied — which the client uses to keep
484
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
485
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
486
+ * committed at — which gates the drop of a per-call optimistic layer.
75
487
  */
76
- declare const isRateLimitedError: (error: unknown) => error is Error & {
77
- code: "TOO_MANY_REQUESTS";
488
+ type RpcResponseBody = {
489
+ error: {
490
+ code: string;
491
+ data?: unknown;
492
+ message: string;
493
+ };
494
+ } | {
495
+ commitCursor?: number;
496
+ lastMutationId?: number;
497
+ result: unknown;
78
498
  };
499
+ /** Subscription protocol — client → server. */
500
+ interface ClientSubscribeMessage {
501
+ id: string;
502
+ /**
503
+ * `sinceSeq` is the persisted `cursor` high-watermark the client last saw
504
+ * for this shard (Pillar 1b resume). Present only when a durable
505
+ * {@link QueryCacheAdapter} restored a cached value with a cursor; the
506
+ * server replies with a lightweight `resume` frame instead of a full
507
+ * snapshot when nothing the query reads changed since it. Absent on a
508
+ * first-time subscribe.
509
+ */
510
+ query: {
511
+ args?: Record<string, unknown>;
512
+ functionPath?: string;
513
+ sinceEpoch?: string;
514
+ sinceSeq?: number;
515
+ table?: string;
516
+ };
517
+ type: "subscribe";
518
+ }
519
+ interface ClientUnsubscribeMessage {
520
+ id: string;
521
+ type: "unsubscribe";
522
+ }
79
523
  /**
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).
524
+ * One-shot control frame sent right after the socket opens. Registers the
525
+ * connection's app `context` (e.g. `{ roomId, sessionId }`) with the server and
526
+ * fires the `onConnect` lifecycle hooks; the same context is replayed to
527
+ * `onDisconnect` when the socket drops.
84
528
  */
85
- declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
529
+ interface ClientConnectMessage {
530
+ /**
531
+ * Wire behaviours this client can handle that an older one cannot, so the
532
+ * server can use them without breaking clients that can't. Currently just
533
+ * `"pageDelta"`; see `shared/page-result.ts` for what it promises and why it
534
+ * must be announced rather than assumed. Omitting it is always safe.
535
+ */
536
+ caps?: ReadonlyArray<string>;
537
+ /**
538
+ * Stable per-client id (persisted alongside the outbox). Lets the server
539
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
540
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
541
+ * use custom mutators.
542
+ */
543
+ clientId?: string;
544
+ context?: Record<string, unknown>;
545
+ id: string;
546
+ type: "connect";
547
+ }
548
+ /**
549
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
550
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
551
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
552
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
553
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
554
+ * echoed as `shapeId` on every poke part.
555
+ */
556
+ interface ClientShapeSubscribeMessage {
557
+ id: string;
558
+ shape: {
559
+ args?: Record<string, unknown>;
560
+ name: string;
561
+ };
562
+ /**
563
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
564
+ * applied for this shape). When absent or below the server's retained floor
565
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
566
+ * delta.
567
+ */
568
+ sinceCheckpoint?: number;
569
+ /**
570
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
571
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
572
+ * the cursor is numerically in range.
573
+ */
574
+ sinceEpoch?: string;
575
+ type: "shape_subscribe";
576
+ }
577
+ /** Cancel a shape subscription started with the same `id`. */
578
+ interface ClientShapeUnsubscribeMessage {
579
+ id: string;
580
+ type: "shape_unsubscribe";
581
+ }
582
+ interface ClientAckMessage {
583
+ id: string;
584
+ type: "ack";
585
+ }
586
+ /**
587
+ * Start a streaming query. The id namespaces a fresh stream and is echoed on
588
+ * every {@link ServerChunkMessage} the server pushes back. Cancel a running
589
+ * stream by sending a {@link ClientUnsubscribeMessage} with the same id —
590
+ * subscription and stream id-spaces share the cancel channel; the prefix
591
+ * (`sub_*` vs `stream_*`) keeps the local registries searchable.
592
+ */
593
+ interface ClientStreamMessage {
594
+ /**
595
+ * Run generation the {@link ClientStreamMessage.sinceChunk} watermark
596
+ * belongs to: the `generation` stamp carried by the chunk frames this
597
+ * client already received, echoed back on a resume. The server refuses to
598
+ * splice a different run's tail onto the held prefix — a mismatch fails
599
+ * with `STREAM_INTERRUPTED` instead. Omitted on a first attach.
600
+ */
601
+ generation?: number;
602
+ id: string;
603
+ query: {
604
+ args?: Record<string, unknown>;
605
+ functionPath: string;
606
+ shardKey?: string;
607
+ };
608
+ /**
609
+ * Resume watermark: the highest chunk `seq` this client already received.
610
+ * Only meaningful for a stream the server declared `durable` — the run
611
+ * replays everything after it and then continues live, which is what turns
612
+ * a reconnect into a resume instead of a lost generation. Omitted on a
613
+ * first attach.
614
+ *
615
+ * Named `sinceChunk`, not `sinceSeq`, because a subscribe envelope already
616
+ * carries a `query.sinceSeq` meaning the CDC cursor.
617
+ */
618
+ sinceChunk?: number;
619
+ type: "stream";
620
+ }
86
621
  /**
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}.
622
+ * Join or leave a whisper `topic` an app-chosen ephemeral channel scoped to a
623
+ * shard. While joined, the client receives every {@link ServerWhisperMessage}
624
+ * other members broadcast to the topic.
91
625
  */
92
- declare const getRetryAfterMs: (error: unknown) => number | undefined;
626
+ interface ClientWhisperSubscribeMessage {
627
+ topic: string;
628
+ type: "whisper_subscribe" | "whisper_unsubscribe";
629
+ }
630
+ /**
631
+ * Broadcast ephemeral `data` to the topic's other members on the shard. The
632
+ * payload is relayed verbatim with no server-side persistence (no SQLite/CDC
633
+ * write) — for typing indicators, live cursors, presence pings. The sender does
634
+ * not receive its own whisper.
635
+ */
636
+ interface ClientWhisperMessage {
637
+ data?: unknown;
638
+ topic: string;
639
+ type: "whisper";
640
+ }
641
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
642
+ /** Subscription protocol — server → client. */
643
+ interface ServerDataMessage {
644
+ /**
645
+ * The `__cdc_log` high-watermark covered by this frame (Pillar 1b). The
646
+ * client persists it as the query's `serverCursor` and replays it as
647
+ * `sinceSeq` on the next reconnect. Absent on shards that never enabled CDC.
648
+ */
649
+ cursor?: number;
650
+ data?: unknown;
651
+ delta?: unknown;
652
+ /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
653
+ epoch?: string;
654
+ id: string;
655
+ /**
656
+ * The highest custom-mutator `mutationId` from this client the server has
657
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
658
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
659
+ * the matching optimistic overlay. Absent on shards without custom mutators.
660
+ */
661
+ lastMutationId?: number;
662
+ type: "data" | "delta";
663
+ }
664
+ /**
665
+ * Lightweight resume acknowledgement (Pillar 1b): the server determined that
666
+ * nothing the subscription reads changed since the client's `sinceSeq`, so it
667
+ * skips re-sending the snapshot. The client keeps its cached value and only
668
+ * advances `serverCursor` to `cursor`.
669
+ */
670
+ interface ServerResumeMessage {
671
+ cursor?: number;
672
+ /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
673
+ epoch?: string;
674
+ id: string;
675
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
676
+ lastMutationId?: number;
677
+ type: "resume";
678
+ }
679
+ /**
680
+ * Settled acknowledgement for a **list** subscription: a write touched one of
681
+ * the subscription's read tables but produced a byte-identical result, so the
682
+ * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
683
+ * client (one that announced a `clientId`, hence has a server-side
684
+ * `__client_watermark`) so its optimistic list overlay drops even when no data
685
+ * frame arrives. Plain `useQuery` subscribers never receive it, and an older
686
+ * client safely ignores the unknown frame.
687
+ */
688
+ interface ServerSettledMessage {
689
+ cursor?: number;
690
+ /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
691
+ epoch?: string;
692
+ id: string;
693
+ /**
694
+ * The highest custom-mutator `mutationId` from this client the server has
695
+ * now applied (the per-client `__client_watermark`). Forwarded to a
696
+ * collection's `onCheckpoint` so it can drop the overlay for the confirmed
697
+ * write whose result didn't change this list.
698
+ */
699
+ lastMutationId?: number;
700
+ type: "settled";
701
+ }
702
+ interface ServerErrorMessage {
703
+ error?: unknown;
704
+ id?: string;
705
+ message?: string;
706
+ type: "error";
707
+ }
708
+ interface ServerAckMessage {
709
+ id: string;
710
+ type: "ack";
711
+ }
712
+ interface ServerCompleteMessage {
713
+ id: string;
714
+ type: "complete";
715
+ }
716
+ /** One frame of a streaming query — `data` carries the user-yielded chunk. */
717
+ interface ServerChunkMessage {
718
+ data: unknown;
719
+ /**
720
+ * Generation stamp of the **durable** run this chunk belongs to. The client
721
+ * stores it beside {@link ServerChunkMessage.seq} and echoes it as
722
+ * {@link ClientStreamMessage.generation} on a resume, so the server can
723
+ * tell a genuine resume from an attempt to splice onto a different run
724
+ * under the same key. Absent on an ephemeral stream.
725
+ */
726
+ generation?: number;
727
+ id: string;
728
+ /**
729
+ * Monotonic position of this chunk within a **durable** run, starting at 1.
730
+ * The client stores the last one it saw and replays it as
731
+ * {@link ClientStreamMessage.sinceChunk} when the socket comes back. Absent
732
+ * on an ephemeral stream, which has nothing to resume from.
733
+ */
734
+ seq?: number;
735
+ type: "chunk";
736
+ }
737
+ /**
738
+ * An ephemeral whisper relayed from another member of `topic` on the same shard
739
+ * (AnyCable-style whispering). `data` is the sender's payload verbatim; `from`
740
+ * is the sender's verified user id when known (absent for an anonymous sender).
741
+ * Never persisted server-side.
742
+ */
743
+ interface ServerWhisperMessage {
744
+ data: unknown;
745
+ from?: string;
746
+ topic: string;
747
+ type: "whisper";
748
+ }
749
+ /**
750
+ * One row-level change in a shape's replication stream — the wire form of the
751
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
752
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
753
+ * the removed row by `key` alone. The client applies these to its local
754
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
755
+ * never had in this shape).
756
+ */
757
+ interface RowOp {
758
+ /** Row primary key (`_id`). */
759
+ key: string;
760
+ op: "delete" | "insert" | "update";
761
+ /** Logical table the row belongs to. */
762
+ table: string;
763
+ /** Post-image document for insert/update; absent on delete. */
764
+ value?: Record<string, unknown>;
765
+ }
766
+ /**
767
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
768
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
769
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
770
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
771
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
772
+ */
773
+ interface ServerPokeStartMessage {
774
+ /**
775
+ * Poke-level fallback base, stamped by single-part senders. Per-shape
776
+ * {@link ServerPokePartMessage.baseCheckpoint} takes precedence; this is what
777
+ * a part without its own base falls back to.
778
+ */
779
+ baseCheckpoint?: number;
780
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
781
+ epoch?: string;
782
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
783
+ pokeId: string;
784
+ type: "pokeStart";
785
+ }
786
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
787
+ interface ServerPokePartMessage {
788
+ /**
789
+ * The checkpoint this shape's view must be at for `rowsPatch` to splice on
790
+ * cleanly. Per shape, because every shape on a socket has its own
791
+ * delivered-through cursor. Absent when the server cannot name a base — the
792
+ * gap check is then disarmed for this part, never guessed at.
793
+ */
794
+ baseCheckpoint?: number;
795
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
796
+ lastMutationId?: number;
797
+ pokeId: string;
798
+ /**
799
+ * `true` when `rowsPatch` is the shape's COMPLETE membership, not a diff (a
800
+ * full seed or re-seed). The client MUST drop its current view for this shape
801
+ * before applying: a seed is inserts-only, so merging it leaves any row that
802
+ * left the shape while the client was disconnected on screen forever.
803
+ *
804
+ * Never inferred from an absent {@link ServerPokePartMessage.baseCheckpoint} —
805
+ * most live poke paths legitimately carry no base.
806
+ */
807
+ reset?: boolean;
808
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
809
+ rowsPatch: RowOp[];
810
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
811
+ shapeId: string;
812
+ type: "pokePart";
813
+ }
814
+ /**
815
+ * Closes a poke: the client commits the buffered parts atomically and advances
816
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
817
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
818
+ * the next reconnect.
819
+ */
820
+ interface ServerPokeEndMessage {
821
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
822
+ checkpoint?: number;
823
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
824
+ epoch?: string;
825
+ pokeId: string;
826
+ type: "pokeEnd";
827
+ }
828
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
829
+ /**
830
+ * The authenticated user as exposed client-side, mirroring better-auth's
831
+ * `user` row (the `user` field of the `get-session` response). Kept minimal
832
+ * and structural — only `id` is guaranteed; the rest are the common better-auth
833
+ * fields, and the index signature carries any plugin-contributed extras.
834
+ */
835
+ interface User {
836
+ readonly createdAt?: NullableTimestamp;
837
+ readonly email?: null | string;
838
+ readonly emailVerified?: boolean | null;
839
+ readonly id: string;
840
+ readonly image?: null | string;
841
+ readonly name?: null | string;
842
+ readonly [key: string]: unknown;
843
+ readonly updatedAt?: NullableTimestamp;
844
+ }
845
+ /**
846
+ * Per-job retry policy carried on a {@link ScheduleRecord}. Mirrors
847
+ * `@lunora/scheduler`'s `RetryPolicy`; absent means the scheduler's defaults.
848
+ */
849
+ interface ScheduleRetryPolicy {
850
+ /** Backoff growth across attempts. Default `"exponential"`. */
851
+ backoff?: "exponential" | "linear";
852
+ /** Base delay in milliseconds for the first retry. Default `30_000`. */
853
+ baseMs?: number;
854
+ /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
855
+ maxAttempts?: number;
856
+ /** Optional ceiling clamping the computed backoff delay. */
857
+ maxMs?: number;
858
+ }
859
+ /**
860
+ * One pending scheduled function, as returned by the worker's
861
+ * `GET /_lunora/admin/scheduled` endpoint. The route is a byte-for-byte proxy of
862
+ * the SchedulerDO's own `/list`, so this mirrors `@lunora/scheduler`'s
863
+ * `ScheduleRecord` field-for-field — structurally, so the client carries no
864
+ * dependency on it. `packages/client/__tests__/structural-mirrors.test.ts` fails
865
+ * when the two drift.
866
+ */
867
+ interface ScheduleRecord {
868
+ args: Record<string, unknown>;
869
+ /**
870
+ * Dispatch attempts already made. Absent (treated as 0) until the first
871
+ * failure; on a dead-letter record it is the exhausted count (> the retry
872
+ * budget). Surfaced so the studio can show how hard a job tried before it
873
+ * was parked.
874
+ */
875
+ attempts?: number;
876
+ enqueuedAt: number;
877
+ /**
878
+ * The `ns:fn` path dispatched on fire. Absent when the job targets a durable
879
+ * workflow/agent instead — exactly one of `functionPath` /
880
+ * {@link ScheduleRecord.workflow} is set, so a view rendering a job's target
881
+ * must fall back to `workflow` rather than assuming a path.
882
+ */
883
+ functionPath?: string;
884
+ id: string;
885
+ /** Scheduler/workpool instance the job was enqueued through. Absent for the default instance. */
886
+ instanceName?: string;
887
+ /** Logical workpool the job is routed to (concurrency-gated), when any. */
888
+ pool?: string;
889
+ /** Per-job retry policy; absent means the scheduler's built-in defaults. */
890
+ retry?: ScheduleRetryPolicy;
891
+ scheduledFor: number;
892
+ shardKey?: string;
893
+ /**
894
+ * The `WORKFLOW_*`/`AGENT_*` binding a fresh durable instance is started from
895
+ * on fire (the {@link ScheduleRecord.args} become its `params`). Set instead
896
+ * of {@link ScheduleRecord.functionPath}.
897
+ */
898
+ workflow?: string;
899
+ }
900
+ /**
901
+ * One workpool's live backlog, as returned by the worker's
902
+ * `GET /_lunora/admin/scheduled/status` endpoint. Mirrors `@lunora/scheduler`'s
903
+ * `SchedulerPoolStatus` structurally so the client carries no dependency on it.
904
+ */
905
+ interface SchedulerPoolStatus {
906
+ /** Jobs currently dispatched-but-not-yet-completed (the held concurrency slots). */
907
+ inFlight: number;
908
+ /** The pool's concurrency cap. */
909
+ maxConcurrency: number;
910
+ /** The logical workpool name. */
911
+ name: string;
912
+ /** Pending jobs routed to this pool but not yet dispatched. */
913
+ queued: number;
914
+ }
915
+ /**
916
+ * The app-level scheduler backlog, as returned by the worker's
917
+ * `GET /_lunora/admin/scheduled/status` endpoint. `pools` is the per-pool
918
+ * breakdown; `backlog` and `inFlight` are the app-wide sums of `queued` and
919
+ * `inFlight` across every pool — the headline numbers for the studio SLO
920
+ * view. Mirrors `@lunora/scheduler`'s `SchedulerStatus` structurally.
921
+ */
922
+ interface SchedulerStatus {
923
+ /** Sum of every pool's `queued` count — the total pending backlog. */
924
+ backlog: number;
925
+ /** Sum of every pool's `inFlight` count — the total held concurrency slots. */
926
+ inFlight: number;
927
+ /** Per-pool backlog breakdown. */
928
+ pools: SchedulerPoolStatus[];
929
+ }
930
+ /**
931
+ * One shard's request volume, as returned by the worker's
932
+ * `POST /_lunora/admin/shard-traffic` endpoint. The cross-shard traffic feed
933
+ * the studio's `hot_shard` advisor lint consumes: `requests` is the shard's
934
+ * lifetime dispatch total, `shardKey` the DO id name (`""` for the root shard).
935
+ */
936
+ interface ShardTrafficEntry {
937
+ requests: number;
938
+ shardKey: string;
939
+ }
940
+ /**
941
+ * The whole-shard-set traffic distribution returned by the worker's
942
+ * `POST /_lunora/admin/shard-traffic` endpoint. `shards` is one entry per live
943
+ * shard (a failed shard surfaces with `requests: 0`); `ok`/`failed` count the
944
+ * shards that returned vs. errored. Shaped to feed the advisor's `hot_shard`
945
+ * lint after the studio tags each entry with its sharded function `group`.
946
+ */
947
+ interface ShardTrafficResult {
948
+ failed: number;
949
+ ok: number;
950
+ shards: ShardTrafficEntry[];
951
+ }
952
+ /**
953
+ * One object in the storage bucket, as returned by the worker's
954
+ * `GET /_lunora/admin/storage` endpoint. Mirrors `@lunora/storage`'s
955
+ * `R2ObjectLike` structurally.
956
+ */
957
+ interface StorageObject {
958
+ customMetadata?: Record<string, string>;
959
+ etag: string;
960
+ httpMetadata?: {
961
+ contentType?: string;
962
+ };
963
+ key: string;
964
+ size: number;
965
+ /**
966
+ * When the object was stored. R2 emits a `Date`, which JSON-serializes to an
967
+ * ISO string over the wire; a mock may supply epoch ms — so consumers should
968
+ * normalise via `new Date(uploaded)`. Absent if the backend didn't report it.
969
+ */
970
+ uploaded?: number | string;
971
+ }
972
+ /** One page of {@link StorageObject}s plus the cursor to fetch the next, if any. */
973
+ interface StorageListPage {
974
+ cursor?: string;
975
+ objects: StorageObject[];
976
+ }
977
+ /**
978
+ * One argument of a registered function, derived from its `v.*` validator by the
979
+ * worker. A compact signature shape — enough to render a function's API without
980
+ * the build-time codegen types.
981
+ */
982
+ interface FunctionArgumentDescriptor {
983
+ /** Element validator kind for an `array` arg (one level), e.g. `string`. */
984
+ element?: string;
985
+ /** The (optional-unwrapped) validator kind, e.g. `string`, `id`, `object`. */
986
+ kind: string;
987
+ /** The argument name. */
988
+ name: string;
989
+ /** True when the arg is wrapped in `v.optional(...)`. */
990
+ optional: boolean;
991
+ /** Target table for an `id` arg (`v.id("table")`). */
992
+ table?: string;
993
+ }
994
+ /**
995
+ * One registered function, as returned by the worker's
996
+ * `GET /_lunora/admin/functions` endpoint: its `<file>:<function>` path, which
997
+ * client method (`query` / `mutation` / `action`) invokes it, and its argument
998
+ * signature. `args` is absent on responses from an older worker.
999
+ */
1000
+ interface FunctionDescriptor {
1001
+ args?: FunctionArgumentDescriptor[];
1002
+ kind: "action" | "mutation" | "query";
1003
+ path: string;
1004
+ }
1005
+ /** A `.global()` (D1-backed) table plus its row count, from `/_lunora/admin/global/tables`. */
1006
+ interface GlobalTableInfo {
1007
+ name: string;
1008
+ rowCount: number;
1009
+ }
1010
+ /** A window of rows from one global table, from `/_lunora/admin/global/table`. */
1011
+ interface GlobalTablePage {
1012
+ columns: string[];
1013
+ /** FK columns (local column → referenced table) for external tables with real `REFERENCES` constraints, from `PRAGMA foreign_key_list`. */
1014
+ refs?: Record<string, string>;
1015
+ rows: Record<string, unknown>[];
1016
+ total: number;
1017
+ }
1018
+ /**
1019
+ * One equality constraint a facet-value click adds to the global browser's view
1020
+ * (`column = value`). `value` is the raw stored scalar the facet returned, sent
1021
+ * as-is and bound server-side, so it never injects SQL.
1022
+ */
1023
+ interface GlobalFilterClause {
1024
+ column: string;
1025
+ value: unknown;
1026
+ }
1027
+ /** One distinct value of a faceted global column with its row count, from `/_lunora/admin/global/facet`. */
1028
+ interface GlobalFacetValue {
1029
+ count: number;
1030
+ value: unknown;
1031
+ }
1032
+ /** Per-column distinct-value summary for the global browser, from `/_lunora/admin/global/facet`. */
1033
+ interface GlobalFacetResult {
1034
+ truncated: boolean;
1035
+ values: GlobalFacetValue[];
1036
+ }
1037
+ /** A nullable timestamp field as better-auth serializes it: epoch-ms, ISO string, or null. */
1038
+ type NullableTimestamp = null | number | string;
1039
+ /** A workflow instance's lifecycle status. Mirrors Cloudflare's `InstanceStatus`. */
1040
+ type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
1041
+ /** The lifecycle mutations the status endpoint accepts. */
1042
+ type WorkflowInstanceAction = "pause" | "resume" | "terminate";
1043
+ /** One row of the workflow-instances list. */
1044
+ interface WorkflowInstanceSummary {
1045
+ createdOn?: string;
1046
+ endedOn?: string;
1047
+ id: string;
1048
+ startedOn?: string;
1049
+ status: WorkflowInstanceStatus;
1050
+ }
1051
+ /** One durable step of an instance's execution timeline. */
1052
+ interface WorkflowStepDetail {
1053
+ /** 1-based attempt count (`> 1` means the step retried). */
1054
+ attempts?: number;
1055
+ end?: string;
1056
+ error?: unknown;
1057
+ name: string;
1058
+ output?: unknown;
1059
+ start?: string;
1060
+ success?: boolean;
1061
+ /** `step` / `sleep` / `waitForEvent` / … (Cloudflare's step `type`). */
1062
+ type?: string;
1063
+ }
1064
+ /** A workflow instance's full detail: summary plus params/output/error and the step timeline. */
1065
+ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
1066
+ error?: unknown;
1067
+ output?: unknown;
1068
+ params?: unknown;
1069
+ steps: WorkflowStepDetail[];
1070
+ }
1071
+ /** A page of workflow instances. */
1072
+ interface WorkflowInstancePage {
1073
+ /**
1074
+ * Whether workflow inspection is configured on the worker (a Cloudflare
1075
+ * account id + API token). `false` when the admin proxy reports it can't
1076
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
1077
+ * caller render a "set credentials" state without a failed request.
1078
+ */
1079
+ configured?: boolean;
1080
+ instances: WorkflowInstanceSummary[];
1081
+ page: number;
1082
+ perPage: number;
1083
+ totalCount?: number;
1084
+ }
93
1085
  type SubscriptionCallback = (data: unknown) => void;
94
1086
  /** A subscription-scoped error the server pushed for this subscription id. */
95
1087
  interface SubscriptionError {
@@ -208,6 +1200,14 @@ declare class SubscriptionRegistry {
208
1200
  add(state: SubscriptionState): void;
209
1201
  remove(state: SubscriptionState): void;
210
1202
  all(): SubscriptionState[];
1203
+ /**
1204
+ * Drop every registration. Terminal — used by `LunoraClient.close()`, whose
1205
+ * whole point is to release the callback closures each {@link SubscriptionState}
1206
+ * holds (`callbacks`, `errorCallbacks`, `checkpointCallbacks` — React state
1207
+ * setters and `@lunora/db` collection closures), which otherwise outlive the
1208
+ * closed client for as long as the client object is reachable.
1209
+ */
1210
+ clear(): void;
211
1211
  }
212
1212
  /**
213
1213
  * Read/write handle over the client's live query cache, handed to a mutation's
@@ -694,6 +1694,31 @@ declare class LunoraClient {
694
1694
  */
695
1695
  private readonly streams;
696
1696
  /** Live shape subscriptions (partial replication), keyed by their wire id. */
1697
+ /**
1698
+ * Teardown callbacks for the admin sockets {@link LunoraClient.subscribeScheduledJobs}
1699
+ * opens. Those run their own reconnect loop off a closure-local `closed`
1700
+ * flag rather than `this.closed` (they predate `ensureSocket`'s guard), so
1701
+ * without this registry a `close()` left every one of them reconnecting on
1702
+ * its backoff forever — re-minting an ephemeral admin sub-token on each
1703
+ * attempt when a `WsTokenProvider` is wired.
1704
+ */
1705
+ private readonly adminSocketTeardowns;
1706
+ /**
1707
+ * The in-flight offline-queue replay per shard (`connectionKey`), while one
1708
+ * is running. Two jobs:
1709
+ *
1710
+ * 1. It serializes overlapping flushes for the same shard — two reconnect
1711
+ * events in quick succession used to drain and replay concurrently.
1712
+ * 2. It is the barrier {@link LunoraClient.mutation} waits on before sending a FRESH
1713
+ * write directly. The socket's `open` handler flips `wsState` to `"open"`
1714
+ * first and calls the flush last, so from that instant `mutation()`'s
1715
+ * offline gate is false and a new write raced straight to `/rpc` against
1716
+ * the replay of the older, queued write for the same document — the newer
1717
+ * one could land first and then be overwritten by the older. Ordering
1718
+ * inside the replay (`replaySequential`) never covered this, because the
1719
+ * racing write was never in the queue.
1720
+ */
1721
+ private readonly offlineFlushes;
697
1722
  private readonly shapeSubscriptions;
698
1723
  /**
699
1724
  * In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
@@ -813,6 +1838,10 @@ declare class LunoraClient {
813
1838
  * Stored per shard and replayed on every (re)connect. When a socket for the
814
1839
  * shard is already open, a fresh `connect` envelope is sent immediately so the
815
1840
  * server sees the new context without waiting for a reconnect.
1841
+ *
1842
+ * Not available on a `crossTabSync` FOLLOWER tab — the context rides the
1843
+ * `connect` envelope of a socket a follower does not own, so it would be
1844
+ * stored and never sent. Throws `NOT_IMPLEMENTED` there.
816
1845
  */
817
1846
  setConnectionContext(context: Record<string, unknown> | undefined, options?: {
818
1847
  shardKey?: string;
@@ -852,6 +1881,11 @@ declare class LunoraClient {
852
1881
  * do not put data on a whisper topic that some shard members shouldn't see,
853
1882
  * and don't trust a whisper's `data` as authorization. Use a query/mutation
854
1883
  * (with RLS) for anything privileged; whispers are for transient awareness.
1884
+ *
1885
+ * Not available on a `crossTabSync` FOLLOWER tab — whisper frames are not
1886
+ * relayed over the cross-tab channel, so this throws `NOT_IMPLEMENTED`
1887
+ * there rather than registering a handler nothing can ever reach. See
1888
+ * {@link LunoraClientOptions.crossTabSync}.
855
1889
  */
856
1890
  whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
857
1891
  shardKey?: string;
@@ -863,6 +1897,11 @@ declare class LunoraClient {
863
1897
  * queued), and the server silently drops it if the sender exceeds its
864
1898
  * whisper rate budget. The sender never receives its own whisper. Omitting
865
1899
  * `data` delivers JSON `null` to receivers (not `undefined`).
1900
+ *
1901
+ * That best-effort drop is for a socket that is momentarily down. A
1902
+ * `crossTabSync` FOLLOWER tab has no socket and never will (see
1903
+ * {@link LunoraClientOptions.crossTabSync}), so every whisper from it would
1904
+ * be dropped forever — it throws `NOT_IMPLEMENTED` instead.
866
1905
  */
867
1906
  whisper(topic: string, data?: unknown, options?: {
868
1907
  shardKey?: string;
@@ -1069,6 +2108,22 @@ declare class LunoraClient {
1069
2108
  * Mutations issued before the very first WS connect to a shard fail fast.
1070
2109
  * Opt into queueing-before-first-connect via
1071
2110
  * `OfflineQueueOptions.queueBeforeFirstConnect`.
2111
+ *
2112
+ * **Return-value caveat — the queued paths do not carry the server's result.**
2113
+ * The declared `Promise<ReturnOf<F>>` only holds when the write goes straight
2114
+ * to the server. Once a write is queued:
2115
+ *
2116
+ * - with a durable `outbox` configured, this resolves **immediately with
2117
+ * `undefined`** (typed as `ReturnOf<F>`) the moment the write is handed to
2118
+ * the outbox — the replay happens later, out of band, with no awaiter;
2119
+ * - with the built-in offline queue, it stays pending until the replay lands
2120
+ * and then resolves with the replayed call's value.
2121
+ *
2122
+ * So `const id = await client.mutation(api.todos.create, …)` is `undefined`
2123
+ * for every write issued while offline under an outbox. Generate ids
2124
+ * client-side (or read them back from a subscription) rather than depending
2125
+ * on a mutation's return value in an offline-capable app —
2126
+ * {@link LunoraClient.importRows} documents the same caveat for its counts.
1072
2127
  */
1073
2128
  mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
1074
2129
  action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
@@ -1309,6 +2364,11 @@ declare class LunoraClient {
1309
2364
  * `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
1310
2365
  * with a `storageDelete` function and `adminToken`. Powers the studio file
1311
2366
  * browser's per-row delete; resolves `{ deleted, key }`.
2367
+ *
2368
+ * An absent `deleted` field reads as `false`, matching every sibling admin
2369
+ * verb (`runCronJob`'s `ran`, …): the studio renders this value as the row's
2370
+ * outcome, so defaulting a missing field to success would report a delete
2371
+ * that a mismatched/older worker never performed.
1312
2372
  */
1313
2373
  deleteStorageObject(key: string, options?: {
1314
2374
  bucket?: string;
@@ -1364,8 +2424,15 @@ declare class LunoraClient {
1364
2424
  /**
1365
2425
  * Read a page of rows from one `.global()` table. `filters` AND-narrows the
1366
2426
  * 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.
2427
+ * facet-value click applies; the array is wire-encoded, JSON-encoded into the
2428
+ * `filters` query param, and the values are bound server-side.
2429
+ *
2430
+ * The response is `decodeWire`d. The worker encodes it (`readGlobalTablePage`
2431
+ * in `@lunora/d1`) because JSON cannot carry a `v.bigint()` column at all and
2432
+ * silently flattens a `v.bytes()` one to `{}` — so without the decode here the
2433
+ * grid renders the raw 3-element tagged array instead of the value. The shard
2434
+ * browser's twin already pairs the same way through `rpc`; this is the global
2435
+ * half of that symmetry.
1369
2436
  */
1370
2437
  readGlobalTablePage(options: {
1371
2438
  filters?: GlobalFilterClause[];
@@ -1379,6 +2446,12 @@ declare class LunoraClient {
1379
2446
  * twin of the shard browser's facet. Hits the admin-gated
1380
2447
  * `GET /_lunora/admin/global/facet` endpoint; `column` is validated + bound
1381
2448
  * server-side. Powers the global data browser's facet sidebar.
2449
+ *
2450
+ * Wire-encoded/decoded on both legs for the same reason
2451
+ * {@link LunoraClient.readGlobalTablePage} is: a facet over a BLOB column
2452
+ * returns bytes, which `Response.json` flattens to `{}` — and since a facet
2453
+ * value is exactly what a click sends back as a `filters` clause, that is a
2454
+ * broken drill-down rather than a display glitch.
1382
2455
  */
1383
2456
  facetGlobalColumn(options: {
1384
2457
  column: string;
@@ -1700,6 +2773,20 @@ declare class LunoraClient {
1700
2773
  offset?: number;
1701
2774
  userId?: string;
1702
2775
  }): Promise<AuthPage<AuthSession>>;
2776
+ /**
2777
+ * Subscribe to a live query. The callback fires with the current value (from
2778
+ * the durable read cache, when one is hydrated) and again on every server
2779
+ * frame; the returned function unsubscribes.
2780
+ *
2781
+ * Subscriptions are deduped by `(functionPath, args, shardKey)` — a second
2782
+ * `subscribe` for the same triple joins the existing registration and shares
2783
+ * its value, cursor and optimistic layers.
2784
+ *
2785
+ * Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
2786
+ * carries the LEADER's own subscriptions outward, so a follower would receive
2787
+ * this query's frames only if the leader happened to hold it too. Throws
2788
+ * `NOT_IMPLEMENTED` there — see {@link LunoraClientOptions.crossTabSync}.
2789
+ */
1703
2790
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1704
2791
  onCheckpoint?: (watermark: SyncWatermark) => void;
1705
2792
  onError?: SubscriptionErrorCallback;
@@ -1716,6 +2803,11 @@ declare class LunoraClient {
1716
2803
  * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1717
2804
  * (name, args): the server resolves them under the socket's verified identity,
1718
2805
  * so every call gets its own id + view. The returned function unsubscribes.
2806
+ *
2807
+ * Not available on a `crossTabSync` FOLLOWER tab: shape pokes are not part of
2808
+ * the leader→follower broadcast set, so a follower's shape could never
2809
+ * resolve. Throws `NOT_IMPLEMENTED` there — see
2810
+ * {@link LunoraClientOptions.crossTabSync}.
1719
2811
  */
1720
2812
  subscribeShape(shape: {
1721
2813
  args?: Record<string, unknown>;
@@ -1770,6 +2862,53 @@ declare class LunoraClient {
1770
2862
  close(): void;
1771
2863
  /** Guard shared by every public entry point: a closed client accepts no further calls. */
1772
2864
  private assertOpen;
2865
+ /**
2866
+ * `true` when this tab is a cross-tab FOLLOWER of a live leader — i.e. it
2867
+ * will not open a socket of its own and another tab is known to hold one.
2868
+ *
2869
+ * Deliberately NOT just `!isLeader()`. Every `crossTabSync` client is a
2870
+ * non-leader for the first `leaderTimeout` of its life, while its
2871
+ * claim-leadership probe is outstanding; a lone tab self-promotes at the end
2872
+ * of that window and `onBecomeLeader` opens the sockets and replays every
2873
+ * registered subscription. That window is a legitimate, self-healing defer,
2874
+ * not a failure. A KNOWN leader on another tab is the state that never heals.
2875
+ */
2876
+ private followsAnotherTab;
2877
+ /**
2878
+ * Reject a call that needs a socket this tab will never have.
2879
+ *
2880
+ * The cross-tab protocol is one-directional: a leader broadcasts
2881
+ * `subscription-data` / `-error` / `-settled` / `connection-status` to
2882
+ * followers, and a follower has no frame with which to tell the leader what
2883
+ * it needs (see `cross-tab.ts`'s `WsFollowerMessage`, which is exactly
2884
+ * heartbeat / claim-leadership / yield-leadership). `subscribeShape` /
2885
+ * `whisper*` / `setConnectionContext` / `acquireConnectionContext` — none of
2886
+ * which the leader broadcasts at all — therefore never worked on a follower
2887
+ * under any circumstances: each returned a handle that looked live, fired no
2888
+ * callback, raised no error, and reported `connectionStatus() ===
2889
+ * "connected"` (mirrored from the leader).
2890
+ *
2891
+ * `subscribe` is deliberately NOT in that set. A follower's `subscribe`
2892
+ * registers the key the leader's broadcast is matched against, so it is the
2893
+ * mechanism the relay is built on rather than a surface that silently fails.
2894
+ * A follower sees a query only while the leader holds the same
2895
+ * `(fn, args, shardKey)` — that is the documented shape of the option, not a
2896
+ * defect.
2897
+ *
2898
+ * `stream()` reaches the same outcome by a different route: it fails the
2899
+ * handle it returns rather than throwing at the call. See
2900
+ * {@link LunoraClientOptions.crossTabSync} for what the option does and does
2901
+ * not cover.
2902
+ */
2903
+ private assertLeaderOwnedSurface;
2904
+ /**
2905
+ * Clear every timer a {@link ShardConnection} can have armed.
2906
+ *
2907
+ * One function because both teardown paths must clear all three and a fourth
2908
+ * timer would otherwise have to be remembered in two places — which is how a
2909
+ * leak gets added rather than written.
2910
+ */
2911
+ private clearConnectionTimers;
1773
2912
  /**
1774
2913
  * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
1775
2914
  * connect timers, stop its heartbeat, and close its socket (if any).
@@ -1777,6 +2916,15 @@ declare class LunoraClient {
1777
2916
  * handler (demoted, but still alive) so a demoted leader can't leak a
1778
2917
  * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
1779
2918
  * an inline `conn.socket?.close()` — which skips both — used to.
2919
+ *
2920
+ * Settles this shard's in-flight streams first. The teardown clears
2921
+ * `conn.socket` BEFORE the real `close` event fires, so that event trips
2922
+ * `openManagedSocket`'s identity guard (`conn.socket !== socket`) and
2923
+ * returns — meaning `handleDisconnect`, the only other place that settles a
2924
+ * shard's streams, never runs for this connection again. `close()` already
2925
+ * failed and cleared `this.streams` before it gets here, so this is a no-op
2926
+ * on that path; the cross-tab demotion path is the one where a consumer's
2927
+ * `for await` used to block forever with no error and no completion.
1780
2928
  */
1781
2929
  private teardownConnection;
1782
2930
  /**
@@ -2155,12 +3303,21 @@ declare class LunoraClient {
2155
3303
  */
2156
3304
  private rejectQueuedForIdentityChange;
2157
3305
  /**
2158
- * Migrate every live identity stamp from `from` to `to` — used when the auth
3306
+ * Migrate every identity stamp from `from` to `to` — used when the auth
2159
3307
  * identity label changes but the underlying credential (token) does NOT, e.g.
2160
3308
  * the user id resolves a tick after the token was set. The in-memory
2161
3309
  * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2162
3310
  * keeps the in-flight writes replayable under the new (more stable) identity
2163
3311
  * instead of the flush guard discarding them as a mismatch.
3312
+ *
3313
+ * That map alone was not enough: it is consumed and DELETED on the first
3314
+ * flush attempt (`passesReplayIdentityGate`), while the queue entry and its
3315
+ * persisted record keep the original stamp. So a reload, or a requeue after a
3316
+ * transient failure, fell back to the old token hash — and once the token had
3317
+ * been refreshed, `isSameCredentialUnderTokenHash` no longer recognised it
3318
+ * and the write was rejected `OFFLINE_IDENTITY_CHANGED` for the very user
3319
+ * `setAuthToken`'s sticky-`subject` contract promises to protect. The queue's
3320
+ * own re-stamp covers both the entry and its durable record.
2164
3321
  */
2165
3322
  private restampQueuedIdentity;
2166
3323
  /**
@@ -2197,7 +3354,15 @@ declare class LunoraClient {
2197
3354
  * nothing), so over-inclusion here is harmless.
2198
3355
  */
2199
3356
  private flushAllOfflineQueues;
3357
+ /**
3358
+ * Replay a shard's queued writes, serialized per shard and published as
3359
+ * {@link offlineFlushes} so a concurrent `mutation()` can wait behind it.
3360
+ * Never rejects: every entry's outcome is settled individually inside
3361
+ * {@link drainOfflineQueue}, and a poisoned chain would strand every later
3362
+ * flush AND every write waiting on the barrier.
3363
+ */
2200
3364
  private flushOfflineQueue;
3365
+ private drainOfflineQueue;
2201
3366
  /**
2202
3367
  * Partition already-gated writes into the encodable ones (returned) and reject
2203
3368
  * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
@@ -2302,4 +3467,4 @@ declare class LunoraClient {
2302
3467
  */
2303
3468
  private settleReplayBatchSlots;
2304
3469
  }
2305
- 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 };
3470
+ export { ServerPokePartMessage as $, ActionCallOptions as A, BookmarkStorage as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, OutboxMutation as E, FunctionArgumentDescriptor as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxSink as I, PersistedMutation as J, RowOp as K, LunoraClient as L, MutationCallOptions as M, RpcEnvelope as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReconnectOptions as R, SubscriptionError as S, RpcResponseBody as T, User as U, ScheduleRecord as V, ScheduleRetryPolicy as W, SchedulerPoolStatus as X, SchedulerStatus as Y, ServerMessage as Z, ServerPokeEndMessage as _, Unsubscribe as a, ServerPokeStartMessage as a0, ShardTrafficEntry as a1, ShardTrafficResult as a2, StorageListPage as a3, StorageObject as a4, StoredQuery as a5, StreamHandle as a6, SubscriptionCallback as a7, SubscriptionRegistry as a8, SubscriptionState as a9, SyncWatermark as aa, WorkflowInstanceAction as ab, WorkflowInstanceDetail as ac, WorkflowInstancePage as ad, WorkflowInstanceStatus as ae, WorkflowInstanceSummary as af, WorkflowStepDetail as ag, WsTokenProvider as ah, createClientQuery as ai, createLocalStore as aj, createStream as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, BatchSlot as g, CachedQuery as h, ClientDebugShard as i, ClientDebugSnapshot as j, ClientDebugSubscription as k, ClientMessage as l, ClientQueryRef as m, ClientShapeSubscribeMessage as n, ClientShapeUnsubscribeMessage as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, MutationSettledEvent as x, OptimisticLocalStore as y, OptimisticUpdate as z };