@lunora/client 1.0.0-alpha.2 → 1.0.0-alpha.21

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/LICENSE.md +6 -0
  2. package/README.md +2 -0
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/auth/index.d.mts +1 -1
  5. package/dist/auth/index.d.ts +1 -1
  6. package/dist/index.d.mts +144 -24
  7. package/dist/index.d.ts +144 -24
  8. package/dist/index.mjs +10 -9
  9. package/dist/packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs +33 -0
  10. package/dist/packem_shared/{DEFAULT_MAX_BUFFER-BDkqO5PW.mjs → DEFAULT_MAX_BUFFER-7hFnzNk9.mjs} +6 -3
  11. package/dist/packem_shared/{LunoraClient-UiULzH_1.mjs → LunoraClient-kXpHNyaE.mjs} +1562 -267
  12. package/dist/packem_shared/OfflineQueue-GGYJRmhF.mjs +1 -0
  13. package/dist/packem_shared/SubscriptionRegistry-DjGKZsqq.mjs +1 -0
  14. package/dist/packem_shared/{applyDelta-4jFGTPA3.mjs → applyDelta-CRKZ1PBt.mjs} +21 -1
  15. package/dist/packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs +79 -0
  16. package/dist/packem_shared/{createInMemoryQueryCache-B1PQ9Twl.mjs → createInMemoryQueryCache-DiaGZkA2.mjs} +25 -51
  17. package/dist/packem_shared/createLocalStore-jRoqmazl.mjs +2 -0
  18. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +31 -0
  19. package/dist/packem_shared/{createServerClient-BjZc3gD8.mjs → createServerClient-DF-3mLmb.mjs} +1 -1
  20. package/dist/packem_shared/idb-utility-DrSVX43Q.mjs +48 -0
  21. package/dist/packem_shared/local-store-BveBeFEo.mjs +106 -0
  22. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.mts → lunora-client.d-BYkEjCEJ.d.mts} +1001 -58
  23. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.ts → lunora-client.d-BYkEjCEJ.d.ts} +1001 -58
  24. package/dist/packem_shared/{OfflineQueue-D5p_QgF_.mjs → offline-queue-B9vfdSqp.mjs} +49 -6
  25. package/dist/packem_shared/{preload.d-BoDmFqSG.d.ts → preload.d-B-vyHnml.d.ts} +1 -1
  26. package/dist/packem_shared/{preload.d-dSaRMuhL.d.mts → preload.d-DrfuisCE.d.mts} +1 -1
  27. package/dist/packem_shared/subscription-BjynOXCU.mjs +68 -0
  28. package/dist/query/index.d.mts +2 -2
  29. package/dist/query/index.d.ts +2 -2
  30. package/dist/ssr/index.d.mts +3 -3
  31. package/dist/ssr/index.d.ts +3 -3
  32. package/dist/ssr/index.mjs +1 -1
  33. package/package.json +5 -2
  34. package/dist/packem_shared/CONFLICT_ERROR_CODE-aUdVbEDw.mjs +0 -4
  35. package/dist/packem_shared/SubscriptionRegistry-B-Qx_Gux.mjs +0 -26
  36. package/dist/packem_shared/createInMemoryPersistence-CW82inU5.mjs +0 -105
  37. package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
@@ -1,4 +1,59 @@
1
- import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthSession } from '@lunora/runtime';
1
+ import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
2
+ /**
3
+ * The machine-readable error codes a client can observe on a failed
4
+ * RPC/batch/subscription. Mirrors the server's `CODE_STATUS` keys
5
+ * (`@lunora/server`'s `error.ts`) by hand — the client is framework-neutral and
6
+ * must never import the server package (wrong dependency direction / would pull
7
+ * the server into the browser bundle). Keep this list in sync when a server code
8
+ * is added or removed (see the drift-guard note in the plan/maintenance docs).
9
+ */
10
+ 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"];
11
+ /** A machine-readable error `code` the client may observe. Mirror of the server's `LunoraErrorCode`. */
12
+ type LunoraErrorCode = (typeof LUNORA_ERROR_CODES)[number];
13
+ /** Error code the server uses for optimistic-concurrency conflicts (HTTP 409). */
14
+ declare const CONFLICT_ERROR_CODE = "CONFLICT";
15
+ /**
16
+ * Whether an unknown rejection is an optimistic-concurrency conflict — the
17
+ * server lost a write race and the caller should refetch and retry (or surface
18
+ * the conflict). Structural check on the `code` property the client attaches
19
+ * when decoding the worker's `{ error: { code, message } }` envelope.
20
+ */
21
+ declare const isConflictError: (error: unknown) => error is Error & {
22
+ code: "CONFLICT";
23
+ };
24
+ /**
25
+ * Whether a rejection is an RLS/policy denial (`FORBIDDEN`, HTTP 403) — the
26
+ * caller is authenticated but not permitted to read/write the row. The most
27
+ * common per-call error a UI must handle in an RLS-first app.
28
+ */
29
+ declare const isForbiddenError: (error: unknown) => error is Error & {
30
+ code: "FORBIDDEN";
31
+ };
32
+ /** Whether a rejection is an authentication failure (`UNAUTHORIZED`, HTTP 401) — no/invalid identity. */
33
+ declare const isUnauthorizedError: (error: unknown) => error is Error & {
34
+ code: "UNAUTHORIZED";
35
+ };
36
+ /**
37
+ * Whether a rejection is a rate-limit denial (`TOO_MANY_REQUESTS`, HTTP 429).
38
+ * The retry hint (if the server sent one) is read with {@link getRetryAfterMs}.
39
+ */
40
+ declare const isRateLimitedError: (error: unknown) => error is Error & {
41
+ code: "TOO_MANY_REQUESTS";
42
+ };
43
+ /**
44
+ * Read the server's machine-readable `code` off a rejection, narrowed to the
45
+ * known {@link LunoraErrorCode} union. Returns `undefined` for a non-`Error`, a
46
+ * missing code, or an unrecognized code string (forward-compat server codes read
47
+ * as `undefined` here rather than being falsely narrowed).
48
+ */
49
+ declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
50
+ /**
51
+ * Read the rate-limit retry hint (`data.retryAfterMs`) off a
52
+ * `TOO_MANY_REQUESTS` rejection without hand-casting the `unknown` `data`
53
+ * payload. Returns the finite millisecond value the server sent, or `undefined`
54
+ * when absent/non-numeric. Pair with {@link isRateLimitedError}.
55
+ */
56
+ declare const getRetryAfterMs: (error: unknown) => number | undefined;
2
57
  /** The registered function kinds a {@link FunctionReference} can describe. `stream` is a query that yields multiple frames over the WS. */
3
58
  type FunctionKind = "action" | "mutation" | "query" | "stream";
4
59
  /**
@@ -98,6 +153,14 @@ interface PersistedMutation {
98
153
  */
99
154
  identity?: string | null;
100
155
  shardKey?: string;
156
+ /**
157
+ * App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
158
+ * On hydrate, a record whose `version` doesn't match the current one is dropped
159
+ * and purged rather than replayed — so a write persisted by an older deploy
160
+ * (with a now-changed function signature) can't replay against the new schema.
161
+ * Absent when no `persistenceVersion` is configured (no version gating).
162
+ */
163
+ version?: string;
101
164
  }
102
165
  /**
103
166
  * Durable store for the offline mutation queue. The default client keeps the
@@ -120,6 +183,40 @@ interface PersistenceAdapter {
120
183
  remove: (id: string) => Promise<void>;
121
184
  }
122
185
  /**
186
+ * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
187
+ * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
188
+ * the durable outbox needs to dedupe and watermark replays.
189
+ */
190
+ interface OutboxMutation {
191
+ args: Record<string, unknown>;
192
+ /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
193
+ clientId: string;
194
+ functionPath: string;
195
+ /** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
196
+ idempotencyKey: string;
197
+ /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
198
+ identity: string | null;
199
+ /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
200
+ mutationId: number;
201
+ shardKey?: string;
202
+ }
203
+ /**
204
+ * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
205
+ * the client delegates offline write durability + at-least-once replay to this
206
+ * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
207
+ * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
208
+ * backed by the TanStack `OfflineExecutor`); the interface itself is
209
+ * dependency-free so `@lunora/client` stays TanStack-free.
210
+ */
211
+ interface OutboxSink {
212
+ /**
213
+ * Persist and schedule a write for replay. Rejects with an
214
+ * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
215
+ * the caller can surface back-pressure to the issuing mutation.
216
+ */
217
+ enqueue: (mutation: OutboxMutation) => Promise<void>;
218
+ }
219
+ /**
123
220
  * One persisted query result in the durable read cache (Pillar 2). Keyed in the
124
221
  * store by `shardKey + functionPath + argsKey`; the record carries everything
125
222
  * needed to render offline on reload and to resume the live subscription.
@@ -148,6 +245,13 @@ interface CachedQuery {
148
245
  ts: number;
149
246
  /** The full query result last seen from the server. */
150
247
  value: unknown;
248
+ /**
249
+ * App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
250
+ * A cached value whose `version` doesn't match the current one is not hydrated —
251
+ * so a result of a now-changed shape from an older deploy can't render. Absent
252
+ * when no `persistenceVersion` is configured (no version gating).
253
+ */
254
+ version?: string;
151
255
  }
