@gonvex/client 0.1.24 → 0.1.26

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
@@ -2,6 +2,7 @@ import type { BrowserTelemetryInfo, JsonValue, MessageTrace, ServerCapabilities,
2
2
  import { type QueryCacheOptions, type QueryCacheStatus } from "./query-cache.js";
3
3
  import { type SyncStoreOptions } from "./sync-store.js";
4
4
  import { type ErrorReporterOptions } from "./error-reporter.js";
5
+ import { OptimisticOverlay, type OptimisticPatch } from "./optimistic.js";
5
6
  export * from "./cache.js";
6
7
  export * from "./cache-coordinator.js";
7
8
  export * from "./browser-cache.js";
@@ -12,8 +13,21 @@ export * from "./persistent-cache.js";
12
13
  export * from "./query-cache.js";
13
14
  export * from "./sync-store.js";
14
15
  export * from "./error-reporter.js";
16
+ export * from "./optimistic.js";
17
+ export * from "./outbox.js";
18
+ export * from "./signals.js";
15
19
  export type { QueryCacheDirective } from "@gonvex/protocol";
16
20
  type SubscriptionHandler = (message: ServerMessage) => void;
21
+ export type SyncReadyMessage = Extract<ServerMessage, {
22
+ type: "sync.ready";
23
+ }> & {
24
+ /** True when the server cut this collection at its row or byte budget. */
25
+ truncated?: boolean;
26
+ };
27
+ export type SyncMessage = Extract<ServerMessage, {
28
+ type: "sync.snapshot" | "sync.delta" | "sync.needHashes" | "sync.syncing" | "sync.reset" | "sync.error";
29
+ }> | SyncReadyMessage;
30
+ export type SyncSubscriptionHandler = (message: SyncMessage) => void;
17
31
  type WatchUpdateHandler = () => void;
18
32
  type TelemetryHandler = (event: GonvexTelemetryEvent) => void;
19
33
  type ConnectionStateHandler = (state: ConnectionState) => void;
@@ -31,7 +45,7 @@ export type GonvexClientErrorCode = "server" | "timeout" | "disconnected" | "clo
31
45
  * - `timeout`: no response arrived within the operation timeout. For
32
46
  * mutations/actions the write may or may not have been applied.
33
47
  * - `disconnected`: the socket dropped while the operation was pending.
34
- * Mutations/actions fail closed and are never replayed automatically.
48
+ * Mutations/actions fail closed unless a mutation opted into the outbox.
35
49
  * - `closed`: the client was explicitly closed.
36
50
  * - `auth`: authentication was rejected.
37
51
  */
@@ -69,12 +83,50 @@ export declare const DEFAULT_ACTION_TIMEOUT_MS = 60000;
69
83
  export type CallOptions = {
70
84
  /** Per-call override of the operation timeout. `0` disables. */
71
85
  timeoutMs?: number;
86
+ /** Ordered row changes to expose until the mutation settles. */
87
+ optimistic?: OptimisticPatch[];
88
+ /** Queue transport failures durably instead of rejecting. Default `reject`. */
89
+ offline?: "queue" | "reject";
90
+ };
91
+ /** Returned when an offline mutation has been accepted by the local outbox. */
92
+ export type QueuedMutationOutcome = {
93
+ status: "queued";
94
+ mutationId: string;
72
95
  };
96
+ export type GonvexAuthTokenFetcher = (args: {
97
+ /** True when the server just rejected the current token — bypass any cache. */
98
+ forceRefreshToken: boolean;
99
+ }) => Promise<string | null | undefined>;
73
100
  export type GonvexClientAuth = {
74
101
  project?: string;
75
102
  token?: string;
76
103
  tenant?: string;
77
104
  telemetry?: boolean;
105
+ /**
106
+ * Async source of the auth token, mirroring Convex's `fetchToken` contract.
107
+ * When installed, the client re-fetches before every auth send — on first
108
+ * connect, on every reconnect, and once more with `forceRefreshToken: true`
109
+ * when the server rejects the current token — so a socket that outlives a
110
+ * short-lived JWT (e.g. an ~1h Firebase ID token) reauthenticates with a
111
+ * live credential instead of replaying the expired one. A `token` passed in
112
+ * the same `setAuth` call is trusted and sent as-is; resolving `null` signs
113
+ * the session out; a rejected fetch keeps the currently installed token so
114
+ * an offline start is not signed out.
115
+ */
116
+ fetchToken?: GonvexAuthTokenFetcher;
117
+ /**
118
+ * Non-secret identity hint ({@link https://datatracker.ietf.org/doc/html/rfc7519 JWT}
119
+ * `sub` and `iss` claims) that stands in for a token when deriving the local
120
+ * cache identity. Lets a cold start with no usable token — e.g. an offline
121
+ * tab whose identity provider cannot refresh — recover the warm query-cache
122
+ * directive and serve cached reads. Never sent to the server; a parseable
123
+ * token always takes precedence, and both must derive the same key for the
124
+ * same user (persist the claims of the last token you installed).
125
+ */
126
+ identity?: {
127
+ sub: string;
128
+ iss?: string;
129
+ };
78
130
  };
79
131
  export type GonvexClientOptions = GonvexClientAuth & {
80
132
  queryCache?: false | QueryCacheOptions;
@@ -90,6 +142,14 @@ export type GonvexClientOptions = GonvexClientAuth & {
90
142
  */
91
143
  syncSubscriptionRetentionMs?: number;
92
144
  sync?: false | SyncStoreOptions;
145
+ /**
146
+ * Durable mutation queue settings. Every replay keeps its original
147
+ * idempotency key, making an accidental cross-tab double-send server-safe.
148
+ */
149
+ outbox?: {
150
+ databaseName?: string;
151
+ enabled?: boolean;
152
+ };
93
153
  errorReporting?: false | Omit<ErrorReporterOptions, "endpoint" | "project" | "tenant">;
94
154
  timeouts?: GonvexTimeoutOptions;
95
155
  };
@@ -124,6 +184,9 @@ export declare class GonvexClient {
124
184
  private auth;
125
185
  private authInFlight;
126
186
  private authWatchdogTimer;
187
+ private authFetchGeneration;
188
+ private authRetriedAfterError;
189
+ private readonly authErrorHandlers;
127
190
  private telemetryEnabled;
128
191
  private readonly queryCache;
129
192
  private readonly queryCacheWaitForScope;
@@ -131,6 +194,14 @@ export declare class GonvexClient {
131
194
  private readonly querySubscriptionRetentionMs;
132
195
  private readonly syncSubscriptionRetentionMs;
133
196
  private readonly syncStore;
197
+ private readonly mutationOutbox;
198
+ private readonly overlay;
199
+ private readonly optimisticMutationIds;
200
+ private readonly outboxReady;
201
+ private readonly unsubscribeOutbox;
202
+ private readonly unsubscribeOverlay;
203
+ private drainingOutbox;
204
+ private outboxDrainTimer;
134
205
  private queryCacheDirective;
135
206
  private queryCacheGeneration;
136
207
  private syncScopeGeneration;
@@ -149,12 +220,24 @@ export declare class GonvexClient {
149
220
  private connectionCount;
150
221
  private readonly timeouts;
151
222
  constructor(url: string, options?: GonvexClientOptions);
223
+ /** The client's materialized optimistic state for pending-row indicators. */
224
+ get optimisticOverlay(): OptimisticOverlay;
225
+ /** Number of mutations waiting for a definitive server result. */
226
+ outboxCount(): Promise<number>;
152
227
  connectionState(): ConnectionState;
153
228
  /** Metadata advertised by the runtime in its latest session.ready frame. */
154
229
  serverInfo(): Readonly<ServerCapabilities>;
155
230
  subscribeToConnectionState(handler: ConnectionStateHandler): () => void;
156
231
  private notifyConnectionState;
157
232
  setAuth(auth: GonvexClientAuth): void;
233
+ /**
234
+ * Subscribe to unrecoverable auth rejections: the server refused the
235
+ * credentials and, when a token fetcher is installed, a force-refreshed
236
+ * token did not fix it. Lets apps route to sign-in instead of silently
237
+ * degrading to an unauthenticated session.
238
+ */
239
+ onAuthError(handler: (error: string) => void): () => void;
240
+ private applyAuth;
158
241
  connect(): void;
159
242
  close(): void;
160
243
  private rejectPendingCalls;
@@ -171,7 +254,7 @@ export declare class GonvexClient {
171
254
  localQueryResult(): T | undefined;
172
255
  onUpdate(handler: WatchUpdateHandler): () => void;
173
256
  };
174
- subscribeSync(ref: FunctionReference, args: JsonValue | undefined, onMessage: SubscriptionHandler): () => void;
257
+ subscribeSync(ref: FunctionReference, args: JsonValue | undefined, onMessage: SyncSubscriptionHandler): () => void;
175
258
  watchSync<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue): {
176
259
  localSyncResult(): T[] | undefined;
177
260
  status(): {
@@ -183,6 +266,8 @@ export declare class GonvexClient {
183
266
  private handleSyncMessage;
184
267
  private acceptSyncReady;
185
268
  private emitSyncMessage;
269
+ private materializeSyncMessage;
270
+ private emitOptimisticCollection;
186
271
  private markSyncSubscriptionsOutOfDate;
187
272
  private startSync;
188
273
  private sendSyncOpen;
@@ -191,6 +276,15 @@ export declare class GonvexClient {
191
276
  private unsubscribeSyncListener;
192
277
  private persistSyncSnapshot;
193
278
  private persistSyncDelta;
279
+ private restoreOutbox;
280
+ private addOptimisticMutation;
281
+ private settleOptimisticMutation;
282
+ private rejectOptimisticMutation;
283
+ private drainOutbox;
284
+ private scheduleOutboxDrain;
285
+ mutation<T = JsonValue>(ref: FunctionReference, args: JsonValue, options: CallOptions & {
286
+ offline: "queue";
287
+ }): Promise<T | QueuedMutationOutcome>;
194
288
  mutation<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
195
289
  action<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
196
290
  query<T = JsonValue>(ref: FunctionReference, args?: JsonValue, options?: CallOptions): Promise<T>;
@@ -243,6 +337,10 @@ export declare class GonvexClient {
243
337
  private emitTelemetry;
244
338
  private reportTelemetry;
245
339
  private sendAuth;
340
+ private sendAuthFrame;
341
+ private fetchAndSendAuth;
342
+ private refreshRejectedAuth;
343
+ private notifyAuthError;
246
344
  private armAuthWatchdog;
247
345
  private send;
248
346
  private sendNow;