@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.
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +66 -4
- package/dist/index.d.ts +66 -4
- package/dist/index.mjs +8 -7
- package/dist/packem_shared/{LunoraClient-Clb118SU.mjs → LunoraClient-D3h4P7hg.mjs} +143 -247
- package/dist/packem_shared/{OfflineQueue-B4HUF7rt.mjs → OfflineQueue-BgarnAub.mjs} +1 -1
- package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs} +2 -2
- package/dist/packem_shared/{TabCoordinator-BwRR8H06.mjs → TabCoordinator-D_5oNTTt.mjs} +48 -12
- package/dist/packem_shared/{createClientQuery-CQ51bWAE.mjs → createClientQuery-dJZg1ohm.mjs} +15 -6
- package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +2 -0
- package/dist/packem_shared/{createServerClient-Dxemst5C.mjs → createServerClient-DzeC2J3A.mjs} +1 -1
- package/dist/packem_shared/{createSnapshotPrecondition-CBwnVz6r.mjs → createSnapshotPrecondition-CxQ1T4ZP.mjs} +3 -3
- package/dist/packem_shared/httpStream-DIdL8NEw.mjs +168 -0
- package/dist/packem_shared/{local-store-DtcIW4c0.mjs → local-store-DIq-UWfD.mjs} +1 -1
- package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.mts → lunora-client.d-B8bdwHLr.d.mts} +132 -18
- package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.ts → lunora-client.d-B8bdwHLr.d.ts} +132 -18
- package/dist/packem_shared/{offline-queue-CF4_Co5k.mjs → offline-queue-N-1JvYb4.mjs} +14 -2
- package/dist/packem_shared/{preload.d-BkQr-3Vh.d.ts → preload.d-BPy9qajK.d.ts} +1 -1
- package/dist/packem_shared/{preload.d-6ME5ubgq.d.mts → preload.d-Ccrgw2z0.d.mts} +1 -1
- package/dist/packem_shared/wire-key-Djie6aaR.mjs +266 -0
- package/dist/query/index.d.mts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/ssr/index.d.mts +3 -3
- package/dist/ssr/index.d.ts +3 -3
- package/dist/ssr/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +0 -2
- 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
|
|
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-
|
|
5
|
-
import { TabCoordinator } from './TabCoordinator-
|
|
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 {
|
|
8
|
-
import {
|
|
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-
|
|
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.).
|
|
896
|
-
*
|
|
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 :
|
|
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
|
|
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 =
|
|
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
|
|
1538
|
-
* browser can't send an `Authorization` header on a WS)
|
|
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
|
|
1348
|
+
const openWith = (token) => {
|
|
1554
1349
|
if (closed || this.WebSocketImpl === void 0) {
|
|
1555
1350
|
return;
|
|
1556
1351
|
}
|
|
1557
|
-
const url =
|
|
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 =
|
|
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.<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 (
|
|
2675
|
-
params.push(`token=${encodeURIComponent(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1
|
+
export { O as OfflineQueue, n as nextId, r as reportPersistenceError } from './offline-queue-N-1JvYb4.mjs';
|
package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { s as
|
|
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}::${
|
|
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
|
-
|
|
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.
|
|
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;
|
package/dist/packem_shared/{createClientQuery-CQ51bWAE.mjs → createClientQuery-dJZg1ohm.mjs}
RENAMED
|
@@ -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
|
-
/**
|
|
53
|
-
|
|
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);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { s as
|
|
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 :
|
|
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
|
|
14
|
+
return stableWireKey(current) === snapshotKey;
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
17
|
|