152
256
  /**
153
257
  * Durable store for the client read cache (Pillar 2): query results survive a
@@ -177,6 +281,16 @@ interface LunoraClientOptions {
177
281
  authBasePath?: string;
178
282
  bookmarkStorage?: BookmarkStorage;
179
283
  /**
284
+ * Stable per-client id backing the custom-mutator watermark. Sent on the
285
+ * `connect` envelope (so the server can scope this client's
286
+ * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
287
+ * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
288
+ * monotonic mutation id to form the idempotency key. The `@lunora/db` path
289
+ * persists a stable id alongside the outbox and passes it here; omit for the
290
+ * standalone client, which generates an ephemeral per-session id.
291
+ */
292
+ clientId?: string;
293
+ /**
180
294
  * Default app context sent in the `connect` envelope right after each socket
181
295
  * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
182
296
  * as `event.context`. A per-shard context registered via
@@ -184,6 +298,16 @@ interface LunoraClientOptions {
184
298
  * hook needs connection context.
185
299
  */
186
300
  connectionContext?: Record<string, unknown>;
301
+ /**
302
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
303
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
304
+ * worker that never upgrades — the client force-closes the socket and routes
305
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
306
+ * of leaving the live channel silently stuck on the browser's much longer
307
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
308
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
309
+ */
310
+ connectTimeoutMs?: number;
187
311
  fetch?: typeof fetch;
188
312
  /**
189
313
  * Interval (ms) between keepalive pings sent on each open subscription
@@ -194,14 +318,47 @@ interface LunoraClientOptions {
194
318
  */
195
319
  heartbeatIntervalMs?: number;
196
320
  offlineQueue?: OfflineQueueOptions;
197
- /** Durable store for the offline mutation queue; omit to keep it in memory. */
198
- persistence?: PersistenceAdapter;
199
321
  /**
200
- * Durable store for the read cache (Pillar 2). When supplied, query results
322
+ * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
323
+ * path wires `createExecutorOutboxSink`), offline mutations are delegated to
324
+ * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
325
+ * is bypassed, so a db app has exactly one durable write path. Omit for the
326
+ * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
327
+ */
328
+ outbox?: OutboxSink;
329
+ /**
330
+ * Durable store for the offline mutation queue. Tri-state — an explicit
331
+ * {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
332
+ * in memory, lost on reload); omitted (the default) auto-probes a durable
333
+ * IndexedDB store when the `indexedDB` global is present (browsers), otherwise
334
+ * in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
335
+ * environments that can persist do. Pass `createAsyncStoragePersistence()` on
336
+ * React Native.
337
+ */
338
+ persistence?: false | PersistenceAdapter;
339
+ /**
340
+ * App/schema version stamped onto every persisted queued write and cached
341
+ * read. Bump it on a breaking change to a function signature or query shape:
342
+ * on the next boot, persisted writes / cached reads stamped with a different
343
+ * version are dropped (and purged) rather than replayed / hydrated against the
344
+ * new schema. Omit to disable version gating (records are never invalidated by
345
+ * version).
346
+ *
347
+ * **Adoption is itself an invalidation event:** records written before you set
348
+ * `persistenceVersion` carry no version, so the first boot after enabling it
349
+ * purges all currently-queued offline writes (and cached reads) as stale. Adopt
350
+ * it on a build where that clean slate is acceptable — typically the same
351
+ * breaking deploy you're protecting against — not purely speculatively.
352
+ */
353
+ persistenceVersion?: string;
354
+ /**
355
+ * Durable store for the read cache (Pillar 2). When active, query results
201
356
  * are persisted as their subscriptions advance and hydrated on construction
202
357
  * so a reload renders cached data before the socket reconnects, then resumes
203
- * the live subscription from the persisted cursor. Omit (or pass `false`) to
204
- * keep reads in memory only the default, unchanged behaviour.
358
+ * the live subscription from the persisted cursor. Tri-state an explicit
359
+ * {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
360
+ * memory only); omitted (the default) auto-probes IndexedDB exactly like
361
+ * {@link LunoraClientOptions.persistence}.
205
362
  */
206
363
  queryCache?: QueryCacheAdapter | false;
207
364
  reconnect?: ReconnectOptions;
