@lunora/client 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (32) hide show
  1. package/README.md +2 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/auth/index.d.mts +1 -1
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/index.d.mts +136 -5
  6. package/dist/index.d.ts +136 -5
  7. package/dist/index.mjs +8 -7
  8. package/dist/packem_shared/{LunoraClient-B00f7VUM.mjs → LunoraClient-6J-4BGWK.mjs} +744 -187
  9. package/dist/packem_shared/OfflineQueue-BI0FNNvc.mjs +1 -0
  10. package/dist/packem_shared/SubscriptionRegistry-Dn-7k7eo.mjs +1 -0
  11. package/dist/packem_shared/{createIndexedDbPersistence-CW82inU5.mjs → createInMemoryPersistence-DlFjWtOm.mjs} +13 -1
  12. package/dist/packem_shared/{createIndexedDbQueryCache-B1PQ9Twl.mjs → createInMemoryQueryCache-4AkTV38-.mjs} +13 -1
  13. package/dist/packem_shared/createLocalStore-IOur0jHF.mjs +1 -0
  14. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +31 -0
  15. package/dist/packem_shared/{createServerClient-DyFIHnWZ.mjs → createServerClient-CtklXrs2.mjs} +1 -1
  16. package/dist/packem_shared/local-store-BNgN3Dw3.mjs +111 -0
  17. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.mts → lunora-client.d-Dm1BYq19.d.mts} +652 -43
  18. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.ts → lunora-client.d-Dm1BYq19.d.ts} +652 -43
  19. package/dist/packem_shared/{OfflineQueue-D5p_QgF_.mjs → offline-queue-7Wc4onA0.mjs} +42 -5
  20. package/dist/packem_shared/{preload.d-dSaRMuhL.d.mts → preload.d-BfWzGJWV.d.mts} +1 -1
  21. package/dist/packem_shared/{preload.d-BoDmFqSG.d.ts → preload.d-Sp_Ef-_u.d.ts} +1 -1
  22. package/dist/packem_shared/subscription-C1Jy7HiF.mjs +55 -0
  23. package/dist/query/index.d.mts +2 -2
  24. package/dist/query/index.d.ts +2 -2
  25. package/dist/ssr/index.d.mts +3 -3
  26. package/dist/ssr/index.d.ts +3 -3
  27. package/dist/ssr/index.mjs +2 -2
  28. package/package.json +2 -2
  29. package/dist/packem_shared/SubscriptionRegistry-B-Qx_Gux.mjs +0 -26
  30. package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
  31. /package/dist/packem_shared/{createStream-BDkqO5PW.mjs → DEFAULT_MAX_BUFFER-BDkqO5PW.mjs} +0 -0
  32. /package/dist/packem_shared/{serializePreloaded-C0eJTY_W.mjs → deserializePreloaded-C0eJTY_W.mjs} +0 -0
@@ -98,6 +98,14 @@ interface PersistedMutation {
98
98
  */
99
99
  identity?: string | null;
100
100
  shardKey?: string;
101
+ /**
102
+ * App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
103
+ * On hydrate, a record whose `version` doesn't match the current one is dropped
104
+ * and purged rather than replayed — so a write persisted by an older deploy
105
+ * (with a now-changed function signature) can't replay against the new schema.
106
+ * Absent when no `persistenceVersion` is configured (no version gating).
107
+ */
108
+ version?: string;
101
109
  }
102
110
  /**
103
111
  * Durable store for the offline mutation queue. The default client keeps the
@@ -120,6 +128,40 @@ interface PersistenceAdapter {
120
128
  remove: (id: string) => Promise<void>;
121
129
  }
122
130
  /**
131
+ * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
132
+ * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
133
+ * the durable outbox needs to dedupe and watermark replays.
134
+ */
135
+ interface OutboxMutation {
136
+ args: Record<string, unknown>;
137
+ /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
138
+ clientId: string;
139
+ functionPath: string;
140
+ /** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
141
+ idempotencyKey: string;
142
+ /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
143
+ identity: string | null;
144
+ /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
145
+ mutationId: number;
146
+ shardKey?: string;
147
+ }
148
+ /**
149
+ * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
150
+ * the client delegates offline write durability + at-least-once replay to this
151
+ * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
152
+ * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
153
+ * backed by the TanStack `OfflineExecutor`); the interface itself is
154
+ * dependency-free so `@lunora/client` stays TanStack-free.
155
+ */
156
+ interface OutboxSink {
157
+ /**
158
+ * Persist and schedule a write for replay. Rejects with an
159
+ * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
160
+ * the caller can surface back-pressure to the issuing mutation.
161
+ */
162
+ enqueue: (mutation: OutboxMutation) => Promise<void>;
163
+ }
164
+ /**
123
165
  * One persisted query result in the durable read cache (Pillar 2). Keyed in the
124
166
  * store by `shardKey + functionPath + argsKey`; the record carries everything
125
167
  * needed to render offline on reload and to resume the live subscription.
@@ -148,6 +190,13 @@ interface CachedQuery {
148
190
  ts: number;
149
191
  /** The full query result last seen from the server. */
150
192
  value: unknown;
193
+ /**
194
+ * App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
195
+ * A cached value whose `version` doesn't match the current one is not hydrated —
196
+ * so a result of a now-changed shape from an older deploy can't render. Absent
197
+ * when no `persistenceVersion` is configured (no version gating).
198
+ */
199
+ version?: string;
151
200
  }
