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/cluster.js CHANGED
@@ -23,9 +23,7 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
23
23
  if (Array.isArray(value)) {
24
24
  const items = [];
25
25
  for (let index = 0; index < value.length; index++) {
26
- items.push(
27
- canonicalize(value[index], seen, String(index), true) ?? "null"
28
- );
26
+ items.push(canonicalize(value[index], seen, String(index), true) ?? "null");
29
27
  }
30
28
  return `[${items.join(",")}]`;
31
29
  }
@@ -42,6 +40,9 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
42
40
  seen.delete(value);
43
41
  }
44
42
  }
43
+ case "bigint":
44
+ case "function":
45
+ case "symbol":
45
46
  default:
46
47
  throw new TypeError(`Unsupported query input value: ${typeof value}`);
47
48
  }
@@ -49,7 +50,6 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
49
50
  function getQueryKey(path, input) {
50
51
  return `${JSON.stringify(path)}:${canonicalize(input, /* @__PURE__ */ new Set(), "") ?? "undefined"}`;
51
52
  }
52
- var { applyPatch } = jsonpatch;
53
53
  function applyPatches(document, patches) {
54
54
  if (patches.length === 0) {
55
55
  return document;
@@ -68,7 +68,7 @@ function applyPatches(document, patches) {
68
68
  return op;
69
69
  });
70
70
  try {
71
- const result = applyPatch(
71
+ const result = jsonpatch.applyPatch(
72
72
  structuredClone(document),
73
73
  operations,
74
74
  true,
@@ -88,12 +88,13 @@ function error(...values) {
88
88
  }
89
89
 
90
90
  // ../client/src/websocket.ts
91
- var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
92
91
  function withDefaults(config) {
93
92
  return {
94
93
  autoReconnect: true,
95
94
  connectionTimeout: 1e4,
96
95
  handshakeTimeout: 1e4,
96
+ heartbeatInterval: 5 * 60 * 1e3,
97
+ heartbeatTimeout: 15e3,
97
98
  maxReconnectAttempts: 10,
98
99
  reconnectDelay: 1e3,
99
100
  ...config
@@ -107,6 +108,7 @@ var WebSocketManager = class {
107
108
  reconnectTimer = null;
108
109
  handshakeTimer = null;
109
110
  heartbeatTimer = null;
111
+ pongTimer = null;
110
112
  messageHandlers = /* @__PURE__ */ new Set();
111
113
  stateHandlers = /* @__PURE__ */ new Set();
112
114
  handshakeHandlers = /* @__PURE__ */ new Set();
@@ -206,16 +208,7 @@ var WebSocketManager = class {
206
208
  };
207
209
  nextSocket.onclose = () => {
208
210
  if (!this.isCurrent(nextSocket, generation)) return;
209
- this.clearConnectionTimers();
210
- this.ws = null;
211
- this._connectionId = null;
212
- this.setState("disconnected");
213
- this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
214
- if (this.config.autoReconnect && !this.intentionalDisconnect) {
215
- this.scheduleReconnect();
216
- } else {
217
- this.rejectWaiters(new Error("WebSocket disconnected"));
218
- }
211
+ this.handleClose();
219
212
  };
220
213
  nextSocket.onerror = () => {
221
214
  if (!this.isCurrent(nextSocket, generation)) return;
@@ -274,6 +267,13 @@ var WebSocketManager = class {
274
267
  this.setState("disconnected");
275
268
  void this.connect().catch(() => void 0);
276
269
  }
270
+ /** Sends an application frame on the open socket; false when not connected. */
271
+ send(frame) {
272
+ const socket = this.ws;
273
+ if (!socket || this.state !== "connected" || socket.readyState !== socket.OPEN) return false;
274
+ socket.send(JSON.stringify(frame));
275
+ return true;
276
+ }
277
277
  onMessage(handler) {
278
278
  this.messageHandlers.add(handler);
279
279
  return () => this.messageHandlers.delete(handler);
@@ -304,6 +304,7 @@ var WebSocketManager = class {
304
304
  return;
305
305
  }
306
306
  if (message.type === "pong") {
307
+ this.clearPongTimer();
307
308
  if (!this._connectionId || this.state === "connected") return;
308
309
  this.clearHandshakeTimer();
309
310
  this.lastConnectionError = void 0;
@@ -332,6 +333,18 @@ var WebSocketManager = class {
332
333
  socket.close(4e3, "Handshake timeout");
333
334
  }, this.config.handshakeTimeout);
334
335
  }
336
+ handleClose() {
337
+ this.clearConnectionTimers();
338
+ this.ws = null;
339
+ this._connectionId = null;
340
+ this.setState("disconnected");
341
+ this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
342
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
343
+ this.scheduleReconnect();
344
+ } else {
345
+ this.rejectWaiters(new Error("WebSocket disconnected"));
346
+ }
347
+ }
335
348
  startHeartbeat(socket, generation) {
336
349
  this.clearHeartbeatTimer();
337
350
  this.heartbeatTimer = setInterval(() => {
@@ -339,7 +352,17 @@ var WebSocketManager = class {
339
352
  return;
340
353
  }
341
354
  socket.send(JSON.stringify({ type: "ping" }));
342
- }, HEARTBEAT_INTERVAL_MS);
355
+ this.pongTimer ??= setTimeout(() => {
356
+ this.pongTimer = null;
357
+ if (!this.isCurrent(socket, generation)) return;
358
+ const failure = new Error("WebSocket heartbeat timed out");
359
+ this.lastConnectionError = failure;
360
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
361
+ socket.onclose = null;
362
+ socket.close(4001, "Heartbeat timeout");
363
+ this.handleClose();
364
+ }, this.config.heartbeatTimeout);
365
+ }, this.config.heartbeatInterval);
343
366
  }
344
367
  isCurrent(socket, generation) {
345
368
  return this.ws === socket && this.socketGeneration === generation;
@@ -406,9 +429,15 @@ var WebSocketManager = class {
406
429
  clearInterval(this.heartbeatTimer);
407
430
  this.heartbeatTimer = null;
408
431
  }
432
+ clearPongTimer() {
433
+ if (!this.pongTimer) return;
434
+ clearTimeout(this.pongTimer);
435
+ this.pongTimer = null;
436
+ }
409
437
  clearConnectionTimers() {
410
438
  this.clearHandshakeTimer();
411
439
  this.clearHeartbeatTimer();
440
+ this.clearPongTimer();
412
441
  }
413
442
  clearTimers() {
414
443
  this.clearConnectionTimers();
@@ -468,6 +497,14 @@ var ReactiveClient = class {
468
497
  getConnectionState() {
469
498
  return this.connectionState;
470
499
  }
500
+ /**
501
+ * Sends an application frame over the socket, such as a presence update a
502
+ * hub's `onMessage` handles. Returns false while disconnected; resend from
503
+ * `onConnectionStateChange` once connected again.
504
+ */
505
+ send(frame) {
506
+ return this.wsManager.send(frame);
507
+ }
471
508
  onConnectionStateChange(listener) {
472
509
  this.stateListeners.add(listener);
473
510
  return () => this.stateListeners.delete(listener);
@@ -508,10 +545,7 @@ var ReactiveClient = class {
508
545
  this.subscriptionsByKey.set(key, shared);
509
546
  this.subscriptionsById.set(shared.id, shared);
510
547
  if (this.connectionState === "connected") {
511
- void this.ensureRegistered(
512
- shared,
513
- this.wsManager.generation
514
- ).catch(() => void 0);
548
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
515
549
  }
516
550
  }
517
551
  const leaseId = generateId();
@@ -520,9 +554,7 @@ var ReactiveClient = class {
520
554
  onError: options.onError
521
555
  });
522
556
  if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
523
- void this.ensureRegistered(shared, this.wsManager.generation).catch(
524
- () => void 0
525
- );
557
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
526
558
  }
527
559
  const state = shared;
528
560
  let released = false;
@@ -611,9 +643,7 @@ var ReactiveClient = class {
611
643
  registerActiveSubscriptions(socketGeneration) {
612
644
  for (const state of this.subscriptionsByKey.values()) {
613
645
  if (state.active) {
614
- void this.ensureRegistered(state, socketGeneration).catch(
615
- () => void 0
616
- );
646
+ void this.ensureRegistered(state, socketGeneration).catch(() => void 0);
617
647
  }
618
648
  }
619
649
  }
@@ -666,9 +696,7 @@ var ReactiveClient = class {
666
696
  })
667
697
  });
668
698
  if (!response.ok) {
669
- throw new Error(
670
- `Subscribe failed: ${response.status} ${response.statusText}`
671
- );
699
+ throw new Error(`Subscribe failed: ${response.status} ${response.statusText}`);
672
700
  }
673
701
  const result = await response.json();
674
702
  if (result.type === "error") {
@@ -687,12 +715,7 @@ var ReactiveClient = class {
687
715
  throw cause;
688
716
  } finally {
689
717
  if ((!state.active || !isCurrent()) && requestIssued) {
690
- void this.sendUnsubscribe(
691
- registrationConnectionId,
692
- state.id,
693
- generation,
694
- registrationId
695
- );
718
+ void this.sendUnsubscribe(registrationConnectionId, state.id, generation, registrationId);
696
719
  }
697
720
  if (state.registrationPromise === request) {
698
721
  state.registrationPromise = void 0;
@@ -797,15 +820,12 @@ var ReactiveClient = class {
797
820
  return;
798
821
  }
799
822
  this.discardStalePatches(state);
800
- while (true) {
801
- const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
823
+ for (; ; ) {
824
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion).at(0);
802
825
  if (!next) break;
803
826
  state.patchBuffer.delete(next.messageId);
804
827
  try {
805
- state.authoritativeData = applyPatches(
806
- state.authoritativeData,
807
- next.patches
808
- );
828
+ state.authoritativeData = applyPatches(state.authoritativeData, next.patches);
809
829
  state.version = next.toVersion;
810
830
  state.appliedMessageIds.add(next.messageId);
811
831
  const awaitingReconciliation = this.hasSettledLayers(state);
@@ -843,12 +863,7 @@ var ReactiveClient = class {
843
863
  state.stale = true;
844
864
  state.loading = true;
845
865
  this.notifySubscription(state);
846
- let request;
847
- request = this.ensureRegistered(
848
- state,
849
- this.wsManager.generation,
850
- true
851
- ).finally(() => {
866
+ const request = this.ensureRegistered(state, this.wsManager.generation, true).finally(() => {
852
867
  if (state.resyncPromise === request) {
853
868
  state.resyncPromise = void 0;
854
869
  state.resyncGeneration = void 0;
@@ -916,7 +931,10 @@ var ReactiveClient = class {
916
931
  if (options?.optimisticData === void 0) return void 0;
917
932
  return (data) => {
918
933
  if (!Array.isArray(data)) return data;
919
- 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;
934
+ const optimistic = settled ? (
935
+ // oxlint-disable-next-line typescript/prefer-nullish-coalescing -- null is a valid mutation result.
936
+ result === void 0 ? typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData : result
937
+ ) : typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData;
920
938
  if (settled && optimistic !== null && typeof optimistic === "object") {
921
939
  const optimisticId = optimistic.id;
922
940
  if (optimisticId !== void 0 && data.some(
@@ -931,12 +949,7 @@ var ReactiveClient = class {
931
949
  settleMutationLayers(mutationId, input, result, options, states) {
932
950
  for (const state of states) {
933
951
  const layer = state.optimisticLayers.get(mutationId);
934
- const updater = this.createOptimisticUpdater(
935
- input,
936
- options,
937
- result,
938
- true
939
- );
952
+ const updater = this.createOptimisticUpdater(input, options, result, true);
940
953
  if (!layer || !updater) continue;
941
954
  layer.updater = updater;
942
955
  layer.settledRevision = ++state.settlementRevision;
@@ -1013,6 +1026,7 @@ var DEFAULT_CLUSTER_HTTP_PATH = "/api/reactive";
1013
1026
  var DEFAULT_RETRY_DELAY_MS = 1e4;
1014
1027
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1015
1028
  var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
1029
+ var DEFAULT_CLUSTER_HEARTBEAT_MS = 3e4;
1016
1030
  function isRecord(value) {
1017
1031
  return typeof value === "object" && value !== null;
1018
1032
  }
@@ -1049,10 +1063,7 @@ function wait(milliseconds, signal) {
1049
1063
  }
1050
1064
  async function fetchClusterTicket(config) {
1051
1065
  const runnerToken = typeof config.runnerToken === "function" ? await config.runnerToken() : config.runnerToken;
1052
- const endpoint = new URL(
1053
- config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH,
1054
- config.baseUrl
1055
- );
1066
+ const endpoint = new URL(config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH, config.baseUrl);
1056
1067
  const response = await (config.fetch ?? fetch)(endpoint, {
1057
1068
  method: "POST",
1058
1069
  headers: { authorization: `Bearer ${runnerToken}` },
@@ -1065,10 +1076,7 @@ async function fetchClusterTicket(config) {
1065
1076
  }
1066
1077
  async function connectClusterWorker(config) {
1067
1078
  const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
1068
- const httpUrl = new URL(
1069
- config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH,
1070
- config.baseUrl
1071
- ).toString();
1079
+ const httpUrl = new URL(config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH, config.baseUrl).toString();
1072
1080
  for (; ; ) {
1073
1081
  if (config.signal?.aborted) throw abortError(config.signal);
1074
1082
  let client;
@@ -1077,6 +1085,7 @@ async function connectClusterWorker(config) {
1077
1085
  client = new ReactiveClient({
1078
1086
  reconnectDelay: retryDelay,
1079
1087
  maxReconnectAttempts: Number.MAX_SAFE_INTEGER,
1088
+ heartbeatInterval: DEFAULT_CLUSTER_HEARTBEAT_MS,
1080
1089
  ...config.client,
1081
1090
  url: ticket.url,
1082
1091
  httpUrl,
@@ -1120,7 +1129,11 @@ async function createSignedClusterTicket(options) {
1120
1129
  encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt }))
1121
1130
  );
1122
1131
  const signature = new Uint8Array(
1123
- await crypto.subtle.sign("HMAC", await hmacKey(options.secret, "sign"), encoder.encode(payload))
1132
+ await crypto.subtle.sign(
1133
+ "HMAC",
1134
+ await hmacKey(options.secret, "sign"),
1135
+ encoder.encode(payload)
1136
+ )
1124
1137
  );
1125
1138
  return parseClusterTicket({
1126
1139
  token: `${payload}.${base64UrlEncode(signature)}`,
@@ -1129,8 +1142,10 @@ async function createSignedClusterTicket(options) {
1129
1142
  });
1130
1143
  }
1131
1144
  async function verifySignedClusterTicket(token, secret, now = Date.now()) {
1132
- const [payload, signature, extra] = token.split(".");
1133
- if (!payload || !signature || extra !== void 0) return void 0;
1145
+ const parts = token.split(".");
1146
+ if (parts.length !== 2) return void 0;
1147
+ const [payload, signature] = parts;
1148
+ if (!payload || !signature) return void 0;
1134
1149
  try {
1135
1150
  const valid = await crypto.subtle.verify(
1136
1151
  "HMAC",