@@ -222,17 +379,47 @@ interface LunoraClientOptions {
222
379
  /** Wire envelope sent on `POST /_lunora/rpc`. */
223
380
  interface RpcEnvelope {
224
381
  args?: Record<string, unknown>;
382
+ /**
383
+ * Stable per-client identifier (custom-mutator push path). Pairs with
384
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
385
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
386
+ */
387
+ clientId?: string;
225
388
  functionPath: string;
389
+ /**
390
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
391
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
392
+ * `client.mutation` calls.
393
+ */
394
+ idempotencyKey?: string;
395
+ /**
396
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
397
+ * server-side per-client watermark: `id &lt;= watermark` is a replay (skipped),
398
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
399
+ * batch so the client resends from `watermark + 1`. Absent on plain
400
+ * `client.mutation` calls.
401
+ */
402
+ mutationId?: number;
226
403
  shardKey?: string;
227
404
  }
228
- /** Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). */
405
+ /**
406
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
407
+ * watermarked custom-mutator push additionally carries `lastMutationId` — the
408
+ * highest per-client sequence the DO has applied — which the client uses to keep
409
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
410
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
411
+ * committed at — which gates the drop of a per-call optimistic layer.
412
+ */
229
413
  type RpcResponseBody = {
230
- result: unknown;
231
- } | {
232
414
  error: {
233
415
  code: string;
416
+ data?: unknown;
234
417
  message: string;
235
418
  };
419
+ } | {
420
+ commitCursor?: number;
421
+ lastMutationId?: number;
422
+ result: unknown;
236
423
  };
237
424
  /** Subscription protocol — client → server. */
238
425
  interface ClientSubscribeMessage {
@@ -265,10 +452,51 @@ interface ClientUnsubscribeMessage {
265
452
  * `onDisconnect` when the socket drops.
266
453
  */
267
454
  interface ClientConnectMessage {
455
+ /**
456
+ * Stable per-client id (persisted alongside the outbox). Lets the server
457
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
458
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
459
+ * use custom mutators.
460
+ */
461
+ clientId?: string;
268
462
  context?: Record<string, unknown>;
269
463
  id: string;
270
464
  type: "connect";
271
465
  }
466
+ /**
467
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
468
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
469
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
470
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
471
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
472
+ * echoed as `shapeId` on every poke part.
473
+ */
474
+ interface ClientShapeSubscribeMessage {
475
+ id: string;
476
+ shape: {
477
+ args?: Record<string, unknown>;
478
+ name: string;
479
+ };
480
+ /**
481
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
482
+ * applied for this shape). When absent or below the server's retained floor
483
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
484
+ * delta.
485
+ */
486
+ sinceCheckpoint?: number;
487
+ /**
488
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
489
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
490
+ * the cursor is numerically in range.
491
+ */
492
+ sinceEpoch?: string;
493
+ type: "shape_subscribe";
494
+ }
495
+ /** Cancel a shape subscription started with the same `id`. */
496
+ interface ClientShapeUnsubscribeMessage {
497
+ id: string;
498
+ type: "shape_unsubscribe";
499
+ }
272
500
  interface ClientAckMessage {
273
501
  id: string;
274
502
  type: "ack";
@@ -309,7 +537,7 @@ interface ClientWhisperMessage {
309
537
  topic: string;
310
538
  type: "whisper";
311
539
  }
312
- type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
540
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
313
541
  /** Subscription protocol — server → client. */
314
542
  interface ServerDataMessage {
315
543
  /**
@@ -323,6 +551,13 @@ interface ServerDataMessage {
323
551
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
324
552
  epoch?: string;
325
553
  id: string;
554
+ /**
555
+ * The highest custom-mutator `mutationId` from this client the server has
556
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
557
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
558
+ * the matching optimistic overlay. Absent on shards without custom mutators.
559
+ */
560
+ lastMutationId?: number;
326
561
  type: "data" | "delta";
327
562
  }
328
563
  /**
@@ -336,8 +571,33 @@ interface ServerResumeMessage {
336
571
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
337
572
  epoch?: string;
338
573
  id: string;
574
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
575
+ lastMutationId?: number;
339
576
  type: "resume";
340
577
  }
578
+ /**
579
+ * Settled acknowledgement for a **list** subscription: a write touched one of
580
+ * the subscription's read tables but produced a byte-identical result, so the
581
+ * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
582
+ * client (one that announced a `clientId`, hence has a server-side
583
+ * `__client_watermark`) so its optimistic list overlay drops even when no data
584
+ * frame arrives. Plain `useQuery` subscribers never receive it, and an older
585
+ * client safely ignores the unknown frame.
586
+ */
587
+ interface ServerSettledMessage {
588
+ cursor?: number;
589
+ /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
590
+ epoch?: string;
591
+ id: string;
592
+ /**
593
+ * The highest custom-mutator `mutationId` from this client the server has
594
+ * now applied (the per-client `__client_watermark`). Forwarded to a
595
+ * collection's `onCheckpoint` so it can drop the overlay for the confirmed
596
+ * write whose result didn't change this list.
597
+ */
598
+ lastMutationId?: number;
599
+ type: "settled";
600
+ }
341
601
  interface ServerErrorMessage {
342
602
  error?: unknown;
343
603
  id?: string;
@@ -370,7 +630,65 @@ interface ServerWhisperMessage {
370
630
  topic: string;
371
631
  type: "whisper";
372
632
  }
373
- type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerResumeMessage | ServerWhisperMessage;
633
+ /**
634
+ * One row-level change in a shape's replication stream — the wire form of the
635
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
636
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
637
+ * the removed row by `key` alone. The client applies these to its local
638
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
639
+ * never had in this shape).
640
+ */
641
+ interface RowOp {
642
+ /** Row primary key (`_id`). */
643
+ key: string;
644
+ op: "delete" | "insert" | "update";
645
+ /** Logical table the row belongs to. */
646
+ table: string;
647
+ /** Post-image document for insert/update; absent on delete. */
648
+ value?: Record<string, unknown>;
649
+ }
650
+ /**
651
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
652
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
653
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
654
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
655
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
656
+ */
657
+ interface ServerPokeStartMessage {
658
+ /** The checkpoint the client's view is expected to be at before this poke applies (for ordering/gap detection). */
659
+ baseCheckpoint?: number;
660
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
661
+ epoch?: string;
662
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
663
+ pokeId: string;
664
+ type: "pokeStart";
665
+ }
666
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
667
+ interface ServerPokePartMessage {
668
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
669
+ lastMutationId?: number;
670
+ pokeId: string;
671
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
672
+ rowsPatch: RowOp[];
673
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
674
+ shapeId: string;
675
+ type: "pokePart";
676
+ }
677
+ /**
678
+ * Closes a poke: the client commits the buffered parts atomically and advances
679
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
680
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
681
+ * the next reconnect.
682
+ */
683
+ interface ServerPokeEndMessage {
684
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
685
+ checkpoint?: number;
686
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
687
+ epoch?: string;
688
+ pokeId: string;
689
+ type: "pokeEnd";
690
+ }
691
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
374
692
  /**
375
693
  * The authenticated user as exposed client-side, mirroring better-auth's
376
694
  * `user` row (the `user` field of the `get-session` response). Kept minimal
@@ -582,6 +900,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
900
  }
583
901
  /** A page of workflow instances. */
584
902
  interface WorkflowInstancePage {
903
+ /**
904
+ * Whether workflow inspection is configured on the worker (a Cloudflare
905
+ * account id + API token). `false` when the admin proxy reports it can't
906
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
907
+ * caller render a "set credentials" state without a failed request.
908
+ */
909
+ configured?: boolean;
585
910
  instances: WorkflowInstanceSummary[];
586
911
  page: number;
587
912
  perPage: number;
@@ -594,6 +919,23 @@ interface SubscriptionError {
594
919
  message: string;
595
920
  }
596
921
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
922
+ /**
923
+ * One active per-call optimistic transform layered onto a subscription. The
924
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
925
+ * folded through every layer's `transform`, in order — so an incoming server
926
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
927
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
928
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
929
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
930
+ * mutation's response, and stays `undefined` while the write is still queued/
931
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
932
+ */
933
+ interface OptimisticLayer {
934
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
935
+ commitCursor?: number;
936
+ readonly id: symbol;
937
+ readonly transform: (current: unknown) => unknown;
938
+ }
597
939
  interface SubscriptionState {
598
940
  /** True once the server has acked the subscription on the current socket. */
599
941
  acked: boolean;
@@ -605,13 +947,52 @@ interface SubscriptionState {
605
947
  */
606
948
  readonly argsKey: string;
607
949
  readonly callbacks: Set<SubscriptionCallback>;
950
+ /**
951
+ * Notified when a `settled` frame advances this subscription's watermark — a
952
+ * write touched the subscription's tables but the result was byte-identical,
953
+ * so the server suppressed the data frame. A `@lunora/db` list collection
954
+ * uses this to drop the optimistic overlay for the confirmed write.
955
+ *
956
+ * A SET (not a single slot) because `SubscriptionState` is SHARED across
957
+ * every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
958
+ * collection may subscribe to a query a plain `useQuery` already opened, so
959
+ * each subscriber registers its own callback (mirroring `callbacks` /
960
+ * `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
961
+ * `useQuery` consumers register nothing, leaving the set empty.
962
+ */
963
+ readonly checkpointCallbacks: Set<(watermark: {
964
+ checkpoint?: number;
965
+ mutationId?: number;
966
+ }) => void>;
608
967
  /** Notified when the server rejects this subscription (e.g. admin auth). */
609
968
  readonly errorCallbacks: Set<SubscriptionErrorCallback>;
610
969
  readonly fn: FunctionReference;
611
970
  readonly id: string;
971
+ /**
972
+ * The highest custom-mutator `mutationId` from this client the server has
973
+ * applied, captured from the last `settled` frame (the suppressed-list-frame
974
+ * watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
975
+ * Absent until a `settled` frame arrives.
976
+ */
977
+ lastMutationId?: number;
612
978
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
613
979
  lastValue: unknown;
614
980
  /**
981
+ * Active per-call optimistic layers, in application order (see
982
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
983
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
984
+ * exactly and behaviour is identical to a plain server-value assignment.
985
+ */
986
+ optimisticLayers: OptimisticLayer[];
987
+ /**
988
+ * The authoritative server value the optimistic layers fold onto — the value
989
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
990
+ * are active; diverges only while a per-call optimistic write is pending. A
991
+ * server frame updates this (and re-folds the layers); the durable read cache
992
+ * persists this, never the optimistic overlay.
993
+ */
994
+ serverBase: unknown;
995
+ /**
615
996
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
616
997
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
617
998
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -627,18 +1008,15 @@ interface SubscriptionState {
627
1008
  * until the first epoch-stamped frame arrives.
628
1009
  */
629
1010
  serverEpoch?: string;
630
- /**
631
- * Monotonic counter incremented on every server-pushed delta or data.
632
- * Used by optimistic-update rollback to detect whether the server has
633
- * already moved past the value we'd otherwise restore.
634
- */
635
- serverVersion: number;
636
1011
  readonly shardKey?: string;
637
1012
  }
638
1013
  /**
639
1014
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
1015
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
1016
+ * single server-side registration. Args are stably encoded (keys sorted at every
1017
+ * depth) so two structurally-equal arg records constructed with a different key
1018
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
1019
+ * duplicate subscription.
642
1020
  */
643
1021
  declare class SubscriptionRegistry {
644
1022
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -656,10 +1034,10 @@ declare class SubscriptionRegistry {
656
1034
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
657
1035
  *
658
1036
  * `getQuery` reads the current value (server value or any still-pending
659
- * optimistic override) of a subscribed query; `setQuery` writes an optimistic
660
- * override on top. Every write is collected as a rollback closure so the whole
661
- * batch unwinds atomically when the mutation settles or the server advances
662
- * past it — the same per-subscription rollback machinery the legacy
1037
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
1038
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
1039
+ * settles together confirmed on the mutation's commit cursor, or rolled back
1040
+ * on failure — the same per-subscription layer machinery the single-query
663
1041
  * per-call `optimistic` transform uses, generalized to N queries.
664
1042
  */
665
1043
  interface OptimisticLocalStore {
@@ -689,30 +1067,20 @@ interface OptimisticLocalStore {
689
1067
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
690
1068
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
691
1069
  /**
692
- * Build an {@link OptimisticLocalStore} bound to a subscription registry, the
693
- * mutation's shard key, and the `writeOptimisticToState` primitive. Returns the
694
- * store plus the ordered rollback closures every `setQuery` produced, so the
695
- * caller can unwind the whole batch (LIFO) if the mutation later fails — and
696
- * leave them in place to be GC'd alongside the subscription on success.
1070
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
1071
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
1072
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
1073
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
1074
+ * query not merged) and drops gaplessly on the mutation's commit cursor, like
1075
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
1076
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
1077
+ * so the caller settles the whole batch when the mutation does.
697
1078
  */
698
- declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, write: (state: SubscriptionState, next: unknown) => () => void, stableStringify: (value: unknown) => string) => {
1079
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined) => {
1080
+ confirms: ((commitCursor: number | undefined) => void)[];
699
1081
  rollbacks: (() => void)[];
700
1082
  store: OptimisticLocalStore;
701
1083
  };
702
- /**
703
- * Bounded async-iterator queue backing `LunoraClient.stream`.
704
- *
705
- * The server pushes one server `chunk` message per yielded value while the
706
- * client iterates with `for await (const chunk of stream)`. A producer that
707
- * outruns its consumer would otherwise OOM the page, so the buffer is bounded
708
- * — exceeding {@link DEFAULT_MAX_BUFFER} surfaces a `STREAM_BACKPRESSURE`
709
- * error to the iterator (and to the server-side cancel path).
710
- *
711
- * The queue is closed exactly once via {@link StreamHandle.complete} (success)
712
- * or {@link StreamHandle.fail} (transport / server error). Subsequent calls
713
- * are silent no-ops so a duplicate `complete` frame after a cancel doesn't
714
- * crash the page.
715
- */
716
1084
  declare const DEFAULT_MAX_BUFFER = 1024;
717
1085
  interface StreamHandle<T = unknown> {
718
1086
  /** Mark the stream complete (no more chunks); resolves any pending consumer to `done:true`. */
@@ -753,6 +1121,41 @@ declare const createStream: <T>(options: {
753
1121
  */
754
1122
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
755
1123
  /**
1124
+ * Terminal verdict for a mutation that passed through the offline queue,
1125
+ * delivered to {@link LunoraClient.onMutationSettled}.
1126
+ *
1127
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1128
+ * original caller can await, and which no longer exists after a reload — this
1129
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1130
+ * including writes restored from durable storage in a later session. It is the
1131
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1132
+ * instead of silently dropping a rolled-back optimistic row.
1133
+ *
1134
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1135
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1136
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1137
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1138
+ * "the caller already saw this" apart from "nothing else will report this".
1139
+ */
1140
+ interface MutationSettledEvent {
1141
+ /** The write's args, so a listener can describe or re-offer the change. */
1142
+ readonly args: Record<string, unknown>;
1143
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
1144
+ readonly code?: string;
1145
+ /** The rejection error on `status: "rejected"`. */
1146
+ readonly error?: unknown;
1147
+ /** The `&lt;file>:&lt;function>` reference of the mutation. */
1148
+ readonly functionPath: string;
1149
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1150
+ readonly hadAwaiter: boolean;
1151
+ /** The write's stable id (idempotency key / queue id). */
1152
+ readonly id: string;
1153
+ /** Shard the write targeted, if any. */
1154
+ readonly shardKey?: string;
1155
+ /** Terminal outcome. */
1156
+ readonly status: "committed" | "rejected";
1157
+ }
1158
+ /**
756
1159
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
757
1160
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
758
1161
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -760,6 +1163,13 @@ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
760
1163
  * re-declaring it.
761
1164
  */
762
1165
  interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
1166
+ /**
1167
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
1168
+ * durable outbox replay a committed-but-unacked write under its *original* key
1169
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
1170
+ * each then gets a fresh key.
1171
+ */
1172
+ mutationId?: string;
763
1173
  optimistic?: (current: TCurrent | undefined) => TValue;
764
1174
  /**
765
1175
  * Convex-parity multi-query optimistic update. Receives an
@@ -770,6 +1180,42 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
770
1180
  optimisticUpdate?: OptimisticUpdate<TArgs>;
771
1181
  shardKey?: string;
772
1182
  }
1183
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
1184
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
1185
+ /**
1186
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
1187
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
1188
+ * echoed for this client. A `@lunora/db` collection feeds these into its
1189
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
1190
+ * rows have landed.
1191
+ */
1192
+ interface SyncWatermark {
1193
+ checkpoint?: number;
1194
+ mutationId?: number;
1195
+ }
1196
+ /**
1197
+ * An `Error` carrying the server's machine-readable `code` and (for a
1198
+ * `LunoraError`) structured `data`, plus an optional actionable `hint` (Markdown)
1199
+ * and `docsUrl` resolved from the central error catalog. The client's public
1200
+ * error contract for RPC/batch failures — a UI can render `hint`/`docsUrl` to
1201
+ * tell the user how to fix the error. The `(string & {})` arm keeps
1202
+ * forward-compat/unknown server codes assignable without losing autocomplete on
1203
+ * the known {@link LunoraErrorCode} union.
1204
+ */
1205
+ type LunoraClientError = Error & {
1206
+ code?: LunoraErrorCode | (string & {});
1207
+ data?: unknown;
1208
+ docsUrl?: string;
1209
+ hint?: string | string[];
1210
+ };
1211
+ /** One demuxed result slot of a {@link LunoraClient.batch} call (plan 088). */
1212
+ type BatchSlot = {
1213
+ error: LunoraClientError;
1214
+ ok: false;
1215
+ } | {
1216
+ ok: true;
1217
+ value: unknown;
1218
+ };
773
1219
  /**
774
1220
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
775
1221
  * a single multiplexed WebSocket.
@@ -778,6 +1224,8 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
778
1224
  * see the package README for the wire protocol.
779
1225
  */
780
1226
  declare class LunoraClient {
1227
+ /** Hard cap on concurrently-buffered pokes — a backstop that reclaims buffers abandoned by a mid-poke disconnect (no `pokeEnd`). Far above any real concurrent-in-flight count. */
1228
+ private static readonly MAX_POKE_BUFFERS;
781
1229
  readonly url: string;
782
1230
  readonly wsUrl: string;
783
1231
  private wsToken;
@@ -787,11 +1235,36 @@ declare class LunoraClient {
787
1235
  private readonly WebSocketImpl;
788
1236
  private readonly bookmark;
789
1237
  private readonly reconnectOptions;
1238
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
1239
+ private readonly connectTimeoutMs;
790
1240
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
1241
  private readonly heartbeatIntervalMs;
792
1242
  private readonly offlineQueue;
1243
+ /**
1244
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
1245
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
1246
+ * is bypassed, so a db app has exactly one durable write path.
1247
+ */
1248
+ private readonly outbox;
1249
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
1250
+ private readonly clientId;
1251
+ /**
1252
+ * Highest custom-mutator watermark the server has echoed for this client,
1253
+ * keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
1254
+ * `__client_watermark` per shard. `callMutator` bumps it from every
1255
+ * ack; the `@lunora/db` mutator runtime seeds its `clientSeq` generator from
1256
+ * it so a reload (which resets the in-memory counter) never reissues a stale
1257
+ * sequence the server would silently swallow as a replay.
1258
+ */
1259
+ private readonly clientWatermarks;
1260
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1261
+ private outboxMutationCounter;
793
1262
  private readonly onPersistenceError;
794
1263
  private readonly persistence;
1264
+ /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
1265
+ private readonly persistenceVersion;
1266
+ /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
1267
+ private outboxLeaderRelease;
795
1268
  /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
796
1269
  private readonly queryCache;
797
1270
  /**
@@ -835,6 +1308,14 @@ declare class LunoraClient {
835
1308
  private readonly connectionContextHolders;
836
1309
  private authToken;
837
1310
  /**
1311
+ * Optional STABLE identity subject (a user id), the basis of the offline-queue
1312
+ * identity stamp when supplied. Keeps a same-user token *refresh* from looking
1313
+ * like an identity change (which would discard queued writes). `undefined` =
1314
+ * not supplied, so identity falls back to a hash of the raw token. See
1315
+ * `setAuthToken` / `identityFingerprint`.
1316
+ */
1317
+ private authSubject;
1318
+ /**
838
1319
  * Identity stamp recorded against each queued offline mutation, keyed by
839
1320
  * the queue-assigned mutation id. Captured at enqueue from the auth token
840
1321
  * in effect at the time, and re-checked at flush so a queued write can
@@ -849,6 +1330,10 @@ declare class LunoraClient {
849
1330
  private readonly statusListeners;
850
1331
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
851
1332
  private readonly tokenExpiredListeners;
1333
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1334
+ private readonly mutationSettledListeners;
1335
+ /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
1336
+ private readonly pendingChangeListeners;
852
1337
  /**
853
1338
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
854
1339
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -866,20 +1351,78 @@ declare class LunoraClient {
866
1351
  * calls `.cancel()` or the iterator is garbage-collected.
867
1352
  */
868
1353
  private readonly streams;
1354
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
1355
+ private readonly shapeSubscriptions;
1356
+ /** In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by `pokeId`. */
1357
+ private readonly pokeBuffers;
1358
+ private nextShapeId;
869
1359
  constructor(options: LunoraClientOptions);
870
1360
  /**
871
1361
  * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
872
1362
  * {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
873
1363
  * sync across all mounted instances.
874
1364
  *
1365
+ * Pass a STABLE `subject` (the user id) to key the offline-queue identity on
1366
+ * it instead of the token bytes, so a token *refresh* (same user, new JWT)
1367
+ * doesn't read as an identity change and discard queued writes. The subject is
1368
+ * **sticky**: a later call that omits it (or passes `undefined`) keeps the
1369
+ * established subject — so `setAuthToken(refreshedToken)` after a prior
1370
+ * `setAuthToken(token, user.id)` retains the identity. Pass `null` to clear it
1371
+ * (an explicit sign-out). Establishing the subject for the first time on an
1372
+ * UNCHANGED token (e.g. the user id resolves a tick after the token was set)
1373
+ * re-stamps any in-flight queued writes rather than dropping them — same
1374
+ * credential, just a more stable label. A real user switch (the token AND
1375
+ * subject both change) still drops the previous user's writes.
1376
+ *
875
1377
  * Does NOT update the WebSocket auth — the WS token is fixed at upgrade
876
1378
  * time and lives in the URL. To refresh live WS auth, call
877
1379
  * {@link setWsToken} explicitly, which closes existing shard sockets to
878
1380
  * force a reconnect with the new credential.
879
1381
  */
880
- setAuthToken(token: string | null): void;
1382
+ setAuthToken(token: string | null, subject?: string | null): void;
881
1383
  getAuthToken(): string | null;
882
1384
  /**
1385
+ * The current identity fingerprint (the same stamp queued offline writes
1386
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
1387
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
1388
+ * can drop a persisted write whose captured `identity` no longer matches the
1389
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
1390
+ */
1391
+ currentIdentity(): string | null;
1392
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
1393
+ clientIdentifier(): string;
1394
+ /**
1395
+ * The highest custom-mutator watermark the server has echoed for this client
1396
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
1397
+ * its `clientSeq` generator from this so a reload never reissues a sequence
1398
+ * the server has already applied (which it would swallow as a replay, silently
1399
+ * dropping the write).
1400
+ */
1401
+ confirmedMutationWatermark(shardKey?: string): number;
1402
+ /**
1403
+ * Push a custom mutator to its authoritative server impl over the watermark
1404
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
1405
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
1406
+ * client's `__client_watermark`.
1407
+ *
1408
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
1409
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
1410
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
1411
+ * A `false` verdict tells the caller to reissue above the now-known watermark
1412
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
1413
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
1414
+ *
1415
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
1416
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
1417
+ */
1418
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
1419
+ clientSeq?: number;
1420
+ shardKey?: string;
1421
+ }): Promise<{
1422
+ applied: boolean;
1423
+ result: unknown;
1424
+ }>;
1425
+ /**
883
1426
  * Subscribe to auth-token changes. Returns an unsubscribe function. The
884
1427
  * listener is NOT invoked on registration — use {@link getAuthToken} for
885
1428
  * the current value.
@@ -990,10 +1533,55 @@ declare class LunoraClient {
990
1533
  * unsubscribe function.
991
1534
  */
992
1535
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
1536
+ /**
1537
+ * Number of offline writes waiting in the built-in queue to be sent — the
1538
+ * depth for a "N changes waiting to sync" indicator. Counts writes that are
1539
+ * queued (offline / mid-reconnect), not ones already in flight on the wire.
1540
+ * A `@lunora/db` app whose writes ride the unified outbox should read
1541
+ * `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
1542
+ */
1543
+ pendingCount(): number;
1544
+ /**
1545
+ * Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
1546
+ * with the current count, then whenever the queue depth changes (a write is
1547
+ * enqueued, flushed, or discarded). Returns an unsubscribe function.
1548
+ */
1549
+ onPendingChange(listener: (pending: number) => void): Unsubscribe;
1550
+ /**
1551
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
1552
+ * fires once per queued write that commits or is rejected — including a write
1553
+ * restored from durable storage after a reload, whose original `mutation()`
1554
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
1555
+ * evicts on overflow or discards on an identity change. This is the durable
1556
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
1557
+ * mutation that never queued still surfaces through the Promise `mutation()`
1558
+ * returns. The listener is NOT invoked on registration. Returns an
1559
+ * unsubscribe function. See {@link MutationSettledEvent}.
1560
+ */
1561
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
993
1562
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
994
1563
  shardKey?: string;
995
1564
  }): Promise<ReturnOf<F>>;
996
1565
  /**
1566
+ * Batch several independent calls into ONE round trip (plan 088). Each call is
1567
+ * dispatched server-side exactly as an individual RPC — per-shard
1568
+ * authorization, `(identity, mutationId)` idempotency, and custom-mutator
1569
+ * watermark ordering are all preserved — and the worker splits the batch by
1570
+ * shard so calls to different shards fan out to their own DOs. Results are
1571
+ * demuxed back in input order; a failing call does NOT fail the batch (its
1572
+ * slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
1573
+ * a single call). Args/results ride the value codec (bytes/bigint survive).
1574
+ *
1575
+ * No promise pipelining and no capability passing — a call's args cannot
1576
+ * reference another call's result (see plan 088 §fence; capabilities are
1577
+ * incompatible with DO hibernation).
1578
+ */
1579
+ batch(calls: ReadonlyArray<{
1580
+ args?: Record<string, unknown>;
1581
+ fn: FunctionReference;
1582
+ shardKey?: string;
1583
+ }>): Promise<BatchSlot[]>;
1584
+ /**
997
1585
  * Invoke a mutation. Errors propagate as rejections.
998
1586
  *
999
1587
  * Offline-queue semantics: a mutation is queued (and replayed on reconnect)
@@ -1069,8 +1657,12 @@ declare class LunoraClient {
1069
1657
  * List a workflow's instances via the admin Workflows proxy
1070
1658
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1659
  * the `Workflow` binding can't expose. Requires the worker to be built with a
1072
- * `workflowsClient` (Cloudflare account id + API token); otherwise the proxy
1073
- * responds 501 and this rejects. `name` is the deployed workflow name.
1660
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1661
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1662
+ * false }` sentinel, so the result resolves with `configured === false` and an
1663
+ * empty `instances` list — callers should branch on that flag rather than
1664
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1665
+ * `name` is the deployed workflow name.
1074
1666
  */
1075
1667
  listWorkflowInstances(options: {
1076
1668
  name: string;
@@ -1269,6 +1861,55 @@ declare class LunoraClient {
1269
1861
  topK?: number;
1270
1862
  }): Promise<VectorQueryMatch[]>;
1271
1863
  /**
1864
+ * List the worker's registered Workers KV namespaces (binding names). Hits
1865
+ * the admin-gated `GET /_lunora/admin/kv/namespaces` endpoint — the worker
1866
+ * must be built with a `kvIntrospector` and `adminToken`. Powers the
1867
+ * studio's KV browser.
1868
+ */
1869
+ listKvNamespaces(): Promise<KvNamespaceSummary[]>;
1870
+ /**
1871
+ * List keys in a KV namespace, optionally filtered by `prefix` and
1872
+ * paginated via `cursor`. Hits the admin-gated
1873
+ * `GET /_lunora/admin/kv/keys` endpoint.
1874
+ */
1875
+ listKvKeys(options: {
1876
+ cursor?: string;
1877
+ limit?: number;
1878
+ namespace: string;
1879
+ prefix?: string;
1880
+ }): Promise<KvKeyListResult>;
1881
+ /**
1882
+ * Read a KV value (as text) and its metadata. Hits the admin-gated
1883
+ * `GET /_lunora/admin/kv/value` endpoint. Returns `{ value: null, metadata: null }`
1884
+ * when the key is absent.
1885
+ */
1886
+ getKvValue(options: {
1887
+ key: string;
1888
+ namespace: string;
1889
+ }): Promise<KvValueResult>;
1890
+ /**
1891
+ * Write a string value to a KV namespace. Accepts an absolute `expiration`
1892
+ * (Unix seconds) or a relative `expirationTtl`, plus optional `metadata` —
1893
+ * re-send the loaded values on edit so a save preserves rather than clears
1894
+ * them. Hits the admin-gated `PUT /_lunora/admin/kv/value` endpoint.
1895
+ */
1896
+ putKvValue(options: {
1897
+ expiration?: number;
1898
+ expirationTtl?: number;
1899
+ key: string;
1900
+ metadata?: unknown;
1901
+ namespace: string;
1902
+ value: string;
1903
+ }): Promise<void>;
1904
+ /**
1905
+ * Delete a key from a KV namespace. No-op when the key is absent. Hits the
1906
+ * admin-gated `DELETE /_lunora/admin/kv/value` endpoint.
1907
+ */
1908
+ deleteKvKey(options: {
1909
+ key: string;
1910
+ namespace: string;
1911
+ }): Promise<void>;
1912
+ /**
1272
1913
  * List authenticated users, paged and optionally searched / filtered / sorted.
1273
1914
  * Hits the admin-gated `GET /_lunora/admin/auth/users` endpoint — the worker
1274
1915
  * must be built with an `authAdmin` and `adminToken`. Powers the studio's
@@ -1393,6 +2034,107 @@ declare class LunoraClient {
1393
2034
  cancelAuthOrgInvitation(input: {
1394
2035
  invitationId: string;
1395
2036
  }): Promise<void>;
2037
+ /**
2038
+ * Report the deployment's auth configuration — enabled plugins, sign-in
2039
+ * methods, user-settable create-user fields, organization sub-features
2040
+ * (teams / roles), and session / rate-limit policy. Drives the config panel
2041
+ * and the dynamic create-user form. Never carries a secret.
2042
+ */
2043
+ getAuthConfig(): Promise<AuthConfigInfo>;
2044
+ /** Create an organization; optionally seed an `owner` member for `ownerId`. */
2045
+ createAuthOrganization(input: {
2046
+ logo?: string;
2047
+ metadata?: Record<string, unknown>;
2048
+ name: string;
2049
+ ownerId?: string;
2050
+ slug?: string;
2051
+ }): Promise<Record<string, unknown>>;
2052
+ /** Update an organization's name/slug/logo/metadata. */
2053
+ updateAuthOrganization(input: {
2054
+ logo?: string;
2055
+ metadata?: Record<string, unknown>;
2056
+ name?: string;
2057
+ organizationId: string;
2058
+ slug?: string;
2059
+ }): Promise<Record<string, unknown>>;
2060
+ /** Delete an organization and cascade its members, invitations, teams, and custom roles. */
2061
+ deleteAuthOrganization(input: {
2062
+ organizationId: string;
2063
+ }): Promise<void>;
2064
+ /** Directly add an existing user to an organization (no invitation/acceptance). */
2065
+ addAuthOrgMember(input: {
2066
+ organizationId: string;
2067
+ role?: string;
2068
+ userId: string;
2069
+ }): Promise<Record<string, unknown>>;
2070
+ /** Create a pending email invitation to an organization. */
2071
+ inviteAuthOrgMember(input: {
2072
+ email: string;
2073
+ inviterId?: string;
2074
+ organizationId: string;
2075
+ role?: string;
2076
+ }): Promise<Record<string, unknown>>;
2077
+ /** Change a member's role. */
2078
+ setAuthOrgMemberRole(input: {
2079
+ memberId: string;
2080
+ role: string | string[];
2081
+ }): Promise<Record<string, unknown>>;
2082
+ /** List an organization's teams (requires the organization plugin with teams enabled). */
2083
+ listAuthOrgTeams(input: {
2084
+ limit?: number;
2085
+ offset?: number;
2086
+ organizationId: string;
2087
+ }): Promise<AuthPage<Record<string, unknown>>>;
2088
+ /** Create a team under an organization. */
2089
+ createAuthOrgTeam(input: {
2090
+ name: string;
2091
+ organizationId: string;
2092
+ }): Promise<Record<string, unknown>>;
2093
+ /** Rename a team. */
2094
+ updateAuthOrgTeam(input: {
2095
+ name: string;
2096
+ teamId: string;
2097
+ }): Promise<Record<string, unknown>>;
2098
+ /** Delete a team and its memberships. */
2099
+ removeAuthOrgTeam(input: {
2100
+ teamId: string;
2101
+ }): Promise<void>;
2102
+ /** List a team's members. */
2103
+ listAuthOrgTeamMembers(input: {
2104
+ limit?: number;
2105
+ offset?: number;
2106
+ teamId: string;
2107
+ }): Promise<AuthPage<Record<string, unknown>>>;
2108
+ /** Add a user to a team. */
2109
+ addAuthOrgTeamMember(input: {
2110
+ teamId: string;
2111
+ userId: string;
2112
+ }): Promise<Record<string, unknown>>;
2113
+ /** Remove a member from a team. */
2114
+ removeAuthOrgTeamMember(input: {
2115
+ teamMemberId: string;
2116
+ }): Promise<void>;
2117
+ /** List an organization's custom roles (requires the organization plugin with dynamic access control). */
2118
+ listAuthOrgRoles(input: {
2119
+ limit?: number;
2120
+ offset?: number;
2121
+ organizationId: string;
2122
+ }): Promise<AuthPage<Record<string, unknown>>>;
2123
+ /** Create a custom org role with a permission grant (a `resource -> actions[]` map). */
2124
+ createAuthOrgRole(input: {
2125
+ organizationId: string;
2126
+ permission: Record<string, string[]>;
2127
+ role: string;
2128
+ }): Promise<Record<string, unknown>>;
2129
+ /** Replace a custom org role's permission grant. */
2130
+ updateAuthOrgRole(input: {
2131
+ permission: Record<string, string[]>;
2132
+ roleId: string;
2133
+ }): Promise<Record<string, unknown>>;
2134
+ /** Delete a custom org role. */
2135
+ deleteAuthOrgRole(input: {
2136
+ roleId: string;
2137
+ }): Promise<void>;
1396
2138
  /** List auth sessions, paged and optionally filtered to one user. */
1397
2139
  listAuthSessions(options?: {
1398
2140
  limit?: number;
@@ -1400,6 +2142,27 @@ declare class LunoraClient {
1400
2142
  userId?: string;
1401
2143
  }): Promise<AuthPage<AuthSession>>;
1402
2144
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
2145
+ onCheckpoint?: (watermark: SyncWatermark) => void;
2146
+ onError?: SubscriptionErrorCallback;
2147
+ shardKey?: string;
2148
+ }): Unsubscribe;
2149
+ /**
2150
+ * Subscribe to a declarative **shape** — server-side partial replication
2151
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
2152
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
2153
+ * validated `args` (never a `where` the client could forge), the server seeds
2154
+ * the current membership as an insert-poke and streams live membership diffs.
2155
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
2156
+ *
2157
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
2158
+ * (name, args): the server resolves them under the socket's verified identity,
2159
+ * so every call gets its own id + view. The returned function unsubscribes.
2160
+ */
2161
+ subscribeShape(shape: {
2162
+ args?: Record<string, unknown>;
2163
+ name: string;
2164
+ }, callback: ShapeCallback, options?: {
2165
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1403
2166
  onError?: SubscriptionErrorCallback;
1404
2167
  shardKey?: string;
1405
2168
  }): Unsubscribe;
@@ -1427,12 +2190,35 @@ declare class LunoraClient {
1427
2190
  }): StreamIterable<ReturnOf<F>>;
1428
2191
  close(): void;
1429
2192
  /**
2193
+ * Persist a mutation that can't go out on the wire right now (offline, or
2194
+ * mid-reconnect after a prior connect). The optimistic update has already
2195
+ * been applied by `mutation`; this only chooses the durable write path and
2196
+ * rolls the optimistic write back if persistence is rejected.
2197
+ *
2198
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
2199
+ * owns persistence + at-least-once replay, so we delegate and return
2200
+ * optimistically (confirmation rides the synced view). Otherwise the
2201
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
2202
+ */
2203
+ private enqueueOfflineMutation;
2204
+ /**
1430
2205
  * Restore offline mutations persisted in a prior session and open a socket
1431
2206
  * for each shard they target so they flush once the WS reconnects. Failures
1432
2207
  * are swallowed — a broken durable store must not stop the client booting.
1433
2208
  */
1434
2209
  private hydratePersistedQueue;
1435
2210
  /**
2211
+ * Re-queue the durable offline writes — but only as the multi-tab LEADER. The
2212
+ * persisted queue is shared across a profile's tabs; without coordination
2213
+ * every tab would re-queue and replay the same writes (correct only because
2214
+ * the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
2215
+ * exactly one tab hydrate; it holds the lock for its lifetime, so when it
2216
+ * closes another tab acquires the lock and takes over. Falls back to
2217
+ * unconditional hydration where Web Locks are unavailable (React Native, older
2218
+ * browsers, SSR) — single-context there, so no coordination is needed.
2219
+ */
2220
+ private hydrateAsOutboxLeader;
2221
+ /**
1436
2222
  * Load every cached query into {@link hydratedQueryCache} so the next
1437
2223
  * `subscribe()` for each key seeds its initial value off disk. A
1438
2224
  * subscription created before this resolves simply misses the cache (it
@@ -1462,21 +2248,37 @@ declare class LunoraClient {
1462
2248
  /** Recompute the aggregate status and notify listeners if it changed. */
1463
2249
  private emitConnectionStatus;
1464
2250
  /**
1465
- * Apply an optimistic update to every subscription that matches the
1466
- * mutation's function ref, shard key, and args, returning the rollback
1467
- * callbacks to invoke if the mutation later fails. Scoping to the same
1468
- * (fn, shardKey, args) keeps one user's mutation from clobbering another
1469
- * subscriber's value on the same function (e.g. two users on different rooms).
2251
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
2252
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
2253
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
2254
+ * is unreachable present only to satisfy the optional queue-id type.
2255
+ */
2256
+ private emitItemSettled;
2257
+ /**
2258
+ * Apply an optimistic update to the subscription that matches the mutation's
2259
+ * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
2260
+ * invoke if the mutation later fails.
2261
+ *
2262
+ * The registry is already indexed by exactly this triple via
2263
+ * `SubscriptionRegistry.key`, so at most one subscription can match. A direct
2264
+ * O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
2265
+ *
2266
+ * `shardKey` normalization: both `undefined` and `""` map to the empty string
2267
+ * inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
2268
+ * a shardKey correctly matches a subscription registered without one regardless
2269
+ * of whether the caller passed `undefined` or omitted the field.
1470
2270
  */
1471
2271
  private applyOptimisticUpdates;
1472
2272
  /**
1473
2273
  * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1474
- * to the live subscription registry, appending each `setQuery` write's
1475
- * rollback to `optimisticRollbacks` (the same LIFO list the legacy path uses,
1476
- * unwound on settle/error). A throwing callback unwinds its own partial
1477
- * writes LIFO over just the rollbacks it producedand is swallowed, so a
1478
- * buggy optimistic update can never fail the mutation or leave a partial
1479
- * patch live, mirroring the legacy transform's throw handling.
2274
+ * to the live subscription registry. Each `setQuery` registers a constant
2275
+ * optimistic LAYER on its target subscription (via the same engine the
2276
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
2277
+ * incoming deltas and drops gaplessly on its commit cursorits `confirm` /
2278
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
2279
+ * callback unwinds its own partial writes LIFO over just the rollbacks it
2280
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
2281
+ * mutation or leave a partial patch live.
1480
2282
  */
1481
2283
  private applyOptimisticUpdate;
1482
2284
  private getConnection;
@@ -1523,6 +2325,13 @@ declare class LunoraClient {
1523
2325
  * to the lifecycle dispatch.
1524
2326
  */
1525
2327
  private sendConnectEnvelope;
2328
+ /**
2329
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
2330
+ * socket. Each frame carries the shape's last applied checkpoint, so the
2331
+ * server resumes from it — or re-seeds when the cursor fell below CDC
2332
+ * retention or the epoch forked.
2333
+ */
2334
+ private resendShapeSubscriptions;
1526
2335
  private ensureSocket;
1527
2336
  private handleDisconnect;
1528
2337
  /**
@@ -1538,8 +2347,14 @@ declare class LunoraClient {
1538
2347
  /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
1539
2348
  private markShardPendingAck;
1540
2349
  private sendSubscribeIfOpen;
2350
+ private sendShapeSubscribeIfOpen;
1541
2351
  private handleServerMessage;
1542
2352
  private handleErrorMessage;
2353
+ private handlePokeStart;
2354
+ private handlePokePart;
2355
+ private handlePokeEnd;
2356
+ /** Materialize a shape's keyed view to an array and invoke its callbacks. */
2357
+ private emitShapeRows;
1543
2358
  private handleDataMessage;
1544
2359
  /**
1545
2360
  * Handle a `resume` frame (Pillar 1b): the server proved nothing the
@@ -1551,6 +2366,25 @@ declare class LunoraClient {
1551
2366
  */
1552
2367
  private handleResumeMessage;
1553
2368
  /**
2369
+ * Handle a `settled` frame: a write touched one of this subscription's read
2370
+ * tables but produced a byte-identical result, so the server suppressed the
2371
+ * data frame. Like {@link handleResumeMessage} the value didn't change — we
2372
+ * advance the resume position and re-persist — but we ALSO surface the echoed
2373
+ * custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
2374
+ * collection drops the optimistic overlay for the confirmed write (otherwise
2375
+ * its checkpoint gate, fed only by data frames, would hang forever). Sent
2376
+ * only to custom-mutator clients; plain `useQuery` subscribers leave
2377
+ * `onCheckpoint` unset and this is a near no-op.
2378
+ */
2379
+ private handleSettledMessage;
2380
+ /**
2381
+ * Mark `state` acked and, when the frame carries a newer cursor/epoch than
2382
+ * the cached position, advance the resume watermark and re-persist. Shared by
2383
+ * the `resume` and `settled` frame handlers — both acknowledge "nothing the
2384
+ * client must re-render changed, but the resume position may have moved".
2385
+ */
2386
+ private ackAndAdvanceCursor;
2387
+ /**
1554
2388
  * Resolve the value to publish for a `data`/`delta` frame.
1555
2389
  *
1556
2390
  * A `data` frame is an authoritative snapshot (the server re-execution path)
@@ -1578,6 +2412,34 @@ declare class LunoraClient {
1578
2412
  */
1579
2413
  private identityFingerprint;
1580
2414
  /**
2415
+ * Stable token-hash fingerprint of a bearer token (the `&lt;len>:&lt;fnv>:&lt;djb2>`
2416
+ * format a token-stamped queued write carries). Extracted so the replay gate
2417
+ * can recompute the hash of the current credential and recognise a write
2418
+ * stamped under it — even after the fingerprint was relabelled to a subject.
2419
+ *
2420
+ * Two independent 32-bit passes (FNV-1a + djb2) give a ~64-bit digest, so
2421
+ * two distinct equal-length tokens are astronomically unlikely to share a
2422
+ * fingerprint. A single 32-bit hash collides ~1-in-4e9 per equal-length
2423
+ * pair — enough that, on a shared device, user B could hydrate A's cached
2424
+ * reads. Different algorithms (not the same FNV with a different seed, which
2425
+ * would be affine-related) keep the two passes genuinely independent.
2426
+ * Still synchronous (no crypto) and stable across surrogate pairs.
2427
+ */
2428
+ private hashToken;
2429
+ /**
2430
+ * True when `stamped` is a token-hash of the SAME credential still held now,
2431
+ * even though the live identity has since been relabelled to a subject. Covers
2432
+ * `setAuthToken(token, userId)` where the subject resolved a tick after the
2433
+ * token was set: a write persisted (or requeued) under the token hash must
2434
+ * still replay — the credential never changed, only its label — instead of
2435
+ * being dropped as an identity mismatch. This is the durable counterpart to
2436
+ * {@link restampQueuedIdentity}, which only relabels the in-memory live stamp
2437
+ * (consumed on the first flush) and never touches `item.identity` or the
2438
+ * persisted record, so a reload or a transient-failure requeue would otherwise
2439
+ * fall back to the stale token-hash and wrongly reject the same user's write.
2440
+ */
2441
+ private isSameCredentialUnderTokenHash;
2442
+ /**
1581
2443
  * Drain every in-memory offline write and reject it because the auth
1582
2444
  * identity changed. Durable entries are also dropped from persistence so a
1583
2445
  * later `hydrate` can't resurrect another user's writes. Stamps are cleared
@@ -1586,6 +2448,15 @@ declare class LunoraClient {
1586
2448
  */
1587
2449
  private rejectQueuedForIdentityChange;
1588
2450
  /**
2451
+ * Migrate every live identity stamp from `from` to `to` — used when the auth
2452
+ * identity label changes but the underlying credential (token) does NOT, e.g.
2453
+ * the user id resolves a tick after the token was set. The in-memory
2454
+ * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2455
+ * keeps the in-flight writes replayable under the new (more stable) identity
2456
+ * instead of the flush guard discarding them as a mismatch.
2457
+ */
2458
+ private restampQueuedIdentity;
2459
+ /**
1589
2460
  * Drop the durable read cache on an identity change so a cached value stamped
1590
2461
  * under the previous identity can never hydrate into a new session. Clears
1591
2462
  * the in-flight write batch and the not-yet-consumed hydrated entries too;
@@ -1593,5 +2464,77 @@ declare class LunoraClient {
1593
2464
  */
1594
2465
  private clearQueryCacheForIdentityChange;
1595
2466
  private flushOfflineQueue;
2467
+ /**
2468
+ * Partition already-gated writes into the encodable ones (returned) and reject
2469
+ * the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
2470
+ * or class instance in a `v.any()` field) can NEVER replay — the codec failure
2471
+ * is deterministic, not transient. Rejecting here is essential: otherwise
2472
+ * `encodeWire` throws mid-flush, is classified as transient (a codec error has
2473
+ * no `.code`), and re-queues forever — a silent hang where the caller's Promise
2474
+ * never settles and the optimistic write never rolls back. Encoding is cheap;
2475
+ * the flush is the slow reconnect path.
2476
+ */
2477
+ private encodableOrSettleTerminal;
2478
+ /**
2479
+ * Identity guard for one queued write about to replay: a write stamped under
2480
+ * one identity must never replay under another. The live `queuedIdentities`
2481
+ * map is the source of truth for the current session; a hydrated write whose
2482
+ * id isn't in the map falls back to the stamp persisted with the record
2483
+ * (`item.identity`), so a reload can't replay another user's queued writes.
2484
+ * Only legacy records (persisted before stamps were durable —
2485
+ * `item.identity === undefined`) replay under whatever identity is current.
2486
+ *
2487
+ * `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
2488
+ * is `undefined` for legacy records; a persisted `null` (queued while signed
2489
+ * out) is a real value that must not collapse into `undefined` — hence the
2490
+ * explicit `=== undefined` check rather than `??`. Returns `true` when the
2491
+ * write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
2492
+ * `false`. Either way the live stamp is consumed.
2493
+ */
2494
+ private passesReplayIdentityGate;
2495
+ /** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
2496
+ private settleReplaySuccess;
2497
+ /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
2498
+ private settleReplayTerminal;
2499
+ /**
2500
+ * Replay already-identity-gated writes one at a time on the single-call `/rpc`
2501
+ * path, preserving FIFO order (parallel `.then()` chains would race the
2502
+ * ordering callers depend on). Each replays under its stable `mutationId` so
2503
+ * the server dedups a write it already committed (exactly-once). A coded error
2504
+ * is a server verdict (drop it); a codeless (transport/transient) failure stops
2505
+ * the flush and re-queues this write and every unreplayed one for the next
2506
+ * reconnect — their callers stay pending, and the identity guard re-applies on
2507
+ * retry via each record's persisted stamp.
2508
+ */
2509
+ private replaySequential;
2510
+ /**
2511
+ * Coalesce already-identity-gated writes for a single shard into ONE
2512
+ * `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
2513
+ * them to the shard DO, which replays each through its single-call dispatch, so
2514
+ * per-entry `mutationId` idempotency and in-order application are inherited from
2515
+ * the proven path. Per-slot demux mirrors {@link replaySequential}'s
2516
+ * classification: success confirms the optimistic layer against the echoed
2517
+ * `commitCursor`; a coded application verdict is terminal; a transient shard
2518
+ * failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
2519
+ * transport failure re-queues for the next reconnect (never dropping a durable
2520
+ * write). A whole-batch coded rejection (bad request / authorization denial the
2521
+ * server reached a verdict on) is terminal for every entry.
2522
+ *
2523
+ * Returns the writes that must be re-queued and `stop` — `true` when the whole
2524
+ * chunk failed at the transport level, so the caller leaves later chunks queued
2525
+ * rather than sending on. The caller re-queues once, in order, so requeuing is
2526
+ * NOT done here.
2527
+ */
2528
+ private replayBatched;
2529
+ /**
2530
+ * Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
2531
+ * in input order. Each slot's envelope classifies its write the same way
2532
+ * {@link replaySequential} does: a success confirms the optimistic layer
2533
+ * against the echoed `commitCursor`; a coded application verdict is terminal;
2534
+ * a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
2535
+ * server never returned is returned for the caller to re-queue.
2536
+ * @returns the writes that must be re-queued (transient slots), in input order
2537
+ */
2538
+ private settleReplayBatchSlots;
1596
2539
  }
1597
- export { ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, StreamHandle as E, FunctionReference as F, GlobalFacetResult as G, StreamIterable as H, SubscriptionCallback as I, SubscriptionRegistry as J, SubscriptionState as K, LunoraClient as L, MutationCallOptions as M, WorkflowInstanceDetail as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, WorkflowInstancePage as T, User as U, WorkflowInstanceStatus as V, WorkflowInstanceAction as W, WorkflowInstanceSummary as X, WorkflowStepDetail as Y, createLocalStore as Z, createStream as _, Unsubscribe as a, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ConnectionStatus as f, FunctionArgumentDescriptor as g, FunctionDescriptor as h, GlobalFacetValue as i, GlobalFilterClause as j, GlobalTableInfo as k, GlobalTablePage as l, LunoraClientOptions as m, OptimisticLocalStore as n, OptimisticUpdate as o, PersistedMutation as p, RpcEnvelope as q, RpcResponseBody as r, ScheduleRecord as s, SchedulerPoolStatus as t, SchedulerStatus as u, ServerMessage as v, ShardTrafficEntry as w, ShardTrafficResult as x, StorageListPage as y, StorageObject as z };
2540
+ export { StreamHandle as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, RpcEnvelope as E, FunctionReference as F, GlobalFacetResult as G, RpcResponseBody as H, ScheduleRecord as I, SchedulerPoolStatus as J, SchedulerStatus as K, LunoraClient as L, MutationCallOptions as M, ServerMessage as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerPokeEndMessage as T, User as U, ServerPokePartMessage as V, ServerPokeStartMessage as W, ShardTrafficEntry as X, ShardTrafficResult as Y, StorageListPage as Z, StorageObject as _, Unsubscribe as a, StreamIterable as a0, SubscriptionCallback as a1, SubscriptionRegistry as a2, SubscriptionState as a3, SyncWatermark as a4, WorkflowInstanceAction as a5, WorkflowInstanceDetail as a6, WorkflowInstancePage as a7, WorkflowInstanceStatus as a8, WorkflowInstanceSummary as a9, WorkflowStepDetail as aa, createLocalStore as ab, createStream as ac, getErrorCode as ad, getRetryAfterMs as ae, isConflictError as af, isForbiddenError as ag, isRateLimitedError as ah, isUnauthorizedError as ai, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, CachedQuery as f, ClientMessage as g, ClientShapeSubscribeMessage as h, ClientShapeUnsubscribeMessage as i, ConnectionStatus as j, FunctionArgumentDescriptor as k, FunctionDescriptor as l, GlobalFacetValue as m, GlobalFilterClause as n, GlobalTableInfo as o, GlobalTablePage as p, LunoraClientError as q, LunoraClientOptions as r, LunoraErrorCode as s, MutationSettledEvent as t, OptimisticLocalStore as u, OptimisticUpdate as v, OutboxMutation as w, OutboxSink as x, PersistedMutation as y, RowOp as z };