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