@lunora/client 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
@@ -0,0 +1,31 @@
1
+ const createMutatorRunner = (handle, sinks) => {
2
+ let inFlight = 0;
3
+ let latestInvocation = 0;
4
+ const mutate = async (args) => {
5
+ latestInvocation += 1;
6
+ const invocation = latestInvocation;
7
+ inFlight += 1;
8
+ sinks.setPending(true);
9
+ try {
10
+ await handle(args).isPersisted.promise;
11
+ if (invocation === latestInvocation) {
12
+ sinks.setError(void 0);
13
+ }
14
+ } catch (error) {
15
+ const normalized = error instanceof Error ? error : new Error(String(error));
16
+ if (invocation === latestInvocation) {
17
+ sinks.setError(normalized);
18
+ }
19
+ throw normalized;
20
+ } finally {
21
+ inFlight -= 1;
22
+ sinks.setPending(inFlight > 0);
23
+ }
24
+ };
25
+ const reset = () => {
26
+ sinks.setError(void 0);
27
+ };
28
+ return { mutate, reset };
29
+ };
30
+
31
+ export { createMutatorRunner };
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-UiULzH_1.mjs';
1
+ import { LunoraClient } from './LunoraClient-BPQx0T7W.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -120,6 +120,40 @@ interface PersistenceAdapter {
120
120
  remove: (id: string) => Promise<void>;
121
121
  }
122
122
  /**
123
+ * One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
124
+ * plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
125
+ * the durable outbox needs to dedupe and watermark replays.
126
+ */
127
+ interface OutboxMutation {
128
+ args: Record<string, unknown>;
129
+ /** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
130
+ clientId: string;
131
+ functionPath: string;
132
+ /** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
133
+ idempotencyKey: string;
134
+ /** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
135
+ identity: string | null;
136
+ /** Monotonic per-client mutation id, backing the server `__client_watermark`. */
137
+ mutationId: number;
138
+ shardKey?: string;
139
+ }
140
+ /**
141
+ * Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
142
+ * the client delegates offline write durability + at-least-once replay to this
143
+ * sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
144
+ * `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
145
+ * backed by the TanStack `OfflineExecutor`); the interface itself is
146
+ * dependency-free so `@lunora/client` stays TanStack-free.
147
+ */
148
+ interface OutboxSink {
149
+ /**
150
+ * Persist and schedule a write for replay. Rejects with an
151
+ * `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
152
+ * the caller can surface back-pressure to the issuing mutation.
153
+ */
154
+ enqueue: (mutation: OutboxMutation) => Promise<void>;
155
+ }
156
+ /**
123
157
  * One persisted query result in the durable read cache (Pillar 2). Keyed in the
124
158
  * store by `shardKey + functionPath + argsKey`; the record carries everything
125
159
  * needed to render offline on reload and to resume the live subscription.
@@ -177,6 +211,16 @@ interface LunoraClientOptions {
177
211
  authBasePath?: string;
178
212
  bookmarkStorage?: BookmarkStorage;
179
213
  /**
214
+ * Stable per-client id backing the custom-mutator watermark. Sent on the
215
+ * `connect` envelope (so the server can scope this client's
216
+ * `__client_watermark`) and stamped onto every {@link OutboxMutation} the
217
+ * {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
218
+ * monotonic mutation id to form the idempotency key. The `@lunora/db` path
219
+ * persists a stable id alongside the outbox and passes it here; omit for the
220
+ * standalone client, which generates an ephemeral per-session id.
221
+ */
222
+ clientId?: string;
223
+ /**
180
224
  * Default app context sent in the `connect` envelope right after each socket
181
225
  * opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
182
226
  * as `event.context`. A per-shard context registered via
@@ -184,6 +228,16 @@ interface LunoraClientOptions {
184
228
  * hook needs connection context.
185
229
  */
186
230
  connectionContext?: Record<string, unknown>;
231
+ /**
232
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
233
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
234
+ * worker that never upgrades — the client force-closes the socket and routes
235
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
236
+ * of leaving the live channel silently stuck on the browser's much longer
237
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
238
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
239
+ */
240
+ connectTimeoutMs?: number;
187
241
  fetch?: typeof fetch;
