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