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

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 (25) 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 +41 -4
  4. package/dist/index.d.ts +41 -4
  5. package/dist/index.mjs +5 -4
  6. package/dist/packem_shared/{LunoraClient-Clb118SU.mjs → LunoraClient-BBCQjjbl.mjs} +126 -244
  7. package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs} +2 -2
  8. package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +2 -0
  9. package/dist/packem_shared/{createServerClient-Dxemst5C.mjs → createServerClient-CTTAmvMx.mjs} +1 -1
  10. package/dist/packem_shared/{createSnapshotPrecondition-CBwnVz6r.mjs → createSnapshotPrecondition-CxQ1T4ZP.mjs} +3 -3
  11. package/dist/packem_shared/httpStream-BJU-aflc.mjs +159 -0
  12. package/dist/packem_shared/{local-store-DtcIW4c0.mjs → local-store-DIq-UWfD.mjs} +1 -1
  13. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.mts → lunora-client.d-JvtVpf8A.d.mts} +120 -18
  14. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.ts → lunora-client.d-JvtVpf8A.d.ts} +120 -18
  15. package/dist/packem_shared/{preload.d-BkQr-3Vh.d.ts → preload.d-C4_d_l5v.d.ts} +1 -1
  16. package/dist/packem_shared/{preload.d-6ME5ubgq.d.mts → preload.d-DKbjGN5O.d.mts} +1 -1
  17. package/dist/packem_shared/wire-key-Djie6aaR.mjs +266 -0
  18. package/dist/query/index.d.mts +2 -2
  19. package/dist/query/index.d.ts +2 -2
  20. package/dist/ssr/index.d.mts +3 -3
  21. package/dist/ssr/index.d.ts +3 -3
  22. package/dist/ssr/index.mjs +1 -1
  23. package/package.json +2 -2
  24. package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +0 -2
  25. 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
4
  import { ClientQueryStore } from './createClientQuery-CQ51bWAE.mjs';
5
5
  import { TabCoordinator } from './TabCoordinator-BwRR8H06.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';
7
+ import { httpStream } from './httpStream-BJU-aflc.mjs';
8
+ import { f as foldOptimistic, a as applyOptimisticLayer, d as dropConfirmedLayers, n as notifySubscription, c as createLocalStore } from './local-store-DIq-UWfD.mjs';
8
9
  import { O as OfflineQueue, n as nextId, i as isStaleVersion, r as reportPersistenceError } from './offline-queue-CF4_Co5k.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) {
