@lunora/client 1.0.0-alpha.22 → 1.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/auth/index.d.mts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/index.d.mts +66 -4
  4. package/dist/index.d.ts +66 -4
  5. package/dist/index.mjs +8 -7
  6. package/dist/packem_shared/{LunoraClient-Clb118SU.mjs → LunoraClient-D3h4P7hg.mjs} +143 -247
  7. package/dist/packem_shared/{OfflineQueue-B4HUF7rt.mjs → OfflineQueue-BgarnAub.mjs} +1 -1
  8. package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs} +2 -2
  9. package/dist/packem_shared/{TabCoordinator-BwRR8H06.mjs → TabCoordinator-D_5oNTTt.mjs} +48 -12
  10. package/dist/packem_shared/{createClientQuery-CQ51bWAE.mjs → createClientQuery-dJZg1ohm.mjs} +15 -6
  11. package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +2 -0
  12. package/dist/packem_shared/{createServerClient-Dxemst5C.mjs → createServerClient-DzeC2J3A.mjs} +1 -1
  13. package/dist/packem_shared/{createSnapshotPrecondition-CBwnVz6r.mjs → createSnapshotPrecondition-CxQ1T4ZP.mjs} +3 -3
  14. package/dist/packem_shared/httpStream-DIdL8NEw.mjs +168 -0
  15. package/dist/packem_shared/{local-store-DtcIW4c0.mjs → local-store-DIq-UWfD.mjs} +1 -1
  16. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.mts → lunora-client.d-B8bdwHLr.d.mts} +132 -18
  17. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.ts → lunora-client.d-B8bdwHLr.d.ts} +132 -18
  18. package/dist/packem_shared/{offline-queue-CF4_Co5k.mjs → offline-queue-N-1JvYb4.mjs} +14 -2
  19. package/dist/packem_shared/{preload.d-BkQr-3Vh.d.ts → preload.d-BPy9qajK.d.ts} +1 -1
  20. package/dist/packem_shared/{preload.d-6ME5ubgq.d.mts → preload.d-Ccrgw2z0.d.mts} +1 -1
  21. package/dist/packem_shared/wire-key-Djie6aaR.mjs +266 -0
  22. package/dist/query/index.d.mts +2 -2
  23. package/dist/query/index.d.ts +2 -2
  24. package/dist/ssr/index.d.mts +3 -3
  25. package/dist/ssr/index.d.ts +3 -3
  26. package/dist/ssr/index.mjs +1 -1
  27. package/package.json +2 -2
  28. package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +0 -2
  29. package/dist/packem_shared/stable-key-wv6eP48B.mjs +0 -40
@@ -1,16 +1,17 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
2
+ import { s as stableWireKey, d as decodeWire, e as encodeWire } from './wire-key-Djie6aaR.mjs';
3
3
  import createInMemoryBookmarkStorage from './createInMemoryBookmarkStorage-BoN7a7TH.mjs';
4
- import { ClientQueryStore } from './createClientQuery-CQ51bWAE.mjs';
5
- import { TabCoordinator } from './TabCoordinator-BwRR8H06.mjs';
4
+ import { ClientQueryStore } from './createClientQuery-dJZg1ohm.mjs';
5
+ import { TabCoordinator } from './TabCoordinator-D_5oNTTt.mjs';
6
6
  import { isMutationDelta, applyDelta } from './applyDelta-CRKZ1PBt.mjs';
7
- import { f as foldOptimistic, a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, c as createLocalStore } from './local-store-DtcIW4c0.mjs';
8
- import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-CF4_Co5k.mjs';
7
+ import { httpStream } from './httpStream-DIdL8NEw.mjs';
8
+ import { f as foldOptimistic, a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, c as createLocalStore } from './local-store-DIq-UWfD.mjs';
9
+ import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-N-1JvYb4.mjs';
9
10
  import { resolvePersistenceAdapter } from './createInMemoryPersistence-DZ2VHWgm.mjs';
10
11
  import { resolveQueryCacheAdapter, queryCacheKey } from './createInMemoryQueryCache-DiaGZkA2.mjs';
11
12
  import { createReconnect } from './createReconnect-Di_-oHH7.mjs';
