dynamodb-reactive 0.1.21 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +378 -153
  2. package/dist/client-r_tnzwqi.d.ts +224 -0
  3. package/dist/client.cjs +1023 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.d.cts +6 -0
  6. package/dist/client.d.ts +4 -70
  7. package/dist/client.js +1013 -1
  8. package/dist/client.js.map +1 -1
  9. package/dist/core.cjs +232 -0
  10. package/dist/core.cjs.map +1 -0
  11. package/dist/core.d.cts +4 -0
  12. package/dist/core.d.ts +3 -3
  13. package/dist/core.js +222 -1
  14. package/dist/core.js.map +1 -1
  15. package/dist/index.cjs +232 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +157 -0
  18. package/dist/index.d.ts +103 -56
  19. package/dist/index.js +222 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/infra.cjs +623 -0
  22. package/dist/infra.cjs.map +1 -0
  23. package/dist/infra.d.cts +119 -0
  24. package/dist/infra.d.ts +50 -102
  25. package/dist/infra.js +371 -144
  26. package/dist/infra.js.map +1 -1
  27. package/dist/react.cjs +1250 -0
  28. package/dist/react.cjs.map +1 -0
  29. package/dist/react.d.cts +68 -0
  30. package/dist/react.d.ts +68 -3
  31. package/dist/react.js +1237 -1
  32. package/dist/react.js.map +1 -1
  33. package/dist/server.cjs +2351 -0
  34. package/dist/server.cjs.map +1 -0
  35. package/dist/server.d.cts +364 -0
  36. package/dist/server.d.ts +58 -313
  37. package/dist/server.js +1855 -1208
  38. package/dist/server.js.map +1 -1
  39. package/dist/{table-tN3D4Tm-.d.ts → table-BijJ73pX.d.ts} +2 -34
  40. package/dist/{types-ISMJfyIv.d.ts → types-LMLk8JVC.d.ts} +56 -30
  41. package/package.json +79 -41
  42. package/dist/chunk-2A7GJ2UO.js +0 -1088
  43. package/dist/chunk-2A7GJ2UO.js.map +0 -1
  44. package/dist/chunk-ZAJA4KBE.js +0 -150
  45. package/dist/chunk-ZAJA4KBE.js.map +0 -1
  46. package/dist/react-B2b5RIz_.d.ts +0 -549
