@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.
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.mjs +5 -4
- package/dist/packem_shared/{LunoraClient-Clb118SU.mjs → LunoraClient-BBCQjjbl.mjs} +126 -244
- package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs} +2 -2
- package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +2 -0
- package/dist/packem_shared/{createServerClient-Dxemst5C.mjs → createServerClient-CTTAmvMx.mjs} +1 -1
- package/dist/packem_shared/{createSnapshotPrecondition-CBwnVz6r.mjs → createSnapshotPrecondition-CxQ1T4ZP.mjs} +3 -3
- package/dist/packem_shared/httpStream-BJU-aflc.mjs +159 -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-JvtVpf8A.d.mts} +120 -18
- package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.ts → lunora-client.d-JvtVpf8A.d.ts} +120 -18
- package/dist/packem_shared/{preload.d-BkQr-3Vh.d.ts → preload.d-C4_d_l5v.d.ts} +1 -1
- package/dist/packem_shared/{preload.d-6ME5ubgq.d.mts → preload.d-DKbjGN5O.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
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 {
|
|
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-
|
|
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) {
|
|
@@ -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 :
|
|
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
|
|
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 =
|
|
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
|
|
1538
|
-
* browser can't send an `Authorization` header on a WS)
|
|
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
|
|
1334
|
+
const openWith = (token) => {
|
|
1554
1335
|
if (closed || this.WebSocketImpl === void 0) {
|
|
1555
1336
|
return;
|
|
1556
1337
|
}
|
|
1557
|
-
const url =
|
|
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 =
|
|
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.<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 (
|
|
2675
|
-
params.push(`token=${encodeURIComponent(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
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();
|
|
@@ -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
|
|
|
@@ -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 };
|