dynamodb-reactive 0.3.0 → 0.5.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 (48) hide show
  1. package/README.md +121 -43
  2. package/dist/{client-Zb67giI5.d.ts → client-wZQ9XC9S.d.ts} +11 -0
  3. package/dist/client.cjs +53 -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 +53 -55
  8. package/dist/client.js.map +1 -1
  9. package/dist/cloudflare.cjs +461 -0
  10. package/dist/cloudflare.cjs.map +1 -0
  11. package/dist/cloudflare.d.cts +129 -0
  12. package/dist/cloudflare.d.ts +129 -0
  13. package/dist/cloudflare.js +456 -0
  14. package/dist/cloudflare.js.map +1 -0
  15. package/dist/cluster.cjs +66 -66
  16. package/dist/cluster.cjs.map +1 -1
  17. package/dist/cluster.d.cts +2 -78
  18. package/dist/cluster.d.ts +2 -78
  19. package/dist/cluster.js +66 -66
  20. package/dist/cluster.js.map +1 -1
  21. package/dist/core.cjs +1 -3
  22. package/dist/core.cjs.map +1 -1
  23. package/dist/core.js +1 -3
  24. package/dist/core.js.map +1 -1
  25. package/dist/hub.cjs +164 -0
  26. package/dist/hub.cjs.map +1 -0
  27. package/dist/hub.d.cts +91 -0
  28. package/dist/hub.d.ts +91 -0
  29. package/dist/hub.js +157 -0
  30. package/dist/hub.js.map +1 -0
  31. package/dist/index.cjs +1 -3
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +1 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/infra.cjs +19 -33
  36. package/dist/infra.cjs.map +1 -1
  37. package/dist/infra.js +19 -33
  38. package/dist/infra.js.map +1 -1
  39. package/dist/protocol-C4Syt_Bl.d.ts +78 -0
  40. package/dist/react.cjs +56 -65
  41. package/dist/react.cjs.map +1 -1
  42. package/dist/react.js +56 -65
  43. package/dist/react.js.map +1 -1
  44. package/dist/server.cjs +46 -139
  45. package/dist/server.cjs.map +1 -1
  46. package/dist/server.js +46 -139
  47. package/dist/server.js.map +1 -1
  48. package/package.json +42 -24
package/dist/cluster.cjs CHANGED
@@ -29,9 +29,7 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
29
29
  if (Array.isArray(value)) {
30
30
  const items = [];
31
31
  for (let index = 0; index < value.length; index++) {
32
- items.push(
33
- canonicalize(value[index], seen, String(index), true) ?? "null"
34
- );
32
+ items.push(canonicalize(value[index], seen, String(index), true) ?? "null");
35
33
  }
36
34
  return `[${items.join(",")}]`;
37
35
  }
@@ -48,6 +46,9 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
48
46
  seen.delete(value);
49
47
  }
50
48
  }
49
+ case "bigint":
50
+ case "function":
51
+ case "symbol":
51
52
  default:
52
53
  throw new TypeError(`Unsupported query input value: ${typeof value}`);
53
54
  }
@@ -55,7 +56,6 @@ function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
55
56
  function getQueryKey(path, input) {
56
57
  return `${JSON.stringify(path)}:${canonicalize(input, /* @__PURE__ */ new Set(), "") ?? "undefined"}`;
57
58
  }
