dynamodb-reactive 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +93 -51
  2. package/dist/{client-Zb67giI5.d.ts → client-BPAct-Dz.d.ts} +19 -0
  3. package/dist/client.cjs +68 -55
  4. package/dist/client.cjs.map +1 -1
  5. package/dist/client.d.cts +1 -1
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +68 -55
  8. package/dist/client.js.map +1 -1
  9. package/dist/cloudflare.cjs +223 -36
  10. package/dist/cloudflare.cjs.map +1 -1
  11. package/dist/cloudflare.d.cts +78 -6
  12. package/dist/cloudflare.d.ts +78 -6
  13. package/dist/cloudflare.js +223 -36
  14. package/dist/cloudflare.js.map +1 -1
  15. package/dist/cluster.cjs +81 -66
  16. package/dist/cluster.cjs.map +1 -1
  17. package/dist/cluster.js +81 -66
  18. package/dist/cluster.js.map +1 -1
  19. package/dist/core.cjs +1 -3
  20. package/dist/core.cjs.map +1 -1
  21. package/dist/core.js +1 -3
  22. package/dist/core.js.map +1 -1
  23. package/dist/hub.cjs +35 -10
  24. package/dist/hub.cjs.map +1 -1
  25. package/dist/hub.d.cts +18 -3
  26. package/dist/hub.d.ts +18 -3
  27. package/dist/hub.js +35 -10
  28. package/dist/hub.js.map +1 -1
  29. package/dist/index.cjs +1 -3
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/infra.cjs +19 -33
  34. package/dist/infra.cjs.map +1 -1
  35. package/dist/infra.js +19 -33
  36. package/dist/infra.js.map +1 -1
  37. package/dist/react.cjs +71 -65
  38. package/dist/react.cjs.map +1 -1
  39. package/dist/react.js +71 -65
  40. package/dist/react.js.map +1 -1
  41. package/dist/server.cjs +46 -139
  42. package/dist/server.cjs.map +1 -1
  43. package/dist/server.js +46 -139
  44. package/dist/server.js.map +1 -1
  45. package/package.json +22 -24
package/dist/react.js CHANGED
@@ -27,9 +27,7 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
27
27
  if (Array.isArray(value)) {
28
28
  const items = [];
29
29
  for (let index = 0; index < value.length; index++) {
30
- items.push(
31
- canonicalize(value[index], seen, String(index), true) ?? "null"
32
- );
30
+ items.push(canonicalize(value[index], seen, String(index), true) ?? "null");
33
31
  }
34
32
  return `[${items.join(",")}]`;
35
33
  }
@@ -46,6 +44,9 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
46
44
  seen.delete(value);
47
45
  }
48
46
  }
47
+ case "bigint":
48
+ case "function":
49
+ case "symbol":
49
50
  default:
50
51
  throw new TypeError(`Unsupported query input value: ${typeof value}`);
51
52
  }
@@ -53,7 +54,6 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
53
54
  function getQueryKey(path, input) {
54
55
  return `${JSON.stringify(path)}:${canonicalize(input, /* @__PURE__ */ new Set(), "") ?? "undefined"}`;
55
56
  }
