@reatomized/convex 0.0.0-stage → 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +110 -2
  3. package/dist/browser/logging.d.ts +39 -0
  4. package/dist/browser/logging.js +117 -0
  5. package/dist/browser/reatom/auth.d.ts +75 -0
  6. package/dist/browser/reatom/auth.js +329 -0
  7. package/dist/browser/reatom/client.d.ts +348 -0
  8. package/dist/browser/reatom/client.js +1138 -0
  9. package/dist/browser/reatom/connection_hooks.d.ts +11 -0
  10. package/dist/browser/reatom/connection_hooks.js +15 -0
  11. package/dist/browser/reatom/errors.d.ts +3 -0
  12. package/dist/browser/reatom/errors.js +8 -0
  13. package/dist/browser/reatom/index.d.ts +6 -0
  14. package/dist/browser/reatom/index.js +2 -0
  15. package/dist/browser/reatom/optimistic.d.ts +44 -0
  16. package/dist/browser/reatom/optimistic.js +70 -0
  17. package/dist/browser/reatom/paginated.d.ts +56 -0
  18. package/dist/browser/reatom/paginated.js +162 -0
  19. package/dist/browser/reatom/socket.d.ts +52 -0
  20. package/dist/browser/reatom/socket.js +523 -0
  21. package/dist/browser/sync/function_result.d.ts +21 -0
  22. package/dist/browser/sync/function_result.js +1 -0
  23. package/dist/browser/sync/metrics.d.ts +9 -0
  24. package/dist/browser/sync/metrics.js +43 -0
  25. package/dist/browser/sync/pagination.d.ts +22 -0
  26. package/dist/browser/sync/pagination.js +20 -0
  27. package/dist/browser/sync/protocol.d.ts +206 -0
  28. package/dist/browser/sync/protocol.js +75 -0
  29. package/dist/browser/sync/session.d.ts +1 -0
  30. package/dist/browser/sync/session.js +10 -0
  31. package/dist/browser/sync/udf_path_utils.d.ts +30 -0
  32. package/dist/browser/sync/udf_path_utils.js +38 -0
  33. package/dist/common/index.d.ts +11 -0
  34. package/dist/common/index.js +52 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/vendor/jwt-decode/LICENSE +21 -0
  38. package/dist/vendor/jwt-decode/index.d.ts +23 -0
  39. package/dist/vendor/jwt-decode/index.js +57 -0
  40. package/dist/vendor/long.d.ts +16 -0
  41. package/dist/vendor/long.js +297 -0
  42. package/package.json +59 -4
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Token fetch, server confirmation and refresh, with the state machine in an
3
+ * atom. By default the client takes 3 steps:
4
+ * 1. Fetch a possibly cached token and send it to the server.
5
+ * 2. Once the server confirms it, fetch a fresh token without the cache.
6
+ * 3. Refetch before each fresh token expires.
7
+ *
8
+ * With `initialAuthTokenReuse` step 2 is skipped: the cached token stays and
9
+ * its refetch is scheduled from its remaining lifetime, estimated with the
10
+ * server's clock skew. When the server rejects a token, the socket stops, a
11
+ * fresh token is fetched and sent, and the socket restarts.
12
+ */
13
+ import { atom, context, peek, wrap } from "@reatom/core";
14
+ import { jwtDecode } from "../../vendor/jwt-decode/index.js";
15
+ // setTimeout takes a 32-bit delay, about 24 days.
16
+ const MAXIMUM_REFRESH_DELAY = 20 * 24 * 60 * 60 * 1000; // 20 days
17
+ const MAX_TOKEN_CONFIRMATION_ATTEMPTS = 2;
18
+ export function reatomAuth(sync, options, name) {
19
+ const { logger, refreshTokenLeewaySeconds, initialAuthTokenReuse } = options;
20
+ const state = atom({ state: "noAuth" }, `${name}.state`);
21
+ const runtime = atom(() => ({
22
+ root: context(),
23
+ configVersion: 0,
24
+ lastRefreshChange: false,
25
+ tokenConfirmationAttempts: 0,
26
+ }), `${name}._runtime`);
27
+ const logVerbose = (message) => logger.logVerbose(`${message} [v${peek(runtime).configVersion}]`);
28
+ const setAuthState = (next) => {
29
+ const authStateForLog = next.state === "waitingForServerConfirmationOfFreshToken"
30
+ ? {
31
+ hadAuth: next.hadAuth,
32
+ state: next.state,
33
+ token: `...${next.token.slice(-7)}`,
34
+ }
35
+ : { state: next.state };
36
+ logVerbose(`setting auth state to ${JSON.stringify(authStateForLog)}`);
37
+ if (next.state === "waitingForScheduledRefetch" ||
38
+ next.state === "notRefetching" ||
39
+ next.state === "noAuth") {
40
+ peek(runtime).tokenConfirmationAttempts = 0;
41
+ }
42
+ const current = state();
43
+ if (current.state === "waitingForScheduledRefetch") {
44
+ clearTimeout(current.refetchTokenTimeoutId);
45
+ }
46
+ state.set(next);
47
+ };
48
+ const notifyRefreshChange = (isRefreshing) => {
49
+ const current = state();
50
+ const rt = peek(runtime);
51
+ if (current.state !== "noAuth" &&
52
+ current.state !== "initialRefetch" &&
53
+ current.config.onRefreshChange &&
54
+ rt.lastRefreshChange !== isRefreshing) {
55
+ rt.lastRefreshChange = isRefreshing;
56
+ current.config.onRefreshChange(isRefreshing);
57
+ }
58
+ };
59
+ // The only way into `noAuth`; pairs any pending `onRefreshChange(true)`.
60
+ const resetAuthState = () => {
61
+ notifyRefreshChange(false);
62
+ setAuthState({ state: "noAuth" });
63
+ };
64
+ const setAndReportAuthFailed = (onAuthChange) => {
65
+ onAuthChange(false);
66
+ resetAuthState();
67
+ };
68
+ /** `isFromOutdatedConfig` when `setConfig` or `stop` ran during the fetch. */
69
+ const fetchTokenAndGuardAgainstRace = async (fetchToken, fetchArgs) => {
70
+ const rt = peek(runtime);
71
+ const originalConfigVersion = ++rt.configVersion;
72
+ logVerbose(`fetching token with config version ${originalConfigVersion}`);
73
+ const token = await wrap(fetchToken(fetchArgs));
74
+ if (rt.configVersion !== originalConfigVersion) {
75
+ logVerbose(`stale config version, expected ${originalConfigVersion}, got ${rt.configVersion}`);
76
+ return { isFromOutdatedConfig: true };
77
+ }
78
+ return { isFromOutdatedConfig: false, value: token };
79
+ };
80
+ const decodeToken = (token) => {
81
+ try {
82
+ return jwtDecode(token);
83
+ }
84
+ catch (e) {
85
+ logVerbose(`Error decoding token: ${e instanceof Error ? e.message : "Unknown error"}`);
86
+ return null;
87
+ }
88
+ };
89
+ const scheduleTokenRefetch = (token, clientClockSkewMs) => {
90
+ const current = state();
91
+ if (current.state === "noAuth")
92
+ return;
93
+ const decodedToken = decodeToken(token);
94
+ if (!decodedToken) {
95
+ // The server only confirms JWTs, so this should not happen.
96
+ logger.error("Auth token is not a valid JWT, cannot refetch the token");
97
+ return;
98
+ }
99
+ // iat: issued at, exp: expires at, both UTC seconds.
100
+ const { iat, exp } = decodedToken;
101
+ if (!iat || !exp) {
102
+ logger.error("Auth token does not have required fields, cannot refetch the token");
103
+ return;
104
+ }
105
+ const fullLifetimeSeconds = exp - iat;
106
+ if (fullLifetimeSeconds <= 2) {
107
+ logger.error("Auth token does not live long enough, cannot refetch the token");
108
+ return;
109
+ }
110
+ let tokenValiditySeconds;
111
+ if (clientClockSkewMs !== undefined) {
112
+ // A cached token's remaining lifetime, on the server's clock. The server
113
+ // computes clientClockSkew = clientTs - serverReceiveTs. Connect latency
114
+ // makes the client look further behind, so the refetch runs early.
115
+ const estimatedServerNowSeconds = (Date.now() - clientClockSkewMs) / 1000;
116
+ tokenValiditySeconds = Math.max(0, exp - estimatedServerNowSeconds);
117
+ }
118
+ else {
119
+ // A fresh token lives `exp - iat` from now.
120
+ tokenValiditySeconds = fullLifetimeSeconds;
121
+ }
122
+ let delay = Math.min(MAXIMUM_REFRESH_DELAY, (tokenValiditySeconds - refreshTokenLeewaySeconds) * 1000);
123
+ if (delay <= 0) {
124
+ logger.warn(`Refetching auth token immediately, configured leeway ${refreshTokenLeewaySeconds}s is larger than the token's lifetime ${tokenValiditySeconds}s`);
125
+ delay = 0;
126
+ }
127
+ const refetchTokenTimeoutId = setTimeout(wrap(() => {
128
+ logVerbose("running scheduled token refetch");
129
+ void refetchToken();
130
+ }, peek(runtime).root), delay);
131
+ setAuthState({
132
+ state: "waitingForScheduledRefetch",
133
+ refetchTokenTimeoutId,
134
+ config: current.config,
135
+ });
136
+ logVerbose(`scheduled preemptive auth token refetching in ${delay}ms`);
137
+ };
138
+ // Force-fetches a token and schedules the next refetch once the server
139
+ // confirms it, so an active client never has to reauthenticate.
140
+ const refetchToken = async () => {
141
+ const current = state();
142
+ if (current.state === "noAuth")
143
+ return;
144
+ logVerbose("refetching auth token");
145
+ const token = await wrap(fetchTokenAndGuardAgainstRace(current.config.fetchToken, {
146
+ forceRefreshToken: true,
147
+ }));
148
+ if (token.isFromOutdatedConfig)
149
+ return;
150
+ if (token.value) {
151
+ if (sync.token() !== token.value) {
152
+ setAuthState({
153
+ state: "waitingForServerConfirmationOfFreshToken",
154
+ hadAuth: sync.hasAuth(),
155
+ token: token.value,
156
+ config: current.config,
157
+ });
158
+ sync.authenticate(token.value);
159
+ }
160
+ else {
161
+ setAuthState({ state: "notRefetching", config: current.config });
162
+ }
163
+ }
164
+ else {
165
+ logVerbose("refetching token failed");
166
+ if (sync.hasAuth()) {
167
+ sync.clearAuth();
168
+ }
169
+ setAndReportAuthFailed(current.config.onAuthChange);
170
+ }
171
+ // In case a scheduled refetch ran during a reauthentication.
172
+ logVerbose("restarting WS after auth token fetch (if currently stopped)");
173
+ sync.tryRestartSocket();
174
+ };
175
+ // Like `refetchToken`, but with the socket stopped so requests don't retry
176
+ // with the rejected token.
177
+ const tryToReauthenticate = async (serverMessage) => {
178
+ logVerbose(`attempting to reauthenticate: ${serverMessage.error}`);
179
+ const current = state();
180
+ const rt = peek(runtime);
181
+ if (
182
+ // No way to fetch another token.
183
+ current.state === "noAuth" ||
184
+ // A fresh token failed too often; give up instead of retrying forever.
185
+ (current.state === "waitingForServerConfirmationOfFreshToken" &&
186
+ rt.tokenConfirmationAttempts >= MAX_TOKEN_CONFIRMATION_ATTEMPTS)) {
187
+ logger.error(`Failed to authenticate: "${serverMessage.error}", check your server auth config`);
188
+ if (sync.hasAuth()) {
189
+ sync.dropAuth();
190
+ }
191
+ if (current.state !== "noAuth") {
192
+ setAndReportAuthFailed(current.config.onAuthChange);
193
+ }
194
+ return;
195
+ }
196
+ if (current.state === "waitingForServerConfirmationOfFreshToken") {
197
+ rt.tokenConfirmationAttempts++;
198
+ logVerbose(`retrying reauthentication, ${MAX_TOKEN_CONFIRMATION_ATTEMPTS - rt.tokenConfirmationAttempts} attempts remaining`);
199
+ }
200
+ notifyRefreshChange(true);
201
+ await wrap(sync.stopSocket());
202
+ const afterStop = state();
203
+ // `setConfig` or `stop` ran during the await.
204
+ if (afterStop.state === "noAuth")
205
+ return;
206
+ const token = await wrap(fetchTokenAndGuardAgainstRace(afterStop.config.fetchToken, {
207
+ forceRefreshToken: true,
208
+ }));
209
+ if (token.isFromOutdatedConfig)
210
+ return;
211
+ const latest = state();
212
+ // Another auth error gave up meanwhile.
213
+ if (latest.state === "noAuth")
214
+ return;
215
+ if (token.value && sync.token() !== token.value) {
216
+ sync.authenticate(token.value);
217
+ setAuthState({
218
+ state: "waitingForServerConfirmationOfFreshToken",
219
+ config: latest.config,
220
+ token: token.value,
221
+ hadAuth: latest.state === "notRefetching" ||
222
+ latest.state === "waitingForScheduledRefetch",
223
+ });
224
+ }
225
+ else {
226
+ logVerbose("reauthentication failed, could not fetch a new token");
227
+ if (sync.hasAuth()) {
228
+ sync.dropAuth();
229
+ }
230
+ setAndReportAuthFailed(latest.config.onAuthChange);
231
+ }
232
+ sync.tryRestartSocket();
233
+ };
234
+ return {
235
+ /** The auth state machine. */
236
+ state,
237
+ async setConfig(fetchToken, onChange, onRefreshChange) {
238
+ resetAuthState();
239
+ logVerbose("pausing WS for auth token fetch");
240
+ sync.pauseSocket();
241
+ const token = await wrap(fetchTokenAndGuardAgainstRace(fetchToken, {
242
+ forceRefreshToken: false,
243
+ }));
244
+ if (token.isFromOutdatedConfig)
245
+ return;
246
+ const config = {
247
+ fetchToken,
248
+ onAuthChange: onChange,
249
+ onRefreshChange,
250
+ };
251
+ if (token.value) {
252
+ setAuthState({
253
+ state: "waitingForServerConfirmationOfCachedToken",
254
+ config,
255
+ hasRetried: false,
256
+ });
257
+ sync.authenticate(token.value);
258
+ }
259
+ else {
260
+ setAuthState({ state: "initialRefetch", config });
261
+ // Try again with `forceRefreshToken: true`.
262
+ await wrap(refetchToken());
263
+ }
264
+ logVerbose("resuming WS after auth token fetch");
265
+ sync.resumeSocket();
266
+ },
267
+ onTransition(serverMessage) {
268
+ // Stale: the client has moved on to a newer identity.
269
+ if (!sync.isCurrentOrNewerAuthVersion(serverMessage.endVersion.identity)) {
270
+ return;
271
+ }
272
+ // Not an answer to `Authenticate`.
273
+ if (serverMessage.endVersion.identity <= serverMessage.startVersion.identity) {
274
+ return;
275
+ }
276
+ const current = state();
277
+ logVerbose(`auth state is ${current.state} when handling transition`);
278
+ // The server accepted the token: client and server identities agree.
279
+ sync.markAuthCompletion();
280
+ if (current.state === "waitingForServerConfirmationOfCachedToken") {
281
+ logVerbose("server confirmed auth token is valid");
282
+ const cachedToken = sync.token();
283
+ if (initialAuthTokenReuse && cachedToken) {
284
+ scheduleTokenRefetch(cachedToken, serverMessage.clientClockSkew);
285
+ }
286
+ else {
287
+ void refetchToken();
288
+ }
289
+ current.config.onAuthChange(true);
290
+ return;
291
+ }
292
+ if (current.state === "waitingForServerConfirmationOfFreshToken") {
293
+ logVerbose("server confirmed new auth token is valid");
294
+ notifyRefreshChange(false);
295
+ scheduleTokenRefetch(current.token);
296
+ peek(runtime).tokenConfirmationAttempts = 0;
297
+ // Read after scheduling, as convex@1.48.0 does: once the refetch is
298
+ // scheduled the state has no `hadAuth`, so every confirmed fresh
299
+ // token reports `true`.
300
+ const after = state();
301
+ if (!("hadAuth" in after && after.hadAuth)) {
302
+ current.config.onAuthChange(true);
303
+ }
304
+ }
305
+ },
306
+ onAuthError(serverMessage) {
307
+ const current = state().state;
308
+ // An expired-token error while a new token is already on its way.
309
+ if (serverMessage.authUpdateAttempted === false &&
310
+ (current === "waitingForServerConfirmationOfFreshToken" ||
311
+ current === "waitingForServerConfirmationOfCachedToken")) {
312
+ logVerbose("ignoring non-auth token expired error");
313
+ return;
314
+ }
315
+ // The error reports the version the server stayed at, hence `+ 1`.
316
+ if (!sync.isCurrentOrNewerAuthVersion(serverMessage.baseVersion + 1)) {
317
+ logVerbose("ignoring auth error for previous auth attempt");
318
+ return;
319
+ }
320
+ void tryToReauthenticate(serverMessage);
321
+ },
322
+ stop() {
323
+ resetAuthState();
324
+ // Drops a token fetch in progress.
325
+ peek(runtime).configVersion++;
326
+ logVerbose(`config version bumped to ${peek(runtime).configVersion}`);
327
+ },
328
+ };
329
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * A Convex sync client whose state lives in Reatom atoms.
3
+ *
4
+ * The query set is the set of connected query atoms: reading a query's
5
+ * `result` in a subscribed graph subscribes it on the server, and losing the
6
+ * last reader unsubscribes it. Query set changes made in one microtask go out
7
+ * as one `ModifyQuerySet`. A server transition updates only the queries it
8
+ * names; optimistic updates are replayed only while some are pending.
9
+ *
10
+ * State is per Reatom context: call the client's methods inside a frame of
11
+ * the context that owns it.
12
+ */
13
+ import { type Action, type AtomLike, type Computed } from "@reatom/core";
14
+ import { FunctionArgs, FunctionReference, FunctionReference_future, FunctionReturnType } from "convex/server";
15
+ import { Value } from "convex/values";
16
+ import { Logger } from "../logging.js";
17
+ import { FunctionResult } from "../sync/function_result.js";
18
+ import { OptimisticUpdate } from "convex/browser";
19
+ import { QueryJournal, RequestId, TS } from "../sync/protocol.js";
20
+ import { QueryToken } from "../sync/udf_path_utils.js";
21
+ import { AuthTokenFetcher } from "./auth.js";
22
+ import { ReatomPaginatedQuery } from "./paginated.js";
23
+ import { SocketState } from "./socket.js";
24
+ /**
25
+ * Options for {@link BaseConvexClient}.
26
+ *
27
+ * @public
28
+ */
29
+ export interface BaseConvexClientOptions {
30
+ /**
31
+ * Whether to prompt the user if they have unsaved changes pending
32
+ * when navigating away or closing a web page.
33
+ *
34
+ * This is only possible when the `window` object exists, i.e. in a browser.
35
+ *
36
+ * The default value is `true` in browsers.
37
+ */
38
+ unsavedChangesWarning?: boolean;
39
+ /**
40
+ * Specifies an alternate
41
+ * [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
42
+ * constructor to use for client communication with the Convex cloud.
43
+ * The default behavior is to use `WebSocket` from the global environment.
44
+ */
45
+ webSocketConstructor?: typeof WebSocket;
46
+ /**
47
+ * Adds additional logging for debugging purposes.
48
+ *
49
+ * The default value is `false`.
50
+ */
51
+ verbose?: boolean;
52
+ /**
53
+ * A logger, `true`, or `false`. If not provided or `true`, logs to the console.
54
+ * If `false`, logs are not printed anywhere.
55
+ *
56
+ * You can construct your own logger to customize logging to log elsewhere.
57
+ * A logger is an object with 4 methods: log(), warn(), error(), and logVerbose().
58
+ * These methods can receive multiple arguments of any types, like console.log().
59
+ */
60
+ logger?: Logger | boolean;
61
+ /**
62
+ * Sends additional metrics to Convex for debugging purposes.
63
+ *
64
+ * The default value is `false`.
65
+ */
66
+ reportDebugInfoToConvex?: boolean;
67
+ /**
68
+ * This API is experimental: it may change or disappear.
69
+ *
70
+ * A function to call on receiving abnormal WebSocket close messages from the
71
+ * connected Convex deployment. The content of these messages is not stable,
72
+ * it is an implementation detail that may change.
73
+ *
74
+ * Consider this API an observability stopgap until higher level codes with
75
+ * recommendations on what to do are available, which could be a more stable
76
+ * interface instead of `string`.
77
+ *
78
+ * Check `connectionState` for more quantitative metrics about connection status.
79
+ */
80
+ onServerDisconnectError?: (message: string) => void;
81
+ /**
82
+ * Skip validating that the Convex deployment URL looks like
83
+ * `https://happy-animal-123.convex.cloud` or localhost.
84
+ *
85
+ * This can be useful if running a self-hosted Convex backend that uses a different
86
+ * URL.
87
+ *
88
+ * The default value is `false`
89
+ */
90
+ skipConvexDeploymentUrlCheck?: boolean;
91
+ /**
92
+ * If using auth, the number of seconds before a token expires that we should refresh it.
93
+ *
94
+ * The default value is `10`.
95
+ */
96
+ authRefreshTokenLeewaySeconds?: number;
97
+ /**
98
+ * This API is experimental: it may change or disappear.
99
+ *
100
+ * Whether query, mutation, and action requests should be held back
101
+ * until the first auth token can be sent.
102
+ *
103
+ * Opting into this behavior works well for pages that should
104
+ * only be viewed by authenticated clients.
105
+ *
106
+ * Defaults to false, not waiting for an auth token.
107
+ */
108
+ expectAuth?: boolean;
109
+ /**
110
+ * This API is experimental: it may change or disappear.
111
+ *
112
+ * When true, the client reuses the initial cached auth token instead
113
+ * of immediately fetching a fresh one. This avoids a second
114
+ * Authenticate message that causes the server to re-execute all
115
+ * authenticated queries.
116
+ *
117
+ * The cached token's remaining lifetime is estimated using the
118
+ * server's clock skew measurement, and a refresh is scheduled
119
+ * before it expires.
120
+ *
121
+ * Defaults to false, preserving the original behavior of immediately
122
+ * fetching a fresh token after the cached token is confirmed.
123
+ */
124
+ initialAuthTokenReuse?: boolean;
125
+ }
126
+ /**
127
+ * State describing the client's connection with the Convex backend.
128
+ *
129
+ * @public
130
+ */
131
+ export type ConnectionState = {
132
+ hasInflightRequests: boolean;
133
+ isWebSocketConnected: boolean;
134
+ timeOfOldestInflightRequest: Date | null;
135
+ /**
136
+ * True if the client has ever opened a WebSocket to the "ready" state.
137
+ */
138
+ hasEverConnected: boolean;
139
+ /**
140
+ * The number of times this client has connected to the Convex backend.
141
+ *
142
+ * A number of things can cause the client to reconnect -- server errors,
143
+ * bad internet, auth expiring. But this number being high is an indication
144
+ * that the client is having trouble keeping a stable connection.
145
+ */
146
+ connectionCount: number;
147
+ /**
148
+ * The number of times this client has tried (and failed) to connect to the Convex backend.
149
+ */
150
+ connectionRetries: number;
151
+ /**
152
+ * The number of mutations currently in flight.
153
+ */
154
+ inflightMutations: number;
155
+ /**
156
+ * The number of actions currently in flight.
157
+ */
158
+ inflightActions: number;
159
+ };
160
+ /**
161
+ * Options for {@link BaseConvexClient.subscribe}.
162
+ *
163
+ * @public
164
+ */
165
+ export interface SubscribeOptions {
166
+ /**
167
+ * An (optional) journal produced from a previous execution of this query
168
+ * function.
169
+ *
170
+ * If there is an existing subscription to a query function with the same
171
+ * name and arguments, this journal will have no effect.
172
+ */
173
+ journal?: QueryJournal;
174
+ }
175
+ /**
176
+ * Options for {@link BaseConvexClient.mutation}.
177
+ *
178
+ * @public
179
+ */
180
+ export interface MutationOptions {
181
+ /**
182
+ * An optimistic update to apply along with this mutation.
183
+ *
184
+ * An optimistic update locally updates queries while a mutation is pending.
185
+ * Once the mutation completes, the update will be rolled back.
186
+ */
187
+ optimisticUpdate?: OptimisticUpdate<any> | undefined;
188
+ }
189
+ /**
190
+ * Type describing updates to a query within a `Transition`.
191
+ *
192
+ * @public
193
+ */
194
+ export type QueryModification = {
195
+ kind: "Updated";
196
+ result: FunctionResult | undefined;
197
+ } | {
198
+ kind: "Removed";
199
+ };
200
+ /**
201
+ * Object describing a transition passed into the `onTransition` handler.
202
+ *
203
+ * These can be from receiving a transition from the server, or from applying an
204
+ * optimistic update locally.
205
+ *
206
+ * @public
207
+ */
208
+ export type Transition = {
209
+ queries: Array<{
210
+ token: QueryToken;
211
+ modification: QueryModification;
212
+ }>;
213
+ reflectedMutations: Array<{
214
+ requestId: RequestId;
215
+ result: FunctionResult;
216
+ }>;
217
+ timestamp: TS;
218
+ };
219
+ /**
220
+ * The status of a {@link ReatomQuery}.
221
+ *
222
+ * @public
223
+ */
224
+ export type QueryStatus = "loading" | "success" | "error";
225
+ /**
226
+ * One query function and arguments on a {@link ReatomConvexClient}.
227
+ *
228
+ * Reading any of its atoms inside a subscribed Reatom graph subscribes the
229
+ * query on the server; it is unsubscribed once nothing reads it. Equal
230
+ * queries share one entry and one server subscription.
231
+ *
232
+ * @public
233
+ */
234
+ export interface ReatomQuery<T = Value> {
235
+ readonly token: QueryToken;
236
+ /** The canonical function path. */
237
+ readonly udfPath: string;
238
+ readonly args: Record<string, Value>;
239
+ /**
240
+ * The function result with optimistic updates applied, `undefined` while
241
+ * loading.
242
+ */
243
+ readonly result: AtomLike<FunctionResult | undefined, []>;
244
+ /** The value, `undefined` while loading or after an error. */
245
+ readonly data: Computed<T | undefined>;
246
+ /** The error the query failed with, a `ConvexError` when it carries data. */
247
+ readonly error: Computed<Error | undefined>;
248
+ readonly status: Computed<QueryStatus>;
249
+ }
250
+ /**
251
+ * Options for {@link ReatomConvexClient.query}.
252
+ *
253
+ * @public
254
+ */
255
+ export type ReatomQueryOptions = SubscribeOptions;
256
+ type QueryRef = FunctionReference<"query"> | FunctionReference_future<"query"> | string;
257
+ type MutationRef = FunctionReference<"mutation"> | FunctionReference_future<"mutation"> | string;
258
+ type ActionRef = FunctionReference<"action"> | FunctionReference_future<"action"> | string;
259
+ type AnyRef = FunctionReference<any, any> | FunctionReference_future<any, any>;
260
+ type ReturnOf<Ref> = Ref extends AnyRef ? FunctionReturnType<Ref> : any;
261
+ type PaginatedItemOf<Ref> = ReturnOf<Ref> extends {
262
+ page: Array<infer Item>;
263
+ } ? Item : any;
264
+ type ArgsOf<Ref> = Ref extends AnyRef ? FunctionArgs<Ref> : Record<string, Value>;
265
+ /**
266
+ * Create a Convex client whose state lives in Reatom atoms.
267
+ *
268
+ * The client connects on first use in a context: a query read, a request,
269
+ * `setAuth` or a `connectionState` subscriber. Call its methods inside a
270
+ * Reatom frame; each context gets its own connection.
271
+ *
272
+ * ```ts
273
+ * const convex = reatomConvexClient(import.meta.env.VITE_CONVEX_URL);
274
+ * const notes = computed(() => convex.query(api.notes.list, {}).data());
275
+ * ```
276
+ *
277
+ * @param address - The url of your Convex deployment, e.g.
278
+ * `https://small-mouse-123.convex.cloud`.
279
+ * @param options - See {@link BaseConvexClientOptions}.
280
+ * @param name - A prefix for the client's atom names.
281
+ *
282
+ * @public
283
+ */
284
+ export declare function reatomConvexClient(address: string, options?: BaseConvexClientOptions, name?: string): ReatomConvexClient;
285
+ /**
286
+ * A Convex client whose state lives in Reatom atoms, created with
287
+ * {@link reatomConvexClient}. Use it inside a Reatom frame.
288
+ *
289
+ * @public
290
+ */
291
+ export interface ReatomConvexClient {
292
+ /** The prefix of the client's atom names. */
293
+ readonly name: string;
294
+ /** The deployment address. */
295
+ readonly url: string;
296
+ /** The connection with the backend; subscribing connects. */
297
+ readonly connectionState: Computed<ConnectionState>;
298
+ /**
299
+ * Whether the server accepted the current token: `null` while a token waits
300
+ * for the server, `false` without auth.
301
+ */
302
+ readonly isAuthenticated: AtomLike<boolean | null, []>;
303
+ /** The server rejected a confirmed token and a replacement is being fetched. */
304
+ readonly isRefreshingAuth: AtomLike<boolean, []>;
305
+ /** The current user JWT and its decoded claims. */
306
+ readonly authClaims: Computed<{
307
+ token: string;
308
+ decoded: Record<string, any>;
309
+ } | undefined>;
310
+ /** The socket state machine. */
311
+ readonly socketState: AtomLike<SocketState, []>;
312
+ /**
313
+ * A query of this client. Equal queries return the same entry.
314
+ *
315
+ * @param query - A query reference, or a function path.
316
+ * @param args - The query arguments, `{}` by default.
317
+ */
318
+ query<Query extends QueryRef>(query: Query, args?: ArgsOf<Query>, options?: ReatomQueryOptions): ReatomQuery<ReturnOf<Query>>;
319
+ /**
320
+ * A paginated query of this client. Equal queries with the same `id` share
321
+ * their pages; `id` is `-1` by default.
322
+ *
323
+ * @param query - A paginated query reference, or a function path.
324
+ * @param args - The query arguments without `paginationOpts`.
325
+ */
326
+ paginatedQuery<Query extends QueryRef>(query: Query, args: Omit<ArgsOf<Query>, "paginationOpts">, options: {
327
+ initialNumItems: number;
328
+ id?: number;
329
+ }): ReatomPaginatedQuery<PaginatedItemOf<Query>>;
330
+ /** Run a mutation; `optimisticUpdate` edits local results until it is reflected. */
331
+ mutation<Mutation extends MutationRef>(mutation: Mutation, args?: ArgsOf<Mutation>, options?: MutationOptions): Promise<ReturnOf<Mutation>>;
332
+ action<Action extends ActionRef>(action: Action, args?: ArgsOf<Action>): Promise<ReturnOf<Action>>;
333
+ /**
334
+ * Fetch a token, send it, and refetch it before it expires or when the
335
+ * server rejects it. `fetchToken` should return `null` when no token can be
336
+ * had, for example after the user's rights were revoked.
337
+ */
338
+ readonly setAuth: Action<[
339
+ fetchToken: AuthTokenFetcher,
340
+ onChange?: ((isAuthenticated: boolean) => void) | undefined,
341
+ onRefreshChange?: ((isRefreshing: boolean) => void) | undefined
342
+ ], void>;
343
+ /** Stop the auth flow and continue without an identity. */
344
+ readonly clearAuth: Action<[], void>;
345
+ /** Close the connection for good. */
346
+ readonly close: Action<[], Promise<void>>;
347
+ }
348
+ export {};