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

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 +109 -5
  6. package/dist/index.d.ts +109 -5
  7. package/dist/index.mjs +8 -7
  8. package/dist/packem_shared/{LunoraClient-B00f7VUM.mjs → LunoraClient-CgZ6FhKP.mjs} +741 -185
  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/createLocalStore-IOur0jHF.mjs +1 -0
  12. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +31 -0
  13. package/dist/packem_shared/{createServerClient-DyFIHnWZ.mjs → createServerClient-BxkNcRlR.mjs} +1 -1
  14. package/dist/packem_shared/local-store-BNgN3Dw3.mjs +111 -0
  15. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.mts → lunora-client.d-B5vWSgvD.d.mts} +637 -38
  16. package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.ts → lunora-client.d-B5vWSgvD.d.ts} +637 -38
  17. package/dist/packem_shared/{OfflineQueue-D5p_QgF_.mjs → offline-queue-7Wc4onA0.mjs} +42 -5
  18. package/dist/packem_shared/{preload.d-dSaRMuhL.d.mts → preload.d-3XJD-2hM.d.mts} +1 -1
  19. package/dist/packem_shared/{preload.d-BoDmFqSG.d.ts → preload.d-CKZR675M.d.ts} +1 -1
  20. package/dist/packem_shared/subscription-C1Jy7HiF.mjs +55 -0
  21. package/dist/query/index.d.mts +2 -2
  22. package/dist/query/index.d.ts +2 -2
  23. package/dist/ssr/index.d.mts +3 -3
  24. package/dist/ssr/index.d.ts +3 -3
  25. package/dist/ssr/index.mjs +2 -2
  26. package/package.json +2 -2
  27. package/dist/packem_shared/SubscriptionRegistry-B-Qx_Gux.mjs +0 -26
  28. package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
  29. /package/dist/packem_shared/{createStream-BDkqO5PW.mjs → DEFAULT_MAX_BUFFER-BDkqO5PW.mjs} +0 -0
  30. /package/dist/packem_shared/{createIndexedDbPersistence-CW82inU5.mjs → createInMemoryPersistence-CW82inU5.mjs} +0 -0
  31. /package/dist/packem_shared/{createIndexedDbQueryCache-B1PQ9Twl.mjs → createInMemoryQueryCache-B1PQ9Twl.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,9 +263,32 @@ interface LunoraClientOptions {
194
263
  */
195
264
  heartbeatIntervalMs?: number;
196
265
  offlineQueue?: OfflineQueueOptions;
266
+ /**
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;
197
274
  /** Durable store for the offline mutation queue; omit to keep it in memory. */
198
275
  persistence?: PersistenceAdapter;
199
276
  /**
277
+ * App/schema version stamped onto every persisted queued write and cached
278
+ * read. Bump it on a breaking change to a function signature or query shape:
279
+ * on the next boot, persisted writes / cached reads stamped with a different
280
+ * version are dropped (and purged) rather than replayed / hydrated against the
281
+ * new schema. Omit to disable version gating (records are never invalidated by
282
+ * version).
283
+ *
284
+ * **Adoption is itself an invalidation event:** records written before you set
285
+ * `persistenceVersion` carry no version, so the first boot after enabling it
286
+ * purges all currently-queued offline writes (and cached reads) as stale. Adopt
287
+ * it on a build where that clean slate is acceptable — typically the same
288
+ * breaking deploy you're protecting against — not purely speculatively.
289
+ */
290
+ persistenceVersion?: string;
291
+ /**
200
292
  * Durable store for the read cache (Pillar 2). When supplied, query results
201
293
  * are persisted as their subscriptions advance and hydrated on construction
202
294
  * so a reload renders cached data before the socket reconnects, then resumes
@@ -222,17 +314,46 @@ interface LunoraClientOptions {
222
314
  /** Wire envelope sent on `POST /_lunora/rpc`. */
223
315
  interface RpcEnvelope {
224
316
  args?: Record<string, unknown>;
317
+ /**
318
+ * Stable per-client identifier (custom-mutator push path). Pairs with
319
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
320
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
321
+ */
322
+ clientId?: string;
225
323
  functionPath: string;
324
+ /**
325
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
326
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
327
+ * `client.mutation` calls.
328
+ */
329
+ idempotencyKey?: string;
330
+ /**
331
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
332
+ * server-side per-client watermark: `id &lt;= watermark` is a replay (skipped),
333
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
334
+ * batch so the client resends from `watermark + 1`. Absent on plain
335
+ * `client.mutation` calls.
336
+ */
337
+ mutationId?: number;
226
338
  shardKey?: string;
227
339
  }
228
- /** Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). */
340
+ /**
341
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
342
+ * watermarked custom-mutator push additionally carries `lastMutationId` — the
343
+ * highest per-client sequence the DO has applied — which the client uses to keep
344
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
345
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
346
+ * committed at — which gates the drop of a per-call optimistic layer.
347
+ */
229
348
  type RpcResponseBody = {
230
- result: unknown;
231
- } | {
232
349
  error: {
233
350
  code: string;
234
351
  message: string;
235
352
  };
353
+ } | {
354
+ commitCursor?: number;
355
+ lastMutationId?: number;
356
+ result: unknown;
236
357
  };
237
358
  /** Subscription protocol — client → server. */
238
359
  interface ClientSubscribeMessage {
@@ -265,10 +386,51 @@ interface ClientUnsubscribeMessage {
265
386
  * `onDisconnect` when the socket drops.
266
387
  */
267
388
  interface ClientConnectMessage {
389
+ /**
390
+ * Stable per-client id (persisted alongside the outbox). Lets the server
391
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
392
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
393
+ * use custom mutators.
394
+ */
395
+ clientId?: string;
268
396
  context?: Record<string, unknown>;
269
397
  id: string;
270
398
  type: "connect";
271
399
  }
400
+ /**
401
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
402
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
403
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
404
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
405
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
406
+ * echoed as `shapeId` on every poke part.
407
+ */
408
+ interface ClientShapeSubscribeMessage {
409
+ id: string;
410
+ shape: {
411
+ args?: Record<string, unknown>;
412
+ name: string;
413
+ };
414
+ /**
415
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
416
+ * applied for this shape). When absent or below the server's retained floor
417
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
418
+ * delta.
419
+ */
420
+ sinceCheckpoint?: number;
421
+ /**
422
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
423
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
424
+ * the cursor is numerically in range.
425
+ */
426
+ sinceEpoch?: string;
427
+ type: "shape_subscribe";
428
+ }
429
+ /** Cancel a shape subscription started with the same `id`. */
430
+ interface ClientShapeUnsubscribeMessage {
431
+ id: string;
432
+ type: "shape_unsubscribe";
433
+ }
272
434
  interface ClientAckMessage {
273
435
  id: string;
274
436
  type: "ack";
@@ -309,7 +471,7 @@ interface ClientWhisperMessage {
309
471
  topic: string;
310
472
  type: "whisper";
311
473
  }
312
- type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
474
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
313
475
  /** Subscription protocol — server → client. */
314
476
  interface ServerDataMessage {
315
477
  /**
@@ -323,6 +485,13 @@ interface ServerDataMessage {
323
485
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
324
486
  epoch?: string;
325
487
  id: string;
488
+ /**
489
+ * The highest custom-mutator `mutationId` from this client the server has
490
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
491
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
492
+ * the matching optimistic overlay. Absent on shards without custom mutators.
493
+ */
494
+ lastMutationId?: number;
326
495
  type: "data" | "delta";
327
496
  }
328
497
  /**
@@ -336,8 +505,33 @@ interface ServerResumeMessage {
336
505
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
337
506
  epoch?: string;
338
507
  id: string;
508
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
509
+ lastMutationId?: number;
339
510
  type: "resume";
340
511
  }
512
+ /**
513
+ * Settled acknowledgement for a **list** subscription: a write touched one of
514
+ * the subscription's read tables but produced a byte-identical result, so the
515
+ * server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
516
+ * client (one that announced a `clientId`, hence has a server-side
517
+ * `__client_watermark`) so its optimistic list overlay drops even when no data
518
+ * frame arrives. Plain `useQuery` subscribers never receive it, and an older
519
+ * client safely ignores the unknown frame.
520
+ */
521
+ interface ServerSettledMessage {
522
+ cursor?: number;
523
+ /** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
524
+ epoch?: string;
525
+ id: string;
526
+ /**
527
+ * The highest custom-mutator `mutationId` from this client the server has
528
+ * now applied (the per-client `__client_watermark`). Forwarded to a
529
+ * collection's `onCheckpoint` so it can drop the overlay for the confirmed
530
+ * write whose result didn't change this list.
531
+ */
532
+ lastMutationId?: number;
533
+ type: "settled";
534
+ }
341
535
  interface ServerErrorMessage {
342
536
  error?: unknown;
343
537
  id?: string;
@@ -370,7 +564,65 @@ interface ServerWhisperMessage {
370
564
  topic: string;
371
565
  type: "whisper";
372
566
  }
373
- type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerResumeMessage | ServerWhisperMessage;
567
+ /**
568
+ * One row-level change in a shape's replication stream — the wire form of the
569
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
570
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
571
+ * the removed row by `key` alone. The client applies these to its local
572
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
573
+ * never had in this shape).
574
+ */
575
+ interface RowOp {
576
+ /** Row primary key (`_id`). */
577
+ key: string;
578
+ op: "delete" | "insert" | "update";
579
+ /** Logical table the row belongs to. */
580
+ table: string;
581
+ /** Post-image document for insert/update; absent on delete. */
582
+ value?: Record<string, unknown>;
583
+ }
584
+ /**
585
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
586
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
587
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
588
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
589
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
590
+ */
591
+ interface ServerPokeStartMessage {
592
+ /** The checkpoint the client's view is expected to be at before this poke applies (for ordering/gap detection). */
593
+ baseCheckpoint?: number;
594
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
595
+ epoch?: string;
596
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
597
+ pokeId: string;
598
+ type: "pokeStart";
599
+ }
600
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
601
+ interface ServerPokePartMessage {
602
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
603
+ lastMutationId?: number;
604
+ pokeId: string;
605
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
606
+ rowsPatch: RowOp[];
607
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
608
+ shapeId: string;
609
+ type: "pokePart";
610
+ }
611
+ /**
612
+ * Closes a poke: the client commits the buffered parts atomically and advances
613
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
614
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
615
+ * the next reconnect.
616
+ */
617
+ interface ServerPokeEndMessage {
618
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
619
+ checkpoint?: number;
620
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
621
+ epoch?: string;
622
+ pokeId: string;
623
+ type: "pokeEnd";
624
+ }
625
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
374
626
  /**
375
627
  * The authenticated user as exposed client-side, mirroring better-auth's
376
628
  * `user` row (the `user` field of the `get-session` response). Kept minimal
@@ -582,6 +834,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
834
  }
583
835
  /** A page of workflow instances. */
584
836
  interface WorkflowInstancePage {
837
+ /**
838
+ * Whether workflow inspection is configured on the worker (a Cloudflare
839
+ * account id + API token). `false` when the admin proxy reports it can't
840
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
841
+ * caller render a "set credentials" state without a failed request.
842
+ */
843
+ configured?: boolean;
585
844
  instances: WorkflowInstanceSummary[];
586
845
  page: number;
587
846
  perPage: number;
@@ -594,6 +853,23 @@ interface SubscriptionError {
594
853
  message: string;
595
854
  }
596
855
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
856
+ /**
857
+ * One active per-call optimistic transform layered onto a subscription. The
858
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
859
+ * folded through every layer's `transform`, in order — so an incoming server
860
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
861
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
862
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
863
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
864
+ * mutation's response, and stays `undefined` while the write is still queued/
865
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
866
+ */
867
+ interface OptimisticLayer {
868
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
869
+ commitCursor?: number;
870
+ readonly id: symbol;
871
+ readonly transform: (current: unknown) => unknown;
872
+ }
597
873
  interface SubscriptionState {
598
874
  /** True once the server has acked the subscription on the current socket. */
599
875
  acked: boolean;
@@ -605,13 +881,52 @@ interface SubscriptionState {
605
881
  */
606
882
  readonly argsKey: string;
607
883
  readonly callbacks: Set<SubscriptionCallback>;
884
+ /**
885
+ * Notified when a `settled` frame advances this subscription's watermark — a
886
+ * write touched the subscription's tables but the result was byte-identical,
887
+ * so the server suppressed the data frame. A `@lunora/db` list collection
888
+ * uses this to drop the optimistic overlay for the confirmed write.
889
+ *
890
+ * A SET (not a single slot) because `SubscriptionState` is SHARED across
891
+ * every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
892
+ * collection may subscribe to a query a plain `useQuery` already opened, so
893
+ * each subscriber registers its own callback (mirroring `callbacks` /
894
+ * `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
895
+ * `useQuery` consumers register nothing, leaving the set empty.
896
+ */
897
+ readonly checkpointCallbacks: Set<(watermark: {
898
+ checkpoint?: number;
899
+ mutationId?: number;
900
+ }) => void>;
608
901
  /** Notified when the server rejects this subscription (e.g. admin auth). */
609
902
  readonly errorCallbacks: Set<SubscriptionErrorCallback>;
610
903
  readonly fn: FunctionReference;
611
904
  readonly id: string;
905
+ /**
906
+ * The highest custom-mutator `mutationId` from this client the server has
907
+ * applied, captured from the last `settled` frame (the suppressed-list-frame
908
+ * watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
909
+ * Absent until a `settled` frame arrives.
910
+ */
911
+ lastMutationId?: number;
612
912
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
613
913
  lastValue: unknown;
614
914
  /**
915
+ * Active per-call optimistic layers, in application order (see
916
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
917
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
918
+ * exactly and behaviour is identical to a plain server-value assignment.
919
+ */
920
+ optimisticLayers: OptimisticLayer[];
921
+ /**
922
+ * The authoritative server value the optimistic layers fold onto — the value
923
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
924
+ * are active; diverges only while a per-call optimistic write is pending. A
925
+ * server frame updates this (and re-folds the layers); the durable read cache
926
+ * persists this, never the optimistic overlay.
927
+ */
928
+ serverBase: unknown;
929
+ /**
615
930
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
616
931
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
617
932
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -627,18 +942,15 @@ interface SubscriptionState {
627
942
  * until the first epoch-stamped frame arrives.
628
943
  */
629
944
  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
945
  readonly shardKey?: string;
637
946
  }
638
947
  /**
639
948
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
949
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
950
+ * single server-side registration. Args are stably encoded (keys sorted at every
951
+ * depth) so two structurally-equal arg records constructed with a different key
952
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
953
+ * duplicate subscription.
642
954
  */
643
955
  declare class SubscriptionRegistry {
644
956
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -656,10 +968,10 @@ declare class SubscriptionRegistry {
656
968
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
657
969
  *
658
970
  * `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
971
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
972
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
973
+ * settles together confirmed on the mutation's commit cursor, or rolled back
974
+ * on failure — the same per-subscription layer machinery the single-query
663
975
  * per-call `optimistic` transform uses, generalized to N queries.
664
976
  */
665
977
  interface OptimisticLocalStore {
@@ -689,13 +1001,17 @@ interface OptimisticLocalStore {
689
1001
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
690
1002
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
691
1003
  /**
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.
1004
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
1005
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
1006
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
1007
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
1008
+ * query not merged) and drops gaplessly on the mutation's commit cursor, like
1009
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
1010
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
1011
+ * so the caller settles the whole batch when the mutation does.
697
1012
  */
698
- declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, write: (state: SubscriptionState, next: unknown) => () => void, stableStringify: (value: unknown) => string) => {
1013
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, stableStringify: (value: unknown) => string) => {
1014
+ confirms: ((commitCursor: number | undefined) => void)[];
699
1015
  rollbacks: (() => void)[];
700
1016
  store: OptimisticLocalStore;
701
1017
  };
@@ -753,6 +1069,41 @@ declare const createStream: <T>(options: {
753
1069
  */
754
1070
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
755
1071
  /**
1072
+ * Terminal verdict for a mutation that passed through the offline queue,
1073
+ * delivered to {@link LunoraClient.onMutationSettled}.
1074
+ *
1075
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1076
+ * original caller can await, and which no longer exists after a reload — this
1077
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1078
+ * including writes restored from durable storage in a later session. It is the
1079
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1080
+ * instead of silently dropping a rolled-back optimistic row.
1081
+ *
1082
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1083
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1084
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1085
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1086
+ * "the caller already saw this" apart from "nothing else will report this".
1087
+ */
1088
+ interface MutationSettledEvent {
1089
+ /** The write's args, so a listener can describe or re-offer the change. */
1090
+ readonly args: Record<string, unknown>;
1091
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
1092
+ readonly code?: string;
1093
+ /** The rejection error on `status: "rejected"`. */
1094
+ readonly error?: unknown;
1095
+ /** The `&lt;file>:&lt;function>` reference of the mutation. */
1096
+ readonly functionPath: string;
1097
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1098
+ readonly hadAwaiter: boolean;
1099
+ /** The write's stable id (idempotency key / queue id). */
1100
+ readonly id: string;
1101
+ /** Shard the write targeted, if any. */
1102
+ readonly shardKey?: string;
1103
+ /** Terminal outcome. */
1104
+ readonly status: "committed" | "rejected";
1105
+ }
1106
+ /**
756
1107
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
757
1108
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
758
1109
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -760,6 +1111,13 @@ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
760
1111
  * re-declaring it.
761
1112
  */
762
1113
  interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
1114
+ /**
1115
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
1116
+ * durable outbox replay a committed-but-unacked write under its *original* key
1117
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
1118
+ * each then gets a fresh key.
1119
+ */
1120
+ mutationId?: string;
763
1121
  optimistic?: (current: TCurrent | undefined) => TValue;
764
1122
  /**
765
1123
  * Convex-parity multi-query optimistic update. Receives an
@@ -770,6 +1128,19 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
770
1128
  optimisticUpdate?: OptimisticUpdate<TArgs>;
771
1129
  shardKey?: string;
772
1130
  }
1131
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
1132
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
1133
+ /**
1134
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
1135
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
1136
+ * echoed for this client. A `@lunora/db` collection feeds these into its
1137
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
1138
+ * rows have landed.
1139
+ */
1140
+ interface SyncWatermark {
1141
+ checkpoint?: number;
1142
+ mutationId?: number;
1143
+ }
773
1144
  /**
774
1145
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
775
1146
  * a single multiplexed WebSocket.
@@ -778,6 +1149,8 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
778
1149
  * see the package README for the wire protocol.
779
1150
  */
780
1151
  declare class LunoraClient {
1152
+ /** 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. */
1153
+ private static readonly MAX_POKE_BUFFERS;
781
1154
  readonly url: string;
782
1155
  readonly wsUrl: string;
783
1156
  private wsToken;
@@ -787,11 +1160,36 @@ declare class LunoraClient {
787
1160
  private readonly WebSocketImpl;
788
1161
  private readonly bookmark;
789
1162
  private readonly reconnectOptions;
1163
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
1164
+ private readonly connectTimeoutMs;
790
1165
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
1166
  private readonly heartbeatIntervalMs;
792
1167
  private readonly offlineQueue;
1168
+ /**
1169
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
1170
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
1171
+ * is bypassed, so a db app has exactly one durable write path.
1172
+ */
1173
+ private readonly outbox;
1174
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
1175
+ private readonly clientId;
1176
+ /**
1177
+ * Highest custom-mutator watermark the server has echoed for this client,
1178
+ * keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
1179
+ * `__client_watermark` per shard. `callMutator` bumps it from every
1180
+ * ack; the `@lunora/db` mutator runtime seeds its `clientSeq` generator from
1181
+ * it so a reload (which resets the in-memory counter) never reissues a stale
1182
+ * sequence the server would silently swallow as a replay.
1183
+ */
1184
+ private readonly clientWatermarks;
1185
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1186
+ private outboxMutationCounter;
793
1187
  private readonly onPersistenceError;
794
1188
  private readonly persistence;
1189
+ /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
1190
+ private readonly persistenceVersion;
1191
+ /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
1192
+ private outboxLeaderRelease;
795
1193
  /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
796
1194
  private readonly queryCache;
797
1195
  /**
@@ -835,6 +1233,14 @@ declare class LunoraClient {
835
1233
  private readonly connectionContextHolders;
836
1234
  private authToken;
837
1235
  /**
1236
+ * Optional STABLE identity subject (a user id), the basis of the offline-queue
1237
+ * identity stamp when supplied. Keeps a same-user token *refresh* from looking
1238
+ * like an identity change (which would discard queued writes). `undefined` =
1239
+ * not supplied, so identity falls back to a hash of the raw token. See
1240
+ * `setAuthToken` / `identityFingerprint`.
1241
+ */
1242
+ private authSubject;
1243
+ /**
838
1244
  * Identity stamp recorded against each queued offline mutation, keyed by
839
1245
  * the queue-assigned mutation id. Captured at enqueue from the auth token
840
1246
  * in effect at the time, and re-checked at flush so a queued write can
@@ -849,6 +1255,10 @@ declare class LunoraClient {
849
1255
  private readonly statusListeners;
850
1256
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
851
1257
  private readonly tokenExpiredListeners;
1258
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1259
+ private readonly mutationSettledListeners;
1260
+ /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
1261
+ private readonly pendingChangeListeners;
852
1262
  /**
853
1263
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
854
1264
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -866,20 +1276,78 @@ declare class LunoraClient {
866
1276
  * calls `.cancel()` or the iterator is garbage-collected.
867
1277
  */
868
1278
  private readonly streams;
1279
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
1280
+ private readonly shapeSubscriptions;
1281
+ /** In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by `pokeId`. */
1282
+ private readonly pokeBuffers;
1283
+ private nextShapeId;
869
1284
  constructor(options: LunoraClientOptions);
870
1285
  /**
871
1286
  * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
872
1287
  * {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
873
1288
  * sync across all mounted instances.
874
1289
  *
1290
+ * Pass a STABLE `subject` (the user id) to key the offline-queue identity on
1291
+ * it instead of the token bytes, so a token *refresh* (same user, new JWT)
1292
+ * doesn't read as an identity change and discard queued writes. The subject is
1293
+ * **sticky**: a later call that omits it (or passes `undefined`) keeps the
1294
+ * established subject — so `setAuthToken(refreshedToken)` after a prior
1295
+ * `setAuthToken(token, user.id)` retains the identity. Pass `null` to clear it
1296
+ * (an explicit sign-out). Establishing the subject for the first time on an
1297
+ * UNCHANGED token (e.g. the user id resolves a tick after the token was set)
1298
+ * re-stamps any in-flight queued writes rather than dropping them — same
1299
+ * credential, just a more stable label. A real user switch (the token AND
1300
+ * subject both change) still drops the previous user's writes.
1301
+ *
875
1302
  * Does NOT update the WebSocket auth — the WS token is fixed at upgrade
876
1303
  * time and lives in the URL. To refresh live WS auth, call
877
1304
  * {@link setWsToken} explicitly, which closes existing shard sockets to
878
1305
  * force a reconnect with the new credential.
879
1306
  */
880
- setAuthToken(token: string | null): void;
1307
+ setAuthToken(token: string | null, subject?: string | null): void;
881
1308
  getAuthToken(): string | null;
882
1309
  /**
1310
+ * The current identity fingerprint (the same stamp queued offline writes
1311
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
1312
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
1313
+ * can drop a persisted write whose captured `identity` no longer matches the
1314
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
1315
+ */
1316
+ currentIdentity(): string | null;
1317
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
1318
+ clientIdentifier(): string;
1319
+ /**
1320
+ * The highest custom-mutator watermark the server has echoed for this client
1321
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
1322
+ * its `clientSeq` generator from this so a reload never reissues a sequence
1323
+ * the server has already applied (which it would swallow as a replay, silently
1324
+ * dropping the write).
1325
+ */
1326
+ confirmedMutationWatermark(shardKey?: string): number;
1327
+ /**
1328
+ * Push a custom mutator to its authoritative server impl over the watermark
1329
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
1330
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
1331
+ * client's `__client_watermark`.
1332
+ *
1333
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
1334
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
1335
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
1336
+ * A `false` verdict tells the caller to reissue above the now-known watermark
1337
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
1338
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
1339
+ *
1340
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
1341
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
1342
+ */
1343
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
1344
+ clientSeq?: number;
1345
+ shardKey?: string;
1346
+ }): Promise<{
1347
+ applied: boolean;
1348
+ result: unknown;
1349
+ }>;
1350
+ /**
883
1351
  * Subscribe to auth-token changes. Returns an unsubscribe function. The
884
1352
  * listener is NOT invoked on registration — use {@link getAuthToken} for
885
1353
  * the current value.
@@ -990,6 +1458,32 @@ declare class LunoraClient {
990
1458
  * unsubscribe function.
991
1459
  */
992
1460
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
1461
+ /**
1462
+ * Number of offline writes waiting in the built-in queue to be sent — the
1463
+ * depth for a "N changes waiting to sync" indicator. Counts writes that are
1464
+ * queued (offline / mid-reconnect), not ones already in flight on the wire.
1465
+ * A `@lunora/db` app whose writes ride the unified outbox should read
1466
+ * `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
1467
+ */
1468
+ pendingCount(): number;
1469
+ /**
1470
+ * Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
1471
+ * with the current count, then whenever the queue depth changes (a write is
1472
+ * enqueued, flushed, or discarded). Returns an unsubscribe function.
1473
+ */
1474
+ onPendingChange(listener: (pending: number) => void): Unsubscribe;
1475
+ /**
1476
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
1477
+ * fires once per queued write that commits or is rejected — including a write
1478
+ * restored from durable storage after a reload, whose original `mutation()`
1479
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
1480
+ * evicts on overflow or discards on an identity change. This is the durable
1481
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
1482
+ * mutation that never queued still surfaces through the Promise `mutation()`
1483
+ * returns. The listener is NOT invoked on registration. Returns an
1484
+ * unsubscribe function. See {@link MutationSettledEvent}.
1485
+ */
1486
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
993
1487
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
994
1488
  shardKey?: string;
995
1489
  }): Promise<ReturnOf<F>>;
@@ -1069,8 +1563,12 @@ declare class LunoraClient {
1069
1563
  * List a workflow's instances via the admin Workflows proxy
1070
1564
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1565
  * 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.
1566
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1567
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1568
+ * false }` sentinel, so the result resolves with `configured === false` and an
1569
+ * empty `instances` list — callers should branch on that flag rather than
1570
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1571
+ * `name` is the deployed workflow name.
1074
1572
  */
1075
1573
  listWorkflowInstances(options: {
1076
1574
  name: string;
@@ -1400,6 +1898,27 @@ declare class LunoraClient {
1400
1898
  userId?: string;
1401
1899
  }): Promise<AuthPage<AuthSession>>;
1402
1900
  subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
1901
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1902
+ onError?: SubscriptionErrorCallback;
1903
+ shardKey?: string;
1904
+ }): Unsubscribe;
1905
+ /**
1906
+ * Subscribe to a declarative **shape** — server-side partial replication
1907
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
1908
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
1909
+ * validated `args` (never a `where` the client could forge), the server seeds
1910
+ * the current membership as an insert-poke and streams live membership diffs.
1911
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
1912
+ *
1913
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1914
+ * (name, args): the server resolves them under the socket's verified identity,
1915
+ * so every call gets its own id + view. The returned function unsubscribes.
1916
+ */
1917
+ subscribeShape(shape: {
1918
+ args?: Record<string, unknown>;
1919
+ name: string;
1920
+ }, callback: ShapeCallback, options?: {
1921
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1403
1922
  onError?: SubscriptionErrorCallback;
1404
1923
  shardKey?: string;
1405
1924
  }): Unsubscribe;
@@ -1427,12 +1946,35 @@ declare class LunoraClient {
1427
1946
  }): StreamIterable<ReturnOf<F>>;
1428
1947
  close(): void;
1429
1948
  /**
1949
+ * Persist a mutation that can't go out on the wire right now (offline, or
1950
+ * mid-reconnect after a prior connect). The optimistic update has already
1951
+ * been applied by `mutation`; this only chooses the durable write path and
1952
+ * rolls the optimistic write back if persistence is rejected.
1953
+ *
1954
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
1955
+ * owns persistence + at-least-once replay, so we delegate and return
1956
+ * optimistically (confirmation rides the synced view). Otherwise the
1957
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
1958
+ */
1959
+ private enqueueOfflineMutation;
1960
+ /**
1430
1961
  * Restore offline mutations persisted in a prior session and open a socket
1431
1962
  * for each shard they target so they flush once the WS reconnects. Failures
1432
1963
  * are swallowed — a broken durable store must not stop the client booting.
1433
1964
  */
1434
1965
  private hydratePersistedQueue;
1435
1966
  /**
1967
+ * Re-queue the durable offline writes — but only as the multi-tab LEADER. The
1968
+ * persisted queue is shared across a profile's tabs; without coordination
1969
+ * every tab would re-queue and replay the same writes (correct only because
1970
+ * the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
1971
+ * exactly one tab hydrate; it holds the lock for its lifetime, so when it
1972
+ * closes another tab acquires the lock and takes over. Falls back to
1973
+ * unconditional hydration where Web Locks are unavailable (React Native, older
1974
+ * browsers, SSR) — single-context there, so no coordination is needed.
1975
+ */
1976
+ private hydrateAsOutboxLeader;
1977
+ /**
1436
1978
  * Load every cached query into {@link hydratedQueryCache} so the next
1437
1979
  * `subscribe()` for each key seeds its initial value off disk. A
1438
1980
  * subscription created before this resolves simply misses the cache (it
@@ -1462,21 +2004,37 @@ declare class LunoraClient {
1462
2004
  /** Recompute the aggregate status and notify listeners if it changed. */
1463
2005
  private emitConnectionStatus;
1464
2006
  /**
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).
2007
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
2008
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
2009
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
2010
+ * is unreachable present only to satisfy the optional queue-id type.
2011
+ */
2012
+ private emitItemSettled;
2013
+ /**
2014
+ * Apply an optimistic update to the subscription that matches the mutation's
2015
+ * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
2016
+ * invoke if the mutation later fails.
2017
+ *
2018
+ * The registry is already indexed by exactly this triple via
2019
+ * `SubscriptionRegistry.key`, so at most one subscription can match. A direct
2020
+ * O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
2021
+ *
2022
+ * `shardKey` normalization: both `undefined` and `""` map to the empty string
2023
+ * inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
2024
+ * a shardKey correctly matches a subscription registered without one regardless
2025
+ * of whether the caller passed `undefined` or omitted the field.
1470
2026
  */
1471
2027
  private applyOptimisticUpdates;
1472
2028
  /**
1473
2029
  * 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.
2030
+ * to the live subscription registry. Each `setQuery` registers a constant
2031
+ * optimistic LAYER on its target subscription (via the same engine the
2032
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
2033
+ * incoming deltas and drops gaplessly on its commit cursorits `confirm` /
2034
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
2035
+ * callback unwinds its own partial writes LIFO over just the rollbacks it
2036
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
2037
+ * mutation or leave a partial patch live.
1480
2038
  */
1481
2039
  private applyOptimisticUpdate;
1482
2040
  private getConnection;
@@ -1523,6 +2081,13 @@ declare class LunoraClient {
1523
2081
  * to the lifecycle dispatch.
1524
2082
  */
1525
2083
  private sendConnectEnvelope;
2084
+ /**
2085
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
2086
+ * socket. Each frame carries the shape's last applied checkpoint, so the
2087
+ * server resumes from it — or re-seeds when the cursor fell below CDC
2088
+ * retention or the epoch forked.
2089
+ */
2090
+ private resendShapeSubscriptions;
1526
2091
  private ensureSocket;
1527
2092
  private handleDisconnect;
1528
2093
  /**
@@ -1538,8 +2103,14 @@ declare class LunoraClient {
1538
2103
  /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
1539
2104
  private markShardPendingAck;
1540
2105
  private sendSubscribeIfOpen;
2106
+ private sendShapeSubscribeIfOpen;
1541
2107
  private handleServerMessage;
1542
2108
  private handleErrorMessage;
2109
+ private handlePokeStart;
2110
+ private handlePokePart;
2111
+ private handlePokeEnd;
2112
+ /** Materialize a shape's keyed view to an array and invoke its callbacks. */
2113
+ private emitShapeRows;
1543
2114
  private handleDataMessage;
1544
2115
  /**
1545
2116
  * Handle a `resume` frame (Pillar 1b): the server proved nothing the
@@ -1551,6 +2122,25 @@ declare class LunoraClient {
1551
2122
  */
1552
2123
  private handleResumeMessage;
1553
2124
  /**
2125
+ * Handle a `settled` frame: a write touched one of this subscription's read
2126
+ * tables but produced a byte-identical result, so the server suppressed the
2127
+ * data frame. Like {@link handleResumeMessage} the value didn't change — we
2128
+ * advance the resume position and re-persist — but we ALSO surface the echoed
2129
+ * custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
2130
+ * collection drops the optimistic overlay for the confirmed write (otherwise
2131
+ * its checkpoint gate, fed only by data frames, would hang forever). Sent
2132
+ * only to custom-mutator clients; plain `useQuery` subscribers leave
2133
+ * `onCheckpoint` unset and this is a near no-op.
2134
+ */
2135
+ private handleSettledMessage;
2136
+ /**
2137
+ * Mark `state` acked and, when the frame carries a newer cursor/epoch than
2138
+ * the cached position, advance the resume watermark and re-persist. Shared by
2139
+ * the `resume` and `settled` frame handlers — both acknowledge "nothing the
2140
+ * client must re-render changed, but the resume position may have moved".
2141
+ */
2142
+ private ackAndAdvanceCursor;
2143
+ /**
1554
2144
  * Resolve the value to publish for a `data`/`delta` frame.
1555
2145
  *
1556
2146
  * A `data` frame is an authoritative snapshot (the server re-execution path)
@@ -1586,6 +2176,15 @@ declare class LunoraClient {
1586
2176
  */
1587
2177
  private rejectQueuedForIdentityChange;
1588
2178
  /**
2179
+ * Migrate every live identity stamp from `from` to `to` — used when the auth
2180
+ * identity label changes but the underlying credential (token) does NOT, e.g.
2181
+ * the user id resolves a tick after the token was set. The in-memory
2182
+ * `queuedIdentities` map is the flush-time source of truth, so re-stamping it
2183
+ * keeps the in-flight writes replayable under the new (more stable) identity
2184
+ * instead of the flush guard discarding them as a mismatch.
2185
+ */
2186
+ private restampQueuedIdentity;
2187
+ /**
1589
2188
  * Drop the durable read cache on an identity change so a cached value stamped
1590
2189
  * under the previous identity can never hydrate into a new session. Clears
1591
2190
  * the in-flight write batch and the not-yet-consumed hydrated entries too;
@@ -1594,4 +2193,4 @@ declare class LunoraClient {
1594
2193
  private clearQueryCacheForIdentityChange;
1595
2194
  private flushOfflineQueue;
1596
2195
  }
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 };
2196
+ 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 };