@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 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 scopeMayChange = (hasOwn(auth, "token") && auth.token !== this.auth.token)
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 = { ...this.auth, ...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
- if (hadScope || this.querySubscriptions.size > 0) {
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;