@lunora/client 1.0.0-alpha.35 → 1.0.0-alpha.37

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 (37) hide show
  1. package/dist/auth/index.d.mts +2 -1
  2. package/dist/auth/index.d.ts +2 -1
  3. package/dist/index.d.mts +131 -8
  4. package/dist/index.d.ts +131 -8
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/LunoraClient-BzVeQ8ub.mjs +1 -0
  7. package/dist/packem_shared/OfflineQueue-y4mymc5n.mjs +1 -0
  8. package/dist/packem_shared/{SubscriptionRegistry-CdrOD7sZ.mjs → SubscriptionRegistry-Dr5H2kVT.mjs} +1 -1
  9. package/dist/packem_shared/TabCoordinator-CMFYQCL-.mjs +1 -0
  10. package/dist/packem_shared/createLocalStore-7bvzJ_xS.mjs +1 -0
  11. package/dist/packem_shared/{createServerClient-DX7Dk2w2.mjs → createServerClient-BMtqMeyY.mjs} +1 -1
  12. package/dist/packem_shared/{createSnapshotPrecondition-DOKRAJ4b.mjs → createSnapshotPrecondition-9rTzUURM.mjs} +1 -1
  13. package/dist/packem_shared/{local-store-C53xDvEr.mjs → local-store-Di_y1q4e.mjs} +1 -1
  14. package/dist/packem_shared/{lunora-client.d-DLcz4GRz.d.mts → lunora-client.d-BkfH0Zom.d.ts} +128 -947
  15. package/dist/packem_shared/{lunora-client.d-DLcz4GRz.d.ts → lunora-client.d-yDX6HUZv.d.mts} +128 -947
  16. package/dist/packem_shared/offline-queue-BeWay5u5.mjs +1 -0
  17. package/dist/packem_shared/{preload.d-C1M_N5mE.d.mts → preload.d--OibiIK_.d.ts} +2 -1
  18. package/dist/packem_shared/{preload.d-hkFC0Uno.d.ts → preload.d-DbtqnQ4C.d.mts} +2 -1
  19. package/dist/packem_shared/types.d-CxmbJf0E.d.mts +942 -0
  20. package/dist/packem_shared/types.d-CxmbJf0E.d.ts +942 -0
  21. package/dist/packem_shared/wire-codec-Ctnni0h6.mjs +1 -0
  22. package/dist/packem_shared/wire-key-DHMKiMxD.mjs +1 -0
  23. package/dist/query/index.d.mts +3 -2
  24. package/dist/query/index.d.ts +3 -2
  25. package/dist/service.d.mts +49 -0
  26. package/dist/service.d.ts +49 -0
  27. package/dist/service.mjs +1 -0
  28. package/dist/ssr/index.d.mts +4 -3
  29. package/dist/ssr/index.d.ts +4 -3
  30. package/dist/ssr/index.mjs +1 -1
  31. package/package.json +6 -2
  32. package/dist/packem_shared/LunoraClient-DRo2LNe9.mjs +0 -1
  33. package/dist/packem_shared/OfflineQueue-l0_y9TmK.mjs +0 -1
  34. package/dist/packem_shared/TabCoordinator-ijLic02x.mjs +0 -1
  35. package/dist/packem_shared/createLocalStore-B3Tw_Tw1.mjs +0 -1
  36. package/dist/packem_shared/offline-queue-b-v6bWWd.mjs +0 -1
  37. package/dist/packem_shared/wire-key-D_zOxXK4.mjs +0 -1
@@ -1,3 +1,4 @@
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-CxmbJf0E.js";
1
2
  import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
2
3
  /**
3
4
  * Reactive key-value store for local-only client state.
@@ -89,947 +90,6 @@ declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
89
90
  * when absent/non-numeric. Pair with {@link isRateLimitedError}.
90
91
  */
91
92
  declare const getRetryAfterMs: (error: unknown) => number | undefined;
