dynamodb-reactive 0.4.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.
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;
@@ -304,6 +297,7 @@ var WebSocketManager = class {
304
297
  return;
305
298
  }
306
299
  if (message.type === "pong") {
300
+ this.clearPongTimer();
307
301
  if (!this._connectionId || this.state === "connected") return;
308
302
  this.clearHandshakeTimer();
309
303
  this.lastConnectionError = void 0;
@@ -332,6 +326,18 @@ var WebSocketManager = class {
332
326
  socket.close(4e3, "Handshake timeout");
333
327
  }, this.config.handshakeTimeout);
334
328
  }
329
+ handleClose() {
330
+ this.clearConnectionTimers();
331
+ this.ws = null;
332
+ this._connectionId = null;
333
+ this.setState("disconnected");
334
+ this.invokeLifecycleCallback("onDisconnect", this.config.onDisconnect);
335
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
336
+ this.scheduleReconnect();
337
+ } else {
338
+ this.rejectWaiters(new Error("WebSocket disconnected"));
339
+ }
340
+ }
335
341
  startHeartbeat(socket, generation) {
336
342
  this.clearHeartbeatTimer();
337
343
  this.heartbeatTimer = setInterval(() => {
@@ -339,7 +345,17 @@ var WebSocketManager = class {
339
345
  return;
340
346
  }
341
347
  socket.send(JSON.stringify({ type: "ping" }));
342
- }, HEARTBEAT_INTERVAL_MS);
348
+ this.pongTimer ??= setTimeout(() => {
349
+ this.pongTimer = null;
350
+ if (!this.isCurrent(socket, generation)) return;
351
+ const failure = new Error("WebSocket heartbeat timed out");
352
+ this.lastConnectionError = failure;
353
+ this.invokeLifecycleCallback("onError", this.config.onError, failure);
354
+ socket.onclose = null;
355
+ socket.close(4001, "Heartbeat timeout");
356
+ this.handleClose();
357
+ }, this.config.heartbeatTimeout);
358
+ }, this.config.heartbeatInterval);
343
359
  }
344
360
  isCurrent(socket, generation) {
345
361
  return this.ws === socket && this.socketGeneration === generation;
@@ -406,9 +422,15 @@ var WebSocketManager = class {
406
422
  clearInterval(this.heartbeatTimer);
407
423
  this.heartbeatTimer = null;
408
424
  }
425
+ clearPongTimer() {
426
+ if (!this.pongTimer) return;
427
+ clearTimeout(this.pongTimer);
428
+ this.pongTimer = null;
429
+ }
409
430
  clearConnectionTimers() {
410
431
  this.clearHandshakeTimer();
411
432
  this.clearHeartbeatTimer();
433
+ this.clearPongTimer();
412
434
  }
413
435
  clearTimers() {
414
436
  this.clearConnectionTimers();
@@ -508,10 +530,7 @@ var ReactiveClient = class {
508
530
  this.subscriptionsByKey.set(key, shared);
509
531
  this.subscriptionsById.set(shared.id, shared);
510
532
  if (this.connectionState === "connected") {
511
- void this.ensureRegistered(
512
- shared,
513
- this.wsManager.generation
514
- ).catch(() => void 0);
533
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
515
534
  }
516
535
  }
517
536
  const leaseId = generateId();
@@ -520,9 +539,7 @@ var ReactiveClient = class {
520
539
  onError: options.onError
521
540
  });
522
541
  if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
523
- void this.ensureRegistered(shared, this.wsManager.generation).catch(
524
- () => void 0
525
- );
542
+ void this.ensureRegistered(shared, this.wsManager.generation).catch(() => void 0);
526
543
  }
527
544
  const state = shared;
528
545
  let released = false;
@@ -611,9 +628,7 @@ var ReactiveClient = class {
611
628
  registerActiveSubscriptions(socketGeneration) {
612
629
  for (const state of this.subscriptionsByKey.values()) {
613
630
  if (state.active) {
614
- void this.ensureRegistered(state, socketGeneration).catch(
615
- () => void 0
616
- );
631
+ void this.ensureRegistered(state, socketGeneration).catch(() => void 0);
617
632
  }
618
633
  }
619
634
  }
@@ -666,9 +681,7 @@ var ReactiveClient = class {
666
681
  })
667
682
  });
668
683
  if (!response.ok) {
669
- throw new Error(
670
- `Subscribe failed: ${response.status} ${response.statusText}`
671
- );
684
+ throw new Error(`Subscribe failed: ${response.status} ${response.statusText}`);
672
685
  }
673
686
  const result = await response.json();