188
242
  /**
189
243
  * Interval (ms) between keepalive pings sent on each open subscription
@@ -194,6 +248,14 @@ interface LunoraClientOptions {
194
248
  */
195
249
  heartbeatIntervalMs?: number;
196
250
  offlineQueue?: OfflineQueueOptions;
251
+ /**
252
+ * Durable outbox seam for offline writes. When supplied (the `@lunora/db`
253
+ * path wires `createExecutorOutboxSink`), offline mutations are delegated to
254
+ * the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
255
+ * is bypassed, so a db app has exactly one durable write path. Omit for the
256
+ * standalone client, which keeps using {@link LunoraClientOptions.persistence}.
257
+ */
258
+ outbox?: OutboxSink;
197
259
  /** Durable store for the offline mutation queue; omit to keep it in memory. */
198
260
  persistence?: PersistenceAdapter;
199
261
  /**
@@ -222,17 +284,43 @@ interface LunoraClientOptions {
222
284
  /** Wire envelope sent on `POST /_lunora/rpc`. */
223
285
  interface RpcEnvelope {
224
286
  args?: Record<string, unknown>;
287
+ /**
288
+ * Stable per-client identifier (custom-mutator push path). Pairs with
289
+ * {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
290
+ * server `__client_watermark`. Absent on plain `client.mutation` calls.
291
+ */
292
+ clientId?: string;
225
293
  functionPath: string;
294
+ /**
295
+ * Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
296
+ * path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
297
+ * `client.mutation` calls.
298
+ */
299
+ idempotencyKey?: string;
300
+ /**
301
+ * Monotonic per-client mutation id (custom-mutator push path), backing the
302
+ * server-side per-client watermark: `id &lt;= watermark` is a replay (skipped),
303
+ * `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
304
+ * batch so the client resends from `watermark + 1`. Absent on plain
305
+ * `client.mutation` calls.
306
+ */
307
+ mutationId?: number;
226
308
  shardKey?: string;
227
309
  }
228
- /** Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). */
310
+ /**
311
+ * Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
312
+ * watermarked custom-mutator push additionally carries `lastMutationId` — the
313
+ * highest per-client sequence the DO has applied — which the client uses to keep
314
+ * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
315
+ */
229
316
  type RpcResponseBody = {
230
- result: unknown;
231
- } | {
232
317
  error: {
233
318
  code: string;
234
319
  message: string;
235
320
  };
321
+ } | {
322
+ lastMutationId?: number;
323
+ result: unknown;
236
324
  };
237
325
  /** Subscription protocol — client → server. */
