@gonvex/client 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +26 -0
- package/dist/index.js +433 -3
- package/dist/index.js.map +1 -1
- package/dist/sync-store.d.ts +71 -0
- package/dist/sync-store.js +396 -0
- package/dist/sync-store.js.map +1 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BrowserTelemetryInfo, JsonValue, MessageTrace, ServerMessage } from "@gonvex/protocol";
|
|
2
2
|
import { type QueryCacheOptions, type QueryCacheStatus } from "./query-cache.js";
|
|
3
|
+
import { type SyncStoreOptions } from "./sync-store.js";
|
|
3
4
|
import { type ErrorReporterOptions } from "./error-reporter.js";
|
|
4
5
|
export * from "./cache.js";
|
|
5
6
|
export * from "./cache-coordinator.js";
|
|
@@ -9,6 +10,7 @@ export * from "./browser-cache-shared-worker.js";
|
|
|
9
10
|
export * from "./browser-capabilities.js";
|
|
10
11
|
export * from "./persistent-cache.js";
|
|
11
12
|
export * from "./query-cache.js";
|
|
13
|
+
export * from "./sync-store.js";
|
|
12
14
|
export * from "./error-reporter.js";
|
|
13
15
|
export type { QueryCacheDirective } from "@gonvex/protocol";
|
|
14
16
|
type SubscriptionHandler = (message: ServerMessage) => void;
|
|
@@ -76,6 +78,7 @@ export type GonvexClientAuth = {
|
|
|
76
78
|
};
|
|
77
79
|
export type GonvexClientOptions = GonvexClientAuth & {
|
|
78
80
|
queryCache?: false | QueryCacheOptions;
|
|
81
|
+
sync?: false | SyncStoreOptions;
|
|
79
82
|
errorReporting?: false | Omit<ErrorReporterOptions, "endpoint" | "project" | "tenant">;
|
|
80
83
|
timeouts?: GonvexTimeoutOptions;
|
|
81
84
|
};
|
|
@@ -97,6 +100,7 @@ export declare class GonvexClient {
|
|
|
97
100
|
private socket;
|
|
98
101
|
private readonly handlers;
|
|
99
102
|
private readonly querySubscriptions;
|
|
103
|
+
private readonly syncSubscriptions;
|
|
100
104
|
private readonly oneShotQueries;
|
|
101
105
|
private readonly telemetryHandlers;
|
|
102
106
|
private readonly pendingMessages;
|
|
@@ -104,8 +108,10 @@ export declare class GonvexClient {
|
|
|
104
108
|
private authInFlight;
|
|
105
109
|
private telemetryEnabled;
|
|
106
110
|
private readonly queryCache;
|
|
111
|
+
private readonly syncStore;
|
|
107
112
|
private queryCacheDirective;
|
|
108
113
|
private queryCacheGeneration;
|
|
114
|
+
private syncIdentityGeneration;
|
|
109
115
|
private readonly sessionScopeHandlers;
|
|
110
116
|
private readonly errorReporter;
|
|
111
117
|
private reconnectTimer;
|
|
@@ -139,6 +145,22 @@ export declare class GonvexClient {
|
|
|
139
145
|
localQueryResult(): T | undefined;
|
|
140
146
|
onUpdate(handler: WatchUpdateHandler): () => void;
|
|
141
147
|
};
|
|
148
|
+
subscribeSync(ref: FunctionReference, args: JsonValue | undefined, onMessage: SubscriptionHandler): () => void;
|
|
149
|
+
watchSync<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue): {
|
|
150
|
+
localSyncResult(): T[] | undefined;
|
|
151
|
+
status(): {
|
|
152
|
+
isLoading: boolean;
|
|
153
|
+
isUpToDate: boolean;
|
|
154
|
+
};
|
|
155
|
+
onUpdate(handler: WatchUpdateHandler): () => void;
|
|
156
|
+
};
|
|
157
|
+
private handleSyncMessage;
|
|
158
|
+
private emitSyncMessage;
|
|
159
|
+
private startSync;
|
|
160
|
+
private sendSyncOpen;
|
|
161
|
+
private unsubscribeSyncListener;
|
|
162
|
+
private persistSyncSnapshot;
|
|
163
|
+
private persistSyncDelta;
|
|
142
164
|
mutation<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
|
|
143
165
|
action<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
|
|
144
166
|
query<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
|
|
@@ -151,11 +173,15 @@ export declare class GonvexClient {
|
|
|
151
173
|
private call;
|
|
152
174
|
private unsubscribeQueryListener;
|
|
153
175
|
private sendSubscription;
|
|
176
|
+
private enqueueSyncPersistence;
|
|
177
|
+
private scheduleSyncRetry;
|
|
178
|
+
private clearSyncRetry;
|
|
154
179
|
private requestSubscriptionSnapshot;
|
|
155
180
|
private sendOneShotQuery;
|
|
156
181
|
private resubscribeQueries;
|
|
157
182
|
private scheduleReconnect;
|
|
158
183
|
private installQueryCacheDirective;
|
|
184
|
+
private recoverWarmSyncDirective;
|
|
159
185
|
private resetQueryCacheScope;
|
|
160
186
|
private startQueryCacheRead;
|
|
161
187
|
private persistQueryResult;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createQueryCacheStore } from "./query-cache.js";
|
|
2
|
+
import { createSyncStore } from "./sync-store.js";
|
|
2
3
|
import { GonvexErrorReporter } from "./error-reporter.js";
|
|
3
4
|
export * from "./cache.js";
|
|
4
5
|
export * from "./cache-coordinator.js";
|
|
@@ -8,6 +9,7 @@ export * from "./browser-cache-shared-worker.js";
|
|
|
8
9
|
export * from "./browser-capabilities.js";
|
|
9
10
|
export * from "./persistent-cache.js";
|
|
10
11
|
export * from "./query-cache.js";
|
|
12
|
+
export * from "./sync-store.js";
|
|
11
13
|
export * from "./error-reporter.js";
|
|
12
14
|
/**
|
|
13
15
|
* Typed error for every rejected Gonvex operation. `code` distinguishes
|
|
@@ -42,6 +44,7 @@ export class GonvexClient {
|
|
|
42
44
|
socket;
|
|
43
45
|
handlers = new Map();
|
|
44
46
|
querySubscriptions = new Map();
|
|
47
|
+
syncSubscriptions = new Map();
|
|
45
48
|
oneShotQueries = new Map();
|
|
46
49
|
telemetryHandlers = new Set();
|
|
47
50
|
pendingMessages = [];
|
|
@@ -49,8 +52,10 @@ export class GonvexClient {
|
|
|
49
52
|
authInFlight = false;
|
|
50
53
|
telemetryEnabled = false;
|
|
51
54
|
queryCache;
|
|
55
|
+
syncStore;
|
|
52
56
|
queryCacheDirective;
|
|
53
57
|
queryCacheGeneration = 0;
|
|
58
|
+
syncIdentityGeneration = 0;
|
|
54
59
|
sessionScopeHandlers = new Set();
|
|
55
60
|
errorReporter;
|
|
56
61
|
reconnectTimer;
|
|
@@ -68,6 +73,7 @@ export class GonvexClient {
|
|
|
68
73
|
this.auth = authFromOptions(options);
|
|
69
74
|
this.telemetryEnabled = options.telemetry === true;
|
|
70
75
|
this.queryCache = createQueryCacheStore(options.queryCache);
|
|
76
|
+
this.syncStore = createSyncStore(options.sync);
|
|
71
77
|
this.timeouts = {
|
|
72
78
|
queryTimeoutMs: options.timeouts?.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
73
79
|
mutationTimeoutMs: options.timeouts?.mutationTimeoutMs ?? DEFAULT_MUTATION_TIMEOUT_MS,
|
|
@@ -76,6 +82,7 @@ export class GonvexClient {
|
|
|
76
82
|
if (options.errorReporting && options.project) {
|
|
77
83
|
this.errorReporter = new GonvexErrorReporter({ endpoint: url, project: options.project, tenant: options.tenant, ...options.errorReporting });
|
|
78
84
|
}
|
|
85
|
+
this.recoverWarmSyncDirective();
|
|
79
86
|
}
|
|
80
87
|
connectionState() {
|
|
81
88
|
const inflightMutations = countPendingCalls(this.pendingCalls, "mutation");
|
|
@@ -107,13 +114,19 @@ export class GonvexClient {
|
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
setAuth(auth) {
|
|
110
|
-
const
|
|
117
|
+
const nextAuth = { ...this.auth, ...auth };
|
|
118
|
+
const tokenScopeChanged = hasOwn(auth, "token")
|
|
119
|
+
&& auth.token !== this.auth.token
|
|
120
|
+
&& !sameAuthTokenIdentity(this.auth, nextAuth);
|
|
121
|
+
const scopeMayChange = tokenScopeChanged
|
|
111
122
|
|| (hasOwn(auth, "tenant") && auth.tenant !== this.auth.tenant)
|
|
112
123
|
|| (hasOwn(auth, "project") && auth.project !== this.auth.project);
|
|
113
124
|
if (scopeMayChange) {
|
|
114
125
|
this.resetQueryCacheScope();
|
|
115
126
|
}
|
|
116
|
-
this.auth =
|
|
127
|
+
this.auth = nextAuth;
|
|
128
|
+
if (scopeMayChange)
|
|
129
|
+
this.recoverWarmSyncDirective();
|
|
117
130
|
if (auth.tenant !== undefined)
|
|
118
131
|
this.errorReporter?.setTenant(auth.tenant);
|
|
119
132
|
if (auth.project !== undefined)
|
|
@@ -213,12 +226,17 @@ export class GonvexClient {
|
|
|
213
226
|
}
|
|
214
227
|
this.oneShotQueries.clear();
|
|
215
228
|
this.rejectPendingCalls((call) => new GonvexClientError(`Gonvex client was closed while waiting for ${call.kind} ${call.path}`, { code: "closed", path: call.path, operation: call.kind }));
|
|
229
|
+
for (const subscription of this.syncSubscriptions.values()) {
|
|
230
|
+
this.clearSyncRetry(subscription);
|
|
231
|
+
}
|
|
216
232
|
this.handlers.clear();
|
|
217
233
|
this.querySubscriptions.clear();
|
|
234
|
+
this.syncSubscriptions.clear();
|
|
218
235
|
this.sessionScopeHandlers.clear();
|
|
219
236
|
this.queryCacheGeneration += 1;
|
|
220
237
|
this.queryCacheDirective = undefined;
|
|
221
238
|
this.queryCache?.close();
|
|
239
|
+
this.syncStore?.close();
|
|
222
240
|
this.errorReporter?.close();
|
|
223
241
|
const socket = this.socket;
|
|
224
242
|
this.socket = undefined;
|
|
@@ -461,6 +479,261 @@ export class GonvexClient {
|
|
|
461
479
|
},
|
|
462
480
|
};
|
|
463
481
|
}
|
|
482
|
+
subscribeSync(ref, args = {}, onMessage) {
|
|
483
|
+
this.connect();
|
|
484
|
+
const key = querySubscriptionKey(ref, args);
|
|
485
|
+
const existing = this.syncSubscriptions.get(key);
|
|
486
|
+
if (existing) {
|
|
487
|
+
existing.listeners.add(onMessage);
|
|
488
|
+
if (existing.lastMessage) {
|
|
489
|
+
queueMicrotask(() => {
|
|
490
|
+
if (existing.listeners.has(onMessage) && existing.lastMessage)
|
|
491
|
+
onMessage(existing.lastMessage);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return () => this.unsubscribeSyncListener(key, onMessage);
|
|
495
|
+
}
|
|
496
|
+
const subscription = {
|
|
497
|
+
id: randomID(),
|
|
498
|
+
key,
|
|
499
|
+
path: ref.path,
|
|
500
|
+
args,
|
|
501
|
+
listeners: new Set([onMessage]),
|
|
502
|
+
rows: [],
|
|
503
|
+
keyField: "id",
|
|
504
|
+
opening: false,
|
|
505
|
+
persistence: Promise.resolve(),
|
|
506
|
+
retryAttempt: 0,
|
|
507
|
+
};
|
|
508
|
+
this.syncSubscriptions.set(key, subscription);
|
|
509
|
+
this.handlers.set(subscription.id, (message) => this.handleSyncMessage(subscription, message));
|
|
510
|
+
this.startSync(subscription);
|
|
511
|
+
return () => this.unsubscribeSyncListener(key, onMessage);
|
|
512
|
+
}
|
|
513
|
+
watchSync(ref, args = {}) {
|
|
514
|
+
let latest;
|
|
515
|
+
let latestError;
|
|
516
|
+
let isUpToDate = false;
|
|
517
|
+
const updateHandlers = new Set();
|
|
518
|
+
const notify = () => {
|
|
519
|
+
for (const handler of updateHandlers)
|
|
520
|
+
handler();
|
|
521
|
+
};
|
|
522
|
+
const unsubscribe = this.subscribeSync(ref, args, (message) => {
|
|
523
|
+
if (message.type === "sync.snapshot") {
|
|
524
|
+
latest = message.result;
|
|
525
|
+
latestError = undefined;
|
|
526
|
+
notify();
|
|
527
|
+
}
|
|
528
|
+
else if (message.type === "sync.ready") {
|
|
529
|
+
isUpToDate = true;
|
|
530
|
+
notify();
|
|
531
|
+
}
|
|
532
|
+
else if (message.type === "sync.error") {
|
|
533
|
+
latestError = new Error(message.error);
|
|
534
|
+
notify();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
const unsubscribeScope = this.onSessionScopeChange(() => {
|
|
538
|
+
latest = undefined;
|
|
539
|
+
latestError = undefined;
|
|
540
|
+
isUpToDate = false;
|
|
541
|
+
notify();
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
localSyncResult() {
|
|
545
|
+
if (latestError)
|
|
546
|
+
throw latestError;
|
|
547
|
+
return latest;
|
|
548
|
+
},
|
|
549
|
+
status() {
|
|
550
|
+
return { isLoading: latest === undefined, isUpToDate };
|
|
551
|
+
},
|
|
552
|
+
onUpdate(handler) {
|
|
553
|
+
updateHandlers.add(handler);
|
|
554
|
+
return () => {
|
|
555
|
+
updateHandlers.delete(handler);
|
|
556
|
+
if (updateHandlers.size === 0) {
|
|
557
|
+
unsubscribe();
|
|
558
|
+
unsubscribeScope();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
handleSyncMessage(subscription, message) {
|
|
565
|
+
if (message.type === "sync.snapshot") {
|
|
566
|
+
this.clearSyncRetry(subscription, true);
|
|
567
|
+
subscription.opening = false;
|
|
568
|
+
subscription.cursor = message.cursor;
|
|
569
|
+
subscription.keyField = message.key;
|
|
570
|
+
subscription.orderBy = message.orderBy;
|
|
571
|
+
subscription.orderDirection = message.orderDirection;
|
|
572
|
+
subscription.maxRows = message.maxRows;
|
|
573
|
+
subscription.maxBytes = message.maxBytes;
|
|
574
|
+
subscription.rows = boundSyncRows(message.result, message.key, message.maxRows, message.maxBytes, message.orderBy, message.orderDirection);
|
|
575
|
+
const snapshot = { ...message, result: subscription.rows };
|
|
576
|
+
subscription.lastMessage = snapshot;
|
|
577
|
+
this.emitSyncMessage(subscription, snapshot);
|
|
578
|
+
this.persistSyncSnapshot(subscription);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (message.type === "sync.delta") {
|
|
582
|
+
if (subscription.cursor && (message.cursor.epoch !== subscription.cursor.epoch
|
|
583
|
+
|| message.cursor.revision <= subscription.cursor.revision))
|
|
584
|
+
return;
|
|
585
|
+
this.clearSyncRetry(subscription, true);
|
|
586
|
+
subscription.cursor = message.cursor;
|
|
587
|
+
subscription.rows = applySyncDelta(subscription.rows, subscription.keyField, message.upserts ?? [], message.deleted ?? [], subscription.maxRows, subscription.maxBytes, subscription.orderBy, subscription.orderDirection);
|
|
588
|
+
const snapshot = {
|
|
589
|
+
type: "sync.snapshot",
|
|
590
|
+
id: subscription.id,
|
|
591
|
+
path: subscription.path,
|
|
592
|
+
result: subscription.rows,
|
|
593
|
+
cursor: message.cursor,
|
|
594
|
+
key: subscription.keyField,
|
|
595
|
+
orderBy: subscription.orderBy,
|
|
596
|
+
orderDirection: subscription.orderDirection,
|
|
597
|
+
maxRows: subscription.maxRows,
|
|
598
|
+
maxBytes: subscription.maxBytes,
|
|
599
|
+
};
|
|
600
|
+
subscription.lastMessage = snapshot;
|
|
601
|
+
this.emitSyncMessage(subscription, snapshot);
|
|
602
|
+
this.persistSyncDelta(subscription, message.upserts ?? [], message.deleted ?? []);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (message.type === "sync.reset") {
|
|
606
|
+
this.clearSyncRetry(subscription, true);
|
|
607
|
+
subscription.cursor = undefined;
|
|
608
|
+
subscription.rows = [];
|
|
609
|
+
subscription.lastMessage = undefined;
|
|
610
|
+
subscription.opening = false;
|
|
611
|
+
const directive = this.queryCacheDirective;
|
|
612
|
+
const store = this.syncStore;
|
|
613
|
+
if (directive && store) {
|
|
614
|
+
this.enqueueSyncPersistence(subscription, () => store.delete(directive.scope, subscription.path, subscription.args));
|
|
615
|
+
}
|
|
616
|
+
queueMicrotask(() => this.sendSyncOpen(subscription));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (message.type === "sync.ready") {
|
|
620
|
+
this.clearSyncRetry(subscription, true);
|
|
621
|
+
subscription.opening = false;
|
|
622
|
+
subscription.cursor = message.cursor;
|
|
623
|
+
}
|
|
624
|
+
if (message.type === "sync.error") {
|
|
625
|
+
subscription.opening = false;
|
|
626
|
+
this.scheduleSyncRetry(subscription);
|
|
627
|
+
}
|
|
628
|
+
this.emitSyncMessage(subscription, message);
|
|
629
|
+
}
|
|
630
|
+
emitSyncMessage(subscription, message) {
|
|
631
|
+
for (const listener of Array.from(subscription.listeners))
|
|
632
|
+
listener(message);
|
|
633
|
+
}
|
|
634
|
+
startSync(subscription) {
|
|
635
|
+
const directive = this.queryCacheDirective;
|
|
636
|
+
const store = this.syncStore;
|
|
637
|
+
if (!directive)
|
|
638
|
+
return;
|
|
639
|
+
if (!store) {
|
|
640
|
+
this.sendSyncOpen(subscription);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const generation = this.queryCacheGeneration;
|
|
644
|
+
if (subscription.cacheReadGeneration === generation)
|
|
645
|
+
return;
|
|
646
|
+
subscription.cacheReadGeneration = generation;
|
|
647
|
+
void store.load(directive.scope, subscription.path, subscription.args).then((cached) => {
|
|
648
|
+
if (this.syncSubscriptions.get(subscription.key) !== subscription
|
|
649
|
+
|| this.queryCacheGeneration !== generation
|
|
650
|
+
|| this.queryCacheDirective?.scope !== directive.scope)
|
|
651
|
+
return;
|
|
652
|
+
if (cached) {
|
|
653
|
+
subscription.rows = cached.rows;
|
|
654
|
+
subscription.cursor = cached.cursor;
|
|
655
|
+
subscription.keyField = cached.keyField;
|
|
656
|
+
subscription.orderBy = cached.orderBy;
|
|
657
|
+
subscription.orderDirection = cached.orderDirection;
|
|
658
|
+
subscription.maxRows = cached.maxRows;
|
|
659
|
+
subscription.maxBytes = cached.maxBytes;
|
|
660
|
+
const message = {
|
|
661
|
+
type: "sync.snapshot",
|
|
662
|
+
id: subscription.id,
|
|
663
|
+
path: subscription.path,
|
|
664
|
+
result: cached.rows,
|
|
665
|
+
cursor: cached.cursor,
|
|
666
|
+
key: cached.keyField,
|
|
667
|
+
orderBy: cached.orderBy,
|
|
668
|
+
orderDirection: cached.orderDirection,
|
|
669
|
+
maxRows: cached.maxRows,
|
|
670
|
+
maxBytes: cached.maxBytes,
|
|
671
|
+
};
|
|
672
|
+
subscription.lastMessage = message;
|
|
673
|
+
this.emitSyncMessage(subscription, message);
|
|
674
|
+
}
|
|
675
|
+
this.sendSyncOpen(subscription);
|
|
676
|
+
}).catch(() => this.sendSyncOpen(subscription));
|
|
677
|
+
}
|
|
678
|
+
sendSyncOpen(subscription) {
|
|
679
|
+
if (subscription.listeners.size === 0 || subscription.opening)
|
|
680
|
+
return;
|
|
681
|
+
subscription.opening = true;
|
|
682
|
+
subscription.socketGeneration = this.socketGeneration;
|
|
683
|
+
this.send({
|
|
684
|
+
type: "sync.open",
|
|
685
|
+
id: subscription.id,
|
|
686
|
+
path: subscription.path,
|
|
687
|
+
args: subscription.args,
|
|
688
|
+
cursor: subscription.cursor,
|
|
689
|
+
keys: subscription.rows.map((row) => syncRowKey(row, subscription.keyField)).filter(Boolean),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
unsubscribeSyncListener(key, listener) {
|
|
693
|
+
const subscription = this.syncSubscriptions.get(key);
|
|
694
|
+
if (!subscription)
|
|
695
|
+
return;
|
|
696
|
+
subscription.listeners.delete(listener);
|
|
697
|
+
if (subscription.listeners.size > 0)
|
|
698
|
+
return;
|
|
699
|
+
this.clearSyncRetry(subscription);
|
|
700
|
+
this.syncSubscriptions.delete(key);
|
|
701
|
+
this.handlers.delete(subscription.id);
|
|
702
|
+
this.send({ type: "sync.close", id: subscription.id });
|
|
703
|
+
}
|
|
704
|
+
persistSyncSnapshot(subscription) {
|
|
705
|
+
const directive = this.queryCacheDirective;
|
|
706
|
+
const store = this.syncStore;
|
|
707
|
+
if (!directive || !store || !subscription.cursor)
|
|
708
|
+
return;
|
|
709
|
+
const value = {
|
|
710
|
+
rows: subscription.rows,
|
|
711
|
+
cursor: subscription.cursor,
|
|
712
|
+
keyField: subscription.keyField,
|
|
713
|
+
orderBy: subscription.orderBy,
|
|
714
|
+
orderDirection: subscription.orderDirection,
|
|
715
|
+
maxRows: subscription.maxRows,
|
|
716
|
+
maxBytes: subscription.maxBytes,
|
|
717
|
+
};
|
|
718
|
+
this.enqueueSyncPersistence(subscription, () => store.replace(directive.scope, subscription.path, subscription.args, value));
|
|
719
|
+
}
|
|
720
|
+
persistSyncDelta(subscription, upserts, deleted) {
|
|
721
|
+
const directive = this.queryCacheDirective;
|
|
722
|
+
const store = this.syncStore;
|
|
723
|
+
if (!directive || !store || !subscription.cursor)
|
|
724
|
+
return;
|
|
725
|
+
const value = {
|
|
726
|
+
cursor: subscription.cursor,
|
|
727
|
+
keyField: subscription.keyField,
|
|
728
|
+
orderBy: subscription.orderBy,
|
|
729
|
+
orderDirection: subscription.orderDirection,
|
|
730
|
+
upserts,
|
|
731
|
+
deleted,
|
|
732
|
+
maxRows: subscription.maxRows,
|
|
733
|
+
maxBytes: subscription.maxBytes,
|
|
734
|
+
};
|
|
735
|
+
this.enqueueSyncPersistence(subscription, () => store.applyDelta(directive.scope, subscription.path, subscription.args, value));
|
|
736
|
+
}
|
|
464
737
|
mutation(ref, args = {}, options = {}) {
|
|
465
738
|
return this.call("mutation", ref, args, options.timeoutMs ?? this.timeouts.mutationTimeoutMs);
|
|
466
739
|
}
|
|
@@ -617,6 +890,39 @@ export class GonvexClient {
|
|
|
617
890
|
args: subscription.args,
|
|
618
891
|
});
|
|
619
892
|
}
|
|
893
|
+
enqueueSyncPersistence(subscription, operation) {
|
|
894
|
+
subscription.persistence = subscription.persistence
|
|
895
|
+
.catch(() => undefined)
|
|
896
|
+
.then(operation)
|
|
897
|
+
.catch(() => undefined);
|
|
898
|
+
}
|
|
899
|
+
scheduleSyncRetry(subscription) {
|
|
900
|
+
if (this.manuallyClosed
|
|
901
|
+
|| subscription.retryTimer
|
|
902
|
+
|| subscription.listeners.size === 0
|
|
903
|
+
|| this.syncSubscriptions.get(subscription.key) !== subscription)
|
|
904
|
+
return;
|
|
905
|
+
const delay = Math.min(250 * (2 ** subscription.retryAttempt), 5_000);
|
|
906
|
+
subscription.retryAttempt += 1;
|
|
907
|
+
subscription.retryTimer = setTimeout(() => {
|
|
908
|
+
subscription.retryTimer = undefined;
|
|
909
|
+
if (this.manuallyClosed
|
|
910
|
+
|| !this.isWebSocketConnected
|
|
911
|
+
|| subscription.listeners.size === 0
|
|
912
|
+
|| this.syncSubscriptions.get(subscription.key) !== subscription)
|
|
913
|
+
return;
|
|
914
|
+
subscription.opening = false;
|
|
915
|
+
this.sendSyncOpen(subscription);
|
|
916
|
+
}, delay);
|
|
917
|
+
}
|
|
918
|
+
clearSyncRetry(subscription, resetAttempt = false) {
|
|
919
|
+
if (subscription.retryTimer) {
|
|
920
|
+
clearTimeout(subscription.retryTimer);
|
|
921
|
+
subscription.retryTimer = undefined;
|
|
922
|
+
}
|
|
923
|
+
if (resetAttempt)
|
|
924
|
+
subscription.retryAttempt = 0;
|
|
925
|
+
}
|
|
620
926
|
requestSubscriptionSnapshot(subscription) {
|
|
621
927
|
subscription.socketGeneration = undefined;
|
|
622
928
|
this.sendSubscription(subscription);
|
|
@@ -639,6 +945,14 @@ export class GonvexClient {
|
|
|
639
945
|
for (const query of this.oneShotQueries.values()) {
|
|
640
946
|
this.sendOneShotQuery(query);
|
|
641
947
|
}
|
|
948
|
+
for (const subscription of this.syncSubscriptions.values()) {
|
|
949
|
+
if (subscription.listeners.size === 0)
|
|
950
|
+
continue;
|
|
951
|
+
this.clearSyncRetry(subscription, true);
|
|
952
|
+
subscription.opening = false;
|
|
953
|
+
subscription.socketGeneration = undefined;
|
|
954
|
+
this.sendSyncOpen(subscription);
|
|
955
|
+
}
|
|
642
956
|
}
|
|
643
957
|
scheduleReconnect() {
|
|
644
958
|
if (this.manuallyClosed || this.reconnectTimer)
|
|
@@ -667,9 +981,30 @@ export class GonvexClient {
|
|
|
667
981
|
this.resetQueryCacheScope();
|
|
668
982
|
}
|
|
669
983
|
this.queryCacheDirective = value;
|
|
984
|
+
const identity = authIdentityKey(this.auth);
|
|
985
|
+
if (identity)
|
|
986
|
+
void this.syncStore?.saveDirective(identity, value).catch(() => undefined);
|
|
670
987
|
for (const subscription of this.querySubscriptions.values()) {
|
|
671
988
|
this.startQueryCacheRead(subscription);
|
|
672
989
|
}
|
|
990
|
+
for (const subscription of this.syncSubscriptions.values()) {
|
|
991
|
+
this.startSync(subscription);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
recoverWarmSyncDirective() {
|
|
995
|
+
const store = this.syncStore;
|
|
996
|
+
const identity = authIdentityKey(this.auth);
|
|
997
|
+
const generation = ++this.syncIdentityGeneration;
|
|
998
|
+
if (!store || !identity)
|
|
999
|
+
return;
|
|
1000
|
+
void store.loadDirective(identity).then((directive) => {
|
|
1001
|
+
if (generation !== this.syncIdentityGeneration
|
|
1002
|
+
|| authIdentityKey(this.auth) !== identity
|
|
1003
|
+
|| this.queryCacheDirective
|
|
1004
|
+
|| !validQueryCacheDirective(directive))
|
|
1005
|
+
return;
|
|
1006
|
+
this.installQueryCacheDirective(directive);
|
|
1007
|
+
}).catch(() => undefined);
|
|
673
1008
|
}
|
|
674
1009
|
resetQueryCacheScope() {
|
|
675
1010
|
const hadScope = this.queryCacheDirective !== undefined;
|
|
@@ -680,7 +1015,15 @@ export class GonvexClient {
|
|
|
680
1015
|
subscription.serverSettled = false;
|
|
681
1016
|
subscription.cacheReadGeneration = undefined;
|
|
682
1017
|
}
|
|
683
|
-
|
|
1018
|
+
for (const subscription of this.syncSubscriptions.values()) {
|
|
1019
|
+
this.clearSyncRetry(subscription, true);
|
|
1020
|
+
subscription.rows = [];
|
|
1021
|
+
subscription.cursor = undefined;
|
|
1022
|
+
subscription.lastMessage = undefined;
|
|
1023
|
+
subscription.cacheReadGeneration = undefined;
|
|
1024
|
+
subscription.opening = false;
|
|
1025
|
+
}
|
|
1026
|
+
if (hadScope || this.querySubscriptions.size > 0 || this.syncSubscriptions.size > 0) {
|
|
684
1027
|
for (const handler of this.sessionScopeHandlers)
|
|
685
1028
|
handler();
|
|
686
1029
|
}
|
|
@@ -857,6 +1200,65 @@ function stableStringify(value) {
|
|
|
857
1200
|
function sameRevision(left, right) {
|
|
858
1201
|
return !!right && left.epoch === right.epoch && left.sequence === right.sequence;
|
|
859
1202
|
}
|
|
1203
|
+
function boundSyncRows(rows, keyField, maxRows, maxBytes, orderBy, orderDirection) {
|
|
1204
|
+
const kept = [];
|
|
1205
|
+
const seen = new Set();
|
|
1206
|
+
let bytes = 0;
|
|
1207
|
+
for (const row of sortClientSyncRows(rows, orderBy, orderDirection)) {
|
|
1208
|
+
const key = syncRowKey(row, keyField);
|
|
1209
|
+
if (!key || seen.has(key))
|
|
1210
|
+
continue;
|
|
1211
|
+
const size = syncJSONSize(row);
|
|
1212
|
+
if (maxRows && kept.length >= maxRows)
|
|
1213
|
+
break;
|
|
1214
|
+
if (maxBytes && bytes + size > maxBytes)
|
|
1215
|
+
break;
|
|
1216
|
+
kept.push(row);
|
|
1217
|
+
seen.add(key);
|
|
1218
|
+
bytes += size;
|
|
1219
|
+
}
|
|
1220
|
+
return kept;
|
|
1221
|
+
}
|
|
1222
|
+
function applySyncDelta(current, keyField, upserts, deleted, maxRows, maxBytes, orderBy, orderDirection) {
|
|
1223
|
+
const deletedSet = new Set(deleted);
|
|
1224
|
+
const upsertKeys = new Set(upserts.map((row) => syncRowKey(row, keyField)).filter(Boolean));
|
|
1225
|
+
const remainder = current.filter((row) => {
|
|
1226
|
+
const key = syncRowKey(row, keyField);
|
|
1227
|
+
return key && !deletedSet.has(key) && !upsertKeys.has(key);
|
|
1228
|
+
});
|
|
1229
|
+
return boundSyncRows([...upserts, ...remainder], keyField, maxRows, maxBytes, orderBy, orderDirection);
|
|
1230
|
+
}
|
|
1231
|
+
function sortClientSyncRows(rows, orderBy, orderDirection) {
|
|
1232
|
+
if (!orderBy)
|
|
1233
|
+
return rows;
|
|
1234
|
+
const direction = orderDirection === "asc" ? 1 : -1;
|
|
1235
|
+
return [...rows].sort((left, right) => {
|
|
1236
|
+
const leftValue = syncOrderValue(left, orderBy);
|
|
1237
|
+
const rightValue = syncOrderValue(right, orderBy);
|
|
1238
|
+
if (leftValue === rightValue)
|
|
1239
|
+
return 0;
|
|
1240
|
+
if (leftValue === null)
|
|
1241
|
+
return 1;
|
|
1242
|
+
if (rightValue === null)
|
|
1243
|
+
return -1;
|
|
1244
|
+
return leftValue < rightValue ? -direction : direction;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function syncOrderValue(value, orderBy) {
|
|
1248
|
+
if (!value || Array.isArray(value) || typeof value !== "object")
|
|
1249
|
+
return null;
|
|
1250
|
+
const candidate = value[orderBy];
|
|
1251
|
+
return typeof candidate === "string" || typeof candidate === "number" ? candidate : null;
|
|
1252
|
+
}
|
|
1253
|
+
function syncRowKey(value, keyField) {
|
|
1254
|
+
if (!value || Array.isArray(value) || typeof value !== "object")
|
|
1255
|
+
return "";
|
|
1256
|
+
const key = value[keyField];
|
|
1257
|
+
return key === null || key === undefined ? "" : String(key);
|
|
1258
|
+
}
|
|
1259
|
+
function syncJSONSize(value) {
|
|
1260
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
1261
|
+
}
|
|
860
1262
|
function applyKeyedPatch(previous, patch) {
|
|
861
1263
|
const rows = new Map();
|
|
862
1264
|
for (const row of previous) {
|
|
@@ -900,6 +1302,34 @@ function authFromOptions(options) {
|
|
|
900
1302
|
telemetry: options.telemetry,
|
|
901
1303
|
};
|
|
902
1304
|
}
|
|
1305
|
+
function authIdentityKey(auth) {
|
|
1306
|
+
if (!auth.token || !auth.tenant)
|
|
1307
|
+
return "";
|
|
1308
|
+
const parts = auth.token.split(".");
|
|
1309
|
+
if (parts.length < 2)
|
|
1310
|
+
return "";
|
|
1311
|
+
try {
|
|
1312
|
+
const encoded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1313
|
+
const padded = encoded.padEnd(Math.ceil(encoded.length / 4) * 4, "=");
|
|
1314
|
+
const payload = JSON.parse(globalThis.atob(padded));
|
|
1315
|
+
if (typeof payload.sub !== "string" || !payload.sub.trim())
|
|
1316
|
+
return "";
|
|
1317
|
+
return [
|
|
1318
|
+
auth.project ?? "",
|
|
1319
|
+
auth.tenant,
|
|
1320
|
+
typeof payload.iss === "string" ? payload.iss : "",
|
|
1321
|
+
payload.sub,
|
|
1322
|
+
].join("\u0000");
|
|
1323
|
+
}
|
|
1324
|
+
catch {
|
|
1325
|
+
return "";
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function sameAuthTokenIdentity(left, right) {
|
|
1329
|
+
const leftIdentity = authIdentityKey(left);
|
|
1330
|
+
const rightIdentity = authIdentityKey(right);
|
|
1331
|
+
return leftIdentity !== "" && leftIdentity === rightIdentity;
|
|
1332
|
+
}
|
|
903
1333
|
function queryCacheDirectiveFromAuthResult(result) {
|
|
904
1334
|
if (!isJsonRecord(result))
|
|
905
1335
|
return undefined;
|