12
13
  import { createStream } from './DEFAULT_MAX_BUFFER-7hFnzNk9.mjs';
13
- import { SubscriptionRegistry } from './SubscriptionRegistry-D4jfIzZu.mjs';
14
+ import { SubscriptionRegistry } from './SubscriptionRegistry-CxS_Inha.mjs';
14
15
 
15
16
  const MAX_BATCH_ENTRIES = 500;
16
17
 
@@ -24,230 +25,6 @@ const evictOldestEntry = (map, capacity) => {
24
25
  }
25
26
  };
26
27
 
27
- const toBase64 = (bytes) => {
28
- let binary = "";
29
- const chunk = 32768;
30
- for (let index = 0; index < bytes.length; index += chunk) {
31
- binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
32
- }
33
- return btoa(binary);
34
- };
35
- const fromBase64 = (base64) => {
36
- const binary = atob(base64);
37
- const bytes = new Uint8Array(binary.length);
38
- for (let index = 0; index < binary.length; index += 1) {
39
- bytes[index] = binary.codePointAt(index) ?? 0;
40
- }
41
- return bytes;
42
- };
43
-
44
- const TAG = "$lunora.wire$";
45
- const MAX_DEPTH = 64;
46
- const MAX_BIGINT_DIGITS = 1024;
47
- const UNSAFE_KEY = "__proto__";
48
- const TYPED_ARRAY_CTORS = {
49
- BigInt64Array,
50
- BigUint64Array,
51
- Float32Array,
52
- Float64Array,
53
- Int8Array,
54
- Int16Array,
55
- Int32Array,
56
- Uint8Array,
57
- Uint8ClampedArray,
58
- Uint16Array,
59
- Uint32Array
60
- };
61
- const ERROR_CTORS = {
62
- Error,
63
- EvalError,
64
- RangeError,
65
- ReferenceError,
66
- SyntaxError,
67
- TypeError,
68
- URIError
69
- };
70
- const encodeWire = (value, depth = 0) => {
71
- if (depth > MAX_DEPTH) {
72
- throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
73
- }
74
- if (value === void 0) {
75
- return [TAG, "undefined"];
76
- }
77
- if (value === null) {
78
- return null;
79
- }
80
- const kind = typeof value;
81
- if (kind === "bigint") {
82
- return [TAG, "bigint", value.toString()];
83
- }
84
- if (kind === "number") {
85
- const numeric = value;
86
- if (Number.isNaN(numeric)) {
87
- return [TAG, "nan"];
88
- }
89
- if (numeric === Infinity) {
90
- return [TAG, "inf"];
91
- }
92
- if (numeric === -Infinity) {
93
- return [TAG, "-inf"];
94
- }
95
- return numeric;
96
- }
97
- if (kind !== "object") {
98
- return value;
99
- }
100
- if (value instanceof Date) {
101
- return [TAG, "date", encodeWire(value.getTime(), depth + 1)];
102
- }
103
- if (value instanceof Error) {
104
- const error = value;
105
- const properties = {};
106
- for (const key of Object.keys(error)) {
107
- if (error[key] !== void 0) {
108
- properties[key] = encodeWire(error[key], depth + 1);
109
- }
110
- }
111
- const encodedError = [TAG, "error", error.name, error.message, properties];
112
- if (error.cause !== void 0) {
113
- encodedError.push(encodeWire(error.cause, depth + 1));
114
- }
115
- return encodedError;
116
- }
117
- if (value instanceof URL) {
118
- return [TAG, "url", value.href];
119
- }
120
- if (value instanceof Map) {
121
- return [TAG, "map", [...value.entries()].map(([k, v]) => [encodeWire(k, depth + 1), encodeWire(v, depth + 1)])];
122
- }
123
- if (value instanceof Set) {
124
- return [TAG, "set", [...value].map((item) => encodeWire(item, depth + 1))];
125
- }
126
- if (value instanceof ArrayBuffer) {
127
- return [TAG, "bytes", toBase64(new Uint8Array(value)), "ArrayBuffer"];
128
- }
129
- if (ArrayBuffer.isView(value)) {
130
- const view = value;
131
- const ctorName = view.constructor.name;
132
- const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
133
- return ctorName === "Uint8Array" ? [TAG, "bytes", toBase64(bytes)] : [TAG, "bytes", toBase64(bytes), ctorName];
134
- }
135
- if (Array.isArray(value)) {
136
- const encoded = value.map((item) => encodeWire(item, depth + 1));
137
- return encoded.length > 0 && encoded[0] === TAG ? [TAG, "arr", encoded] : encoded;
138
- }
139
- const proto = Object.getPrototypeOf(value);
140
- if (proto !== null && proto !== Object.prototype) {
141
- const name = value.constructor?.name ?? "value";
142
- throw new TypeError(
143
- `wire-codec: cannot encode a ${name} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`
144
- );
145
- }
146
- const source = value;
147
- const result = {};
148
- for (const key of Object.keys(source)) {
149
- const field = source[key];
150
- if (field !== void 0) {
151
- result[key] = encodeWire(field, depth + 1);
152
- }
153
- }
154
- return result;
155
- };
156
- const decodeWire = (value, depth = 0) => {
157
- if (depth > MAX_DEPTH) {
158
- throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
159
- }
160
- if (value === null || typeof value !== "object") {
161
- return value;
162
- }
163
- if (Array.isArray(value)) {
164
- if (value[0] === TAG) {
165
- const tag = value[1];
166
- switch (tag) {
167
- case "-inf": {
168
- return -Infinity;
169
- }
170
- case "arr": {
171
- return value[2].map((item) => decodeWire(item, depth + 1));
172
- }
173
- case "bigint": {
174
- const raw = value[2];
175
- if (typeof raw !== "string" || raw.length > MAX_BIGINT_DIGITS || !/^-?\d+$/.test(raw)) {
176
- throw new RangeError(`wire-codec: invalid or over-long bigint (max ${MAX_BIGINT_DIGITS} digits)`);
177
- }
178
- return BigInt(raw);
179
- }
180
- case "date": {
181
- return new Date(decodeWire(value[2], depth + 1));
182
- }
183
- case "map": {
184
- return new Map(value[2].map(([k, v]) => [decodeWire(k, depth + 1), decodeWire(v, depth + 1)]));
185
- }
186
- case "set": {
187
- return new Set(value[2].map((item) => decodeWire(item, depth + 1)));
188
- }
189
- case "url": {
190
- return new URL(value[2]);
191
- }
192
- case "error": {
193
- const name = value[2];
194
- const message = value[3];
195
- const Ctor = (Object.hasOwn(ERROR_CTORS, name) ? ERROR_CTORS[name] : void 0) ?? Error;
196
- const error = new Ctor(message);
197
- if (error.name !== name) {
198
- Object.defineProperty(error, "name", { configurable: true, value: name, writable: true });
199
- }
200
- const props = decodeWire(value[4], depth + 1);
201
- for (const key of Object.keys(props)) {
202
- if (key === UNSAFE_KEY) {
203
- Object.defineProperty(error, key, { configurable: true, enumerable: true, value: props[key], writable: true });
204
- } else {
205
- error[key] = props[key];
206
- }
207
- }
208
- if (value.length > 5) {
209
- Object.defineProperty(error, "cause", { configurable: true, value: decodeWire(value[5], depth + 1), writable: true });
210
- }
211
- return error;
212
- }
213
- case "bytes": {
214
- const bytes = fromBase64(value[2]);
215
- const ctorName = value[3] ?? "Uint8Array";
216
- if (ctorName === "ArrayBuffer") {
217
- return bytes.buffer.byteLength === bytes.byteLength ? bytes.buffer : bytes.slice().buffer;
218
- }
219
- const Ctor = Object.hasOwn(TYPED_ARRAY_CTORS, ctorName) ? TYPED_ARRAY_CTORS[ctorName] : void 0;
220
- return Ctor ? new Ctor(bytes.slice().buffer) : bytes;
221
- }
222
- case "inf": {
223
- return Infinity;
224
- }
225
- case "nan": {
226
- return Number.NaN;
227
- }
228
- case "undefined": {
229
- return void 0;
230
- }
231
- default: {
232
- return value.map((item) => decodeWire(item, depth + 1));
233
- }
234
- }
235
- }
236
- return value.map((item) => decodeWire(item, depth + 1));
237
- }
238
- const source = value;
239
- const result = {};
240
- for (const key of Object.keys(source)) {
241
- const decoded = decodeWire(source[key], depth + 1);
242
- if (key === UNSAFE_KEY) {
243
- Object.defineProperty(result, key, { configurable: true, enumerable: true, value: decoded, writable: true });
244
- } else {
245
- result[key] = decoded;
246
- }
247
- }
248
- return result;
249
- };
250
-
251
28
  class Listeners {
252
29
  listeners = /* @__PURE__ */ new Set();
253
30
  add(listener) {
@@ -892,8 +669,10 @@ class LunoraClient {
892
669
  * Replace the token appended to WS upgrade URLs as `?token=…` and close
893
670
  * every open shard socket so the reconnect picks up the new value. Call
894
671
  * this whenever the user's WS credential changes (rotating the admin token
895
- * in the studio, switching workspaces, etc.). Bearer tokens for HTTP
896
- * RPC are independentsee {@link setAuthToken}.
672
+ * in the studio, switching workspaces, etc.). Accepts a static string or a
673
+ * {@link WsTokenProvider} resolved fresh at every (re)connect the channel
674
+ * for short-lived credentials like the minted ephemeral admin sub-token.
675
+ * Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
897
676
  */
898
677
  setWsToken(token) {
899
678
  if (this.wsToken === token) {
@@ -1109,6 +888,20 @@ class LunoraClient {
1109
888
  onMutationSettled(listener) {
1110
889
  return this.mutationSettledListeners.add(listener);
1111
890
  }
891
+ /**
892
+ * The `WebSocket` implementation this client was constructed with (an
893
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
894
+ * one) — `undefined` if neither is available. This is the seam a feature
895
+ * that opens its OWN socket outside the client's multiplexed connection
896
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
897
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
898
+ * constructor to inject the auth-headers factory's credential onto the
899
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
900
+ * `new globalThis.WebSocket(url)` would silently bypass.
901
+ */
902
+ getWebSocketImpl() {
903
+ return this.WebSocketImpl;
904
+ }
1112
905
  // --- Client Query (local state) ----------------------------------------
1113
906
  /**
1114
907
  * Read the current value for a {@link ClientQueryRef}. Returns
@@ -1162,7 +955,7 @@ class LunoraClient {
1162
955
  */
1163
956
  snapshotPrecondition(functionRef, args, shardKey) {
1164
957
  const snapshot = this.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
1165
- const snapshotKey = snapshot === void 0 ? void 0 : stableStringify(snapshot);
958
+ const snapshotKey = snapshot === void 0 ? void 0 : stableWireKey(snapshot);
1166
959
  return () => {
1167
960
  const current = this.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
1168
961
  if (snapshotKey === void 0 && current === void 0) {
@@ -1171,7 +964,7 @@ class LunoraClient {
1171
964
  if (snapshotKey === void 0 || current === void 0) {
1172
965
  return false;
1173
966
  }
1174
- return stableStringify(current) === snapshotKey;
967
+ return stableWireKey(current) === snapshotKey;
1175
968
  };
1176
969
  }
1177
970
  // --- Hydration helpers --------------------------------------------------
@@ -1213,7 +1006,7 @@ class LunoraClient {
1213
1006
  if (!this.readyResolved) {
1214
1007
  return void 0;
1215
1008
  }
1216
- const argsKey = stableStringify(args);
1009
+ const argsKey = stableWireKey(args);
1217
1010
  const key = queryCacheKey(functionPath, argsKey, shardKey);
1218
1011
  const entry = this.hydratedQueryCache.get(key);
1219
1012
  if (entry === void 0) {
@@ -1534,8 +1327,10 @@ class LunoraClient {
1534
1327
  * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
1535
1328
  * WebSocket. `onJobs` fires with the full list on connect and on every
1536
1329
  * change (schedule / cancel / alarm-fire). Reconnects with the client's
1537
- * configured backoff. Requires `wsToken` to be set to the admin token (the
1538
- * browser can't send an `Authorization` header on a WS). Returns an
1330
+ * configured backoff. Requires `wsToken` to be set to an admin credential
1331
+ * (the browser can't send an `Authorization` header on a WS) the master
1332
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
1333
+ * sub-token so the master credential stays out of the URL. Returns an
1539
1334
  * unsubscribe function that closes the socket and stops reconnecting.
1540
1335
  */
1541
1336
  subscribeScheduledJobs(onJobs) {
@@ -1550,11 +1345,11 @@ class LunoraClient {
1550
1345
  let socket;
1551
1346
  let timer;
1552
1347
  let closed = false;
1553
- const connect = () => {
1348
+ const openWith = (token) => {
1554
1349
  if (closed || this.WebSocketImpl === void 0) {
1555
1350
  return;
1556
1351
  }
1557
- const url = this.wsToken === void 0 ? base : `${base}?token=${encodeURIComponent(this.wsToken)}`;
1352
+ const url = token === void 0 ? base : `${base}?token=${encodeURIComponent(token)}`;
1558
1353
  socket = new this.WebSocketImpl(url);
1559
1354
  socket.addEventListener("open", () => {
1560
1355
  reconnect.reset();
@@ -1577,6 +1372,29 @@ class LunoraClient {
1577
1372
  socket.addEventListener("error", () => {
1578
1373
  });
1579
1374
  };
1375
+ const connectWithProvider = async (provider) => {
1376
+ let token;
1377
+ try {
1378
+ token = await provider();
1379
+ } catch {
1380
+ if (!closed) {
1381
+ timer = setTimeout(connect, reconnect.next());
1382
+ }
1383
+ return;
1384
+ }
1385
+ openWith(token);
1386
+ };
1387
+ const connect = () => {
1388
+ if (closed || this.WebSocketImpl === void 0) {
1389
+ return;
1390
+ }
1391
+ const { wsToken } = this;
1392
+ if (typeof wsToken === "function") {
1393
+ connectWithProvider(wsToken).catch(() => void 0);
1394
+ return;
1395
+ }
1396
+ openWith(wsToken);
1397
+ };
1580
1398
  connect();
1581
1399
  return () => {
1582
1400
  closed = true;
@@ -2143,7 +1961,7 @@ class LunoraClient {
2143
1961
  if (!state) {
2144
1962
  this.nextSubId += 1;
2145
1963
  const id = `sub_${this.nextSubId.toString()}`;
2146
- const argsKey = stableStringify(argsRecord);
1964
+ const argsKey = stableWireKey(argsRecord);
2147
1965
  const cached = this.takeHydratedCache(function_.__lunoraRef, argsKey, options.shardKey);
2148
1966
  state = {
2149
1967
  acked: false,
@@ -2223,7 +2041,10 @@ class LunoraClient {
2223
2041
  name: shape.name,
2224
2042
  onCheckpoint: options.onCheckpoint,
2225
2043
  rows: /* @__PURE__ */ new Map(),
2226
- shardKey: options.shardKey
2044
+ shardKey: options.shardKey,
2045
+ // Encode ONCE, here, so an unsupported arg value throws at this call
2046
+ // site instead of inside a reconnect's open handler.
2047
+ wireArgs: shape.args === void 0 ? void 0 : encodeCallArgs(shape.args, `shape args for '${shape.name}'`)
2227
2048
  };
2228
2049
  this.shapeSubscriptions.set(id, state);
2229
2050
  this.ensureSocket(options.shardKey);
@@ -2307,6 +2128,39 @@ class LunoraClient {
2307
2128
  }
2308
2129
  return iterable;
2309
2130
  }
2131
+ /**
2132
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.&lt;verb>(path).stream()`).
2133
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
2134
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
2135
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
2136
+ * chunks, a final `event: complete`, an `event: error` on throw).
2137
+ *
2138
+ * The reference comes from the generated `httpStreams.*` registry, so the
2139
+ * yielded chunk type is the route handler's yielded type. Cancelling the
2140
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
2141
+ * the server handler observes via its `signal`. The client's bearer token
2142
+ * (when set) rides as an `authorization` header.
2143
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
2144
+ */
2145
+ httpStream(route, args, options = {}) {
2146
+ if (this.closed) {
2147
+ throw new LunoraError("CLIENT_CLOSED", "LunoraClient is closed");
2148
+ }
2149
+ if (!this.fetchImpl) {
2150
+ throw new LunoraError("INTERNAL", "LunoraClient: no `fetch` implementation available");
2151
+ }
2152
+ const headers = {
2153
+ ...this.authToken ? { authorization: `Bearer ${this.authToken}` } : {},
2154
+ ...options.headers
2155
+ };
2156
+ return httpStream(route, args, {
2157
+ baseUrl: this.url,
2158
+ fetch: this.fetchImpl,
2159
+ headers,
2160
+ maxBuffer: options.maxBuffer,
2161
+ signal: options.signal
2162
+ });
2163
+ }
2310
2164
  close() {
2311
2165
  this.closed = true;
2312
2166
  this.outboxLeaderRelease?.();
@@ -2666,13 +2520,13 @@ class LunoraClient {
2666
2520
  }
2667
2521
  return conn;
2668
2522
  }
2669
- wsUrlFor(shardKey) {
2523
+ wsUrlFor(shardKey, token) {
2670
2524
  const params = [];
2671
2525
  if (shardKey !== void 0) {
2672
2526
  params.push(`shard=${encodeURIComponent(shardKey)}`);
2673
2527
  }
2674
- if (this.wsToken !== void 0) {
2675
- params.push(`token=${encodeURIComponent(this.wsToken)}`);
2528
+ if (token !== void 0) {
2529
+ params.push(`token=${encodeURIComponent(token)}`);
2676
2530
  }
2677
2531
  if (params.length === 0) {
2678
2532
  return this.wsUrl;
@@ -2869,7 +2723,40 @@ class LunoraClient {
2869
2723
  }
2870
2724
  conn.wsState = "connecting";
2871
2725
  this.emitConnectionStatus();
2872
- const socket = new this.WebSocketImpl(this.wsUrlFor(shardKey));
2726
+ if (typeof this.wsToken === "function") {
2727
+ this.openSocketWithProvidedToken(conn, shardKey, this.wsToken).catch(() => void 0);
2728
+ return;
2729
+ }
2730
+ this.openSocket(conn, shardKey, this.wsToken);
2731
+ }
2732
+ /**
2733
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
2734
+ * minted token. The connection is already in the `connecting` state, so the
2735
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
2736
+ * competing connect that landed first all abandon this attempt. A provider
2737
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
2738
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
2739
+ * a silent tokenless socket the admin gate would reject.
2740
+ */
2741
+ async openSocketWithProvidedToken(conn, shardKey, provider) {
2742
+ let token;
2743
+ try {
2744
+ token = await provider();
2745
+ } catch {
2746
+ this.handleDisconnect(conn);
2747
+ return;
2748
+ }
2749
+ if (this.closed || this.WebSocketImpl === void 0 || conn.wsState !== "connecting" || conn.socket !== void 0) {
2750
+ return;
2751
+ }
2752
+ this.openSocket(conn, shardKey, token);
2753
+ }
2754
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
2755
+ openSocket(conn, shardKey, token) {
2756
+ if (this.WebSocketImpl === void 0) {
2757
+ return;
2758
+ }
2759
+ const socket = new this.WebSocketImpl(this.wsUrlFor(shardKey, token));
2873
2760
  conn.socket = socket;
2874
2761
  if (this.connectTimeoutMs > 0) {
2875
2762
  conn.connectTimer = setTimeout(() => {
@@ -3031,7 +2918,12 @@ class LunoraClient {
3031
2918
  // sub (a hydrated read or an earlier frame), so the server can
3032
2919
  // resume instead of re-snapshotting. Omitted on a cold sub.
3033
2920
  query: {
3034
- args: state.args,
2921
+ // Wire-encode so a `bigint`/`Date`/bytes arg survives the frame's
2922
+ // `JSON.stringify` (the shard `decodeWire`s at its subscribe entry
2923
+ // point). Identity for pure-JSON args. Cannot throw here: the
2924
+ // registry key (`stableWireKey`) already encoded these args at
2925
+ // subscribe() time, so reconnect resends stay safe.
2926
+ args: encodeWire(state.args),
3035
2927
  functionPath: state.fn.__lunoraRef,
3036
2928
  table,
3037
2929
  ...state.serverCursor === void 0 ? {} : { sinceSeq: state.serverCursor },
@@ -3047,7 +2939,11 @@ class LunoraClient {
3047
2939
  }
3048
2940
  sendOn(conn, {
3049
2941
  id: state.id,
3050
- shape: { name: state.name, ...state.args === void 0 ? {} : { args: state.args } },
2942
+ // `wireArgs` is the pre-encoded form of `args` (computed at
2943
+ // `subscribeShape` time) so a `bigint`/`Date`/bytes arg survives the
2944
+ // frame's `JSON.stringify`; the shard `decodeWire`s it at its
2945
+ // `shape_subscribe` entry point. Identity for pure-JSON args.
2946
+ shape: { name: state.name, ...state.wireArgs === void 0 ? {} : { args: state.wireArgs } },
3051
2947
  type: "shape_subscribe",
3052
2948
  // Resume from the last applied checkpoint when we hold one; a cold
3053
2949
  // subscribe omits it and the server seeds the full membership.
@@ -1 +1 @@
1
- export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-CF4_Co5k.mjs';
1
+ export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-N-1JvYb4.mjs';
@@ -1,8 +1,8 @@
1
- import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
1
+ import { s as stableWireKey } from './wire-key-Djie6aaR.mjs';
2
2
 
3
3
  class SubscriptionRegistry {
4
4
  static key(functionPath, args, shardKey) {
5
- return `${functionPath}::${stableStringify(args)}::${shardKey ?? ""}`;
5
+ return `${functionPath}::${stableWireKey(args)}::${shardKey ?? ""}`;
6
6
  }
7
7
  byKey = /* @__PURE__ */ new Map();
8
8
  byId = /* @__PURE__ */ new Map();
@@ -142,21 +142,11 @@ class TabCoordinator {
142
142
  handleMessage(message) {
143
143
  switch (message.type) {
144
144
  case "claim-leadership": {
145
- if (message.tabId === this.tabId) {
146
- break;
147
- }
148
- if (this.leader) {
149
- this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
150
- break;
151
- }
152
- if (this.knownLeader === void 0 && message.tabId < this.tabId) {
153
- this.knownLeader = message.tabId;
154
- }
145
+ this.handleClaimLeadership(message);
155
146
  break;
156
147
  }
157
148
  case "heartbeat": {
158
- this.lastHeartbeat = message.ts;
159
- this.knownLeader = message.tabId;
149
+ this.handleHeartbeat(message);
160
150
  break;
161
151
  }
162
152
  case "subscription-data": {
@@ -181,6 +171,52 @@ class TabCoordinator {
181
171
  }
182
172
  }
183
173
  }
174
+ handleClaimLeadership(message) {
175
+ if (message.tabId === this.tabId) {
176
+ return;
177
+ }
178
+ if (this.leader) {
179
+ this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
180
+ return;
181
+ }
182
+ if (this.knownLeader === void 0 && message.tabId < this.tabId) {
183
+ this.knownLeader = message.tabId;
184
+ this.lastHeartbeat = message.ts;
185
+ }
186
+ }
187
+ handleHeartbeat(message) {
188
+ if (message.tabId === this.tabId) {
189
+ return;
190
+ }
191
+ if (this.leader) {
192
+ this.resolveLeaderVsLeaderTieBreak(message);
193
+ return;
194
+ }
195
+ this.lastHeartbeat = message.ts;
196
+ this.knownLeader = message.tabId;
197
+ }
198
+ /**
199
+ * Resolve two leaders existing at once — e.g. this tab was backgrounded
200
+ * and its heartbeat/health timers were throttled while a foreground
201
+ * follower's were not, so the follower's `checkLeaderHealth` timed out
202
+ * the (still-alive) leader and self-promoted. `BroadcastChannel` message
203
+ * delivery isn't subject to the same timer-throttling clamp, so even a
204
+ * backgrounded leader eventually observes the pretender's heartbeat here
205
+ * — resolve the split-brain deterministically with the same
206
+ * lexicographically-smaller-tabId rule used at claim-adoption. If the
207
+ * other tab wins, step down; if we win, reassert immediately so the
208
+ * pretender demotes itself the moment it processes our heartbeat.
209
+ */
210
+ resolveLeaderVsLeaderTieBreak(message) {
211
+ if (message.tabId < this.tabId) {
212
+ this.leader = false;
213
+ this.knownLeader = message.tabId;
214
+ this.lastHeartbeat = message.ts;
215
+ this.onStopBeingLeader?.();
216
+ } else {
217
+ this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
218
+ }
219
+ }
184
220
  becomeLeader() {
185
221
  if (this.leader) {
186
222
  return;
@@ -16,11 +16,16 @@ class ClientQueryStore {
16
16
  }
17
17
  /**
18
18
  * Set a new value for `ref` and notify every subscriber. Pass `undefined`
19
- * to reset the slot to `ref.defaultValue`.
19
+ * to reset the slot to `ref.defaultValue` — delegates to {@link reset} so
20
+ * `get` reports the default rather than a stored `undefined`.
20
21
  */
21
22
  set(ref, value) {
23
+ if (value === void 0) {
24
+ this.reset(ref);
25
+ return;
26
+ }
22
27
  this.values.set(ref.key, value);
23
- this.notify(ref.key);
28
+ this.notify(ref.key, value);
24
29
  }
25
30
  /**
26
31
  * Delete the stored value for `ref`, resetting to `ref.defaultValue` and
@@ -28,7 +33,7 @@ class ClientQueryStore {
28
33
  */
29
34
  reset(ref) {
30
35
  this.values.delete(ref.key);
31
- this.notify(ref.key);
36
+ this.notify(ref.key, ref.defaultValue);
32
37
  }
33
38
  /**
34
39
  * Subscribe to changes for `ref`. The callback is NOT invoked on
@@ -49,13 +54,17 @@ class ClientQueryStore {
49
54
  }
50
55
  };
51
56
  }
52
- /** Notify every subscriber of a value change for the given key. */
53
- notify(key) {
57
+ /**
58
+ * Notify every subscriber of a value change for the given key with the
59
+ * resolved value — the caller passes the just-stored value on `set` or
60
+ * `ref.defaultValue` on `reset`, so a subscriber always sees the same value
61
+ * {@link get} would return (never a stale re-read after the store mutation).
62
+ */
63
+ notify(key, value) {
54
64
  const subs = this.subscribers.get(key);
55
65
  if (!subs) {
56
66
  return;
57
67
  }
58
- const value = this.values.get(key);
59
68
  for (const callback of subs) {
60
69
  try {
61
70
  callback(value);
@@ -0,0 +1,2 @@
1
+ export { c as createLocalStore } from './local-store-DIq-UWfD.mjs';
2
+ import './SubscriptionRegistry-CxS_Inha.mjs';
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-Clb118SU.mjs';
1
+ import { LunoraClient } from './LunoraClient-D3h4P7hg.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -1,8 +1,8 @@
1
- import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
1
+ import { s as stableWireKey } from './wire-key-Djie6aaR.mjs';
2
2
 
3
3
  const createSnapshotPrecondition = (client, functionRef, args, shardKey) => {
4
4
  const snapshot = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
5
- const snapshotKey = snapshot === void 0 ? void 0 : stableStringify(snapshot);
5
+ const snapshotKey = snapshot === void 0 ? void 0 : stableWireKey(snapshot);
6
6
  return () => {
7
7
  const current = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
8
8
  if (snapshotKey === void 0 && current === void 0) {
@@ -11,7 +11,7 @@ const createSnapshotPrecondition = (client, functionRef, args, shardKey) => {
11
11
  if (snapshotKey === void 0 || current === void 0) {
12
12
  return false;
13
13
  }
14
- return stableStringify(current) === snapshotKey;
14
+ return stableWireKey(current) === snapshotKey;
15
15
  };
16
16
  };
17
17