238
326
  interface ClientSubscribeMessage {
@@ -265,10 +353,51 @@ interface ClientUnsubscribeMessage {
265
353
  * `onDisconnect` when the socket drops.
266
354
  */
267
355
  interface ClientConnectMessage {
356
+ /**
357
+ * Stable per-client id (persisted alongside the outbox). Lets the server
358
+ * scope this connection's `__client_watermark` so custom-mutator pokes can
359
+ * echo the right per-client `lastMutationId`. Omitted by clients that don't
360
+ * use custom mutators.
361
+ */
362
+ clientId?: string;
268
363
  context?: Record<string, unknown>;
269
364
  id: string;
270
365
  type: "connect";
271
366
  }
367
+ /**
368
+ * Subscribe to a declarative **shape** — server-side partial replication scoped
369
+ * by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
370
+ * + validated `args`; the server resolves the trusted `where` (identity/RLS
371
+ * `baseWhere` the client can't forge) and streams the matching rowset, then live
372
+ * {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
373
+ * echoed as `shapeId` on every poke part.
374
+ */
375
+ interface ClientShapeSubscribeMessage {
376
+ id: string;
377
+ shape: {
378
+ args?: Record<string, unknown>;
379
+ name: string;
380
+ };
381
+ /**
382
+ * Resume from this checkpoint (the `__cdc_log` cursor the client last
383
+ * applied for this shape). When absent or below the server's retained floor
384
+ * (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
385
+ * delta.
386
+ */
387
+ sinceCheckpoint?: number;
388
+ /**
389
+ * The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
390
+ * to. A mismatch (forked changelog timeline) forces a full re-seed even when
391
+ * the cursor is numerically in range.
392
+ */
393
+ sinceEpoch?: string;
394
+ type: "shape_subscribe";
395
+ }
396
+ /** Cancel a shape subscription started with the same `id`. */
397
+ interface ClientShapeUnsubscribeMessage {
398
+ id: string;
399
+ type: "shape_unsubscribe";
400
+ }
272
401
  interface ClientAckMessage {
273
402
  id: string;
274
403
  type: "ack";
@@ -309,7 +438,7 @@ interface ClientWhisperMessage {
309
438
  topic: string;
310
439
  type: "whisper";
311
440
  }
312
- type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
441
+ type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
313
442
  /** Subscription protocol — server → client. */
314
443
  interface ServerDataMessage {
315
444
  /**
@@ -323,6 +452,13 @@ interface ServerDataMessage {
323
452
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
324
453
  epoch?: string;
325
454
  id: string;
455
+ /**
456
+ * The highest custom-mutator `mutationId` from this client the server has
457
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
458
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
459
+ * the matching optimistic overlay. Absent on shards without custom mutators.
460
+ */
461
+ lastMutationId?: number;
326
462
  type: "data" | "delta";
327
463
  }
328
464
  /**
@@ -336,6 +472,8 @@ interface ServerResumeMessage {
336
472
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
337
473
  epoch?: string;
338
474
  id: string;
475
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
476
+ lastMutationId?: number;
339
477
  type: "resume";
340
478
  }
341
479
  interface ServerErrorMessage {
@@ -370,7 +508,65 @@ interface ServerWhisperMessage {
370
508
  topic: string;
371
509
  type: "whisper";
372
510
  }
373
- type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerResumeMessage | ServerWhisperMessage;
511
+ /**
512
+ * One row-level change in a shape's replication stream — the wire form of the
513
+ * DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
514
+ * `value` (projected to the shape's `columns`); `delete` omits it, identifying
515
+ * the removed row by `key` alone. The client applies these to its local
516
+ * collection; an unknown `key` on a `delete` is a safe no-op (a row the client
517
+ * never had in this shape).
518
+ */
519
+ interface RowOp {
520
+ /** Row primary key (`_id`). */
521
+ key: string;
522
+ op: "delete" | "insert" | "update";
523
+ /** Logical table the row belongs to. */
524
+ table: string;
525
+ /** Post-image document for insert/update; absent on delete. */
526
+ value?: Record<string, unknown>;
527
+ }
528
+ /**
529
+ * Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
530
+ * protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
531
+ * frames and closed by exactly one {@link ServerPokeEndMessage}; the client
532
+ * buffers every part and applies them in a single transaction at `pokeEnd`, so a
533
+ * socket that drops mid-poke simply re-seeds on reconnect (no torn view).
534
+ */
535
+ interface ServerPokeStartMessage {
536
+ /** The checkpoint the client's view is expected to be at before this poke applies (for ordering/gap detection). */
537
+ baseCheckpoint?: number;
538
+ /** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
539
+ epoch?: string;
540
+ /** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
541
+ pokeId: string;
542
+ type: "pokeStart";
543
+ }
544
+ /** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
545
+ interface ServerPokePartMessage {
546
+ /** Per-client custom-mutator watermark carried with this slice (see {@link ServerDataMessage.lastMutationId}). */
547
+ lastMutationId?: number;
548
+ pokeId: string;
549
+ /** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
550
+ rowsPatch: RowOp[];
551
+ /** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
552
+ shapeId: string;
553
+ type: "pokePart";
554
+ }
555
+ /**
556
+ * Closes a poke: the client commits the buffered parts atomically and advances
557
+ * its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
558
+ * cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
559
+ * the next reconnect.
560
+ */
561
+ interface ServerPokeEndMessage {
562
+ /** The `__cdc_log` cursor the view is at after applying this poke. */
563
+ checkpoint?: number;
564
+ /** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
565
+ epoch?: string;
566
+ pokeId: string;
567
+ type: "pokeEnd";
568
+ }
569
+ type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerWhisperMessage;
374
570
  /**
375
571
  * The authenticated user as exposed client-side, mirroring better-auth's
376
572
  * `user` row (the `user` field of the `get-session` response). Kept minimal
@@ -582,6 +778,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
778
  }
583
779
  /** A page of workflow instances. */
584
780
  interface WorkflowInstancePage {
781
+ /**
782
+ * Whether workflow inspection is configured on the worker (a Cloudflare
783
+ * account id + API token). `false` when the admin proxy reports it can't
784
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
785
+ * caller render a "set credentials" state without a failed request.
786
+ */
787
+ configured?: boolean;
585
788
  instances: WorkflowInstanceSummary[];
586
789
  page: number;
587
790
  perPage: number;
@@ -637,8 +840,11 @@ interface SubscriptionState {
637
840
  }
638
841
  /**
639
842
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
843
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
844
+ * single server-side registration. Args are stably encoded (keys sorted at every
845
+ * depth) so two structurally-equal arg records constructed with a different key
846
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
847
+ * duplicate subscription.
642
848
  */
643
849
  declare class SubscriptionRegistry {
644
850
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -760,6 +966,13 @@ type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
760
966
  * re-declaring it.
761
967
  */
762
968
  interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
969
+ /**
970
+ * Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
971
+ * durable outbox replay a committed-but-unacked write under its *original* key
972
+ * so the server dedups it instead of applying it twice. Omit for normal calls —
973
+ * each then gets a fresh key.
974
+ */
975
+ mutationId?: string;
763
976
  optimistic?: (current: TCurrent | undefined) => TValue;
764
977
  /**
765
978
  * Convex-parity multi-query optimistic update. Receives an
@@ -770,6 +983,19 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
770
983
  optimisticUpdate?: OptimisticUpdate<TArgs>;
771
984
  shardKey?: string;
772
985
  }
986
+ /** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
987
+ type ShapeCallback = (rows: Record<string, unknown>[]) => void;
988
+ /**
989
+ * The high-water marks a shape poke has now synced to the client: `checkpoint`
990
+ * is the op-log cursor and `mutationId` the highest custom-mutator id the server
991
+ * echoed for this client. A `@lunora/db` collection feeds these into its
992
+ * checkpoint registry to drop optimistic overlays once the server's authoritative
993
+ * rows have landed.
994
+ */
995
+ interface SyncWatermark {
996
+ checkpoint?: number;
997
+ mutationId?: number;
998
+ }
773
999
  /**
774
1000
  * Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
775
1001
  * a single multiplexed WebSocket.
@@ -778,6 +1004,8 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
778
1004
  * see the package README for the wire protocol.
779
1005
  */
780
1006
  declare class LunoraClient {
1007
+ /** 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. */
1008
+ private static readonly MAX_POKE_BUFFERS;
781
1009
  readonly url: string;
782
1010
  readonly wsUrl: string;
783
1011
  private wsToken;
@@ -787,9 +1015,30 @@ declare class LunoraClient {
787
1015
  private readonly WebSocketImpl;
788
1016
  private readonly bookmark;
789
1017
  private readonly reconnectOptions;
1018
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
1019
+ private readonly connectTimeoutMs;
790
1020
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
1021
  private readonly heartbeatIntervalMs;
792
1022
  private readonly offlineQueue;
1023
+ /**
1024
+ * Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
1025
+ * set, offline writes are delegated here and the built-in {@link OfflineQueue}
1026
+ * is bypassed, so a db app has exactly one durable write path.
1027
+ */
1028
+ private readonly outbox;
1029
+ /** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
1030
+ private readonly clientId;
1031
+ /**
1032
+ * Highest custom-mutator watermark the server has echoed for this client,
1033
+ * keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
1034
+ * `__client_watermark` per shard. `callMutator` bumps it from every
1035
+ * ack; the `@lunora/db` mutator runtime seeds its `clientSeq` generator from
1036
+ * it so a reload (which resets the in-memory counter) never reissues a stale
1037
+ * sequence the server would silently swallow as a replay.
1038
+ */
1039
+ private readonly clientWatermarks;
1040
+ /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1041
+ private outboxMutationCounter;
793
1042
  private readonly onPersistenceError;
794
1043
  private readonly persistence;
795
1044
  /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
@@ -866,6 +1115,11 @@ declare class LunoraClient {
866
1115
  * calls `.cancel()` or the iterator is garbage-collected.
867
1116
  */
868
1117
  private readonly streams;
1118
+ /** Live shape subscriptions (partial replication), keyed by their wire id. */
1119
+ private readonly shapeSubscriptions;
1120
+ /** In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by `pokeId`. */
1121
+ private readonly pokeBuffers;
1122
+ private nextShapeId;
869
1123
  constructor(options: LunoraClientOptions);
870
1124
  /**
871
1125
  * Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
@@ -880,6 +1134,47 @@ declare class LunoraClient {
880
1134
  setAuthToken(token: string | null): void;
881
1135
  getAuthToken(): string | null;
882
1136
  /**
1137
+ * The current identity fingerprint (the same stamp queued offline writes
1138
+ * carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
1139
+ * owns its own at-least-once replay outside the built-in `OfflineQueue` —
1140
+ * can drop a persisted write whose captured `identity` no longer matches the
1141
+ * signed-in user, the guard the queue path applies in `flushOfflineQueue`.
1142
+ */
1143
+ currentIdentity(): string | null;
1144
+ /** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
1145
+ clientIdentifier(): string;
1146
+ /**
1147
+ * The highest custom-mutator watermark the server has echoed for this client
1148
+ * on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
1149
+ * its `clientSeq` generator from this so a reload never reissues a sequence
1150
+ * the server has already applied (which it would swallow as a replay, silently
1151
+ * dropping the write).
1152
+ */
1153
+ confirmedMutationWatermark(shardKey?: string): number;
1154
+ /**
1155
+ * Push a custom mutator to its authoritative server impl over the watermark
1156
+ * protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
1157
+ * `x-lunora-client-seq`, so the DO runs it exactly once and advances this
1158
+ * client's `__client_watermark`.
1159
+ *
1160
+ * Returns the server `result` plus `applied`: `true` when the DO ran this push
1161
+ * as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
1162
+ * was at or below the stored watermark — e.g. a stale sequence after a reload).
1163
+ * A `false` verdict tells the caller to reissue above the now-known watermark
1164
+ * (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
1165
+ * ack as a confirmed write. Every ack — applied or not — bumps the watermark.
1166
+ *
1167
+ * This is the online transport for `@lunora/db`'s client-mutator runtime; the
1168
+ * optimistic overlay + durable-outbox concerns live in that runtime, not here.
1169
+ */
1170
+ callMutator(functionPath: string, args: Record<string, unknown>, options?: {
1171
+ clientSeq?: number;
1172
+ shardKey?: string;
1173
+ }): Promise<{
1174
+ applied: boolean;
1175
+ result: unknown;
1176
+ }>;
1177
+ /**
883
1178
  * Subscribe to auth-token changes. Returns an unsubscribe function. The
884
1179
  * listener is NOT invoked on registration — use {@link getAuthToken} for
885
1180
  * the current value.
@@ -1069,8 +1364,12 @@ declare class LunoraClient {
1069
1364
  * List a workflow's instances via the admin Workflows proxy
1070
1365
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1366
  * 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.
1367
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1368
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1369
+ * false }` sentinel, so the result resolves with `configured === false` and an
1370
+ * empty `instances` list — callers should branch on that flag rather than
1371
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1372
+ * `name` is the deployed workflow name.
1074
1373
  */
1075
1374
  listWorkflowInstances(options: {
1076
1375
  name: string;
@@ -1404,6 +1703,26 @@ declare class LunoraClient {
1404
1703
  shardKey?: string;
1405
1704
  }): Unsubscribe;
1406
1705
  /**
1706
+ * Subscribe to a declarative **shape** — server-side partial replication
1707
+ * scoped by `shardBy` + the shape's predicate + RLS. The parallel to
1708
+ * {@link subscribe} for the poke protocol: the client sends the shape *name* +
1709
+ * validated `args` (never a `where` the client could forge), the server seeds
1710
+ * the current membership as an insert-poke and streams live membership diffs.
1711
+ * Each applied poke materializes the shape's rowset and invokes `callback`.
1712
+ *
1713
+ * Unlike {@link subscribe}, shape subscriptions are NOT deduped by
1714
+ * (name, args): the server resolves them under the socket's verified identity,
1715
+ * so every call gets its own id + view. The returned function unsubscribes.
1716
+ */
1717
+ subscribeShape(shape: {
1718
+ args?: Record<string, unknown>;
1719
+ name: string;
1720
+ }, callback: ShapeCallback, options?: {
1721
+ onCheckpoint?: (watermark: SyncWatermark) => void;
1722
+ onError?: SubscriptionErrorCallback;
1723
+ shardKey?: string;
1724
+ }): Unsubscribe;
1725
+ /**
1407
1726
  * Open a streaming query. The function reference must be a
1408
1727
  * `kind:"stream"` registration (built with `c.query.input(...).stream(...)`);
1409
1728
  * the type constraint catches accidental use of a query/mutation/action
@@ -1427,6 +1746,18 @@ declare class LunoraClient {
1427
1746
  }): StreamIterable<ReturnOf<F>>;
1428
1747
  close(): void;
1429
1748
  /**
1749
+ * Persist a mutation that can't go out on the wire right now (offline, or
1750
+ * mid-reconnect after a prior connect). The optimistic update has already
1751
+ * been applied by `mutation`; this only chooses the durable write path and
1752
+ * rolls the optimistic write back if persistence is rejected.
1753
+ *
1754
+ * Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
1755
+ * owns persistence + at-least-once replay, so we delegate and return
1756
+ * optimistically (confirmation rides the synced view). Otherwise the
1757
+ * built-in `OfflineQueue` resolves/rejects the returned promise on replay.
1758
+ */
1759
+ private enqueueOfflineMutation;
1760
+ /**
1430
1761
  * Restore offline mutations persisted in a prior session and open a socket
1431
1762
  * for each shard they target so they flush once the WS reconnects. Failures
1432
1763
  * are swallowed — a broken durable store must not stop the client booting.
@@ -1523,6 +1854,13 @@ declare class LunoraClient {
1523
1854
  * to the lifecycle dispatch.
1524
1855
  */
1525
1856
  private sendConnectEnvelope;
1857
+ /**
1858
+ * Re-send every shape subscription bound to `shardKey` over its (now open)
1859
+ * socket. Each frame carries the shape's last applied checkpoint, so the
1860
+ * server resumes from it — or re-seeds when the cursor fell below CDC
1861
+ * retention or the epoch forked.
1862
+ */
1863
+ private resendShapeSubscriptions;
1526
1864
  private ensureSocket;
1527
1865
  private handleDisconnect;
1528
1866
  /**
@@ -1538,8 +1876,14 @@ declare class LunoraClient {
1538
1876
  /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
1539
1877
  private markShardPendingAck;
1540
1878
  private sendSubscribeIfOpen;
1879
+ private sendShapeSubscribeIfOpen;
1541
1880
  private handleServerMessage;
1542
1881
  private handleErrorMessage;
1882
+ private handlePokeStart;
1883
+ private handlePokePart;
1884
+ private handlePokeEnd;
1885
+ /** Materialize a shape's keyed view to an array and invoke its callbacks. */
1886
+ private emitShapeRows;
1543
1887
  private handleDataMessage;
1544
1888
  /**
1545
1889
  * Handle a `resume` frame (Pillar 1b): the server proved nothing the
@@ -1594,4 +1938,4 @@ declare class LunoraClient {
1594
1938
  private clearQueryCacheForIdentityChange;
1595
1939
  private flushOfflineQueue;
1596
1940
  }
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 };
1941
+ export { SyncWatermark as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, ServerMessage as E, FunctionReference as F, GlobalFacetResult as G, ServerPokeEndMessage as H, ServerPokePartMessage as I, ServerPokeStartMessage as J, ShardTrafficEntry as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficResult as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, StorageListPage as T, User as U, StorageObject as V, StreamHandle as W, StreamIterable as X, SubscriptionCallback as Y, SubscriptionRegistry as Z, SubscriptionState as _, Unsubscribe as a, WorkflowInstanceAction as a0, WorkflowInstanceDetail as a1, WorkflowInstancePage as a2, WorkflowInstanceStatus as a3, WorkflowInstanceSummary as a4, WorkflowStepDetail as a5, createLocalStore as a6, createStream as a7, 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, OptimisticLocalStore as p, OptimisticUpdate as q, OutboxMutation as r, OutboxSink as s, PersistedMutation as t, RowOp as u, RpcEnvelope as v, RpcResponseBody as w, ScheduleRecord as x, SchedulerPoolStatus as y, SchedulerStatus as z };