92
- /** The registered function kinds a {@link FunctionReference} can describe. `stream` is a query that yields multiple frames over the WS. */
93
- type FunctionKind = "action" | "mutation" | "query" | "stream";
94
- /**
95
- * Opaque reference to a registered function emitted by `@lunora/codegen`.
96
- *
97
- * At runtime it carries the `<file>:<function>` identifier in `__lunoraRef`.
98
- * Generated declarations decorate this with phantom type parameters so the
99
- * client can infer args / return values per call site.
100
- */
101
- interface FunctionReference<Kind extends FunctionKind = FunctionKind, Args = unknown, Return = unknown> {
102
- /**
103
- * Phantom marker carrying the `Kind`/`Args`/`Return` type parameters for
104
- * inference. Never present at runtime; declared as a covariant (output)
105
- * position so a concrete reference stays assignable to a widened one.
106
- */
107
- readonly __lunoraPhantom?: {
108
- args: Args;
109
- kind: Kind;
110
- returns: Return;
111
- };
112
- readonly __lunoraRef: string;
113
- }
114
- /** Extract the args type from a {@link FunctionReference}. */
115
- type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
116
- /** Extract the return type from a {@link FunctionReference}. */
117
- type ReturnOf<F> = F extends FunctionReference<infer _K, infer _A, infer R> ? R : never;
118
- /**
119
- * Typed reference to an HTTP-SSE stream route (`httpRoute.&lt;verb>(path).stream()`)
120
- * emitted by `@lunora/codegen` as `httpStreams.&lt;namespace>.&lt;name>`.
121
- *
122
- * Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
123
- * (opened with `fetch` + `ReadableStream` against the route's own URL), not the
124
- * WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
125
- * and the route path; the phantom marker carries the chunk / searchParams /
126
- * params types so `httpStream` (and the framework hooks over it) infer the
127
- * chunk type end-to-end.
128
- * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
129
- */
130
- interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
131
- /**
132
- * Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
133
- * parameters for inference. Never present at runtime; declared in a
134
- * covariant (output) position so a concrete reference stays assignable to
135
- * a widened one.
136
- */
137
- readonly __lunoraHttpStream?: {
138
- chunk: Chunk;
139
- params: Params;
140
- searchParams: SearchParams;
141
- };
142
- /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
143
- readonly method: string;
144
- /** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
145
- readonly path: string;
146
- }
147
- /**
148
- * The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
149
- * @experimental Part of the HTTP-SSE stream surface.
150
- */
151
- interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
152
- /** Values for the route path's `:name` segments. */
153
- params?: Params;
154
- /** URL query params, appended to the request URL (undefined entries are skipped). */
155
- searchParams?: SearchParams;
156
- }
157
- /**
158
- * Extract the chunk type from a {@link HttpStreamRef}.
159
- * @experimental Part of the HTTP-SSE stream surface.
160
- */
161
- type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
162
- /**
163
- * Extract the call-side args type from a {@link HttpStreamRef}.
164
- * @experimental Part of the HTTP-SSE stream surface.
165
- */
166
- type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
167
- type Unsubscribe = () => void;
168
- /**
169
- * Serializable result of `preloadQuery`. Produced on the server during SSR,
170
- * embedded in the rendered HTML, then handed to `usePreloadedQuery` on the
171
- * client so the first render shows the server value with no loading flash
172
- * before a live subscription attaches. Every field survives `JSON.stringify`.
173
- */
174
- interface Preloaded<T = unknown> {
175
- readonly __lunoraPreloaded: true;
176
- readonly args: Record<string, unknown>;
177
- readonly functionPath: string;
178
- readonly shardKey?: string;
179
- readonly value: T;
180
- }
181
- /**
182
- * Pluggable storage for the `x-d1-bookmark` value used to provide
183
- * read-your-writes between a mutation and subsequent queries.
184
- */
185
- interface BookmarkStorage {
186
- get: () => string | null;
187
- set: (value: string | null) => void;
188
- }
189
- interface ReconnectOptions {
190
- initialDelayMs?: number;
191
- jitter?: boolean;
192
- maxDelayMs?: number;
193
- }
194
- /** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
195
- type PersistenceOperation = "append" | "clear" | "load" | "remove";
196
- /** Context handed to a persistence-error handler. */
197
- interface PersistenceErrorContext {
198
- readonly error: unknown;
199
- /** The mutation id involved, when the failing op was scoped to one (`append`/`remove`). */
200
- readonly mutationId?: string;
201
- readonly operation: PersistenceOperation;
202
- }
203
- interface OfflineQueueOptions {
204
- maxItems?: number;
205
- /**
206
- * Invoked when a {@link PersistenceAdapter} call rejects (e.g. IndexedDB quota
207
- * exceeded). Without a handler, failures are logged via `console.warn` so they
208
- * are never fully silent. Note: a failed `append` means the write is queued in
209
- * memory but NOT durable — it will not survive a reload.
210
- */
211
- onPersistenceError?: (context: PersistenceErrorContext) => void;
212
- /**
213
- * Queue mutations issued before a shard's first successful WebSocket
214
- * connect (defaults to `false`). The standard behaviour (`LunoraClient`'s
215
- * `mutation()`) queues only when the targeted shard has been connected at
216
- * least once (`wasEverConnected`), so the registry / resubscribe handshake
217
- * has run. Set this to `true` for offline-first apps that want to enqueue
218
- * writes on the very first session before the WS is up.
219
- */
220
- queueBeforeFirstConnect?: boolean;
221
- }
222
- /**
223
- * Serializable shape of an offline mutation, durably stored by a
224
- * {@link PersistenceAdapter} so queued writes survive a reload/crash. The live
225
- * `resolve`/`reject` callbacks of an in-flight `QueuedMutation` are *not*
226
- * persisted — a restored mutation is replayed with no original awaiter.
227
- */
228
- interface PersistedMutation {
229
- args: Record<string, unknown>;
230
- functionPath: string;
231
- id: string;
232
- /**
233
- * Issuing identity fingerprint, persisted so a hydrated write replays only
234
- * under the identity that queued it (`null` = queued while signed out).
235
- * Absent on records written by older client versions, which replay under
236
- * the ambient identity for back-compat.
237
- */
238
- identity?: string | null;
239
- shardKey?: string;
240
- /**
241
- * App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
242
- * On hydrate, a record whose `version` doesn't match the current one is dropped
243
- * and purged rather than replayed — so a write persisted by an older deploy
244
- * (with a now-changed function signature) can't replay against the new schema.
245
- * Absent when no `persistenceVersion` is configured (no version gating).
246
- */
247
- version?: string;
248
- }
249
- /**
250
- * Durable store for the offline mutation queue. The default client keeps the
251
- * queue in memory; supplying an adapter (e.g. `createIndexedDbPersistence`)
252
- * makes queued writes survive a page reload. Implementations must preserve FIFO
253
- * (enqueue) order in `PersistenceAdapter.load`.
254
- *
255
- * Replay semantics are at-least-once: a mutation is removed only after the
256
- * server confirms (or rejects) it, so a crash between commit and `remove` can
257
- * replay it again on the next load.
258
- */
259
- interface PersistenceAdapter {
260
- /** Append a mutation to durable storage (called on enqueue). */
261
- append: (mutation: PersistedMutation) => Promise<void>;
262
- /** Drop every persisted mutation (e.g. on logout). */
263
- clear: () => Promise<void>;
264
- /** Load all persisted mutations in FIFO order — called once at startup. */
265
- load: () => Promise<PersistedMutation[]>;
266
- /** Remove a mutation by id once it has been replayed (resolved or rejected). */
267
- remove: (id: string) => Promise<void>;
268
- }
269
- /**
270
- * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
271
- * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
272
- * the durable outbox needs to dedupe and watermark replays.
273
- */
274
- interface OutboxMutation {
275
- args: Record<string, unknown>;
276
- /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
277
- clientId: string;
278
- functionPath: string;
279
- /** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
280
- idempotencyKey: string;
281
- /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
282
- identity: string | null;
283
- /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
284
- mutationId: number;
285
- shardKey?: string;
286
- }
287
- /**
288
- * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
289
- * the client delegates offline write durability + at-least-once replay to this
290
- * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
291
- * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
292
- * backed by the TanStack `OfflineExecutor`); the interface itself is
293
- * dependency-free so `@lunora/client` stays TanStack-free.
294
- */
295
- interface OutboxSink {
296
- /**
297
- * Persist and schedule a write for replay. Rejects with an
298
- * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
299
- * the caller can surface back-pressure to the issuing mutation.
300
- */
301
- enqueue: (mutation: OutboxMutation) => Promise<void>;
302
- }
303
- /**
304
- * One persisted query result in the durable read cache (Pillar 2). Keyed in the
305
- * store by `shardKey + functionPath + argsKey`; the record carries everything
306
- * needed to render offline on reload and to resume the live subscription.
307
- */
308
- interface CachedQuery {
309
- /**
310
- * Issuing identity fingerprint (same shape the offline queue stamps). A
311
- * cached value only hydrates when it matches the current identity, so a
312
- * signed-out cache never leaks into a new session. `null` = cached while
313
- * signed out.
314
- */
315
- identity: string | null;
316
- /**
317
- * The `cursor` high-watermark this value reflects, replayed as `sinceSeq`
318
- * on reconnect so the server can resume instead of re-snapshotting. Absent
319
- * when the value predates CDC / no cursor was advertised.
320
- */
321
- serverCursor?: number;
322
- /**
323
- * The CDC `epoch` the `serverCursor` belongs to, replayed as `sinceEpoch`
324
- * on reconnect so the server only resumes when the client is still on the
325
- * same changelog timeline. Absent when no epoch was advertised.
326
- */
327
- serverEpoch?: string;
328
- /** Wall-clock millis the value was written — drives LRU eviction. */
329
- ts: number;
330
- /** The full query result last seen from the server. */
331
- value: unknown;
332
- /**
333
- * App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
334
- * A cached value whose `version` doesn't match the current one is not hydrated —
335
- * so a result of a now-changed shape from an older deploy can't render. Absent
336
- * when no `persistenceVersion` is configured (no version gating).
337
- */
338
- version?: string;
339
- }
340
- /**
341
- * Durable store for the client read cache (Pillar 2): query results survive a
342
- * reload so reads hydrate from disk and render immediately while the socket
343
- * reconnects. Opt-in via {@link LunoraClientOptions.queryCache}; omit to keep
344
- * reads in memory only (today's behaviour). Mirrors {@link PersistenceAdapter}'s
345
- * shape over the same IndexedDB plumbing.
346
- */
347
- interface QueryCacheAdapter {
348
- /** Drop every cached query (e.g. on logout / identity change). */
349
- clear: () => Promise<void>;
350
- /** Load every cached query — called once at startup to hydrate reads. */
351
- load: () => Promise<(CachedQuery & {
352
- key: string;
353
- })[]>;
354
- /** Upsert one cached query by key (called when a subscription value advances). */
355
- put: (key: string, entry: CachedQuery) => Promise<void>;
356
- /** Remove one cached query by key. */
357
- remove: (key: string) => Promise<void>;
358
- }
359
- /**
360
- * Resolves the WS `?token=` credential fresh at every (re)connect — the channel
361
- * for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
362
- * at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
363
- * May return the token synchronously or as a Promise; returning `undefined`
364
- * connects without a token. A thrown error / rejected Promise fails that
365
- * connect attempt, and the client retries with its normal reconnect backoff.
366
- */
367
- type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
368
- interface LunoraClientOptions {
369
- /**
370
- * Base path the worker mounts better-auth at, used by the client's
371
- * `getCurrentUser()` to reach the `get-session` route. Defaults to
372
- * `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
373
- */
374
- authBasePath?: string;
375
- bookmarkStorage?: BookmarkStorage;
376
- /**
377
- * Stable per-client id backing the custom-mutator watermark. Sent on the
378
- * `connect` envelope (so the server can scope this client's
379
- * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
380
- * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
381
- * monotonic mutation id to form the idempotency key. The `@lunora/db` path
382
- * persists a stable id alongside the outbox and passes it here; omit for the
383
- * standalone client, which generates an ephemeral per-session id.
384
- */
385
- clientId?: string;
386
- /**
387
- * Default app context sent in the `connect` envelope right after each socket
388
- * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
389
- * as `event.context`. A per-shard context registered via
390
- * `setConnectionContext` overrides this for that shard. Omit when no lifecycle
391
- * hook needs connection context.
392
- */
393
- connectionContext?: Record<string, unknown>;
394
- /**
395
- * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
396
- * handshake doesn't complete within this window — a hung dev proxy or a cold
397
- * worker that never upgrades — the client force-closes the socket and routes
398
- * through its normal reconnect/backoff (surfacing `offline` status) instead
399
- * of leaving the live channel silently stuck on the browser's much longer
400
- * default. Does not affect HTTP queries/mutations (those never ride the WS).
401
- * Defaults to 10000 (10s); set to `0` (or negative) to disable.
402
- */
403
- connectTimeoutMs?: number;
404
- /**
405
- * When `true`, tabs sharing the same origin coordinate via BroadcastChannel
406
- * so only one tab (the "leader") opens WebSocket connections to the server.
407
- * Follower tabs receive subscription data through the channel instead.
408
- *
409
- * Reduces simultaneous WS connections, bandwidth, and cross-tab state drift.
410
- * Requires `BroadcastChannel` (browser-only); silently ignored otherwise.
411
- * Defaults to `false`.
412
- */
413
- crossTabSync?: boolean;
414
- fetch?: typeof fetch;
415
- /**
416
- * Interval (ms) between keepalive pings sent on each open subscription
417
- * socket. The server answers them via the Durable Object's hibernation
418
- * auto-response WITHOUT waking the DO, so an idle socket stays alive across
419
- * hibernation without a billable wakeup. Defaults to 30000 (30s); set to
420
- * `0` (or a negative value) to disable the heartbeat entirely.
421
- */
422
- heartbeatIntervalMs?: number;
423
- /**
424
- * When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
425
- * wait for the durable cache to finish hydrating before their first render
426
- * with an enabled subscription, so users see cached data instead of an
427
- * undefined flash before the socket round-trip. Defaults to `false`.
428
- *
429
- * Requires `queryCache` to be set (not `false`); silently ignored otherwise.
430
- */
431
- hydrateOnStart?: boolean;
432
- offlineQueue?: OfflineQueueOptions;
433
- /**
434
- * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
435
- * path wires `createExecutorOutboxSink`), offline mutations are delegated to
436
- * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
437
- * is bypassed, so a db app has exactly one durable write path. Omit for the
438
- * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
439
- */
440
- outbox?: OutboxSink;
441
- /**
442
- * Durable store for the offline mutation queue. Tri-state — an explicit
443
- * {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
444
- * in memory, lost on reload); omitted (the default) auto-probes a durable
445
- * IndexedDB store when the `indexedDB` global is present (browsers), otherwise
446
- * in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
447
- * environments that can persist do. Pass `createAsyncStoragePersistence()` on
448
- * React Native.
449
- */
450
- persistence?: false | PersistenceAdapter;
451
- /**
452
- * App/schema version stamped onto every persisted queued write and cached
453
- * read. Bump it on a breaking change to a function signature or query shape:
454
- * on the next boot, persisted writes / cached reads stamped with a different
455
- * version are dropped (and purged) rather than replayed / hydrated against the
456
- * new schema. Omit to disable version gating (records are never invalidated by
457
- * version).
458
- *
459
- * **Adoption is itself an invalidation event:** records written before you set
460
- * `persistenceVersion` carry no version, so the first boot after enabling it
461
- * purges all currently-queued offline writes (and cached reads) as stale. Adopt
462
- * it on a build where that clean slate is acceptable — typically the same
463
- * breaking deploy you're protecting against — not purely speculatively.
464
- */
465
- persistenceVersion?: string;
466
- /**
467
- * Durable store for the read cache (Pillar 2). When active, query results
468
- * are persisted as their subscriptions advance and hydrated on construction
469
- * so a reload renders cached data before the socket reconnects, then resumes
470
- * the live subscription from the persisted cursor. Tri-state — an explicit
471
- * {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
472
- * memory only); omitted (the default) auto-probes IndexedDB exactly like
473
- * {@link LunoraClientOptions.persistence}.
474
- */
475
- queryCache?: QueryCacheAdapter | false;
476
- reconnect?: ReconnectOptions;
477
- url: string;
478
- WebSocket?: typeof WebSocket;
479
- /**
480
- * Credential appended to the WebSocket URL as `?token=…`. The server matches
481
- * it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
482
- * `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
483
- * what the studio supplies). Browsers can't set headers on the `WebSocket`
484
- * constructor, so the query parameter is the only channel; it ends up in
485
- * server logs and history, so prefer a short-lived rotating token in
486
- * production over a static secret.
487
- *
488
- * Pass a {@link WsTokenProvider} function to resolve the token fresh at
489
- * every (re)connect — the channel for short-lived credentials such as the
490
- * ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
491
- * provider re-mints on each reconnect, including the one following a `4001`
492
- * token-expired drop, so a static master token never has to ride the URL.
493
- */
494
- wsToken?: string | WsTokenProvider;
495
- wsUrl?: string;
496
- }
497
- /** Wire envelope sent on `POST /_lunora/rpc`. */
498
- interface RpcEnvelope {
499
- args?: Record<string, unknown>;
500
- /**
501
- * Stable per-client identifier (custom-mutator push path). Pairs with
502
- * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
503
- * server `__client_watermark`. Absent on plain `client.mutation` calls.
504
- */
505
- clientId?: string;
506
- functionPath: string;
507
- /**
508
- * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
509
- * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
510
- * `client.mutation` calls.
511
- */
512
- idempotencyKey?: string;
513
- /**
514
- * Monotonic per-client mutation id (custom-mutator push path), backing the
515
- * server-side per-client watermark: `id &lt;= watermark` is a replay (skipped),
516
- * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
517
- * batch so the client resends from `watermark + 1`. Absent on plain
518
- * `client.mutation` calls.
519
- */
520
- mutationId?: number;
521
- shardKey?: string;
522
- }
523
- /**
524
- * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
525
- * watermarked custom-mutator push additionally carries `lastMutationId` — the
526
- * highest per-client sequence the DO has applied — which the client uses to keep
527
- * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
528
- * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
529
- * committed at — which gates the drop of a per-call optimistic layer.
530
- */
531
- type RpcResponseBody = {
532
- error: {
533
- code: string;
534
- data?: unknown;
535
- message: string;
536
- };
537
- } | {
538
- commitCursor?: number;
539
- lastMutationId?: number;
540
- result: unknown;
541
- };
542
- /** Subscription protocol — client → server. */
543
- interface ClientSubscribeMessage {
544
- id: string;
545
- /**
546
- * `sinceSeq` is the persisted `cursor` high-watermark the client last saw
547
- * for this shard (Pillar 1b resume). Present only when a durable
548
- * {@link QueryCacheAdapter} restored a cached value with a cursor; the
549
- * server replies with a lightweight `resume` frame instead of a full
550
- * snapshot when nothing the query reads changed since it. Absent on a
551
- * first-time subscribe.
552
- */
553
- query: {
554
- args?: Record<string, unknown>;
555
- functionPath?: string;
556
- sinceEpoch?: string;
557
- sinceSeq?: number;
558
- table?: string;
559
- };
560
- type: "subscribe";
561
- }
562
- interface ClientUnsubscribeMessage {
563
- id: string;
564
- type: "unsubscribe";
565
- }
566
- /**
567
- * One-shot control frame sent right after the socket opens. Registers the
568
- * connection's app `context` (e.g. `{ roomId, sessionId }`) with the server and
569
- * fires the `onConnect` lifecycle hooks; the same context is replayed to
570
- * `onDisconnect` when the socket drops.
571
- */
572
- interface ClientConnectMessage {
573
- /**
574
- * Stable per-client id (persisted alongside the outbox). Lets the server
575
- * scope this connection's `__client_watermark` so custom-mutator pokes can
576
- * echo the right per-client `lastMutationId`. Omitted by clients that don't
577
- * use custom mutators.
578
- */
579
- clientId?: string;
580
- context?: Record<string, unknown>;
581
- id: string;
582
- type: "connect";
583
- }
584
- /**
585
- * Subscribe to a declarative **shape** — server-side partial replication scoped
586
- * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
587
- * + validated `args`; the server resolves the trusted `where` (identity/RLS
588
- * `baseWhere` the client can't forge) and streams the matching rowset, then live
589
- * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
590
- * echoed as `shapeId` on every poke part.
591
- */
592
- interface ClientShapeSubscribeMessage {
593
- id: string;
594
- shape: {
595
- args?: Record<string, unknown>;
596
- name: string;
597
- };
598
- /**
599
- * Resume from this checkpoint (the `__cdc_log` cursor the client last
600
- * applied for this shape). When absent or below the server's retained floor
601
- * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
602
- * delta.
603
- */
604
- sinceCheckpoint?: number;
605
- /**
606
- * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
607
- * to. A mismatch (forked changelog timeline) forces a full re-seed even when
608
- * the cursor is numerically in range.
609
- */
610
- sinceEpoch?: string;
611
- type: "shape_subscribe";
612
- }
613
- /** Cancel a shape subscription started with the same `id`. */
614
- interface ClientShapeUnsubscribeMessage {
615
- id: string;
616
- type: "shape_unsubscribe";
617
- }
618
- interface ClientAckMessage {
619
- id: string;
620
- type: "ack";
621
- }
622
- /**
623
- * Start a streaming query. The id namespaces a fresh stream and is echoed on
624
- * every {@link ServerChunkMessage} the server pushes back. Cancel a running
625
- * stream by sending a {@link ClientUnsubscribeMessage} with the same id —
626
- * subscription and stream id-spaces share the cancel channel; the prefix
627
- * (`sub_*` vs `stream_*`) keeps the local registries searchable.
628
- */
629
- interface ClientStreamMessage {
630
- id: string;
631
- query: {
632
- args?: Record<string, unknown>;
633
- functionPath: string;
634
- shardKey?: string;
635
- };
636
- type: "stream";
637
- }
638
- /**
639
- * Join or leave a whisper `topic` — an app-chosen ephemeral channel scoped to a
640
- * shard. While joined, the client receives every {@link ServerWhisperMessage}
641
- * other members broadcast to the topic.
642
- */
643
- interface ClientWhisperSubscribeMessage {
644
- topic: string;
645
- type: "whisper_subscribe" | "whisper_unsubscribe";
646
- }
647
- /**
648
- * Broadcast ephemeral `data` to the topic's other members on the shard. The
649
- * payload is relayed verbatim with no server-side persistence (no SQLite/CDC
650
- * write) — for typing indicators, live cursors, presence pings. The sender does
651
- * not receive its own whisper.
652
- */
653
- interface ClientWhisperMessage {
654
- data?: unknown;
655
- topic: string;
656
- type: "whisper";
657
- }
658
- type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
659
- /** Subscription protocol — server → client. */
660
- interface ServerDataMessage {
661
- /**
662
- * The `__cdc_log` high-watermark covered by this frame (Pillar 1b). The
663
- * client persists it as the query's `serverCursor` and replays it as
664
- * `sinceSeq` on the next reconnect. Absent on shards that never enabled CDC.
665
- */
666
- cursor?: number;
667
- data?: unknown;
668
- delta?: unknown;
669
- /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
670
- epoch?: string;
671
- id: string;
672
- /**
673
- * The highest custom-mutator `mutationId` from this client the server has
674
- * now applied (the per-client `__client_watermark`). Echoed so the client's
675
- * outbox can drop confirmed pending mutations and let TanStack DB collapse
676
- * the matching optimistic overlay. Absent on shards without custom mutators.
677
- */
678
- lastMutationId?: number;
679
- type: "data" | "delta";
680
- }
681
- /**
682
- * Lightweight resume acknowledgement (Pillar 1b): the server determined that
683
- * nothing the subscription reads changed since the client's `sinceSeq`, so it
684
- * skips re-sending the snapshot. The client keeps its cached value and only
685
- * advances `serverCursor` to `cursor`.
686
- */
687
- interface ServerResumeMessage {
688
- cursor?: number;
689
- /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
690
- epoch?: string;
691
- id: string;
692
- /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
693
- lastMutationId?: number;
694
- type: "resume";
695
- }
696
- /**
697
- * Settled acknowledgement for a **list** subscription: a write touched one of
698
- * the subscription's read tables but produced a byte-identical result, so the
699
- * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
700
- * client (one that announced a `clientId`, hence has a server-side
701
- * `__client_watermark`) so its optimistic list overlay drops even when no data
702
- * frame arrives. Plain `useQuery` subscribers never receive it, and an older
703
- * client safely ignores the unknown frame.
704
- */
705
- interface ServerSettledMessage {
706
- cursor?: number;
707
- /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
708
- epoch?: string;
709
- id: string;
710
- /**
711
- * The highest custom-mutator `mutationId` from this client the server has
712
- * now applied (the per-client `__client_watermark`). Forwarded to a
713
- * collection's `onCheckpoint` so it can drop the overlay for the confirmed
714
- * write whose result didn't change this list.
715
- */
716
- lastMutationId?: number;
717
- type: "settled";
718
- }
719
- interface ServerErrorMessage {
720
- error?: unknown;
721
- id?: string;
722
- message?: string;
723
- type: "error";
724
- }
725
- interface ServerAckMessage {
726
- id: string;
727
- type: "ack";
728
- }
729
- interface ServerCompleteMessage {
730
- id: string;
731
- type: "complete";
732
- }
733
- /** One frame of a streaming query — `data` carries the user-yielded chunk. */
734
- interface ServerChunkMessage {
735
- data: unknown;
736
- id: string;
737
- type: "chunk";
738
- }
739
- /**
740
- * An ephemeral whisper relayed from another member of `topic` on the same shard
741
- * (AnyCable-style whispering). `data` is the sender's payload verbatim; `from`
742
- * is the sender's verified user id when known (absent for an anonymous sender).
743
- * Never persisted server-side.
744
- */
745
- interface ServerWhisperMessage {
746
- data: unknown;
747
- from?: string;
748
- topic: string;
749
- type: "whisper";
750
- }
751
- /**
752
- * One row-level change in a shape's replication stream — the wire form of the
753
- * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
754
- * `value` (projected to the shape's `columns`); `delete` omits it, identifying
755
- * the removed row by `key` alone. The client applies these to its local
756
- * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
757
- * never had in this shape).
758
- */
759
- interface RowOp {
760
- /** Row primary key (`_id`). */
761
- key: string;
762
- op: "delete" | "insert" | "update";
763
- /** Logical table the row belongs to. */
764
- table: string;
765
- /** Post-image document for insert/update; absent on delete. */
766
- value?: Record<string, unknown>;
767
- }
768
- /**
769
- * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
770
- * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
771
- * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
772
- * buffers every part and applies them in a single transaction at `pokeEnd`, so a
773
- * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
774
- */
775
- interface ServerPokeStartMessage {
776
- /** The checkpoint the client's view is expected to be at before this poke applies (for ordering/gap detection). */
777
- baseCheckpoint?: number;
778
- /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
779
- epoch?: string;
780
- /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
781
- pokeId: string;
782
- type: "pokeStart";
783
- }
784
- /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
785
- interface ServerPokePartMessage {
786
- /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
787
- lastMutationId?: number;
788
- pokeId: string;
789
- /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
790
- rowsPatch: RowOp[];
791
- /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
792
- shapeId: string;
793
- type: "pokePart";
794
- }
795
- /**
796
- * Closes a poke: the client commits the buffered parts atomically and advances
797
- * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
798
- * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
799
- * the next reconnect.
800
- */
801
- interface ServerPokeEndMessage {
802
- /** The `__cdc_log` cursor the view is at after applying this poke. */
803
- checkpoint?: number;
804
- /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
805
- epoch?: string;
806
- pokeId: string;
807
- type: "pokeEnd";
808
- }
809
- type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
810
- /**
811
- * The authenticated user as exposed client-side, mirroring better-auth's
812
- * `user` row (the `user` field of the `get-session` response). Kept minimal
813
- * and structural — only `id` is guaranteed; the rest are the common better-auth
814
- * fields, and the index signature carries any plugin-contributed extras.
815
- */
816
- interface User {
817
- readonly createdAt?: NullableTimestamp;
818
- readonly email?: null | string;
819
- readonly emailVerified?: boolean | null;
820
- readonly id: string;
821
- readonly image?: null | string;
822
- readonly name?: null | string;
823
- readonly [key: string]: unknown;
824
- readonly updatedAt?: NullableTimestamp;
825
- }
826
- /**
827
- * One pending scheduled function, as returned by the worker's
828
- * `GET /_lunora/admin/scheduled` endpoint. Mirrors `@lunora/scheduler`'s
829
- * `ScheduleRecord` structurally so the client carries no dependency on it.
830
- */
831
- interface ScheduleRecord {
832
- args: Record<string, unknown>;
833
- /**
834
- * Dispatch attempts already made. Absent (treated as 0) until the first
835
- * failure; on a dead-letter record it is the exhausted count (> the retry
836
- * budget). Surfaced so the studio can show how hard a job tried before it
837
- * was parked.
838
- */
839
- attempts?: number;
840
- enqueuedAt: number;
841
- functionPath: string;
842
- id: string;
843
- /** Logical workpool the job is routed to (concurrency-gated), when any. */
844
- pool?: string;
845
- scheduledFor: number;
846
- shardKey?: string;
847
- }
848
- /**
849
- * One workpool's live backlog, as returned by the worker's
850
- * `GET /_lunora/admin/scheduled/status` endpoint. Mirrors `@lunora/scheduler`'s
851
- * `SchedulerPoolStatus` structurally so the client carries no dependency on it.
852
- */
853
- interface SchedulerPoolStatus {
854
- /** Jobs currently dispatched-but-not-yet-completed (the held concurrency slots). */
855
- inFlight: number;
856
- /** The pool's concurrency cap. */
857
- maxConcurrency: number;
858
- /** The logical workpool name. */
859
- name: string;
860
- /** Pending jobs routed to this pool but not yet dispatched. */
861
- queued: number;
862
- }
863
- /**
864
- * The app-level scheduler backlog, as returned by the worker's
865
- * `GET /_lunora/admin/scheduled/status` endpoint. `pools` is the per-pool
866
- * breakdown; `backlog` and `inFlight` are the app-wide sums of `queued` and
867
- * `inFlight` across every pool — the headline numbers for the studio SLO
868
- * view. Mirrors `@lunora/scheduler`'s `SchedulerStatus` structurally.
869
- */
870
- interface SchedulerStatus {
871
- /** Sum of every pool's `queued` count — the total pending backlog. */
872
- backlog: number;
873
- /** Sum of every pool's `inFlight` count — the total held concurrency slots. */
874
- inFlight: number;
875
- /** Per-pool backlog breakdown. */
876
- pools: SchedulerPoolStatus[];
877
- }
878
- /**
879
- * One shard's request volume, as returned by the worker's
880
- * `POST /_lunora/admin/shard-traffic` endpoint. The cross-shard traffic feed
881
- * the studio's `hot_shard` advisor lint consumes: `requests` is the shard's
882
- * lifetime dispatch total, `shardKey` the DO id name (`""` for the root shard).
883
- */
884
- interface ShardTrafficEntry {
885
- requests: number;
886
- shardKey: string;
887
- }
888
- /**
889
- * The whole-shard-set traffic distribution returned by the worker's
890
- * `POST /_lunora/admin/shard-traffic` endpoint. `shards` is one entry per live
891
- * shard (a failed shard surfaces with `requests: 0`); `ok`/`failed` count the
892
- * shards that returned vs. errored. Shaped to feed the advisor's `hot_shard`
893
- * lint after the studio tags each entry with its sharded function `group`.
894
- */
895
- interface ShardTrafficResult {
896
- failed: number;
897
- ok: number;
898
- shards: ShardTrafficEntry[];
899
- }
900
- /**
901
- * One object in the storage bucket, as returned by the worker's
902
- * `GET /_lunora/admin/storage` endpoint. Mirrors `@lunora/storage`'s
903
- * `R2ObjectLike` structurally.
904
- */
905
- interface StorageObject {
906
- customMetadata?: Record<string, string>;
907
- etag: string;
908
- httpMetadata?: {
909
- contentType?: string;
910
- };
911
- key: string;
912
- size: number;
913
- /**
914
- * When the object was stored. R2 emits a `Date`, which JSON-serializes to an
915
- * ISO string over the wire; a mock may supply epoch ms — so consumers should
916
- * normalise via `new Date(uploaded)`. Absent if the backend didn't report it.
917
- */
918
- uploaded?: number | string;
919
- }
920
- /** One page of {@link StorageObject}s plus the cursor to fetch the next, if any. */
921
- interface StorageListPage {
922
- cursor?: string;
923
- objects: StorageObject[];
924
- }
925
- /**
926
- * One argument of a registered function, derived from its `v.*` validator by the
927
- * worker. A compact signature shape — enough to render a function's API without
928
- * the build-time codegen types.
929
- */
930
- interface FunctionArgumentDescriptor {
931
- /** Element validator kind for an `array` arg (one level), e.g. `string`. */
932
- element?: string;
933
- /** The (optional-unwrapped) validator kind, e.g. `string`, `id`, `object`. */
934
- kind: string;
935
- /** The argument name. */
936
- name: string;
937
- /** True when the arg is wrapped in `v.optional(...)`. */
938
- optional: boolean;
939
- /** Target table for an `id` arg (`v.id("table")`). */
940
- table?: string;
941
- }
942
- /**
943
- * One registered function, as returned by the worker's
944
- * `GET /_lunora/admin/functions` endpoint: its `&lt;file>:&lt;function>` path, which
945
- * client method (`query` / `mutation` / `action`) invokes it, and its argument
946
- * signature. `args` is absent on responses from an older worker.
947
- */
948
- interface FunctionDescriptor {
949
- args?: FunctionArgumentDescriptor[];
950
- kind: "action" | "mutation" | "query";
951
- path: string;
952
- }
953
- /** A `.global()` (D1-backed) table plus its row count, from `/_lunora/admin/global/tables`. */
954
- interface GlobalTableInfo {
955
- name: string;
956
- rowCount: number;
957
- }
958
- /** A window of rows from one global table, from `/_lunora/admin/global/table`. */
959
- interface GlobalTablePage {
960
- columns: string[];
961
- /** FK columns (local column → referenced table) for external tables with real `REFERENCES` constraints, from `PRAGMA foreign_key_list`. */
962
- refs?: Record<string, string>;
963
- rows: Record<string, unknown>[];
964
- total: number;
965
- }
966
- /**
967
- * One equality constraint a facet-value click adds to the global browser's view
968
- * (`column = value`). `value` is the raw stored scalar the facet returned, sent
969
- * as-is and bound server-side, so it never injects SQL.
970
- */
971
- interface GlobalFilterClause {
972
- column: string;
973
- value: unknown;
974
- }
975
- /** One distinct value of a faceted global column with its row count, from `/_lunora/admin/global/facet`. */
976
- interface GlobalFacetValue {
977
- count: number;
978
- value: unknown;
979
- }
980
- /** Per-column distinct-value summary for the global browser, from `/_lunora/admin/global/facet`. */
981
- interface GlobalFacetResult {
982
- truncated: boolean;
983
- values: GlobalFacetValue[];
984
- }
985
- /** A nullable timestamp field as better-auth serializes it: epoch-ms, ISO string, or null. */
986
- type NullableTimestamp = null | number | string;
987
- /** A workflow instance's lifecycle status. Mirrors Cloudflare's `InstanceStatus`. */
988
- type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
989
- /** The lifecycle mutations the status endpoint accepts. */
990
- type WorkflowInstanceAction = "pause" | "resume" | "terminate";
991
- /** One row of the workflow-instances list. */
992
- interface WorkflowInstanceSummary {
993
- createdOn?: string;
994
- endedOn?: string;
995
- id: string;
996
- startedOn?: string;
997
- status: WorkflowInstanceStatus;
998
- }
999
- /** One durable step of an instance's execution timeline. */
1000
- interface WorkflowStepDetail {
1001
- /** 1-based attempt count (`> 1` means the step retried). */
1002
- attempts?: number;
1003
- end?: string;
1004
- error?: unknown;
1005
- name: string;
1006
- output?: unknown;
1007
- start?: string;
1008
- success?: boolean;
1009
- /** `step` / `sleep` / `waitForEvent` / … (Cloudflare's step `type`). */
1010
- type?: string;
1011
- }
1012
- /** A workflow instance's full detail: summary plus params/output/error and the step timeline. */
1013
- interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
1014
- error?: unknown;
1015
- output?: unknown;
1016
- params?: unknown;
1017
- steps: WorkflowStepDetail[];
1018
- }
1019
- /** A page of workflow instances. */
1020
- interface WorkflowInstancePage {
1021
- /**
1022
- * Whether workflow inspection is configured on the worker (a Cloudflare
1023
- * account id + API token). `false` when the admin proxy reports it can't
1024
- * inspect instances; omitted (treated as configured) otherwise. Lets a
1025
- * caller render a "set credentials" state without a failed request.
1026
- */
1027
- configured?: boolean;
1028
- instances: WorkflowInstanceSummary[];
1029
- page: number;
1030
- perPage: number;
1031
- totalCount?: number;
1032
- }
1033
93
  type SubscriptionCallback = (data: unknown) => void;