58
- var { applyPatch } = jsonpatch__default.default;
59
59
  function applyPatches(document, patches) {
60
60
  if (patches.length === 0) {
61
61
  return document;
@@ -74,7 +74,7 @@ function applyPatches(document, patches) {
74
74
  return op;
75
75
  });
76
76
  try {
77
- const result = applyPatch(
77
+ const result = jsonpatch__default.default.applyPatch(
78
78
  structuredClone(document),
79
79
  operations,
80
80
  true,
@@ -94,12 +94,13 @@ function error(...values) {
94
94
  }
95
95
 
96
96
  // ../client/src/websocket.ts
97
- var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
98
97
  function withDefaults(config) {
99
98
  return {
100
99
  autoReconnect: true,
101
100
  connectionTimeout: 1e4,
102
101
  handshakeTimeout: 1e4,
102
+ heartbeatInterval: 5 * 60 * 1e3,
103
+ heartbeatTimeout: 15e3,
103
104
  maxReconnectAttempts: 10,
104
105
  reconnectDelay: 1e3,
105
106
  ...config
@@ -113,6 +114,7 @@ var WebSocketManager = class {
113
114
  reconnectTimer = null;
114
115
  handshakeTimer = null;
115
116
  heartbeatTimer = null;
117
+ pongTimer = null;
116
118
  messageHandlers = /* @__PURE__ */ new Set();
117
119
  stateHandlers = /* @__PURE__ */ new Set();
118
120
  handshakeHandlers = /* @__PURE__ */ new Set();
@@ -212,16 +214,7 @@ var WebSocketManager = class {
212
214
  };
213
215
  nextSocket.onclose = () => {
214
216
  if (!this.isCurrent(nextSocket, generation)) return;
215
- this.clearConnectionTimers();
216
- this.ws = null;
217
- this._connectionId = null;
218
- this.setState("disconnected");
219
- this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
220
- if (this.config.autoReconnect && !this.intentionalDisconnect) {
221
- this.scheduleReconnect();
222
- } else {
223
- this.rejectWaiters(new Error("WebSocket disconnected"));
224
- }
217
+ this.handleClose();
225
218
  };
226
219
  nextSocket.onerror = () => {
227
220
  if (!this.isCurrent(nextSocket, generation)) return;
@@ -310,6 +303,7 @@ var WebSocketManager = class {
310
303
  return;
311
304
  }
312
305
  if (message.type === "pong") {
306
+ this.clearPongTimer();
313
307
  if (!this._connectionId || this.state === "connected") return;
314
308
  this.clearHandshakeTimer();
315
309
  this.lastConnectionError = void 0;
@@ -338,6 +332,18 @@ var WebSocketManager = class {
338
332
  socket.close(4e3, "Handshake timeout");
339
333
  }, this.config.handshakeTimeout);
340
334
  }
335
+ handleClose() {
336
+ this.clearConnectionTimers();
337
+ this.ws = null;
338
+ this._connectionId = null;
339
+ this.setState("disconnected");
340
+ this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
341
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
342
+ this.scheduleReconnect();
343
+ } else {
344
+ this.rejectWaiters(new Error("WebSocket disconnected"));
345
+ }
346
+ }
341
347
  startHeartbeat(socket, generation) {
342
348
  this.clearHeartbeatTimer();
343
349
  this.heartbeatTimer = setInterval(() => {
@@ -345,7 +351,17 @@ var WebSocketManager = class {
345
351
  return;
346
352
  }
347
353
  socket.send(JSON.stringify({ type: "ping" }));
348
- }, HEARTBEAT_INTERVAL_MS);
354
+ this.pongTimer ??= setTimeout(() => {
355
+ this.pongTimer = null;
356
+ if (!this.isCurrent(socket, generation)) return;
357
+ const failure = new Error("WebSocket heartbeat timed out");
358
+ this.lastConnectionError = failure;
359
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
360
+ socket.onclose = null;
361
+ socket.close(4001, "Heartbeat timeout");
362
+ this.handleClose();
363
+ }, this.config.heartbeatTimeout);
364
+ }, this.config.heartbeatInterval);
349
365
  }
350
366
  isCurrent(socket, generation) {
351
367
  return this.ws === socket && this.socketGeneration === generation;
@@ -412,9 +428,15 @@ var WebSocketManager = class {
412
428
  clearInterval(this.heartbeatTimer);
413
429
  this.heartbeatTimer = null;
414
430
  }
431
+ clearPongTimer() {
432
+ if (!this.pongTimer) return;
433
+ clearTimeout(this.pongTimer);
434
+ this.pongTimer = null;
435
+ }
415
436
  clearConnectionTimers() {
416
437
  this.clearHandshakeTimer();
417
438
  this.clearHeartbeatTimer();
439
+ this.clearPongTimer();
418
440
  }
419
441
  clearTimers() {
420
442
  this.clearConnectionTimers();
@@ -514,10 +536,7 @@ var ReactiveClient = class {
514
536
  this.subscriptionsByKey.set(key, shared);
515
537
  this.subscriptionsById.set(shared.id, shared);
516
538
  if (this.connectionState === "connected") {
517
- void this.ensureRegistered(
518
- shared,
519
- this.wsManager.generation
520
- ).catch(() => void 0);
539
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
521
540
  }
522
541
  }
523
542
  const leaseId = generateId();
@@ -526,9 +545,7 @@ var ReactiveClient = class {
526
545
  onError: options.onError
527
546
  });
528
547
  if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
529
- void this.ensureRegistered(shared, this.wsManager.generation).catch(
530
- () => void 0
531
- );
548
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
532
549
  }
533
550
  const state = shared;
534
551
  let released = false;
@@ -617,9 +634,7 @@ var ReactiveClient = class {
617
634
  registerActiveSubscriptions(socketGeneration) {
618
635
  for (const state of this.subscriptionsByKey.values()) {
619
636
  if (state.active) {
620
- void this.ensureRegistered(state, socketGeneration).catch(
621
- () => void 0
622
- );
637
+ void this.ensureRegistered(state, socketGeneration).catch(() => void 0);
623
638
  }
624
639
  }
625
640
  }
@@ -672,9 +687,7 @@ var ReactiveClient = class {
672
687
  })
673
688
  });
674
689
  if (!response.ok) {
675
- throw new Error(
676
- `Subscribe failed: ${response.status} ${response.statusText}`
677
- );
690
+ throw new Error(`Subscribe failed: ${response.status} ${response.statusText}`);
678
691
  }
679
692
  const result = await response.json();
680
693
  if (result.type === "error") {
@@ -693,12 +706,7 @@ var ReactiveClient = class {
693
706
  throw cause;
694
707
  } finally {
695
708
  if ((!state.active || !isCurrent()) && requestIssued) {
696
- void this.sendUnsubscribe(
697
- registrationConnectionId,
698
- state.id,
699
- generation,
700
- registrationId
701
- );
709
+ void this.sendUnsubscribe(registrationConnectionId, state.id, generation, registrationId);
702
710
  }
703
711
  if (state.registrationPromise === request) {
704
712
  state.registrationPromise = void 0;
@@ -803,15 +811,12 @@ var ReactiveClient = class {
803
811
  return;
804
812
  }
805
813
  this.discardStalePatches(state);
806
- while (true) {
807
- const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
814
+ for (; ; ) {
815
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion).at(0);
808
816
  if (!next) break;
809
817
  state.patchBuffer.delete(next.messageId);
810
818
  try {
811
- state.authoritativeData = applyPatches(
812
- state.authoritativeData,
813
- next.patches
814
- );
819
+ state.authoritativeData = applyPatches(state.authoritativeData, next.patches);
815
820
  state.version = next.toVersion;
816
821
  state.appliedMessageIds.add(next.messageId);
817
822
  const awaitingReconciliation = this.hasSettledLayers(state);
@@ -849,12 +854,7 @@ var ReactiveClient = class {
849
854
  state.stale = true;
850
855
  state.loading = true;
851
856
  this.notifySubscription(state);
852
- let request;
853
- request = this.ensureRegistered(
854
- state,
855
- this.wsManager.generation,
856
- true
857
- ).finally(() => {
857
+ const request = this.ensureRegistered(state, this.wsManager.generation, true).finally(() => {
858
858
  if (state.resyncPromise === request) {
859
859
  state.resyncPromise = void 0;
860
860
  state.resyncGeneration = void 0;
@@ -922,7 +922,10 @@ var ReactiveClient = class {
922
922
  if (options?.optimisticData === void 0) return void 0;
923
923
  return (data) => {
924
924
  if (!Array.isArray(data)) return data;
925
- 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;
925
+ const optimistic = settled ? (
926
+ // oxlint-disable-next-line typescript/prefer-nullish-coalescing -- null is a valid mutation result.
927
+ result === void 0 ? typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData : result
928
+ ) : typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData;
926
929
  if (settled && optimistic !== null && typeof optimistic === "object") {
927
930
  const optimisticId = optimistic.id;
928
931
  if (optimisticId !== void 0 && data.some(
@@ -937,12 +940,7 @@ var ReactiveClient = class {
937
940
  settleMutationLayers(mutationId, input, result, options, states) {
938
941
  for (const state of states) {
939
942
  const layer = state.optimisticLayers.get(mutationId);
940
- const updater = this.createOptimisticUpdater(
941
- input,
942
- options,
943
- result,
944
- true
945
- );
943
+ const updater = this.createOptimisticUpdater(input, options, result, true);
946
944
  if (!layer || !updater) continue;
947
945
  layer.updater = updater;
948
946
  layer.settledRevision = ++state.settlementRevision;
@@ -1019,6 +1017,7 @@ var DEFAULT_CLUSTER_HTTP_PATH = "/api/reactive";
1019
1017
  var DEFAULT_RETRY_DELAY_MS = 1e4;
1020
1018
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1021
1019
  var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
1020
+ var DEFAULT_CLUSTER_HEARTBEAT_MS = 3e4;
1022
1021
  function isRecord(value) {
1023
1022
  return typeof value === "object" && value !== null;
1024
1023
  }
@@ -1055,10 +1054,7 @@ function wait(milliseconds, signal) {
1055
1054
  }
1056
1055
  async function fetchClusterTicket(config) {
1057
1056
  const runnerToken = typeof config.runnerToken === "function" ? await config.runnerToken() : config.runnerToken;
1058
- const endpoint = new URL(
1059
- config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH,
1060
- config.baseUrl
1061
- );
1057
+ const endpoint = new URL(config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH, config.baseUrl);
1062
1058
  const response = await (config.fetch ?? fetch)(endpoint, {
1063
1059
  method: "POST",
1064
1060
  headers: { authorization: `Bearer ${runnerToken}` },
@@ -1071,10 +1067,7 @@ async function fetchClusterTicket(config) {
1071
1067
  }
1072
1068
  async function connectClusterWorker(config) {
1073
1069
  const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
1074
- const httpUrl = new URL(
1075
- config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH,
1076
- config.baseUrl
1077
- ).toString();
1070
+ const httpUrl = new URL(config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH, config.baseUrl).toString();
1078
1071
  for (; ; ) {
1079
1072
  if (config.signal?.aborted) throw abortError(config.signal);
1080
1073
  let client;
@@ -1083,6 +1076,7 @@ async function connectClusterWorker(config) {
1083
1076
  client = new ReactiveClient({
1084
1077
  reconnectDelay: retryDelay,
1085
1078
  maxReconnectAttempts: Number.MAX_SAFE_INTEGER,
1079
+ heartbeatInterval: DEFAULT_CLUSTER_HEARTBEAT_MS,
1086
1080
  ...config.client,
1087
1081
  url: ticket.url,
1088
1082
  httpUrl,
@@ -1126,7 +1120,11 @@ async function createSignedClusterTicket(options) {
1126
1120
  encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt }))
1127
1121
  );
1128
1122
  const signature = new Uint8Array(
1129
- await crypto.subtle.sign("HMAC", await hmacKey(options.secret, "sign"), encoder.encode(payload))
1123
+ await crypto.subtle.sign(
1124
+ "HMAC",
1125
+ await hmacKey(options.secret, "sign"),
1126
+ encoder.encode(payload)
1127
+ )
1130
1128
  );
1131
1129
  return parseClusterTicket({
1132
1130
  token: `${payload}.${base64UrlEncode(signature)}`,
@@ -1135,8 +1133,10 @@ async function createSignedClusterTicket(options) {
1135
1133
  });
1136
1134
  }
1137
1135
  async function verifySignedClusterTicket(token, secret, now = Date.now()) {
1138
- const [payload, signature, extra] = token.split(".");
1139
- if (!payload || !signature || extra !== void 0) return void 0;
1136
+ const parts = token.split(".");
1137
+ if (parts.length !== 2) return void 0;
1138
+ const [payload, signature] = parts;
1139
+ if (!payload || !signature) return void 0;
1140
1140
  try {
1141
1141
  const valid = await crypto.subtle.verify(
1142
1142
  "HMAC",