@@ -1162,7 +941,7 @@ class LunoraClient {
1162
941
  */
1163
942
  snapshotPrecondition(functionRef, args, shardKey) {
1164
943
  const snapshot = this.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
1165
- const snapshotKey = snapshot === void 0 ? void 0 : stableStringify(snapshot);
944
+ const snapshotKey = snapshot === void 0 ? void 0 : stableWireKey(snapshot);
1166
945
  return () => {
1167
946
  const current = this.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
1168
947
  if (snapshotKey === void 0 && current === void 0) {
@@ -1171,7 +950,7 @@ class LunoraClient {
1171
950
  if (snapshotKey === void 0 || current === void 0) {
1172
951
  return false;
1173
952
  }
1174
- return stableStringify(current) === snapshotKey;
953
+ return stableWireKey(current) === snapshotKey;
1175
954
  };
1176
955
  }
1177
956
  // --- Hydration helpers --------------------------------------------------
@@ -1213,7 +992,7 @@ class LunoraClient {
1213
992
  if (!this.readyResolved) {
1214
993
  return void 0;
1215
994
  }
1216
- const argsKey = stableStringify(args);
995
+ const argsKey = stableWireKey(args);
1217
996
  const key = queryCacheKey(functionPath, argsKey, shardKey);
1218
997
  const entry = this.hydratedQueryCache.get(key);
1219
998
  if (entry === void 0) {
@@ -1534,8 +1313,10 @@ class LunoraClient {
1534
1313
  * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
1535
1314
  * WebSocket. `onJobs` fires with the full list on connect and on every
1536
1315
  * 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
1316
+ * configured backoff. Requires `wsToken` to be set to an admin credential
1317
+ * (the browser can't send an `Authorization` header on a WS) the master
1318
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
1319
+ * sub-token so the master credential stays out of the URL. Returns an
1539
1320
  * unsubscribe function that closes the socket and stops reconnecting.
1540
1321
  */
1541
1322
  subscribeScheduledJobs(onJobs) {
@@ -1550,11 +1331,11 @@ class LunoraClient {
1550
1331
  let socket;
1551
1332
  let timer;
1552
1333
  let closed = false;
1553
- const connect = () => {
1334
+ const openWith = (token) => {
1554
1335
  if (closed || this.WebSocketImpl === void 0) {
1555
1336
  return;
1556
1337
  }
1557
- const url = this.wsToken === void 0 ? base : `${base}?token=${encodeURIComponent(this.wsToken)}`;
1338
+ const url = token === void 0 ? base : `${base}?token=${encodeURIComponent(token)}`;
1558
1339
  socket = new this.WebSocketImpl(url);
1559
1340
  socket.addEventListener("open", () => {
1560
1341
  reconnect.reset();
@@ -1577,6 +1358,29 @@ class LunoraClient {
1577
1358
  socket.addEventListener("error", () => {
1578
1359
  });
1579
1360
  };
1361
+ const connectWithProvider = async (provider) => {
1362
+ let token;
1363
+ try {
1364
+ token = await provider();
1365
+ } catch {
1366
+ if (!closed) {
1367
+ timer = setTimeout(connect, reconnect.next());
1368
+ }
1369
+ return;
1370
+ }
1371
+ openWith(token);
1372
+ };
1373
+ const connect = () => {
1374
+ if (closed || this.WebSocketImpl === void 0) {
1375
+ return;
1376
+ }
1377
+ const { wsToken } = this;
1378
+ if (typeof wsToken === "function") {
1379
+ connectWithProvider(wsToken).catch(() => void 0);
1380
+ return;
1381
+ }
1382
+ openWith(wsToken);
1383
+ };
1580
1384
  connect();
1581
1385
  return () => {
1582
1386
  closed = true;
@@ -2143,7 +1947,7 @@ class LunoraClient {
2143
1947
  if (!state) {
2144
1948
  this.nextSubId += 1;
2145
1949
  const id = `sub_${this.nextSubId.toString()}`;
2146
- const argsKey = stableStringify(argsRecord);
1950
+ const argsKey = stableWireKey(argsRecord);
2147
1951
  const cached = this.takeHydratedCache(function_.__lunoraRef, argsKey, options.shardKey);
2148
1952
  state = {
2149
1953
  acked: false,
@@ -2223,7 +2027,10 @@ class LunoraClient {
2223
2027
  name: shape.name,
2224
2028
  onCheckpoint: options.onCheckpoint,
2225
2029
  rows: /* @__PURE__ */ new Map(),
2226
- shardKey: options.shardKey
2030
+ shardKey: options.shardKey,
2031
+ // Encode ONCE, here, so an unsupported arg value throws at this call
2032
+ // site instead of inside a reconnect's open handler.
2033
+ wireArgs: shape.args === void 0 ? void 0 : encodeCallArgs(shape.args, `shape args for '${shape.name}'`)
2227
2034
  };
2228
2035
  this.shapeSubscriptions.set(id, state);
2229
2036
  this.ensureSocket(options.shardKey);
@@ -2307,6 +2114,39 @@ class LunoraClient {
2307
2114
  }
2308
2115
  return iterable;
2309
2116
  }
2117
+ /**
2118
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.&lt;verb>(path).stream()`).
2119
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
2120
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
2121
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
2122
+ * chunks, a final `event: complete`, an `event: error` on throw).
2123
+ *
2124
+ * The reference comes from the generated `httpStreams.*` registry, so the
2125
+ * yielded chunk type is the route handler's yielded type. Cancelling the
2126
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
2127
+ * the server handler observes via its `signal`. The client's bearer token
2128
+ * (when set) rides as an `authorization` header.
2129
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
2130
+ */
2131
+ httpStream(route, args, options = {}) {
2132
+ if (this.closed) {
2133
+ throw new LunoraError("CLIENT_CLOSED", "LunoraClient is closed");
2134
+ }
2135
+ if (!this.fetchImpl) {
2136
+ throw new LunoraError("INTERNAL", "LunoraClient: no `fetch` implementation available");
2137
+ }
2138
+ const headers = {
2139
+ ...this.authToken ? { authorization: `Bearer ${this.authToken}` } : {},
2140
+ ...options.headers
2141
+ };
2142
+ return httpStream(route, args, {
2143
+ baseUrl: this.url,
2144
+ fetch: this.fetchImpl,
2145
+ headers,
2146
+ maxBuffer: options.maxBuffer,
2147
+ signal: options.signal
2148
+ });
2149
+ }
2310
2150
  close() {
2311
2151
  this.closed = true;
2312
2152
  this.outboxLeaderRelease?.();
@@ -2666,13 +2506,13 @@ class LunoraClient {
2666
2506
  }
2667
2507
  return conn;
2668
2508
  }
2669
- wsUrlFor(shardKey) {
2509
+ wsUrlFor(shardKey, token) {
2670
2510
  const params = [];
2671
2511
  if (shardKey !== void 0) {
2672
2512
  params.push(`shard=${encodeURIComponent(shardKey)}`);
2673
2513
  }
2674
- if (this.wsToken !== void 0) {
2675
- params.push(`token=${encodeURIComponent(this.wsToken)}`);
2514
+ if (token !== void 0) {
2515
+ params.push(`token=${encodeURIComponent(token)}`);
2676
2516
  }
2677
2517
  if (params.length === 0) {
2678
2518
  return this.wsUrl;
@@ -2869,7 +2709,40 @@ class LunoraClient {
2869
2709
  }
2870
2710
  conn.wsState = "connecting";
2871
2711
  this.emitConnectionStatus();
2872
- const socket = new this.WebSocketImpl(this.wsUrlFor(shardKey));
2712
+ if (typeof this.wsToken === "function") {
2713
+ this.openSocketWithProvidedToken(conn, shardKey, this.wsToken).catch(() => void 0);
2714
+ return;
2715
+ }
2716
+ this.openSocket(conn, shardKey, this.wsToken);
2717
+ }
2718
+ /**
2719
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
2720
+ * minted token. The connection is already in the `connecting` state, so the
2721
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
2722
+ * competing connect that landed first all abandon this attempt. A provider
2723
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
2724
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
2725
+ * a silent tokenless socket the admin gate would reject.
2726
+ */
2727
+ async openSocketWithProvidedToken(conn, shardKey, provider) {
2728
+ let token;
2729
+ try {
2730
+ token = await provider();
2731
+ } catch {
2732
+ this.handleDisconnect(conn);
2733
+ return;
2734
+ }
2735
+ if (this.closed || this.WebSocketImpl === void 0 || conn.wsState !== "connecting" || conn.socket !== void 0) {
2736
+ return;
2737
+ }
2738
+ this.openSocket(conn, shardKey, token);
2739
+ }
2740
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
2741
+ openSocket(conn, shardKey, token) {
2742
+ if (this.WebSocketImpl === void 0) {
2743
+ return;
2744
+ }
2745
+ const socket = new this.WebSocketImpl(this.wsUrlFor(shardKey, token));
2873
2746
  conn.socket = socket;
2874
2747
  if (this.connectTimeoutMs > 0) {
2875
2748
  conn.connectTimer = setTimeout(() => {
@@ -3031,7 +2904,12 @@ class LunoraClient {
3031
2904
  // sub (a hydrated read or an earlier frame), so the server can
3032
2905
  // resume instead of re-snapshotting. Omitted on a cold sub.
3033
2906
  query: {
3034
- args: state.args,
2907
+ // Wire-encode so a `bigint`/`Date`/bytes arg survives the frame's
2908
+ // `JSON.stringify` (the shard `decodeWire`s at its subscribe entry
2909
+ // point). Identity for pure-JSON args. Cannot throw here: the
2910
+ // registry key (`stableWireKey`) already encoded these args at
2911
+ // subscribe() time, so reconnect resends stay safe.
2912
+ args: encodeWire(state.args),
3035
2913
  functionPath: state.fn.__lunoraRef,
3036
2914
  table,
3037
2915
  ...state.serverCursor === void 0 ? {} : { sinceSeq: state.serverCursor },
@@ -3047,7 +2925,11 @@ class LunoraClient {
3047
2925
  }
3048
2926
  sendOn(conn, {
3049
2927
  id: state.id,
3050
- shape: { name: state.name, ...state.args === void 0 ? {} : { args: state.args } },
2928
+ // `wireArgs` is the pre-encoded form of `args` (computed at
2929
+ // `subscribeShape` time) so a `bigint`/`Date`/bytes arg survives the
2930
+ // frame's `JSON.stringify`; the shard `decodeWire`s it at its
2931
+ // `shape_subscribe` entry point. Identity for pure-JSON args.
2932
+ shape: { name: state.name, ...state.wireArgs === void 0 ? {} : { args: state.wireArgs } },
3051
2933
  type: "shape_subscribe",
3052
2934
  // Resume from the last applied checkpoint when we hold one; a cold
3053
2935
  // subscribe omits it and the server seeds the full membership.
@@ -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();
@@ -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-BBCQjjbl.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
 
@@ -0,0 +1,159 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { createStream } from './DEFAULT_MAX_BUFFER-7hFnzNk9.mjs';
3
+
4
+ const SSE_FIELD_SPACE_RE = /^ /u;
5
+ const parameterToString = (value) => typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
6
+ const parseSseFrame = (raw) => {
7
+ let event = "";
8
+ const dataLines = [];
9
+ for (const line of raw.split("\n")) {
10
+ if (line.startsWith("event:")) {
11
+ event = line.slice("event:".length).replace(SSE_FIELD_SPACE_RE, "");
12
+ } else if (line.startsWith("data:")) {
13
+ dataLines.push(line.slice("data:".length).replace(SSE_FIELD_SPACE_RE, ""));
14
+ }
15
+ }
16
+ return { data: dataLines.join("\n"), event };
17
+ };
18
+ const buildHttpStreamUrl = (route, args, baseUrl) => {
19
+ const parameters = args.params ?? {};
20
+ const path = route.path.split("/").map((segment) => {
21
+ if (!segment.startsWith(":")) {
22
+ return segment;
23
+ }
24
+ const name = segment.slice(1);
25
+ const value = parameters[name];
26
+ if (value === void 0) {
27
+ throw new LunoraError("HTTP_STREAM_MISSING_PARAM", `httpStream: missing path param ":${name}" for route ${route.path}`);
28
+ }
29
+ return encodeURIComponent(parameterToString(value));
30
+ }).join("/");
31
+ const search = new URLSearchParams();
32
+ for (const [key, value] of Object.entries(args.searchParams ?? {})) {
33
+ if (value !== void 0) {
34
+ search.set(key, parameterToString(value));
35
+ }
36
+ }
37
+ const query = search.toString();
38
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
39
+ return `${trimmedBase}${path}${query === "" ? "" : `?${query}`}`;
40
+ };
41
+ const handleSseFrame = (frame, handle) => {
42
+ if (frame.event === "complete") {
43
+ handle.complete();
44
+ return true;
45
+ }
46
+ if (frame.event === "error") {
47
+ let payload = {};
48
+ try {
49
+ payload = JSON.parse(frame.data);
50
+ } catch {
51
+ }
52
+ const message = typeof payload.message === "string" ? payload.message : "stream error";
53
+ const code = typeof payload.code === "string" ? payload.code : "HTTP_STREAM_ERROR";
54
+ handle.fail(new LunoraError(code, message));
55
+ return true;
56
+ }
57
+ if ((frame.event === "" || frame.event === "message") && frame.data !== "") {
58
+ try {
59
+ handle.push(JSON.parse(frame.data));
60
+ } catch {
61
+ handle.fail(new LunoraError("HTTP_STREAM_BAD_CHUNK", "httpStream: malformed SSE chunk (invalid JSON)"));
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ };
67
+ const pumpSseBody = async (body, handle) => {
68
+ const reader = body.getReader();
69
+ const decoder = new TextDecoder();
70
+ let buffer = "";
71
+ const drainFrames = () => {
72
+ let separatorIndex = buffer.indexOf("\n\n");
73
+ while (separatorIndex !== -1) {
74
+ const frame = parseSseFrame(buffer.slice(0, separatorIndex));
75
+ buffer = buffer.slice(separatorIndex + 2);
76
+ if (handleSseFrame(frame, handle)) {
77
+ return true;
78
+ }
79
+ separatorIndex = buffer.indexOf("\n\n");
80
+ }
81
+ return false;
82
+ };
83
+ for (; ; ) {
84
+ const { done, value } = await reader.read();
85
+ if (done) {
86
+ break;
87
+ }
88
+ buffer += decoder.decode(value, { stream: true }).replaceAll("\r\n", "\n");
89
+ if (drainFrames()) {
90
+ await reader.cancel().catch(() => {
91
+ });
92
+ return;
93
+ }
94
+ }
95
+ buffer += decoder.decode().replaceAll("\r\n", "\n");
96
+ if (!drainFrames()) {
97
+ handle.fail(new LunoraError("HTTP_STREAM_INTERRUPTED", "httpStream: stream ended without a complete frame"));
98
+ }
99
+ };
100
+ const httpStream = (route, args, options = {}) => {
101
+ const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : void 0);
102
+ if (!fetchImpl) {
103
+ throw new LunoraError("INTERNAL", "httpStream: no `fetch` implementation available");
104
+ }
105
+ const url = buildHttpStreamUrl(route, args ?? {}, options.baseUrl ?? "");
106
+ const ac = new AbortController();
107
+ if (options.signal) {
108
+ if (options.signal.aborted) {
109
+ ac.abort();
110
+ } else {
111
+ options.signal.addEventListener(
112
+ "abort",
113
+ () => {
114
+ ac.abort();
115
+ },
116
+ { once: true }
117
+ );
118
+ }
119
+ }
120
+ const { handle, iterable } = createStream({
121
+ maxBuffer: options.maxBuffer,
122
+ onCancel: () => {
123
+ ac.abort();
124
+ }
125
+ });
126
+ (async () => {
127
+ const response = await fetchImpl(url, {
128
+ headers: { accept: "text/event-stream", ...options.headers },
129
+ method: route.method,
130
+ signal: ac.signal
131
+ });
132
+ if (!response.ok) {
133
+ handle.fail(
134
+ new LunoraError("HTTP_STREAM_STATUS", `httpStream: request failed (status ${response.status.toString()})`, {
135
+ status: response.status
136
+ })
137
+ );
138
+ return;
139
+ }
140
+ if (!response.body) {
141
+ handle.fail(new LunoraError("HTTP_STREAM_NO_BODY", "httpStream: response has no body"));
142
+ return;
143
+ }
144
+ await pumpSseBody(response.body, handle);
145
+ })().catch((error) => {
146
+ if (ac.signal.aborted) {
147
+ handle.complete();
148
+ return;
149
+ }
150
+ if (error instanceof Error && "code" in error) {
151
+ handle.fail(error);
152
+ return;
153
+ }
154
+ handle.fail(new LunoraError("HTTP_STREAM_TRANSPORT", error instanceof Error ? error.message : String(error), { cause: error }));
155
+ });
156
+ return iterable;
157
+ };
158
+
159
+ export { httpStream };