152
201
  /**
153
202
  * Durable store for the client read cache (Pillar 2): query results survive a
@@ -177,6 +226,16 @@ interface LunoraClientOptions {
177
226
  authBasePath?: string;
178
227
  bookmarkStorage?: BookmarkStorage;
179
228
  /**
229
+ * Stable per-client id backing the custom-mutator watermark. Sent on the
230
+ * `connect` envelope (so the server can scope this client's
231
+ * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
232
+ * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
233
+ * monotonic mutation id to form the idempotency key. The `@lunora/db` path
234
+ * persists a stable id alongside the outbox and passes it here; omit for the
235
+ * standalone client, which generates an ephemeral per-session id.
236
+ */
237
+ clientId?: string;
238
+ /**
180
239
  * Default app context sent in the `connect` envelope right after each socket
181
240
  * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
182
241
  * as `event.context`. A per-shard context registered via
@@ -184,6 +243,16 @@ interface LunoraClientOptions {
184
243
  * hook needs connection context.
185
244
  */
186
245
  connectionContext?: Record<string, unknown>;
246
+ /**
247
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
248
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
249
+ * worker that never upgrades — the client force-closes the socket and routes
250
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
251
+ * of leaving the live channel silently stuck on the browser's much longer
252
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
253
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
254
+ */
255
+ connectTimeoutMs?: number;
187
256
  fetch?: typeof fetch;
188
257
  /**
189
258
  * Interval (ms) between keepalive pings sent on each open subscription
@@ -194,14 +263,47 @@ interface LunoraClientOptions {
194
263
  */
195
264
  heartbeatIntervalMs?: number;
196
265
  offlineQueue?: OfflineQueueOptions;
197
- /** Durable store for the offline mutation queue; omit to keep it in memory. */
198
- persistence?: PersistenceAdapter;
199
266
  /**
200
- * Durable store for the read cache (Pillar 2). When supplied, query results
267
+ * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
268
+ * path wires `createExecutorOutboxSink`), offline mutations are delegated to
269
+ * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
270
+ * is bypassed, so a db app has exactly one durable write path. Omit for the
271
+ * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
272
+ */
273
+ outbox?: OutboxSink;
274
+ /**
275
+ * Durable store for the offline mutation queue. Tri-state — an explicit
276
+ * {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
277
+ * in memory, lost on reload); omitted (the default) auto-probes a durable
278
+ * IndexedDB store when the `indexedDB` global is present (browsers), otherwise
279
+ * in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
280
+ * environments that can persist do. Pass `createAsyncStoragePersistence()` on
281
+ * React Native.
282
+ */
283
+ persistence?: false | PersistenceAdapter;
284
+ /**
285
+ * App/schema version stamped onto every persisted queued write and cached
286
+ * read. Bump it on a breaking change to a function signature or query shape:
287
+ * on the next boot, persisted writes / cached reads stamped with a different
288
+ * version are dropped (and purged) rather than replayed / hydrated against the
289
+ * new schema. Omit to disable version gating (records are never invalidated by
290
+ * version).
291
+ *
292
+ * **Adoption is itself an invalidation event:** records written before you set
293
+ * `persistenceVersion` carry no version, so the first boot after enabling it
294
+ * purges all currently-queued offline writes (and cached reads) as stale. Adopt
295
+ * it on a build where that clean slate is acceptable — typically the same
296
+ * breaking deploy you're protecting against — not purely speculatively.
297
+ */
298
+ persistenceVersion?: string;
299
+ /**
300
+ * Durable store for the read cache (Pillar 2). When active, query results
201
301
  * are persisted as their subscriptions advance and hydrated on construction
202
302
  * 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.
303
+ * the live subscription from the persisted cursor. Tri-state an explicit
304
+ * {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
305
+ * memory only); omitted (the default) auto-probes IndexedDB exactly like
306
+ * {@link LunoraClientOptions.persistence}.
205
307
  */
206
308
  queryCache?: QueryCacheAdapter | false;
207
309
  reconnect?: ReconnectOptions;