1034
94
  /** A subscription-scoped error the server pushed for this subscription id. */
1035
95
  interface SubscriptionError {
@@ -1492,6 +552,24 @@ declare class LunoraClient {
1492
552
  * Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
1493
553
  */
1494
554
  private tabCoordinator;
555
+ /**
556
+ * The leader's last-broadcast aggregate {@link ConnectionStatus}, mirrored
557
+ * on a follower tab — which owns no `ShardConnection` of its own to
558
+ * compute a status from (see `computeStatus`). `undefined` until the
559
+ * leader's first broadcast (falls back to `"idle"`), and reset back to
560
+ * `undefined` whenever this tab stops being a follower of the CURRENT
561
+ * leader (becomes leader itself, or the leader changes), so a stale
562
+ * mirror from a previous leader never survives a leadership change.
563
+ */
564
+ private leaderStatus;
565
+ /**
566
+ * Sticky "has the mirrored leader status ever reported `connected`" flag —
567
+ * the follower's counterpart to {@link ShardConnection.wasEverConnected},
568
+ * since a follower has no `ShardConnection` of its own. Feeds the
569
+ * offline-queue gate (see `mutation`) exactly like the real per-shard flag
570
+ * does on the leader/single-tab path.
571
+ */
572
+ private leaderWasEverConnected;
1495
573
  /** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
1496
574
  private readonly connections;
1497
575
  /** Default `connect`-envelope context applied to a shard with no explicit override. */
@@ -1534,6 +612,17 @@ declare class LunoraClient {
1534
612
  * See `identityFingerprint` for the fingerprint shape.
1535
613
  */
1536
614
  private readonly queuedIdentities;
615
+ /**
616
+ * Distinct shard keys with a mutation currently sitting in `offlineQueue`
617
+ * — fresh writes queued this session (`enqueueOfflineMutation`) or writes
618
+ * restored from durable storage (`hydratePersistedQueue`). A follower tab
619
+ * has no per-shard `ShardConnection` to iterate when its mirrored leader
620
+ * status turns `"connected"` (see the `onConnectionStatus` coordinator
621
+ * option), so this is what that flush walks instead. Entries are never
622
+ * removed — flushing an already-empty shard is a cheap no-op, and the set
623
+ * is bounded by the app's own distinct shard-key cardinality.
624
+ */
625
+ private readonly queuedOfflineShardKeys;
1537
626
  private closed;
1538
627
  /** Subscribers to auth-token changes (see `onAuthTokenChange`). */
1539
628
  private readonly authTokenListeners;
@@ -2624,6 +1713,25 @@ declare class LunoraClient {
2624
1713
  signal?: AbortSignal;
2625
1714
  }): StreamIterable<HttpStreamChunkOf<Ref>>;
2626
1715
  close(): void;
1716
+ /**
1717
+ * Tear down one {@link ShardConnection}'s live state: clear its reconnect/
1718
+ * connect timers, stop its heartbeat, and close its socket (if any).
1719
+ * Shared by `close()` (terminal) and the cross-tab `onStopBeingLeader`
1720
+ * handler (demoted, but still alive) so a demoted leader can't leak a
1721
+ * pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
1722
+ * an inline `conn.socket?.close()` — which skips both — used to.
1723
+ */
1724
+ private teardownConnection;
1725
+ /**
1726
+ * Build (but do not start) this client's `TabCoordinator`. Extracted out of
1727
+ * the constructor so `setAuthToken` can rebuild it on an identity change —
1728
+ * the default channel name embeds the identity fingerprint (see below), so
1729
+ * a new identity needs a new coordinator on a new channel. The callback
1730
+ * bodies are the drift-sensitive region (a hand-merged identity guard on
1731
+ * the shard message listener sits ahead of an extracted `lastFrameAt`
1732
+ * stamp elsewhere in this file) — moved verbatim, not reflowed.
1733
+ */
1734
+ private createTabCoordinator;
2627
1735
  /**
2628
1736
  * Persist a mutation that can't go out on the wire right now (offline, or
2629
1737
  * mid-reconnect after a prior connect). The optimistic update has already
@@ -2717,6 +1825,19 @@ declare class LunoraClient {
2717
1825
  */
2718
1826
  private applyOptimisticUpdate;
2719
1827
  private getConnection;
1828
+ /**
1829
+ * The `(wsState, hasSocket, wasEverConnected)` triple `mutation()`'s
1830
+ * offline-queue gate reads. On the leader/single-tab path this is exactly
1831
+ * the real `ShardConnection`'s state (byte-identical to the pre-cross-tab
1832
+ * behavior). A follower has no `ShardConnection` of its own (see
1833
+ * `ensureSocket`), so it derives the same triple from the mirrored
1834
+ * `leaderStatus`/`leaderWasEverConnected` instead: `"connected"` maps to
1835
+ * `"open"` (queue-eligible once `wasEverConnected`), `"connecting"` stays
1836
+ * `"connecting"` (the mid-reconnect queue branch), anything else is
1837
+ * `"idle"`. `hasSocket` is always `false` for a follower — it never has
1838
+ * one.
1839
+ */
1840
+ private connectionGateState;
2720
1841
  private getOrCreateConnection;
2721
1842
  private wsUrlFor;
2722
1843
  /**
@@ -2778,15 +1899,46 @@ declare class LunoraClient {
2778
1899
  * a silent tokenless socket the admin gate would reject.
2779
1900
  */
2780
1901
  private openSocketWithProvidedToken;
1902
+ /**
1903
+ * Construct one WebSocket connection attempt and wire the shared
1904
+ * lifecycle guarantees around it — the fail-fast connect-timeout, the
1905
+ * identity guard that stops a superseded attempt's late `open`/`message`/
1906
+ * `close`/`error` from touching a connection a newer attempt already
1907
+ * owns, and (once open) the keepalive heartbeat with its half-open
1908
+ * watchdog (plan 217). One call opens ONE attempt; the caller owns
1909
+ * reconnect scheduling from `onClose` — mirrors the shard's existing
1910
+ * `ensureSocket` / `handleDisconnect` split, now shared with
1911
+ * `subscribeScheduledJobs` so it stops re-living the bug that split
1912
+ * already fixed once (CLIENT-05).
1913
+ *
1914
+ * The identity guard is `conn.socket !== socket`, re-checked before every
1915
+ * action below. `conn.socket` is reassigned to a new attempt's socket
1916
+ * synchronously — right here, before `open` ever fires — so an older
1917
+ * attempt's guard trips the instant it's superseded, even if its
1918
+ * underlying socket only fires its real `close`/`error` much later. This
1919
+ * ordering is load-bearing: preserve it exactly.
1920
+ */
1921
+ private openManagedSocket;
2781
1922
  /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
2782
1923
  private openSocket;
2783
1924
  private handleDisconnect;
2784
1925
  /**
2785
- * Begin the keepalive heartbeat on an open connection. Each tick sends a
2786
- * {@link WS_KEEPALIVE_PING} text frame the server answers from its
2787
- * hibernation auto-response without waking the DO. A no-op when the
2788
- * heartbeat is disabled (an interval of zero or less); idempotent — any
2789
- * existing timer is cleared first so a reconnect can't leak intervals.
1926
+ * Begin the keepalive heartbeat on an open connection attempt the only
1927
+ * caller is {@link openManagedSocket}'s own `open` handler, so both the
1928
+ * shard socket and `subscribeScheduledJobs` share this one implementation
1929
+ * instead of each hand-rolling their own (plan 217, generalized).
1930
+ *
1931
+ * Each tick first checks the half-open watchdog (see
1932
+ * {@link ManagedSocketState.lastFrameAt}): if no frame at all has arrived
1933
+ * within `heartbeatIntervalMs * 2.5`, the far end has gone quiet without
1934
+ * the socket ever firing `close` — force it closed and report it through
1935
+ * `onWatchdogTrip` (the caller's `onClose`) so the normal reconnect/backoff
1936
+ * takes over instead of every live query on it silently staling forever.
1937
+ * Otherwise it sends a {@link WS_KEEPALIVE_PING} text frame the server
1938
+ * answers from its hibernation auto-response without waking the DO. A
1939
+ * no-op when the heartbeat is disabled (an interval of zero or less);
1940
+ * idempotent — any existing timer is cleared first so a reconnect can't
1941
+ * leak intervals.
2790
1942
  */
2791
1943
  private startHeartbeat;
2792
1944
  /** Clear a connection's keepalive timer, if any. Safe to call repeatedly. */
@@ -2848,6 +2000,23 @@ declare class LunoraClient {
2848
2000
  private dispatchWhisper;
2849
2001
  /** Notify every {@link onTokenExpired} listener (best-effort, listener throws swallowed). */
2850
2002
  private notifyTokenExpired;
2003
+ /**
2004
+ * CLIENT-04: `type: "complete"` today is sent ONLY by `@lunora/do`'s
2005
+ * `handleStream` (see `shard-do.ts`), gated to the `stream` envelope type
2006
+ * and minting only `stream_*` ids — the `subscribe` path never sends it, so
2007
+ * a live SUBSCRIPTION provably never receives `complete` from the current
2008
+ * server. But `ServerCompleteMessage` is a generic `id`-keyed frame and
2009
+ * `ShardDO` is user-subclassable, so this stays defensive rather than
2010
+ * assuming a `sub_*` id can never reach here: unlike the historical
2011
+ * `subscriptions.remove(state)`, which dropped the state out of
2012
+ * `subscriptions.all()` — the set the reconnect resubscribe loop walks
2013
+ * (`ensureSocket`'s `open` handler) — and so froze the query forever across
2014
+ * every future reconnect, this fans a cancellation error to any listener
2015
+ * and marks the registration un-acked instead. Non-destructive: the state
2016
+ * stays in the registry, so the very next reconnect resubscribes it. The
2017
+ * two id-spaces don't overlap (`sub_*` vs `stream_*`), so the stream and
2018
+ * subscription lookups below are mutually exclusive.
2019
+ */
2851
2020
  private handleCompleteMessage;
2852
2021
  private unpersist;
2853
2022
  /**
@@ -2910,6 +2079,18 @@ declare class LunoraClient {
2910
2079
  * the durable `clear()` is best-effort.
2911
2080
  */
2912
2081
  private clearQueryCacheForIdentityChange;
2082
+ /**
2083
+ * Flush every shard with a mutation currently queued in `offlineQueue`
2084
+ * (see `queuedOfflineShardKeys`). Used on a FOLLOWER tab when the
2085
+ * mirrored leader status transitions to `"connected"` — a follower has no
2086
+ * per-shard `ShardConnection` reconnect event to hang the usual
2087
+ * single-shard `flushOfflineQueue(shardKey)` call off of (see the
2088
+ * `handleConnect` call site), so this walks every shard that might have
2089
+ * something queued instead. Flushing an already-empty shard is a cheap
2090
+ * no-op (`flushOfflineQueue` returns immediately once `drain` yields
2091
+ * nothing), so over-inclusion here is harmless.
2092
+ */
2093
+ private flushAllOfflineQueues;
2913
2094
  private flushOfflineQueue;
2914
2095
  /**
2915
2096
  * Partition already-gated writes into the encodable ones (returned) and reject
@@ -2984,4 +2165,4 @@ declare class LunoraClient {
2984
2165
  */
2985
2166
  private settleReplayBatchSlots;
2986
2167
  }
2987
- export { SchedulerStatus as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, LunoraErrorCode as E, FunctionReference as F, GlobalFacetResult as G, HttpStreamRef as H, MutationSettledEvent as I, OptimisticLocalStore as J, OptimisticUpdate as K, LunoraClient as L, MutationCallOptions as M, OutboxMutation as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, OutboxSink as T, User as U, PersistedMutation as V, RowOp as W, RpcEnvelope as X, RpcResponseBody as Y, ScheduleRecord as Z, SchedulerPoolStatus as _, Unsubscribe as a, ServerMessage as a0, ServerPokeEndMessage as a1, ServerPokePartMessage as a2, ServerPokeStartMessage as a3, ShardTrafficEntry as a4, ShardTrafficResult as a5, StorageListPage as a6, StorageObject as a7, StreamHandle as a8, SubscriptionCallback as a9, SubscriptionRegistry as aa, SubscriptionState as ab, SyncWatermark as ac, WorkflowInstanceAction as ad, WorkflowInstanceDetail as ae, WorkflowInstancePage as af, WorkflowInstanceStatus as ag, WorkflowInstanceSummary as ah, WorkflowStepDetail as ai, WsTokenProvider as aj, createClientQuery as ak, createLocalStore as al, createStream as am, getErrorCode as an, getRetryAfterMs as ao, isConflictError as ap, isForbiddenError as aq, isRateLimitedError as ar, isUnauthorizedError as as, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, ReconnectOptions as g, BatchSlot as h, CachedQuery as i, ClientDebugShard as j, ClientDebugSnapshot as k, ClientDebugSubscription as l, ClientMessage as m, ClientQueryRef as n, ClientShapeSubscribeMessage as o, ClientShapeUnsubscribeMessage as p, ConnectionStatus as q, FunctionArgumentDescriptor as r, FunctionDescriptor as s, GlobalFacetValue as t, GlobalFilterClause as u, GlobalTableInfo as v, GlobalTablePage as w, HttpStreamCallArgs as x, LunoraClientError as y, LunoraClientOptions as z };
2168
+ export { 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 };