dynamodb-reactive 0.1.22 → 0.2.2

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 +437 -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 +634 -0
  22. package/dist/infra.cjs.map +1 -0
  23. package/dist/infra.d.cts +126 -0
  24. package/dist/infra.d.ts +57 -102
  25. package/dist/infra.js +385 -147
  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 +2382 -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 +1882 -1204
  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-2KC44Q5I.js +0 -1103
  43. package/dist/chunk-2KC44Q5I.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-D_NLHuuO.d.ts +0 -556
@@ -1,1103 +0,0 @@
1
- import jsonpatch from 'fast-json-patch';
2
- import { error, debug, warn } from 'ag-common/dist/common/helpers/log.js';
3
- import { createContext, useState, useEffect, useContext, useSyncExternalStore, useRef, useCallback } from 'react';
4
- import { jsx } from 'react/jsx-runtime';
5
-
6
- // ../client/src/patcher.ts
7
- var { applyPatch } = jsonpatch;
8
- function applyPatches(document2, patches) {
9
- if (patches.length === 0) {
10
- return document2;
11
- }
12
- const operations = patches.map((patch) => {
13
- const op = {
14
- op: patch.op,
15
- path: patch.path
16
- };
17
- if ("value" in patch && patch.value !== void 0) {
18
- op.value = patch.value;
19
- }
20
- if ("from" in patch && patch.from !== void 0) {
21
- op.from = patch.from;
22
- }
23
- return op;
24
- });
25
- try {
26
- const result = applyPatch(
27
- structuredClone(document2),
28
- operations,
29
- true,
30
- // Validate operations
31
- false
32
- // Don't mutate the original
33
- );
34
- return result.newDocument;
35
- } catch (error2) {
36
- throw new Error("Failed to apply patches", { cause: error2 });
37
- }
38
- }
39
- var WebSocketManager = class {
40
- config;
41
- ws = null;
42
- state = "disconnected";
43
- reconnectAttempts = 0;
44
- reconnectTimer = null;
45
- messageQueue = [];
46
- messageHandlers = /* @__PURE__ */ new Set();
47
- stateHandlers = /* @__PURE__ */ new Set();
48
- _connectionId = null;
49
- connectionIdResolvers = [];
50
- /** Flag to indicate intentional disconnect (skip auto-reconnect) */
51
- intentionalDisconnect = false;
52
- constructor(config) {
53
- this.config = {
54
- autoReconnect: true,
55
- reconnectDelay: 1e3,
56
- maxReconnectAttempts: 10,
57
- ...config
58
- };
59
- }
60
- /**
61
- * Get the current connection ID (null if not connected)
62
- */
63
- get connectionId() {
64
- return this._connectionId;
65
- }
66
- /**
67
- * Wait for the connection ID to be available
68
- */
69
- waitForConnectionId() {
70
- if (this._connectionId) {
71
- return Promise.resolve(this._connectionId);
72
- }
73
- return new Promise((resolve) => {
74
- this.connectionIdResolvers.push(resolve);
75
- });
76
- }
77
- /**
78
- * Get current connection state
79
- */
80
- getState() {
81
- return this.state;
82
- }
83
- /**
84
- * Connect to the WebSocket server
85
- */
86
- async connect() {
87
- if (this.state === "connected" || this.state === "connecting") {
88
- return;
89
- }
90
- this.setState("connecting");
91
- try {
92
- const url = await this.buildUrl();
93
- this.ws = new WebSocket(url);
94
- this.ws.onopen = () => {
95
- this.reconnectAttempts = 0;
96
- this.ws?.send(JSON.stringify({ type: "init" }));
97
- };
98
- this.ws.onclose = () => {
99
- this.ws = null;
100
- this._connectionId = null;
101
- this.setState("disconnected");
102
- this.config.onDisconnect?.();
103
- if (this.config.autoReconnect && !this.intentionalDisconnect) {
104
- this.scheduleReconnect();
105
- }
106
- this.intentionalDisconnect = false;
107
- };
108
- this.ws.onerror = () => {
109
- const error2 = new Error("WebSocket error");
110
- this.config.onError?.(error2);
111
- };
112
- this.ws.onmessage = (event) => {
113
- try {
114
- const message = JSON.parse(event.data);
115
- if (message.type === "connected") {
116
- this._connectionId = message.connectionId;
117
- this.ws?.send(JSON.stringify({ type: "ping" }));
118
- return;
119
- }
120
- if (message.type === "pong") {
121
- this.setState("connected");
122
- this.flushMessageQueue();
123
- this.config.onConnect?.();
124
- for (const resolve of this.connectionIdResolvers) {
125
- resolve(this._connectionId);
126
- }
127
- this.connectionIdResolvers = [];
128
- return;
129
- }
130
- this.handleMessage(message);
131
- } catch (e) {
132
- error("Failed to parse WebSocket message:", e);
133
- }
134
- };
135
- } catch (e) {
136
- this.setState("disconnected");
137
- this.config.onError?.(e instanceof Error ? e : new Error(String(e)));
138
- if (this.config.autoReconnect) {
139
- this.scheduleReconnect();
140
- }
141
- }
142
- }
143
- /**
144
- * Disconnect from the WebSocket server
145
- */
146
- disconnect() {
147
- this.intentionalDisconnect = true;
148
- if (this.reconnectTimer) {
149
- clearTimeout(this.reconnectTimer);
150
- this.reconnectTimer = null;
151
- }
152
- if (this.ws) {
153
- this.ws.close(1e3, "Client disconnect");
154
- this.ws = null;
155
- }
156
- this._connectionId = null;
157
- this.setState("disconnected");
158
- }
159
- /**
160
- * Force reconnect - closes existing connection and reconnects
161
- * Useful for recovering from stale connections
162
- */
163
- forceReconnect() {
164
- if (this.reconnectTimer) {
165
- clearTimeout(this.reconnectTimer);
166
- this.reconnectTimer = null;
167
- }
168
- if (this.ws) {
169
- this.ws.close(1e3, "Force reconnect");
170
- this.ws = null;
171
- }
172
- this._connectionId = null;
173
- this.reconnectAttempts = 0;
174
- this.setState("disconnected");
175
- void this.connect();
176
- }
177
- /**
178
- * Send a message to the server
179
- */
180
- send(message) {
181
- if (this.state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
182
- this.ws.send(JSON.stringify(message));
183
- } else {
184
- this.messageQueue.push(message);
185
- }
186
- }
187
- /**
188
- * Subscribe to incoming messages
189
- */
190
- onMessage(handler) {
191
- this.messageHandlers.add(handler);
192
- return () => this.messageHandlers.delete(handler);
193
- }
194
- /**
195
- * Subscribe to connection state changes
196
- */
197
- onStateChange(handler) {
198
- this.stateHandlers.add(handler);
199
- return () => this.stateHandlers.delete(handler);
200
- }
201
- async buildUrl() {
202
- let url = this.config.url;
203
- if (this.config.auth) {
204
- const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
205
- const separator = url.includes("?") ? "&" : "?";
206
- url = `${url}${separator}token=${encodeURIComponent(token)}`;
207
- }
208
- return url;
209
- }
210
- setState(state) {
211
- this.state = state;
212
- for (const handler of this.stateHandlers) {
213
- handler(state);
214
- }
215
- }
216
- handleMessage(message) {
217
- for (const handler of this.messageHandlers) {
218
- handler(message);
219
- }
220
- }
221
- scheduleReconnect() {
222
- if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
223
- error("Max reconnection attempts reached");
224
- return;
225
- }
226
- this.setState("reconnecting");
227
- this.reconnectAttempts++;
228
- const delay = Math.min(
229
- this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
230
- 3e4
231
- // Max 30 seconds
232
- ) * (0.5 + Math.random() * 0.5);
233
- this.reconnectTimer = setTimeout(() => {
234
- this.reconnectTimer = null;
235
- void this.connect();
236
- }, delay);
237
- }
238
- flushMessageQueue() {
239
- const queue = this.messageQueue;
240
- this.messageQueue = [];
241
- for (const message of queue) {
242
- this.send(message);
243
- }
244
- }
245
- };
246
- var DEFAULT_HTTP_URL = "/api/reactive";
247
- function generateId() {
248
- return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
249
- }
250
- var ReactiveClient = class {
251
- wsManager;
252
- httpUrl;
253
- authGetter;
254
- headersGetter;
255
- subscriptions = /* @__PURE__ */ new Map();
256
- pendingCalls = /* @__PURE__ */ new Map();
257
- connectionState = "disconnected";
258
- stateListeners = /* @__PURE__ */ new Set();
259
- // Stores original data for rollback when optimistic updates fail
260
- optimisticUpdates = /* @__PURE__ */ new Map();
261
- constructor(config) {
262
- this.wsManager = new WebSocketManager(config);
263
- this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
264
- if (typeof config.auth === "function") {
265
- this.authGetter = config.auth;
266
- } else if (config.auth) {
267
- this.authGetter = () => config.auth;
268
- }
269
- if (typeof config.headers === "function") {
270
- this.headersGetter = config.headers;
271
- } else if (config.headers) {
272
- this.headersGetter = () => config.headers;
273
- }
274
- this.wsManager.onMessage((message) => this.handleMessage(message));
275
- this.wsManager.onStateChange((state) => {
276
- this.connectionState = state;
277
- this.notifyStateListeners();
278
- if (state === "connected") {
279
- this.resubscribeAll();
280
- }
281
- });
282
- }
283
- /**
284
- * Connect to the server
285
- */
286
- async connect() {
287
- await this.wsManager.connect();
288
- }
289
- /**
290
- * Disconnect from the server
291
- */
292
- disconnect() {
293
- this.wsManager.disconnect();
294
- }
295
- /**
296
- * Force reconnect - closes existing connection and reconnects
297
- * Useful for recovering from stale connections
298
- */
299
- forceReconnect() {
300
- this.wsManager.forceReconnect();
301
- }
302
- /**
303
- * Update the headers getter for HTTP requests.
304
- * Useful for dynamically setting auth or user context headers.
305
- */
306
- setHeaders(headers) {
307
- if (typeof headers === "function") {
308
- this.headersGetter = headers;
309
- } else {
310
- this.headersGetter = () => headers;
311
- }
312
- }
313
- /**
314
- * Get current connection state
315
- */
316
- getConnectionState() {
317
- return this.connectionState;
318
- }
319
- /**
320
- * Subscribe to connection state changes
321
- */
322
- onConnectionStateChange(listener) {
323
- this.stateListeners.add(listener);
324
- return () => this.stateListeners.delete(listener);
325
- }
326
- /**
327
- * Subscribe to a procedure via HTTP.
328
- * Snapshot is returned via HTTP, patches come via WebSocket.
329
- */
330
- subscribe(path, options = {}) {
331
- const id = generateId();
332
- const state = {
333
- id,
334
- path,
335
- input: options.input,
336
- data: void 0,
337
- loading: true,
338
- error: void 0,
339
- listeners: /* @__PURE__ */ new Set(),
340
- options
341
- };
342
- this.subscriptions.set(id, state);
343
- this.sendSubscribeRequest(id, path, options.input).catch((error2) => {
344
- state.error = error2 instanceof Error ? error2 : new Error(String(error2));
345
- state.loading = false;
346
- this.notifySubscriptionListeners(id);
347
- options.onError?.(state.error);
348
- });
349
- const subscription = {
350
- id,
351
- get data() {
352
- return state.data;
353
- },
354
- get loading() {
355
- return state.loading;
356
- },
357
- get error() {
358
- return state.error;
359
- },
360
- unsubscribe: () => this.unsubscribe(id),
361
- refetch: () => this.refetch(id)
362
- };
363
- return subscription;
364
- }
365
- /**
366
- * Send subscribe request via HTTP
367
- */
368
- async sendSubscribeRequest(subscriptionId, path, input) {
369
- const connectionId = await this.wsManager.waitForConnectionId();
370
- const headers = {
371
- "Content-Type": "application/json"
372
- };
373
- if (this.headersGetter) {
374
- const customHeaders = await this.headersGetter();
375
- Object.assign(headers, customHeaders);
376
- }
377
- if (this.authGetter) {
378
- const token = await this.authGetter();
379
- headers["Authorization"] = `Bearer ${token}`;
380
- }
381
- const response = await fetch(this.httpUrl, {
382
- method: "POST",
383
- headers,
384
- body: JSON.stringify({
385
- type: "subscribe",
386
- connectionId,
387
- subscriptionId,
388
- path,
389
- input
390
- })
391
- });
392
- if (!response.ok) {
393
- throw new Error(
394
- `Subscribe failed: ${response.status} ${response.statusText}`
395
- );
396
- }
397
- const result = await response.json();
398
- if (result.type === "error") {
399
- throw new Error(result.message ?? "Unknown error");
400
- }
401
- if (result.type === "snapshot") {
402
- this.handleSnapshot(subscriptionId, result.data);
403
- }
404
- }
405
- /**
406
- * Call a mutation procedure via HTTP with optimistic update support.
407
- * All optimistic updates are applied INSTANTLY (before HTTP request).
408
- */
409
- async call(path, input, options) {
410
- const invalidatesPath = options?.invalidates;
411
- const updateType = options?.optimisticUpdate;
412
- let targetSubscriptionId;
413
- if (invalidatesPath) {
414
- targetSubscriptionId = this.findSubscriptionByPath(invalidatesPath);
415
- debug(
416
- "[optimistic] Looking for subscription:",
417
- invalidatesPath,
418
- "-> found:",
419
- targetSubscriptionId ?? "NONE"
420
- );
421
- }
422
- if (targetSubscriptionId) {
423
- debug(
424
- "[optimistic] Applying update type:",
425
- updateType,
426
- "to subscription:",
427
- targetSubscriptionId
428
- );
429
- const state = this.subscriptions.get(targetSubscriptionId);
430
- const currentData = state?.data;
431
- if (updateType === "remove") {
432
- const inputId = input.id;
433
- if (inputId) {
434
- this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
435
- if (!Array.isArray(data)) return data;
436
- return data.filter((item) => item.id !== inputId);
437
- });
438
- }
439
- } else if (updateType === "merge") {
440
- const inputWithId = input;
441
- if (inputWithId.id) {
442
- this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
443
- if (!Array.isArray(data)) return data;
444
- const idx = data.findIndex(
445
- (item) => item.id === inputWithId.id
446
- );
447
- if (idx >= 0) {
448
- return [
449
- ...data.slice(0, idx),
450
- { ...data[idx], ...input },
451
- ...data.slice(idx + 1)
452
- ];
453
- }
454
- return data;
455
- });
456
- } else if (options?.optimisticData) {
457
- const optimistic = typeof options.optimisticData === "function" ? options.optimisticData(input, Array.isArray(currentData) ? currentData : []) : options.optimisticData;
458
- if (optimistic) {
459
- this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
460
- if (!Array.isArray(data)) return data;
461
- return [optimistic, ...data];
462
- });
463
- }
464
- }
465
- } else if (typeof updateType === "function") {
466
- this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
467
- if (!Array.isArray(data)) return data;
468
- return updateType(data, input, void 0);
469
- });
470
- }
471
- }
472
- const connectionId = await this.wsManager.waitForConnectionId();
473
- const headers = {
474
- "Content-Type": "application/json"
475
- };
476
- if (this.headersGetter) {
477
- const customHeaders = await this.headersGetter();
478
- Object.assign(headers, customHeaders);
479
- }
480
- if (this.authGetter) {
481
- const token = await this.authGetter();
482
- headers["Authorization"] = `Bearer ${token}`;
483
- }
484
- try {
485
- const response = await fetch(this.httpUrl, {
486
- method: "POST",
487
- headers,
488
- body: JSON.stringify({
489
- type: "call",
490
- connectionId,
491
- path,
492
- input
493
- })
494
- });
495
- if (!response.ok) {
496
- throw new Error(
497
- `Call failed: ${response.status} ${response.statusText}`
498
- );
499
- }
500
- const result = await response.json();
501
- if (result.type === "error") {
502
- throw new Error(result.message ?? "Unknown error");
503
- }
504
- const data = result.data;
505
- if (targetSubscriptionId && updateType === "merge") {
506
- const resultWithId = data;
507
- if (resultWithId.id) {
508
- this.applyOptimisticUpdate(targetSubscriptionId, (currentData) => {
509
- if (!Array.isArray(currentData)) return currentData;
510
- const idx = currentData.findIndex(
511
- (item) => item.id === resultWithId.id
512
- );
513
- if (idx >= 0) {
514
- return [
515
- ...currentData.slice(0, idx),
516
- data,
517
- ...currentData.slice(idx + 1)
518
- ];
519
- } else {
520
- return [
521
- data,
522
- ...currentData.filter(
523
- (item) => item.id && !item.id.startsWith("temp-")
524
- )
525
- ];
526
- }
527
- });
528
- }
529
- }
530
- return data;
531
- } catch (error2) {
532
- if (targetSubscriptionId) {
533
- this.rollbackOptimisticUpdate(targetSubscriptionId);
534
- }
535
- throw error2;
536
- }
537
- }
538
- /**
539
- * Unsubscribe from a subscription
540
- */
541
- unsubscribe(id) {
542
- const state = this.subscriptions.get(id);
543
- if (!state) return;
544
- this.subscriptions.delete(id);
545
- this.wsManager.send({
546
- type: "unsubscribe",
547
- subscriptionId: id
548
- });
549
- }
550
- /**
551
- * Refetch a subscription via HTTP
552
- */
553
- async refetch(id) {
554
- const state = this.subscriptions.get(id);
555
- if (!state) return;
556
- state.loading = true;
557
- this.notifySubscriptionListeners(id);
558
- try {
559
- await this.sendSubscribeRequest(id, state.path, state.input);
560
- } catch (error2) {
561
- state.error = error2 instanceof Error ? error2 : new Error(String(error2));
562
- state.loading = false;
563
- this.notifySubscriptionListeners(id);
564
- state.options.onError?.(state.error);
565
- }
566
- }
567
- /**
568
- * Handle incoming server messages.
569
- * Note: 'connected' messages are handled by WebSocketManager before reaching here.
570
- */
571
- handleMessage(message) {
572
- switch (message.type) {
573
- case "connected":
574
- break;
575
- case "pong":
576
- break;
577
- case "snapshot":
578
- this.handleSnapshot(message.subscriptionId, message.data);
579
- break;
580
- case "patch":
581
- this.handlePatch(message.subscriptionId, message.patches);
582
- break;
583
- case "result":
584
- this.handleResult(message.callId, message.data);
585
- break;
586
- case "error":
587
- this.handleError(message);
588
- break;
589
- }
590
- }
591
- /**
592
- * Handle a snapshot message
593
- */
594
- handleSnapshot(subscriptionId, data) {
595
- const state = this.subscriptions.get(subscriptionId);
596
- if (!state) return;
597
- this.clearOptimisticUpdate(subscriptionId);
598
- state.data = data;
599
- state.loading = false;
600
- state.error = void 0;
601
- this.notifySubscriptionListeners(subscriptionId);
602
- state.options.onData?.(data);
603
- }
604
- /**
605
- * Handle a patch message
606
- */
607
- handlePatch(subscriptionId, patches) {
608
- const state = this.subscriptions.get(subscriptionId);
609
- if (!state || state.data === void 0) return;
610
- const originalData = this.optimisticUpdates.get(subscriptionId);
611
- const baseData = originalData !== void 0 ? originalData : state.data;
612
- this.clearOptimisticUpdate(subscriptionId);
613
- try {
614
- state.data = applyPatches(baseData, patches);
615
- this.notifySubscriptionListeners(subscriptionId);
616
- state.options.onData?.(state.data);
617
- } catch (error2) {
618
- state.error = error2 instanceof Error ? error2 : new Error(String(error2));
619
- this.notifySubscriptionListeners(subscriptionId);
620
- state.options.onError?.(state.error);
621
- }
622
- }
623
- /**
624
- * Handle a result message
625
- */
626
- handleResult(callId, data) {
627
- const pending = this.pendingCalls.get(callId);
628
- if (!pending) return;
629
- this.pendingCalls.delete(callId);
630
- pending.resolve(data);
631
- }
632
- /**
633
- * Handle an error message
634
- */
635
- handleError(message) {
636
- const error2 = new Error(message.message);
637
- if (message.subscriptionId) {
638
- const state = this.subscriptions.get(message.subscriptionId);
639
- if (state) {
640
- state.error = error2;
641
- state.loading = false;
642
- this.notifySubscriptionListeners(message.subscriptionId);
643
- state.options.onError?.(error2);
644
- }
645
- }
646
- if (message.callId) {
647
- const pending = this.pendingCalls.get(message.callId);
648
- if (pending) {
649
- this.pendingCalls.delete(message.callId);
650
- pending.reject(error2);
651
- }
652
- }
653
- }
654
- /**
655
- * Resubscribe to all subscriptions after reconnect via HTTP
656
- */
657
- resubscribeAll() {
658
- for (const [id, state] of this.subscriptions) {
659
- if (state.options.resubscribeOnReconnect !== false) {
660
- state.loading = true;
661
- this.notifySubscriptionListeners(id);
662
- this.sendSubscribeRequest(id, state.path, state.input).catch(
663
- (error2) => {
664
- state.error = error2 instanceof Error ? error2 : new Error(String(error2));
665
- state.loading = false;
666
- this.notifySubscriptionListeners(id);
667
- state.options.onError?.(state.error);
668
- }
669
- );
670
- }
671
- }
672
- }
673
- /**
674
- * Notify subscription listeners of state changes
675
- */
676
- notifySubscriptionListeners(id) {
677
- const state = this.subscriptions.get(id);
678
- if (!state) return;
679
- for (const listener of state.listeners) {
680
- listener();
681
- }
682
- }
683
- /**
684
- * Notify connection state listeners
685
- */
686
- notifyStateListeners() {
687
- for (const listener of this.stateListeners) {
688
- listener(this.connectionState);
689
- }
690
- }
691
- /**
692
- * Add a listener for subscription state changes
693
- * Used internally by React hooks
694
- */
695
- addSubscriptionListener(id, listener) {
696
- const state = this.subscriptions.get(id);
697
- if (!state) return () => {
698
- };
699
- state.listeners.add(listener);
700
- return () => state.listeners.delete(listener);
701
- }
702
- /**
703
- * Get subscription state
704
- * Used internally by React hooks
705
- */
706
- getSubscriptionState(id) {
707
- return this.subscriptions.get(id);
708
- }
709
- /**
710
- * Find subscription by path
711
- */
712
- findSubscriptionByPath(path) {
713
- debug(
714
- "[optimistic] Searching for path:",
715
- path,
716
- "in subscriptions:",
717
- Array.from(this.subscriptions.entries()).map(([id, s]) => ({
718
- id,
719
- path: s.path
720
- }))
721
- );
722
- for (const [id, state] of this.subscriptions) {
723
- if (state.path === path) {
724
- return id;
725
- }
726
- }
727
- return void 0;
728
- }
729
- /**
730
- * Apply an optimistic update to a subscription's data.
731
- * Stores the original data for rollback (on mutation error) or as base for
732
- * incoming server patches. Safe to call after external API calls — the next
733
- * server snapshot/patch will reconcile automatically.
734
- */
735
- applyOptimisticUpdate(subscriptionId, updater) {
736
- const state = this.subscriptions.get(subscriptionId);
737
- if (!state) {
738
- debug(
739
- "[optimistic] applyOptimisticUpdate: subscription not found:",
740
- subscriptionId
741
- );
742
- return;
743
- }
744
- if (state.data === void 0) {
745
- debug(
746
- "[optimistic] applyOptimisticUpdate: subscription data is undefined (still loading?):",
747
- subscriptionId
748
- );
749
- return;
750
- }
751
- debug(
752
- "[optimistic] applyOptimisticUpdate: applying update, current data length:",
753
- Array.isArray(state.data) ? state.data.length : "not array"
754
- );
755
- if (!this.optimisticUpdates.has(subscriptionId)) {
756
- this.optimisticUpdates.set(
757
- subscriptionId,
758
- JSON.parse(JSON.stringify(state.data))
759
- );
760
- }
761
- state.data = updater(state.data);
762
- this.notifySubscriptionListeners(subscriptionId);
763
- }
764
- /**
765
- * Rollback an optimistic update
766
- */
767
- rollbackOptimisticUpdate(subscriptionId) {
768
- const original = this.optimisticUpdates.get(subscriptionId);
769
- if (original === void 0) return;
770
- const state = this.subscriptions.get(subscriptionId);
771
- if (state) {
772
- state.data = original;
773
- this.notifySubscriptionListeners(subscriptionId);
774
- }
775
- this.optimisticUpdates.delete(subscriptionId);
776
- }
777
- /**
778
- * Clear optimistic state (called when real data arrives)
779
- */
780
- clearOptimisticUpdate(subscriptionId) {
781
- this.optimisticUpdates.delete(subscriptionId);
782
- }
783
- };
784
- function createReactiveClient(config) {
785
- const client = new ReactiveClient(config);
786
- return createTypedProxy(client, []);
787
- }
788
- function createTypedProxy(client, path) {
789
- return new Proxy(
790
- {},
791
- {
792
- get(_target, prop) {
793
- if (prop === "_client") return client;
794
- if (prop === "connect") return () => client.connect();
795
- if (prop === "disconnect") return () => client.disconnect();
796
- if (typeof prop !== "string") return void 0;
797
- const newPath = [...path, prop];
798
- if (prop === "useSubscription") {
799
- return (input, options) => {
800
- const parentPath = path.join(".");
801
- return client.subscribe(parentPath, { ...options, input });
802
- };
803
- }
804
- if (prop === "mutate") {
805
- return async (input) => {
806
- const parentPath = path.join(".");
807
- const invalidatesPath = path.length >= 2 ? `${path.slice(0, -1).join(".")}.list` : void 0;
808
- const updateType = parentPath.endsWith(".delete") ? "remove" : "merge";
809
- return client.call(parentPath, input, {
810
- invalidates: invalidatesPath,
811
- optimisticUpdate: updateType
812
- });
813
- };
814
- }
815
- return createTypedProxy(client, newPath);
816
- }
817
- }
818
- );
819
- }
820
- var ReactiveClientContext = createContext(null);
821
- function ReactiveClientProvider({
822
- children,
823
- config,
824
- client: externalClient
825
- }) {
826
- const url = config?.url;
827
- const hasUrl = Boolean(url);
828
- const [client, setClient] = useState(
829
- externalClient ?? null
830
- );
831
- useEffect(() => {
832
- if (externalClient || !url) return;
833
- if (!client) {
834
- const newClient = new ReactiveClient({ ...config, url });
835
- setClient(newClient);
836
- }
837
- }, [url, externalClient]);
838
- useEffect(() => {
839
- if (!client) return;
840
- void client.connect();
841
- return () => {
842
- client.disconnect();
843
- };
844
- }, [client]);
845
- useEffect(() => {
846
- if (typeof window === "undefined" || typeof document === "undefined") {
847
- return;
848
- }
849
- if (!client) return;
850
- const attemptReconnect = () => {
851
- const state = client.getConnectionState();
852
- if (state === "connected") {
853
- return;
854
- }
855
- if (state === "connecting") {
856
- return;
857
- }
858
- if (state === "reconnecting") {
859
- debug(
860
- "[dynamodb-reactive] Tab became active during reconnect backoff, forcing reconnect..."
861
- );
862
- client.forceReconnect();
863
- } else {
864
- debug(
865
- "[dynamodb-reactive] Tab became active while disconnected, reconnecting..."
866
- );
867
- void client.connect();
868
- }
869
- };
870
- const handleVisibilityChange = () => {
871
- if (document.visibilityState === "visible") {
872
- attemptReconnect();
873
- }
874
- };
875
- const handleFocus = () => {
876
- attemptReconnect();
877
- };
878
- document.addEventListener("visibilitychange", handleVisibilityChange);
879
- window.addEventListener("focus", handleFocus);
880
- return () => {
881
- document.removeEventListener("visibilitychange", handleVisibilityChange);
882
- window.removeEventListener("focus", handleFocus);
883
- };
884
- }, [client]);
885
- useEffect(() => {
886
- if (!hasUrl && process.env.NODE_ENV === "development") {
887
- warn(
888
- "[dynamodb-reactive] WebSocket URL not configured, real-time features disabled"
889
- );
890
- }
891
- }, [hasUrl]);
892
- return /* @__PURE__ */ jsx(ReactiveClientContext.Provider, { value: client, children });
893
- }
894
- function useReactiveClient() {
895
- return useContext(ReactiveClientContext);
896
- }
897
- function useReactiveClientOrThrow() {
898
- const client = useContext(ReactiveClientContext);
899
- if (!client) {
900
- throw new Error(
901
- "useReactiveClientOrThrow must be used within a ReactiveClientProvider with a configured URL"
902
- );
903
- }
904
- return client;
905
- }
906
- function useConnectionState() {
907
- const client = useReactiveClient();
908
- return useSyncExternalStore(
909
- (callback) => {
910
- if (!client) return () => {
911
- };
912
- return client.onConnectionStateChange(callback);
913
- },
914
- () => client ? client.getConnectionState() : "disabled",
915
- () => "disabled"
916
- );
917
- }
918
- function useSubscription(path, options = {}) {
919
- const client = useReactiveClient();
920
- const subscriptionRef = useRef(null);
921
- const [state, setState] = useState({
922
- data: void 0,
923
- loading: !client ? false : true,
924
- error: void 0
925
- });
926
- const inputKey = JSON.stringify(options.input);
927
- const onDataRef = useRef(options.onData);
928
- const onErrorRef = useRef(options.onError);
929
- onDataRef.current = options.onData;
930
- onErrorRef.current = options.onError;
931
- useEffect(() => {
932
- if (!client) return;
933
- const input = inputKey ? JSON.parse(inputKey) : void 0;
934
- const subscription = client.subscribe(path, {
935
- input,
936
- onData: (data) => {
937
- setState((prev) => ({
938
- ...prev,
939
- data,
940
- loading: false,
941
- error: void 0
942
- }));
943
- onDataRef.current?.(data);
944
- },
945
- onError: (error2) => {
946
- setState((prev) => ({
947
- ...prev,
948
- error: error2,
949
- loading: false
950
- }));
951
- onErrorRef.current?.(error2);
952
- }
953
- });
954
- subscriptionRef.current = subscription;
955
- const unregisterListener = client.addSubscriptionListener(
956
- subscription.id,
957
- () => {
958
- debug(
959
- "[optimistic] useSubscription listener fired for:",
960
- path,
961
- "data length:",
962
- Array.isArray(subscription.data) ? subscription.data.length : "not array"
963
- );
964
- setState({
965
- data: subscription.data,
966
- loading: subscription.loading,
967
- error: subscription.error
968
- });
969
- }
970
- );
971
- setState({
972
- data: subscription.data,
973
- loading: subscription.loading,
974
- error: subscription.error
975
- });
976
- return () => {
977
- unregisterListener();
978
- subscription.unsubscribe();
979
- subscriptionRef.current = null;
980
- };
981
- }, [client, path, inputKey]);
982
- const refetch = async () => {
983
- if (subscriptionRef.current) {
984
- setState((prev) => ({ ...prev, loading: true }));
985
- await subscriptionRef.current.refetch();
986
- }
987
- };
988
- const setData = useCallback(
989
- (updater) => {
990
- if (client && subscriptionRef.current) {
991
- client.applyOptimisticUpdate(
992
- subscriptionRef.current.id,
993
- updater
994
- );
995
- }
996
- },
997
- [client]
998
- );
999
- return {
1000
- ...state,
1001
- disabled: !client,
1002
- refetch,
1003
- setData
1004
- };
1005
- }
1006
- function useMutation(path, options) {
1007
- const client = useReactiveClient();
1008
- const [state, setState] = useState({
1009
- data: void 0,
1010
- loading: false,
1011
- error: void 0
1012
- });
1013
- const autoInvalidates = options?.invalidates ?? (() => {
1014
- const parts = path.split(".");
1015
- if (parts.length >= 2) {
1016
- return `${parts.slice(0, -1).join(".")}.list`;
1017
- }
1018
- return void 0;
1019
- })();
1020
- const autoUpdateType = options?.optimisticUpdate ?? (path.endsWith(".delete") ? "remove" : "merge");
1021
- const mutate = async (input) => {
1022
- if (!client) {
1023
- throw new Error("WebSocket not configured - mutations are disabled");
1024
- }
1025
- debug("[optimistic] useMutation.mutate called:", {
1026
- path,
1027
- autoInvalidates,
1028
- autoUpdateType,
1029
- input
1030
- });
1031
- setState((prev) => ({ ...prev, loading: true, error: void 0 }));
1032
- try {
1033
- const result = await client.call(path, input, {
1034
- invalidates: autoInvalidates,
1035
- optimisticUpdate: autoUpdateType
1036
- });
1037
- setState({ data: result, loading: false, error: void 0 });
1038
- return result;
1039
- } catch (error2) {
1040
- const err = error2 instanceof Error ? error2 : new Error(String(error2));
1041
- setState((prev) => ({ ...prev, loading: false, error: err }));
1042
- throw err;
1043
- }
1044
- };
1045
- const reset = () => {
1046
- setState({ data: void 0, loading: false, error: void 0 });
1047
- };
1048
- return {
1049
- mutate,
1050
- ...state,
1051
- disabled: !client,
1052
- reset
1053
- };
1054
- }
1055
- function useClient() {
1056
- const client = useReactiveClient();
1057
- function buildProxy(path) {
1058
- return new Proxy(
1059
- {},
1060
- {
1061
- get(_target, prop) {
1062
- if (prop === "useQuery") {
1063
- return function useQueryHook(input, options) {
1064
- const pathStr = path.join(".");
1065
- return useSubscription(pathStr, { ...options, input });
1066
- };
1067
- }
1068
- if (prop === "useMutation") {
1069
- return function useMutationHook(options) {
1070
- const pathStr = path.join(".");
1071
- return useMutation(pathStr, options);
1072
- };
1073
- }
1074
- if (prop === "setHeaders") {
1075
- return (headers) => {
1076
- client?.setHeaders(headers);
1077
- };
1078
- }
1079
- return buildProxy([...path, prop]);
1080
- }
1081
- }
1082
- );
1083
- }
1084
- return buildProxy([]);
1085
- }
1086
- function createReactiveHooks() {
1087
- return {
1088
- useSubscription: (path, options) => useSubscription(path, options),
1089
- useMutation: (path, options) => useMutation(path, options),
1090
- useConnectionState,
1091
- useReactiveClient
1092
- };
1093
- }
1094
- function createProcedureHooks(path) {
1095
- return {
1096
- useSubscription: (input, options) => useSubscription(path, { ...options, input }),
1097
- useMutation: (options) => useMutation(path, options)
1098
- };
1099
- }
1100
-
1101
- export { ReactiveClient, ReactiveClientProvider, WebSocketManager, applyPatches, createProcedureHooks, createReactiveClient, createReactiveHooks, useClient, useConnectionState, useMutation, useReactiveClient, useReactiveClientOrThrow, useSubscription };
1102
- //# sourceMappingURL=chunk-2KC44Q5I.js.map
1103
- //# sourceMappingURL=chunk-2KC44Q5I.js.map