dynamodb-reactive 0.1.1 → 0.1.4
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 +114 -24
- package/dist/{chunk-KRZQWA2W.js → chunk-MI2ZLLB2.js} +363 -51
- package/dist/chunk-MI2ZLLB2.js.map +1 -0
- package/dist/client.d.ts +12 -2
- package/dist/client.js +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/infra.d.ts +1 -1
- package/dist/infra.js +1 -1
- package/dist/infra.js.map +1 -1
- package/dist/{react-BMZQ8Mth.d.ts → react-B8Q_XoCk.d.ts} +183 -16
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/server.d.ts +43 -15
- package/dist/server.js +106 -12
- package/dist/server.js.map +1 -1
- package/dist/{table-CSJysZPQ.d.ts → table-CfIWxbuJ.d.ts} +1 -1
- package/dist/{types-Ci7IieDA.d.ts → types-DeshTSf5.d.ts} +1 -1
- package/package.json +4 -2
- package/dist/chunk-KRZQWA2W.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import jsonpatch from 'fast-json-patch';
|
|
2
2
|
import { createContext, useState, useEffect, useContext, useSyncExternalStore, useRef } from 'react';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
// ../client/src/patcher.ts
|
|
6
|
+
var { applyPatch } = jsonpatch;
|
|
6
7
|
function applyPatches(document, patches) {
|
|
7
8
|
if (patches.length === 0) {
|
|
8
9
|
return document;
|
|
@@ -46,6 +47,8 @@ var WebSocketManager = class {
|
|
|
46
47
|
messageQueue = [];
|
|
47
48
|
messageHandlers = /* @__PURE__ */ new Set();
|
|
48
49
|
stateHandlers = /* @__PURE__ */ new Set();
|
|
50
|
+
_connectionId = null;
|
|
51
|
+
connectionIdResolvers = [];
|
|
49
52
|
constructor(config) {
|
|
50
53
|
this.config = {
|
|
51
54
|
autoReconnect: true,
|
|
@@ -54,6 +57,23 @@ var WebSocketManager = class {
|
|
|
54
57
|
...config
|
|
55
58
|
};
|
|
56
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the current connection ID (null if not connected)
|
|
62
|
+
*/
|
|
63
|
+
get connectionId() {
|
|
64
|
+
return this._connectionId;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wait for the connection ID to be available
|
|
68
|
+
*/
|
|
69
|
+
waitForConnectionId() {
|
|
70
|
+
if (this._connectionId) {
|
|
71
|
+
return Promise.resolve(this._connectionId);
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
this.connectionIdResolvers.push(resolve);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
57
77
|
/**
|
|
58
78
|
* Get current connection state
|
|
59
79
|
*/
|
|
@@ -72,13 +92,12 @@ var WebSocketManager = class {
|
|
|
72
92
|
const url = await this.buildUrl();
|
|
73
93
|
this.ws = new WebSocket(url);
|
|
74
94
|
this.ws.onopen = () => {
|
|
75
|
-
this.setState("connected");
|
|
76
95
|
this.reconnectAttempts = 0;
|
|
77
|
-
this.
|
|
78
|
-
this.config.onConnect?.();
|
|
96
|
+
this.ws?.send(JSON.stringify({ type: "init" }));
|
|
79
97
|
};
|
|
80
98
|
this.ws.onclose = (event) => {
|
|
81
99
|
this.ws = null;
|
|
100
|
+
this._connectionId = null;
|
|
82
101
|
this.setState("disconnected");
|
|
83
102
|
this.config.onDisconnect?.();
|
|
84
103
|
if (this.config.autoReconnect && !event.wasClean) {
|
|
@@ -92,6 +111,17 @@ var WebSocketManager = class {
|
|
|
92
111
|
this.ws.onmessage = (event) => {
|
|
93
112
|
try {
|
|
94
113
|
const message = JSON.parse(event.data);
|
|
114
|
+
if (message.type === "connected") {
|
|
115
|
+
this._connectionId = message.connectionId;
|
|
116
|
+
this.setState("connected");
|
|
117
|
+
this.flushMessageQueue();
|
|
118
|
+
this.config.onConnect?.();
|
|
119
|
+
for (const resolve of this.connectionIdResolvers) {
|
|
120
|
+
resolve(message.connectionId);
|
|
121
|
+
}
|
|
122
|
+
this.connectionIdResolvers = [];
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
95
125
|
this.handleMessage(message);
|
|
96
126
|
} catch (error) {
|
|
97
127
|
console.error("Failed to parse WebSocket message:", error);
|
|
@@ -119,6 +149,7 @@ var WebSocketManager = class {
|
|
|
119
149
|
this.ws.close(1e3, "Client disconnect");
|
|
120
150
|
this.ws = null;
|
|
121
151
|
}
|
|
152
|
+
this._connectionId = null;
|
|
122
153
|
this.setState("disconnected");
|
|
123
154
|
}
|
|
124
155
|
/**
|
|
@@ -192,17 +223,34 @@ var WebSocketManager = class {
|
|
|
192
223
|
};
|
|
193
224
|
|
|
194
225
|
// ../client/src/client.ts
|
|
226
|
+
var DEFAULT_HTTP_URL = "/api/reactive";
|
|
195
227
|
function generateId() {
|
|
196
228
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
197
229
|
}
|
|
198
230
|
var ReactiveClient = class {
|
|
199
231
|
wsManager;
|
|
232
|
+
httpUrl;
|
|
233
|
+
authGetter;
|
|
234
|
+
headersGetter;
|
|
200
235
|
subscriptions = /* @__PURE__ */ new Map();
|
|
201
236
|
pendingCalls = /* @__PURE__ */ new Map();
|
|
202
237
|
connectionState = "disconnected";
|
|
203
238
|
stateListeners = /* @__PURE__ */ new Set();
|
|
239
|
+
// Stores original data for rollback when optimistic updates fail
|
|
240
|
+
optimisticUpdates = /* @__PURE__ */ new Map();
|
|
204
241
|
constructor(config) {
|
|
205
242
|
this.wsManager = new WebSocketManager(config);
|
|
243
|
+
this.httpUrl = config.httpUrl ?? DEFAULT_HTTP_URL;
|
|
244
|
+
if (typeof config.auth === "function") {
|
|
245
|
+
this.authGetter = config.auth;
|
|
246
|
+
} else if (config.auth) {
|
|
247
|
+
this.authGetter = () => config.auth;
|
|
248
|
+
}
|
|
249
|
+
if (typeof config.headers === "function") {
|
|
250
|
+
this.headersGetter = config.headers;
|
|
251
|
+
} else if (config.headers) {
|
|
252
|
+
this.headersGetter = () => config.headers;
|
|
253
|
+
}
|
|
206
254
|
this.wsManager.onMessage((message) => this.handleMessage(message));
|
|
207
255
|
this.wsManager.onStateChange((state) => {
|
|
208
256
|
this.connectionState = state;
|
|
@@ -224,6 +272,17 @@ var ReactiveClient = class {
|
|
|
224
272
|
disconnect() {
|
|
225
273
|
this.wsManager.disconnect();
|
|
226
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Update the headers getter for HTTP requests.
|
|
277
|
+
* Useful for dynamically setting auth or user context headers.
|
|
278
|
+
*/
|
|
279
|
+
setHeaders(headers) {
|
|
280
|
+
if (typeof headers === "function") {
|
|
281
|
+
this.headersGetter = headers;
|
|
282
|
+
} else {
|
|
283
|
+
this.headersGetter = () => headers;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
227
286
|
/**
|
|
228
287
|
* Get current connection state
|
|
229
288
|
*/
|
|
@@ -238,7 +297,8 @@ var ReactiveClient = class {
|
|
|
238
297
|
return () => this.stateListeners.delete(listener);
|
|
239
298
|
}
|
|
240
299
|
/**
|
|
241
|
-
* Subscribe to a procedure
|
|
300
|
+
* Subscribe to a procedure via HTTP.
|
|
301
|
+
* Snapshot is returned via HTTP, patches come via WebSocket.
|
|
242
302
|
*/
|
|
243
303
|
subscribe(path, options = {}) {
|
|
244
304
|
const id = generateId();
|
|
@@ -253,13 +313,14 @@ var ReactiveClient = class {
|
|
|
253
313
|
options
|
|
254
314
|
};
|
|
255
315
|
this.subscriptions.set(id, state);
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
316
|
+
this.sendSubscribeRequest(id, path, options.input).catch((error) => {
|
|
317
|
+
state.error = error instanceof Error ? error : new Error(String(error));
|
|
318
|
+
state.loading = false;
|
|
319
|
+
this.notifySubscriptionListeners(id);
|
|
320
|
+
options.onError?.(state.error);
|
|
261
321
|
});
|
|
262
322
|
const subscription = {
|
|
323
|
+
id,
|
|
263
324
|
get data() {
|
|
264
325
|
return state.data;
|
|
265
326
|
},
|
|
@@ -275,28 +336,165 @@ var ReactiveClient = class {
|
|
|
275
336
|
return subscription;
|
|
276
337
|
}
|
|
277
338
|
/**
|
|
278
|
-
*
|
|
339
|
+
* Send subscribe request via HTTP
|
|
279
340
|
*/
|
|
280
|
-
async
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
341
|
+
async sendSubscribeRequest(subscriptionId, path, input) {
|
|
342
|
+
const connectionId = await this.wsManager.waitForConnectionId();
|
|
343
|
+
const headers = {
|
|
344
|
+
"Content-Type": "application/json"
|
|
345
|
+
};
|
|
346
|
+
if (this.headersGetter) {
|
|
347
|
+
const customHeaders = await this.headersGetter();
|
|
348
|
+
Object.assign(headers, customHeaders);
|
|
349
|
+
}
|
|
350
|
+
if (this.authGetter) {
|
|
351
|
+
const token = await this.authGetter();
|
|
352
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
353
|
+
}
|
|
354
|
+
const response = await fetch(this.httpUrl, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers,
|
|
357
|
+
body: JSON.stringify({
|
|
358
|
+
type: "subscribe",
|
|
359
|
+
connectionId,
|
|
360
|
+
subscriptionId,
|
|
290
361
|
path,
|
|
291
362
|
input
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Subscribe failed: ${response.status} ${response.statusText}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const result = await response.json();
|
|
371
|
+
if (result.type === "error") {
|
|
372
|
+
throw new Error(result.message ?? "Unknown error");
|
|
373
|
+
}
|
|
374
|
+
if (result.type === "snapshot") {
|
|
375
|
+
this.handleSnapshot(subscriptionId, result.data);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Call a mutation procedure via HTTP with optimistic update support.
|
|
380
|
+
* All optimistic updates are applied INSTANTLY (before HTTP request).
|
|
381
|
+
*/
|
|
382
|
+
async call(path, input, options) {
|
|
383
|
+
const invalidatesPath = options?.invalidates;
|
|
384
|
+
const updateType = options?.optimisticUpdate;
|
|
385
|
+
let targetSubscriptionId;
|
|
386
|
+
if (invalidatesPath) {
|
|
387
|
+
targetSubscriptionId = this.findSubscriptionByPath(invalidatesPath);
|
|
388
|
+
}
|
|
389
|
+
if (targetSubscriptionId) {
|
|
390
|
+
const state = this.subscriptions.get(targetSubscriptionId);
|
|
391
|
+
const currentData = state?.data;
|
|
392
|
+
if (updateType === "remove") {
|
|
393
|
+
const inputId = input.id;
|
|
394
|
+
if (inputId) {
|
|
395
|
+
this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
|
|
396
|
+
if (!Array.isArray(data)) return data;
|
|
397
|
+
return data.filter((item) => item.id !== inputId);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
} else if (updateType === "merge") {
|
|
401
|
+
const inputWithId = input;
|
|
402
|
+
if (inputWithId.id) {
|
|
403
|
+
this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
|
|
404
|
+
if (!Array.isArray(data)) return data;
|
|
405
|
+
const idx = data.findIndex(
|
|
406
|
+
(item) => item.id === inputWithId.id
|
|
407
|
+
);
|
|
408
|
+
if (idx >= 0) {
|
|
409
|
+
return [
|
|
410
|
+
...data.slice(0, idx),
|
|
411
|
+
{ ...data[idx], ...input },
|
|
412
|
+
...data.slice(idx + 1)
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
return data;
|
|
416
|
+
});
|
|
417
|
+
} else if (options?.optimisticData) {
|
|
418
|
+
const optimistic = typeof options.optimisticData === "function" ? options.optimisticData(input, Array.isArray(currentData) ? currentData : []) : options.optimisticData;
|
|
419
|
+
if (optimistic) {
|
|
420
|
+
this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
|
|
421
|
+
if (!Array.isArray(data)) return data;
|
|
422
|
+
return [optimistic, ...data];
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else if (typeof updateType === "function") {
|
|
427
|
+
this.applyOptimisticUpdate(targetSubscriptionId, (data) => {
|
|
428
|
+
if (!Array.isArray(data)) return data;
|
|
429
|
+
return updateType(data, input, void 0);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const connectionId = await this.wsManager.waitForConnectionId();
|
|
434
|
+
const headers = {
|
|
435
|
+
"Content-Type": "application/json"
|
|
436
|
+
};
|
|
437
|
+
if (this.headersGetter) {
|
|
438
|
+
const customHeaders = await this.headersGetter();
|
|
439
|
+
Object.assign(headers, customHeaders);
|
|
440
|
+
}
|
|
441
|
+
if (this.authGetter) {
|
|
442
|
+
const token = await this.authGetter();
|
|
443
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetch(this.httpUrl, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers,
|
|
449
|
+
body: JSON.stringify({
|
|
450
|
+
type: "call",
|
|
451
|
+
connectionId,
|
|
452
|
+
path,
|
|
453
|
+
input
|
|
454
|
+
})
|
|
292
455
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Call failed: ${response.status} ${response.statusText}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const result = await response.json();
|
|
462
|
+
if (result.type === "error") {
|
|
463
|
+
throw new Error(result.message ?? "Unknown error");
|
|
464
|
+
}
|
|
465
|
+
const data = result.data;
|
|
466
|
+
if (targetSubscriptionId && updateType === "merge") {
|
|
467
|
+
const resultWithId = data;
|
|
468
|
+
if (resultWithId && resultWithId.id) {
|
|
469
|
+
this.applyOptimisticUpdate(targetSubscriptionId, (currentData) => {
|
|
470
|
+
if (!Array.isArray(currentData)) return currentData;
|
|
471
|
+
const idx = currentData.findIndex(
|
|
472
|
+
(item) => item.id === resultWithId.id
|
|
473
|
+
);
|
|
474
|
+
if (idx >= 0) {
|
|
475
|
+
return [
|
|
476
|
+
...currentData.slice(0, idx),
|
|
477
|
+
data,
|
|
478
|
+
...currentData.slice(idx + 1)
|
|
479
|
+
];
|
|
480
|
+
} else {
|
|
481
|
+
return [
|
|
482
|
+
data,
|
|
483
|
+
...currentData.filter(
|
|
484
|
+
(item) => item.id && !item.id.startsWith("temp-")
|
|
485
|
+
)
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
});
|
|
297
489
|
}
|
|
298
|
-
}
|
|
299
|
-
|
|
490
|
+
}
|
|
491
|
+
return data;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (targetSubscriptionId) {
|
|
494
|
+
this.rollbackOptimisticUpdate(targetSubscriptionId);
|
|
495
|
+
}
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
300
498
|
}
|
|
301
499
|
/**
|
|
302
500
|
* Unsubscribe from a subscription
|
|
@@ -311,25 +509,30 @@ var ReactiveClient = class {
|
|
|
311
509
|
});
|
|
312
510
|
}
|
|
313
511
|
/**
|
|
314
|
-
* Refetch a subscription
|
|
512
|
+
* Refetch a subscription via HTTP
|
|
315
513
|
*/
|
|
316
514
|
async refetch(id) {
|
|
317
515
|
const state = this.subscriptions.get(id);
|
|
318
516
|
if (!state) return;
|
|
319
517
|
state.loading = true;
|
|
320
518
|
this.notifySubscriptionListeners(id);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
519
|
+
try {
|
|
520
|
+
await this.sendSubscribeRequest(id, state.path, state.input);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
state.error = error instanceof Error ? error : new Error(String(error));
|
|
523
|
+
state.loading = false;
|
|
524
|
+
this.notifySubscriptionListeners(id);
|
|
525
|
+
state.options.onError?.(state.error);
|
|
526
|
+
}
|
|
327
527
|
}
|
|
328
528
|
/**
|
|
329
|
-
* Handle incoming server messages
|
|
529
|
+
* Handle incoming server messages.
|
|
530
|
+
* Note: 'connected' messages are handled by WebSocketManager before reaching here.
|
|
330
531
|
*/
|
|
331
532
|
handleMessage(message) {
|
|
332
533
|
switch (message.type) {
|
|
534
|
+
case "connected":
|
|
535
|
+
break;
|
|
333
536
|
case "snapshot":
|
|
334
537
|
this.handleSnapshot(message.subscriptionId, message.data);
|
|
335
538
|
break;
|
|
@@ -350,6 +553,7 @@ var ReactiveClient = class {
|
|
|
350
553
|
handleSnapshot(subscriptionId, data) {
|
|
351
554
|
const state = this.subscriptions.get(subscriptionId);
|
|
352
555
|
if (!state) return;
|
|
556
|
+
this.clearOptimisticUpdate(subscriptionId);
|
|
353
557
|
state.data = data;
|
|
354
558
|
state.loading = false;
|
|
355
559
|
state.error = void 0;
|
|
@@ -362,8 +566,11 @@ var ReactiveClient = class {
|
|
|
362
566
|
handlePatch(subscriptionId, patches) {
|
|
363
567
|
const state = this.subscriptions.get(subscriptionId);
|
|
364
568
|
if (!state || state.data === void 0) return;
|
|
569
|
+
const originalData = this.optimisticUpdates.get(subscriptionId);
|
|
570
|
+
const baseData = originalData !== void 0 ? originalData : state.data;
|
|
571
|
+
this.clearOptimisticUpdate(subscriptionId);
|
|
365
572
|
try {
|
|
366
|
-
state.data = applyPatches(
|
|
573
|
+
state.data = applyPatches(baseData, patches);
|
|
367
574
|
this.notifySubscriptionListeners(subscriptionId);
|
|
368
575
|
state.options.onData?.(state.data);
|
|
369
576
|
} catch (error) {
|
|
@@ -404,19 +611,21 @@ var ReactiveClient = class {
|
|
|
404
611
|
}
|
|
405
612
|
}
|
|
406
613
|
/**
|
|
407
|
-
* Resubscribe to all subscriptions after reconnect
|
|
614
|
+
* Resubscribe to all subscriptions after reconnect via HTTP
|
|
408
615
|
*/
|
|
409
616
|
resubscribeAll() {
|
|
410
617
|
for (const [id, state] of this.subscriptions) {
|
|
411
618
|
if (state.options.resubscribeOnReconnect !== false) {
|
|
412
619
|
state.loading = true;
|
|
413
620
|
this.notifySubscriptionListeners(id);
|
|
414
|
-
this.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
621
|
+
this.sendSubscribeRequest(id, state.path, state.input).catch(
|
|
622
|
+
(error) => {
|
|
623
|
+
state.error = error instanceof Error ? error : new Error(String(error));
|
|
624
|
+
state.loading = false;
|
|
625
|
+
this.notifySubscriptionListeners(id);
|
|
626
|
+
state.options.onError?.(state.error);
|
|
627
|
+
}
|
|
628
|
+
);
|
|
420
629
|
}
|
|
421
630
|
}
|
|
422
631
|
}
|
|
@@ -456,6 +665,51 @@ var ReactiveClient = class {
|
|
|
456
665
|
getSubscriptionState(id) {
|
|
457
666
|
return this.subscriptions.get(id);
|
|
458
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Find subscription by path
|
|
670
|
+
*/
|
|
671
|
+
findSubscriptionByPath(path) {
|
|
672
|
+
for (const [id, state] of this.subscriptions) {
|
|
673
|
+
if (state.path === path) {
|
|
674
|
+
return id;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return void 0;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Apply an optimistic update to a subscription's data
|
|
681
|
+
*/
|
|
682
|
+
applyOptimisticUpdate(subscriptionId, updater) {
|
|
683
|
+
const state = this.subscriptions.get(subscriptionId);
|
|
684
|
+
if (!state || state.data === void 0) return;
|
|
685
|
+
if (!this.optimisticUpdates.has(subscriptionId)) {
|
|
686
|
+
this.optimisticUpdates.set(
|
|
687
|
+
subscriptionId,
|
|
688
|
+
JSON.parse(JSON.stringify(state.data))
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
state.data = updater(state.data);
|
|
692
|
+
this.notifySubscriptionListeners(subscriptionId);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Rollback an optimistic update
|
|
696
|
+
*/
|
|
697
|
+
rollbackOptimisticUpdate(subscriptionId) {
|
|
698
|
+
const original = this.optimisticUpdates.get(subscriptionId);
|
|
699
|
+
if (original === void 0) return;
|
|
700
|
+
const state = this.subscriptions.get(subscriptionId);
|
|
701
|
+
if (state) {
|
|
702
|
+
state.data = original;
|
|
703
|
+
this.notifySubscriptionListeners(subscriptionId);
|
|
704
|
+
}
|
|
705
|
+
this.optimisticUpdates.delete(subscriptionId);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Clear optimistic state (called when real data arrives)
|
|
709
|
+
*/
|
|
710
|
+
clearOptimisticUpdate(subscriptionId) {
|
|
711
|
+
this.optimisticUpdates.delete(subscriptionId);
|
|
712
|
+
}
|
|
459
713
|
};
|
|
460
714
|
function createReactiveClient(config) {
|
|
461
715
|
const client = new ReactiveClient(config);
|
|
@@ -480,7 +734,12 @@ function createTypedProxy(client, path) {
|
|
|
480
734
|
if (prop === "mutate") {
|
|
481
735
|
return async (input) => {
|
|
482
736
|
const parentPath = path.join(".");
|
|
483
|
-
|
|
737
|
+
const invalidatesPath = path.length >= 2 ? `${path.slice(0, -1).join(".")}.list` : void 0;
|
|
738
|
+
const updateType = parentPath.endsWith(".delete") ? "remove" : "merge";
|
|
739
|
+
return client.call(parentPath, input, {
|
|
740
|
+
invalidates: invalidatesPath,
|
|
741
|
+
optimisticUpdate: updateType
|
|
742
|
+
});
|
|
484
743
|
};
|
|
485
744
|
}
|
|
486
745
|
return createTypedProxy(client, newPath);
|
|
@@ -583,12 +842,23 @@ function useSubscription(path, options = {}) {
|
|
|
583
842
|
}
|
|
584
843
|
});
|
|
585
844
|
subscriptionRef.current = subscription;
|
|
845
|
+
const unregisterListener = client.addSubscriptionListener(
|
|
846
|
+
subscription.id,
|
|
847
|
+
() => {
|
|
848
|
+
setState({
|
|
849
|
+
data: subscription.data,
|
|
850
|
+
loading: subscription.loading,
|
|
851
|
+
error: subscription.error
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
);
|
|
586
855
|
setState({
|
|
587
856
|
data: subscription.data,
|
|
588
857
|
loading: subscription.loading,
|
|
589
858
|
error: subscription.error
|
|
590
859
|
});
|
|
591
860
|
return () => {
|
|
861
|
+
unregisterListener();
|
|
592
862
|
subscription.unsubscribe();
|
|
593
863
|
subscriptionRef.current = null;
|
|
594
864
|
};
|
|
@@ -605,20 +875,31 @@ function useSubscription(path, options = {}) {
|
|
|
605
875
|
refetch
|
|
606
876
|
};
|
|
607
877
|
}
|
|
608
|
-
function useMutation(path) {
|
|
878
|
+
function useMutation(path, options) {
|
|
609
879
|
const client = useReactiveClient();
|
|
610
880
|
const [state, setState] = useState({
|
|
611
881
|
data: void 0,
|
|
612
882
|
loading: false,
|
|
613
883
|
error: void 0
|
|
614
884
|
});
|
|
885
|
+
const autoInvalidates = options?.invalidates ?? (() => {
|
|
886
|
+
const parts = path.split(".");
|
|
887
|
+
if (parts.length >= 2) {
|
|
888
|
+
return `${parts.slice(0, -1).join(".")}.list`;
|
|
889
|
+
}
|
|
890
|
+
return void 0;
|
|
891
|
+
})();
|
|
892
|
+
const autoUpdateType = options?.optimisticUpdate ?? (path.endsWith(".delete") ? "remove" : "merge");
|
|
615
893
|
const mutate = async (input) => {
|
|
616
894
|
if (!client) {
|
|
617
895
|
throw new Error("WebSocket not configured - mutations are disabled");
|
|
618
896
|
}
|
|
619
897
|
setState((prev) => ({ ...prev, loading: true, error: void 0 }));
|
|
620
898
|
try {
|
|
621
|
-
const result = await client.call(path, input
|
|
899
|
+
const result = await client.call(path, input, {
|
|
900
|
+
invalidates: autoInvalidates,
|
|
901
|
+
optimisticUpdate: autoUpdateType
|
|
902
|
+
});
|
|
622
903
|
setState({ data: result, loading: false, error: void 0 });
|
|
623
904
|
return result;
|
|
624
905
|
} catch (error) {
|
|
@@ -637,10 +918,41 @@ function useMutation(path) {
|
|
|
637
918
|
reset
|
|
638
919
|
};
|
|
639
920
|
}
|
|
921
|
+
function useClient() {
|
|
922
|
+
const client = useReactiveClient();
|
|
923
|
+
function buildProxy(path) {
|
|
924
|
+
return new Proxy(
|
|
925
|
+
{},
|
|
926
|
+
{
|
|
927
|
+
get(_target, prop) {
|
|
928
|
+
if (prop === "useQuery") {
|
|
929
|
+
return function useQueryHook(input, options) {
|
|
930
|
+
const pathStr = path.join(".");
|
|
931
|
+
return useSubscription(pathStr, { ...options, input });
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (prop === "useMutation") {
|
|
935
|
+
return function useMutationHook(options) {
|
|
936
|
+
const pathStr = path.join(".");
|
|
937
|
+
return useMutation(pathStr, options);
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (prop === "setHeaders") {
|
|
941
|
+
return (headers) => {
|
|
942
|
+
client?.setHeaders(headers);
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return buildProxy([...path, prop]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
return buildProxy([]);
|
|
951
|
+
}
|
|
640
952
|
function createReactiveHooks() {
|
|
641
953
|
return {
|
|
642
954
|
useSubscription: (path, options) => useSubscription(path, options),
|
|
643
|
-
useMutation: (path) => useMutation(path),
|
|
955
|
+
useMutation: (path, options) => useMutation(path, options),
|
|
644
956
|
useConnectionState,
|
|
645
957
|
useReactiveClient
|
|
646
958
|
};
|
|
@@ -648,10 +960,10 @@ function createReactiveHooks() {
|
|
|
648
960
|
function createProcedureHooks(path) {
|
|
649
961
|
return {
|
|
650
962
|
useSubscription: (input, options) => useSubscription(path, { ...options, input }),
|
|
651
|
-
useMutation: () => useMutation(path)
|
|
963
|
+
useMutation: (options) => useMutation(path, options)
|
|
652
964
|
};
|
|
653
965
|
}
|
|
654
966
|
|
|
655
|
-
export { ReactiveClient, ReactiveClientProvider, WebSocketManager, applyPatches, createProcedureHooks, createReactiveClient, createReactiveHooks, useConnectionState, useMutation, useReactiveClient, useReactiveClientOrThrow, useSubscription };
|
|
656
|
-
//# sourceMappingURL=chunk-
|
|
657
|
-
//# sourceMappingURL=chunk-
|
|
967
|
+
export { ReactiveClient, ReactiveClientProvider, WebSocketManager, applyPatches, createProcedureHooks, createReactiveClient, createReactiveHooks, useClient, useConnectionState, useMutation, useReactiveClient, useReactiveClientOrThrow, useSubscription };
|
|
968
|
+
//# sourceMappingURL=chunk-MI2ZLLB2.js.map
|
|
969
|
+
//# sourceMappingURL=chunk-MI2ZLLB2.js.map
|