@@ -222,17 +324,46 @@ interface LunoraClientOptions {
222
324
  /** Wire envelope sent on `POST /_lunora/rpc`. */
223
325
  interface RpcEnvelope {
224
326
  args?: Record<string, unknown>;
327
+ /**
328
+ * Stable per-client identifier (custom-mutator push path). Pairs with
329
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
330
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
331
+ */
332
+ clientId?: string;
225
333
  functionPath: string;
334
+ /**
335
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
336
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
337
+ * `client.mutation` calls.
338
+ */
339
+ idempotencyKey?: string;
340
+ /**
341
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
342
+ * server-side per-client watermark: `id &lt;= watermark` is a replay (skipped),
343
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
344
+ * batch so the client resends from `watermark + 1`. Absent on plain
345
+ * `client.mutation` calls.
346
+ */
347
+ mutationId?: number;
226
348
  shardKey?: string;
227
349
  }
228
- /** Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). */
350
+ /**
351
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
352
+ * watermarked custom-mutator push additionally carries `lastMutationId` — the
353
+ * highest per-client sequence the DO has applied — which the client uses to keep
354
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
355
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
356
+ * committed at — which gates the drop of a per-call optimistic layer.
357
+ */
229
358
  type RpcResponseBody = {
230
- result: unknown;
231
- } | {
232
359
  error: {
233
360
  code: string;
234
361
  message: string;
235
362
  };
363
+ } | {
364
+ commitCursor?: number;
365
+ lastMutationId?: number;
366
+ result: unknown;
236
367
  };
237
368
  /** Subscription protocol — client → server. */
238
369
  interface ClientSubscribeMessage {
@@ -265,10 +396,51 @@ interface ClientUnsubscribeMessage {
265
396
  * `onDisconnect` when the socket drops.
266
397
  */
267
398
  interface ClientConnectMessage {
399
+ /**
400
+ * Stable per-client id (persisted alongside the outbox). Lets the server
401
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
402
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
403
+ * use custom mutators.
404
+ */
405
+ clientId?: string;
268
406
  context?: Record<string, unknown>;
269
407
  id: string;
270
408
  type: "connect";
271
409
  }
410
+ /**
411
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
412
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
413
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
414
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
415
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
416
+ * echoed as `shapeId` on every poke part.
417
+ */
418
+ interface ClientShapeSubscribeMessage {
419
+ id: string;
420
+ shape: {
421
+ args?: Record<string, unknown>;
422
+ name: string;
423
+ };
424
+ /**
425
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
426
+ * applied for this shape). When absent or below the server's retained floor
427
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
428
+ * delta.
429
+ */
430
+ sinceCheckpoint?: number;
431
+ /**
432
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
433
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
434
+ * the cursor is numerically in range.
435
+ */
436
+ sinceEpoch?: string;
437
+ type: "shape_subscribe";
438
+ }
439
+ /** Cancel a shape subscription started with the same `id`. */
440
+ interface ClientShapeUnsubscribeMessage {
441
+ id: string;
442
+ type: "shape_unsubscribe";
443
+ }
272
444
  interface ClientAckMessage {
273
445
  id: string;
274
446
  type: "ack";
@@ -309,7 +481,7 @@ interface ClientWhisperMessage {
309
481
  topic: string;
310
482
  type: "whisper";
311
483
  }
312
- type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
484
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
313
485
  /** Subscription protocol — server → client. */
314
486
  interface ServerDataMessage {
315
487
  /**
@@ -323,6 +495,13 @@ interface ServerDataMessage {
323
495
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
324
496
  epoch?: string;
325
497
  id: string;
498
+ /**
499
+ * The highest custom-mutator `mutationId` from this client the server has
500
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
501
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
502
+ * the matching optimistic overlay. Absent on shards without custom mutators.
503
+ */
504
+ lastMutationId?: number;
326
505
  type: "data" | "delta";
327
506
  }
328
507
  /**
@@ -336,8 +515,33 @@ interface ServerResumeMessage {
336
515
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
337
516
  epoch?: string;
338
517
  id: string;
518
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
519
+ lastMutationId?: number;
339
520
  type: "resume";
340
521
  }
522
+ /**
523
+ * Settled acknowledgement for a **list** subscription: a write touched one of
524
+ * the subscription's read tables but produced a byte-identical result, so the
525
+ * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
526
+ * client (one that announced a `clientId`, hence has a server-side
527
+ * `__client_watermark`) so its optimistic list overlay drops even when no data
528
+ * frame arrives. Plain `useQuery` subscribers never receive it, and an older
529
+ * client safely ignores the unknown frame.
530
+ */
531
+ interface ServerSettledMessage {
532
+ cursor?: number;
533
+ /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
534
+ epoch?: string;
535
+ id: string;
536
+ /**
537
+ * The highest custom-mutator `mutationId` from this client the server has
538
+ * now applied (the per-client `__client_watermark`). Forwarded to a
539
+ * collection's `onCheckpoint` so it can drop the overlay for the confirmed
540
+ * write whose result didn't change this list.
541
+ */
542
+ lastMutationId?: number;
543
+ type: "settled";
544
+ }
341
545
  interface ServerErrorMessage {
342
546
  error?: unknown;
343
547
  id?: string;
@@ -370,7 +574,65 @@ interface ServerWhisperMessage {
370
574
  topic: string;
371
575
  type: "whisper";
372
576
  }
373
- type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerResumeMessage | ServerWhisperMessage;
577
+ /**
578
+ * One row-level change in a shape's replication stream — the wire form of the
579
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
580
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
581
+ * the removed row by `key` alone. The client applies these to its local
582
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
583
+ * never had in this shape).
584
+ */
585
+ interface RowOp {
586
+ /** Row primary key (`_id`). */
587
+ key: string;
588
+ op: "delete" | "insert" | "update";
589
+ /** Logical table the row belongs to. */
590
+ table: string;
591
+ /** Post-image document for insert/update; absent on delete. */
592
+ value?: Record<string, unknown>;
593
+ }
594
+ /**
595
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
596
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
597
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
598
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
599
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
600
+ */
601
+ interface ServerPokeStartMessage {
602
+ /** The checkpoint the client's view is expected to be at before this poke applies (for ordering/gap detection). */
603
+ baseCheckpoint?: number;
604
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
605
+ epoch?: string;
606
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
607
+ pokeId: string;
608
+ type: "pokeStart";
609
+ }
610
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
611
+ interface ServerPokePartMessage {
612
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
613
+ lastMutationId?: number;
614
+ pokeId: string;
615
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
616
+ rowsPatch: RowOp[];
617
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
618
+ shapeId: string;
619
+ type: "pokePart";
620
+ }
621
+ /**
622
+ * Closes a poke: the client commits the buffered parts atomically and advances
623
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
624
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
625
+ * the next reconnect.
626
+ */
627
+ interface ServerPokeEndMessage {
628
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
629
+ checkpoint?: number;
630
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
631
+ epoch?: string;
632
+ pokeId: string;
633
+ type: "pokeEnd";
634
+ }
635
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
374
636
  /**
375
637
  * The authenticated user as exposed client-side, mirroring better-auth's
376
638
  * `user` row (the `user` field of the `get-session` response). Kept minimal
@@ -582,6 +844,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
844
  }
583
845
  /** A page of workflow instances. */
584
846
  interface WorkflowInstancePage {
847
+ /**
848
+ * Whether workflow inspection is configured on the worker (a Cloudflare
849
+ * account id + API token). `false` when the admin proxy reports it can't
850
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
851
+ * caller render a "set credentials" state without a failed request.
852
+ */
853
+ configured?: boolean;
585
854
  instances: WorkflowInstanceSummary[];
586
855
  page: number;
587
856
  perPage: number;
@@ -594,6 +863,23 @@ interface SubscriptionError {
594
863
  message: string;
595
864
  }
596
865
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
866
+ /**
867
+ * One active per-call optimistic transform layered onto a subscription. The
868
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
869
+ * folded through every layer's `transform`, in order — so an incoming server
870
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
871
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
872
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
873
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
874
+ * mutation's response, and stays `undefined` while the write is still queued/
875
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
876
+ */
877
+ interface OptimisticLayer {
878
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
879
+ commitCursor?: number;
880
+ readonly id: symbol;
881
+ readonly transform: (current: unknown) => unknown;
882
+ }
597
883
  interface SubscriptionState {
598
884
  /** True once the server has acked the subscription on the current socket. */
599
885
  acked: boolean;
@@ -605,13 +891,52 @@ interface SubscriptionState {
605
891
  */
606
892
  readonly argsKey: string;
607
893
  readonly callbacks: Set<SubscriptionCallback>;
894
+ /**
895
+ * Notified when a `settled` frame advances this subscription's watermark — a
896
+ * write touched the subscription's tables but the result was byte-identical,
897
+ * so the server suppressed the data frame. A `@lunora/db` list collection
898
+ * uses this to drop the optimistic overlay for the confirmed write.
899
+ *
900
+ * A SET (not a single slot) because `SubscriptionState` is SHARED across
901
+ * every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
902
+ * collection may subscribe to a query a plain `useQuery` already opened, so
903
+ * each subscriber registers its own callback (mirroring `callbacks` /
904
+ * `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
905
+ * `useQuery` consumers register nothing, leaving the set empty.
906
+ */
907
+ readonly checkpointCallbacks: Set<(watermark: {
908
+ checkpoint?: number;
909
+ mutationId?: number;
910
+ }) => void>;
608
911
  /** Notified when the server rejects this subscription (e.g. admin auth). */
609
912
  readonly errorCallbacks: Set<SubscriptionErrorCallback>;
610
913
  readonly fn: FunctionReference;
611
914
  readonly id: string;
915
+ /**
916
+ * The highest custom-mutator `mutationId` from this client the server has
917
+ * applied, captured from the last `settled` frame (the suppressed-list-frame
918
+ * watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
919
+ * Absent until a `settled` frame arrives.
920
+ */
921
+ lastMutationId?: number;
612
922
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
613
923
  lastValue: unknown;
614
924
  /**
925
+ * Active per-call optimistic layers, in application order (see
926
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
927
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
928
+ * exactly and behaviour is identical to a plain server-value assignment.
929
+ */
930
+ optimisticLayers: OptimisticLayer[];
931
+ /**
932
+ * The authoritative server value the optimistic layers fold onto — the value
933
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
934
+ * are active; diverges only while a per-call optimistic write is pending. A
935
+ * server frame updates this (and re-folds the layers); the durable read cache
936
+ * persists this, never the optimistic overlay.
937
+ */
938
+ serverBase: unknown;
939
+ /**
615
940
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
616
941
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
617
942
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -627,18 +952,15 @@ interface SubscriptionState {
627
952
  * until the first epoch-stamped frame arrives.
628
953
  */
629
954
  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
955
  readonly shardKey?: string;
637
956
  }
638
957
  /**
639
958
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
959
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
960
+ * single server-side registration. Args are stably encoded (keys sorted at every
961
+ * depth) so two structurally-equal arg records constructed with a different key
962
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
963
+ * duplicate subscription.
642
964
  */
643
965
  declare class SubscriptionRegistry {
644
966
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -656,10 +978,10 @@ declare class SubscriptionRegistry {
656
978
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
657
979
  *
658
980
  * `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
981
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
982
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
983
+ * settles together confirmed on the mutation's commit cursor, or rolled back
984
+ * on failure — the same per-subscription layer machinery the single-query
663
985
  * per-call `optimistic` transform uses, generalized to N queries.
664
986
  */
665
987
  interface OptimisticLocalStore {
@@ -689,13 +1011,17 @@ interface OptimisticLocalStore {
689
1011
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
690
1012
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
691
1013
  /**
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.
1014
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
1015
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
1016
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
1017
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
1018
+ * query not merged) and drops gaplessly on the mutation's commit cursor, like
1019
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
1020
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
1021
+ * so the caller settles the whole batch when the mutation does.
697
1022
  */
698
- declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, write: (state: SubscriptionState, next: unknown) => () => void, stableStringify: (value: unknown) => string) => {
1023
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, stableStringify: (value: unknown) => string) => {
1024
+ confirms: ((commitCursor: number | undefined) => void)[];
699
1025
  rollbacks: (() => void)[];
700
1026
  store: OptimisticLocalStore;
701
1027
  };
@@ -753,6 +1079,41 @@ declare const createStream: <T>(options: {
753
1079
  */
754
1080
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
755
1081
  /**
1082
+ * Terminal verdict for a mutation that passed through the offline queue,
1083
+ * delivered to {@link LunoraClient.onMutationSettled}.
1084
+ *
1085
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1086
+ * original caller can await, and which no longer exists after a reload — this
1087
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1088
+ * including writes restored from durable storage in a later session. It is the
1089
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1090
+ * instead of silently dropping a rolled-back optimistic row.
1091
+ *
1092
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1093
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1094
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1095
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1096
+ * "the caller already saw this" apart from "nothing else will report this".
1097
+ */
1098
+ interface MutationSettledEvent {
1099
+ /** The write's args, so a listener can describe or re-offer the change. */
1100
+ readonly args: Record<string, unknown>;
1101
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
1102
+ readonly code?: string;
1103
+ /** The rejection error on `status: "rejected"`. */
1104
+ readonly error?: unknown;
1105
+ /** The `&lt;file>:&lt;function>` reference of the mutation. */
1106
+ readonly functionPath: string;
1107
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1108
+ readonly hadAwaiter: boolean;
1109
+ /** The write's stable id (idempotency key / queue id). */
1110
+ readonly id: string;
1111
+ /** Shard the write targeted, if any. */
1112
+ readonly shardKey?: string;
1113
+ /** Terminal outcome. */
1114
+ readonly status: "committed" | "rejected";
1115
+ }
1116
+ /**
756
1117
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
757
1118
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
758
1119
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -760,6 +1121,13 @@ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
760
1121
  * re-declaring it.
761
1122
  */
762
1123
  interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
1124
+ /**
1125
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
1126
+ * durable outbox replay a committed-but-unacked write under its *original* key
1127
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
1128
+ * each then gets a fresh key.
1129
+ */
1130
+ mutationId?: string;
763
1131
  optimistic?: (current: TCurrent | undefined) => TValue;
764
1132
  /**
765
1133
  * Convex-parity multi-query optimistic update. Receives an
@@ -770,6 +1138,19 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
770
1138
  optimisticUpdate?: OptimisticUpdate<TArgs>;
771
1139
  shardKey?: string;
772
1140
  }
1141
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
1142
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
1143
+ /**
1144
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
1145
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
1146
+ * echoed for this client. A `@lunora/db` collection feeds these into its
1147
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
1148
+ * rows have landed.
1149
+ */
1150
+ interface SyncWatermark {
1151
+ checkpoint?: number;
1152
+ mutationId?: number;
1153
+ }
773
1154
  /**
774
1155
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
775
1156
  * a single multiplexed WebSocket.
@@ -778,6 +1159,8 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
778
1159
  * see the package README for the wire protocol.
779
1160
  */
780
1161
  declare class LunoraClient {
1162
+ /** 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. */
1163
+ private static readonly MAX_POKE_BUFFERS;
781
1164
  readonly url: string;
782
1165
  readonly wsUrl: string;
783
1166
  private wsToken;
@@ -787,11 +1170,36 @@ declare class LunoraClient {
787
1170
  private readonly WebSocketImpl;
788
1171
  private readonly bookmark;
789
1172
  private readonly reconnectOptions;
1173
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
1174
+ private readonly connectTimeoutMs;
790
1175
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
1176
  private readonly heartbeatIntervalMs;
792
1177
  private readonly offlineQueue;
1178
+ /**
1179
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
1180
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
1181
+ * is bypassed, so a db app has exactly one durable write path.
1182
+ */
1183
+ private readonly outbox;
1184
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
1185
+ private readonly clientId;
1186
+ /**
1187
+ * Highest custom-mutator watermark the server has echoed for this client,
1188
+ * keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
1189
+ * `__client_watermark` per shard. `callMutator` bumps it from every
1190
+ * ack; the `@lunora/db` mutator runtime seeds its `clientSeq` generator from
1191
+ * it so a reload (which resets the in-memory counter) never reissues a stale
1192
+ * sequence the server would silently swallow as a replay.
1193
+ */
1194
+ private readonly clientWatermarks;
1195
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1196
+ private outboxMutationCounter;
793
1197
  private readonly onPersistenceError;
794
1198
  private readonly persistence;
1199
+ /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
1200
+ private readonly persistenceVersion;
1201
+ /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
1202
+ private outboxLeaderRelease;
795
1203
  /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
796
1204
  private readonly queryCache;
797
1205
  /**
@@ -835,6 +1243,14 @@ declare class LunoraClient {
835
1243
  private readonly connectionContextHolders;
836
1244
  private authToken;
837
1245
  /**
1246
+ * Optional STABLE identity subject (a user id), the basis of the offline-queue
1247
+ * identity stamp when supplied. Keeps a same-user token *refresh* from looking
1248
+ * like an identity change (which would discard queued writes). `undefined` =
1249
+ * not supplied, so identity falls back to a hash of the raw token. See
1250
+ * `setAuthToken` / `identityFingerprint`.
1251
+ */
1252
+ private authSubject;
1253
+ /**
838
1254
  * Identity stamp recorded against each queued offline mutation, keyed by
839
1255
  * the queue-assigned mutation id. Captured at enqueue from the auth token
840
1256
  * in effect at the time, and re-checked at flush so a queued write can
@@ -849,6 +1265,10 @@ declare class LunoraClient {
849
1265
  private readonly statusListeners;
850
1266
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
851
1267
  private readonly tokenExpiredListeners;
1268
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1269
+ private readonly mutationSettledListeners;
1270
+ /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
1271
+ private readonly pendingChangeListeners;
852
1272
  /**
853
1273
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
854
1274
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -866,20 +1286,78 @@ declare class LunoraClient {
866
1286
  * calls `.cancel()` or the iterator is garbage-collected.
867
1287
  */
868
1288
  private readonly streams;
1289
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
1290
+ private readonly shapeSubscriptions;
1291
+ /** In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by `pokeId`. */
1292
+ private readonly pokeBuffers;
1293
+ private nextShapeId;
869
1294
  constructor(options: LunoraClientOptions);
870
1295
  /**
871
1296
  * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
872
1297
  * {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
873
1298
  * sync across all mounted instances.
874
1299
  *
1300
+ * Pass a STABLE `subject` (the user id) to key the offline-queue identity on
1301
+ * it instead of the token bytes, so a token *refresh* (same user, new JWT)
1302
+ * doesn't read as an identity change and discard queued writes. The subject is
1303
+ * **sticky**: a later call that omits it (or passes `undefined`) keeps the
1304
+ * established subject — so `setAuthToken(refreshedToken)` after a prior
1305
+ * `setAuthToken(token, user.id)` retains the identity. Pass `null` to clear it
1306
+ * (an explicit sign-out). Establishing the subject for the first time on an
1307
+ * UNCHANGED token (e.g. the user id resolves a tick after the token was set)
1308
+ * re-stamps any in-flight queued writes rather than dropping them — same
1309
+ * credential, just a more stable label. A real user switch (the token AND
1310
+ * subject both change) still drops the previous user's writes.
1311
+ *
875
1312
  * Does NOT update the WebSocket auth — the WS token is fixed at upgrade
876
1313
  * time and lives in the URL. To refresh live WS auth, call
877
1314
  * {@link setWsToken} explicitly, which closes existing shard sockets to
878
1315
  * force a reconnect with the new credential.
879
1316
  */
880
- setAuthToken(token: string | null): void;
1317
+ setAuthToken(token: string | null, subject?: string | null): void;
881
1318
  getAuthToken(): string | null;
882
1319
  /**
1320
+ * The current identity fingerprint (the same stamp queued offline writes
1321
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
1322
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
1323
+ * can drop a persisted write whose captured `identity` no longer matches the
1324
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
1325
+ */
1326
+ currentIdentity(): string | null;
1327
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
1328
+ clientIdentifier(): string;
1329
+ /**
1330
+ * The highest custom-mutator watermark the server has echoed for this client
1331
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
1332
+ * its `clientSeq` generator from this so a reload never reissues a sequence
1333
+ * the server has already applied (which it would swallow as a replay, silently
1334
+ * dropping the write).
1335
+ */
1336
+ confirmedMutationWatermark(shardKey?: string): number;
1337
+ /**
1338
+ * Push a custom mutator to its authoritative server impl over the watermark
1339
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
1340
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
1341
+ * client's `__client_watermark`.
1342
+ *
1343
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
1344
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
1345
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
1346
+ * A `false` verdict tells the caller to reissue above the now-known watermark
1347
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
1348
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
1349
+ *
1350
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
1351
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
1352
+ */
1353
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
1354
+ clientSeq?: number;
1355
+ shardKey?: string;
1356
+ }): Promise<{
1357
+ applied: boolean;
1358
+ result: unknown;
1359
+ }>;
1360
+ /**
883
1361
  * Subscribe to auth-token changes. Returns an unsubscribe function. The
884
1362
  * listener is NOT invoked on registration — use {@link getAuthToken} for
885
1363
  * the current value.
@@ -990,6 +1468,32 @@ declare class LunoraClient {
990
1468
  * unsubscribe function.
991
1469
  */
992
1470
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
1471
+ /**
1472
+ * Number of offline writes waiting in the built-in queue to be sent — the
1473
+ * depth for a "N changes waiting to sync" indicator. Counts writes that are
1474
+ * queued (offline / mid-reconnect), not ones already in flight on the wire.
1475
+ * A `@lunora/db` app whose writes ride the unified outbox should read
1476
+ * `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
1477
+ */
1478
+ pendingCount(): number;
1479
+ /**
1480
+ * Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
1481
+ * with the current count, then whenever the queue depth changes (a write is
1482
+ * enqueued, flushed, or discarded). Returns an unsubscribe function.
1483
+ */
1484
+ onPendingChange(listener: (pending: number) => void): Unsubscribe;
1485
+ /**
1486
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
1487
+ * fires once per queued write that commits or is rejected — including a write
1488
+ * restored from durable storage after a reload, whose original `mutation()`
1489
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
1490
+ * evicts on overflow or discards on an identity change. This is the durable
1491
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
1492
+ * mutation that never queued still surfaces through the Promise `mutation()`
1493
+ * returns. The listener is NOT invoked on registration. Returns an
1494
+ * unsubscribe function. See {@link MutationSettledEvent}.
1495
+ */
1496
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
993
1497
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
994
1498
  shardKey?: string;
995
1499
  }): Promise<ReturnOf<F>>;
@@ -1069,8 +1573,12 @@ declare class LunoraClient {
1069
1573
  * List a workflow's instances via the admin Workflows proxy
1070
1574
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1575
  * 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.
1576
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1577
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1578
+ * false }` sentinel, so the result resolves with `configured === false` and an
1579
+ * empty `instances` list — callers should branch on that flag rather than
1580
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1581
+ * `name` is the deployed workflow name.
1074
1582
  */
1075
1583
  listWorkflowInstances(options: {
1076
1584
  name: string;
@@ -1400,6 +1908,27 @@ declare class LunoraClient {
1400
1908
  userId?: string;
1401
1909
  }): Promise<AuthPage<AuthSession>>;
1402
1910
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1911
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1912
+ onError?: SubscriptionErrorCallback;
1913
+ shardKey?: string;
1914
+ }): Unsubscribe;
1915
+ /**
1916
+ * Subscribe to a declarative **shape** — server-side partial replication
1917
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
1918
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
1919
+ * validated `args` (never a `where` the client could forge), the server seeds
1920
+ * the current membership as an insert-poke and streams live membership diffs.
1921
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
1922
+ *
1923
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1924
+ * (name, args): the server resolves them under the socket's verified identity,
1925
+ * so every call gets its own id + view. The returned function unsubscribes.
1926
+ */
1927
+ subscribeShape(shape: {
1928
+ args?: Record<string, unknown>;
1929
+ name: string;
1930
+ }, callback: ShapeCallback, options?: {
1931
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1403
1932
  onError?: SubscriptionErrorCallback;
1404
1933
  shardKey?: string;
1405
1934
  }): Unsubscribe;
@@ -1427,12 +1956,35 @@ declare class LunoraClient {
1427
1956
  }): StreamIterable<ReturnOf<F>>;
1428
1957
  close(): void;
1429
1958
  /**
1959
+ * Persist a mutation that can't go out on the wire right now (offline, or
1960
+ * mid-reconnect after a prior connect). The optimistic update has already
1961
+ * been applied by `mutation`; this only chooses the durable write path and
1962
+ * rolls the optimistic write back if persistence is rejected.
1963
+ *
1964
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
1965
+ * owns persistence + at-least-once replay, so we delegate and return
1966
+ * optimistically (confirmation rides the synced view). Otherwise the
1967
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
1968
+ */
1969
+ private enqueueOfflineMutation;
1970
+ /**
1430
1971
  * Restore offline mutations persisted in a prior session and open a socket
1431
1972
  * for each shard they target so they flush once the WS reconnects. Failures
1432
1973
  * are swallowed — a broken durable store must not stop the client booting.
1433
1974
  */
1434
1975
  private hydratePersistedQueue;
1435
1976
  /**
1977
+ * Re-queue the durable offline writes — but only as the multi-tab LEADER. The
1978
+ * persisted queue is shared across a profile's tabs; without coordination
1979
+ * every tab would re-queue and replay the same writes (correct only because
1980
+ * the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
1981
+ * exactly one tab hydrate; it holds the lock for its lifetime, so when it
1982
+ * closes another tab acquires the lock and takes over. Falls back to
1983
+ * unconditional hydration where Web Locks are unavailable (React Native, older
1984
+ * browsers, SSR) — single-context there, so no coordination is needed.
1985
+ */
1986
+ private hydrateAsOutboxLeader;
1987
+ /**
1436
1988
  * Load every cached query into {@link hydratedQueryCache} so the next
1437
1989
  * `subscribe()` for each key seeds its initial value off disk. A
1438
1990
  * subscription created before this resolves simply misses the cache (it
@@ -1462,21 +2014,37 @@ declare class LunoraClient {
1462
2014
  /** Recompute the aggregate status and notify listeners if it changed. */
1463
2015
  private emitConnectionStatus;
1464
2016
  /**
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).
2017
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
2018
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
2019
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
2020
+ * is unreachable present only to satisfy the optional queue-id type.
2021
+ */
2022
+ private emitItemSettled;
2023
+ /**
2024
+ * Apply an optimistic update to the subscription that matches the mutation's
2025
+ * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
2026
+ * invoke if the mutation later fails.
2027
+ *
2028
+ * The registry is already indexed by exactly this triple via
2029
+ * `SubscriptionRegistry.key`, so at most one subscription can match. A direct
2030
+ * O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
2031
+ *
2032
+ * `shardKey` normalization: both `undefined` and `""` map to the empty string
2033
+ * inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
2034
+ * a shardKey correctly matches a subscription registered without one regardless
2035
+ * of whether the caller passed `undefined` or omitted the field.
1470
2036
  */
1471
2037
  private applyOptimisticUpdates;
1472
2038
  /**
1473
2039
  * 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.
2040
+ * to the live subscription registry. Each `setQuery` registers a constant
2041
+ * optimistic LAYER on its target subscription (via the same engine the
2042
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
2043
+ * incoming deltas and drops gaplessly on its commit cursorits `confirm` /
2044
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
2045
+ * callback unwinds its own partial writes LIFO over just the rollbacks it
2046
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
2047
+ * mutation or leave a partial patch live.
1480
2048
  */
1481
2049
  private applyOptimisticUpdate;
1482
2050
  private getConnection;
@@ -1523,6 +2091,13 @@ declare class LunoraClient {
1523
2091
  * to the lifecycle dispatch.
1524
2092
  */
1525
2093
  private sendConnectEnvelope;
2094
+ /**
2095
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
2096
+ * socket. Each frame carries the shape's last applied checkpoint, so the
2097
+ * server resumes from it — or re-seeds when the cursor fell below CDC
2098
+ * retention or the epoch forked.
2099
+ */
2100
+ private resendShapeSubscriptions;
1526
2101
  private ensureSocket;
1527
2102
  private handleDisconnect;
1528
2103
  /**
@@ -1538,8 +2113,14 @@ declare class LunoraClient {
1538
2113
  /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
1539
2114
  private markShardPendingAck;
1540
2115
  private sendSubscribeIfOpen;
2116
+ private sendShapeSubscribeIfOpen;
1541
2117
  private handleServerMessage;
1542
2118
  private handleErrorMessage;
2119
+ private handlePokeStart;
2120
+ private handlePokePart;
2121
+ private handlePokeEnd;
2122
+ /** Materialize a shape's keyed view to an array and invoke its callbacks. */
2123
+ private emitShapeRows;
1543
2124
  private handleDataMessage;
1544
2125
  /**
1545
2126
  * Handle a `resume` frame (Pillar 1b): the server proved nothing the
@@ -1551,6 +2132,25 @@ declare class LunoraClient {
1551
2132
  */
1552
2133
  private handleResumeMessage;
1553
2134
  /**
2135
+ * Handle a `settled` frame: a write touched one of this subscription's read
2136
+ * tables but produced a byte-identical result, so the server suppressed the
2137
+ * data frame. Like {@link handleResumeMessage} the value didn't change — we
2138
+ * advance the resume position and re-persist — but we ALSO surface the echoed
2139
+ * custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
2140
+ * collection drops the optimistic overlay for the confirmed write (otherwise
2141
+ * its checkpoint gate, fed only by data frames, would hang forever). Sent
2142
+ * only to custom-mutator clients; plain `useQuery` subscribers leave
2143
+ * `onCheckpoint` unset and this is a near no-op.
2144
+ */
2145
+ private handleSettledMessage;
2146
+ /**
2147
+ * Mark `state` acked and, when the frame carries a newer cursor/epoch than
2148
+ * the cached position, advance the resume watermark and re-persist. Shared by
2149
+ * the `resume` and `settled` frame handlers — both acknowledge "nothing the
2150
+ * client must re-render changed, but the resume position may have moved".
2151
+ */
2152
+ private ackAndAdvanceCursor;
2153
+ /**
1554
2154
  * Resolve the value to publish for a `data`/`delta` frame.
1555
2155
  *
1556
2156
  * A `data` frame is an authoritative snapshot (the server re-execution path)
@@ -1586,6 +2186,15 @@ declare class LunoraClient {
1586
2186
  */
1587
2187
  private rejectQueuedForIdentityChange;
1588
2188
  /**
2189
+ * Migrate every live identity stamp from `from` to `to` — used when the auth
2190
+ * identity label changes but the underlying credential (token) does NOT, e.g.
2191
+ * the user id resolves a tick after the token was set. The in-memory
2192
+ * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2193
+ * keeps the in-flight writes replayable under the new (more stable) identity
2194
+ * instead of the flush guard discarding them as a mismatch.
2195
+ */
2196
+ private restampQueuedIdentity;
2197
+ /**
1589
2198
  * Drop the durable read cache on an identity change so a cached value stamped
1590
2199
  * under the previous identity can never hydrate into a new session. Clears
1591
2200
  * the in-flight write batch and the not-yet-consumed hydrated entries too;
@@ -1594,4 +2203,4 @@ declare class LunoraClient {
1594
2203
  private clearQueryCacheForIdentityChange;
1595
2204
  private flushOfflineQueue;
1596
2205
  }
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 };
2206
+ export { SubscriptionState as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, SchedulerStatus as E, FunctionReference as F, GlobalFacetResult as G, ServerMessage as H, ServerPokeEndMessage as I, ServerPokePartMessage as J, ServerPokeStartMessage as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficEntry as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ShardTrafficResult as T, User as U, StorageListPage as V, StorageObject as W, StreamHandle as X, StreamIterable as Y, SubscriptionCallback as Z, SubscriptionRegistry as _, Unsubscribe as a, SyncWatermark as a0, WorkflowInstanceAction as a1, WorkflowInstanceDetail as a2, WorkflowInstancePage as a3, WorkflowInstanceStatus as a4, WorkflowInstanceSummary as a5, WorkflowStepDetail as a6, createLocalStore as a7, createStream as a8, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, MutationSettledEvent as p, OptimisticLocalStore as q, OptimisticUpdate as r, OutboxMutation as s, OutboxSink as t, PersistedMutation as u, RowOp as v, RpcEnvelope as w, RpcResponseBody as x, ScheduleRecord as y, SchedulerPoolStatus as z };