674
687
  if (result.type === "error") {
@@ -687,12 +700,7 @@ var ReactiveClient = class {
687
700
  throw cause;
688
701
  } finally {
689
702
  if ((!state.active || !isCurrent()) && requestIssued) {
690
- void this.sendUnsubscribe(
691
- registrationConnectionId,
692
- state.id,
693
- generation,
694
- registrationId
695
- );
703
+ void this.sendUnsubscribe(registrationConnectionId, state.id, generation, registrationId);
696
704
  }
697
705
  if (state.registrationPromise === request) {
698
706
  state.registrationPromise = void 0;
@@ -797,15 +805,12 @@ var ReactiveClient = class {
797
805
  return;
798
806
  }
799
807
  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];
808
+ for (; ; ) {
809
+ const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion).at(0);
802
810
  if (!next) break;
803
811
  state.patchBuffer.delete(next.messageId);
804
812
  try {
805
- state.authoritativeData = applyPatches(
806
- state.authoritativeData,
807
- next.patches
808
- );
813
+ state.authoritativeData = applyPatches(state.authoritativeData, next.patches);
809
814
  state.version = next.toVersion;
810
815
  state.appliedMessageIds.add(next.messageId);
811
816
  const awaitingReconciliation = this.hasSettledLayers(state);
@@ -843,12 +848,7 @@ var ReactiveClient = class {
843
848
  state.stale = true;
844
849
  state.loading = true;
845
850
  this.notifySubscription(state);
846
- let request;
847
- request = this.ensureRegistered(
848
- state,
849
- this.wsManager.generation,
850
- true
851
- ).finally(() => {
851
+ const request = this.ensureRegistered(state, this.wsManager.generation, true).finally(() => {
852
852
  if (state.resyncPromise === request) {
853
853
  state.resyncPromise = void 0;
854
854
  state.resyncGeneration = void 0;
@@ -916,7 +916,10 @@ var ReactiveClient = class {
916
916
  if (options?.optimisticData === void 0) return void 0;
917
917
  return (data) => {
918
918
  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;
919
+ const optimistic = settled ? (
920
+ // oxlint-disable-next-line typescript/prefer-nullish-coalescing -- null is a valid mutation result.
921
+ result === void 0 ? typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData : result
922
+ ) : typeof options.optimisticData === "function" ? options.optimisticData(input, data) : options.optimisticData;
920
923
  if (settled && optimistic !== null && typeof optimistic === "object") {
921
924
  const optimisticId = optimistic.id;
922
925
  if (optimisticId !== void 0 && data.some(
@@ -931,12 +934,7 @@ var ReactiveClient = class {
931
934
  settleMutationLayers(mutationId, input, result, options, states) {
932
935
  for (const state of states) {
933
936
  const layer = state.optimisticLayers.get(mutationId);
934
- const updater = this.createOptimisticUpdater(
935
- input,
936
- options,
937
- result,
938
- true
939
- );
937
+ const updater = this.createOptimisticUpdater(input, options, result, true);
940
938
  if (!layer || !updater) continue;
941
939
  layer.updater = updater;
942
940
  layer.settledRevision = ++state.settlementRevision;
@@ -1013,6 +1011,7 @@ var DEFAULT_CLUSTER_HTTP_PATH = "/api/reactive";
1013
1011
  var DEFAULT_RETRY_DELAY_MS = 1e4;
1014
1012
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1015
1013
  var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
1014
+ var DEFAULT_CLUSTER_HEARTBEAT_MS = 3e4;
1016
1015
  function isRecord(value) {
1017
1016
  return typeof value === "object" && value !== null;
1018
1017
  }
@@ -1049,10 +1048,7 @@ function wait(milliseconds, signal) {
1049
1048
  }
1050
1049
  async function fetchClusterTicket(config) {
1051
1050
  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
- );
1051
+ const endpoint = new URL(config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH, config.baseUrl);
1056
1052
  const response = await (config.fetch ?? fetch)(endpoint, {
1057
1053
  method: "POST",
1058
1054
  headers: { authorization: `Bearer ${runnerToken}` },
@@ -1065,10 +1061,7 @@ async function fetchClusterTicket(config) {
1065
1061
  }
1066
1062
  async function connectClusterWorker(config) {
1067
1063
  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();
1064
+ const httpUrl = new URL(config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH, config.baseUrl).toString();
1072
1065
  for (; ; ) {
1073
1066
  if (config.signal?.aborted) throw abortError(config.signal);
1074
1067
  let client;
@@ -1077,6 +1070,7 @@ async function connectClusterWorker(config) {
1077
1070
  client = new ReactiveClient({
1078
1071
  reconnectDelay: retryDelay,
1079
1072
  maxReconnectAttempts: Number.MAX_SAFE_INTEGER,
1073
+ heartbeatInterval: DEFAULT_CLUSTER_HEARTBEAT_MS,
1080
1074
  ...config.client,
1081
1075
  url: ticket.url,
1082
1076
  httpUrl,
@@ -1120,7 +1114,11 @@ async function createSignedClusterTicket(options) {
1120
1114
  encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt }))
1121
1115
  );
1122
1116
  const signature = new Uint8Array(
1123
- await crypto.subtle.sign("HMAC", await hmacKey(options.secret, "sign"), encoder.encode(payload))
1117
+ await crypto.subtle.sign(
1118
+ "HMAC",
1119
+ await hmacKey(options.secret, "sign"),
1120
+ encoder.encode(payload)
1121
+ )
1124
1122
  );
1125
1123
  return parseClusterTicket({
1126
1124
  token: `${payload}.${base64UrlEncode(signature)}`,
@@ -1129,8 +1127,10 @@ async function createSignedClusterTicket(options) {
1129
1127
  });
1130
1128
  }
1131
1129
  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;
1130
+ const parts = token.split(".");
1131
+ if (parts.length !== 2) return void 0;
1132
+ const [payload, signature] = parts;
1133
+ if (!payload || !signature) return void 0;
1134
1134
  try {
1135
1135
  const valid = await crypto.subtle.verify(
1136
1136
  "HMAC",