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.
- package/README.md +93 -51
- package/dist/{client-Zb67giI5.d.ts → client-BPAct-Dz.d.ts} +19 -0
- package/dist/client.cjs +68 -55
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +68 -55
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.cjs +223 -36
- package/dist/cloudflare.cjs.map +1 -1
- package/dist/cloudflare.d.cts +78 -6
- package/dist/cloudflare.d.ts +78 -6
- package/dist/cloudflare.js +223 -36
- package/dist/cloudflare.js.map +1 -1
- package/dist/cluster.cjs +81 -66
- package/dist/cluster.cjs.map +1 -1
- package/dist/cluster.js +81 -66
- package/dist/cluster.js.map +1 -1
- package/dist/core.cjs +1 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +1 -3
- package/dist/core.js.map +1 -1
- package/dist/hub.cjs +35 -10
- package/dist/hub.cjs.map +1 -1
- package/dist/hub.d.cts +18 -3
- package/dist/hub.d.ts +18 -3
- package/dist/hub.js +35 -10
- package/dist/hub.js.map +1 -1
- package/dist/index.cjs +1 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/infra.cjs +19 -33
- package/dist/infra.cjs.map +1 -1
- package/dist/infra.js +19 -33
- package/dist/infra.js.map +1 -1
- package/dist/react.cjs +71 -65
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +71 -65
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +46 -139
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +46 -139
- package/dist/server.js.map +1 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
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
|
|
1133
|
-
if (
|
|
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",
|