@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.
- package/README.md +2 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +136 -5
- package/dist/index.d.ts +136 -5
- package/dist/index.mjs +8 -7
- package/dist/packem_shared/{LunoraClient-B00f7VUM.mjs → LunoraClient-6J-4BGWK.mjs} +744 -187
- package/dist/packem_shared/OfflineQueue-BI0FNNvc.mjs +1 -0
- package/dist/packem_shared/SubscriptionRegistry-Dn-7k7eo.mjs +1 -0
- package/dist/packem_shared/{createIndexedDbPersistence-CW82inU5.mjs → createInMemoryPersistence-DlFjWtOm.mjs} +13 -1
- package/dist/packem_shared/{createIndexedDbQueryCache-B1PQ9Twl.mjs → createInMemoryQueryCache-4AkTV38-.mjs} +13 -1
- package/dist/packem_shared/createLocalStore-IOur0jHF.mjs +1 -0
- package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +31 -0
- package/dist/packem_shared/{createServerClient-DyFIHnWZ.mjs → createServerClient-CtklXrs2.mjs} +1 -1
- package/dist/packem_shared/local-store-BNgN3Dw3.mjs +111 -0
- package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.mts → lunora-client.d-Dm1BYq19.d.mts} +652 -43
- package/dist/packem_shared/{lunora-client.d-DGvyuJ_p.d.ts → lunora-client.d-Dm1BYq19.d.ts} +652 -43
- package/dist/packem_shared/{OfflineQueue-D5p_QgF_.mjs → offline-queue-7Wc4onA0.mjs} +42 -5
- package/dist/packem_shared/{preload.d-dSaRMuhL.d.mts → preload.d-BfWzGJWV.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-BoDmFqSG.d.ts → preload.d-Sp_Ef-_u.d.ts} +1 -1
- package/dist/packem_shared/subscription-C1Jy7HiF.mjs +55 -0
- package/dist/query/index.d.mts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/ssr/index.d.mts +3 -3
- package/dist/ssr/index.d.ts +3 -3
- package/dist/ssr/index.mjs +2 -2
- package/package.json +2 -2
- package/dist/packem_shared/SubscriptionRegistry-B-Qx_Gux.mjs +0 -26
- package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
- /package/dist/packem_shared/{createStream-BDkqO5PW.mjs → DEFAULT_MAX_BUFFER-BDkqO5PW.mjs} +0 -0
- /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
|
|
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.
|
|
204
|
-
*
|
|
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 <= 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
|
-
/**
|
|
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
|
-
|
|
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,
|
|
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`
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
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
|
|
693
|
-
* mutation's shard key
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
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,
|
|
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 `<file>:<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)
|
|
1073
|
-
*
|
|
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
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
|
|
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
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
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 cursor — its `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,
|
|
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 };
|