56
- var { applyPatch } = jsonpatch;
57
57
  function applyPatches(document2, patches) {
58
58
  if (patches.length === 0) {
59
59
  return document2;
@@ -72,7 +72,7 @@ function applyPatches(document2, patches) {
72
72
  return op;
73
73
  });
74
74
  try {
75
- const result = applyPatch(
75
+ const result = jsonpatch.applyPatch(
76
76
  structuredClone(document2),
77
77
  operations,
78
78
  true,
@@ -100,12 +100,13 @@ function error(...values) {
100
100
  }
101
101
 
102
102
  // ../client/src/websocket.ts
103
- var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
104
103
  function withDefaults(config) {
105
104
  return {
106
105
  autoReconnect: true,
107
106
  connectionTimeout: 1e4,
108
107
  handshakeTimeout: 1e4,
108
+ heartbeatInterval: 5 * 60 * 1e3,
109
+ heartbeatTimeout: 15e3,
109
110
  maxReconnectAttempts: 10,
110
111
  reconnectDelay: 1e3,
111
112
  ...config
@@ -119,6 +120,7 @@ var WebSocketManager = class {
119
120
  reconnectTimer = null;
120
121
  handshakeTimer = null;
121
122
  heartbeatTimer = null;
123
+ pongTimer = null;
122
124
  messageHandlers = /* @__PURE__ */ new Set();
123
125
  stateHandlers = /* @__PURE__ */ new Set();
124
126
  handshakeHandlers = /* @__PURE__ */ new Set();
@@ -218,16 +220,7 @@ var WebSocketManager = class {
218
220
  };
219
221
  nextSocket.onclose = () => {
220
222
  if (!this.isCurrent(nextSocket, generation)) return;
221
- this.clearConnectionTimers();
222
- this.ws = null;
223
- this._connectionId = null;
224
- this.setState("disconnected");
225
- this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
226
- if (this.config.autoReconnect && !this.intentionalDisconnect) {
227
- this.scheduleReconnect();
228
- } else {
229
- this.rejectWaiters(new Error("WebSocket disconnected"));
230
- }
223
+ this.handleClose();
231
224
  };
232
225
  nextSocket.onerror = () => {
233
226
  if (!this.isCurrent(nextSocket, generation)) return;
@@ -286,6 +279,13 @@ var WebSocketManager = class {
286
279
  this.setState("disconnected");
287
280
  void this.connect().catch(() => void 0);
288
281
  }
282
+ /** Sends an application frame on the open socket; false when not connected. */
283
+ send(frame) {
284
+ const socket = this.ws;
285
+ if (!socket || this.state !== "connected" || socket.readyState !== socket.OPEN) return false;
286
+ socket.send(JSON.stringify(frame));
287
+ return true;
288
+ }
289
289
  onMessage(handler) {
290
290
  this.messageHandlers.add(handler);
291
291
  return () => this.messageHandlers.delete(handler);
@@ -316,6 +316,7 @@ var WebSocketManager = class {
316
316
  return;
317
317
  }
318
318
  if (message.type === "pong") {
319
+ this.clearPongTimer();
319
320
  if (!this._connectionId || this.state === "connected") return;
320
321
  this.clearHandshakeTimer();
321
322
  this.lastConnectionError = void 0;
@@ -344,6 +345,18 @@ var WebSocketManager = class {
344
345
  socket.close(4e3, "Handshake timeout");
345
346
  }, this.config.handshakeTimeout);
346
347
  }
348
+ handleClose() {
349
+ this.clearConnectionTimers();
350
+ this.ws = null;
351
+ this._connectionId = null;
352
+ this.setState("disconnected");
353
+ this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
354
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
355
+ this.scheduleReconnect();
356
+ } else {
357
+ this.rejectWaiters(new Error("WebSocket disconnected"));
358
+ }
359
+ }
347
360
  startHeartbeat(socket, generation) {
348
361
  this.clearHeartbeatTimer();
349
362
  this.heartbeatTimer = setInterval(() => {
@@ -351,7 +364,17 @@ var WebSocketManager = class {
351
364
  return;
352
365
  }
353
366
  socket.send(JSON.stringify({ type: "ping" }));
354
- }, HEARTBEAT_INTERVAL_MS);
367
+ this.pongTimer ??= setTimeout(() => {
368
+ this.pongTimer = null;
369
+ if (!this.isCurrent(socket, generation)) return;
370
+ const failure = new Error("WebSocket heartbeat timed out");
371
+ this.lastConnectionError = failure;
372
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
373
+ socket.onclose = null;
374
+ socket.close(4001, "Heartbeat timeout");
375
+ this.handleClose();
376
+ }, this.config.heartbeatTimeout);
377
+ }, this.config.heartbeatInterval);
355
378
  }
356
379
  isCurrent(socket, generation) {
357
380
  return this.ws === socket && this.socketGeneration === generation;
@@ -418,9 +441,15 @@ var WebSocketManager = class {
418
441
  clearInterval(this.heartbeatTimer);
419
442
  this.heartbeatTimer = null;
420
443
  }
444
+ clearPongTimer() {
445
+ if (!this.pongTimer) return;
446
+ clearTimeout(this.pongTimer);
447
+ this.pongTimer = null;
448
+ }
421
449
  clearConnectionTimers() {
422
450
  this.clearHandshakeTimer();
423
451
  this.clearHeartbeatTimer();
452
+ this.clearPongTimer();
424
453
  }
425
454
  clearTimers() {
426
455
  this.clearConnectionTimers();
@@ -480,6 +509,14 @@ var ReactiveClient = class {
480
509
  getConnectionState() {
481
510
  return this.connectionState;
482
511
  }
512
+ /**
513
+ * Sends an application frame over the socket, such as a presence update a
514
+ * hub's `onMessage` handles. Returns false while disconnected; resend from
515
+ * `onConnectionStateChange` once connected again.
516
+ */
517
+ send(frame) {
518
+ return this.wsManager.send(frame);
519
+ }
483
520
  onConnectionStateChange(listener) {
484
521
  this.stateListeners.add(listener);
485
522
  return () => this.stateListeners.delete(listener);
@@ -520,10 +557,7 @@ var ReactiveClient = class {
520
557
  this.subscriptionsByKey.set(key, shared);
521
558
  this.subscriptionsById.set(shared.id, shared);
522
559
  if (this.connectionState === "connected") {
523
- void this.ensureRegistered(
524
- shared,
525
- this.wsManager.generation
526
- ).catch(() => void 0);
560
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
527
561
  }
528
562
  }
529
563
  const leaseId = generateId();
@@ -532,9 +566,7 @@ var ReactiveClient = class {
532
566
  onError: options.onError
533
567
  });
534
568
  if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
535
- void this.ensureRegistered(shared, this.wsManager.generation).catch(
536
- () => void 0
537
- );
569
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
538
570
  }
539
571
  const state = shared;
540
572
  let released = false;
@@ -623,9 +655,7 @@ var ReactiveClient = class {
623
655
  registerActiveSubscriptions(socketGeneration) {
624
656
  for (const state of this.subscriptionsByKey.values()) {
625
657
  if (state.active) {
626
- void this.ensureRegistered(state, socketGeneration).catch(
627
- () => void 0
628
- );
658
+ void this.ensureRegistered(state, socketGeneration).catch(() => void 0);
629
659
  }
630
660
  }
631
661
  }
@@ -678,9 +708,7 @@ var ReactiveClient = class {
678
708
  })
679
709
  });
680
710
  if (!response.ok) {
681
- throw new Error(
682
- `Subscribe failed: ${response.status} ${response.statusText}`
683
- );
711
+ throw new Error(`Subscribe failed: ${response.status} ${response.statusText}`);
684
712
  }
685
713
  const result = await response.json();
686
714
  if (result.type === "error") {
@@ -699,12 +727,7 @@ var ReactiveClient = class {
699
727
  throw cause;
700
728
  } finally {
701
729
  if ((!state.active || !isCurrent()) && requestIssued) {
702
- void this.sendUnsubscribe(
703
- registrationConnectionId,
704
- state.id,
705
- generation,
706
- registrationId
707
- );
730
+ void this.sendUnsubscribe(registrationConnectionId, state.id, generation, registrationId);
708
731
  }
709
732
  if (state.registrationPromise === request) {
710
733
  state.registrationPromise = void 0;
@@ -809,15 +832,12 @@ var ReactiveClient = class {
809
832
  return;
810
833
  }
811
834
  this.discardStalePatches(state);
812
- while (true) {
813
- const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
835
+ for (; ; ) {
836
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion).at(0);
814
837
  if (!next) break;
815
838
  state.patchBuffer.delete(next.messageId);
816
839
  try {
817
- state.authoritativeData = applyPatches(
818
- state.authoritativeData,
819
- next.patches
820
- );
840
+ state.authoritativeData = applyPatches(state.authoritativeData, next.patches);
821
841
  state.version = next.toVersion;
822
842
  state.appliedMessageIds.add(next.messageId);
823
843
  const awaitingReconciliation = this.hasSettledLayers(state);
@@ -855,12 +875,7 @@ var ReactiveClient = class {
855
875
  state.stale = true;
856
876
  state.loading = true;
857
877
  this.notifySubscription(state);
858
- let request;
859
- request = this.ensureRegistered(
860
- state,
861
- this.wsManager.generation,
862
- true
863
- ).finally(() => {
878
+ const request = this.ensureRegistered(state, this.wsManager.generation, true).finally(() => {
864
879
  if (state.resyncPromise === request) {
865
880
  state.resyncPromise = void 0;
866
881
  state.resyncGeneration = void 0;
@@ -928,7 +943,10 @@ var ReactiveClient = class {
928
943
  if (options?.optimisticData === void 0) return void 0;
929
944
  return (data) => {
930
945
  if (!Array.isArray(data)) return data;
931
- 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;
946
+ const optimistic = settled ? (
947
+ // oxlint-disable-next-line typescript/prefer-nullish-coalescing -- null is a valid mutation result.
948
+ result === void 0 ? typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData : result
949
+ ) : typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData;
932
950
  if (settled && optimistic !== null && typeof optimistic === "object") {
933
951
  const optimisticId = optimistic.id;
934
952
  if (optimisticId !== void 0 && data.some(
@@ -943,12 +961,7 @@ var ReactiveClient = class {
943
961
  settleMutationLayers(mutationId, input, result, options, states) {
944
962
  for (const state of states) {
945
963
  const layer = state.optimisticLayers.get(mutationId);
946
- const updater = this.createOptimisticUpdater(
947
- input,
948
- options,
949
- result,
950
- true
951
- );
964
+ const updater = this.createOptimisticUpdater(input, options, result, true);
952
965
  if (!layer || !updater) continue;
953
966
  layer.updater = updater;
954
967
  layer.settledRevision = ++state.settlementRevision;
@@ -1094,9 +1107,7 @@ function ReactiveClientProvider({
1094
1107
  const state = renderedManaged.client.getConnectionState();
1095
1108
  if (state === "connected" || state === "connecting") return;
1096
1109
  if (state === "reconnecting") {
1097
- debug(
1098
- "[dynamodb-reactive] Tab active during reconnect; reconnecting now"
1099
- );
1110
+ debug("[dynamodb-reactive] Tab active during reconnect; reconnecting now");
1100
1111
  renderedManaged.client.forceReconnect();
1101
1112
  } else {
1102
1113
  void renderedManaged.client.connect().catch(() => void 0);
@@ -1114,9 +1125,7 @@ function ReactiveClientProvider({
1114
1125
  }, [renderedManaged]);
1115
1126
  useEffect(() => {
1116
1127
  if (!config?.url && !externalClient) {
1117
- warn(
1118
- "[dynamodb-reactive] WebSocket URL not configured; real-time features disabled"
1119
- );
1128
+ warn("[dynamodb-reactive] WebSocket URL not configured; real-time features disabled");
1120
1129
  }
1121
1130
  }, [config?.url, externalClient]);
1122
1131
  return /* @__PURE__ */ jsx(ReactiveClientContext.Provider, { value: renderedManaged?.client ?? null, children });
@@ -1164,10 +1173,7 @@ function useSubscription(path, options = {}) {
1164
1173
  };
1165
1174
  const setData = (updater) => {
1166
1175
  if (client && subscription) {
1167
- client.applyOptimisticUpdate(
1168
- subscription.id,
1169
- updater
1170
- );
1176
+ client.applyOptimisticUpdate(subscription.id, updater);
1171
1177
  }
1172
1178
  };
1173
1179
  return {