@@ -0,0 +1,1023 @@
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
+ constructor(config) {
124
+ this.config = withDefaults(config);
125
+ }
126
+ /** Apply settings used by future requests/events without replacing the socket. */
127
+ configure(config) {
128
+ this.config = withDefaults(config);
129
+ }
130
+ get connectionId() {
131
+ return this._connectionId;
132
+ }
133
+ get generation() {
134
+ return this.socketGeneration;
135
+ }
136
+ waitForConnectionId(options = {}) {
137
+ if (this._connectionId && this.state === "connected") {
138
+ return Promise.resolve(this._connectionId);
139
+ }
140
+ if (options.signal?.aborted) {
141
+ return Promise.reject(new Error("Connection wait aborted"));
142
+ }
143
+ const timeoutMs = options.timeoutMs ?? this.config.connectionTimeout;
144
+ return new Promise((resolve, reject) => {
145
+ let timeout;
146
+ const abort = () => finish(new Error("Connection wait aborted"));
147
+ const waiter = {
148
+ resolve,
149
+ reject,
150
+ cleanup: () => {
151
+ if (timeout) clearTimeout(timeout);
152
+ options.signal?.removeEventListener("abort", abort);
153
+ }
154
+ };
155
+ const finish = (failure) => {
156
+ if (!this.connectionWaiters.delete(waiter)) return;
157
+ waiter.cleanup();
158
+ reject(failure);
159
+ };
160
+ this.connectionWaiters.add(waiter);
161
+ options.signal?.addEventListener("abort", abort, { once: true });
162
+ timeout = setTimeout(
163
+ () => finish(new Error(`Connection timed out after ${timeoutMs}ms`)),
164
+ timeoutMs
165
+ );
166
+ });
167
+ }
168
+ getState() {
169
+ return this.state;
170
+ }
171
+ async connect() {
172
+ if (this.state === "connected") return;
173
+ if (this.state === "connecting") {
174
+ await this.waitForConnectionId();
175
+ return;
176
+ }
177
+ this.intentionalDisconnect = false;
178
+ const generation = ++this.socketGeneration;
179
+ this.setState("connecting");
180
+ let socket;
181
+ try {
182
+ const url = await this.buildUrl();
183
+ if (generation !== this.socketGeneration || this.intentionalDisconnect) {
184
+ return;
185
+ }
186
+ const nextSocket = new WebSocket(url);
187
+ socket = nextSocket;
188
+ this.ws = nextSocket;
189
+ this.startHandshakeTimer(nextSocket, generation);
190
+ nextSocket.onopen = () => {
191
+ if (!this.isCurrent(nextSocket, generation)) return;
192
+ nextSocket.send(JSON.stringify({ type: "init" }));
193
+ };
194
+ nextSocket.onclose = () => {
195
+ if (!this.isCurrent(nextSocket, generation)) return;
196
+ this.clearConnectionTimers();
197
+ this.ws = null;
198
+ this._connectionId = null;
199
+ this.setState("disconnected");
200
+ this.invokeLifecycleCallback(
201
+ "onDisconnect",
202
+ this.config.onDisconnect
203
+ );
204
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
205
+ this.scheduleReconnect();
206
+ } else {
207
+ this.rejectWaiters(new Error("WebSocket disconnected"));
208
+ }
209
+ };
210
+ nextSocket.onerror = () => {
211
+ if (!this.isCurrent(nextSocket, generation)) return;
212
+ this.invokeLifecycleCallback(
213
+ "onError",
214
+ this.config.onError,
215
+ new Error("WebSocket error")
216
+ );
217
+ };
218
+ nextSocket.onmessage = (event) => {
219
+ if (!this.isCurrent(nextSocket, generation)) return;
220
+ this.receiveMessage(nextSocket, generation, event.data);
221
+ };
222
+ } catch (cause) {
223
+ if (generation !== this.socketGeneration) return;
224
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
225
+ this.clearConnectionTimers();
226
+ if (socket && this.ws === socket) socket.close();
227
+ this.ws = null;
228
+ this._connectionId = null;
229
+ this.setState("disconnected");
230
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
231
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
232
+ this.scheduleReconnect();
233
+ await this.waitForConnectionId();
234
+ return;
235
+ }
236
+ this.rejectWaiters(failure);
237
+ throw failure;
238
+ }
239
+ await this.waitForConnectionId();
240
+ }
241
+ disconnect() {
242
+ this.intentionalDisconnect = true;
243
+ ++this.socketGeneration;
244
+ this.clearTimers();
245
+ const socket = this.ws;
246
+ this.ws = null;
247
+ socket?.close(1e3, "Client disconnect");
248
+ this._connectionId = null;
249
+ this.setState("disconnected");
250
+ this.rejectWaiters(new Error("WebSocket disconnected"));
251
+ }
252
+ forceReconnect() {
253
+ this.intentionalDisconnect = false;
254
+ ++this.socketGeneration;
255
+ this.clearTimers();
256
+ const socket = this.ws;
257
+ this.ws = null;
258
+ socket?.close(1e3, "Force reconnect");
259
+ this._connectionId = null;
260
+ this.reconnectAttempts = 0;
261
+ this.setState("disconnected");
262
+ void this.connect().catch(() => void 0);
263
+ }
264
+ onMessage(handler) {
265
+ this.messageHandlers.add(handler);
266
+ return () => this.messageHandlers.delete(handler);
267
+ }
268
+ onStateChange(handler) {
269
+ this.stateHandlers.add(handler);
270
+ return () => this.stateHandlers.delete(handler);
271
+ }
272
+ onHandshake(handler) {
273
+ this.handshakeHandlers.add(handler);
274
+ return () => this.handshakeHandlers.delete(handler);
275
+ }
276
+ async buildUrl() {
277
+ let url = this.config.url;
278
+ if (!this.config.auth) return url;
279
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
280
+ const separator = url.includes("?") ? "&" : "?";
281
+ url = `${url}${separator}token=${encodeURIComponent(token)}`;
282
+ return url;
283
+ }
284
+ receiveMessage(socket, generation, rawData) {
285
+ try {
286
+ const message = JSON.parse(String(rawData));
287
+ if (message.type === "connected") {
288
+ if (this._connectionId) return;
289
+ this._connectionId = message.connectionId;
290
+ socket.send(JSON.stringify({ type: "ping" }));
291
+ return;
292
+ }
293
+ if (message.type === "pong") {
294
+ if (!this._connectionId || this.state === "connected") return;
295
+ this.clearHandshakeTimer();
296
+ this.reconnectAttempts = 0;
297
+ this.setState("connected");
298
+ this.startHeartbeat(socket, generation);
299
+ this.invokeLifecycleCallback("onConnect", this.config.onConnect);
300
+ this.resolveWaiters(this._connectionId);
301
+ for (const handler of this.handshakeHandlers) handler(generation);
302
+ return;
303
+ }
304
+ for (const handler of this.messageHandlers) handler(message);
305
+ } catch (cause) {
306
+ error("Failed to parse WebSocket message:", cause);
307
+ }
308
+ }
309
+ startHandshakeTimer(socket, generation) {
310
+ this.clearHandshakeTimer();
311
+ this.handshakeTimer = setTimeout(() => {
312
+ if (!this.isCurrent(socket, generation) || this.state === "connected") {
313
+ return;
314
+ }
315
+ const failure = new Error("WebSocket handshake timed out");
316
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
317
+ socket.close(4e3, "Handshake timeout");
318
+ }, this.config.handshakeTimeout);
319
+ }
320
+ startHeartbeat(socket, generation) {
321
+ this.clearHeartbeatTimer();
322
+ this.heartbeatTimer = setInterval(() => {
323
+ if (!this.isCurrent(socket, generation) || this.state !== "connected" || socket.readyState !== WebSocket.OPEN) {
324
+ return;
325
+ }
326
+ socket.send(JSON.stringify({ type: "ping" }));
327
+ }, HEARTBEAT_INTERVAL_MS);
328
+ }
329
+ isCurrent(socket, generation) {
330
+ return this.ws === socket && this.socketGeneration === generation;
331
+ }
332
+ setState(state) {
333
+ if (this.state === state) return;
334
+ this.state = state;
335
+ for (const handler of this.stateHandlers) handler(state);
336
+ }
337
+ scheduleReconnect() {
338
+ if (this.reconnectTimer || this.intentionalDisconnect) return;
339
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
340
+ const failure = new Error("Max reconnection attempts reached");
341
+ error(failure.message);
342
+ this.setState("disconnected");
343
+ this.rejectWaiters(failure);
344
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
345
+ return;
346
+ }
347
+ this.setState("reconnecting");
348
+ this.reconnectAttempts++;
349
+ const delay = Math.min(
350
+ this.config.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
351
+ 3e4
352
+ ) * (0.5 + Math.random() * 0.5);
353
+ this.reconnectTimer = setTimeout(() => {
354
+ this.reconnectTimer = null;
355
+ void this.connect().catch(() => void 0);
356
+ }, delay);
357
+ }
358
+ resolveWaiters(connectionId) {
359
+ const waiters = [...this.connectionWaiters];
360
+ this.connectionWaiters.clear();
361
+ for (const waiter of waiters) {
362
+ waiter.cleanup();
363
+ waiter.resolve(connectionId);
364
+ }
365
+ }
366
+ invokeLifecycleCallback(name, callback, ...args) {
367
+ if (!callback) return;
368
+ try {
369
+ callback(...args);
370
+ } catch (cause) {
371
+ try {
372
+ error(`[dynamodb-reactive] ${name} callback failed:`, cause);
373
+ } catch {
374
+ }
375
+ }
376
+ }
377
+ rejectWaiters(failure) {
378
+ const waiters = [...this.connectionWaiters];
379
+ this.connectionWaiters.clear();
380
+ for (const waiter of waiters) {
381
+ waiter.cleanup();
382
+ waiter.reject(failure);
383
+ }
384
+ }
385
+ clearHandshakeTimer() {
386
+ if (!this.handshakeTimer) return;
387
+ clearTimeout(this.handshakeTimer);
388
+ this.handshakeTimer = null;
389
+ }
390
+ clearHeartbeatTimer() {
391
+ if (!this.heartbeatTimer) return;
392
+ clearInterval(this.heartbeatTimer);
393
+ this.heartbeatTimer = null;
394
+ }
395
+ clearConnectionTimers() {
396
+ this.clearHandshakeTimer();
397
+ this.clearHeartbeatTimer();
398
+ }
399
+ clearTimers() {
400
+ this.clearConnectionTimers();
401
+ if (!this.reconnectTimer) return;
402
+ clearTimeout(this.reconnectTimer);
403
+ this.reconnectTimer = null;
404
+ }
405
+ };
406
+
407
+ // ../client/src/client.ts
408
+ var DEFAULT_HTTP_URL = "/api/reactive";
409
+ function generateId() {
410
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
411
+ }
412
+ var ReactiveClient = class {
413
+ wsManager;
414
+ httpUrl;
415
+ authGetter;
416
+ headersGetter;
417
+ subscriptionsById = /* @__PURE__ */ new Map();
418
+ subscriptionsByKey = /* @__PURE__ */ new Map();
419
+ connectionState = "disconnected";
420
+ stateListeners = /* @__PURE__ */ new Set();
421
+ constructor(config) {
422
+ this.wsManager = new WebSocketManager(config);
423
+ this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
424
+ this.setAuth(config.auth);
425
+ this.setInitialHeaders(config.headers);
426
+ this.wsManager.onMessage((message) => this.handleMessage(message));
427
+ this.wsManager.onStateChange((state) => {
428
+ this.connectionState = state;
429
+ this.notifyStateListeners();
430
+ if (state === "disconnected" || state === "reconnecting") {
431
+ this.markSubscriptionsStale();
432
+ }
433
+ });
434
+ this.wsManager.onHandshake((socketGeneration) => {
435
+ this.registerActiveSubscriptions(socketGeneration);
436
+ });
437
+ }
438
+ /** Apply live configuration without replacing subscription caches or the socket. */
439
+ configure(config) {
440
+ this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
441
+ this.authGetter = this.toAuthGetter(config.auth);
442
+ this.headersGetter = this.toHeadersGetter(config.headers);
443
+ this.wsManager.configure(config);
444
+ }
445
+ async connect() {
446
+ await this.wsManager.connect();
447
+ }
448
+ disconnect() {
449
+ this.wsManager.disconnect();
450
+ }
451
+ forceReconnect() {
452
+ this.wsManager.forceReconnect();
453
+ }
454
+ getConnectionState() {
455
+ return this.connectionState;
456
+ }
457
+ onConnectionStateChange(listener) {
458
+ this.stateListeners.add(listener);
459
+ return () => this.stateListeners.delete(listener);
460
+ }
461
+ subscribe(path, options = {}) {
462
+ const key = getQueryKey(path, options.input);
463
+ let shared = this.subscriptionsByKey.get(key);
464
+ if (!shared) {
465
+ shared = {
466
+ id: generateId(),
467
+ key,
468
+ path,
469
+ input: options.input,
470
+ authoritativeData: void 0,
471
+ data: void 0,
472
+ loading: true,
473
+ stale: true,
474
+ error: void 0,
475
+ listeners: /* @__PURE__ */ new Set(),
476
+ observers: /* @__PURE__ */ new Map(),
477
+ active: true,
478
+ hasRegistered: false,
479
+ generation: 0,
480
+ registrationId: void 0,
481
+ registrationConnectionId: void 0,
482
+ registrationSocketGeneration: void 0,
483
+ registrationPromise: void 0,
484
+ version: void 0,
485
+ patchBuffer: /* @__PURE__ */ new Map(),
486
+ appliedMessageIds: /* @__PURE__ */ new Set(),
487
+ resyncPromise: void 0,
488
+ resyncGeneration: void 0,
489
+ optimisticLayers: /* @__PURE__ */ new Map(),
490
+ settlementRevision: 0,
491
+ registrationSettlementRevision: 0,
492
+ revision: 0
493
+ };
494
+ this.subscriptionsByKey.set(key, shared);
495
+ this.subscriptionsById.set(shared.id, shared);
496
+ if (this.connectionState === "connected") {
497
+ void this.ensureRegistered(
498
+ shared,
499
+ this.wsManager.generation
500
+ ).catch(() => void 0);
501
+ }
502
+ }
503
+ const leaseId = generateId();
504
+ shared.observers.set(leaseId, {
505
+ onData: options.onData ? (data) => options.onData?.(data) : void 0,
506
+ onError: options.onError
507
+ });
508
+ if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
509
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(
510
+ () => void 0
511
+ );
512
+ }
513
+ const state = shared;
514
+ let released = false;
515
+ return {
516
+ id: state.id,
517
+ key: state.key,
518
+ get data() {
519
+ return state.data;
520
+ },
521
+ get loading() {
522
+ return state.loading;
523
+ },
524
+ get stale() {
525
+ return state.stale;
526
+ },
527
+ get error() {
528
+ return state.error;
529
+ },
530
+ unsubscribe: () => {
531
+ if (released) return;
532
+ released = true;
533
+ this.releaseSubscription(state, leaseId);
534
+ },
535
+ refetch: () => this.refetch(state)
536
+ };
537
+ }
538
+ async call(path, input, options) {
539
+ const mutationId = generateId();
540
+ const layers = [];
541
+ try {
542
+ this.createMutationLayers(mutationId, input, options, layers);
543
+ const connectionId = await this.wsManager.waitForConnectionId();
544
+ const headers = await this.buildHeaders();
545
+ const response = await fetch(this.httpUrl, {
546
+ method: "POST",
547
+ headers,
548
+ body: JSON.stringify({ type: "call", connectionId, path, input })
549
+ });
550
+ if (!response.ok) {
551
+ throw new Error(`Call failed: ${response.status} ${response.statusText}`);
552
+ }
553
+ const result = await response.json();
554
+ if (result.type === "error") {
555
+ throw new Error(result.message ?? "Unknown error");
556
+ }
557
+ const data = result.data;
558
+ this.settleMutationLayers(mutationId, input, data, options, layers);
559
+ return data;
560
+ } catch (cause) {
561
+ this.removeMutationLayers(mutationId, layers);
562
+ throw cause;
563
+ }
564
+ }
565
+ addSubscriptionListener(id, listener) {
566
+ const state = this.subscriptionsById.get(id);
567
+ if (!state) return () => void 0;
568
+ state.listeners.add(listener);
569
+ return () => state.listeners.delete(listener);
570
+ }
571
+ getSubscriptionRevision(id) {
572
+ return this.subscriptionsById.get(id)?.revision ?? -1;
573
+ }
574
+ applyOptimisticUpdate(subscriptionId, updater) {
575
+ const state = this.subscriptionsById.get(subscriptionId);
576
+ if (!state || state.data === void 0) return;
577
+ state.optimisticLayers.set(`local-${generateId()}`, {
578
+ updater
579
+ });
580
+ this.recomputeData(state);
581
+ this.notifySubscription(state, true);
582
+ }
583
+ setAuth(auth) {
584
+ this.authGetter = this.toAuthGetter(auth);
585
+ }
586
+ setInitialHeaders(headers) {
587
+ this.headersGetter = this.toHeadersGetter(headers);
588
+ }
589
+ toAuthGetter(auth) {
590
+ if (!auth) return void 0;
591
+ return typeof auth === "function" ? auth : () => auth;
592
+ }
593
+ toHeadersGetter(headers) {
594
+ if (!headers) return void 0;
595
+ return typeof headers === "function" ? headers : () => headers;
596
+ }
597
+ registerActiveSubscriptions(socketGeneration) {
598
+ for (const state of this.subscriptionsByKey.values()) {
599
+ if (state.active) {
600
+ void this.ensureRegistered(state, socketGeneration).catch(
601
+ () => void 0
602
+ );
603
+ }
604
+ }
605
+ }
606
+ ensureRegistered(state, socketGeneration, force = false) {
607
+ if (!force && state.registrationSocketGeneration === socketGeneration && state.registrationPromise) {
608
+ return state.registrationPromise;
609
+ }
610
+ state.generation++;
611
+ state.registrationSettlementRevision = state.settlementRevision;
612
+ state.registrationId = generateId();
613
+ state.registrationSocketGeneration = socketGeneration;
614
+ state.registrationConnectionId = void 0;
615
+ state.registrationPromise = void 0;
616
+ state.resyncPromise = void 0;
617
+ state.resyncGeneration = void 0;
618
+ state.hasRegistered = false;
619
+ state.version = void 0;
620
+ state.patchBuffer.clear();
621
+ state.appliedMessageIds.clear();
622
+ state.loading = true;
623
+ state.stale = true;
624
+ state.error = void 0;
625
+ this.notifySubscription(state);
626
+ const generation = state.generation;
627
+ const registrationId = state.registrationId;
628
+ const isCurrent = () => state.active && state.generation === generation && state.registrationId === registrationId && state.registrationSocketGeneration === socketGeneration && this.wsManager.generation === socketGeneration;
629
+ let requestIssued = false;
630
+ let registrationConnectionId;
631
+ let request;
632
+ request = (async () => {
633
+ try {
634
+ const connectionId = await this.wsManager.waitForConnectionId();
635
+ if (!isCurrent()) return;
636
+ registrationConnectionId = connectionId;
637
+ state.registrationConnectionId = connectionId;
638
+ const headers = await this.buildHeaders();
639
+ if (!isCurrent()) return;
640
+ requestIssued = true;
641
+ const response = await fetch(this.httpUrl, {
642
+ method: "POST",
643
+ headers,
644
+ body: JSON.stringify({
645
+ type: "subscribe",
646
+ connectionId,
647
+ subscriptionId: state.id,
648
+ generation,
649
+ registrationId,
650
+ path: state.path,
651
+ input: state.input
652
+ })
653
+ });
654
+ if (!response.ok) {
655
+ throw new Error(
656
+ `Subscribe failed: ${response.status} ${response.statusText}`
657
+ );
658
+ }
659
+ const result = await response.json();
660
+ if (result.type === "error") {
661
+ throw new Error(result.message);
662
+ }
663
+ if (!isCurrent()) return;
664
+ this.handleSnapshot(result);
665
+ if (isCurrent()) state.hasRegistered = true;
666
+ } catch (cause) {
667
+ if (isCurrent()) {
668
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
669
+ state.error = failure;
670
+ state.loading = this.hasSettledLayers(state);
671
+ this.notifySubscription(state, false, failure);
672
+ }
673
+ throw cause;
674
+ } finally {
675
+ if ((!state.active || !isCurrent()) && requestIssued) {
676
+ void this.sendUnsubscribe(
677
+ registrationConnectionId,
678
+ state.id,
679
+ generation,
680
+ registrationId
681
+ );
682
+ }
683
+ if (state.registrationPromise === request) {
684
+ state.registrationPromise = void 0;
685
+ }
686
+ }
687
+ })();
688
+ state.registrationPromise = request;
689
+ return request;
690
+ }
691
+ releaseSubscription(state, leaseId) {
692
+ state.observers.delete(leaseId);
693
+ if (state.observers.size > 0) return;
694
+ state.active = false;
695
+ this.subscriptionsById.delete(state.id);
696
+ this.subscriptionsByKey.delete(state.key);
697
+ if (state.registrationId && state.registrationConnectionId && state.hasRegistered) {
698
+ void this.sendUnsubscribe(
699
+ state.registrationConnectionId,
700
+ state.id,
701
+ state.generation,
702
+ state.registrationId
703
+ );
704
+ }
705
+ }
706
+ async sendUnsubscribe(connectionId, subscriptionId, generation, registrationId) {
707
+ if (!connectionId) return;
708
+ try {
709
+ const headers = await this.buildHeaders();
710
+ await fetch(this.httpUrl, {
711
+ method: "POST",
712
+ headers,
713
+ body: JSON.stringify({
714
+ type: "unsubscribe",
715
+ connectionId,
716
+ subscriptionId,
717
+ generation,
718
+ registrationId
719
+ })
720
+ });
721
+ } catch {
722
+ }
723
+ }
724
+ async refetch(state) {
725
+ if (!state.active) return;
726
+ if (!state.registrationId) {
727
+ await this.ensureRegistered(state, this.wsManager.generation);
728
+ return;
729
+ }
730
+ await this.requestResync(state);
731
+ }
732
+ handleMessage(message) {
733
+ switch (message.type) {
734
+ case "patch":
735
+ this.handlePatch(message);
736
+ break;
737
+ case "resync": {
738
+ const state = this.subscriptionsById.get(message.subscriptionId);
739
+ if (state?.generation === message.generation) {
740
+ void this.requestResync(state).catch(() => void 0);
741
+ }
742
+ break;
743
+ }
744
+ }
745
+ }
746
+ handleSnapshot(message) {
747
+ const state = this.subscriptionsById.get(message.subscriptionId);
748
+ if (!state || message.generation !== state.generation) return;
749
+ if (state.version !== void 0 && message.version <= state.version) return;
750
+ state.authoritativeData = message.data;
751
+ state.version = message.version;
752
+ state.error = void 0;
753
+ const coveredRevision = state.registrationSettlementRevision;
754
+ for (const [layerId, layer] of state.optimisticLayers) {
755
+ if (layer.settledRevision !== void 0 && layer.settledRevision <= coveredRevision) {
756
+ state.optimisticLayers.delete(layerId);
757
+ }
758
+ }
759
+ const awaitingReconciliation = this.hasSettledLayers(state);
760
+ state.loading = awaitingReconciliation;
761
+ state.stale = awaitingReconciliation;
762
+ this.recomputeData(state);
763
+ this.discardStalePatches(state);
764
+ this.notifySubscription(state, true);
765
+ this.drainPatchBuffer(state);
766
+ }
767
+ handlePatch(message) {
768
+ const state = this.subscriptionsById.get(message.subscriptionId);
769
+ if (!state || message.generation !== state.generation) return;
770
+ if (state.appliedMessageIds.has(message.messageId) || state.patchBuffer.has(message.messageId)) {
771
+ return;
772
+ }
773
+ if (state.version !== void 0 && message.toVersion <= state.version) {
774
+ state.appliedMessageIds.add(message.messageId);
775
+ return;
776
+ }
777
+ state.patchBuffer.set(message.messageId, message);
778
+ if (state.version === void 0) return;
779
+ this.drainPatchBuffer(state);
780
+ }
781
+ drainPatchBuffer(state) {
782
+ if (state.version === void 0 || state.authoritativeData === void 0) {
783
+ return;
784
+ }
785
+ this.discardStalePatches(state);
786
+ while (true) {
787
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
788
+ if (!next) break;
789
+ state.patchBuffer.delete(next.messageId);
790
+ try {
791
+ state.authoritativeData = applyPatches(
792
+ state.authoritativeData,
793
+ next.patches
794
+ );
795
+ state.version = next.toVersion;
796
+ state.appliedMessageIds.add(next.messageId);
797
+ const awaitingReconciliation = this.hasSettledLayers(state);
798
+ state.loading = awaitingReconciliation;
799
+ state.stale = awaitingReconciliation;
800
+ state.error = void 0;
801
+ this.recomputeData(state);
802
+ this.notifySubscription(state, true);
803
+ this.discardStalePatches(state);
804
+ } catch {
805
+ void this.requestResync(state).catch(() => void 0);
806
+ return;
807
+ }
808
+ }
809
+ if ([...state.patchBuffer.values()].some(
810
+ (patch) => patch.toVersion > state.version && patch.fromVersion !== state.version
811
+ )) {
812
+ void this.requestResync(state).catch(() => void 0);
813
+ }
814
+ }
815
+ discardStalePatches(state) {
816
+ if (state.version === void 0) return;
817
+ for (const [messageId, patch] of state.patchBuffer) {
818
+ if (patch.toVersion <= state.version) {
819
+ state.patchBuffer.delete(messageId);
820
+ state.appliedMessageIds.add(messageId);
821
+ }
822
+ }
823
+ }
824
+ requestResync(state) {
825
+ if (state.resyncPromise && state.resyncGeneration === state.generation) {
826
+ return state.resyncPromise;
827
+ }
828
+ if (!state.active || !state.registrationId) return Promise.resolve();
829
+ state.stale = true;
830
+ state.loading = true;
831
+ this.notifySubscription(state);
832
+ let request;
833
+ request = this.ensureRegistered(
834
+ state,
835
+ this.wsManager.generation,
836
+ true
837
+ ).finally(() => {
838
+ if (state.resyncPromise === request) {
839
+ state.resyncPromise = void 0;
840
+ state.resyncGeneration = void 0;
841
+ }
842
+ }).then(() => {
843
+ if (state.active && this.hasSettledLayers(state)) {
844
+ return this.requestResync(state);
845
+ }
846
+ return void 0;
847
+ });
848
+ state.resyncPromise = request;
849
+ state.resyncGeneration = state.generation;
850
+ return request;
851
+ }
852
+ createMutationLayers(mutationId, input, options, layered) {
853
+ const targets = this.findMutationTargets(options);
854
+ for (const state of targets) {
855
+ const updater = this.createOptimisticUpdater(input, options);
856
+ if (!updater || state.authoritativeData === void 0) continue;
857
+ state.optimisticLayers.set(mutationId, {
858
+ updater
859
+ });
860
+ layered.push(state);
861
+ this.recomputeData(state);
862
+ this.notifySubscription(state, true);
863
+ }
864
+ }
865
+ findMutationTargets(options) {
866
+ if (options?.queryKey) {
867
+ const exact = this.subscriptionsByKey.get(options.queryKey);
868
+ return exact ? [exact] : [];
869
+ }
870
+ if (typeof options?.invalidates === "object") {
871
+ const exact = this.subscriptionsByKey.get(
872
+ getQueryKey(options.invalidates.path, options.invalidates.input)
873
+ );
874
+ return exact ? [exact] : [];
875
+ }
876
+ if (typeof options?.invalidates === "string") {
877
+ return [...this.subscriptionsByKey.values()].filter(
878
+ (state) => state.path === options.invalidates
879
+ );
880
+ }
881
+ return [];
882
+ }
883
+ createOptimisticUpdater(input, options, result, settled = false) {
884
+ const update = options?.optimisticUpdate;
885
+ if (typeof update === "function") {
886
+ return (data) => Array.isArray(data) ? update(data, input, result) : data;
887
+ }
888
+ const itemId = typeof input === "object" && input !== null ? input.id : void 0;
889
+ if (update === "remove" && itemId !== void 0) {
890
+ return (data) => Array.isArray(data) ? data.filter((item) => item.id !== itemId) : data;
891
+ }
892
+ if (update !== "merge") return void 0;
893
+ if (itemId !== void 0) {
894
+ const resultFields = settled && typeof result === "object" && result !== null ? result : {};
895
+ return (data) => {
896
+ if (!Array.isArray(data)) return data;
897
+ return data.map(
898
+ (item) => item.id === itemId ? { ...item, ...input, ...resultFields } : item
899
+ );
900
+ };
901
+ }
902
+ if (options?.optimisticData === void 0) return void 0;
903
+ return (data) => {
904
+ if (!Array.isArray(data)) return data;
905
+ 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;
906
+ if (settled && optimistic !== null && typeof optimistic === "object") {
907
+ const optimisticId = optimistic.id;
908
+ if (optimisticId !== void 0 && data.some(
909
+ (item) => typeof item === "object" && item !== null && item.id === optimisticId
910
+ )) {
911
+ return data;
912
+ }
913
+ }
914
+ return optimistic === void 0 ? data : [optimistic, ...data];
915
+ };
916
+ }
917
+ settleMutationLayers(mutationId, input, result, options, states) {
918
+ for (const state of states) {
919
+ const layer = state.optimisticLayers.get(mutationId);
920
+ const updater = this.createOptimisticUpdater(
921
+ input,
922
+ options,
923
+ result,
924
+ true
925
+ );
926
+ if (!layer || !updater) continue;
927
+ layer.updater = updater;
928
+ layer.settledRevision = ++state.settlementRevision;
929
+ state.loading = true;
930
+ state.stale = true;
931
+ this.recomputeData(state);
932
+ this.notifySubscription(state, true);
933
+ void this.requestResync(state).catch(() => void 0);
934
+ }
935
+ }
936
+ removeMutationLayers(mutationId, states) {
937
+ for (const state of states) {
938
+ if (!state.optimisticLayers.delete(mutationId)) continue;
939
+ this.recomputeData(state);
940
+ this.notifySubscription(state, true);
941
+ }
942
+ }
943
+ recomputeData(state) {
944
+ let data = state.authoritativeData;
945
+ for (const layer of state.optimisticLayers.values()) {
946
+ data = layer.updater(data);
947
+ }
948
+ state.data = data;
949
+ }
950
+ hasSettledLayers(state) {
951
+ for (const layer of state.optimisticLayers.values()) {
952
+ if (layer.settledRevision !== void 0) return true;
953
+ }
954
+ return false;
955
+ }
956
+ async buildHeaders() {
957
+ const headers = {
958
+ "Content-Type": "application/json"
959
+ };
960
+ if (this.headersGetter) Object.assign(headers, await this.headersGetter());
961
+ if (this.authGetter) {
962
+ headers["Authorization"] = `Bearer ${await this.authGetter()}`;
963
+ }
964
+ return headers;
965
+ }
966
+ markSubscriptionsStale() {
967
+ for (const state of this.subscriptionsByKey.values()) {
968
+ if (!state.active) continue;
969
+ state.stale = true;
970
+ state.loading = true;
971
+ this.notifySubscription(state);
972
+ }
973
+ }
974
+ notifySubscription(state, dataChanged = false, failure) {
975
+ if (!state.active) return;
976
+ state.revision++;
977
+ for (const listener of state.listeners) listener();
978
+ if (dataChanged) {
979
+ for (const observer of state.observers.values()) {
980
+ observer.onData?.(state.data);
981
+ }
982
+ }
983
+ if (failure) {
984
+ for (const observer of state.observers.values()) {
985
+ observer.onError?.(failure);
986
+ }
987
+ }
988
+ }
989
+ notifyStateListeners() {
990
+ for (const listener of this.stateListeners) {
991
+ listener(this.connectionState);
992
+ }
993
+ }
994
+ };
995
+ function createReactiveClient(config) {
996
+ const client = new ReactiveClient(config);
997
+ return createTypedProxy(client, []);
998
+ }
999
+ function createTypedProxy(client, path) {
1000
+ return new Proxy(
1001
+ {},
1002
+ {
1003
+ get(_target, property) {
1004
+ if (property === "connect") return () => client.connect();
1005
+ if (property === "disconnect") return () => client.disconnect();
1006
+ if (typeof property !== "string") return void 0;
1007
+ if (property === "useSubscription") {
1008
+ return (input, options) => client.subscribe(path.join("."), { ...options, input });
1009
+ }
1010
+ if (property === "mutate") {
1011
+ return (input, options) => client.call(path.join("."), input, options);
1012
+ }
1013
+ return createTypedProxy(client, [...path, property]);
1014
+ }
1015
+ }
1016
+ );
1017
+ }
1018
+
1019
+ exports.ReactiveClient = ReactiveClient;
1020
+ exports.createReactiveClient = createReactiveClient;
1021
+ exports.getQueryKey = getQueryKey;
1022
+ //# sourceMappingURL=client.cjs.map
1023
+ //# sourceMappingURL=client.cjs.map