dynamodb-reactive 0.1.21 → 0.2.1
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 +378 -153
- package/dist/client-r_tnzwqi.d.ts +224 -0
- package/dist/client.cjs +1023 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +6 -0
- package/dist/client.d.ts +4 -70
- package/dist/client.js +1013 -1
- package/dist/client.js.map +1 -1
- package/dist/core.cjs +232 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +4 -0
- package/dist/core.d.ts +3 -3
- package/dist/core.js +222 -1
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +232 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +103 -56
- package/dist/index.js +222 -1
- package/dist/index.js.map +1 -1
- package/dist/infra.cjs +623 -0
- package/dist/infra.cjs.map +1 -0
- package/dist/infra.d.cts +119 -0
- package/dist/infra.d.ts +50 -102
- package/dist/infra.js +371 -144
- package/dist/infra.js.map +1 -1
- package/dist/react.cjs +1250 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +68 -0
- package/dist/react.d.ts +68 -3
- package/dist/react.js +1237 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +2351 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +364 -0
- package/dist/server.d.ts +58 -313
- package/dist/server.js +1855 -1208
- package/dist/server.js.map +1 -1
- package/dist/{table-tN3D4Tm-.d.ts → table-BijJ73pX.d.ts} +2 -34
- package/dist/{types-ISMJfyIv.d.ts → types-LMLk8JVC.d.ts} +56 -30
- package/package.json +79 -41
- package/dist/chunk-2A7GJ2UO.js +0 -1088
- package/dist/chunk-2A7GJ2UO.js.map +0 -1
- package/dist/chunk-ZAJA4KBE.js +0 -150
- package/dist/chunk-ZAJA4KBE.js.map +0 -1
- package/dist/react-B2b5RIz_.d.ts +0 -549
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsonpatch = require('fast-json-patch');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var jsonpatch__default = /*#__PURE__*/_interopDefault(jsonpatch);
|
|
10
|
+
|
|
11
|
+
// ../client/src/react.tsx
|
|
12
|
+
|
|
13
|
+
// ../client/src/canonical.ts
|
|
14
|
+
function canonicalize(value, seen, key, arrayItem = false, applyToJSON = true) {
|
|
15
|
+
if (value === null) return "null";
|
|
16
|
+
switch (typeof value) {
|
|
17
|
+
case "string":
|
|
18
|
+
case "boolean":
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
case "number":
|
|
21
|
+
return Number.isFinite(value) ? JSON.stringify(value) : "null";
|
|
22
|
+
case "undefined":
|
|
23
|
+
return arrayItem ? "null" : void 0;
|
|
24
|
+
case "object": {
|
|
25
|
+
if (applyToJSON && "toJSON" in value && typeof value.toJSON === "function") {
|
|
26
|
+
return canonicalize(value.toJSON(key), seen, key, arrayItem, false);
|
|
27
|
+
}
|
|
28
|
+
if (seen.has(value)) {
|
|
29
|
+
throw new TypeError("Query input must not contain circular references");
|
|
30
|
+
}
|
|
31
|
+
seen.add(value);
|
|
32
|
+
try {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
const items = [];
|
|
35
|
+
for (let index = 0; index < value.length; index++) {
|
|
36
|
+
items.push(
|
|
37
|
+
canonicalize(value[index], seen, String(index), true) ?? "null"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return `[${items.join(",")}]`;
|
|
41
|
+
}
|
|
42
|
+
const object = value;
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const key2 of Object.keys(object).sort()) {
|
|
45
|
+
const item = canonicalize(object[key2], seen, key2);
|
|
46
|
+
if (item !== void 0) {
|
|
47
|
+
entries.push(`${JSON.stringify(key2)}:${item}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return `{${entries.join(",")}}`;
|
|
51
|
+
} finally {
|
|
52
|
+
seen.delete(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
throw new TypeError(`Unsupported query input value: ${typeof value}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getQueryKey(path, input) {
|
|
60
|
+
return `${JSON.stringify(path)}:${canonicalize(input, /* @__PURE__ */ new Set(), "") ?? "undefined"}`;
|
|
61
|
+
}
|
|
62
|
+
var { applyPatch } = jsonpatch__default.default;
|
|
63
|
+
function applyPatches(document2, patches) {
|
|
64
|
+
if (patches.length === 0) {
|
|
65
|
+
return document2;
|
|
66
|
+
}
|
|
67
|
+
const operations = patches.map((patch) => {
|
|
68
|
+
const op = {
|
|
69
|
+
op: patch.op,
|
|
70
|
+
path: patch.path
|
|
71
|
+
};
|
|
72
|
+
if ("value" in patch && patch.value !== void 0) {
|
|
73
|
+
op.value = patch.value;
|
|
74
|
+
}
|
|
75
|
+
if ("from" in patch && patch.from !== void 0) {
|
|
76
|
+
op.from = patch.from;
|
|
77
|
+
}
|
|
78
|
+
return op;
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const result = applyPatch(
|
|
82
|
+
structuredClone(document2),
|
|
83
|
+
operations,
|
|
84
|
+
true,
|
|
85
|
+
// Validate operations
|
|
86
|
+
false
|
|
87
|
+
// Don't mutate the original
|
|
88
|
+
);
|
|
89
|
+
return result.newDocument;
|
|
90
|
+
} catch (error2) {
|
|
91
|
+
throw new Error("Failed to apply patches", { cause: error2 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ../client/src/log.ts
|
|
96
|
+
function debug(...values) {
|
|
97
|
+
if (globalThis.__DYNAMODB_REACTIVE_DEBUG__) {
|
|
98
|
+
console.debug(...values);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function warn(...values) {
|
|
102
|
+
console.warn(...values);
|
|
103
|
+
}
|
|
104
|
+
function error(...values) {
|
|
105
|
+
console.error(...values);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ../client/src/websocket.ts
|
|
109
|
+
var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
110
|
+
function withDefaults(config) {
|
|
111
|
+
return {
|
|
112
|
+
autoReconnect: true,
|
|
113
|
+
connectionTimeout: 1e4,
|
|
114
|
+
handshakeTimeout: 1e4,
|
|
115
|
+
maxReconnectAttempts: 10,
|
|
116
|
+
reconnectDelay: 1e3,
|
|
117
|
+
...config
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var WebSocketManager = class {
|
|
121
|
+
config;
|
|
122
|
+
ws = null;
|
|
123
|
+
state = "disconnected";
|
|
124
|
+
reconnectAttempts = 0;
|
|
125
|
+
reconnectTimer = null;
|
|
126
|
+
handshakeTimer = null;
|
|
127
|
+
heartbeatTimer = null;
|
|
128
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
129
|
+
stateHandlers = /* @__PURE__ */ new Set();
|
|
130
|
+
handshakeHandlers = /* @__PURE__ */ new Set();
|
|
131
|
+
_connectionId = null;
|
|
132
|
+
connectionWaiters = /* @__PURE__ */ new Set();
|
|
133
|
+
socketGeneration = 0;
|
|
134
|
+
intentionalDisconnect = false;
|
|
135
|
+
constructor(config) {
|
|
136
|
+
this.config = withDefaults(config);
|
|
137
|
+
}
|
|
138
|
+
/** Apply settings used by future requests/events without replacing the socket. */
|
|
139
|
+
configure(config) {
|
|
140
|
+
this.config = withDefaults(config);
|
|
141
|
+
}
|
|
142
|
+
get connectionId() {
|
|
143
|
+
return this._connectionId;
|
|
144
|
+
}
|
|
145
|
+
get generation() {
|
|
146
|
+
return this.socketGeneration;
|
|
147
|
+
}
|
|
148
|
+
waitForConnectionId(options = {}) {
|
|
149
|
+
if (this._connectionId && this.state === "connected") {
|
|
150
|
+
return Promise.resolve(this._connectionId);
|
|
151
|
+
}
|
|
152
|
+
if (options.signal?.aborted) {
|
|
153
|
+
return Promise.reject(new Error("Connection wait aborted"));
|
|
154
|
+
}
|
|
155
|
+
const timeoutMs = options.timeoutMs ?? this.config.connectionTimeout;
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
let timeout;
|
|
158
|
+
const abort = () => finish(new Error("Connection wait aborted"));
|
|
159
|
+
const waiter = {
|
|
160
|
+
resolve,
|
|
161
|
+
reject,
|
|
162
|
+
cleanup: () => {
|
|
163
|
+
if (timeout) clearTimeout(timeout);
|
|
164
|
+
options.signal?.removeEventListener("abort", abort);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const finish = (failure) => {
|
|
168
|
+
if (!this.connectionWaiters.delete(waiter)) return;
|
|
169
|
+
waiter.cleanup();
|
|
170
|
+
reject(failure);
|
|
171
|
+
};
|
|
172
|
+
this.connectionWaiters.add(waiter);
|
|
173
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
174
|
+
timeout = setTimeout(
|
|
175
|
+
() => finish(new Error(`Connection timed out after ${timeoutMs}ms`)),
|
|
176
|
+
timeoutMs
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
getState() {
|
|
181
|
+
return this.state;
|
|
182
|
+
}
|
|
183
|
+
async connect() {
|
|
184
|
+
if (this.state === "connected") return;
|
|
185
|
+
if (this.state === "connecting") {
|
|
186
|
+
await this.waitForConnectionId();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.intentionalDisconnect = false;
|
|
190
|
+
const generation = ++this.socketGeneration;
|
|
191
|
+
this.setState("connecting");
|
|
192
|
+
let socket;
|
|
193
|
+
try {
|
|
194
|
+
const url = await this.buildUrl();
|
|
195
|
+
if (generation !== this.socketGeneration || this.intentionalDisconnect) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const nextSocket = new WebSocket(url);
|
|
199
|
+
socket = nextSocket;
|
|
200
|
+
this.ws = nextSocket;
|
|
201
|
+
this.startHandshakeTimer(nextSocket, generation);
|
|
202
|
+
nextSocket.onopen = () => {
|
|
203
|
+
if (!this.isCurrent(nextSocket, generation)) return;
|
|
204
|
+
nextSocket.send(JSON.stringify({ type: "init" }));
|
|
205
|
+
};
|
|
206
|
+
nextSocket.onclose = () => {
|
|
207
|
+
if (!this.isCurrent(nextSocket, generation)) return;
|
|
208
|
+
this.clearConnectionTimers();
|
|
209
|
+
this.ws = null;
|
|
210
|
+
this._connectionId = null;
|
|
211
|
+
this.setState("disconnected");
|
|
212
|
+
this.invokeLifecycleCallback(
|
|
213
|
+
"onDisconnect",
|
|
214
|
+
this.config.onDisconnect
|
|
215
|
+
);
|
|
216
|
+
if (this.config.autoReconnect && !this.intentionalDisconnect) {
|
|
217
|
+
this.scheduleReconnect();
|
|
218
|
+
} else {
|
|
219
|
+
this.rejectWaiters(new Error("WebSocket disconnected"));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
nextSocket.onerror = () => {
|
|
223
|
+
if (!this.isCurrent(nextSocket, generation)) return;
|
|
224
|
+
this.invokeLifecycleCallback(
|
|
225
|
+
"onError",
|
|
226
|
+
this.config.onError,
|
|
227
|
+
new Error("WebSocket error")
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
nextSocket.onmessage = (event) => {
|
|
231
|
+
if (!this.isCurrent(nextSocket, generation)) return;
|
|
232
|
+
this.receiveMessage(nextSocket, generation, event.data);
|
|
233
|
+
};
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
if (generation !== this.socketGeneration) return;
|
|
236
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
237
|
+
this.clearConnectionTimers();
|
|
238
|
+
if (socket && this.ws === socket) socket.close();
|
|
239
|
+
this.ws = null;
|
|
240
|
+
this._connectionId = null;
|
|
241
|
+
this.setState("disconnected");
|
|
242
|
+
this.invokeLifecycleCallback("onError", this.config.onError, failure);
|
|
243
|
+
if (this.config.autoReconnect && !this.intentionalDisconnect) {
|
|
244
|
+
this.scheduleReconnect();
|
|
245
|
+
await this.waitForConnectionId();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.rejectWaiters(failure);
|
|
249
|
+
throw failure;
|
|
250
|
+
}
|
|
251
|
+
await this.waitForConnectionId();
|
|
252
|
+
}
|
|
253
|
+
disconnect() {
|
|
254
|
+
this.intentionalDisconnect = true;
|
|
255
|
+
++this.socketGeneration;
|
|
256
|
+
this.clearTimers();
|
|
257
|
+
const socket = this.ws;
|
|
258
|
+
this.ws = null;
|
|
259
|
+
socket?.close(1e3, "Client disconnect");
|
|
260
|
+
this._connectionId = null;
|
|
261
|
+
this.setState("disconnected");
|
|
262
|
+
this.rejectWaiters(new Error("WebSocket disconnected"));
|
|
263
|
+
}
|
|
264
|
+
forceReconnect() {
|
|
265
|
+
this.intentionalDisconnect = false;
|
|
266
|
+
++this.socketGeneration;
|
|
267
|
+
this.clearTimers();
|
|
268
|
+
const socket = this.ws;
|
|
269
|
+
this.ws = null;
|
|
270
|
+
socket?.close(1e3, "Force reconnect");
|
|
271
|
+
this._connectionId = null;
|
|
272
|
+
this.reconnectAttempts = 0;
|
|
273
|
+
this.setState("disconnected");
|
|
274
|
+
void this.connect().catch(() => void 0);
|
|
275
|
+
}
|
|
276
|
+
onMessage(handler) {
|
|
277
|
+
this.messageHandlers.add(handler);
|
|
278
|
+
return () => this.messageHandlers.delete(handler);
|
|
279
|
+
}
|
|
280
|
+
onStateChange(handler) {
|
|
281
|
+
this.stateHandlers.add(handler);
|
|
282
|
+
return () => this.stateHandlers.delete(handler);
|
|
283
|
+
}
|
|
284
|
+
onHandshake(handler) {
|
|
285
|
+
this.handshakeHandlers.add(handler);
|
|
286
|
+
return () => this.handshakeHandlers.delete(handler);
|
|
287
|
+
}
|
|
288
|
+
async buildUrl() {
|
|
289
|
+
let url = this.config.url;
|
|
290
|
+
if (!this.config.auth) return url;
|
|
291
|
+
const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
|
|
292
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
293
|
+
url = `${url}${separator}token=${encodeURIComponent(token)}`;
|
|
294
|
+
return url;
|
|
295
|
+
}
|
|
296
|
+
receiveMessage(socket, generation, rawData) {
|
|
297
|
+
try {
|
|
298
|
+
const message = JSON.parse(String(rawData));
|
|
299
|
+
if (message.type === "connected") {
|
|
300
|
+
if (this._connectionId) return;
|
|
301
|
+
this._connectionId = message.connectionId;
|
|
302
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (message.type === "pong") {
|
|
306
|
+
if (!this._connectionId || this.state === "connected") return;
|
|
307
|
+
this.clearHandshakeTimer();
|
|
308
|
+
this.reconnectAttempts = 0;
|
|
309
|
+
this.setState("connected");
|
|
310
|
+
this.startHeartbeat(socket, generation);
|
|
311
|
+
this.invokeLifecycleCallback("onConnect", this.config.onConnect);
|
|
312
|
+
this.resolveWaiters(this._connectionId);
|
|
313
|
+
for (const handler of this.handshakeHandlers) handler(generation);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (const handler of this.messageHandlers) handler(message);
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
error("Failed to parse WebSocket message:", cause);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
startHandshakeTimer(socket, generation) {
|
|
322
|
+
this.clearHandshakeTimer();
|
|
323
|
+
this.handshakeTimer = setTimeout(() => {
|
|
324
|
+
if (!this.isCurrent(socket, generation) || this.state === "connected") {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const failure = new Error("WebSocket handshake timed out");
|
|
328
|
+
this.invokeLifecycleCallback("onError", this.config.onError, failure);
|
|
329
|
+
socket.close(4e3, "Handshake timeout");
|
|
330
|
+
}, this.config.handshakeTimeout);
|
|
331
|
+
}
|
|
332
|
+
startHeartbeat(socket, generation) {
|
|
333
|
+
this.clearHeartbeatTimer();
|
|
334
|
+
this.heartbeatTimer = setInterval(() => {
|
|
335
|
+
if (!this.isCurrent(socket, generation) || this.state !== "connected" || socket.readyState !== WebSocket.OPEN) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
339
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
340
|
+
}
|
|
341
|
+
isCurrent(socket, generation) {
|
|
342
|
+
return this.ws === socket && this.socketGeneration === generation;
|
|
343
|
+
}
|
|
344
|
+
setState(state) {
|
|
345
|
+
if (this.state === state) return;
|
|
346
|
+
this.state = state;
|
|
347
|
+
for (const handler of this.stateHandlers) handler(state);
|
|
348
|
+
}
|
|
349
|
+
scheduleReconnect() {
|
|
350
|
+
if (this.reconnectTimer || this.intentionalDisconnect) return;
|
|
351
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
352
|
+
const failure = new Error("Max reconnection attempts reached");
|
|
353
|
+
error(failure.message);
|
|
354
|
+
this.setState("disconnected");
|
|
355
|
+
this.rejectWaiters(failure);
|
|
356
|
+
this.invokeLifecycleCallback("onError", this.config.onError, failure);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.setState("reconnecting");
|
|
360
|
+
this.reconnectAttempts++;
|
|
361
|
+
const delay = Math.min(
|
|
362
|
+
this.config.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
|
|
363
|
+
3e4
|
|
364
|
+
) * (0.5 + Math.random() * 0.5);
|
|
365
|
+
this.reconnectTimer = setTimeout(() => {
|
|
366
|
+
this.reconnectTimer = null;
|
|
367
|
+
void this.connect().catch(() => void 0);
|
|
368
|
+
}, delay);
|
|
369
|
+
}
|
|
370
|
+
resolveWaiters(connectionId) {
|
|
371
|
+
const waiters = [...this.connectionWaiters];
|
|
372
|
+
this.connectionWaiters.clear();
|
|
373
|
+
for (const waiter of waiters) {
|
|
374
|
+
waiter.cleanup();
|
|
375
|
+
waiter.resolve(connectionId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
invokeLifecycleCallback(name, callback, ...args) {
|
|
379
|
+
if (!callback) return;
|
|
380
|
+
try {
|
|
381
|
+
callback(...args);
|
|
382
|
+
} catch (cause) {
|
|
383
|
+
try {
|
|
384
|
+
error(`[dynamodb-reactive] ${name} callback failed:`, cause);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
rejectWaiters(failure) {
|
|
390
|
+
const waiters = [...this.connectionWaiters];
|
|
391
|
+
this.connectionWaiters.clear();
|
|
392
|
+
for (const waiter of waiters) {
|
|
393
|
+
waiter.cleanup();
|
|
394
|
+
waiter.reject(failure);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
clearHandshakeTimer() {
|
|
398
|
+
if (!this.handshakeTimer) return;
|
|
399
|
+
clearTimeout(this.handshakeTimer);
|
|
400
|
+
this.handshakeTimer = null;
|
|
401
|
+
}
|
|
402
|
+
clearHeartbeatTimer() {
|
|
403
|
+
if (!this.heartbeatTimer) return;
|
|
404
|
+
clearInterval(this.heartbeatTimer);
|
|
405
|
+
this.heartbeatTimer = null;
|
|
406
|
+
}
|
|
407
|
+
clearConnectionTimers() {
|
|
408
|
+
this.clearHandshakeTimer();
|
|
409
|
+
this.clearHeartbeatTimer();
|
|
410
|
+
}
|
|
411
|
+
clearTimers() {
|
|
412
|
+
this.clearConnectionTimers();
|
|
413
|
+
if (!this.reconnectTimer) return;
|
|
414
|
+
clearTimeout(this.reconnectTimer);
|
|
415
|
+
this.reconnectTimer = null;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// ../client/src/client.ts
|
|
420
|
+
var DEFAULT_HTTP_URL = "/api/reactive";
|
|
421
|
+
function generateId() {
|
|
422
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
423
|
+
}
|
|
424
|
+
var ReactiveClient = class {
|
|
425
|
+
wsManager;
|
|
426
|
+
httpUrl;
|
|
427
|
+
authGetter;
|
|
428
|
+
headersGetter;
|
|
429
|
+
subscriptionsById = /* @__PURE__ */ new Map();
|
|
430
|
+
subscriptionsByKey = /* @__PURE__ */ new Map();
|
|
431
|
+
connectionState = "disconnected";
|
|
432
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
433
|
+
constructor(config) {
|
|
434
|
+
this.wsManager = new WebSocketManager(config);
|
|
435
|
+
this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
|
|
436
|
+
this.setAuth(config.auth);
|
|
437
|
+
this.setInitialHeaders(config.headers);
|
|
438
|
+
this.wsManager.onMessage((message) => this.handleMessage(message));
|
|
439
|
+
this.wsManager.onStateChange((state) => {
|
|
440
|
+
this.connectionState = state;
|
|
441
|
+
this.notifyStateListeners();
|
|
442
|
+
if (state === "disconnected" || state === "reconnecting") {
|
|
443
|
+
this.markSubscriptionsStale();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
this.wsManager.onHandshake((socketGeneration) => {
|
|
447
|
+
this.registerActiveSubscriptions(socketGeneration);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/** Apply live configuration without replacing subscription caches or the socket. */
|
|
451
|
+
configure(config) {
|
|
452
|
+
this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
|
|
453
|
+
this.authGetter = this.toAuthGetter(config.auth);
|
|
454
|
+
this.headersGetter = this.toHeadersGetter(config.headers);
|
|
455
|
+
this.wsManager.configure(config);
|
|
456
|
+
}
|
|
457
|
+
async connect() {
|
|
458
|
+
await this.wsManager.connect();
|
|
459
|
+
}
|
|
460
|
+
disconnect() {
|
|
461
|
+
this.wsManager.disconnect();
|
|
462
|
+
}
|
|
463
|
+
forceReconnect() {
|
|
464
|
+
this.wsManager.forceReconnect();
|
|
465
|
+
}
|
|
466
|
+
getConnectionState() {
|
|
467
|
+
return this.connectionState;
|
|
468
|
+
}
|
|
469
|
+
onConnectionStateChange(listener) {
|
|
470
|
+
this.stateListeners.add(listener);
|
|
471
|
+
return () => this.stateListeners.delete(listener);
|
|
472
|
+
}
|
|
473
|
+
subscribe(path, options = {}) {
|
|
474
|
+
const key = getQueryKey(path, options.input);
|
|
475
|
+
let shared = this.subscriptionsByKey.get(key);
|
|
476
|
+
if (!shared) {
|
|
477
|
+
shared = {
|
|
478
|
+
id: generateId(),
|
|
479
|
+
key,
|
|
480
|
+
path,
|
|
481
|
+
input: options.input,
|
|
482
|
+
authoritativeData: void 0,
|
|
483
|
+
data: void 0,
|
|
484
|
+
loading: true,
|
|
485
|
+
stale: true,
|
|
486
|
+
error: void 0,
|
|
487
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
488
|
+
observers: /* @__PURE__ */ new Map(),
|
|
489
|
+
active: true,
|
|
490
|
+
hasRegistered: false,
|
|
491
|
+
generation: 0,
|
|
492
|
+
registrationId: void 0,
|
|
493
|
+
registrationConnectionId: void 0,
|
|
494
|
+
registrationSocketGeneration: void 0,
|
|
495
|
+
registrationPromise: void 0,
|
|
496
|
+
version: void 0,
|
|
497
|
+
patchBuffer: /* @__PURE__ */ new Map(),
|
|
498
|
+
appliedMessageIds: /* @__PURE__ */ new Set(),
|
|
499
|
+
resyncPromise: void 0,
|
|
500
|
+
resyncGeneration: void 0,
|
|
501
|
+
optimisticLayers: /* @__PURE__ */ new Map(),
|
|
502
|
+
settlementRevision: 0,
|
|
503
|
+
registrationSettlementRevision: 0,
|
|
504
|
+
revision: 0
|
|
505
|
+
};
|
|
506
|
+
this.subscriptionsByKey.set(key, shared);
|
|
507
|
+
this.subscriptionsById.set(shared.id, shared);
|
|
508
|
+
if (this.connectionState === "connected") {
|
|
509
|
+
void this.ensureRegistered(
|
|
510
|
+
shared,
|
|
511
|
+
this.wsManager.generation
|
|
512
|
+
).catch(() => void 0);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const leaseId = generateId();
|
|
516
|
+
shared.observers.set(leaseId, {
|
|
517
|
+
onData: options.onData ? (data) => options.onData?.(data) : void 0,
|
|
518
|
+
onError: options.onError
|
|
519
|
+
});
|
|
520
|
+
if (this.connectionState === "connected" && (!shared.hasRegistered || shared.stale) && !shared.registrationPromise) {
|
|
521
|
+
void this.ensureRegistered(shared, this.wsManager.generation).catch(
|
|
522
|
+
() => void 0
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const state = shared;
|
|
526
|
+
let released = false;
|
|
527
|
+
return {
|
|
528
|
+
id: state.id,
|
|
529
|
+
key: state.key,
|
|
530
|
+
get data() {
|
|
531
|
+
return state.data;
|
|
532
|
+
},
|
|
533
|
+
get loading() {
|
|
534
|
+
return state.loading;
|
|
535
|
+
},
|
|
536
|
+
get stale() {
|
|
537
|
+
return state.stale;
|
|
538
|
+
},
|
|
539
|
+
get error() {
|
|
540
|
+
return state.error;
|
|
541
|
+
},
|
|
542
|
+
unsubscribe: () => {
|
|
543
|
+
if (released) return;
|
|
544
|
+
released = true;
|
|
545
|
+
this.releaseSubscription(state, leaseId);
|
|
546
|
+
},
|
|
547
|
+
refetch: () => this.refetch(state)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async call(path, input, options) {
|
|
551
|
+
const mutationId = generateId();
|
|
552
|
+
const layers = [];
|
|
553
|
+
try {
|
|
554
|
+
this.createMutationLayers(mutationId, input, options, layers);
|
|
555
|
+
const connectionId = await this.wsManager.waitForConnectionId();
|
|
556
|
+
const headers = await this.buildHeaders();
|
|
557
|
+
const response = await fetch(this.httpUrl, {
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers,
|
|
560
|
+
body: JSON.stringify({ type: "call", connectionId, path, input })
|
|
561
|
+
});
|
|
562
|
+
if (!response.ok) {
|
|
563
|
+
throw new Error(`Call failed: ${response.status} ${response.statusText}`);
|
|
564
|
+
}
|
|
565
|
+
const result = await response.json();
|
|
566
|
+
if (result.type === "error") {
|
|
567
|
+
throw new Error(result.message ?? "Unknown error");
|
|
568
|
+
}
|
|
569
|
+
const data = result.data;
|
|
570
|
+
this.settleMutationLayers(mutationId, input, data, options, layers);
|
|
571
|
+
return data;
|
|
572
|
+
} catch (cause) {
|
|
573
|
+
this.removeMutationLayers(mutationId, layers);
|
|
574
|
+
throw cause;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
addSubscriptionListener(id, listener) {
|
|
578
|
+
const state = this.subscriptionsById.get(id);
|
|
579
|
+
if (!state) return () => void 0;
|
|
580
|
+
state.listeners.add(listener);
|
|
581
|
+
return () => state.listeners.delete(listener);
|
|
582
|
+
}
|
|
583
|
+
getSubscriptionRevision(id) {
|
|
584
|
+
return this.subscriptionsById.get(id)?.revision ?? -1;
|
|
585
|
+
}
|
|
586
|
+
applyOptimisticUpdate(subscriptionId, updater) {
|
|
587
|
+
const state = this.subscriptionsById.get(subscriptionId);
|
|
588
|
+
if (!state || state.data === void 0) return;
|
|
589
|
+
state.optimisticLayers.set(`local-${generateId()}`, {
|
|
590
|
+
updater
|
|
591
|
+
});
|
|
592
|
+
this.recomputeData(state);
|
|
593
|
+
this.notifySubscription(state, true);
|
|
594
|
+
}
|
|
595
|
+
setAuth(auth) {
|
|
596
|
+
this.authGetter = this.toAuthGetter(auth);
|
|
597
|
+
}
|
|
598
|
+
setInitialHeaders(headers) {
|
|
599
|
+
this.headersGetter = this.toHeadersGetter(headers);
|
|
600
|
+
}
|
|
601
|
+
toAuthGetter(auth) {
|
|
602
|
+
if (!auth) return void 0;
|
|
603
|
+
return typeof auth === "function" ? auth : () => auth;
|
|
604
|
+
}
|
|
605
|
+
toHeadersGetter(headers) {
|
|
606
|
+
if (!headers) return void 0;
|
|
607
|
+
return typeof headers === "function" ? headers : () => headers;
|
|
608
|
+
}
|
|
609
|
+
registerActiveSubscriptions(socketGeneration) {
|
|
610
|
+
for (const state of this.subscriptionsByKey.values()) {
|
|
611
|
+
if (state.active) {
|
|
612
|
+
void this.ensureRegistered(state, socketGeneration).catch(
|
|
613
|
+
() => void 0
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
ensureRegistered(state, socketGeneration, force = false) {
|
|
619
|
+
if (!force && state.registrationSocketGeneration === socketGeneration && state.registrationPromise) {
|
|
620
|
+
return state.registrationPromise;
|
|
621
|
+
}
|
|
622
|
+
state.generation++;
|
|
623
|
+
state.registrationSettlementRevision = state.settlementRevision;
|
|
624
|
+
state.registrationId = generateId();
|
|
625
|
+
state.registrationSocketGeneration = socketGeneration;
|
|
626
|
+
state.registrationConnectionId = void 0;
|
|
627
|
+
state.registrationPromise = void 0;
|
|
628
|
+
state.resyncPromise = void 0;
|
|
629
|
+
state.resyncGeneration = void 0;
|
|
630
|
+
state.hasRegistered = false;
|
|
631
|
+
state.version = void 0;
|
|
632
|
+
state.patchBuffer.clear();
|
|
633
|
+
state.appliedMessageIds.clear();
|
|
634
|
+
state.loading = true;
|
|
635
|
+
state.stale = true;
|
|
636
|
+
state.error = void 0;
|
|
637
|
+
this.notifySubscription(state);
|
|
638
|
+
const generation = state.generation;
|
|
639
|
+
const registrationId = state.registrationId;
|
|
640
|
+
const isCurrent = () => state.active && state.generation === generation && state.registrationId === registrationId && state.registrationSocketGeneration === socketGeneration && this.wsManager.generation === socketGeneration;
|
|
641
|
+
let requestIssued = false;
|
|
642
|
+
let registrationConnectionId;
|
|
643
|
+
let request;
|
|
644
|
+
request = (async () => {
|
|
645
|
+
try {
|
|
646
|
+
const connectionId = await this.wsManager.waitForConnectionId();
|
|
647
|
+
if (!isCurrent()) return;
|
|
648
|
+
registrationConnectionId = connectionId;
|
|
649
|
+
state.registrationConnectionId = connectionId;
|
|
650
|
+
const headers = await this.buildHeaders();
|
|
651
|
+
if (!isCurrent()) return;
|
|
652
|
+
requestIssued = true;
|
|
653
|
+
const response = await fetch(this.httpUrl, {
|
|
654
|
+
method: "POST",
|
|
655
|
+
headers,
|
|
656
|
+
body: JSON.stringify({
|
|
657
|
+
type: "subscribe",
|
|
658
|
+
connectionId,
|
|
659
|
+
subscriptionId: state.id,
|
|
660
|
+
generation,
|
|
661
|
+
registrationId,
|
|
662
|
+
path: state.path,
|
|
663
|
+
input: state.input
|
|
664
|
+
})
|
|
665
|
+
});
|
|
666
|
+
if (!response.ok) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Subscribe failed: ${response.status} ${response.statusText}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const result = await response.json();
|
|
672
|
+
if (result.type === "error") {
|
|
673
|
+
throw new Error(result.message);
|
|
674
|
+
}
|
|
675
|
+
if (!isCurrent()) return;
|
|
676
|
+
this.handleSnapshot(result);
|
|
677
|
+
if (isCurrent()) state.hasRegistered = true;
|
|
678
|
+
} catch (cause) {
|
|
679
|
+
if (isCurrent()) {
|
|
680
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
681
|
+
state.error = failure;
|
|
682
|
+
state.loading = this.hasSettledLayers(state);
|
|
683
|
+
this.notifySubscription(state, false, failure);
|
|
684
|
+
}
|
|
685
|
+
throw cause;
|
|
686
|
+
} finally {
|
|
687
|
+
if ((!state.active || !isCurrent()) && requestIssued) {
|
|
688
|
+
void this.sendUnsubscribe(
|
|
689
|
+
registrationConnectionId,
|
|
690
|
+
state.id,
|
|
691
|
+
generation,
|
|
692
|
+
registrationId
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (state.registrationPromise === request) {
|
|
696
|
+
state.registrationPromise = void 0;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
})();
|
|
700
|
+
state.registrationPromise = request;
|
|
701
|
+
return request;
|
|
702
|
+
}
|
|
703
|
+
releaseSubscription(state, leaseId) {
|
|
704
|
+
state.observers.delete(leaseId);
|
|
705
|
+
if (state.observers.size > 0) return;
|
|
706
|
+
state.active = false;
|
|
707
|
+
this.subscriptionsById.delete(state.id);
|
|
708
|
+
this.subscriptionsByKey.delete(state.key);
|
|
709
|
+
if (state.registrationId && state.registrationConnectionId && state.hasRegistered) {
|
|
710
|
+
void this.sendUnsubscribe(
|
|
711
|
+
state.registrationConnectionId,
|
|
712
|
+
state.id,
|
|
713
|
+
state.generation,
|
|
714
|
+
state.registrationId
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async sendUnsubscribe(connectionId, subscriptionId, generation, registrationId) {
|
|
719
|
+
if (!connectionId) return;
|
|
720
|
+
try {
|
|
721
|
+
const headers = await this.buildHeaders();
|
|
722
|
+
await fetch(this.httpUrl, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers,
|
|
725
|
+
body: JSON.stringify({
|
|
726
|
+
type: "unsubscribe",
|
|
727
|
+
connectionId,
|
|
728
|
+
subscriptionId,
|
|
729
|
+
generation,
|
|
730
|
+
registrationId
|
|
731
|
+
})
|
|
732
|
+
});
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async refetch(state) {
|
|
737
|
+
if (!state.active) return;
|
|
738
|
+
if (!state.registrationId) {
|
|
739
|
+
await this.ensureRegistered(state, this.wsManager.generation);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
await this.requestResync(state);
|
|
743
|
+
}
|
|
744
|
+
handleMessage(message) {
|
|
745
|
+
switch (message.type) {
|
|
746
|
+
case "patch":
|
|
747
|
+
this.handlePatch(message);
|
|
748
|
+
break;
|
|
749
|
+
case "resync": {
|
|
750
|
+
const state = this.subscriptionsById.get(message.subscriptionId);
|
|
751
|
+
if (state?.generation === message.generation) {
|
|
752
|
+
void this.requestResync(state).catch(() => void 0);
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
handleSnapshot(message) {
|
|
759
|
+
const state = this.subscriptionsById.get(message.subscriptionId);
|
|
760
|
+
if (!state || message.generation !== state.generation) return;
|
|
761
|
+
if (state.version !== void 0 && message.version <= state.version) return;
|
|
762
|
+
state.authoritativeData = message.data;
|
|
763
|
+
state.version = message.version;
|
|
764
|
+
state.error = void 0;
|
|
765
|
+
const coveredRevision = state.registrationSettlementRevision;
|
|
766
|
+
for (const [layerId, layer] of state.optimisticLayers) {
|
|
767
|
+
if (layer.settledRevision !== void 0 && layer.settledRevision <= coveredRevision) {
|
|
768
|
+
state.optimisticLayers.delete(layerId);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const awaitingReconciliation = this.hasSettledLayers(state);
|
|
772
|
+
state.loading = awaitingReconciliation;
|
|
773
|
+
state.stale = awaitingReconciliation;
|
|
774
|
+
this.recomputeData(state);
|
|
775
|
+
this.discardStalePatches(state);
|
|
776
|
+
this.notifySubscription(state, true);
|
|
777
|
+
this.drainPatchBuffer(state);
|
|
778
|
+
}
|
|
779
|
+
handlePatch(message) {
|
|
780
|
+
const state = this.subscriptionsById.get(message.subscriptionId);
|
|
781
|
+
if (!state || message.generation !== state.generation) return;
|
|
782
|
+
if (state.appliedMessageIds.has(message.messageId) || state.patchBuffer.has(message.messageId)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (state.version !== void 0 && message.toVersion <= state.version) {
|
|
786
|
+
state.appliedMessageIds.add(message.messageId);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
state.patchBuffer.set(message.messageId, message);
|
|
790
|
+
if (state.version === void 0) return;
|
|
791
|
+
this.drainPatchBuffer(state);
|
|
792
|
+
}
|
|
793
|
+
drainPatchBuffer(state) {
|
|
794
|
+
if (state.version === void 0 || state.authoritativeData === void 0) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.discardStalePatches(state);
|
|
798
|
+
while (true) {
|
|
799
|
+
const next = [...state.patchBuffer.values()].filter((patch) => patch.fromVersion === state.version).sort((left, right) => left.toVersion - right.toVersion)[0];
|
|
800
|
+
if (!next) break;
|
|
801
|
+
state.patchBuffer.delete(next.messageId);
|
|
802
|
+
try {
|
|
803
|
+
state.authoritativeData = applyPatches(
|
|
804
|
+
state.authoritativeData,
|
|
805
|
+
next.patches
|
|
806
|
+
);
|
|
807
|
+
state.version = next.toVersion;
|
|
808
|
+
state.appliedMessageIds.add(next.messageId);
|
|
809
|
+
const awaitingReconciliation = this.hasSettledLayers(state);
|
|
810
|
+
state.loading = awaitingReconciliation;
|
|
811
|
+
state.stale = awaitingReconciliation;
|
|
812
|
+
state.error = void 0;
|
|
813
|
+
this.recomputeData(state);
|
|
814
|
+
this.notifySubscription(state, true);
|
|
815
|
+
this.discardStalePatches(state);
|
|
816
|
+
} catch {
|
|
817
|
+
void this.requestResync(state).catch(() => void 0);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if ([...state.patchBuffer.values()].some(
|
|
822
|
+
(patch) => patch.toVersion > state.version && patch.fromVersion !== state.version
|
|
823
|
+
)) {
|
|
824
|
+
void this.requestResync(state).catch(() => void 0);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
discardStalePatches(state) {
|
|
828
|
+
if (state.version === void 0) return;
|
|
829
|
+
for (const [messageId, patch] of state.patchBuffer) {
|
|
830
|
+
if (patch.toVersion <= state.version) {
|
|
831
|
+
state.patchBuffer.delete(messageId);
|
|
832
|
+
state.appliedMessageIds.add(messageId);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
requestResync(state) {
|
|
837
|
+
if (state.resyncPromise && state.resyncGeneration === state.generation) {
|
|
838
|
+
return state.resyncPromise;
|
|
839
|
+
}
|
|
840
|
+
if (!state.active || !state.registrationId) return Promise.resolve();
|
|
841
|
+
state.stale = true;
|
|
842
|
+
state.loading = true;
|
|
843
|
+
this.notifySubscription(state);
|
|
844
|
+
let request;
|
|
845
|
+
request = this.ensureRegistered(
|
|
846
|
+
state,
|
|
847
|
+
this.wsManager.generation,
|
|
848
|
+
true
|
|
849
|
+
).finally(() => {
|
|
850
|
+
if (state.resyncPromise === request) {
|
|
851
|
+
state.resyncPromise = void 0;
|
|
852
|
+
state.resyncGeneration = void 0;
|
|
853
|
+
}
|
|
854
|
+
}).then(() => {
|
|
855
|
+
if (state.active && this.hasSettledLayers(state)) {
|
|
856
|
+
return this.requestResync(state);
|
|
857
|
+
}
|
|
858
|
+
return void 0;
|
|
859
|
+
});
|
|
860
|
+
state.resyncPromise = request;
|
|
861
|
+
state.resyncGeneration = state.generation;
|
|
862
|
+
return request;
|
|
863
|
+
}
|
|
864
|
+
createMutationLayers(mutationId, input, options, layered) {
|
|
865
|
+
const targets = this.findMutationTargets(options);
|
|
866
|
+
for (const state of targets) {
|
|
867
|
+
const updater = this.createOptimisticUpdater(input, options);
|
|
868
|
+
if (!updater || state.authoritativeData === void 0) continue;
|
|
869
|
+
state.optimisticLayers.set(mutationId, {
|
|
870
|
+
updater
|
|
871
|
+
});
|
|
872
|
+
layered.push(state);
|
|
873
|
+
this.recomputeData(state);
|
|
874
|
+
this.notifySubscription(state, true);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
findMutationTargets(options) {
|
|
878
|
+
if (options?.queryKey) {
|
|
879
|
+
const exact = this.subscriptionsByKey.get(options.queryKey);
|
|
880
|
+
return exact ? [exact] : [];
|
|
881
|
+
}
|
|
882
|
+
if (typeof options?.invalidates === "object") {
|
|
883
|
+
const exact = this.subscriptionsByKey.get(
|
|
884
|
+
getQueryKey(options.invalidates.path, options.invalidates.input)
|
|
885
|
+
);
|
|
886
|
+
return exact ? [exact] : [];
|
|
887
|
+
}
|
|
888
|
+
if (typeof options?.invalidates === "string") {
|
|
889
|
+
return [...this.subscriptionsByKey.values()].filter(
|
|
890
|
+
(state) => state.path === options.invalidates
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
createOptimisticUpdater(input, options, result, settled = false) {
|
|
896
|
+
const update = options?.optimisticUpdate;
|
|
897
|
+
if (typeof update === "function") {
|
|
898
|
+
return (data) => Array.isArray(data) ? update(data, input, result) : data;
|
|
899
|
+
}
|
|
900
|
+
const itemId = typeof input === "object" && input !== null ? input.id : void 0;
|
|
901
|
+
if (update === "remove" && itemId !== void 0) {
|
|
902
|
+
return (data) => Array.isArray(data) ? data.filter((item) => item.id !== itemId) : data;
|
|
903
|
+
}
|
|
904
|
+
if (update !== "merge") return void 0;
|
|
905
|
+
if (itemId !== void 0) {
|
|
906
|
+
const resultFields = settled && typeof result === "object" && result !== null ? result : {};
|
|
907
|
+
return (data) => {
|
|
908
|
+
if (!Array.isArray(data)) return data;
|
|
909
|
+
return data.map(
|
|
910
|
+
(item) => item.id === itemId ? { ...item, ...input, ...resultFields } : item
|
|
911
|
+
);
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (options?.optimisticData === void 0) return void 0;
|
|
915
|
+
return (data) => {
|
|
916
|
+
if (!Array.isArray(data)) return data;
|
|
917
|
+
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;
|
|
918
|
+
if (settled && optimistic !== null && typeof optimistic === "object") {
|
|
919
|
+
const optimisticId = optimistic.id;
|
|
920
|
+
if (optimisticId !== void 0 && data.some(
|
|
921
|
+
(item) => typeof item === "object" && item !== null && item.id === optimisticId
|
|
922
|
+
)) {
|
|
923
|
+
return data;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return optimistic === void 0 ? data : [optimistic, ...data];
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
settleMutationLayers(mutationId, input, result, options, states) {
|
|
930
|
+
for (const state of states) {
|
|
931
|
+
const layer = state.optimisticLayers.get(mutationId);
|
|
932
|
+
const updater = this.createOptimisticUpdater(
|
|
933
|
+
input,
|
|
934
|
+
options,
|
|
935
|
+
result,
|
|
936
|
+
true
|
|
937
|
+
);
|
|
938
|
+
if (!layer || !updater) continue;
|
|
939
|
+
layer.updater = updater;
|
|
940
|
+
layer.settledRevision = ++state.settlementRevision;
|
|
941
|
+
state.loading = true;
|
|
942
|
+
state.stale = true;
|
|
943
|
+
this.recomputeData(state);
|
|
944
|
+
this.notifySubscription(state, true);
|
|
945
|
+
void this.requestResync(state).catch(() => void 0);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
removeMutationLayers(mutationId, states) {
|
|
949
|
+
for (const state of states) {
|
|
950
|
+
if (!state.optimisticLayers.delete(mutationId)) continue;
|
|
951
|
+
this.recomputeData(state);
|
|
952
|
+
this.notifySubscription(state, true);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
recomputeData(state) {
|
|
956
|
+
let data = state.authoritativeData;
|
|
957
|
+
for (const layer of state.optimisticLayers.values()) {
|
|
958
|
+
data = layer.updater(data);
|
|
959
|
+
}
|
|
960
|
+
state.data = data;
|
|
961
|
+
}
|
|
962
|
+
hasSettledLayers(state) {
|
|
963
|
+
for (const layer of state.optimisticLayers.values()) {
|
|
964
|
+
if (layer.settledRevision !== void 0) return true;
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
async buildHeaders() {
|
|
969
|
+
const headers = {
|
|
970
|
+
"Content-Type": "application/json"
|
|
971
|
+
};
|
|
972
|
+
if (this.headersGetter) Object.assign(headers, await this.headersGetter());
|
|
973
|
+
if (this.authGetter) {
|
|
974
|
+
headers["Authorization"] = `Bearer ${await this.authGetter()}`;
|
|
975
|
+
}
|
|
976
|
+
return headers;
|
|
977
|
+
}
|
|
978
|
+
markSubscriptionsStale() {
|
|
979
|
+
for (const state of this.subscriptionsByKey.values()) {
|
|
980
|
+
if (!state.active) continue;
|
|
981
|
+
state.stale = true;
|
|
982
|
+
state.loading = true;
|
|
983
|
+
this.notifySubscription(state);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
notifySubscription(state, dataChanged = false, failure) {
|
|
987
|
+
if (!state.active) return;
|
|
988
|
+
state.revision++;
|
|
989
|
+
for (const listener of state.listeners) listener();
|
|
990
|
+
if (dataChanged) {
|
|
991
|
+
for (const observer of state.observers.values()) {
|
|
992
|
+
observer.onData?.(state.data);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (failure) {
|
|
996
|
+
for (const observer of state.observers.values()) {
|
|
997
|
+
observer.onError?.(failure);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
notifyStateListeners() {
|
|
1002
|
+
for (const listener of this.stateListeners) {
|
|
1003
|
+
listener(this.connectionState);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
var ReactiveClientContext = react.createContext(null);
|
|
1008
|
+
function createManagedClient(externalClient, config) {
|
|
1009
|
+
if (externalClient) return { client: externalClient, owned: false };
|
|
1010
|
+
if (!config?.url) return null;
|
|
1011
|
+
return {
|
|
1012
|
+
client: new ReactiveClient(config),
|
|
1013
|
+
owned: true,
|
|
1014
|
+
scope: getClientScope(config)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function sameRecord(left, right) {
|
|
1018
|
+
if (left === right) return true;
|
|
1019
|
+
if (!left || !right) return false;
|
|
1020
|
+
const keys = Object.keys(left);
|
|
1021
|
+
return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
|
|
1022
|
+
}
|
|
1023
|
+
function getClientScope(config) {
|
|
1024
|
+
return {
|
|
1025
|
+
url: config.url,
|
|
1026
|
+
httpUrl: config.httpUrl ?? "/api/reactive",
|
|
1027
|
+
auth: typeof config.auth === "string" ? config.auth : void 0,
|
|
1028
|
+
dynamicAuth: typeof config.auth === "function",
|
|
1029
|
+
headers: typeof config.headers === "object" ? { ...config.headers } : void 0,
|
|
1030
|
+
dynamicHeaders: typeof config.headers === "function"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function sameScope(left, right) {
|
|
1034
|
+
if (!left) return false;
|
|
1035
|
+
return left.url === right.url && left.httpUrl === right.httpUrl && left.auth === right.auth && left.dynamicAuth === right.dynamicAuth && left.dynamicHeaders === right.dynamicHeaders && sameRecord(left.headers, right.headers);
|
|
1036
|
+
}
|
|
1037
|
+
function ReactiveClientProvider({
|
|
1038
|
+
children,
|
|
1039
|
+
config,
|
|
1040
|
+
client: externalClient
|
|
1041
|
+
}) {
|
|
1042
|
+
const [managed, setManaged] = react.useState(
|
|
1043
|
+
() => createManagedClient(externalClient, config)
|
|
1044
|
+
);
|
|
1045
|
+
let renderedManaged = managed;
|
|
1046
|
+
if (externalClient) {
|
|
1047
|
+
if (managed?.owned || managed?.client !== externalClient) {
|
|
1048
|
+
renderedManaged = { client: externalClient, owned: false };
|
|
1049
|
+
}
|
|
1050
|
+
} else if (!config?.url) {
|
|
1051
|
+
renderedManaged = null;
|
|
1052
|
+
} else {
|
|
1053
|
+
const scope = getClientScope(config);
|
|
1054
|
+
if (!managed?.owned || !sameScope(managed.scope, scope)) {
|
|
1055
|
+
renderedManaged = {
|
|
1056
|
+
client: new ReactiveClient(config),
|
|
1057
|
+
owned: true,
|
|
1058
|
+
scope
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (renderedManaged !== managed) setManaged(renderedManaged);
|
|
1063
|
+
react.useEffect(() => {
|
|
1064
|
+
if (renderedManaged?.owned && config) {
|
|
1065
|
+
renderedManaged.client.configure(config);
|
|
1066
|
+
}
|
|
1067
|
+
}, [config, renderedManaged]);
|
|
1068
|
+
react.useEffect(() => {
|
|
1069
|
+
if (!renderedManaged?.owned) return;
|
|
1070
|
+
void renderedManaged.client.connect().catch(() => void 0);
|
|
1071
|
+
return () => {
|
|
1072
|
+
renderedManaged.client.disconnect();
|
|
1073
|
+
};
|
|
1074
|
+
}, [renderedManaged]);
|
|
1075
|
+
react.useEffect(() => {
|
|
1076
|
+
if (typeof window === "undefined" || typeof document === "undefined" || !renderedManaged?.owned) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const attemptReconnect = () => {
|
|
1080
|
+
const state = renderedManaged.client.getConnectionState();
|
|
1081
|
+
if (state === "connected" || state === "connecting") return;
|
|
1082
|
+
if (state === "reconnecting") {
|
|
1083
|
+
debug(
|
|
1084
|
+
"[dynamodb-reactive] Tab active during reconnect; reconnecting now"
|
|
1085
|
+
);
|
|
1086
|
+
renderedManaged.client.forceReconnect();
|
|
1087
|
+
} else {
|
|
1088
|
+
void renderedManaged.client.connect().catch(() => void 0);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
const handleVisibility = () => {
|
|
1092
|
+
if (document.visibilityState === "visible") attemptReconnect();
|
|
1093
|
+
};
|
|
1094
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
1095
|
+
window.addEventListener("focus", attemptReconnect);
|
|
1096
|
+
return () => {
|
|
1097
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
1098
|
+
window.removeEventListener("focus", attemptReconnect);
|
|
1099
|
+
};
|
|
1100
|
+
}, [renderedManaged]);
|
|
1101
|
+
react.useEffect(() => {
|
|
1102
|
+
if (!config?.url && !externalClient) {
|
|
1103
|
+
warn(
|
|
1104
|
+
"[dynamodb-reactive] WebSocket URL not configured; real-time features disabled"
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}, [config?.url, externalClient]);
|
|
1108
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ReactiveClientContext.Provider, { value: renderedManaged?.client ?? null, children });
|
|
1109
|
+
}
|
|
1110
|
+
function useReactiveClient() {
|
|
1111
|
+
return react.useContext(ReactiveClientContext);
|
|
1112
|
+
}
|
|
1113
|
+
function useConnectionState() {
|
|
1114
|
+
const client = useReactiveClient();
|
|
1115
|
+
return react.useSyncExternalStore(
|
|
1116
|
+
(listener) => client ? client.onConnectionStateChange(listener) : () => void 0,
|
|
1117
|
+
() => client ? client.getConnectionState() : "disabled",
|
|
1118
|
+
() => "disabled"
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function useSubscription(path, options = {}) {
|
|
1122
|
+
const client = useReactiveClient();
|
|
1123
|
+
const queryKey = getQueryKey(path, options.input);
|
|
1124
|
+
const [active, setActive] = react.useState(null);
|
|
1125
|
+
const onDataRef = react.useRef(options.onData);
|
|
1126
|
+
const onErrorRef = react.useRef(options.onError);
|
|
1127
|
+
onDataRef.current = options.onData;
|
|
1128
|
+
onErrorRef.current = options.onError;
|
|
1129
|
+
react.useEffect(() => {
|
|
1130
|
+
if (!client) {
|
|
1131
|
+
setActive(null);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const next = client.subscribe(path, {
|
|
1135
|
+
input: options.input,
|
|
1136
|
+
onData: (data) => onDataRef.current?.(data),
|
|
1137
|
+
onError: (failure) => onErrorRef.current?.(failure)
|
|
1138
|
+
});
|
|
1139
|
+
setActive({ client, subscription: next });
|
|
1140
|
+
return () => next.unsubscribe();
|
|
1141
|
+
}, [client, path, queryKey]);
|
|
1142
|
+
const subscription = active?.client === client && active.subscription.key === queryKey ? active.subscription : null;
|
|
1143
|
+
react.useSyncExternalStore(
|
|
1144
|
+
(listener) => client && subscription ? client.addSubscriptionListener(subscription.id, listener) : () => void 0,
|
|
1145
|
+
() => client && subscription ? client.getSubscriptionRevision(subscription.id) : -1,
|
|
1146
|
+
() => -1
|
|
1147
|
+
);
|
|
1148
|
+
const refetch = async () => {
|
|
1149
|
+
await subscription?.refetch();
|
|
1150
|
+
};
|
|
1151
|
+
const setData = (updater) => {
|
|
1152
|
+
if (client && subscription) {
|
|
1153
|
+
client.applyOptimisticUpdate(
|
|
1154
|
+
subscription.id,
|
|
1155
|
+
updater
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
return {
|
|
1160
|
+
data: subscription?.data,
|
|
1161
|
+
loading: client ? subscription?.loading ?? true : false,
|
|
1162
|
+
stale: client ? subscription?.stale ?? true : false,
|
|
1163
|
+
error: subscription?.error,
|
|
1164
|
+
disabled: !client,
|
|
1165
|
+
refetch,
|
|
1166
|
+
setData
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function useMutation(path, options) {
|
|
1170
|
+
const client = useReactiveClient();
|
|
1171
|
+
const [state, setState] = react.useState({ data: void 0, loading: false, error: void 0 });
|
|
1172
|
+
const pendingCountRef = react.useRef(0);
|
|
1173
|
+
const latestCallRef = react.useRef(0);
|
|
1174
|
+
const mutate = async (input) => {
|
|
1175
|
+
if (!client) {
|
|
1176
|
+
throw new Error("WebSocket not configured; mutations are disabled");
|
|
1177
|
+
}
|
|
1178
|
+
const callId = ++latestCallRef.current;
|
|
1179
|
+
pendingCountRef.current++;
|
|
1180
|
+
setState((current) => ({ ...current, loading: true, error: void 0 }));
|
|
1181
|
+
try {
|
|
1182
|
+
const result = await client.call(path, input, options);
|
|
1183
|
+
pendingCountRef.current--;
|
|
1184
|
+
setState(
|
|
1185
|
+
(current) => callId === latestCallRef.current ? {
|
|
1186
|
+
data: result,
|
|
1187
|
+
loading: pendingCountRef.current > 0,
|
|
1188
|
+
error: void 0
|
|
1189
|
+
} : { ...current, loading: pendingCountRef.current > 0 }
|
|
1190
|
+
);
|
|
1191
|
+
return result;
|
|
1192
|
+
} catch (cause) {
|
|
1193
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
1194
|
+
pendingCountRef.current--;
|
|
1195
|
+
setState(
|
|
1196
|
+
(current) => callId === latestCallRef.current ? {
|
|
1197
|
+
...current,
|
|
1198
|
+
loading: pendingCountRef.current > 0,
|
|
1199
|
+
error: failure
|
|
1200
|
+
} : { ...current, loading: pendingCountRef.current > 0 }
|
|
1201
|
+
);
|
|
1202
|
+
throw failure;
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const reset = () => {
|
|
1206
|
+
latestCallRef.current++;
|
|
1207
|
+
setState({
|
|
1208
|
+
data: void 0,
|
|
1209
|
+
loading: pendingCountRef.current > 0,
|
|
1210
|
+
error: void 0
|
|
1211
|
+
});
|
|
1212
|
+
};
|
|
1213
|
+
return { mutate, ...state, disabled: !client, reset };
|
|
1214
|
+
}
|
|
1215
|
+
function useClient() {
|
|
1216
|
+
function buildProxy(path) {
|
|
1217
|
+
return new Proxy(
|
|
1218
|
+
{},
|
|
1219
|
+
{
|
|
1220
|
+
get(_target, property) {
|
|
1221
|
+
if (typeof property !== "string") return void 0;
|
|
1222
|
+
if (property === "useQuery") {
|
|
1223
|
+
return function useQueryHook(input, queryOptions) {
|
|
1224
|
+
return useSubscription(path.join("."), {
|
|
1225
|
+
...queryOptions,
|
|
1226
|
+
input
|
|
1227
|
+
});
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
if (property === "useMutation") {
|
|
1231
|
+
return function useMutationHook(mutationOptions) {
|
|
1232
|
+
return useMutation(path.join("."), mutationOptions);
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
return buildProxy([...path, property]);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
return buildProxy([]);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
exports.ReactiveClientProvider = ReactiveClientProvider;
|
|
1244
|
+
exports.useClient = useClient;
|
|
1245
|
+
exports.useConnectionState = useConnectionState;
|
|
1246
|
+
exports.useMutation = useMutation;
|
|
1247
|
+
exports.useReactiveClient = useReactiveClient;
|
|
1248
|
+
exports.useSubscription = useSubscription;
|
|
1249
|
+
//# sourceMappingURL=react.cjs.map
|
|
1250
|
+
//# sourceMappingURL=react.cjs.map
|