@lunora/client 1.0.0-alpha.21 → 1.0.0-alpha.22
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/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +212 -4
- package/dist/index.d.ts +212 -4
- package/dist/index.mjs +9 -4
- package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +100 -0
- package/dist/packem_shared/{LunoraClient-kXpHNyaE.mjs → LunoraClient-Clb118SU.mjs} +279 -22
- package/dist/packem_shared/{OfflineQueue-GGYJRmhF.mjs → OfflineQueue-B4HUF7rt.mjs} +1 -1
- package/dist/packem_shared/SubscriptionRegistry-D4jfIzZu.mjs +31 -0
- package/dist/packem_shared/TabCoordinator-BwRR8H06.mjs +222 -0
- package/dist/packem_shared/createClientQuery-CQ51bWAE.mjs +71 -0
- package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +2 -0
- package/dist/packem_shared/createReply-lI4tVS2w.mjs +36 -0
- package/dist/packem_shared/{createServerClient-DF-3mLmb.mjs → createServerClient-Dxemst5C.mjs} +1 -1
- package/dist/packem_shared/createSnapshotPrecondition-CBwnVz6r.mjs +18 -0
- package/dist/packem_shared/{local-store-BveBeFEo.mjs → local-store-DtcIW4c0.mjs} +1 -1
- package/dist/packem_shared/{lunora-client.d-BYkEjCEJ.d.mts → lunora-client.d-pw-9sLl0.d.mts} +183 -1
- package/dist/packem_shared/{lunora-client.d-BYkEjCEJ.d.ts → lunora-client.d-pw-9sLl0.d.ts} +183 -1
- package/dist/packem_shared/{offline-queue-B9vfdSqp.mjs → offline-queue-CF4_Co5k.mjs} +29 -0
- package/dist/packem_shared/{preload.d-DrfuisCE.d.mts → preload.d-6ME5ubgq.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-B-vyHnml.d.ts → preload.d-BkQr-3Vh.d.ts} +1 -1
- package/dist/packem_shared/{subscription-BjynOXCU.mjs → stable-key-wv6eP48B.mjs} +1 -29
- 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 +1 -1
- package/package.json +1 -1
- package/dist/packem_shared/SubscriptionRegistry-DjGKZsqq.mjs +0 -1
- package/dist/packem_shared/createLocalStore-jRoqmazl.mjs +0 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class ClientQueryStore {
|
|
2
|
+
/** Current values, keyed by the ref's stable key. Absent = never set. */
|
|
3
|
+
values = /* @__PURE__ */ new Map();
|
|
4
|
+
/** Subscribers keyed by ref key — notified on every set. */
|
|
5
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Return the current value for `ref`, or `ref.defaultValue` if none has
|
|
8
|
+
* been set explicitly. Returns `ref.defaultValue` when the slot has been
|
|
9
|
+
* set to `undefined` (which is distinct from "never set").
|
|
10
|
+
*/
|
|
11
|
+
get(ref) {
|
|
12
|
+
if (this.values.has(ref.key)) {
|
|
13
|
+
return this.values.get(ref.key);
|
|
14
|
+
}
|
|
15
|
+
return ref.defaultValue;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set a new value for `ref` and notify every subscriber. Pass `undefined`
|
|
19
|
+
* to reset the slot to `ref.defaultValue`.
|
|
20
|
+
*/
|
|
21
|
+
set(ref, value) {
|
|
22
|
+
this.values.set(ref.key, value);
|
|
23
|
+
this.notify(ref.key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Delete the stored value for `ref`, resetting to `ref.defaultValue` and
|
|
27
|
+
* notifying subscribers.
|
|
28
|
+
*/
|
|
29
|
+
reset(ref) {
|
|
30
|
+
this.values.delete(ref.key);
|
|
31
|
+
this.notify(ref.key);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to changes for `ref`. The callback is NOT invoked on
|
|
35
|
+
* registration — callers should read the current value via
|
|
36
|
+
* {@link get} first. Returns an unsubscribe function.
|
|
37
|
+
*/
|
|
38
|
+
subscribe(ref, callback) {
|
|
39
|
+
let subs = this.subscribers.get(ref.key);
|
|
40
|
+
if (!subs) {
|
|
41
|
+
subs = /* @__PURE__ */ new Set();
|
|
42
|
+
this.subscribers.set(ref.key, subs);
|
|
43
|
+
}
|
|
44
|
+
subs.add(callback);
|
|
45
|
+
return () => {
|
|
46
|
+
subs.delete(callback);
|
|
47
|
+
if (subs.size === 0) {
|
|
48
|
+
this.subscribers.delete(ref.key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Notify every subscriber of a value change for the given key. */
|
|
53
|
+
notify(key) {
|
|
54
|
+
const subs = this.subscribers.get(key);
|
|
55
|
+
if (!subs) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const value = this.values.get(key);
|
|
59
|
+
for (const callback of subs) {
|
|
60
|
+
try {
|
|
61
|
+
callback(value);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const createClientQuery = (key, defaultValue) => {
|
|
68
|
+
return { defaultValue, key };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { ClientQueryStore, createClientQuery };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const sendToSw = (sw, message, expectResponse = false) => new Promise((resolve, reject) => {
|
|
2
|
+
if (!sw) {
|
|
3
|
+
reject(new Error("No active service worker"));
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const id = message.correlationId ?? crypto.randomUUID();
|
|
7
|
+
const outgoingMessage = { ...message, correlationId: id };
|
|
8
|
+
if (expectResponse) {
|
|
9
|
+
let timer;
|
|
10
|
+
const handler = (event) => {
|
|
11
|
+
if (event.data.correlationId === id) {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
navigator.serviceWorker.removeEventListener("message", handler);
|
|
14
|
+
resolve(event.data.payload);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
navigator.serviceWorker.removeEventListener("message", handler);
|
|
19
|
+
reject(new Error(`SW message ${id} timed out`));
|
|
20
|
+
}, 3e4);
|
|
21
|
+
navigator.serviceWorker.addEventListener("message", handler);
|
|
22
|
+
sw.postMessage(outgoingMessage);
|
|
23
|
+
} else {
|
|
24
|
+
sw.postMessage(outgoingMessage);
|
|
25
|
+
resolve(void 0);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const createReply = (original, payload) => {
|
|
29
|
+
return {
|
|
30
|
+
type: `${original.type}:reply`,
|
|
31
|
+
payload,
|
|
32
|
+
correlationId: original.correlationId
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { createReply, sendToSw };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
|
|
2
|
+
|
|
3
|
+
const createSnapshotPrecondition = (client, functionRef, args, shardKey) => {
|
|
4
|
+
const snapshot = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
|
|
5
|
+
const snapshotKey = snapshot === void 0 ? void 0 : stableStringify(snapshot);
|
|
6
|
+
return () => {
|
|
7
|
+
const current = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
|
|
8
|
+
if (snapshotKey === void 0 && current === void 0) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (snapshotKey === void 0 || current === void 0) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return stableStringify(current) === snapshotKey;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { createSnapshotPrecondition as default };
|
package/dist/packem_shared/{lunora-client.d-BYkEjCEJ.d.mts → lunora-client.d-pw-9sLl0.d.mts}
RENAMED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
|
|
2
2
|
/**
|
|
3
|
+
* Reactive key-value store for local-only client state.
|
|
4
|
+
*
|
|
5
|
+
* Unlike a server {@link SubscriptionState} (which tracks a live WS connection,
|
|
6
|
+
* an `acked` flag, `serverBase`, optimistic layers, and the full subscription
|
|
7
|
+
* machinery), a `ClientQueryRef` is purely local — no server round-trip, no
|
|
8
|
+
* WebSocket, no persistence. It exists so framework adapters can offer a
|
|
9
|
+
* `useClientQuery` hook whose values survive component remounts and are shared
|
|
10
|
+
* across every consumer of the same ref, with none of the ceremony or coupling
|
|
11
|
+
* of a dedicated context provider.
|
|
12
|
+
*
|
|
13
|
+
* The store lives inside `LunoraClient` (a private field) and is surfaced through
|
|
14
|
+
* `client.getClientQuery(ref)` / `setClientQuery(ref, value)` /
|
|
15
|
+
* `subscribeClientQuery(ref, callback)`.
|
|
16
|
+
*/
|
|
17
|
+
/** Opaque handle for a typed client-local query slot. */
|
|
18
|
+
interface ClientQueryRef<T = unknown> {
|
|
19
|
+
/** Default value when no value has been set explicitly. */
|
|
20
|
+
readonly defaultValue: T;
|
|
21
|
+
/** Stable identity for the slot. Must be unique within a client instance. */
|
|
22
|
+
readonly key: string;
|
|
23
|
+
}
|
|
24
|
+
/** A subscriber callback for value changes to a {@link ClientQueryRef}. */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a typed {@link ClientQueryRef}. Call once per slot at module scope
|
|
28
|
+
* (or inside a component module) — the ref object is the stable identity.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // lunora/client-queries.ts
|
|
32
|
+
* import { createClientQuery } from "@lunora/client";
|
|
33
|
+
*
|
|
34
|
+
* export const sidebarOpen = createClientQuery("sidebarOpen", true);
|
|
35
|
+
* export const selectedMessageId = createClientQuery("selectedMessageId", undefined as string | undefined);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
|
|
39
|
+
/**
|
|
3
40
|
* The machine-readable error codes a client can observe on a failed
|
|
4
41
|
* RPC/batch/subscription. Mirrors the server's `CODE_STATUS` keys
|
|
5
42
|
* (`@lunora/server`'s `error.ts`) by hand — the client is framework-neutral and
|
|
@@ -308,6 +345,16 @@ interface LunoraClientOptions {
|
|
|
308
345
|
* Defaults to 10000 (10s); set to `0` (or negative) to disable.
|
|
309
346
|
*/
|
|
310
347
|
connectTimeoutMs?: number;
|
|
348
|
+
/**
|
|
349
|
+
* When `true`, tabs sharing the same origin coordinate via BroadcastChannel
|
|
350
|
+
* so only one tab (the "leader") opens WebSocket connections to the server.
|
|
351
|
+
* Follower tabs receive subscription data through the channel instead.
|
|
352
|
+
*
|
|
353
|
+
* Reduces simultaneous WS connections, bandwidth, and cross-tab state drift.
|
|
354
|
+
* Requires `BroadcastChannel` (browser-only); silently ignored otherwise.
|
|
355
|
+
* Defaults to `false`.
|
|
356
|
+
*/
|
|
357
|
+
crossTabSync?: boolean;
|
|
311
358
|
fetch?: typeof fetch;
|
|
312
359
|
/**
|
|
313
360
|
* Interval (ms) between keepalive pings sent on each open subscription
|
|
@@ -317,6 +364,15 @@ interface LunoraClientOptions {
|
|
|
317
364
|
* `0` (or a negative value) to disable the heartbeat entirely.
|
|
318
365
|
*/
|
|
319
366
|
heartbeatIntervalMs?: number;
|
|
367
|
+
/**
|
|
368
|
+
* When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
|
|
369
|
+
* wait for the durable cache to finish hydrating before their first render
|
|
370
|
+
* with an enabled subscription, so users see cached data instead of an
|
|
371
|
+
* undefined flash before the socket round-trip. Defaults to `false`.
|
|
372
|
+
*
|
|
373
|
+
* Requires `queryCache` to be set (not `false`); silently ignored otherwise.
|
|
374
|
+
*/
|
|
375
|
+
hydrateOnStart?: boolean;
|
|
320
376
|
offlineQueue?: OfflineQueueOptions;
|
|
321
377
|
/**
|
|
322
378
|
* Durable outbox seam for offline writes. When supplied (the `@lunora/db`
|
|
@@ -1178,6 +1234,14 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
|
|
|
1178
1234
|
* once; every write is rolled back atomically if the mutation fails.
|
|
1179
1235
|
*/
|
|
1180
1236
|
optimisticUpdate?: OptimisticUpdate<TArgs>;
|
|
1237
|
+
/**
|
|
1238
|
+
* Sync predicate evaluated just before the offline queue replays this
|
|
1239
|
+
* write on reconnect. When it returns `false` the mutation is dropped
|
|
1240
|
+
* instead of replayed — use it to guard against replaying writes whose
|
|
1241
|
+
* assumptions are no longer valid (e.g. the document it referred to was
|
|
1242
|
+
* deleted by another client while this tab was offline).
|
|
1243
|
+
*/
|
|
1244
|
+
precondition?: () => boolean;
|
|
1181
1245
|
shardKey?: string;
|
|
1182
1246
|
}
|
|
1183
1247
|
/** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
|
|
@@ -1226,8 +1290,19 @@ type BatchSlot = {
|
|
|
1226
1290
|
declare class LunoraClient {
|
|
1227
1291
|
/** 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. */
|
|
1228
1292
|
private static readonly MAX_POKE_BUFFERS;
|
|
1293
|
+
/**
|
|
1294
|
+
* Create a typed {@link ClientQueryRef}. Convenience wrapper around
|
|
1295
|
+
* {@link createClientQuery} so you don't need a separate import.
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```ts
|
|
1298
|
+
* const sidebarOpen = LunoraClient.createClientQuery("sidebarOpen", true);
|
|
1299
|
+
* ```
|
|
1300
|
+
*/
|
|
1301
|
+
static createClientQuery<T>(key: string, defaultValue: T): ClientQueryRef<T>;
|
|
1229
1302
|
readonly url: string;
|
|
1230
1303
|
readonly wsUrl: string;
|
|
1304
|
+
/** Local reactive store for {@link ClientQueryRef} values — no server round-trip. Private; reach it via `getClientQuery` / `setClientQuery` / `subscribeClientQuery`. */
|
|
1305
|
+
private readonly clientQueryStore;
|
|
1231
1306
|
private wsToken;
|
|
1232
1307
|
/** Better-auth base path (trailing slash stripped) for the `get-session` lookup. */
|
|
1233
1308
|
private readonly authBasePath;
|
|
@@ -1249,6 +1324,20 @@ declare class LunoraClient {
|
|
|
1249
1324
|
/** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
|
|
1250
1325
|
private readonly clientId;
|
|
1251
1326
|
/**
|
|
1327
|
+
* `true` when the constructor's hydration microtask has finished loading the
|
|
1328
|
+
* durable read cache (Pillar 2) into `hydratedQueryCache`. Signals that
|
|
1329
|
+
* the cache is ready for synchronous `peekHydratedQuery` reads.
|
|
1330
|
+
*/
|
|
1331
|
+
private readyResolved;
|
|
1332
|
+
/** Resolvers for `whenReady()` — called once hydration completes. */
|
|
1333
|
+
private readyResolve;
|
|
1334
|
+
/**
|
|
1335
|
+
* Promise that resolves once the durable read cache has been loaded. When
|
|
1336
|
+
* `hydrateOnStart` is not set or no query cache is configured, resolves
|
|
1337
|
+
* immediately (the constructor creates an already-resolved promise).
|
|
1338
|
+
*/
|
|
1339
|
+
private readonly readyPromise;
|
|
1340
|
+
/**
|
|
1252
1341
|
* Highest custom-mutator watermark the server has echoed for this client,
|
|
1253
1342
|
* keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
|
|
1254
1343
|
* `__client_watermark` per shard. `callMutator` bumps it from every
|
|
@@ -1281,6 +1370,12 @@ declare class LunoraClient {
|
|
|
1281
1370
|
private readonly pendingCacheWrites;
|
|
1282
1371
|
private cacheFlushTimer;
|
|
1283
1372
|
private readonly subscriptions;
|
|
1373
|
+
/**
|
|
1374
|
+
* Cross-tab coordinator; created only when `crossTabSync: true`. When the
|
|
1375
|
+
* client is not the elected leader, all WebSocket operations are skipped.
|
|
1376
|
+
* Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
|
|
1377
|
+
*/
|
|
1378
|
+
private tabCoordinator;
|
|
1284
1379
|
/** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
|
|
1285
1380
|
private readonly connections;
|
|
1286
1381
|
/** Default `connect`-envelope context applied to a shard with no explicit override. */
|
|
@@ -1559,6 +1654,93 @@ declare class LunoraClient {
|
|
|
1559
1654
|
* unsubscribe function. See {@link MutationSettledEvent}.
|
|
1560
1655
|
*/
|
|
1561
1656
|
onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
|
|
1657
|
+
/**
|
|
1658
|
+
* Read the current value for a {@link ClientQueryRef}. Returns
|
|
1659
|
+
* `ref.defaultValue` when no value has been explicitly set.
|
|
1660
|
+
*/
|
|
1661
|
+
getClientQuery<T>(ref: ClientQueryRef<T>): T;
|
|
1662
|
+
/**
|
|
1663
|
+
* Set a new value for `ref` and notify every subscriber. Pass `undefined`
|
|
1664
|
+
* to reset the slot to `ref.defaultValue`.
|
|
1665
|
+
*/
|
|
1666
|
+
setClientQuery<T>(ref: ClientQueryRef<T>, value: T): void;
|
|
1667
|
+
/**
|
|
1668
|
+
* Subscribe to changes for `ref`. The callback is NOT invoked on
|
|
1669
|
+
* registration — call {@link getClientQuery} for the current value.
|
|
1670
|
+
* Returns an unsubscribe function.
|
|
1671
|
+
*/
|
|
1672
|
+
subscribeClientQuery(ref: ClientQueryRef, callback: (value: unknown) => void): Unsubscribe;
|
|
1673
|
+
/**
|
|
1674
|
+
* Reset a {@link ClientQueryRef} to its default value, notifying every
|
|
1675
|
+
* subscriber. Equivalent to `setClientQuery(ref, ref.defaultValue)` but
|
|
1676
|
+
* removes the stored entry so a future {@link getClientQuery} returns
|
|
1677
|
+
* the default rather than an explicitly-set value.
|
|
1678
|
+
*/
|
|
1679
|
+
resetClientQuery(ref: ClientQueryRef): void;
|
|
1680
|
+
/**
|
|
1681
|
+
* Capture a snapshot of the current live query value at call time and
|
|
1682
|
+
* produce a `() => boolean` precondition that compares it against the
|
|
1683
|
+
* value at replay time (on queue drain / reconnect).
|
|
1684
|
+
*
|
|
1685
|
+
* When the precondition is checked it re-reads the query's current value
|
|
1686
|
+
* via `peekActiveQueryValue`. If the value differs from what was
|
|
1687
|
+
* captured at call time the precondition returns `false` and the offline
|
|
1688
|
+
* mutation is dropped as stale.
|
|
1689
|
+
*
|
|
1690
|
+
* This is a method wrapper around `createSnapshotPrecondition` that
|
|
1691
|
+
* binds the client instance for you — no need to pass `client` explicitly.
|
|
1692
|
+
* @example
|
|
1693
|
+
* ```ts
|
|
1694
|
+
* client.mutation(api.todos.update, { id, text }, {
|
|
1695
|
+
* precondition: client.snapshotPrecondition(api.todos.list, { userId }),
|
|
1696
|
+
* });
|
|
1697
|
+
* ```
|
|
1698
|
+
*/
|
|
1699
|
+
snapshotPrecondition(functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string): () => boolean;
|
|
1700
|
+
/**
|
|
1701
|
+
* Resolves once the durable read cache has been loaded into memory. When
|
|
1702
|
+
* `hydrateOnStart` is not configured or no query cache adapter is active,
|
|
1703
|
+
* returns an already-resolved promise so callers can always await it
|
|
1704
|
+
* unconditionally.
|
|
1705
|
+
*
|
|
1706
|
+
* Framework adapters (React, Vue, etc.) use this to gate the first
|
|
1707
|
+
* (enabled) render of a live query behind hydration, so the user sees
|
|
1708
|
+
* cached data instead of an undefined flash before the socket round-trip.
|
|
1709
|
+
*/
|
|
1710
|
+
whenReady(): Promise<void>;
|
|
1711
|
+
/**
|
|
1712
|
+
* Synchronously reports whether {@link whenReady} has already resolved (the
|
|
1713
|
+
* durable read cache is loaded, or none is configured). Framework adapters
|
|
1714
|
+
* read this to seed the hydration-gate state on the first render without
|
|
1715
|
+
* awaiting, then subscribe via {@link whenReady} for the pending case.
|
|
1716
|
+
*/
|
|
1717
|
+
get isReady(): boolean;
|
|
1718
|
+
/**
|
|
1719
|
+
* Synchronously peek at a value the durable read cache loaded for the given
|
|
1720
|
+
* function path + args + shard key. Returns `undefined` when:
|
|
1721
|
+
*
|
|
1722
|
+
* - No query cache adapter is configured.
|
|
1723
|
+
* - Hydration hasn't completed yet (race — await {@link whenReady} first).
|
|
1724
|
+
* - The cached value's identity fingerprint doesn't match the current auth.
|
|
1725
|
+
*
|
|
1726
|
+
* Unlike the internal {@link takeHydratedCache}, this is a READ-ONLY peek:
|
|
1727
|
+
* the cached entry stays in `hydratedQueryCache` so the subscription created
|
|
1728
|
+
* later by {@link subscribe} consumes it normally.
|
|
1729
|
+
*/
|
|
1730
|
+
peekHydratedQuery(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
|
|
1731
|
+
/**
|
|
1732
|
+
* Peek at the **current live value** of an active subscription, if one
|
|
1733
|
+
* exists. Returns the subscription's `lastValue` (which includes any
|
|
1734
|
+
* optimistic overlay) or `undefined` if no subscription is active for the
|
|
1735
|
+
* given `(functionPath, args, shardKey)`.
|
|
1736
|
+
*
|
|
1737
|
+
* Unlike {@link peekHydratedQuery} (which reads from the durable read cache
|
|
1738
|
+
* and is independent of active subscriptions), this method reflects the
|
|
1739
|
+
* current in-memory state of an already-opened subscription — useful for
|
|
1740
|
+
* offline mutation preconditions that need to snapshot the value at call time
|
|
1741
|
+
* and compare it at replay time.
|
|
1742
|
+
*/
|
|
1743
|
+
peekActiveQueryValue(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
|
|
1562
1744
|
query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
|
|
1563
1745
|
shardKey?: string;
|
|
1564
1746
|
}): Promise<ReturnOf<F>>;
|
|
@@ -2537,4 +2719,4 @@ declare class LunoraClient {
|
|
|
2537
2719
|
*/
|
|
2538
2720
|
private settleReplayBatchSlots;
|
|
2539
2721
|
}
|
|
2540
|
-
export {
|
|
2722
|
+
export { StorageObject as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, RowOp as E, FunctionReference as F, GlobalFacetResult as G, RpcEnvelope as H, RpcResponseBody as I, ScheduleRecord as J, SchedulerPoolStatus as K, LunoraClient as L, MutationCallOptions as M, SchedulerStatus as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerMessage as T, User as U, ServerPokeEndMessage as V, ServerPokePartMessage as W, ServerPokeStartMessage as X, ShardTrafficEntry as Y, ShardTrafficResult as Z, StorageListPage as _, Unsubscribe as a, StreamHandle as a0, StreamIterable as a1, SubscriptionCallback as a2, SubscriptionRegistry as a3, SubscriptionState as a4, SyncWatermark as a5, WorkflowInstanceAction as a6, WorkflowInstanceDetail as a7, WorkflowInstancePage as a8, WorkflowInstanceStatus as a9, WorkflowInstanceSummary as aa, WorkflowStepDetail as ab, createClientQuery as ac, createLocalStore as ad, createStream as ae, getErrorCode as af, getRetryAfterMs as ag, isConflictError as ah, isForbiddenError as ai, isRateLimitedError as aj, isUnauthorizedError as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, CachedQuery as f, ClientMessage as g, ClientQueryRef as h, ClientShapeSubscribeMessage as i, ClientShapeUnsubscribeMessage as j, ConnectionStatus as k, FunctionArgumentDescriptor as l, FunctionDescriptor as m, GlobalFacetValue as n, GlobalFilterClause as o, GlobalTableInfo as p, GlobalTablePage as q, LunoraClientError as r, LunoraClientOptions as s, LunoraErrorCode as t, MutationSettledEvent as u, OptimisticLocalStore as v, OptimisticUpdate as w, OutboxMutation as x, OutboxSink as y, PersistedMutation as z };
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
|
|
2
2
|
/**
|
|
3
|
+
* Reactive key-value store for local-only client state.
|
|
4
|
+
*
|
|
5
|
+
* Unlike a server {@link SubscriptionState} (which tracks a live WS connection,
|
|
6
|
+
* an `acked` flag, `serverBase`, optimistic layers, and the full subscription
|
|
7
|
+
* machinery), a `ClientQueryRef` is purely local — no server round-trip, no
|
|
8
|
+
* WebSocket, no persistence. It exists so framework adapters can offer a
|
|
9
|
+
* `useClientQuery` hook whose values survive component remounts and are shared
|
|
10
|
+
* across every consumer of the same ref, with none of the ceremony or coupling
|
|
11
|
+
* of a dedicated context provider.
|
|
12
|
+
*
|
|
13
|
+
* The store lives inside `LunoraClient` (a private field) and is surfaced through
|
|
14
|
+
* `client.getClientQuery(ref)` / `setClientQuery(ref, value)` /
|
|
15
|
+
* `subscribeClientQuery(ref, callback)`.
|
|
16
|
+
*/
|
|
17
|
+
/** Opaque handle for a typed client-local query slot. */
|
|
18
|
+
interface ClientQueryRef<T = unknown> {
|
|
19
|
+
/** Default value when no value has been set explicitly. */
|
|
20
|
+
readonly defaultValue: T;
|
|
21
|
+
/** Stable identity for the slot. Must be unique within a client instance. */
|
|
22
|
+
readonly key: string;
|
|
23
|
+
}
|
|
24
|
+
/** A subscriber callback for value changes to a {@link ClientQueryRef}. */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a typed {@link ClientQueryRef}. Call once per slot at module scope
|
|
28
|
+
* (or inside a component module) — the ref object is the stable identity.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // lunora/client-queries.ts
|
|
32
|
+
* import { createClientQuery } from "@lunora/client";
|
|
33
|
+
*
|
|
34
|
+
* export const sidebarOpen = createClientQuery("sidebarOpen", true);
|
|
35
|
+
* export const selectedMessageId = createClientQuery("selectedMessageId", undefined as string | undefined);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
|
|
39
|
+
/**
|
|
3
40
|
* The machine-readable error codes a client can observe on a failed
|
|
4
41
|
* RPC/batch/subscription. Mirrors the server's `CODE_STATUS` keys
|
|
5
42
|
* (`@lunora/server`'s `error.ts`) by hand — the client is framework-neutral and
|
|
@@ -308,6 +345,16 @@ interface LunoraClientOptions {
|
|
|
308
345
|
* Defaults to 10000 (10s); set to `0` (or negative) to disable.
|
|
309
346
|
*/
|
|
310
347
|
connectTimeoutMs?: number;
|
|
348
|
+
/**
|
|
349
|
+
* When `true`, tabs sharing the same origin coordinate via BroadcastChannel
|
|
350
|
+
* so only one tab (the "leader") opens WebSocket connections to the server.
|
|
351
|
+
* Follower tabs receive subscription data through the channel instead.
|
|
352
|
+
*
|
|
353
|
+
* Reduces simultaneous WS connections, bandwidth, and cross-tab state drift.
|
|
354
|
+
* Requires `BroadcastChannel` (browser-only); silently ignored otherwise.
|
|
355
|
+
* Defaults to `false`.
|
|
356
|
+
*/
|
|
357
|
+
crossTabSync?: boolean;
|
|
311
358
|
fetch?: typeof fetch;
|
|
312
359
|
/**
|
|
313
360
|
* Interval (ms) between keepalive pings sent on each open subscription
|
|
@@ -317,6 +364,15 @@ interface LunoraClientOptions {
|
|
|
317
364
|
* `0` (or a negative value) to disable the heartbeat entirely.
|
|
318
365
|
*/
|
|
319
366
|
heartbeatIntervalMs?: number;
|
|
367
|
+
/**
|
|
368
|
+
* When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
|
|
369
|
+
* wait for the durable cache to finish hydrating before their first render
|
|
370
|
+
* with an enabled subscription, so users see cached data instead of an
|
|
371
|
+
* undefined flash before the socket round-trip. Defaults to `false`.
|
|
372
|
+
*
|
|
373
|
+
* Requires `queryCache` to be set (not `false`); silently ignored otherwise.
|
|
374
|
+
*/
|
|
375
|
+
hydrateOnStart?: boolean;
|
|
320
376
|
offlineQueue?: OfflineQueueOptions;
|
|
321
377
|
/**
|
|
322
378
|
* Durable outbox seam for offline writes. When supplied (the `@lunora/db`
|
|
@@ -1178,6 +1234,14 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
|
|
|
1178
1234
|
* once; every write is rolled back atomically if the mutation fails.
|
|
1179
1235
|
*/
|
|
1180
1236
|
optimisticUpdate?: OptimisticUpdate<TArgs>;
|
|
1237
|
+
/**
|
|
1238
|
+
* Sync predicate evaluated just before the offline queue replays this
|
|
1239
|
+
* write on reconnect. When it returns `false` the mutation is dropped
|
|
1240
|
+
* instead of replayed — use it to guard against replaying writes whose
|
|
1241
|
+
* assumptions are no longer valid (e.g. the document it referred to was
|
|
1242
|
+
* deleted by another client while this tab was offline).
|
|
1243
|
+
*/
|
|
1244
|
+
precondition?: () => boolean;
|
|
1181
1245
|
shardKey?: string;
|
|
1182
1246
|
}
|
|
1183
1247
|
/** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
|
|
@@ -1226,8 +1290,19 @@ type BatchSlot = {
|
|
|
1226
1290
|
declare class LunoraClient {
|
|
1227
1291
|
/** 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. */
|
|
1228
1292
|
private static readonly MAX_POKE_BUFFERS;
|
|
1293
|
+
/**
|
|
1294
|
+
* Create a typed {@link ClientQueryRef}. Convenience wrapper around
|
|
1295
|
+
* {@link createClientQuery} so you don't need a separate import.
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```ts
|
|
1298
|
+
* const sidebarOpen = LunoraClient.createClientQuery("sidebarOpen", true);
|
|
1299
|
+
* ```
|
|
1300
|
+
*/
|
|
1301
|
+
static createClientQuery<T>(key: string, defaultValue: T): ClientQueryRef<T>;
|
|
1229
1302
|
readonly url: string;
|
|
1230
1303
|
readonly wsUrl: string;
|
|
1304
|
+
/** Local reactive store for {@link ClientQueryRef} values — no server round-trip. Private; reach it via `getClientQuery` / `setClientQuery` / `subscribeClientQuery`. */
|
|
1305
|
+
private readonly clientQueryStore;
|
|
1231
1306
|
private wsToken;
|
|
1232
1307
|
/** Better-auth base path (trailing slash stripped) for the `get-session` lookup. */
|
|
1233
1308
|
private readonly authBasePath;
|
|
@@ -1249,6 +1324,20 @@ declare class LunoraClient {
|
|
|
1249
1324
|
/** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
|
|
1250
1325
|
private readonly clientId;
|
|
1251
1326
|
/**
|
|
1327
|
+
* `true` when the constructor's hydration microtask has finished loading the
|
|
1328
|
+
* durable read cache (Pillar 2) into `hydratedQueryCache`. Signals that
|
|
1329
|
+
* the cache is ready for synchronous `peekHydratedQuery` reads.
|
|
1330
|
+
*/
|
|
1331
|
+
private readyResolved;
|
|
1332
|
+
/** Resolvers for `whenReady()` — called once hydration completes. */
|
|
1333
|
+
private readyResolve;
|
|
1334
|
+
/**
|
|
1335
|
+
* Promise that resolves once the durable read cache has been loaded. When
|
|
1336
|
+
* `hydrateOnStart` is not set or no query cache is configured, resolves
|
|
1337
|
+
* immediately (the constructor creates an already-resolved promise).
|
|
1338
|
+
*/
|
|
1339
|
+
private readonly readyPromise;
|
|
1340
|
+
/**
|
|
1252
1341
|
* Highest custom-mutator watermark the server has echoed for this client,
|
|
1253
1342
|
* keyed by shard bucket (`shardKey ?? ""`) since the DO tracks one
|
|
1254
1343
|
* `__client_watermark` per shard. `callMutator` bumps it from every
|
|
@@ -1281,6 +1370,12 @@ declare class LunoraClient {
|
|
|
1281
1370
|
private readonly pendingCacheWrites;
|
|
1282
1371
|
private cacheFlushTimer;
|
|
1283
1372
|
private readonly subscriptions;
|
|
1373
|
+
/**
|
|
1374
|
+
* Cross-tab coordinator; created only when `crossTabSync: true`. When the
|
|
1375
|
+
* client is not the elected leader, all WebSocket operations are skipped.
|
|
1376
|
+
* Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
|
|
1377
|
+
*/
|
|
1378
|
+
private tabCoordinator;
|
|
1284
1379
|
/** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
|
|
1285
1380
|
private readonly connections;
|
|
1286
1381
|
/** Default `connect`-envelope context applied to a shard with no explicit override. */
|
|
@@ -1559,6 +1654,93 @@ declare class LunoraClient {
|
|
|
1559
1654
|
* unsubscribe function. See {@link MutationSettledEvent}.
|
|
1560
1655
|
*/
|
|
1561
1656
|
onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
|
|
1657
|
+
/**
|
|
1658
|
+
* Read the current value for a {@link ClientQueryRef}. Returns
|
|
1659
|
+
* `ref.defaultValue` when no value has been explicitly set.
|
|
1660
|
+
*/
|
|
1661
|
+
getClientQuery<T>(ref: ClientQueryRef<T>): T;
|
|
1662
|
+
/**
|
|
1663
|
+
* Set a new value for `ref` and notify every subscriber. Pass `undefined`
|
|
1664
|
+
* to reset the slot to `ref.defaultValue`.
|
|
1665
|
+
*/
|
|
1666
|
+
setClientQuery<T>(ref: ClientQueryRef<T>, value: T): void;
|
|
1667
|
+
/**
|
|
1668
|
+
* Subscribe to changes for `ref`. The callback is NOT invoked on
|
|
1669
|
+
* registration — call {@link getClientQuery} for the current value.
|
|
1670
|
+
* Returns an unsubscribe function.
|
|
1671
|
+
*/
|
|
1672
|
+
subscribeClientQuery(ref: ClientQueryRef, callback: (value: unknown) => void): Unsubscribe;
|
|
1673
|
+
/**
|
|
1674
|
+
* Reset a {@link ClientQueryRef} to its default value, notifying every
|
|
1675
|
+
* subscriber. Equivalent to `setClientQuery(ref, ref.defaultValue)` but
|
|
1676
|
+
* removes the stored entry so a future {@link getClientQuery} returns
|
|
1677
|
+
* the default rather than an explicitly-set value.
|
|
1678
|
+
*/
|
|
1679
|
+
resetClientQuery(ref: ClientQueryRef): void;
|
|
1680
|
+
/**
|
|
1681
|
+
* Capture a snapshot of the current live query value at call time and
|
|
1682
|
+
* produce a `() => boolean` precondition that compares it against the
|
|
1683
|
+
* value at replay time (on queue drain / reconnect).
|
|
1684
|
+
*
|
|
1685
|
+
* When the precondition is checked it re-reads the query's current value
|
|
1686
|
+
* via `peekActiveQueryValue`. If the value differs from what was
|
|
1687
|
+
* captured at call time the precondition returns `false` and the offline
|
|
1688
|
+
* mutation is dropped as stale.
|
|
1689
|
+
*
|
|
1690
|
+
* This is a method wrapper around `createSnapshotPrecondition` that
|
|
1691
|
+
* binds the client instance for you — no need to pass `client` explicitly.
|
|
1692
|
+
* @example
|
|
1693
|
+
* ```ts
|
|
1694
|
+
* client.mutation(api.todos.update, { id, text }, {
|
|
1695
|
+
* precondition: client.snapshotPrecondition(api.todos.list, { userId }),
|
|
1696
|
+
* });
|
|
1697
|
+
* ```
|
|
1698
|
+
*/
|
|
1699
|
+
snapshotPrecondition(functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string): () => boolean;
|
|
1700
|
+
/**
|
|
1701
|
+
* Resolves once the durable read cache has been loaded into memory. When
|
|
1702
|
+
* `hydrateOnStart` is not configured or no query cache adapter is active,
|
|
1703
|
+
* returns an already-resolved promise so callers can always await it
|
|
1704
|
+
* unconditionally.
|
|
1705
|
+
*
|
|
1706
|
+
* Framework adapters (React, Vue, etc.) use this to gate the first
|
|
1707
|
+
* (enabled) render of a live query behind hydration, so the user sees
|
|
1708
|
+
* cached data instead of an undefined flash before the socket round-trip.
|
|
1709
|
+
*/
|
|
1710
|
+
whenReady(): Promise<void>;
|
|
1711
|
+
/**
|
|
1712
|
+
* Synchronously reports whether {@link whenReady} has already resolved (the
|
|
1713
|
+
* durable read cache is loaded, or none is configured). Framework adapters
|
|
1714
|
+
* read this to seed the hydration-gate state on the first render without
|
|
1715
|
+
* awaiting, then subscribe via {@link whenReady} for the pending case.
|
|
1716
|
+
*/
|
|
1717
|
+
get isReady(): boolean;
|
|
1718
|
+
/**
|
|
1719
|
+
* Synchronously peek at a value the durable read cache loaded for the given
|
|
1720
|
+
* function path + args + shard key. Returns `undefined` when:
|
|
1721
|
+
*
|
|
1722
|
+
* - No query cache adapter is configured.
|
|
1723
|
+
* - Hydration hasn't completed yet (race — await {@link whenReady} first).
|
|
1724
|
+
* - The cached value's identity fingerprint doesn't match the current auth.
|
|
1725
|
+
*
|
|
1726
|
+
* Unlike the internal {@link takeHydratedCache}, this is a READ-ONLY peek:
|
|
1727
|
+
* the cached entry stays in `hydratedQueryCache` so the subscription created
|
|
1728
|
+
* later by {@link subscribe} consumes it normally.
|
|
1729
|
+
*/
|
|
1730
|
+
peekHydratedQuery(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
|
|
1731
|
+
/**
|
|
1732
|
+
* Peek at the **current live value** of an active subscription, if one
|
|
1733
|
+
* exists. Returns the subscription's `lastValue` (which includes any
|
|
1734
|
+
* optimistic overlay) or `undefined` if no subscription is active for the
|
|
1735
|
+
* given `(functionPath, args, shardKey)`.
|
|
1736
|
+
*
|
|
1737
|
+
* Unlike {@link peekHydratedQuery} (which reads from the durable read cache
|
|
1738
|
+
* and is independent of active subscriptions), this method reflects the
|
|
1739
|
+
* current in-memory state of an already-opened subscription — useful for
|
|
1740
|
+
* offline mutation preconditions that need to snapshot the value at call time
|
|
1741
|
+
* and compare it at replay time.
|
|
1742
|
+
*/
|
|
1743
|
+
peekActiveQueryValue(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
|
|
1562
1744
|
query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
|
|
1563
1745
|
shardKey?: string;
|
|
1564
1746
|
}): Promise<ReturnOf<F>>;
|
|
@@ -2537,4 +2719,4 @@ declare class LunoraClient {
|
|
|
2537
2719
|
*/
|
|
2538
2720
|
private settleReplayBatchSlots;
|
|
2539
2721
|
}
|
|
2540
|
-
export {
|
|
2722
|
+
export { StorageObject as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, RowOp as E, FunctionReference as F, GlobalFacetResult as G, RpcEnvelope as H, RpcResponseBody as I, ScheduleRecord as J, SchedulerPoolStatus as K, LunoraClient as L, MutationCallOptions as M, SchedulerStatus as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerMessage as T, User as U, ServerPokeEndMessage as V, ServerPokePartMessage as W, ServerPokeStartMessage as X, ShardTrafficEntry as Y, ShardTrafficResult as Z, StorageListPage as _, Unsubscribe as a, StreamHandle as a0, StreamIterable as a1, SubscriptionCallback as a2, SubscriptionRegistry as a3, SubscriptionState as a4, SyncWatermark as a5, WorkflowInstanceAction as a6, WorkflowInstanceDetail as a7, WorkflowInstancePage as a8, WorkflowInstanceStatus as a9, WorkflowInstanceSummary as aa, WorkflowStepDetail as ab, createClientQuery as ac, createLocalStore as ad, createStream as ae, getErrorCode as af, getRetryAfterMs as ag, isConflictError as ah, isForbiddenError as ai, isRateLimitedError as aj, isUnauthorizedError as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, CachedQuery as f, ClientMessage as g, ClientQueryRef as h, ClientShapeSubscribeMessage as i, ClientShapeUnsubscribeMessage as j, ConnectionStatus as k, FunctionArgumentDescriptor as l, FunctionDescriptor as m, GlobalFacetValue as n, GlobalFilterClause as o, GlobalTableInfo as p, GlobalTablePage as q, LunoraClientError as r, LunoraClientOptions as s, LunoraErrorCode as t, MutationSettledEvent as u, OptimisticLocalStore as v, OptimisticUpdate as w, OutboxMutation as x, OutboxSink as y, PersistedMutation as z };
|