dynamodb-reactive 0.1.0 → 0.1.3

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,658 @@
1
+ import jsonpatch from 'fast-json-patch';
2
+ import { createContext, useState, useEffect, useContext, useSyncExternalStore, useRef } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // ../client/src/patcher.ts
6
+ var { applyPatch } = jsonpatch;
7
+ function applyPatches(document, patches) {
8
+ if (patches.length === 0) {
9
+ return document;
10
+ }
11
+ const operations = patches.map((patch) => {
12
+ const op = {
13
+ op: patch.op,
14
+ path: patch.path
15
+ };
16
+ if ("value" in patch && patch.value !== void 0) {
17
+ op.value = patch.value;
18
+ }
19
+ if ("from" in patch && patch.from !== void 0) {
20
+ op.from = patch.from;
21
+ }
22
+ return op;
23
+ });
24
+ try {
25
+ const result = applyPatch(
26
+ structuredClone(document),
27
+ operations,
28
+ true,
29
+ // Validate operations
30
+ false
31
+ // Don't mutate the original
32
+ );
33
+ return result.newDocument;
34
+ } catch (error) {
35
+ console.error("Failed to apply patches:", error);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ // ../client/src/websocket.ts
41
+ var WebSocketManager = class {
42
+ config;
43
+ ws = null;
44
+ state = "disconnected";
45
+ reconnectAttempts = 0;
46
+ reconnectTimer = null;
47
+ messageQueue = [];
48
+ messageHandlers = /* @__PURE__ */ new Set();
49
+ stateHandlers = /* @__PURE__ */ new Set();
50
+ constructor(config) {
51
+ this.config = {
52
+ autoReconnect: true,
53
+ reconnectDelay: 1e3,
54
+ maxReconnectAttempts: 10,
55
+ ...config
56
+ };
57
+ }
58
+ /**
59
+ * Get current connection state
60
+ */
61
+ getState() {
62
+ return this.state;
63
+ }
64
+ /**
65
+ * Connect to the WebSocket server
66
+ */
67
+ async connect() {
68
+ if (this.state === "connected" || this.state === "connecting") {
69
+ return;
70
+ }
71
+ this.setState("connecting");
72
+ try {
73
+ const url = await this.buildUrl();
74
+ this.ws = new WebSocket(url);
75
+ this.ws.onopen = () => {
76
+ this.setState("connected");
77
+ this.reconnectAttempts = 0;
78
+ this.flushMessageQueue();
79
+ this.config.onConnect?.();
80
+ };
81
+ this.ws.onclose = (event) => {
82
+ this.ws = null;
83
+ this.setState("disconnected");
84
+ this.config.onDisconnect?.();
85
+ if (this.config.autoReconnect && !event.wasClean) {
86
+ this.scheduleReconnect();
87
+ }
88
+ };
89
+ this.ws.onerror = () => {
90
+ const error = new Error("WebSocket error");
91
+ this.config.onError?.(error);
92
+ };
93
+ this.ws.onmessage = (event) => {
94
+ try {
95
+ const message = JSON.parse(event.data);
96
+ this.handleMessage(message);
97
+ } catch (error) {
98
+ console.error("Failed to parse WebSocket message:", error);
99
+ }
100
+ };
101
+ } catch (error) {
102
+ this.setState("disconnected");
103
+ this.config.onError?.(
104
+ error instanceof Error ? error : new Error(String(error))
105
+ );
106
+ if (this.config.autoReconnect) {
107
+ this.scheduleReconnect();
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Disconnect from the WebSocket server
113
+ */
114
+ disconnect() {
115
+ if (this.reconnectTimer) {
116
+ clearTimeout(this.reconnectTimer);
117
+ this.reconnectTimer = null;
118
+ }
119
+ if (this.ws) {
120
+ this.ws.close(1e3, "Client disconnect");
121
+ this.ws = null;
122
+ }
123
+ this.setState("disconnected");
124
+ }
125
+ /**
126
+ * Send a message to the server
127
+ */
128
+ send(message) {
129
+ if (this.state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
130
+ this.ws.send(JSON.stringify(message));
131
+ } else {
132
+ this.messageQueue.push(message);
133
+ }
134
+ }
135
+ /**
136
+ * Subscribe to incoming messages
137
+ */
138
+ onMessage(handler) {
139
+ this.messageHandlers.add(handler);
140
+ return () => this.messageHandlers.delete(handler);
141
+ }
142
+ /**
143
+ * Subscribe to connection state changes
144
+ */
145
+ onStateChange(handler) {
146
+ this.stateHandlers.add(handler);
147
+ return () => this.stateHandlers.delete(handler);
148
+ }
149
+ async buildUrl() {
150
+ let url = this.config.url;
151
+ if (this.config.auth) {
152
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
153
+ const separator = url.includes("?") ? "&" : "?";
154
+ url = `${url}${separator}token=${encodeURIComponent(token)}`;
155
+ }
156
+ return url;
157
+ }
158
+ setState(state) {
159
+ this.state = state;
160
+ for (const handler of this.stateHandlers) {
161
+ handler(state);
162
+ }
163
+ }
164
+ handleMessage(message) {
165
+ for (const handler of this.messageHandlers) {
166
+ handler(message);
167
+ }
168
+ }
169
+ scheduleReconnect() {
170
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
171
+ console.error("Max reconnection attempts reached");
172
+ return;
173
+ }
174
+ this.setState("reconnecting");
175
+ this.reconnectAttempts++;
176
+ const delay = Math.min(
177
+ this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
178
+ 3e4
179
+ // Max 30 seconds
180
+ ) * (0.5 + Math.random() * 0.5);
181
+ this.reconnectTimer = setTimeout(() => {
182
+ this.reconnectTimer = null;
183
+ void this.connect();
184
+ }, delay);
185
+ }
186
+ flushMessageQueue() {
187
+ const queue = this.messageQueue;
188
+ this.messageQueue = [];
189
+ for (const message of queue) {
190
+ this.send(message);
191
+ }
192
+ }
193
+ };
194
+
195
+ // ../client/src/client.ts
196
+ function generateId() {
197
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
198
+ }
199
+ var ReactiveClient = class {
200
+ wsManager;
201
+ subscriptions = /* @__PURE__ */ new Map();
202
+ pendingCalls = /* @__PURE__ */ new Map();
203
+ connectionState = "disconnected";
204
+ stateListeners = /* @__PURE__ */ new Set();
205
+ constructor(config) {
206
+ this.wsManager = new WebSocketManager(config);
207
+ this.wsManager.onMessage((message) => this.handleMessage(message));
208
+ this.wsManager.onStateChange((state) => {
209
+ this.connectionState = state;
210
+ this.notifyStateListeners();
211
+ if (state === "connected") {
212
+ this.resubscribeAll();
213
+ }
214
+ });
215
+ }
216
+ /**
217
+ * Connect to the server
218
+ */
219
+ async connect() {
220
+ await this.wsManager.connect();
221
+ }
222
+ /**
223
+ * Disconnect from the server
224
+ */
225
+ disconnect() {
226
+ this.wsManager.disconnect();
227
+ }
228
+ /**
229
+ * Get current connection state
230
+ */
231
+ getConnectionState() {
232
+ return this.connectionState;
233
+ }
234
+ /**
235
+ * Subscribe to connection state changes
236
+ */
237
+ onConnectionStateChange(listener) {
238
+ this.stateListeners.add(listener);
239
+ return () => this.stateListeners.delete(listener);
240
+ }
241
+ /**
242
+ * Subscribe to a procedure
243
+ */
244
+ subscribe(path, options = {}) {
245
+ const id = generateId();
246
+ const state = {
247
+ id,
248
+ path,
249
+ input: options.input,
250
+ data: void 0,
251
+ loading: true,
252
+ error: void 0,
253
+ listeners: /* @__PURE__ */ new Set(),
254
+ options
255
+ };
256
+ this.subscriptions.set(id, state);
257
+ this.wsManager.send({
258
+ type: "subscribe",
259
+ subscriptionId: id,
260
+ path,
261
+ input: options.input
262
+ });
263
+ const subscription = {
264
+ get data() {
265
+ return state.data;
266
+ },
267
+ get loading() {
268
+ return state.loading;
269
+ },
270
+ get error() {
271
+ return state.error;
272
+ },
273
+ unsubscribe: () => this.unsubscribe(id),
274
+ refetch: () => this.refetch(id)
275
+ };
276
+ return subscription;
277
+ }
278
+ /**
279
+ * Call a mutation procedure
280
+ */
281
+ async call(path, input) {
282
+ const callId = generateId();
283
+ return new Promise((resolve, reject) => {
284
+ this.pendingCalls.set(callId, {
285
+ resolve,
286
+ reject
287
+ });
288
+ this.wsManager.send({
289
+ type: "call",
290
+ callId,
291
+ path,
292
+ input
293
+ });
294
+ setTimeout(() => {
295
+ if (this.pendingCalls.has(callId)) {
296
+ this.pendingCalls.delete(callId);
297
+ reject(new Error("Call timeout"));
298
+ }
299
+ }, 3e4);
300
+ });
301
+ }
302
+ /**
303
+ * Unsubscribe from a subscription
304
+ */
305
+ unsubscribe(id) {
306
+ const state = this.subscriptions.get(id);
307
+ if (!state) return;
308
+ this.subscriptions.delete(id);
309
+ this.wsManager.send({
310
+ type: "unsubscribe",
311
+ subscriptionId: id
312
+ });
313
+ }
314
+ /**
315
+ * Refetch a subscription
316
+ */
317
+ async refetch(id) {
318
+ const state = this.subscriptions.get(id);
319
+ if (!state) return;
320
+ state.loading = true;
321
+ this.notifySubscriptionListeners(id);
322
+ this.wsManager.send({
323
+ type: "subscribe",
324
+ subscriptionId: id,
325
+ path: state.path,
326
+ input: state.input
327
+ });
328
+ }
329
+ /**
330
+ * Handle incoming server messages
331
+ */
332
+ handleMessage(message) {
333
+ switch (message.type) {
334
+ case "snapshot":
335
+ this.handleSnapshot(message.subscriptionId, message.data);
336
+ break;
337
+ case "patch":
338
+ this.handlePatch(message.subscriptionId, message.patches);
339
+ break;
340
+ case "result":
341
+ this.handleResult(message.callId, message.data);
342
+ break;
343
+ case "error":
344
+ this.handleError(message);
345
+ break;
346
+ }
347
+ }
348
+ /**
349
+ * Handle a snapshot message
350
+ */
351
+ handleSnapshot(subscriptionId, data) {
352
+ const state = this.subscriptions.get(subscriptionId);
353
+ if (!state) return;
354
+ state.data = data;
355
+ state.loading = false;
356
+ state.error = void 0;
357
+ this.notifySubscriptionListeners(subscriptionId);
358
+ state.options.onData?.(data);
359
+ }
360
+ /**
361
+ * Handle a patch message
362
+ */
363
+ handlePatch(subscriptionId, patches) {
364
+ const state = this.subscriptions.get(subscriptionId);
365
+ if (!state || state.data === void 0) return;
366
+ try {
367
+ state.data = applyPatches(state.data, patches);
368
+ this.notifySubscriptionListeners(subscriptionId);
369
+ state.options.onData?.(state.data);
370
+ } catch (error) {
371
+ state.error = error instanceof Error ? error : new Error(String(error));
372
+ this.notifySubscriptionListeners(subscriptionId);
373
+ state.options.onError?.(state.error);
374
+ }
375
+ }
376
+ /**
377
+ * Handle a result message
378
+ */
379
+ handleResult(callId, data) {
380
+ const pending = this.pendingCalls.get(callId);
381
+ if (!pending) return;
382
+ this.pendingCalls.delete(callId);
383
+ pending.resolve(data);
384
+ }
385
+ /**
386
+ * Handle an error message
387
+ */
388
+ handleError(message) {
389
+ const error = new Error(message.message);
390
+ if (message.subscriptionId) {
391
+ const state = this.subscriptions.get(message.subscriptionId);
392
+ if (state) {
393
+ state.error = error;
394
+ state.loading = false;
395
+ this.notifySubscriptionListeners(message.subscriptionId);
396
+ state.options.onError?.(error);
397
+ }
398
+ }
399
+ if (message.callId) {
400
+ const pending = this.pendingCalls.get(message.callId);
401
+ if (pending) {
402
+ this.pendingCalls.delete(message.callId);
403
+ pending.reject(error);
404
+ }
405
+ }
406
+ }
407
+ /**
408
+ * Resubscribe to all subscriptions after reconnect
409
+ */
410
+ resubscribeAll() {
411
+ for (const [id, state] of this.subscriptions) {
412
+ if (state.options.resubscribeOnReconnect !== false) {
413
+ state.loading = true;
414
+ this.notifySubscriptionListeners(id);
415
+ this.wsManager.send({
416
+ type: "subscribe",
417
+ subscriptionId: id,
418
+ path: state.path,
419
+ input: state.input
420
+ });
421
+ }
422
+ }
423
+ }
424
+ /**
425
+ * Notify subscription listeners of state changes
426
+ */
427
+ notifySubscriptionListeners(id) {
428
+ const state = this.subscriptions.get(id);
429
+ if (!state) return;
430
+ for (const listener of state.listeners) {
431
+ listener();
432
+ }
433
+ }
434
+ /**
435
+ * Notify connection state listeners
436
+ */
437
+ notifyStateListeners() {
438
+ for (const listener of this.stateListeners) {
439
+ listener(this.connectionState);
440
+ }
441
+ }
442
+ /**
443
+ * Add a listener for subscription state changes
444
+ * Used internally by React hooks
445
+ */
446
+ addSubscriptionListener(id, listener) {
447
+ const state = this.subscriptions.get(id);
448
+ if (!state) return () => {
449
+ };
450
+ state.listeners.add(listener);
451
+ return () => state.listeners.delete(listener);
452
+ }
453
+ /**
454
+ * Get subscription state
455
+ * Used internally by React hooks
456
+ */
457
+ getSubscriptionState(id) {
458
+ return this.subscriptions.get(id);
459
+ }
460
+ };
461
+ function createReactiveClient(config) {
462
+ const client = new ReactiveClient(config);
463
+ return createTypedProxy(client, []);
464
+ }
465
+ function createTypedProxy(client, path) {
466
+ return new Proxy(
467
+ {},
468
+ {
469
+ get(_target, prop) {
470
+ if (prop === "_client") return client;
471
+ if (prop === "connect") return () => client.connect();
472
+ if (prop === "disconnect") return () => client.disconnect();
473
+ if (typeof prop !== "string") return void 0;
474
+ const newPath = [...path, prop];
475
+ if (prop === "useSubscription") {
476
+ return (input, options) => {
477
+ const parentPath = path.join(".");
478
+ return client.subscribe(parentPath, { ...options, input });
479
+ };
480
+ }
481
+ if (prop === "mutate") {
482
+ return async (input) => {
483
+ const parentPath = path.join(".");
484
+ return client.call(parentPath, input);
485
+ };
486
+ }
487
+ return createTypedProxy(client, newPath);
488
+ }
489
+ }
490
+ );
491
+ }
492
+ var ReactiveClientContext = createContext(null);
493
+ function ReactiveClientProvider({
494
+ children,
495
+ config,
496
+ client: externalClient
497
+ }) {
498
+ const url = config?.url;
499
+ const hasUrl = Boolean(url);
500
+ const [client, setClient] = useState(
501
+ externalClient ?? null
502
+ );
503
+ useEffect(() => {
504
+ if (externalClient || !url) return;
505
+ if (!client) {
506
+ const newClient = new ReactiveClient({ ...config, url });
507
+ setClient(newClient);
508
+ }
509
+ }, [url, externalClient]);
510
+ useEffect(() => {
511
+ if (!client) return;
512
+ void client.connect();
513
+ return () => {
514
+ client.disconnect();
515
+ };
516
+ }, [client]);
517
+ useEffect(() => {
518
+ if (!hasUrl && process.env.NODE_ENV === "development") {
519
+ console.warn(
520
+ "[dynamodb-reactive] WebSocket URL not configured, real-time features disabled"
521
+ );
522
+ }
523
+ }, [hasUrl]);
524
+ return /* @__PURE__ */ jsx(ReactiveClientContext.Provider, { value: client, children });
525
+ }
526
+ function useReactiveClient() {
527
+ return useContext(ReactiveClientContext);
528
+ }
529
+ function useReactiveClientOrThrow() {
530
+ const client = useContext(ReactiveClientContext);
531
+ if (!client) {
532
+ throw new Error(
533
+ "useReactiveClientOrThrow must be used within a ReactiveClientProvider with a configured URL"
534
+ );
535
+ }
536
+ return client;
537
+ }
538
+ function useConnectionState() {
539
+ const client = useReactiveClient();
540
+ return useSyncExternalStore(
541
+ (callback) => {
542
+ if (!client) return () => {
543
+ };
544
+ return client.onConnectionStateChange(callback);
545
+ },
546
+ () => client ? client.getConnectionState() : "disabled",
547
+ () => "disabled"
548
+ );
549
+ }
550
+ function useSubscription(path, options = {}) {
551
+ const client = useReactiveClient();
552
+ const subscriptionRef = useRef(null);
553
+ const [state, setState] = useState({
554
+ data: void 0,
555
+ loading: !client ? false : true,
556
+ error: void 0
557
+ });
558
+ const inputKey = JSON.stringify(options.input);
559
+ const onDataRef = useRef(options.onData);
560
+ const onErrorRef = useRef(options.onError);
561
+ onDataRef.current = options.onData;
562
+ onErrorRef.current = options.onError;
563
+ useEffect(() => {
564
+ if (!client) return;
565
+ const input = inputKey ? JSON.parse(inputKey) : void 0;
566
+ const subscription = client.subscribe(path, {
567
+ input,
568
+ onData: (data) => {
569
+ setState((prev) => ({
570
+ ...prev,
571
+ data,
572
+ loading: false,
573
+ error: void 0
574
+ }));
575
+ onDataRef.current?.(data);
576
+ },
577
+ onError: (error) => {
578
+ setState((prev) => ({
579
+ ...prev,
580
+ error,
581
+ loading: false
582
+ }));
583
+ onErrorRef.current?.(error);
584
+ }
585
+ });
586
+ subscriptionRef.current = subscription;
587
+ setState({
588
+ data: subscription.data,
589
+ loading: subscription.loading,
590
+ error: subscription.error
591
+ });
592
+ return () => {
593
+ subscription.unsubscribe();
594
+ subscriptionRef.current = null;
595
+ };
596
+ }, [client, path, inputKey]);
597
+ const refetch = async () => {
598
+ if (subscriptionRef.current) {
599
+ setState((prev) => ({ ...prev, loading: true }));
600
+ await subscriptionRef.current.refetch();
601
+ }
602
+ };
603
+ return {
604
+ ...state,
605
+ disabled: !client,
606
+ refetch
607
+ };
608
+ }
609
+ function useMutation(path) {
610
+ const client = useReactiveClient();
611
+ const [state, setState] = useState({
612
+ data: void 0,
613
+ loading: false,
614
+ error: void 0
615
+ });
616
+ const mutate = async (input) => {
617
+ if (!client) {
618
+ throw new Error("WebSocket not configured - mutations are disabled");
619
+ }
620
+ setState((prev) => ({ ...prev, loading: true, error: void 0 }));
621
+ try {
622
+ const result = await client.call(path, input);
623
+ setState({ data: result, loading: false, error: void 0 });
624
+ return result;
625
+ } catch (error) {
626
+ const err = error instanceof Error ? error : new Error(String(error));
627
+ setState((prev) => ({ ...prev, loading: false, error: err }));
628
+ throw err;
629
+ }
630
+ };
631
+ const reset = () => {
632
+ setState({ data: void 0, loading: false, error: void 0 });
633
+ };
634
+ return {
635
+ mutate,
636
+ ...state,
637
+ disabled: !client,
638
+ reset
639
+ };
640
+ }
641
+ function createReactiveHooks() {
642
+ return {
643
+ useSubscription: (path, options) => useSubscription(path, options),
644
+ useMutation: (path) => useMutation(path),
645
+ useConnectionState,
646
+ useReactiveClient
647
+ };
648
+ }
649
+ function createProcedureHooks(path) {
650
+ return {
651
+ useSubscription: (input, options) => useSubscription(path, { ...options, input }),
652
+ useMutation: () => useMutation(path)
653
+ };
654
+ }
655
+
656
+ export { ReactiveClient, ReactiveClientProvider, WebSocketManager, applyPatches, createProcedureHooks, createReactiveClient, createReactiveHooks, useConnectionState, useMutation, useReactiveClient, useReactiveClientOrThrow, useSubscription };
657
+ //# sourceMappingURL=chunk-IPEBRXIL.js.map
658
+ //# sourceMappingURL=chunk-IPEBRXIL.js.map