dynamodb-reactive 0.2.3 → 0.3.0

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.
@@ -0,0 +1,1166 @@
1
+ 'use strict';
2
+
3
+ var jsonpatch = require('fast-json-patch');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var jsonpatch__default = /*#__PURE__*/_interopDefault(jsonpatch);
8
+
9
+ // ../client/src/canonical.ts
10
+ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
11
+ if (value === null) return "null";
12
+ switch (typeof value) {
13
+ case "string":
14
+ case "boolean":
15
+ return JSON.stringify(value);
16
+ case "number":
17
+ return Number.isFinite(value) ? JSON.stringify(value) : "null";
18
+ case "undefined":
19
+ return arrayItem ? "null" : void 0;
20
+ case "object": {
21
+ if (applyToJSON && "toJSON" in value && typeof value.toJSON === "function") {
22
+ return canonicalize(value.toJSON(key), seen, key, arrayItem, false);
23
+ }
24
+ if (seen.has(value)) {
25
+ throw new TypeError("Query input must not contain circular references");
26
+ }
27
+ seen.add(value);
28
+ try {
29
+ if (Array.isArray(value)) {
30
+ const items = [];
31
+ for (let index = 0; index < value.length; index++) {
32
+ items.push(
33
+ canonicalize(value[index], seen, String(index), true) ?? "null"
34
+ );
35
+ }
36
+ return `[${items.join(",")}]`;
37
+ }
38
+ const object = value;
39
+ const entries = [];
40
+ for (const key2 of Object.keys(object).sort()) {
41
+ const item = canonicalize(object[key2], seen, key2);
42
+ if (item !== void 0) {
43
+ entries.push(`${JSON.stringify(key2)}:${item}`);
44
+ }
45
+ }
46
+ return `{${entries.join(",")}}`;
47
+ } finally {
48
+ seen.delete(value);
49
+ }
50
+ }
51
+ default:
52
+ throw new TypeError(`Unsupported query input value: ${typeof value}`);
53
+ }
54
+ }
55
+ function getQueryKey(path, input) {
56
+ return `${JSON.stringify(path)}:${canonicalize(input, /* @__PURE__ */ new Set(), "") ?? "undefined"}`;
57
+ }
58
+ var { applyPatch } = jsonpatch__default.default;
59
+ function applyPatches(document, patches) {
60
+ if (patches.length === 0) {
61
+ return document;
62
+ }
63
+ const operations = patches.map((patch) => {
64
+ const op = {
65
+ op: patch.op,
66
+ path: patch.path
67
+ };
68
+ if ("value" in patch && patch.value !== void 0) {
69
+ op.value = patch.value;
70
+ }
71
+ if ("from" in patch && patch.from !== void 0) {
72
+ op.from = patch.from;
73
+ }
74
+ return op;
75
+ });
76
+ try {
77
+ const result = applyPatch(
78
+ structuredClone(document),
79
+ operations,
80
+ true,
81
+ // Validate operations
82
+ false
83
+ // Don't mutate the original
84
+ );
85
+ return result.newDocument;
86
+ } catch (error2) {
87
+ throw new Error("Failed to apply patches", { cause: error2 });
88
+ }
89
+ }
90
+
91
+ // ../client/src/log.ts
92
+ function error(...values) {
93
+ console.error(...values);
94
+ }
95
+
96
+ // ../client/src/websocket.ts
97
+ var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
98
+ function withDefaults(config) {
99
+ return {
100
+ autoReconnect: true,
101
+ connectionTimeout: 1e4,
102
+ handshakeTimeout: 1e4,
103
+ maxReconnectAttempts: 10,
104
+ reconnectDelay: 1e3,
105
+ ...config
106
+ };
107
+ }
108
+ var WebSocketManager = class {
109
+ config;
110
+ ws = null;
111
+ state = "disconnected";
112
+ reconnectAttempts = 0;
113
+ reconnectTimer = null;
114
+ handshakeTimer = null;
115
+ heartbeatTimer = null;
116
+ messageHandlers = /* @__PURE__ */ new Set();
117
+ stateHandlers = /* @__PURE__ */ new Set();
118
+ handshakeHandlers = /* @__PURE__ */ new Set();
119
+ _connectionId = null;
120
+ connectionWaiters = /* @__PURE__ */ new Set();
121
+ socketGeneration = 0;
122
+ intentionalDisconnect = false;
123
+ lastConnectionError;
124
+ constructor(config) {
125
+ this.config = withDefaults(config);
126
+ }
127
+ /** Apply settings used by future requests/events without replacing the socket. */
128
+ configure(config) {
129
+ this.config = withDefaults(config);
130
+ }
131
+ get connectionId() {
132
+ return this._connectionId;
133
+ }
134
+ get generation() {
135
+ return this.socketGeneration;
136
+ }
137
+ waitForConnectionId(options = {}) {
138
+ if (this._connectionId && this.state === "connected") {
139
+ return Promise.resolve(this._connectionId);
140
+ }
141
+ if (options.signal?.aborted) {
142
+ return Promise.reject(new Error("Connection wait aborted"));
143
+ }
144
+ const timeoutMs = options.timeoutMs ?? this.config.connectionTimeout;
145
+ return new Promise((resolve, reject) => {
146
+ let timeout;
147
+ const abort = () => finish(new Error("Connection wait aborted"));
148
+ const waiter = {
149
+ resolve,
150
+ reject,
151
+ cleanup: () => {
152
+ if (timeout) clearTimeout(timeout);
153
+ options.signal?.removeEventListener("abort", abort);
154
+ }
155
+ };
156
+ const finish = (failure) => {
157
+ if (!this.connectionWaiters.delete(waiter)) return;
158
+ waiter.cleanup();
159
+ reject(failure);
160
+ };
161
+ this.connectionWaiters.add(waiter);
162
+ options.signal?.addEventListener("abort", abort, { once: true });
163
+ timeout = setTimeout(
164
+ () => finish(
165
+ new Error(`Connection timed out after ${timeoutMs}ms`, {
166
+ cause: this.lastConnectionError
167
+ })
168
+ ),
169
+ timeoutMs
170
+ );
171
+ });
172
+ }
173
+ getState() {
174
+ return this.state;
175
+ }
176
+ async connect() {
177
+ if (this.state === "connected") return;
178
+ if (this.state === "connecting") {
179
+ await this.waitForConnectionId();
180
+ return;
181
+ }
182
+ if (this.state !== "reconnecting") this.lastConnectionError = void 0;
183
+ this.intentionalDisconnect = false;
184
+ const generation = ++this.socketGeneration;
185
+ this.setState("connecting");
186
+ let socket;
187
+ let retryable = true;
188
+ try {
189
+ if (!this.config.webSocketFactory && typeof globalThis.WebSocket !== "function") {
190
+ retryable = false;
191
+ throw new Error(
192
+ "WebSocket is unavailable in this runtime. Enable the native WebSocket API (Node 20: --experimental-websocket), use Node 22 or newer, or configure webSocketFactory."
193
+ );
194
+ }
195
+ const url = await this.buildUrl();
196
+ if (generation !== this.socketGeneration || this.intentionalDisconnect) {
197
+ return;
198
+ }
199
+ let nextSocket;
200
+ try {
201
+ nextSocket = this.config.webSocketFactory ? this.config.webSocketFactory(url) : new globalThis.WebSocket(url);
202
+ } catch (cause) {
203
+ retryable = false;
204
+ throw cause;
205
+ }
206
+ socket = nextSocket;
207
+ this.ws = nextSocket;
208
+ this.startHandshakeTimer(nextSocket, generation);
209
+ nextSocket.onopen = () => {
210
+ if (!this.isCurrent(nextSocket, generation)) return;
211
+ nextSocket.send(JSON.stringify({ type: "init" }));
212
+ };
213
+ nextSocket.onclose = () => {
214
+ if (!this.isCurrent(nextSocket, generation)) return;
215
+ this.clearConnectionTimers();
216
+ this.ws = null;
217
+ this._connectionId = null;
218
+ this.setState("disconnected");
219
+ this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
220
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
221
+ this.scheduleReconnect();
222
+ } else {
223
+ this.rejectWaiters(new Error("WebSocket disconnected"));
224
+ }
225
+ };
226
+ nextSocket.onerror = () => {
227
+ if (!this.isCurrent(nextSocket, generation)) return;
228
+ const failure = new Error("WebSocket error");
229
+ this.lastConnectionError = failure;
230
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
231
+ };
232
+ nextSocket.onmessage = (event) => {
233
+ if (!this.isCurrent(nextSocket, generation)) return;
234
+ this.receiveMessage(nextSocket, generation, event.data);
235
+ };
236
+ } catch (cause) {
237
+ if (generation !== this.socketGeneration) return;
238
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
239
+ this.lastConnectionError = failure;
240
+ this.clearConnectionTimers();
241
+ if (!retryable) {
242
+ this.clearTimers();
243
+ this.reconnectAttempts = 0;
244
+ }
245
+ if (socket && this.ws === socket) socket.close();
246
+ this.ws = null;
247
+ this._connectionId = null;
248
+ this.setState("disconnected");
249
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
250
+ if (retryable && this.config.autoReconnect && !this.intentionalDisconnect) {
251
+ this.scheduleReconnect();
252
+ await this.waitForConnectionId();
253
+ return;
254
+ }
255
+ this.rejectWaiters(failure);
256
+ throw failure;
257
+ }
258
+ await this.waitForConnectionId();
259
+ }
260
+ disconnect() {
261
+ this.intentionalDisconnect = true;
262
+ ++this.socketGeneration;
263
+ this.clearTimers();
264
+ const socket = this.ws;
265
+ this.ws = null;
266
+ socket?.close(1e3, "Client disconnect");
267
+ this._connectionId = null;
268
+ this.setState("disconnected");
269
+ this.rejectWaiters(new Error("WebSocket disconnected"));
270
+ }
271
+ forceReconnect() {
272
+ this.intentionalDisconnect = false;
273
+ ++this.socketGeneration;
274
+ this.clearTimers();
275
+ const socket = this.ws;
276
+ this.ws = null;
277
+ socket?.close(1e3, "Force reconnect");
278
+ this._connectionId = null;
279
+ this.reconnectAttempts = 0;
280
+ this.setState("disconnected");
281
+ void this.connect().catch(() => void 0);
282
+ }
283
+ onMessage(handler) {
284
+ this.messageHandlers.add(handler);
285
+ return () => this.messageHandlers.delete(handler);
286
+ }
287
+ onStateChange(handler) {
288
+ this.stateHandlers.add(handler);
289
+ return () => this.stateHandlers.delete(handler);
290
+ }
291
+ onHandshake(handler) {
292
+ this.handshakeHandlers.add(handler);
293
+ return () => this.handshakeHandlers.delete(handler);
294
+ }
295
+ async buildUrl() {
296
+ let url = this.config.url;
297
+ if (!this.config.auth) return url;
298
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
299
+ const separator = url.includes("?") ? "&" : "?";
300
+ url = `${url}${separator}token=${encodeURIComponent(token)}`;
301
+ return url;
302
+ }
303
+ receiveMessage(socket, generation, rawData) {
304
+ try {
305
+ const message = JSON.parse(String(rawData));
306
+ if (message.type === "connected") {
307
+ if (this._connectionId) return;
308
+ this._connectionId = message.connectionId;
309
+ socket.send(JSON.stringify({ type: "ping" }));
310
+ return;
311
+ }
312
+ if (message.type === "pong") {
313
+ if (!this._connectionId || this.state === "connected") return;
314
+ this.clearHandshakeTimer();
315
+ this.lastConnectionError = void 0;
316
+ this.reconnectAttempts = 0;
317
+ this.setState("connected");
318
+ this.startHeartbeat(socket, generation);
319
+ this.invokeLifecycleCallback("onConnect", this.config.onConnect);
320
+ this.resolveWaiters(this._connectionId);
321
+ for (const handler of this.handshakeHandlers) handler(generation);
322
+ return;
323
+ }
324
+ for (const handler of this.messageHandlers) handler(message);
325
+ } catch (cause) {
326
+ error("Failed to parse WebSocket message:", cause);
327
+ }
328
+ }
329
+ startHandshakeTimer(socket, generation) {
330
+ this.clearHandshakeTimer();
331
+ this.handshakeTimer = setTimeout(() => {
332
+ if (!this.isCurrent(socket, generation) || this.state === "connected") {
333
+ return;
334
+ }
335
+ const failure = new Error("WebSocket handshake timed out");
336
+ this.lastConnectionError = failure;
337
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
338
+ socket.close(4e3, "Handshake timeout");
339
+ }, this.config.handshakeTimeout);
340
+ }
341
+ startHeartbeat(socket, generation) {
342
+ this.clearHeartbeatTimer();
343
+ this.heartbeatTimer = setInterval(() => {
344
+ if (!this.isCurrent(socket, generation) || this.state !== "connected" || socket.readyState !== socket.OPEN) {
345
+ return;
346
+ }
347
+ socket.send(JSON.stringify({ type: "ping" }));
348
+ }, HEARTBEAT_INTERVAL_MS);
349
+ }
350
+ isCurrent(socket, generation) {
351
+ return this.ws === socket && this.socketGeneration === generation;
352
+ }
353
+ setState(state) {
354
+ if (this.state === state) return;
355
+ this.state = state;
356
+ for (const handler of this.stateHandlers) handler(state);
357
+ }
358
+ scheduleReconnect() {
359
+ if (this.reconnectTimer || this.intentionalDisconnect) return;
360
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
361
+ const failure = new Error("Max reconnection attempts reached", {
362
+ cause: this.lastConnectionError
363
+ });
364
+ error(failure.message);
365
+ this.setState("disconnected");
366
+ this.rejectWaiters(failure);
367
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
368
+ return;
369
+ }
370
+ this.setState("reconnecting");
371
+ this.reconnectAttempts++;
372
+ const delay = Math.min(this.config.reconnectDelay * 2 ** (this.reconnectAttempts - 1), 3e4) * (0.5 + Math.random() * 0.5);
373
+ this.reconnectTimer = setTimeout(() => {
374
+ this.reconnectTimer = null;
375
+ void this.connect().catch(() => void 0);
376
+ }, delay);
377
+ }
378
+ resolveWaiters(connectionId) {
379
+ const waiters = [...this.connectionWaiters];
380
+ this.connectionWaiters.clear();
381
+ for (const waiter of waiters) {
382
+ waiter.cleanup();
383
+ waiter.resolve(connectionId);
384
+ }
385
+ }
386
+ invokeLifecycleCallback(name, callback, ...args) {
387
+ if (!callback) return;
388
+ try {
389
+ callback(...args);
390
+ } catch (cause) {
391
+ try {
392
+ error(`[dynamodb-reactive] ${name} callback failed:`, cause);
393
+ } catch {
394
+ }
395
+ }
396
+ }
397
+ rejectWaiters(failure) {
398
+ const waiters = [...this.connectionWaiters];
399
+ this.connectionWaiters.clear();
400
+ for (const waiter of waiters) {
401
+ waiter.cleanup();
402
+ waiter.reject(failure);
403
+ }
404
+ }
405
+ clearHandshakeTimer() {
406
+ if (!this.handshakeTimer) return;
407
+ clearTimeout(this.handshakeTimer);
408
+ this.handshakeTimer = null;
409
+ }
410
+ clearHeartbeatTimer() {
411
+ if (!this.heartbeatTimer) return;
412
+ clearInterval(this.heartbeatTimer);
413
+ this.heartbeatTimer = null;
414
+ }
415
+ clearConnectionTimers() {
416
+ this.clearHandshakeTimer();
417
+ this.clearHeartbeatTimer();
418
+ }
419
+ clearTimers() {
420
+ this.clearConnectionTimers();
421
+ if (!this.reconnectTimer) return;
422
+ clearTimeout(this.reconnectTimer);
423
+ this.reconnectTimer = null;
424
+ }
425
+ };
426
+
427
+ // ../client/src/client.ts
428
+ var DEFAULT_HTTP_URL = "/api/reactive";
429
+ function generateId() {
430
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
431
+ }
432
+ var ReactiveClient = class {
433
+ wsManager;
434
+ httpUrl;
435
+ authGetter;
436
+ headersGetter;
437
+ subscriptionsById = /* @__PURE__ */ new Map();
438
+ subscriptionsByKey = /* @__PURE__ */ new Map();
439
+ connectionState = "disconnected";
440
+ stateListeners = /* @__PURE__ */ new Set();
441
+ constructor(config) {
442
+ this.wsManager = new WebSocketManager(config);
443
+ this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
444
+ this.setAuth(config.auth);
445
+ this.setInitialHeaders(config.headers);
446
+ this.wsManager.onMessage((message) => this.handleMessage(message));
447
+ this.wsManager.onStateChange((state) => {
448
+ this.connectionState = state;
449
+ this.notifyStateListeners();
450
+ if (state === "disconnected" || state === "reconnecting") {
451
+ this.markSubscriptionsStale();
452
+ }
453
+ });
454
+ this.wsManager.onHandshake((socketGeneration) => {
455
+ this.registerActiveSubscriptions(socketGeneration);
456
+ });
457
+ }
458
+ /** Apply live configuration without replacing subscription caches or the socket. */
459
+ configure(config) {
460
+ this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
461
+ this.authGetter = this.toAuthGetter(config.auth);
462
+ this.headersGetter = this.toHeadersGetter(config.headers);
463
+ this.wsManager.configure(config);
464
+ }
465
+ async connect() {
466
+ await this.wsManager.connect();
467
+ }
468
+ disconnect() {
469
+ this.wsManager.disconnect();
470
+ }
471
+ forceReconnect() {
472
+ this.wsManager.forceReconnect();
473
+ }
474
+ getConnectionState() {
475
+ return this.connectionState;
476
+ }
477
+ onConnectionStateChange(listener) {
478
+ this.stateListeners.add(listener);
479
+ return () => this.stateListeners.delete(listener);
480
+ }
481
+ subscribe(path, options = {}) {
482
+ const key = getQueryKey(path, options.input);
483
+ let shared = this.subscriptionsByKey.get(key);
484
+ if (!shared) {
485
+ shared = {
486
+ id: generateId(),
487
+ key,
488
+ path,
489
+ input: options.input,
490
+ authoritativeData: void 0,
491
+ data: void 0,
492
+ loading: true,
493
+ stale: true,
494
+ error: void 0,
495
+ listeners: /* @__PURE__ */ new Set(),
496
+ observers: /* @__PURE__ */ new Map(),
497
+ active: true,
498
+ hasRegistered: false,
499
+ generation: 0,
500
+ registrationId: void 0,
501
+ registrationConnectionId: void 0,
502
+ registrationSocketGeneration: void 0,
503
+ registrationPromise: void 0,
504
+ version: void 0,
505
+ patchBuffer: /* @__PURE__ */ new Map(),
506
+ appliedMessageIds: /* @__PURE__ */ new Set(),
507
+ resyncPromise: void 0,
508
+ resyncGeneration: void 0,
509
+ optimisticLayers: /* @__PURE__ */ new Map(),
510
+ settlementRevision: 0,
511
+ registrationSettlementRevision: 0,
512
+ revision: 0
513
+ };
514
+ this.subscriptionsByKey.set(key, shared);
515
+ this.subscriptionsById.set(shared.id, shared);
516
+ if (this.connectionState === "connected") {
517
+ void this.ensureRegistered(
518
+ shared,
519
+ this.wsManager.generation
520
+ ).catch(() => void 0);
521
+ }
522
+ }
523
+ const leaseId = generateId();
524
+ shared.observers.set(leaseId, {
525
+ onData: options.onData ? (data) => options.onData?.(data) : void 0,
526
+ onError: options.onError
527
+ });
528
+ if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
529
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(
530
+ () => void 0
531
+ );
532
+ }
533
+ const state = shared;
534
+ let released = false;
535
+ return {
536
+ id: state.id,
537
+ key: state.key,
538
+ get data() {
539
+ return state.data;
540
+ },
541
+ get loading() {
542
+ return state.loading;
543
+ },
544
+ get stale() {
545
+ return state.stale;
546
+ },
547
+ get error() {
548
+ return state.error;
549
+ },
550
+ unsubscribe: () => {
551
+ if (released) return;
552
+ released = true;
553
+ this.releaseSubscription(state, leaseId);
554
+ },
555
+ refetch: () => this.refetch(state)
556
+ };
557
+ }
558
+ async call(path, input, options) {
559
+ const mutationId = generateId();
560
+ const layers = [];
561
+ try {
562
+ this.createMutationLayers(mutationId, input, options, layers);
563
+ const connectionId = await this.wsManager.waitForConnectionId();
564
+ const headers = await this.buildHeaders();
565
+ const response = await fetch(this.httpUrl, {
566
+ method: "POST",
567
+ headers,
568
+ body: JSON.stringify({ type: "call", connectionId, path, input })
569
+ });
570
+ if (!response.ok) {
571
+ throw new Error(`Call failed: ${response.status} ${response.statusText}`);
572
+ }
573
+ const result = await response.json();
574
+ if (result.type === "error") {
575
+ throw new Error(result.message ?? "Unknown error");
576
+ }
577
+ const data = result.data;
578
+ this.settleMutationLayers(mutationId, input, data, options, layers);
579
+ return data;
580
+ } catch (cause) {
581
+ this.removeMutationLayers(mutationId, layers);
582
+ throw cause;
583
+ }
584
+ }
585
+ addSubscriptionListener(id, listener) {
586
+ const state = this.subscriptionsById.get(id);
587
+ if (!state) return () => void 0;
588
+ state.listeners.add(listener);
589
+ return () => state.listeners.delete(listener);
590
+ }
591
+ getSubscriptionRevision(id) {
592
+ return this.subscriptionsById.get(id)?.revision ?? -1;
593
+ }
594
+ applyOptimisticUpdate(subscriptionId, updater) {
595
+ const state = this.subscriptionsById.get(subscriptionId);
596
+ if (!state || state.data === void 0) return;
597
+ state.optimisticLayers.set(`local-${generateId()}`, {
598
+ updater
599
+ });
600
+ this.recomputeData(state);
601
+ this.notifySubscription(state, true);
602
+ }
603
+ setAuth(auth) {
604
+ this.authGetter = this.toAuthGetter(auth);
605
+ }
606
+ setInitialHeaders(headers) {
607
+ this.headersGetter = this.toHeadersGetter(headers);
608
+ }
609
+ toAuthGetter(auth) {
610
+ if (!auth) return void 0;
611
+ return typeof auth === "function" ? auth : () => auth;
612
+ }
613
+ toHeadersGetter(headers) {
614
+ if (!headers) return void 0;
615
+ return typeof headers === "function" ? headers : () => headers;
616
+ }
617
+ registerActiveSubscriptions(socketGeneration) {
618
+ for (const state of this.subscriptionsByKey.values()) {
619
+ if (state.active) {
620
+ void this.ensureRegistered(state, socketGeneration).catch(
621
+ () => void 0
622
+ );
623
+ }
624
+ }
625
+ }
626
+ ensureRegistered(state, socketGeneration, force = false) {
627
+ if (!force && state.registrationSocketGeneration === socketGeneration && state.registrationPromise) {
628
+ return state.registrationPromise;
629
+ }
630
+ state.generation++;
631
+ state.registrationSettlementRevision = state.settlementRevision;
632
+ state.registrationId = generateId();
633
+ state.registrationSocketGeneration = socketGeneration;
634
+ state.registrationConnectionId = void 0;
635
+ state.registrationPromise = void 0;
636
+ state.resyncPromise = void 0;
637
+ state.resyncGeneration = void 0;
638
+ state.hasRegistered = false;
639
+ state.version = void 0;
640
+ state.patchBuffer.clear();
641
+ state.appliedMessageIds.clear();
642
+ state.loading = true;
643
+ state.stale = true;
644
+ state.error = void 0;
645
+ this.notifySubscription(state);
646
+ const generation = state.generation;
647
+ const registrationId = state.registrationId;
648
+ const isCurrent = () => state.active && state.generation === generation && state.registrationId === registrationId && state.registrationSocketGeneration === socketGeneration && this.wsManager.generation === socketGeneration;
649
+ let requestIssued = false;
650
+ let registrationConnectionId;
651
+ let request;
652
+ request = (async () => {
653
+ try {
654
+ const connectionId = await this.wsManager.waitForConnectionId();
655
+ if (!isCurrent()) return;
656
+ registrationConnectionId = connectionId;
657
+ state.registrationConnectionId = connectionId;
658
+ const headers = await this.buildHeaders();
659
+ if (!isCurrent()) return;
660
+ requestIssued = true;
661
+ const response = await fetch(this.httpUrl, {
662
+ method: "POST",
663
+ headers,
664
+ body: JSON.stringify({
665
+ type: "subscribe",
666
+ connectionId,
667
+ subscriptionId: state.id,
668
+ generation,
669
+ registrationId,
670
+ path: state.path,
671
+ input: state.input
672
+ })
673
+ });
674
+ if (!response.ok) {
675
+ throw new Error(
676
+ `Subscribe failed: ${response.status} ${response.statusText}`
677
+ );
678
+ }
679
+ const result = await response.json();
680
+ if (result.type === "error") {
681
+ throw new Error(result.message);
682
+ }
683
+ if (!isCurrent()) return;
684
+ this.handleSnapshot(result);
685
+ if (isCurrent()) state.hasRegistered = true;
686
+ } catch (cause) {
687
+ if (isCurrent()) {
688
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
689
+ state.error = failure;
690
+ state.loading = this.hasSettledLayers(state);
691
+ this.notifySubscription(state, false, failure);
692
+ }
693
+ throw cause;
694
+ } finally {
695
+ if ((!state.active || !isCurrent()) && requestIssued) {
696
+ void this.sendUnsubscribe(
697
+ registrationConnectionId,
698
+ state.id,
699
+ generation,
700
+ registrationId
701
+ );
702
+ }
703
+ if (state.registrationPromise === request) {
704
+ state.registrationPromise = void 0;
705
+ }
706
+ }
707
+ })();
708
+ state.registrationPromise = request;
709
+ return request;
710
+ }
711
+ releaseSubscription(state, leaseId) {
712
+ state.observers.delete(leaseId);
713
+ if (state.observers.size > 0) return;
714
+ state.active = false;
715
+ this.subscriptionsById.delete(state.id);
716
+ this.subscriptionsByKey.delete(state.key);
717
+ if (state.registrationId && state.registrationConnectionId && state.hasRegistered) {
718
+ void this.sendUnsubscribe(
719
+ state.registrationConnectionId,
720
+ state.id,
721
+ state.generation,
722
+ state.registrationId
723
+ );
724
+ }
725
+ }
726
+ async sendUnsubscribe(connectionId, subscriptionId, generation, registrationId) {
727
+ if (!connectionId) return;
728
+ try {
729
+ const headers = await this.buildHeaders();
730
+ await fetch(this.httpUrl, {
731
+ method: "POST",
732
+ headers,
733
+ body: JSON.stringify({
734
+ type: "unsubscribe",
735
+ connectionId,
736
+ subscriptionId,
737
+ generation,
738
+ registrationId
739
+ })
740
+ });
741
+ } catch {
742
+ }
743
+ }
744
+ async refetch(state) {
745
+ if (!state.active) return;
746
+ if (!state.registrationId) {
747
+ await this.ensureRegistered(state, this.wsManager.generation);
748
+ return;
749
+ }
750
+ await this.requestResync(state);
751
+ }
752
+ handleMessage(message) {
753
+ switch (message.type) {
754
+ case "patch":
755
+ this.handlePatch(message);
756
+ break;
757
+ case "resync": {
758
+ const state = this.subscriptionsById.get(message.subscriptionId);
759
+ if (state?.generation === message.generation) {
760
+ void this.requestResync(state).catch(() => void 0);
761
+ }
762
+ break;
763
+ }
764
+ }
765
+ }
766
+ handleSnapshot(message) {
767
+ const state = this.subscriptionsById.get(message.subscriptionId);
768
+ if (!state || message.generation !== state.generation) return;
769
+ if (state.version !== void 0 && message.version <= state.version) return;
770
+ state.authoritativeData = message.data;
771
+ state.version = message.version;
772
+ state.error = void 0;
773
+ const coveredRevision = state.registrationSettlementRevision;
774
+ for (const [layerId, layer] of state.optimisticLayers) {
775
+ if (layer.settledRevision !== void 0 && layer.settledRevision <= coveredRevision) {
776
+ state.optimisticLayers.delete(layerId);
777
+ }
778
+ }
779
+ const awaitingReconciliation = this.hasSettledLayers(state);
780
+ state.loading = awaitingReconciliation;
781
+ state.stale = awaitingReconciliation;
782
+ this.recomputeData(state);
783
+ this.discardStalePatches(state);
784
+ this.notifySubscription(state, true);
785
+ this.drainPatchBuffer(state);
786
+ }
787
+ handlePatch(message) {
788
+ const state = this.subscriptionsById.get(message.subscriptionId);
789
+ if (!state || message.generation !== state.generation) return;
790
+ if (state.appliedMessageIds.has(message.messageId) || state.patchBuffer.has(message.messageId)) {
791
+ return;
792
+ }
793
+ if (state.version !== void 0 && message.toVersion <= state.version) {
794
+ state.appliedMessageIds.add(message.messageId);
795
+ return;
796
+ }
797
+ state.patchBuffer.set(message.messageId, message);
798
+ if (state.version === void 0) return;
799
+ this.drainPatchBuffer(state);
800
+ }
801
+ drainPatchBuffer(state) {
802
+ if (state.version === void 0 || state.authoritativeData === void 0) {
803
+ return;
804
+ }
805
+ this.discardStalePatches(state);
806
+ while (true) {
807
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
808
+ if (!next) break;
809
+ state.patchBuffer.delete(next.messageId);
810
+ try {
811
+ state.authoritativeData = applyPatches(
812
+ state.authoritativeData,
813
+ next.patches
814
+ );
815
+ state.version = next.toVersion;
816
+ state.appliedMessageIds.add(next.messageId);
817
+ const awaitingReconciliation = this.hasSettledLayers(state);
818
+ state.loading = awaitingReconciliation;
819
+ state.stale = awaitingReconciliation;
820
+ state.error = void 0;
821
+ this.recomputeData(state);
822
+ this.notifySubscription(state, true);
823
+ this.discardStalePatches(state);
824
+ } catch {
825
+ void this.requestResync(state).catch(() => void 0);
826
+ return;
827
+ }
828
+ }
829
+ if ([...state.patchBuffer.values()].some(
830
+ (patch) => patch.toVersion > state.version && patch.fromVersion !== state.version
831
+ )) {
832
+ void this.requestResync(state).catch(() => void 0);
833
+ }
834
+ }
835
+ discardStalePatches(state) {
836
+ if (state.version === void 0) return;
837
+ for (const [messageId, patch] of state.patchBuffer) {
838
+ if (patch.toVersion <= state.version) {
839
+ state.patchBuffer.delete(messageId);
840
+ state.appliedMessageIds.add(messageId);
841
+ }
842
+ }
843
+ }
844
+ requestResync(state) {
845
+ if (state.resyncPromise && state.resyncGeneration === state.generation) {
846
+ return state.resyncPromise;
847
+ }
848
+ if (!state.active || !state.registrationId) return Promise.resolve();
849
+ state.stale = true;
850
+ state.loading = true;
851
+ this.notifySubscription(state);
852
+ let request;
853
+ request = this.ensureRegistered(
854
+ state,
855
+ this.wsManager.generation,
856
+ true
857
+ ).finally(() => {
858
+ if (state.resyncPromise === request) {
859
+ state.resyncPromise = void 0;
860
+ state.resyncGeneration = void 0;
861
+ }
862
+ }).then(() => {
863
+ if (state.active && this.hasSettledLayers(state)) {
864
+ return this.requestResync(state);
865
+ }
866
+ return void 0;
867
+ });
868
+ state.resyncPromise = request;
869
+ state.resyncGeneration = state.generation;
870
+ return request;
871
+ }
872
+ createMutationLayers(mutationId, input, options, layered) {
873
+ const targets = this.findMutationTargets(options);
874
+ for (const state of targets) {
875
+ const updater = this.createOptimisticUpdater(input, options);
876
+ if (!updater || state.authoritativeData === void 0) continue;
877
+ state.optimisticLayers.set(mutationId, {
878
+ updater
879
+ });
880
+ layered.push(state);
881
+ this.recomputeData(state);
882
+ this.notifySubscription(state, true);
883
+ }
884
+ }
885
+ findMutationTargets(options) {
886
+ if (options?.queryKey) {
887
+ const exact = this.subscriptionsByKey.get(options.queryKey);
888
+ return exact ? [exact] : [];
889
+ }
890
+ if (typeof options?.invalidates === "object") {
891
+ const exact = this.subscriptionsByKey.get(
892
+ getQueryKey(options.invalidates.path, options.invalidates.input)
893
+ );
894
+ return exact ? [exact] : [];
895
+ }
896
+ if (typeof options?.invalidates === "string") {
897
+ return [...this.subscriptionsByKey.values()].filter(
898
+ (state) => state.path === options.invalidates
899
+ );
900
+ }
901
+ return [];
902
+ }
903
+ createOptimisticUpdater(input, options, result, settled = false) {
904
+ const update = options?.optimisticUpdate;
905
+ if (typeof update === "function") {
906
+ return (data) => Array.isArray(data) ? update(data, input, result) : data;
907
+ }
908
+ const itemId = typeof input === "object" && input !== null ? input.id : void 0;
909
+ if (update === "remove" && itemId !== void 0) {
910
+ return (data) => Array.isArray(data) ? data.filter((item) => item.id !== itemId) : data;
911
+ }
912
+ if (update !== "merge") return void 0;
913
+ if (itemId !== void 0) {
914
+ const resultFields = settled && typeof result === "object" && result !== null ? result : {};
915
+ return (data) => {
916
+ if (!Array.isArray(data)) return data;
917
+ return data.map(
918
+ (item) => item.id === itemId ? { ...item, ...input, ...resultFields } : item
919
+ );
920
+ };
921
+ }
922
+ if (options?.optimisticData === void 0) return void 0;
923
+ return (data) => {
924
+ if (!Array.isArray(data)) return data;
925
+ const optimistic = settled ? result === void 0 ? typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData : result : typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData;
926
+ if (settled && optimistic !== null && typeof optimistic === "object") {
927
+ const optimisticId = optimistic.id;
928
+ if (optimisticId !== void 0 && data.some(
929
+ (item) => typeof item === "object" && item !== null && item.id === optimisticId
930
+ )) {
931
+ return data;
932
+ }
933
+ }
934
+ return optimistic === void 0 ? data : [optimistic, ...data];
935
+ };
936
+ }
937
+ settleMutationLayers(mutationId, input, result, options, states) {
938
+ for (const state of states) {
939
+ const layer = state.optimisticLayers.get(mutationId);
940
+ const updater = this.createOptimisticUpdater(
941
+ input,
942
+ options,
943
+ result,
944
+ true
945
+ );
946
+ if (!layer || !updater) continue;
947
+ layer.updater = updater;
948
+ layer.settledRevision = ++state.settlementRevision;
949
+ state.loading = true;
950
+ state.stale = true;
951
+ this.recomputeData(state);
952
+ this.notifySubscription(state, true);
953
+ void this.requestResync(state).catch(() => void 0);
954
+ }
955
+ }
956
+ removeMutationLayers(mutationId, states) {
957
+ for (const state of states) {
958
+ if (!state.optimisticLayers.delete(mutationId)) continue;
959
+ this.recomputeData(state);
960
+ this.notifySubscription(state, true);
961
+ }
962
+ }
963
+ recomputeData(state) {
964
+ let data = state.authoritativeData;
965
+ for (const layer of state.optimisticLayers.values()) {
966
+ data = layer.updater(data);
967
+ }
968
+ state.data = data;
969
+ }
970
+ hasSettledLayers(state) {
971
+ for (const layer of state.optimisticLayers.values()) {
972
+ if (layer.settledRevision !== void 0) return true;
973
+ }
974
+ return false;
975
+ }
976
+ async buildHeaders() {
977
+ const headers = {
978
+ "Content-Type": "application/json"
979
+ };
980
+ if (this.headersGetter) Object.assign(headers, await this.headersGetter());
981
+ if (this.authGetter) {
982
+ headers["Authorization"] = `Bearer ${await this.authGetter()}`;
983
+ }
984
+ return headers;
985
+ }
986
+ markSubscriptionsStale() {
987
+ for (const state of this.subscriptionsByKey.values()) {
988
+ if (!state.active) continue;
989
+ state.stale = true;
990
+ state.loading = true;
991
+ this.notifySubscription(state);
992
+ }
993
+ }
994
+ notifySubscription(state, dataChanged = false, failure) {
995
+ if (!state.active) return;
996
+ state.revision++;
997
+ for (const listener of state.listeners) listener();
998
+ if (dataChanged) {
999
+ for (const observer of state.observers.values()) {
1000
+ observer.onData?.(state.data);
1001
+ }
1002
+ }
1003
+ if (failure) {
1004
+ for (const observer of state.observers.values()) {
1005
+ observer.onError?.(failure);
1006
+ }
1007
+ }
1008
+ }
1009
+ notifyStateListeners() {
1010
+ for (const listener of this.stateListeners) {
1011
+ listener(this.connectionState);
1012
+ }
1013
+ }
1014
+ };
1015
+
1016
+ // ../client/src/cluster.ts
1017
+ var DEFAULT_CLUSTER_TICKET_PATH = "/api/cluster/token";
1018
+ var DEFAULT_CLUSTER_HTTP_PATH = "/api/reactive";
1019
+ var DEFAULT_RETRY_DELAY_MS = 1e4;
1020
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1021
+ var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
1022
+ function isRecord(value) {
1023
+ return typeof value === "object" && value !== null;
1024
+ }
1025
+ function parseClusterTicket(value) {
1026
+ if (!isRecord(value) || typeof value.token !== "string" || value.token.length === 0 || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt) || typeof value.url !== "string") {
1027
+ throw new Error("Cluster ticket response is invalid");
1028
+ }
1029
+ const url = new URL(value.url);
1030
+ if (url.protocol !== "wss:") throw new Error("Cluster ticket URL must use WSS");
1031
+ return { token: value.token, expiresAt: value.expiresAt, url: url.toString() };
1032
+ }
1033
+ function toError(cause) {
1034
+ return cause instanceof Error ? cause : new Error(String(cause));
1035
+ }
1036
+ function abortError(signal) {
1037
+ return toError(signal.reason ?? new Error("Cluster worker connection aborted"));
1038
+ }
1039
+ function wait(milliseconds, signal) {
1040
+ return new Promise((resolve, reject) => {
1041
+ if (signal?.aborted) {
1042
+ reject(abortError(signal));
1043
+ return;
1044
+ }
1045
+ const onAbort = () => {
1046
+ clearTimeout(timer);
1047
+ reject(abortError(signal));
1048
+ };
1049
+ const timer = setTimeout(() => {
1050
+ signal?.removeEventListener("abort", onAbort);
1051
+ resolve();
1052
+ }, milliseconds);
1053
+ signal?.addEventListener("abort", onAbort, { once: true });
1054
+ });
1055
+ }
1056
+ async function fetchClusterTicket(config) {
1057
+ const runnerToken = typeof config.runnerToken === "function" ? await config.runnerToken() : config.runnerToken;
1058
+ const endpoint = new URL(
1059
+ config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH,
1060
+ config.baseUrl
1061
+ );
1062
+ const response = await (config.fetch ?? fetch)(endpoint, {
1063
+ method: "POST",
1064
+ headers: { authorization: `Bearer ${runnerToken}` },
1065
+ signal: AbortSignal.timeout(config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS)
1066
+ });
1067
+ if (!response.ok) {
1068
+ throw new Error(`Cluster ticket request failed: HTTP ${response.status}`);
1069
+ }
1070
+ return parseClusterTicket(await response.json());
1071
+ }
1072
+ async function connectClusterWorker(config) {
1073
+ const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
1074
+ const httpUrl = new URL(
1075
+ config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH,
1076
+ config.baseUrl
1077
+ ).toString();
1078
+ for (; ; ) {
1079
+ if (config.signal?.aborted) throw abortError(config.signal);
1080
+ let client;
1081
+ try {
1082
+ const ticket = await fetchClusterTicket(config);
1083
+ client = new ReactiveClient({
1084
+ reconnectDelay: retryDelay,
1085
+ maxReconnectAttempts: Number.MAX_SAFE_INTEGER,
1086
+ ...config.client,
1087
+ url: ticket.url,
1088
+ httpUrl,
1089
+ auth: async () => (await fetchClusterTicket(config)).token
1090
+ });
1091
+ await client.connect();
1092
+ return client;
1093
+ } catch (cause) {
1094
+ client?.disconnect();
1095
+ if (config.signal?.aborted) throw abortError(config.signal);
1096
+ config.onRetry?.(toError(cause));
1097
+ await wait(retryDelay, config.signal);
1098
+ }
1099
+ }
1100
+ }
1101
+ var encoder = new TextEncoder();
1102
+ function base64UrlEncode(bytes) {
1103
+ let binary = "";
1104
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1105
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1106
+ }
1107
+ function base64UrlDecode(value) {
1108
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/");
1109
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
1110
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
1111
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1112
+ return bytes;
1113
+ }
1114
+ function hmacKey(secret, usage) {
1115
+ return crypto.subtle.importKey(
1116
+ "raw",
1117
+ encoder.encode(secret),
1118
+ { name: "HMAC", hash: "SHA-256" },
1119
+ false,
1120
+ [usage]
1121
+ );
1122
+ }
1123
+ async function createSignedClusterTicket(options) {
1124
+ const expiresAt = Math.floor((options.now ?? Date.now()) / 1e3) + (options.lifetimeSeconds ?? DEFAULT_TICKET_LIFETIME_SECONDS);
1125
+ const payload = base64UrlEncode(
1126
+ encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt }))
1127
+ );
1128
+ const signature = new Uint8Array(
1129
+ await crypto.subtle.sign("HMAC", await hmacKey(options.secret, "sign"), encoder.encode(payload))
1130
+ );
1131
+ return parseClusterTicket({
1132
+ token: `${payload}.${base64UrlEncode(signature)}`,
1133
+ expiresAt,
1134
+ url: options.url
1135
+ });
1136
+ }
1137
+ async function verifySignedClusterTicket(token, secret, now = Date.now()) {
1138
+ const [payload, signature, extra] = token.split(".");
1139
+ if (!payload || !signature || extra !== void 0) return void 0;
1140
+ try {
1141
+ const valid = await crypto.subtle.verify(
1142
+ "HMAC",
1143
+ await hmacKey(secret, "verify"),
1144
+ base64UrlDecode(signature),
1145
+ encoder.encode(payload)
1146
+ );
1147
+ if (!valid) return void 0;
1148
+ const claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payload)));
1149
+ if (!isRecord(claims) || typeof claims.p !== "string" || typeof claims.exp !== "number" || claims.exp <= Math.floor(now / 1e3)) {
1150
+ return void 0;
1151
+ }
1152
+ return claims.p;
1153
+ } catch {
1154
+ return void 0;
1155
+ }
1156
+ }
1157
+
1158
+ exports.DEFAULT_CLUSTER_HTTP_PATH = DEFAULT_CLUSTER_HTTP_PATH;
1159
+ exports.DEFAULT_CLUSTER_TICKET_PATH = DEFAULT_CLUSTER_TICKET_PATH;
1160
+ exports.connectClusterWorker = connectClusterWorker;
1161
+ exports.createSignedClusterTicket = createSignedClusterTicket;
1162
+ exports.fetchClusterTicket = fetchClusterTicket;
1163
+ exports.parseClusterTicket = parseClusterTicket;
1164
+ exports.verifySignedClusterTicket = verifySignedClusterTicket;
1165
+ //# sourceMappingURL=cluster.cjs.map
1166
+ //# sourceMappingURL=cluster.cjs.map