kitcn 0.0.1 → 0.12.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.
- package/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13264 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -34
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +761 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,2526 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { C as defaultIsUnauthorized, S as CRPCClientError, _ as useConvexAuthBridge, a as ConvexProviderWithAuth, b as useMaybeAuth, c as MaybeUnauthenticated, d as isSessionSyncGraceActive, f as useAuth, g as useAuthValue, h as useAuthStore, i as ConvexAuthBridge, l as Unauthenticated, m as useAuthState, n as AuthProvider, o as FetchAccessTokenContext, p as useAuthGuard, r as Authenticated, s as MaybeAuthenticated, t as AUTH_SESSION_SYNC_GRACE_MS, u as decodeJwtExp, v as useFetchAccessToken, w as isCRPCClientError, x as useSafeConvexAuth, y as useIsAuth } from "../auth-store-Cljlmdmi.js";
|
|
3
|
+
import { ConvexProvider, ConvexReactClient, ConvexReactClient as ConvexReactClient$1, useAction, useConvex, useMutation } from "convex/react";
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { getFunctionName } from "convex/server";
|
|
7
|
+
import { notifyManager, skipToken, useQueries, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
9
|
+
import { hashKey } from "@tanstack/query-core";
|
|
10
|
+
import { convexToJson } from "convex/values";
|
|
11
|
+
|
|
12
|
+
//#region src/shared/meta-utils.ts
|
|
13
|
+
const metaCache = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null;
|
|
17
|
+
}
|
|
18
|
+
function isFunctionType(value) {
|
|
19
|
+
return value === "query" || value === "mutation" || value === "action";
|
|
20
|
+
}
|
|
21
|
+
function isMetaScalar(value) {
|
|
22
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
23
|
+
}
|
|
24
|
+
function extractLeafMeta(value) {
|
|
25
|
+
const type = value.type;
|
|
26
|
+
if (!isFunctionType(type)) return;
|
|
27
|
+
const result = { type };
|
|
28
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
29
|
+
if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
|
|
30
|
+
if (entry === void 0) continue;
|
|
31
|
+
if (isMetaScalar(entry)) result[key] = entry;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function getHttpRoutes(api) {
|
|
36
|
+
const routes = api._http;
|
|
37
|
+
if (!isRecord(routes)) return;
|
|
38
|
+
const normalized = {};
|
|
39
|
+
for (const [routeKey, routeValue] of Object.entries(routes)) {
|
|
40
|
+
if (!isRecord(routeValue)) continue;
|
|
41
|
+
const routePath = routeValue.path;
|
|
42
|
+
const routeMethod = routeValue.method;
|
|
43
|
+
if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
|
|
44
|
+
path: routePath,
|
|
45
|
+
method: routeMethod
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a metadata index from merged API leaves.
|
|
52
|
+
* Supports both generated `api` objects and plain metadata fixtures.
|
|
53
|
+
*/
|
|
54
|
+
function buildMetaIndex(api) {
|
|
55
|
+
const cached = metaCache.get(api);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
const meta = {};
|
|
58
|
+
const httpRoutes = getHttpRoutes(api);
|
|
59
|
+
if (httpRoutes) meta._http = httpRoutes;
|
|
60
|
+
const walk = (node, path) => {
|
|
61
|
+
for (const [key, value] of Object.entries(node)) {
|
|
62
|
+
if (key.startsWith("_")) continue;
|
|
63
|
+
if (!isRecord(value)) continue;
|
|
64
|
+
const leafMeta = extractLeafMeta(value);
|
|
65
|
+
if (leafMeta) {
|
|
66
|
+
if (path.length === 0) continue;
|
|
67
|
+
const namespace = path.join("/");
|
|
68
|
+
meta[namespace] ??= {};
|
|
69
|
+
meta[namespace][key] = leafMeta;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
walk(value, [...path, key]);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(api, []);
|
|
76
|
+
metaCache.set(api, meta);
|
|
77
|
+
return meta;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get a function reference from the API object by traversing the path.
|
|
81
|
+
*/
|
|
82
|
+
function getFuncRef(api, path) {
|
|
83
|
+
let current = api;
|
|
84
|
+
for (const key of path) if (current && typeof current === "object") {
|
|
85
|
+
const next = current[key];
|
|
86
|
+
if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
|
|
87
|
+
current = next;
|
|
88
|
+
} else throw new Error(`Invalid path: ${path.join(".")}`);
|
|
89
|
+
if (current && typeof current === "object") {
|
|
90
|
+
const maybeFunctionRef = current.functionRef;
|
|
91
|
+
if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
|
|
92
|
+
}
|
|
93
|
+
if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get function type from meta using path.
|
|
98
|
+
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
|
|
99
|
+
*
|
|
100
|
+
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
|
|
101
|
+
* @param meta - The meta object from codegen
|
|
102
|
+
* @returns Function type or 'query' as default
|
|
103
|
+
*/
|
|
104
|
+
function getFunctionType(path, source) {
|
|
105
|
+
if (path.length < 2) return "query";
|
|
106
|
+
const meta = buildMetaIndex(source);
|
|
107
|
+
const fnName = path.at(-1);
|
|
108
|
+
const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
|
|
109
|
+
if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
|
|
110
|
+
return "query";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get function metadata from meta using path.
|
|
114
|
+
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
|
|
115
|
+
*
|
|
116
|
+
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
|
|
117
|
+
* @param meta - The meta object from codegen
|
|
118
|
+
* @returns Function metadata or undefined
|
|
119
|
+
*/
|
|
120
|
+
function getFunctionMeta(path, source) {
|
|
121
|
+
if (path.length < 2) return;
|
|
122
|
+
const meta = buildMetaIndex(source);
|
|
123
|
+
const fnName = path.at(-1);
|
|
124
|
+
return meta[path.slice(0, -1).join("/")]?.[fnName];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/crpc/http-types.ts
|
|
129
|
+
/** HTTP client error */
|
|
130
|
+
var HttpClientError = class extends Error {
|
|
131
|
+
name = "HttpClientError";
|
|
132
|
+
code;
|
|
133
|
+
status;
|
|
134
|
+
procedureName;
|
|
135
|
+
constructor(opts) {
|
|
136
|
+
super(opts.message ?? `${opts.code}: ${opts.procedureName}`);
|
|
137
|
+
this.code = opts.code;
|
|
138
|
+
this.status = opts.status;
|
|
139
|
+
this.procedureName = opts.procedureName;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/crpc/http-client.ts
|
|
145
|
+
/**
|
|
146
|
+
* HTTP Client Helpers
|
|
147
|
+
*
|
|
148
|
+
* Framework-agnostic utilities for executing HTTP requests
|
|
149
|
+
* against Convex HTTP endpoints.
|
|
150
|
+
*/
|
|
151
|
+
/** Reserved keys that are not part of JSON body */
|
|
152
|
+
const RESERVED_KEYS = new Set([
|
|
153
|
+
"params",
|
|
154
|
+
"searchParams",
|
|
155
|
+
"form",
|
|
156
|
+
"fetch",
|
|
157
|
+
"init",
|
|
158
|
+
"headers"
|
|
159
|
+
]);
|
|
160
|
+
/**
|
|
161
|
+
* Replace URL path parameters with actual values.
|
|
162
|
+
* e.g., '/users/:id' with { id: '123' } -> '/users/123'
|
|
163
|
+
*/
|
|
164
|
+
function replaceUrlParam(url, params) {
|
|
165
|
+
return url.replace(/:(\w+)/g, (_, key) => {
|
|
166
|
+
const value = params[key];
|
|
167
|
+
return value !== void 0 ? encodeURIComponent(value) : `:${key}`;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build URLSearchParams from query object.
|
|
172
|
+
* Handles array values as multiple params with same key (like Hono).
|
|
173
|
+
*/
|
|
174
|
+
function buildSearchParams(query) {
|
|
175
|
+
const params = new URLSearchParams();
|
|
176
|
+
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) for (const v of value) params.append(key, v);
|
|
177
|
+
else if (value !== void 0 && value !== null) params.append(key, value);
|
|
178
|
+
return params;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Hono-style HTTP request executor.
|
|
182
|
+
* Processes args in the same way as Hono's ClientRequestImpl.fetch().
|
|
183
|
+
*/
|
|
184
|
+
async function executeHttpRequest(opts) {
|
|
185
|
+
const { method, path } = opts.route;
|
|
186
|
+
const args = opts.args ?? {};
|
|
187
|
+
let rBody;
|
|
188
|
+
let cType;
|
|
189
|
+
if (args.form) {
|
|
190
|
+
const form = new FormData();
|
|
191
|
+
for (const [k, v] of Object.entries(args.form)) if (Array.isArray(v)) for (const v2 of v) form.append(k, v2);
|
|
192
|
+
else form.append(k, v);
|
|
193
|
+
rBody = form;
|
|
194
|
+
} else {
|
|
195
|
+
const jsonBody = {};
|
|
196
|
+
for (const [key, value] of Object.entries(args)) if (!RESERVED_KEYS.has(key) && value !== void 0) jsonBody[key] = value;
|
|
197
|
+
if (Object.keys(jsonBody).length > 0) {
|
|
198
|
+
rBody = JSON.stringify(opts.transformer.input.serialize(jsonBody));
|
|
199
|
+
cType = "application/json";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const argsClientOpts = {};
|
|
203
|
+
if (args.fetch) argsClientOpts.fetch = args.fetch;
|
|
204
|
+
if (args.init) argsClientOpts.init = args.init;
|
|
205
|
+
if (args.headers) argsClientOpts.headers = args.headers;
|
|
206
|
+
const mergedClientOpts = {
|
|
207
|
+
...opts.clientOpts,
|
|
208
|
+
...argsClientOpts
|
|
209
|
+
};
|
|
210
|
+
const resolvedBaseHeaders = typeof opts.baseHeaders === "function" ? await opts.baseHeaders() : opts.baseHeaders;
|
|
211
|
+
const headerValues = { ...typeof mergedClientOpts.headers === "function" ? await mergedClientOpts.headers() : mergedClientOpts.headers };
|
|
212
|
+
if (cType) headerValues["Content-Type"] = cType;
|
|
213
|
+
const finalHeaders = {};
|
|
214
|
+
if (resolvedBaseHeaders) {
|
|
215
|
+
for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
|
|
216
|
+
}
|
|
217
|
+
Object.assign(finalHeaders, headerValues);
|
|
218
|
+
let url = opts.convexSiteUrl + path;
|
|
219
|
+
if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
|
|
220
|
+
if (args.searchParams) {
|
|
221
|
+
const queryString = buildSearchParams(args.searchParams).toString();
|
|
222
|
+
if (queryString) url = `${url}?${queryString}`;
|
|
223
|
+
}
|
|
224
|
+
const methodUpperCase = method.toUpperCase();
|
|
225
|
+
const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
|
|
226
|
+
const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
|
|
227
|
+
body: setBody ? rBody : void 0,
|
|
228
|
+
method: methodUpperCase,
|
|
229
|
+
headers: finalHeaders,
|
|
230
|
+
...mergedClientOpts.init
|
|
231
|
+
});
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
const errorData = await response.json().catch(() => ({ error: {
|
|
234
|
+
code: "UNKNOWN",
|
|
235
|
+
message: response.statusText
|
|
236
|
+
} }));
|
|
237
|
+
const errorCode = errorData?.error?.code || "UNKNOWN";
|
|
238
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
239
|
+
throw new HttpClientError({
|
|
240
|
+
code: errorCode,
|
|
241
|
+
status: response.status,
|
|
242
|
+
procedureName: opts.procedureName,
|
|
243
|
+
message: errorMessage
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (response.headers.get("content-length") === "0" || response.status === 204) return;
|
|
247
|
+
if ((response.headers.get("content-type") || "").includes("application/json")) return opts.transformer.output.deserialize(await response.json());
|
|
248
|
+
return response.text();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/crpc/transformer.ts
|
|
253
|
+
const isPlainObject = (value) => {
|
|
254
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
255
|
+
const prototype = Object.getPrototypeOf(value);
|
|
256
|
+
return prototype === Object.prototype || prototype === null;
|
|
257
|
+
};
|
|
258
|
+
const CODEC_MARKER_KEY = "__crpc";
|
|
259
|
+
const CODEC_MARKER_VALUE = 1;
|
|
260
|
+
const CODEC_TAG_KEY = "t";
|
|
261
|
+
const CODEC_VALUE_KEY = "v";
|
|
262
|
+
const hasOnlyCodecPayloadKeys = (value) => {
|
|
263
|
+
let keyCount = 0;
|
|
264
|
+
for (const key in value) {
|
|
265
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
266
|
+
keyCount += 1;
|
|
267
|
+
if (key !== CODEC_MARKER_KEY && key !== CODEC_TAG_KEY && key !== CODEC_VALUE_KEY) return false;
|
|
268
|
+
if (keyCount > 3) return false;
|
|
269
|
+
}
|
|
270
|
+
return keyCount === 3;
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Date wire tag (Convex-style reserved key).
|
|
274
|
+
*/
|
|
275
|
+
const DATE_CODEC_TAG = "$date";
|
|
276
|
+
/**
|
|
277
|
+
* Built-in Date codec.
|
|
278
|
+
*/
|
|
279
|
+
const dateWireCodec = {
|
|
280
|
+
tag: DATE_CODEC_TAG,
|
|
281
|
+
isType: (value) => value instanceof Date,
|
|
282
|
+
encode: (value) => value.getTime(),
|
|
283
|
+
decode: (value) => {
|
|
284
|
+
if (typeof value !== "number") return value;
|
|
285
|
+
return new Date(value);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Build a recursive tagged transformer from codecs.
|
|
290
|
+
*/
|
|
291
|
+
const createTaggedTransformer = (codecs) => {
|
|
292
|
+
const codecByTag = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const codec of codecs) {
|
|
294
|
+
if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`);
|
|
295
|
+
if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`);
|
|
296
|
+
codecByTag.set(codec.tag, codec);
|
|
297
|
+
}
|
|
298
|
+
const serialize = (value) => {
|
|
299
|
+
for (const codec of codecs) if (codec.isType(value)) return {
|
|
300
|
+
[CODEC_MARKER_KEY]: CODEC_MARKER_VALUE,
|
|
301
|
+
[CODEC_TAG_KEY]: codec.tag,
|
|
302
|
+
[CODEC_VALUE_KEY]: serialize(codec.encode(value))
|
|
303
|
+
};
|
|
304
|
+
if (Array.isArray(value)) {
|
|
305
|
+
let result;
|
|
306
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
307
|
+
const item = value[index];
|
|
308
|
+
const serialized = serialize(item);
|
|
309
|
+
if (serialized !== item) {
|
|
310
|
+
if (!result) result = value.slice();
|
|
311
|
+
result[index] = serialized;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result ?? value;
|
|
315
|
+
}
|
|
316
|
+
if (isPlainObject(value)) {
|
|
317
|
+
let result;
|
|
318
|
+
for (const key in value) {
|
|
319
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
320
|
+
const nested = value[key];
|
|
321
|
+
const serialized = serialize(nested);
|
|
322
|
+
if (serialized !== nested) {
|
|
323
|
+
if (!result) result = { ...value };
|
|
324
|
+
result[key] = serialized;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result ?? value;
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
};
|
|
331
|
+
const deserialize = (value) => {
|
|
332
|
+
if (Array.isArray(value)) {
|
|
333
|
+
let result;
|
|
334
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
335
|
+
const item = value[index];
|
|
336
|
+
const deserialized = deserialize(item);
|
|
337
|
+
if (deserialized !== item) {
|
|
338
|
+
if (!result) result = value.slice();
|
|
339
|
+
result[index] = deserialized;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result ?? value;
|
|
343
|
+
}
|
|
344
|
+
if (isPlainObject(value)) {
|
|
345
|
+
const marker = value[CODEC_MARKER_KEY];
|
|
346
|
+
const tag = value[CODEC_TAG_KEY];
|
|
347
|
+
if (marker === CODEC_MARKER_VALUE && typeof tag === "string" && CODEC_VALUE_KEY in value && hasOnlyCodecPayloadKeys(value)) {
|
|
348
|
+
const codec = codecByTag.get(tag);
|
|
349
|
+
if (codec) return codec.decode(deserialize(value[CODEC_VALUE_KEY]));
|
|
350
|
+
}
|
|
351
|
+
let result;
|
|
352
|
+
for (const key in value) {
|
|
353
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
354
|
+
const nested = value[key];
|
|
355
|
+
const deserialized = deserialize(nested);
|
|
356
|
+
if (deserialized !== nested) {
|
|
357
|
+
if (!result) result = { ...value };
|
|
358
|
+
result[key] = deserialized;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return result ?? value;
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
serialize,
|
|
367
|
+
deserialize
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Default cRPC transformer (Date-enabled).
|
|
372
|
+
*/
|
|
373
|
+
const defaultCRPCTransformer = createTaggedTransformer([dateWireCodec]);
|
|
374
|
+
const DEFAULT_COMBINED_TRANSFORMER = {
|
|
375
|
+
input: defaultCRPCTransformer,
|
|
376
|
+
output: defaultCRPCTransformer
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* Normalize transformer config to split input/output shape.
|
|
380
|
+
*/
|
|
381
|
+
const normalizeCustomTransformer = (transformer) => {
|
|
382
|
+
if (!transformer) return;
|
|
383
|
+
if ("input" in transformer && "output" in transformer) return transformer;
|
|
384
|
+
return {
|
|
385
|
+
input: transformer,
|
|
386
|
+
output: transformer
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Compose user transformer with default Date transformer.
|
|
391
|
+
*
|
|
392
|
+
* Date transformer is always active:
|
|
393
|
+
* - serialize: user -> default(Date)
|
|
394
|
+
* - deserialize: default(Date) -> user
|
|
395
|
+
*/
|
|
396
|
+
const composeWithDefault = (transformer) => {
|
|
397
|
+
if (!transformer) return defaultCRPCTransformer;
|
|
398
|
+
return {
|
|
399
|
+
serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)),
|
|
400
|
+
deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value))
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const transformerCache = /* @__PURE__ */ new WeakMap();
|
|
404
|
+
/**
|
|
405
|
+
* Normalize transformer config to split input/output shape.
|
|
406
|
+
* User transformers are additive and always composed with default Date handling.
|
|
407
|
+
*/
|
|
408
|
+
const getTransformer = (transformer) => {
|
|
409
|
+
if (!transformer) return DEFAULT_COMBINED_TRANSFORMER;
|
|
410
|
+
const cacheKey = transformer;
|
|
411
|
+
const cached = transformerCache.get(cacheKey);
|
|
412
|
+
if (cached) return cached;
|
|
413
|
+
const custom = normalizeCustomTransformer(transformer);
|
|
414
|
+
const resolved = {
|
|
415
|
+
input: composeWithDefault(custom?.input),
|
|
416
|
+
output: composeWithDefault(custom?.output)
|
|
417
|
+
};
|
|
418
|
+
transformerCache.set(cacheKey, resolved);
|
|
419
|
+
return resolved;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/react/http-proxy.ts
|
|
424
|
+
/**
|
|
425
|
+
* Create a recursive proxy for HTTP routes with TanStack Query integration.
|
|
426
|
+
*
|
|
427
|
+
* Terminal methods:
|
|
428
|
+
* - GET endpoints: `queryOptions`, `queryKey`
|
|
429
|
+
* - POST/PUT/PATCH/DELETE: `mutationOptions`, `mutationKey`
|
|
430
|
+
*/
|
|
431
|
+
function createRecursiveHttpProxy(opts, path = []) {
|
|
432
|
+
return new Proxy(() => {}, { get(_, prop) {
|
|
433
|
+
if (typeof prop === "symbol") return;
|
|
434
|
+
if (prop === "then") return;
|
|
435
|
+
const routeKey = path.join(".");
|
|
436
|
+
const route = opts.routes[routeKey];
|
|
437
|
+
if (prop === "query") {
|
|
438
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
439
|
+
return async (args) => {
|
|
440
|
+
try {
|
|
441
|
+
return await executeHttpRequest({
|
|
442
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
443
|
+
route,
|
|
444
|
+
procedureName: routeKey,
|
|
445
|
+
args,
|
|
446
|
+
baseHeaders: opts.headers,
|
|
447
|
+
baseFetch: opts.fetch,
|
|
448
|
+
transformer: opts.transformer
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (prop === "mutate") {
|
|
457
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
458
|
+
return async (args) => {
|
|
459
|
+
try {
|
|
460
|
+
return await executeHttpRequest({
|
|
461
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
462
|
+
route,
|
|
463
|
+
procedureName: routeKey,
|
|
464
|
+
args,
|
|
465
|
+
baseHeaders: opts.headers,
|
|
466
|
+
baseFetch: opts.fetch,
|
|
467
|
+
transformer: opts.transformer
|
|
468
|
+
});
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (prop === "queryOptions") {
|
|
476
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
477
|
+
if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
|
|
478
|
+
return (args, queryOpts) => ({
|
|
479
|
+
...queryOpts,
|
|
480
|
+
queryKey: [
|
|
481
|
+
"httpQuery",
|
|
482
|
+
routeKey,
|
|
483
|
+
args
|
|
484
|
+
],
|
|
485
|
+
queryFn: async () => {
|
|
486
|
+
try {
|
|
487
|
+
return await executeHttpRequest({
|
|
488
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
489
|
+
route,
|
|
490
|
+
procedureName: routeKey,
|
|
491
|
+
args,
|
|
492
|
+
baseHeaders: opts.headers,
|
|
493
|
+
baseFetch: opts.fetch,
|
|
494
|
+
transformer: opts.transformer
|
|
495
|
+
});
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (prop === "queryKey") return (args) => {
|
|
504
|
+
return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
|
|
505
|
+
"httpQuery",
|
|
506
|
+
routeKey,
|
|
507
|
+
args
|
|
508
|
+
] : ["httpQuery", routeKey];
|
|
509
|
+
};
|
|
510
|
+
if (prop === "queryFilter") return (args, filters) => {
|
|
511
|
+
const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
|
|
512
|
+
return {
|
|
513
|
+
...filters,
|
|
514
|
+
queryKey: hasArgs ? [
|
|
515
|
+
"httpQuery",
|
|
516
|
+
routeKey,
|
|
517
|
+
args
|
|
518
|
+
] : ["httpQuery", routeKey]
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
if (prop === "mutationOptions") {
|
|
522
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
523
|
+
return (mutationOpts) => ({
|
|
524
|
+
...mutationOpts,
|
|
525
|
+
mutationKey: ["httpMutation", routeKey],
|
|
526
|
+
mutationFn: async (args) => {
|
|
527
|
+
try {
|
|
528
|
+
return await executeHttpRequest({
|
|
529
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
530
|
+
route,
|
|
531
|
+
procedureName: routeKey,
|
|
532
|
+
args,
|
|
533
|
+
baseHeaders: opts.headers,
|
|
534
|
+
baseFetch: opts.fetch,
|
|
535
|
+
transformer: opts.transformer
|
|
536
|
+
});
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (prop === "mutationKey") return () => ["httpMutation", routeKey];
|
|
545
|
+
return createRecursiveHttpProxy(opts, [...path, prop]);
|
|
546
|
+
} });
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Create an HTTP proxy with TanStack Query integration.
|
|
550
|
+
*
|
|
551
|
+
* Returns a proxy that provides:
|
|
552
|
+
* - `queryOptions` for GET endpoints (no subscription)
|
|
553
|
+
* - `mutationOptions` for POST/PUT/PATCH/DELETE endpoints
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```ts
|
|
557
|
+
* const httpProxy = createHttpProxy<AppRouter>({
|
|
558
|
+
* convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
|
559
|
+
* routes: httpRoutes,
|
|
560
|
+
* });
|
|
561
|
+
*
|
|
562
|
+
* // GET endpoint
|
|
563
|
+
* const opts = httpProxy.todos.get.queryOptions({ id: '123' });
|
|
564
|
+
* const { data } = useQuery(opts);
|
|
565
|
+
*
|
|
566
|
+
* // POST endpoint
|
|
567
|
+
* const mutation = useMutation(httpProxy.todos.create.mutationOptions());
|
|
568
|
+
* await mutation.mutateAsync({ title: 'New todo' });
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
function createHttpProxy(opts) {
|
|
572
|
+
const transformer = getTransformer(opts.transformer);
|
|
573
|
+
return createRecursiveHttpProxy({
|
|
574
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
575
|
+
routes: opts.routes,
|
|
576
|
+
headers: opts.headers,
|
|
577
|
+
fetch: opts.fetch,
|
|
578
|
+
onError: opts.onError,
|
|
579
|
+
transformer
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/crpc/query-options.ts
|
|
585
|
+
/**
|
|
586
|
+
* Query options factory for Convex query function subscriptions.
|
|
587
|
+
* Requires `convexQueryClient.queryFn()` set as the default `queryFn` globally.
|
|
588
|
+
*/
|
|
589
|
+
function convexQuery(funcRef, args, meta, opts) {
|
|
590
|
+
const finalArgs = args ?? {};
|
|
591
|
+
const isSkip = finalArgs === "skip";
|
|
592
|
+
const funcName = getFunctionName(funcRef);
|
|
593
|
+
const [namespace, fnName] = funcName.split(":");
|
|
594
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
595
|
+
const skipUnauth = opts?.skipUnauth;
|
|
596
|
+
return {
|
|
597
|
+
queryKey: [
|
|
598
|
+
"convexQuery",
|
|
599
|
+
funcName,
|
|
600
|
+
isSkip ? "skip" : finalArgs
|
|
601
|
+
],
|
|
602
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
603
|
+
refetchInterval: false,
|
|
604
|
+
refetchOnMount: false,
|
|
605
|
+
refetchOnReconnect: false,
|
|
606
|
+
refetchOnWindowFocus: false,
|
|
607
|
+
...isSkip ? { enabled: false } : {},
|
|
608
|
+
meta: {
|
|
609
|
+
authType,
|
|
610
|
+
skipUnauth,
|
|
611
|
+
subscribe: true
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Query options factory for Convex action functions.
|
|
617
|
+
* Actions are NOT reactive - they follow normal TanStack Query semantics.
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```ts
|
|
621
|
+
* useQuery(convexAction(api.ai.generate, { prompt }))
|
|
622
|
+
* ```
|
|
623
|
+
*
|
|
624
|
+
* @example With additional options (use spread):
|
|
625
|
+
* ```ts
|
|
626
|
+
* useQuery({
|
|
627
|
+
* ...convexAction(api.files.process, { fileId }),
|
|
628
|
+
* staleTime: 60_000
|
|
629
|
+
* });
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
function convexAction(funcRef, args, meta, opts) {
|
|
633
|
+
const finalArgs = args ?? {};
|
|
634
|
+
const isSkip = finalArgs === "skip";
|
|
635
|
+
const funcName = getFunctionName(funcRef);
|
|
636
|
+
const [namespace, fnName] = funcName.split(":");
|
|
637
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
638
|
+
const skipUnauth = opts?.skipUnauth;
|
|
639
|
+
return {
|
|
640
|
+
queryKey: [
|
|
641
|
+
"convexAction",
|
|
642
|
+
funcName,
|
|
643
|
+
isSkip ? {} : finalArgs
|
|
644
|
+
],
|
|
645
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
646
|
+
refetchInterval: false,
|
|
647
|
+
refetchOnMount: false,
|
|
648
|
+
refetchOnReconnect: false,
|
|
649
|
+
refetchOnWindowFocus: false,
|
|
650
|
+
...isSkip ? { enabled: false } : {},
|
|
651
|
+
meta: {
|
|
652
|
+
authType,
|
|
653
|
+
skipUnauth,
|
|
654
|
+
subscribe: false
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Infinite query options factory for paginated Convex queries.
|
|
660
|
+
* Server-safe (non-hook) - can be used in RSC.
|
|
661
|
+
*
|
|
662
|
+
* Uses flat { cursor, limit } input like tRPC.
|
|
663
|
+
*/
|
|
664
|
+
function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
|
|
665
|
+
const { limit, skipUnauth, enabled, ...queryOptions } = opts;
|
|
666
|
+
const finalArgs = args === "skip" ? {} : args;
|
|
667
|
+
const isSkip = args === "skip";
|
|
668
|
+
const funcName = getFunctionName(funcRef);
|
|
669
|
+
const [namespace, fnName] = funcName.split(":");
|
|
670
|
+
const authType = (meta?.[namespace]?.[fnName])?.auth;
|
|
671
|
+
const firstPageArgs = {
|
|
672
|
+
...finalArgs,
|
|
673
|
+
cursor: null,
|
|
674
|
+
limit
|
|
675
|
+
};
|
|
676
|
+
const finalEnabled = enabled === false || isSkip ? false : void 0;
|
|
677
|
+
return {
|
|
678
|
+
queryKey: [
|
|
679
|
+
"convexQuery",
|
|
680
|
+
funcName,
|
|
681
|
+
firstPageArgs
|
|
682
|
+
],
|
|
683
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
684
|
+
refetchInterval: false,
|
|
685
|
+
refetchOnMount: false,
|
|
686
|
+
refetchOnReconnect: false,
|
|
687
|
+
refetchOnWindowFocus: false,
|
|
688
|
+
...queryOptions,
|
|
689
|
+
...finalEnabled === false ? { enabled: false } : {},
|
|
690
|
+
meta: {
|
|
691
|
+
authType,
|
|
692
|
+
skipUnauth,
|
|
693
|
+
subscribe: true,
|
|
694
|
+
queryName: funcName,
|
|
695
|
+
args: finalArgs,
|
|
696
|
+
limit
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/crpc/types.ts
|
|
703
|
+
/** Symbol key for attaching FunctionReference to options (non-serializable) */
|
|
704
|
+
const FUNC_REF_SYMBOL = Symbol.for("convex.funcRef");
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/internal/auth.ts
|
|
708
|
+
/** Get auth type from meta for a function */
|
|
709
|
+
function getAuthType(meta, funcName) {
|
|
710
|
+
const [namespace, fnName] = funcName.split(":");
|
|
711
|
+
return meta?.[namespace]?.[fnName]?.auth;
|
|
712
|
+
}
|
|
713
|
+
/** Hook to compute auth-based skip logic for queries */
|
|
714
|
+
function useAuthSkip(funcRef, opts) {
|
|
715
|
+
const { isAuthenticated, isLoading: isAuthLoading } = useSafeConvexAuth();
|
|
716
|
+
const authType = getAuthType(useMeta(), getFunctionName(funcRef));
|
|
717
|
+
const authLoadingApplies = authType === "optional" || authType === "required";
|
|
718
|
+
return {
|
|
719
|
+
authType,
|
|
720
|
+
isAuthLoading,
|
|
721
|
+
isAuthenticated,
|
|
722
|
+
shouldSkip: opts?.enabled === false || authLoadingApplies && isAuthLoading || authType === "required" && !isAuthenticated && !isAuthLoading || !isAuthenticated && !isAuthLoading && !!opts?.skipUnauth
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
//#endregion
|
|
727
|
+
//#region src/react/use-query-options.ts
|
|
728
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: Convex type compatibility */
|
|
729
|
+
/**
|
|
730
|
+
* Query options factories for Convex functions.
|
|
731
|
+
* Forked from @convex-dev/react-query to support auth-aware error handling.
|
|
732
|
+
*/
|
|
733
|
+
/**
|
|
734
|
+
* Hook that returns query options for use with useQuery.
|
|
735
|
+
* Handles skipUnauth by setting enabled: false when unauthorized.
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```tsx
|
|
739
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }));
|
|
740
|
+
* ```
|
|
741
|
+
*
|
|
742
|
+
* @example With skipToken for conditional queries
|
|
743
|
+
* ```tsx
|
|
744
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, userId ? { id: userId } : skipToken));
|
|
745
|
+
* ```
|
|
746
|
+
*
|
|
747
|
+
* @example With skipUnauth
|
|
748
|
+
* ```tsx
|
|
749
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { skipUnauth: true }));
|
|
750
|
+
* ```
|
|
751
|
+
*
|
|
752
|
+
* @example With TanStack Query options
|
|
753
|
+
* ```tsx
|
|
754
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { enabled: !!id, placeholderData: [] }));
|
|
755
|
+
* ```
|
|
756
|
+
*/
|
|
757
|
+
function useConvexQueryOptions(funcRef, args, options) {
|
|
758
|
+
const isSkipped = args === skipToken;
|
|
759
|
+
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
|
|
760
|
+
const { authType, shouldSkip } = useAuthSkip(funcRef, {
|
|
761
|
+
enabled: isSkipped ? false : enabled,
|
|
762
|
+
skipUnauth: options?.skipUnauth
|
|
763
|
+
});
|
|
764
|
+
const baseOptions = convexQuery(funcRef, isSkipped ? {} : args);
|
|
765
|
+
const { skipUnauth: _, subscribe, ...queryOptions } = options ?? {};
|
|
766
|
+
return {
|
|
767
|
+
...baseOptions,
|
|
768
|
+
...queryOptions,
|
|
769
|
+
enabled: isSkipped ? false : !shouldSkip,
|
|
770
|
+
meta: {
|
|
771
|
+
...baseOptions.meta,
|
|
772
|
+
authType,
|
|
773
|
+
subscribe: subscribe !== false
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Hook that returns infinite query options for use with useInfiniteQuery.
|
|
779
|
+
* Handles auth type detection from meta and skipUnauth.
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```tsx
|
|
783
|
+
* const { data } = useInfiniteQuery(
|
|
784
|
+
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20 })
|
|
785
|
+
* );
|
|
786
|
+
* ```
|
|
787
|
+
*
|
|
788
|
+
* @example With skipToken for conditional queries
|
|
789
|
+
* ```tsx
|
|
790
|
+
* const { data } = useInfiniteQuery(
|
|
791
|
+
* useConvexInfiniteQueryOptions(api.posts.list, userId ? { userId } : skipToken, { limit: 20 })
|
|
792
|
+
* );
|
|
793
|
+
* ```
|
|
794
|
+
*
|
|
795
|
+
* @example With skipUnauth
|
|
796
|
+
* ```tsx
|
|
797
|
+
* const { data } = useInfiniteQuery(
|
|
798
|
+
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20, skipUnauth: true })
|
|
799
|
+
* );
|
|
800
|
+
* ```
|
|
801
|
+
*/
|
|
802
|
+
function useConvexInfiniteQueryOptions(funcRef, args, opts) {
|
|
803
|
+
const meta = useMeta();
|
|
804
|
+
const isSkipped = args === skipToken;
|
|
805
|
+
const enabledOpt = typeof opts.enabled === "function" ? void 0 : opts.enabled;
|
|
806
|
+
const { authType, shouldSkip } = useAuthSkip(funcRef, {
|
|
807
|
+
enabled: isSkipped ? false : enabledOpt,
|
|
808
|
+
skipUnauth: opts.skipUnauth
|
|
809
|
+
});
|
|
810
|
+
const enabled = isSkipped || shouldSkip ? false : enabledOpt;
|
|
811
|
+
const baseOptions = convexInfiniteQueryOptions(funcRef, isSkipped ? {} : args, {
|
|
812
|
+
...opts,
|
|
813
|
+
enabled
|
|
814
|
+
}, meta);
|
|
815
|
+
return {
|
|
816
|
+
...baseOptions,
|
|
817
|
+
meta: {
|
|
818
|
+
...baseOptions.meta,
|
|
819
|
+
authType
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Hook that returns query options for using an action as a one-shot query.
|
|
825
|
+
* Actions don't support WebSocket subscriptions - they're one-time calls.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```tsx
|
|
829
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }));
|
|
830
|
+
* ```
|
|
831
|
+
*
|
|
832
|
+
* @example With skipToken for conditional queries
|
|
833
|
+
* ```tsx
|
|
834
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, id ? { id } : skipToken));
|
|
835
|
+
* ```
|
|
836
|
+
*
|
|
837
|
+
* @example With skipUnauth
|
|
838
|
+
* ```tsx
|
|
839
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }, { skipUnauth: true }));
|
|
840
|
+
* ```
|
|
841
|
+
*/
|
|
842
|
+
function useConvexActionQueryOptions(action, args, options) {
|
|
843
|
+
const isSkipped = args === skipToken;
|
|
844
|
+
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
|
|
845
|
+
const { shouldSkip } = useAuthSkip(action, {
|
|
846
|
+
enabled: isSkipped ? false : enabled,
|
|
847
|
+
skipUnauth: options?.skipUnauth
|
|
848
|
+
});
|
|
849
|
+
const baseOptions = convexAction(action, isSkipped ? {} : args);
|
|
850
|
+
const { skipUnauth: _, ...queryOptions } = options ?? {};
|
|
851
|
+
return {
|
|
852
|
+
...baseOptions,
|
|
853
|
+
...queryOptions,
|
|
854
|
+
enabled: isSkipped ? false : !shouldSkip
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Hook that returns mutation options for use with useMutation.
|
|
859
|
+
* Wraps the Convex mutation with auth guard logic.
|
|
860
|
+
*
|
|
861
|
+
* @example
|
|
862
|
+
* ```tsx
|
|
863
|
+
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update));
|
|
864
|
+
* ```
|
|
865
|
+
*
|
|
866
|
+
* @example With TanStack Query options
|
|
867
|
+
* ```tsx
|
|
868
|
+
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update, {
|
|
869
|
+
* onSuccess: () => toast.success('Updated!'),
|
|
870
|
+
* }));
|
|
871
|
+
* ```
|
|
872
|
+
*/
|
|
873
|
+
function useConvexMutationOptions(mutation, options, transformer) {
|
|
874
|
+
const guard = useAuthGuard();
|
|
875
|
+
const getMeta = useFnMeta();
|
|
876
|
+
const name = getFunctionName(mutation);
|
|
877
|
+
const [namespace, fnName] = name.split(":");
|
|
878
|
+
const authType = getMeta(namespace, fnName)?.auth;
|
|
879
|
+
const convexMutation = useMutation(mutation);
|
|
880
|
+
const resolvedTransformer = getTransformer(transformer);
|
|
881
|
+
return {
|
|
882
|
+
...options,
|
|
883
|
+
mutationFn: async (args) => {
|
|
884
|
+
if (authType === "required" && guard()) throw new CRPCClientError({
|
|
885
|
+
code: "UNAUTHORIZED",
|
|
886
|
+
functionName: name
|
|
887
|
+
});
|
|
888
|
+
return convexMutation(resolvedTransformer.input.serialize(args));
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Hook that returns action options for use with useMutation.
|
|
894
|
+
* Wraps the Convex action with auth guard logic.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```tsx
|
|
898
|
+
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate));
|
|
899
|
+
* ```
|
|
900
|
+
*
|
|
901
|
+
* @example With TanStack Query options
|
|
902
|
+
* ```tsx
|
|
903
|
+
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate, {
|
|
904
|
+
* onSuccess: (data) => console.info(data),
|
|
905
|
+
* }));
|
|
906
|
+
* ```
|
|
907
|
+
*/
|
|
908
|
+
function useConvexActionOptions(action, options, transformer) {
|
|
909
|
+
const guard = useAuthGuard();
|
|
910
|
+
const getMeta = useFnMeta();
|
|
911
|
+
const name = getFunctionName(action);
|
|
912
|
+
const [namespace, fnName] = name.split(":");
|
|
913
|
+
const authType = getMeta(namespace, fnName)?.auth;
|
|
914
|
+
const convexAction = useAction(action);
|
|
915
|
+
const resolvedTransformer = getTransformer(transformer);
|
|
916
|
+
return {
|
|
917
|
+
...options,
|
|
918
|
+
mutationFn: async (args) => {
|
|
919
|
+
if (authType === "required" && guard()) throw new CRPCClientError({
|
|
920
|
+
code: "UNAUTHORIZED",
|
|
921
|
+
functionName: name
|
|
922
|
+
});
|
|
923
|
+
return convexAction(resolvedTransformer.input.serialize(args));
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Hook that returns upload mutation options for use with useMutation.
|
|
929
|
+
* Generates a presigned URL, then uploads the file directly to storage.
|
|
930
|
+
*
|
|
931
|
+
* @example
|
|
932
|
+
* ```tsx
|
|
933
|
+
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl));
|
|
934
|
+
* mutate({ file, ...otherArgs });
|
|
935
|
+
* ```
|
|
936
|
+
*
|
|
937
|
+
* @example With TanStack Query options
|
|
938
|
+
* ```tsx
|
|
939
|
+
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl, {
|
|
940
|
+
* onSuccess: (result) => console.info('Uploaded:', result.key),
|
|
941
|
+
* }));
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
944
|
+
function useUploadMutationOptions(generateUrlMutation, options) {
|
|
945
|
+
const generateUrl = useMutation(generateUrlMutation);
|
|
946
|
+
return {
|
|
947
|
+
...options,
|
|
948
|
+
mutationFn: async ({ file, ...args }) => {
|
|
949
|
+
const result = await generateUrl(args);
|
|
950
|
+
const { url } = result;
|
|
951
|
+
const response = await fetch(url, {
|
|
952
|
+
body: file,
|
|
953
|
+
headers: { "Content-Type": file.type },
|
|
954
|
+
method: "PUT"
|
|
955
|
+
});
|
|
956
|
+
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/react/proxy.ts
|
|
964
|
+
/**
|
|
965
|
+
* CRPC Recursive Proxy
|
|
966
|
+
*
|
|
967
|
+
* Creates a tRPC-like proxy that wraps Convex API functions with
|
|
968
|
+
* TanStack Query options builders.
|
|
969
|
+
*
|
|
970
|
+
* @example
|
|
971
|
+
* ```ts
|
|
972
|
+
* const crpc = createCRPCOptionsProxy(api);
|
|
973
|
+
* const opts = crpc.user.get.queryOptions({ id: '123' });
|
|
974
|
+
* const { data } = useQuery(opts);
|
|
975
|
+
* ```
|
|
976
|
+
*/
|
|
977
|
+
/** Get query key prefix based on function type */
|
|
978
|
+
function getQueryKeyPrefix(path, meta) {
|
|
979
|
+
if (getFunctionType(path, meta) === "action") return "convexAction";
|
|
980
|
+
return "convexQuery";
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Create a recursive proxy that accumulates path segments.
|
|
984
|
+
*/
|
|
985
|
+
function createRecursiveProxy(api, path, meta, transformer) {
|
|
986
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
987
|
+
if (typeof prop === "symbol") return;
|
|
988
|
+
if (prop === "then") return;
|
|
989
|
+
if (prop === "queryOptions") return (args = {}, opts) => {
|
|
990
|
+
const funcRef = getFuncRef(api, path);
|
|
991
|
+
if (getFunctionType(path, meta) === "action") return useConvexActionQueryOptions(funcRef, args, opts);
|
|
992
|
+
return useConvexQueryOptions(funcRef, args, opts);
|
|
993
|
+
};
|
|
994
|
+
if (prop === "staticQueryOptions") return (args = {}, opts) => {
|
|
995
|
+
const funcRef = getFuncRef(api, path);
|
|
996
|
+
const fnType = getFunctionType(path, meta);
|
|
997
|
+
const finalArgs = args === skipToken ? "skip" : args;
|
|
998
|
+
if (fnType === "action") return convexAction(funcRef, finalArgs, meta, opts);
|
|
999
|
+
return convexQuery(funcRef, finalArgs, meta, opts);
|
|
1000
|
+
};
|
|
1001
|
+
if (prop === "queryKey") return (args = {}) => {
|
|
1002
|
+
const funcName = getFunctionName(getFuncRef(api, path));
|
|
1003
|
+
return [
|
|
1004
|
+
getQueryKeyPrefix(path, meta),
|
|
1005
|
+
funcName,
|
|
1006
|
+
args
|
|
1007
|
+
];
|
|
1008
|
+
};
|
|
1009
|
+
if (prop === "queryFilter") return (args, filters) => {
|
|
1010
|
+
const funcName = getFunctionName(getFuncRef(api, path));
|
|
1011
|
+
const prefix = getQueryKeyPrefix(path, meta);
|
|
1012
|
+
return {
|
|
1013
|
+
...filters,
|
|
1014
|
+
queryKey: [
|
|
1015
|
+
prefix,
|
|
1016
|
+
funcName,
|
|
1017
|
+
args
|
|
1018
|
+
]
|
|
1019
|
+
};
|
|
1020
|
+
};
|
|
1021
|
+
if (prop === "infiniteQueryOptions") return (args = {}, opts = {}) => {
|
|
1022
|
+
const funcRef = getFuncRef(api, path);
|
|
1023
|
+
const options = useConvexInfiniteQueryOptions(funcRef, args, opts);
|
|
1024
|
+
Object.defineProperty(options, FUNC_REF_SYMBOL, {
|
|
1025
|
+
value: funcRef,
|
|
1026
|
+
enumerable: false,
|
|
1027
|
+
configurable: false
|
|
1028
|
+
});
|
|
1029
|
+
return options;
|
|
1030
|
+
};
|
|
1031
|
+
if (prop === "infiniteQueryKey") return (args) => {
|
|
1032
|
+
return [
|
|
1033
|
+
"convexQuery",
|
|
1034
|
+
getFunctionName(getFuncRef(api, path)),
|
|
1035
|
+
args ?? {}
|
|
1036
|
+
];
|
|
1037
|
+
};
|
|
1038
|
+
if (prop === "meta" && path.length >= 2) return getFunctionMeta(path, meta);
|
|
1039
|
+
if (prop === "mutationKey") return () => {
|
|
1040
|
+
return ["convexMutation", getFunctionName(getFuncRef(api, path))];
|
|
1041
|
+
};
|
|
1042
|
+
if (prop === "mutationOptions") return (opts) => {
|
|
1043
|
+
const funcRef = getFuncRef(api, path);
|
|
1044
|
+
if (getFunctionType(path, meta) === "action") return useConvexActionOptions(funcRef, opts, transformer);
|
|
1045
|
+
return useConvexMutationOptions(funcRef, opts, transformer);
|
|
1046
|
+
};
|
|
1047
|
+
return createRecursiveProxy(api, [...path, prop], meta, transformer);
|
|
1048
|
+
} });
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Create a CRPC proxy for a Convex API object.
|
|
1052
|
+
*
|
|
1053
|
+
* The proxy provides a tRPC-like interface for accessing Convex functions
|
|
1054
|
+
* with TanStack Query options builders.
|
|
1055
|
+
*
|
|
1056
|
+
* @param api - The Convex API object (from `@convex/api`)
|
|
1057
|
+
* @param meta - Generated function metadata for runtime type detection
|
|
1058
|
+
* @returns A typed proxy with queryOptions/mutationOptions methods
|
|
1059
|
+
*
|
|
1060
|
+
* @example
|
|
1061
|
+
* ```tsx
|
|
1062
|
+
* import { api } from '@convex/api';
|
|
1063
|
+
*
|
|
1064
|
+
* // Usually you should use createCRPCContext({ api }) instead.
|
|
1065
|
+
* // createCRPCOptionsProxy is a low-level helper.
|
|
1066
|
+
* const crpc = createCRPCOptionsProxy(api, {} as any);
|
|
1067
|
+
*
|
|
1068
|
+
* function MyComponent() {
|
|
1069
|
+
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
|
|
1070
|
+
* const { mutate } = useMutation(crpc.user.update.mutationOptions());
|
|
1071
|
+
* }
|
|
1072
|
+
* ```
|
|
1073
|
+
*/
|
|
1074
|
+
function createCRPCOptionsProxy(api, meta, transformer) {
|
|
1075
|
+
return createRecursiveProxy(api, [], meta, transformer);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/react/vanilla-client.ts
|
|
1080
|
+
/**
|
|
1081
|
+
* Create a recursive proxy for vanilla (direct) calls.
|
|
1082
|
+
*/
|
|
1083
|
+
function createRecursiveVanillaProxy(api, path, meta, convexClient, transformer) {
|
|
1084
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
1085
|
+
if (typeof prop === "symbol") return;
|
|
1086
|
+
if (prop === "then") return;
|
|
1087
|
+
if (prop === "query") return async (args = {}) => {
|
|
1088
|
+
const funcRef = getFuncRef(api, path);
|
|
1089
|
+
const fnType = getFunctionType(path, meta);
|
|
1090
|
+
const wireArgs = transformer.input.serialize(args);
|
|
1091
|
+
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
|
|
1092
|
+
return transformer.output.deserialize(await convexClient.query(funcRef, wireArgs));
|
|
1093
|
+
};
|
|
1094
|
+
if (prop === "watchQuery") return (args = {}, opts) => {
|
|
1095
|
+
const funcRef = getFuncRef(api, path);
|
|
1096
|
+
return convexClient.watchQuery(funcRef, transformer.input.serialize(args), opts);
|
|
1097
|
+
};
|
|
1098
|
+
if (prop === "mutate") return async (args = {}) => {
|
|
1099
|
+
const funcRef = getFuncRef(api, path);
|
|
1100
|
+
const fnType = getFunctionType(path, meta);
|
|
1101
|
+
const wireArgs = transformer.input.serialize(args);
|
|
1102
|
+
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
|
|
1103
|
+
return transformer.output.deserialize(await convexClient.mutation(funcRef, wireArgs));
|
|
1104
|
+
};
|
|
1105
|
+
return createRecursiveVanillaProxy(api, [...path, prop], meta, convexClient, transformer);
|
|
1106
|
+
} });
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Create a vanilla CRPC proxy for direct procedural calls.
|
|
1110
|
+
*
|
|
1111
|
+
* The proxy provides a tRPC-like interface for imperative Convex function calls.
|
|
1112
|
+
*
|
|
1113
|
+
* @param api - The Convex API object (from `@convex/api`)
|
|
1114
|
+
* @param meta - Generated function metadata for runtime type detection
|
|
1115
|
+
* @param convexClient - The ConvexReactClient instance
|
|
1116
|
+
* @returns A typed proxy with query/mutate methods
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* ```tsx
|
|
1120
|
+
* const client = createVanillaCRPCProxy(api, meta, convexClient);
|
|
1121
|
+
*
|
|
1122
|
+
* // Direct calls (no React Query)
|
|
1123
|
+
* const user = await client.user.get.query({ id });
|
|
1124
|
+
* await client.user.update.mutate({ id, name: 'test' });
|
|
1125
|
+
* ```
|
|
1126
|
+
*/
|
|
1127
|
+
function createVanillaCRPCProxy(api, meta, convexClient, transformer) {
|
|
1128
|
+
return createRecursiveVanillaProxy(api, [], meta, convexClient, getTransformer(transformer));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/react/context.tsx
|
|
1133
|
+
const ConvexQueryClientContext = createContext(null);
|
|
1134
|
+
/** Access ConvexQueryClient (e.g., for logout cleanup) */
|
|
1135
|
+
const useConvexQueryClient = () => useContext(ConvexQueryClientContext);
|
|
1136
|
+
const MetaContext = createContext(void 0);
|
|
1137
|
+
/**
|
|
1138
|
+
* Hook to access the meta object from context.
|
|
1139
|
+
* Returns undefined if meta was not provided to createCRPCContext.
|
|
1140
|
+
*/
|
|
1141
|
+
function useMeta() {
|
|
1142
|
+
return useContext(MetaContext);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Hook to get auth type for a function from meta.
|
|
1146
|
+
*/
|
|
1147
|
+
function useFnMeta() {
|
|
1148
|
+
const meta = useMeta();
|
|
1149
|
+
return (namespace, fnName) => meta?.[namespace]?.[fnName];
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Create CRPC context, provider, and hooks for a Convex API.
|
|
1153
|
+
*
|
|
1154
|
+
* @param options - Configuration object containing api and optional HTTP settings
|
|
1155
|
+
* @returns Object with CRPCProvider, useCRPC, and useCRPCClient
|
|
1156
|
+
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* ```tsx
|
|
1159
|
+
* // lib/crpc.ts
|
|
1160
|
+
* import { api } from '@convex/api';
|
|
1161
|
+
* import { createCRPCContext } from 'kitcn/react';
|
|
1162
|
+
*
|
|
1163
|
+
* // Works for both regular Convex functions and generated HTTP router types
|
|
1164
|
+
* export const { useCRPC } = createCRPCContext({
|
|
1165
|
+
* api,
|
|
1166
|
+
* convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
|
1167
|
+
* });
|
|
1168
|
+
*
|
|
1169
|
+
* // components/user-profile.tsx
|
|
1170
|
+
* function UserProfile({ id }) {
|
|
1171
|
+
* const crpc = useCRPC();
|
|
1172
|
+
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
|
|
1173
|
+
*
|
|
1174
|
+
* // HTTP endpoints (if configured)
|
|
1175
|
+
* const { data: httpData } = useQuery(crpc.http.todos.get.queryOptions({ id }));
|
|
1176
|
+
* }
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
function createCRPCContext(options) {
|
|
1180
|
+
const { api, ...httpOptions } = options;
|
|
1181
|
+
const meta = buildMetaIndex(api);
|
|
1182
|
+
const CRPCProxyContext = createContext(null);
|
|
1183
|
+
const VanillaClientContext = createContext(null);
|
|
1184
|
+
const HttpProxyContext = createContext(void 0);
|
|
1185
|
+
/** Inner provider */
|
|
1186
|
+
function CRPCProviderInner({ children, convexClient, convexQueryClient }) {
|
|
1187
|
+
const authStore = useAuthStore();
|
|
1188
|
+
const fetchAccessToken = useFetchAccessToken();
|
|
1189
|
+
const httpProxy = useMemo(() => {
|
|
1190
|
+
if (!httpOptions.convexSiteUrl || !meta._http) return;
|
|
1191
|
+
return createHttpProxy({
|
|
1192
|
+
convexSiteUrl: httpOptions.convexSiteUrl,
|
|
1193
|
+
routes: meta._http,
|
|
1194
|
+
headers: async () => {
|
|
1195
|
+
const token = authStore.get("token");
|
|
1196
|
+
const expiresAt = authStore.get("expiresAt");
|
|
1197
|
+
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
1198
|
+
if (token && expiresAt && timeRemaining >= 6e4) return {
|
|
1199
|
+
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
|
|
1200
|
+
Authorization: `Bearer ${token}`
|
|
1201
|
+
};
|
|
1202
|
+
if (fetchAccessToken) {
|
|
1203
|
+
const newToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt });
|
|
1204
|
+
if (newToken) return {
|
|
1205
|
+
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
|
|
1206
|
+
Authorization: `Bearer ${newToken}`
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
|
|
1210
|
+
},
|
|
1211
|
+
fetch: httpOptions.fetch,
|
|
1212
|
+
onError: httpOptions.onError,
|
|
1213
|
+
transformer: options.transformer
|
|
1214
|
+
});
|
|
1215
|
+
}, [authStore, fetchAccessToken]);
|
|
1216
|
+
const proxy = useMemo(() => createCRPCOptionsProxy(api, meta, options.transformer), []);
|
|
1217
|
+
const vanillaClient = useMemo(() => createVanillaCRPCProxy(api, meta, convexClient, options.transformer), [convexClient]);
|
|
1218
|
+
return /* @__PURE__ */ jsx(ConvexQueryClientContext.Provider, {
|
|
1219
|
+
value: convexQueryClient,
|
|
1220
|
+
children: /* @__PURE__ */ jsx(MetaContext.Provider, {
|
|
1221
|
+
value: meta,
|
|
1222
|
+
children: /* @__PURE__ */ jsx(VanillaClientContext.Provider, {
|
|
1223
|
+
value: vanillaClient,
|
|
1224
|
+
children: /* @__PURE__ */ jsx(HttpProxyContext.Provider, {
|
|
1225
|
+
value: httpProxy,
|
|
1226
|
+
children: /* @__PURE__ */ jsx(CRPCProxyContext.Provider, {
|
|
1227
|
+
value: proxy,
|
|
1228
|
+
children
|
|
1229
|
+
})
|
|
1230
|
+
})
|
|
1231
|
+
})
|
|
1232
|
+
})
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Provider component that wraps the app with CRPC context.
|
|
1237
|
+
* For auth, wrap with ConvexAuthProvider (or AuthProvider) above this.
|
|
1238
|
+
*/
|
|
1239
|
+
function CRPCProvider({ children, convexClient, convexQueryClient }) {
|
|
1240
|
+
return /* @__PURE__ */ jsx(CRPCProviderInner, {
|
|
1241
|
+
convexClient,
|
|
1242
|
+
convexQueryClient,
|
|
1243
|
+
children
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Hook to access the CRPC proxy for building query/mutation options.
|
|
1248
|
+
*
|
|
1249
|
+
* @returns The typed CRPC proxy (with http namespace if configured)
|
|
1250
|
+
* @throws If used outside of CRPCProvider
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ```tsx
|
|
1254
|
+
* const crpc = useCRPC();
|
|
1255
|
+
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
|
|
1256
|
+
*
|
|
1257
|
+
* // HTTP endpoints (if configured)
|
|
1258
|
+
* const { data: httpData } = useQuery(crpc.http.todos.get.queryOptions({ id }));
|
|
1259
|
+
* ```
|
|
1260
|
+
*/
|
|
1261
|
+
function useCRPC() {
|
|
1262
|
+
const ctx = useContext(CRPCProxyContext);
|
|
1263
|
+
const httpProxy = useContext(HttpProxyContext);
|
|
1264
|
+
if (!ctx) throw new Error("useCRPC must be used within CRPCProvider");
|
|
1265
|
+
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
|
|
1266
|
+
if (prop === "http") return httpProxy;
|
|
1267
|
+
return Reflect.get(target, prop);
|
|
1268
|
+
} });
|
|
1269
|
+
return ctx;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Hook to access the vanilla CRPC client for direct procedural calls.
|
|
1273
|
+
*
|
|
1274
|
+
* @returns The typed VanillaCRPCClient for direct .query()/.mutate() calls
|
|
1275
|
+
* @throws If used outside of CRPCProvider
|
|
1276
|
+
*
|
|
1277
|
+
* @example
|
|
1278
|
+
* ```tsx
|
|
1279
|
+
* const client = useCRPCClient();
|
|
1280
|
+
*
|
|
1281
|
+
* // Direct calls (no React Query)
|
|
1282
|
+
* const user = await client.user.get.query({ id });
|
|
1283
|
+
* await client.user.update.mutate({ id, name: 'test' });
|
|
1284
|
+
*
|
|
1285
|
+
* // HTTP endpoints (if configured)
|
|
1286
|
+
* const todos = await client.http.todos.list.queryOptions({});
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
1289
|
+
function useCRPCClient() {
|
|
1290
|
+
const ctx = useContext(VanillaClientContext);
|
|
1291
|
+
const httpProxy = useContext(HttpProxyContext);
|
|
1292
|
+
if (!ctx) throw new Error("useCRPCClient must be used within CRPCProvider");
|
|
1293
|
+
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
|
|
1294
|
+
if (prop === "http") return httpProxy;
|
|
1295
|
+
return Reflect.get(target, prop);
|
|
1296
|
+
} });
|
|
1297
|
+
return ctx;
|
|
1298
|
+
}
|
|
1299
|
+
return {
|
|
1300
|
+
CRPCProvider,
|
|
1301
|
+
useCRPC,
|
|
1302
|
+
useCRPCClient
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/crpc/auth-error.ts
|
|
1308
|
+
/**
|
|
1309
|
+
* Auth Mutation Error
|
|
1310
|
+
*
|
|
1311
|
+
* Framework-agnostic error class for Better Auth mutations.
|
|
1312
|
+
*/
|
|
1313
|
+
/**
|
|
1314
|
+
* Error thrown when a Better Auth mutation fails.
|
|
1315
|
+
* Contains the original error details from Better Auth.
|
|
1316
|
+
*/
|
|
1317
|
+
var AuthMutationError = class extends Error {
|
|
1318
|
+
/** Error code from Better Auth (e.g., 'INVALID_PASSWORD', 'EMAIL_ALREADY_REGISTERED') */
|
|
1319
|
+
code;
|
|
1320
|
+
/** HTTP status code */
|
|
1321
|
+
status;
|
|
1322
|
+
/** HTTP status text */
|
|
1323
|
+
statusText;
|
|
1324
|
+
constructor(authError) {
|
|
1325
|
+
super(authError.message || authError.statusText);
|
|
1326
|
+
this.name = "AuthMutationError";
|
|
1327
|
+
this.code = authError.code;
|
|
1328
|
+
this.status = authError.status;
|
|
1329
|
+
this.statusText = authError.statusText;
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
/**
|
|
1333
|
+
* Type guard to check if an error is an AuthMutationError.
|
|
1334
|
+
*/
|
|
1335
|
+
function isAuthMutationError(error) {
|
|
1336
|
+
return error instanceof AuthMutationError;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/react/auth-mutations.ts
|
|
1341
|
+
/** Poll until JWT token exists (auth complete) (max 5s) */
|
|
1342
|
+
const waitForAuth = async (store, timeout = 5e3) => {
|
|
1343
|
+
const start = Date.now();
|
|
1344
|
+
while (Date.now() - start < timeout) {
|
|
1345
|
+
if (store.get("token")) return true;
|
|
1346
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
};
|
|
1350
|
+
const authStateTimeoutError = () => new AuthMutationError({
|
|
1351
|
+
code: "AUTH_STATE_TIMEOUT",
|
|
1352
|
+
message: "Authentication did not complete. Try again.",
|
|
1353
|
+
status: 401,
|
|
1354
|
+
statusText: "UNAUTHORIZED"
|
|
1355
|
+
});
|
|
1356
|
+
const ensureAuth = async (store) => {
|
|
1357
|
+
if (await waitForAuth(store)) return;
|
|
1358
|
+
throw authStateTimeoutError();
|
|
1359
|
+
};
|
|
1360
|
+
const readReturnedToken = (value) => {
|
|
1361
|
+
if (!value || typeof value !== "object") return null;
|
|
1362
|
+
const record = value;
|
|
1363
|
+
if (typeof record.token === "string" && record.token.length > 0) return record.token;
|
|
1364
|
+
return readReturnedToken(record.data) ?? readReturnedToken(record.session);
|
|
1365
|
+
};
|
|
1366
|
+
const seedReturnedToken = (store, value) => {
|
|
1367
|
+
const token = readReturnedToken(value);
|
|
1368
|
+
if (!token) return;
|
|
1369
|
+
store.set("token", token);
|
|
1370
|
+
store.set("expiresAt", decodeJwtExp(token));
|
|
1371
|
+
store.set("sessionSyncGraceUntil", Date.now() + AUTH_SESSION_SYNC_GRACE_MS);
|
|
1372
|
+
};
|
|
1373
|
+
const syncSessionAtom = (authClient, sessionData) => {
|
|
1374
|
+
const sessionAtom = authClient.$store?.atoms?.session;
|
|
1375
|
+
if (typeof sessionAtom?.get !== "function" || typeof sessionAtom.set !== "function") return;
|
|
1376
|
+
const current = sessionAtom.get();
|
|
1377
|
+
sessionAtom.set({
|
|
1378
|
+
data: sessionData,
|
|
1379
|
+
error: null,
|
|
1380
|
+
isPending: false,
|
|
1381
|
+
isRefetching: false,
|
|
1382
|
+
refetch: current?.refetch ?? (async () => {})
|
|
1383
|
+
});
|
|
1384
|
+
};
|
|
1385
|
+
const hydrateReturnedSession = async (authClient, value) => {
|
|
1386
|
+
const token = readReturnedToken(value);
|
|
1387
|
+
if (!token || typeof authClient.getSession !== "function") return;
|
|
1388
|
+
const session = await authClient.getSession({ fetchOptions: {
|
|
1389
|
+
credentials: "omit",
|
|
1390
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1391
|
+
} });
|
|
1392
|
+
if (session?.data) syncSessionAtom(authClient, session.data);
|
|
1393
|
+
};
|
|
1394
|
+
const withDisabledSessionSignal = (args) => {
|
|
1395
|
+
const record = args && typeof args === "object" ? args : {};
|
|
1396
|
+
return {
|
|
1397
|
+
...record,
|
|
1398
|
+
fetchOptions: {
|
|
1399
|
+
...record.fetchOptions,
|
|
1400
|
+
disableSignal: true
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
};
|
|
1404
|
+
/**
|
|
1405
|
+
* Create mutation option hooks from a better-auth client.
|
|
1406
|
+
*
|
|
1407
|
+
* @example
|
|
1408
|
+
* ```tsx
|
|
1409
|
+
* // lib/auth-client.ts
|
|
1410
|
+
* import { createAuthMutations } from 'kitcn/react';
|
|
1411
|
+
*
|
|
1412
|
+
* export const authClient = createAuthClient({...});
|
|
1413
|
+
*
|
|
1414
|
+
* export const {
|
|
1415
|
+
* useSignOutMutationOptions,
|
|
1416
|
+
* useSignInSocialMutationOptions,
|
|
1417
|
+
* useSignInMutationOptions,
|
|
1418
|
+
* useSignUpMutationOptions,
|
|
1419
|
+
* } = createAuthMutations(authClient);
|
|
1420
|
+
*
|
|
1421
|
+
* // components/header.tsx
|
|
1422
|
+
* const signOutMutation = useMutation(useSignOutMutationOptions({
|
|
1423
|
+
* onSuccess: () => router.push('/login'),
|
|
1424
|
+
* }));
|
|
1425
|
+
* ```
|
|
1426
|
+
*/
|
|
1427
|
+
function createAuthMutations(authClient) {
|
|
1428
|
+
const useSignOutMutationOptions = ((options) => {
|
|
1429
|
+
const convexQueryClient = useConvexQueryClient();
|
|
1430
|
+
const authStoreApi = useAuthStore();
|
|
1431
|
+
return {
|
|
1432
|
+
...options,
|
|
1433
|
+
mutationFn: async (args) => {
|
|
1434
|
+
authStoreApi.set("isAuthenticated", false);
|
|
1435
|
+
convexQueryClient?.unsubscribeAuthQueries();
|
|
1436
|
+
const res = await authClient.signOut(args);
|
|
1437
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1438
|
+
authStoreApi.set("token", null);
|
|
1439
|
+
authStoreApi.set("expiresAt", null);
|
|
1440
|
+
authStoreApi.set("sessionSyncGraceUntil", null);
|
|
1441
|
+
return res;
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
});
|
|
1445
|
+
const useSignInSocialMutationOptions = ((options) => {
|
|
1446
|
+
const authStoreApi = useAuthStore();
|
|
1447
|
+
return {
|
|
1448
|
+
...options,
|
|
1449
|
+
mutationFn: async (args) => {
|
|
1450
|
+
const res = await authClient.signIn.social(withDisabledSessionSignal(args));
|
|
1451
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1452
|
+
seedReturnedToken(authStoreApi, res);
|
|
1453
|
+
await hydrateReturnedSession(authClient, res);
|
|
1454
|
+
await ensureAuth(authStoreApi);
|
|
1455
|
+
return res;
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
const useSignInMutationOptions = ((options) => {
|
|
1460
|
+
const authStoreApi = useAuthStore();
|
|
1461
|
+
return {
|
|
1462
|
+
...options,
|
|
1463
|
+
mutationFn: async (args) => {
|
|
1464
|
+
const res = await authClient.signIn.email(withDisabledSessionSignal(args));
|
|
1465
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1466
|
+
seedReturnedToken(authStoreApi, res);
|
|
1467
|
+
await hydrateReturnedSession(authClient, res);
|
|
1468
|
+
await ensureAuth(authStoreApi);
|
|
1469
|
+
return res;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
});
|
|
1473
|
+
const useSignUpMutationOptions = ((options) => {
|
|
1474
|
+
const authStoreApi = useAuthStore();
|
|
1475
|
+
return {
|
|
1476
|
+
...options,
|
|
1477
|
+
mutationFn: async (args) => {
|
|
1478
|
+
const res = await authClient.signUp.email(withDisabledSessionSignal(args));
|
|
1479
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1480
|
+
seedReturnedToken(authStoreApi, res);
|
|
1481
|
+
await hydrateReturnedSession(authClient, res);
|
|
1482
|
+
await ensureAuth(authStoreApi);
|
|
1483
|
+
return res;
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
});
|
|
1487
|
+
return {
|
|
1488
|
+
useSignOutMutationOptions,
|
|
1489
|
+
useSignInSocialMutationOptions,
|
|
1490
|
+
useSignInMutationOptions,
|
|
1491
|
+
useSignUpMutationOptions
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/internal/query-key.ts
|
|
1497
|
+
/**
|
|
1498
|
+
* Shared query key utilities for Convex + TanStack Query.
|
|
1499
|
+
* This file has NO React dependencies so it can be imported in both
|
|
1500
|
+
* server (RSC) and client contexts.
|
|
1501
|
+
*/
|
|
1502
|
+
/**
|
|
1503
|
+
* Check if query key is for a Convex query function.
|
|
1504
|
+
* Format: ['convexQuery', 'namespace:functionName', { args }]
|
|
1505
|
+
*/
|
|
1506
|
+
function isConvexQuery(queryKey) {
|
|
1507
|
+
return queryKey.length >= 2 && queryKey[0] === "convexQuery";
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Check if query key is for a Convex action function.
|
|
1511
|
+
* Format: ['convexAction', 'namespace:functionName', { args }]
|
|
1512
|
+
*/
|
|
1513
|
+
function isConvexAction$1(queryKey) {
|
|
1514
|
+
return queryKey.length >= 2 && queryKey[0] === "convexAction";
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Create stable hash for Convex query keys.
|
|
1518
|
+
* Uses Convex's JSON serialization for consistent argument hashing.
|
|
1519
|
+
*/
|
|
1520
|
+
function hashConvexQuery(queryKey) {
|
|
1521
|
+
const [, funcName, args] = queryKey;
|
|
1522
|
+
return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Create stable hash for Convex action keys.
|
|
1526
|
+
* Uses Convex's JSON serialization for consistent argument hashing.
|
|
1527
|
+
*/
|
|
1528
|
+
function hashConvexAction(queryKey) {
|
|
1529
|
+
const [, funcName, args] = queryKey;
|
|
1530
|
+
return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
//#endregion
|
|
1534
|
+
//#region src/internal/hash.ts
|
|
1535
|
+
/**
|
|
1536
|
+
* Create a hash function for TanStack Query that handles Convex keys.
|
|
1537
|
+
*/
|
|
1538
|
+
function createHashFn(fallback = hashKey) {
|
|
1539
|
+
return (queryKey) => {
|
|
1540
|
+
if (isConvexQuery(queryKey)) return hashConvexQuery(queryKey);
|
|
1541
|
+
if (isConvexAction$1(queryKey)) return hashConvexAction(queryKey);
|
|
1542
|
+
return fallback(queryKey);
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region src/react/client.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* ConvexQueryClient - Real-time subscription bridge for TanStack Query + Convex
|
|
1550
|
+
*
|
|
1551
|
+
* ## Why This Exists
|
|
1552
|
+
*
|
|
1553
|
+
* TanStack Query is request-based (fetch once, cache, refetch on stale).
|
|
1554
|
+
* Convex is subscription-based (WebSocket push updates in real-time).
|
|
1555
|
+
*
|
|
1556
|
+
* This client bridges the two by:
|
|
1557
|
+
* 1. Listening to TanStack Query cache events (query added/removed)
|
|
1558
|
+
* 2. Creating Convex WebSocket subscriptions for each active query
|
|
1559
|
+
* 3. Pushing Convex updates into TanStack Query cache
|
|
1560
|
+
*
|
|
1561
|
+
* ## Architecture
|
|
1562
|
+
*
|
|
1563
|
+
* ```
|
|
1564
|
+
* useConvexQuery (hook)
|
|
1565
|
+
* │
|
|
1566
|
+
* ▼
|
|
1567
|
+
* TanStack Query Cache ◄──── ConvexQueryClient listens to cache events
|
|
1568
|
+
* │ │
|
|
1569
|
+
* │ ▼
|
|
1570
|
+
* │ Convex WebSocket subscriptions
|
|
1571
|
+
* │ │
|
|
1572
|
+
* │ ▼
|
|
1573
|
+
* └──────────────────── Real-time updates pushed to cache
|
|
1574
|
+
* ```
|
|
1575
|
+
*
|
|
1576
|
+
* ## Subscription Lifecycle
|
|
1577
|
+
*
|
|
1578
|
+
* WebSocket subscriptions are decoupled from cache retention:
|
|
1579
|
+
* - Subscribe when query has observers (component mounted)
|
|
1580
|
+
* - Unsubscribe immediately when last observer removed (component unmounted)
|
|
1581
|
+
* - Cache data persists for gcTime (default 5 min) for instant back-navigation
|
|
1582
|
+
* - On remount: show cached data instantly, re-subscribe for fresh updates
|
|
1583
|
+
*
|
|
1584
|
+
* ## Query Key Format
|
|
1585
|
+
*
|
|
1586
|
+
* Convex queries use this key format:
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* ['convexQuery', 'functionName', { args }]
|
|
1589
|
+
* ['convexAction', 'functionName', { args }]
|
|
1590
|
+
* ['convexQuery', 'functionName', 'skip'] // skipped queries
|
|
1591
|
+
* ```
|
|
1592
|
+
*
|
|
1593
|
+
* ## SSR Support
|
|
1594
|
+
*
|
|
1595
|
+
* On server: Uses ConvexHttpClient for one-shot queries (no WebSocket).
|
|
1596
|
+
* On client: Uses ConvexReactClient with WebSocket subscriptions.
|
|
1597
|
+
*
|
|
1598
|
+
* @module
|
|
1599
|
+
*/
|
|
1600
|
+
const isServer = typeof window === "undefined";
|
|
1601
|
+
/**
|
|
1602
|
+
* Check if query is marked as skipped (used when auth required but not authenticated).
|
|
1603
|
+
* Skipped queries have 'skip' as the third element instead of args.
|
|
1604
|
+
*/
|
|
1605
|
+
function isConvexSkipped(queryKey) {
|
|
1606
|
+
return queryKey.length >= 2 && ["convexQuery", "convexAction"].includes(queryKey[0]) && queryKey[2] === "skip";
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Check if query key is for a Convex action function.
|
|
1610
|
+
* Format: ['convexAction', 'namespace:functionName', { args }]
|
|
1611
|
+
*/
|
|
1612
|
+
function isConvexAction(queryKey) {
|
|
1613
|
+
return queryKey.length >= 2 && queryKey[0] === "convexAction";
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Bridges TanStack Query with Convex real-time subscriptions.
|
|
1617
|
+
*
|
|
1618
|
+
* ## Setup
|
|
1619
|
+
*
|
|
1620
|
+
* ```ts
|
|
1621
|
+
* const convexQueryClient = new ConvexQueryClient(CONVEX_URL);
|
|
1622
|
+
*
|
|
1623
|
+
* const queryClient = new QueryClient({
|
|
1624
|
+
* defaultOptions: {
|
|
1625
|
+
* queries: {
|
|
1626
|
+
* queryFn: convexQueryClient.queryFn(),
|
|
1627
|
+
* queryKeyHashFn: convexQueryClient.hashFn(),
|
|
1628
|
+
* },
|
|
1629
|
+
* },
|
|
1630
|
+
* });
|
|
1631
|
+
*
|
|
1632
|
+
* convexQueryClient.connect(queryClient);
|
|
1633
|
+
* ```
|
|
1634
|
+
*
|
|
1635
|
+
* ## How It Works
|
|
1636
|
+
*
|
|
1637
|
+
* 1. **Query Added**: When TanStack adds a Convex query to cache,
|
|
1638
|
+
* ConvexQueryClient creates a WebSocket subscription via `watchQuery`.
|
|
1639
|
+
*
|
|
1640
|
+
* 2. **Real-time Updates**: When Convex pushes an update, the subscription
|
|
1641
|
+
* callback updates the TanStack cache via `setQueryData`.
|
|
1642
|
+
*
|
|
1643
|
+
* 3. **Query Removed**: When TanStack removes a query (no observers),
|
|
1644
|
+
* ConvexQueryClient unsubscribes from the WebSocket.
|
|
1645
|
+
*
|
|
1646
|
+
* ## Auth Integration
|
|
1647
|
+
*
|
|
1648
|
+
* Auth is handled via TanStack Query `meta`:
|
|
1649
|
+
* - `convexQuery` includes `meta.authType` from generated Convex metadata
|
|
1650
|
+
* - `queryFn` checks `meta.authType` via `getAuthState()` and throws `CRPCClientError` if unauthorized
|
|
1651
|
+
* - `subscribeInner` respects `meta.subscribe === false` to skip WebSocket
|
|
1652
|
+
*/
|
|
1653
|
+
var ConvexQueryClient = class {
|
|
1654
|
+
/** Convex client for WebSocket subscriptions (client) and one-shot queries */
|
|
1655
|
+
convexClient;
|
|
1656
|
+
/**
|
|
1657
|
+
* Active WebSocket subscriptions, keyed by TanStack query hash.
|
|
1658
|
+
* Each subscription has:
|
|
1659
|
+
* - watch: Convex Watch object for the query
|
|
1660
|
+
* - unsubscribe: Cleanup function to remove the subscription
|
|
1661
|
+
* - queryKey: Original query key for cache updates
|
|
1662
|
+
*/
|
|
1663
|
+
subscriptions;
|
|
1664
|
+
/** Cleanup function for QueryCache subscription */
|
|
1665
|
+
unsubscribe;
|
|
1666
|
+
/**
|
|
1667
|
+
* Pending unsubscribes with timeout IDs.
|
|
1668
|
+
* Used to debounce unsubscribe/subscribe cycles from React StrictMode.
|
|
1669
|
+
*/
|
|
1670
|
+
pendingUnsubscribes = /* @__PURE__ */ new Map();
|
|
1671
|
+
/** HTTP client for SSR queries (no WebSocket on server) */
|
|
1672
|
+
serverHttpClient;
|
|
1673
|
+
/** TanStack QueryClient reference */
|
|
1674
|
+
_queryClient;
|
|
1675
|
+
/** SSR query mode: 'consistent' guarantees same timestamp, 'inconsistent' is faster */
|
|
1676
|
+
ssrQueryMode;
|
|
1677
|
+
/** Auth store for checking auth state */
|
|
1678
|
+
authStore;
|
|
1679
|
+
/** Delay before unsubscribing when query has no observers */
|
|
1680
|
+
unsubscribeDelay;
|
|
1681
|
+
/** Payload transformer used across request/response boundaries. */
|
|
1682
|
+
transformer;
|
|
1683
|
+
/** Runtime-safe accessor for pending unsubscribe map (defensive for HMR edge cases) */
|
|
1684
|
+
getPendingUnsubscribesMap() {
|
|
1685
|
+
const self = this;
|
|
1686
|
+
if (!self.pendingUnsubscribes) self.pendingUnsubscribes = /* @__PURE__ */ new Map();
|
|
1687
|
+
return self.pendingUnsubscribes;
|
|
1688
|
+
}
|
|
1689
|
+
/** Cancel a pending delayed unsubscribe for a query hash. */
|
|
1690
|
+
cancelPendingUnsubscribe(queryHash) {
|
|
1691
|
+
const pendingUnsubscribes = this.getPendingUnsubscribesMap();
|
|
1692
|
+
const timeoutId = pendingUnsubscribes.get(queryHash);
|
|
1693
|
+
if (!timeoutId) return;
|
|
1694
|
+
clearTimeout(timeoutId);
|
|
1695
|
+
pendingUnsubscribes.delete(queryHash);
|
|
1696
|
+
}
|
|
1697
|
+
/** Unsubscribe a live Convex watch (if present) and remove it from the subscription map. */
|
|
1698
|
+
unsubscribeQueryByHash(queryHash) {
|
|
1699
|
+
const sub = this.subscriptions[queryHash];
|
|
1700
|
+
if (!sub) return;
|
|
1701
|
+
sub.unsubscribe();
|
|
1702
|
+
delete this.subscriptions[queryHash];
|
|
1703
|
+
}
|
|
1704
|
+
/** Update auth store (for HMR where jotai store may reset) */
|
|
1705
|
+
updateAuthStore(authStore) {
|
|
1706
|
+
this.authStore = authStore;
|
|
1707
|
+
}
|
|
1708
|
+
/** Get current auth state from store */
|
|
1709
|
+
getAuthState() {
|
|
1710
|
+
if (!this.authStore) return;
|
|
1711
|
+
return {
|
|
1712
|
+
isLoading: this.authStore.get("isLoading"),
|
|
1713
|
+
isAuthenticated: this.authStore.get("isAuthenticated"),
|
|
1714
|
+
onUnauthorized: this.authStore.get("onQueryUnauthorized"),
|
|
1715
|
+
isUnauthorized: this.authStore.get("isUnauthorized")
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Check if subscription should be skipped due to auth state.
|
|
1720
|
+
* Needed for useSuspenseQuery which ignores enabled: false.
|
|
1721
|
+
* Regular useQuery already handles this via enabled: false in hooks.
|
|
1722
|
+
*/
|
|
1723
|
+
shouldSkipSubscription(authType) {
|
|
1724
|
+
if (!authType || !this.authStore) return false;
|
|
1725
|
+
const authState = this.getAuthState();
|
|
1726
|
+
if (authState?.isLoading) return true;
|
|
1727
|
+
if (authType === "required" && !authState?.isAuthenticated) return true;
|
|
1728
|
+
return false;
|
|
1729
|
+
}
|
|
1730
|
+
/** Get QueryClient, throwing if not connected */
|
|
1731
|
+
get queryClient() {
|
|
1732
|
+
if (!this._queryClient) throw new Error("ConvexQueryClient not connected to TanStack QueryClient.");
|
|
1733
|
+
return this._queryClient;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Create a ConvexQueryClient.
|
|
1737
|
+
*
|
|
1738
|
+
* @param client - Convex URL string or existing ConvexReactClient
|
|
1739
|
+
* @param options - Configuration options
|
|
1740
|
+
*/
|
|
1741
|
+
constructor(client, options = {}) {
|
|
1742
|
+
if (typeof client === "string") this.convexClient = new ConvexReactClient$1(client, options);
|
|
1743
|
+
else this.convexClient = client;
|
|
1744
|
+
this.ssrQueryMode = options.dangerouslyUseInconsistentQueriesDuringSSR ? "inconsistent" : "consistent";
|
|
1745
|
+
this.subscriptions = {};
|
|
1746
|
+
this.authStore = options.authStore;
|
|
1747
|
+
this.unsubscribeDelay = options.unsubscribeDelay ?? 3e3;
|
|
1748
|
+
this.transformer = getTransformer(options.transformer);
|
|
1749
|
+
if (options.queryClient) {
|
|
1750
|
+
this._queryClient = options.queryClient;
|
|
1751
|
+
this.unsubscribe = this.subscribeInner(options.queryClient.getQueryCache());
|
|
1752
|
+
}
|
|
1753
|
+
if (isServer) this.serverHttpClient = new ConvexHttpClient(this.convexClient.url, { fetch: options.serverFetch });
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Connect to TanStack QueryClient.
|
|
1757
|
+
* Starts listening to cache events for subscription management.
|
|
1758
|
+
*/
|
|
1759
|
+
connect(queryClient) {
|
|
1760
|
+
if (this._queryClient === queryClient && this.unsubscribe) return;
|
|
1761
|
+
if (this.unsubscribe) this.unsubscribe();
|
|
1762
|
+
this._queryClient = queryClient;
|
|
1763
|
+
this.unsubscribe = this.subscribeInner(queryClient.getQueryCache());
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Clean up all subscriptions.
|
|
1767
|
+
* Call this when the client is no longer needed.
|
|
1768
|
+
*/
|
|
1769
|
+
destroy() {
|
|
1770
|
+
this.unsubscribe?.();
|
|
1771
|
+
for (const timeoutId of this.getPendingUnsubscribesMap().values()) clearTimeout(timeoutId);
|
|
1772
|
+
this.getPendingUnsubscribesMap().clear();
|
|
1773
|
+
for (const sub of Object.values(this.subscriptions)) sub.unsubscribe();
|
|
1774
|
+
this.subscriptions = {};
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Unsubscribe from all auth-required queries.
|
|
1778
|
+
* Call before logout to prevent UNAUTHORIZED errors during session invalidation.
|
|
1779
|
+
*/
|
|
1780
|
+
unsubscribeAuthQueries() {
|
|
1781
|
+
for (const queryHash of Object.keys(this.subscriptions)) if ((this.queryClient.getQueryCache().get(queryHash)?.meta)?.authType === "required") {
|
|
1782
|
+
this.cancelPendingUnsubscribe(queryHash);
|
|
1783
|
+
this.unsubscribeQueryByHash(queryHash);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Batch update all subscriptions.
|
|
1788
|
+
* Called internally when Convex client reconnects.
|
|
1789
|
+
*/
|
|
1790
|
+
onUpdate = () => {
|
|
1791
|
+
notifyManager.batch(() => {
|
|
1792
|
+
for (const key of Object.keys(this.subscriptions)) this.onUpdateQueryKeyHash(key);
|
|
1793
|
+
});
|
|
1794
|
+
};
|
|
1795
|
+
/**
|
|
1796
|
+
* Handle Convex subscription update for a specific query.
|
|
1797
|
+
* Reads latest value from Watch and updates TanStack cache.
|
|
1798
|
+
*
|
|
1799
|
+
* @param queryHash - TanStack query hash identifying the query
|
|
1800
|
+
*/
|
|
1801
|
+
onUpdateQueryKeyHash(queryHash) {
|
|
1802
|
+
const subscription = this.subscriptions[queryHash];
|
|
1803
|
+
if (!subscription) throw new Error(`Internal ConvexQueryClient error: onUpdateQueryKeyHash called for ${queryHash}`);
|
|
1804
|
+
const query = this.queryClient.getQueryCache().get(queryHash);
|
|
1805
|
+
if (!query) return;
|
|
1806
|
+
const { queryKey, watch } = subscription;
|
|
1807
|
+
let result;
|
|
1808
|
+
try {
|
|
1809
|
+
result = {
|
|
1810
|
+
ok: true,
|
|
1811
|
+
value: watch.localQueryResult()
|
|
1812
|
+
};
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
result = {
|
|
1815
|
+
ok: false,
|
|
1816
|
+
error
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
if (result.ok) {
|
|
1820
|
+
const existingData = this.queryClient.getQueryData(queryKey);
|
|
1821
|
+
if (result.value !== null && result.value !== void 0 || !(existingData !== null && existingData !== void 0)) this.queryClient.setQueryData(queryKey, this.transformer.output.deserialize(result.value));
|
|
1822
|
+
} else {
|
|
1823
|
+
const { error } = result;
|
|
1824
|
+
const authState = this.getAuthState();
|
|
1825
|
+
const meta = query.meta;
|
|
1826
|
+
const isUnauthorized = authState?.isUnauthorized(error) ?? false;
|
|
1827
|
+
if (isUnauthorized && meta?.skipUnauth) {
|
|
1828
|
+
this.queryClient.setQueryData(queryKey, this.transformer.output.deserialize(null));
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
query.setState({
|
|
1832
|
+
error,
|
|
1833
|
+
errorUpdateCount: query.state.errorUpdateCount + 1,
|
|
1834
|
+
errorUpdatedAt: Date.now(),
|
|
1835
|
+
fetchFailureCount: query.state.fetchFailureCount + 1,
|
|
1836
|
+
fetchFailureReason: error,
|
|
1837
|
+
fetchStatus: "idle",
|
|
1838
|
+
status: "error"
|
|
1839
|
+
}, { meta: "set by ConvexQueryClient" });
|
|
1840
|
+
if (isUnauthorized && authState?.isAuthenticated) {
|
|
1841
|
+
const [, funcName] = queryKey;
|
|
1842
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Subscribe to TanStack QueryCache events.
|
|
1848
|
+
* Creates/removes Convex WebSocket subscriptions as queries are added/removed.
|
|
1849
|
+
*
|
|
1850
|
+
* @param queryCache - TanStack QueryCache to subscribe to
|
|
1851
|
+
* @returns Cleanup function to unsubscribe
|
|
1852
|
+
*/
|
|
1853
|
+
subscribeInner(queryCache) {
|
|
1854
|
+
if (isServer) return () => {};
|
|
1855
|
+
return queryCache.subscribe((event) => {
|
|
1856
|
+
if (!isConvexQuery(event.query.queryKey)) return;
|
|
1857
|
+
if (isConvexSkipped(event.query.queryKey)) return;
|
|
1858
|
+
switch (event.type) {
|
|
1859
|
+
case "removed":
|
|
1860
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
1861
|
+
this.unsubscribeQueryByHash(event.query.queryHash);
|
|
1862
|
+
break;
|
|
1863
|
+
case "added": {
|
|
1864
|
+
const meta = event.query.meta;
|
|
1865
|
+
if (meta?.subscribe === false) break;
|
|
1866
|
+
const [, funcName, args] = event.query.queryKey;
|
|
1867
|
+
if (event.query.getObserversCount() === 0) break;
|
|
1868
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
1869
|
+
const watch = this.convexClient.watchQuery(funcName, this.transformer.input.serialize(args));
|
|
1870
|
+
const unsubscribe = watch.onUpdate(() => {
|
|
1871
|
+
this.onUpdateQueryKeyHash(event.query.queryHash);
|
|
1872
|
+
});
|
|
1873
|
+
this.subscriptions[event.query.queryHash] = {
|
|
1874
|
+
queryKey: event.query.queryKey,
|
|
1875
|
+
watch,
|
|
1876
|
+
unsubscribe
|
|
1877
|
+
};
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
case "observerAdded": {
|
|
1881
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
1882
|
+
if (this.subscriptions[event.query.queryHash]) break;
|
|
1883
|
+
if (event.query.options.enabled === false) break;
|
|
1884
|
+
const meta = event.query.meta;
|
|
1885
|
+
if (meta?.subscribe === false) break;
|
|
1886
|
+
const [, funcName, args] = event.query.queryKey;
|
|
1887
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
1888
|
+
const watch = this.convexClient.watchQuery(funcName, this.transformer.input.serialize(args));
|
|
1889
|
+
const unsubscribe = watch.onUpdate(() => {
|
|
1890
|
+
this.onUpdateQueryKeyHash(event.query.queryHash);
|
|
1891
|
+
});
|
|
1892
|
+
this.subscriptions[event.query.queryHash] = {
|
|
1893
|
+
queryKey: event.query.queryKey,
|
|
1894
|
+
watch,
|
|
1895
|
+
unsubscribe
|
|
1896
|
+
};
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
case "observerRemoved":
|
|
1900
|
+
if (event.query.getObserversCount() === 0 && this.subscriptions[event.query.queryHash]) {
|
|
1901
|
+
const queryHash = event.query.queryHash;
|
|
1902
|
+
const timeoutId = setTimeout(() => {
|
|
1903
|
+
this.getPendingUnsubscribesMap().delete(queryHash);
|
|
1904
|
+
if (event.query.getObserversCount() === 0) this.unsubscribeQueryByHash(queryHash);
|
|
1905
|
+
}, this.unsubscribeDelay);
|
|
1906
|
+
this.getPendingUnsubscribesMap().set(queryHash, timeoutId);
|
|
1907
|
+
}
|
|
1908
|
+
break;
|
|
1909
|
+
case "observerResultsUpdated": break;
|
|
1910
|
+
case "updated":
|
|
1911
|
+
if (event.action.type === "setState" && event.action.setStateOptions?.meta === "set by ConvexQueryClient") break;
|
|
1912
|
+
break;
|
|
1913
|
+
case "observerOptionsUpdated": {
|
|
1914
|
+
const isDisabled = event.query.options.enabled === false;
|
|
1915
|
+
const isSubscribed = !!this.subscriptions[event.query.queryHash];
|
|
1916
|
+
if (isDisabled && isSubscribed) {
|
|
1917
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
1918
|
+
this.unsubscribeQueryByHash(event.query.queryHash);
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
if (isSubscribed || isDisabled) break;
|
|
1922
|
+
const meta = event.query.meta;
|
|
1923
|
+
if (meta?.subscribe === false) break;
|
|
1924
|
+
const [, funcName, args] = event.query.queryKey;
|
|
1925
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
1926
|
+
const watch = this.convexClient.watchQuery(funcName, this.transformer.input.serialize(args));
|
|
1927
|
+
const unsubscribe = watch.onUpdate(() => {
|
|
1928
|
+
this.onUpdateQueryKeyHash(event.query.queryHash);
|
|
1929
|
+
});
|
|
1930
|
+
this.subscriptions[event.query.queryHash] = {
|
|
1931
|
+
queryKey: event.query.queryKey,
|
|
1932
|
+
watch,
|
|
1933
|
+
unsubscribe
|
|
1934
|
+
};
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Create default queryFn for TanStack QueryClient.
|
|
1942
|
+
*
|
|
1943
|
+
* Handles:
|
|
1944
|
+
* - Convex queries and actions
|
|
1945
|
+
* - Auth checking (throws CRPCClientError if unauthorized)
|
|
1946
|
+
* - SSR via HTTP client
|
|
1947
|
+
* - Client via WebSocket client
|
|
1948
|
+
*
|
|
1949
|
+
* ## Usage
|
|
1950
|
+
*
|
|
1951
|
+
* ```ts
|
|
1952
|
+
* const queryClient = new QueryClient({
|
|
1953
|
+
* defaultOptions: {
|
|
1954
|
+
* queries: {
|
|
1955
|
+
* queryFn: convexQueryClient.queryFn(),
|
|
1956
|
+
* },
|
|
1957
|
+
* },
|
|
1958
|
+
* });
|
|
1959
|
+
* ```
|
|
1960
|
+
*
|
|
1961
|
+
* @param otherFetch - Fallback queryFn for non-Convex queries
|
|
1962
|
+
* @returns QueryFunction compatible with TanStack Query
|
|
1963
|
+
*/
|
|
1964
|
+
queryFn(otherFetch = throwBecauseNotConvexQuery) {
|
|
1965
|
+
return async (context) => {
|
|
1966
|
+
const { queryKey, meta: rawMeta } = context;
|
|
1967
|
+
const meta = rawMeta;
|
|
1968
|
+
if (isConvexSkipped(queryKey)) throw new Error("Skipped query should not actually run, should { enabled: false }");
|
|
1969
|
+
if (isConvexQuery(queryKey)) {
|
|
1970
|
+
const [, funcName, args] = queryKey;
|
|
1971
|
+
const wireArgs = this.transformer.input.serialize(args);
|
|
1972
|
+
const skipUnauth = meta?.skipUnauth ?? false;
|
|
1973
|
+
if (meta?.authType === "required" && !isServer && this.authStore) {
|
|
1974
|
+
const authState = this.getAuthState();
|
|
1975
|
+
if (authState && !authState.isLoading && !authState.isAuthenticated) {
|
|
1976
|
+
if (skipUnauth) return null;
|
|
1977
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
1978
|
+
throw new CRPCClientError({
|
|
1979
|
+
code: "UNAUTHORIZED",
|
|
1980
|
+
functionName: funcName
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
if (isServer) {
|
|
1986
|
+
if (this.ssrQueryMode === "consistent") return this.transformer.output.deserialize(await this.serverHttpClient.consistentQuery(funcName, wireArgs));
|
|
1987
|
+
return this.transformer.output.deserialize(await this.serverHttpClient.query(funcName, wireArgs));
|
|
1988
|
+
}
|
|
1989
|
+
return this.transformer.output.deserialize(await this.convexClient.query(funcName, wireArgs));
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
if (skipUnauth && defaultIsUnauthorized(error)) return null;
|
|
1992
|
+
throw error;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (isConvexAction(queryKey)) {
|
|
1996
|
+
const [, funcName, args] = queryKey;
|
|
1997
|
+
const wireArgs = this.transformer.input.serialize(args);
|
|
1998
|
+
const skipUnauth = meta?.skipUnauth ?? false;
|
|
1999
|
+
if (meta?.authType === "required" && !isServer && this.authStore) {
|
|
2000
|
+
const authState = this.getAuthState();
|
|
2001
|
+
if (authState && !authState.isLoading && !authState.isAuthenticated) {
|
|
2002
|
+
if (skipUnauth) return null;
|
|
2003
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
2004
|
+
throw new CRPCClientError({
|
|
2005
|
+
code: "UNAUTHORIZED",
|
|
2006
|
+
functionName: funcName
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
if (isServer) return this.transformer.output.deserialize(await this.serverHttpClient.action(funcName, wireArgs));
|
|
2012
|
+
return this.transformer.output.deserialize(await this.convexClient.action(funcName, wireArgs));
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
if (skipUnauth && defaultIsUnauthorized(error)) return null;
|
|
2015
|
+
throw error;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return otherFetch(context);
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Create hash function for TanStack QueryClient.
|
|
2023
|
+
*
|
|
2024
|
+
* Uses Convex-specific hashing for Convex queries to ensure
|
|
2025
|
+
* consistent cache keys across serialization.
|
|
2026
|
+
*
|
|
2027
|
+
* ## Usage
|
|
2028
|
+
*
|
|
2029
|
+
* ```ts
|
|
2030
|
+
* const queryClient = new QueryClient({
|
|
2031
|
+
* defaultOptions: {
|
|
2032
|
+
* queries: {
|
|
2033
|
+
* queryKeyHashFn: convexQueryClient.hashFn(),
|
|
2034
|
+
* },
|
|
2035
|
+
* },
|
|
2036
|
+
* });
|
|
2037
|
+
* ```
|
|
2038
|
+
*
|
|
2039
|
+
* @param otherHashKey - Fallback hash function for non-Convex queries
|
|
2040
|
+
* @returns Hash function compatible with TanStack Query
|
|
2041
|
+
*/
|
|
2042
|
+
hashFn(fallback) {
|
|
2043
|
+
return createHashFn(fallback);
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
/** Default fallback queryFn that throws for non-Convex queries */
|
|
2047
|
+
function throwBecauseNotConvexQuery(context) {
|
|
2048
|
+
throw new Error(`Query key is not for a Convex Query: ${context.queryKey}`);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
//#endregion
|
|
2052
|
+
//#region src/react/singleton.ts
|
|
2053
|
+
const globalStore = globalThis;
|
|
2054
|
+
/** Get/create QueryClient singleton (fresh on SSR, singleton on client) */
|
|
2055
|
+
const getQueryClientSingleton = (factory, symbolKey = "convex.queryClient") => {
|
|
2056
|
+
const key = Symbol.for(symbolKey);
|
|
2057
|
+
if (typeof window === "undefined") return factory();
|
|
2058
|
+
if (!globalStore[key]) globalStore[key] = factory();
|
|
2059
|
+
return globalStore[key];
|
|
2060
|
+
};
|
|
2061
|
+
/** Get/create ConvexQueryClient singleton (fresh on SSR, singleton on client) */
|
|
2062
|
+
const getConvexQueryClientSingleton = ({ authStore, convex, queryClient, symbolKey = "convex.convexQueryClient", unsubscribeDelay, transformer }) => {
|
|
2063
|
+
const key = Symbol.for(symbolKey);
|
|
2064
|
+
const isServer = typeof window === "undefined";
|
|
2065
|
+
let client;
|
|
2066
|
+
if (isServer) client = new ConvexQueryClient(convex, {
|
|
2067
|
+
authStore,
|
|
2068
|
+
unsubscribeDelay,
|
|
2069
|
+
transformer
|
|
2070
|
+
});
|
|
2071
|
+
else {
|
|
2072
|
+
if (globalStore[key]) globalStore[key].updateAuthStore(authStore);
|
|
2073
|
+
else globalStore[key] = new ConvexQueryClient(convex, {
|
|
2074
|
+
authStore,
|
|
2075
|
+
unsubscribeDelay,
|
|
2076
|
+
transformer
|
|
2077
|
+
});
|
|
2078
|
+
client = globalStore[key];
|
|
2079
|
+
client.connect(queryClient);
|
|
2080
|
+
}
|
|
2081
|
+
const currentOpts = queryClient.getDefaultOptions();
|
|
2082
|
+
queryClient.setDefaultOptions({
|
|
2083
|
+
...currentOpts,
|
|
2084
|
+
queries: {
|
|
2085
|
+
...currentOpts.queries,
|
|
2086
|
+
queryFn: client.queryFn(),
|
|
2087
|
+
queryKeyHashFn: client.hashFn()
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
return client;
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region src/react/use-infinite-query.ts
|
|
2095
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: Convex query/mutation type compatibility */
|
|
2096
|
+
const PAGINATION_KEY_PREFIX = "__pagination__";
|
|
2097
|
+
const paginationIdStore = /* @__PURE__ */ new Map();
|
|
2098
|
+
let paginationIdCounter = 0;
|
|
2099
|
+
const getOrCreatePaginationId = (storeKey) => {
|
|
2100
|
+
const existing = paginationIdStore.get(storeKey);
|
|
2101
|
+
if (existing !== void 0) return existing;
|
|
2102
|
+
const newId = ++paginationIdCounter;
|
|
2103
|
+
paginationIdStore.set(storeKey, newId);
|
|
2104
|
+
return newId;
|
|
2105
|
+
};
|
|
2106
|
+
/** Build a unique key for recovery attempt detection */
|
|
2107
|
+
const buildRecoveryKey = (pageKeys, page0Cursor, page0UpdatedAt) => JSON.stringify({
|
|
2108
|
+
pageKeys,
|
|
2109
|
+
page0Cursor,
|
|
2110
|
+
page0UpdatedAt
|
|
2111
|
+
});
|
|
2112
|
+
/**
|
|
2113
|
+
* Hook for auto-recovering from stale cursors after WebSocket reconnection.
|
|
2114
|
+
*
|
|
2115
|
+
* When Convex WebSocket reconnects, page 0 (cursor: null) resubscribes and
|
|
2116
|
+
* gets fresh data. However, pages 1+ may have stale cursors that fail.
|
|
2117
|
+
*
|
|
2118
|
+
* This hook detects this pattern and creates a recovery page that fetches
|
|
2119
|
+
* enough items to cover the lost pages, preserving the user's scroll position.
|
|
2120
|
+
*/
|
|
2121
|
+
const useStaleCursorRecovery = ({ argsObject, combined, limit, setState, state }) => {
|
|
2122
|
+
useEffect(() => {
|
|
2123
|
+
if (!combined.isFetchNextPageError) return;
|
|
2124
|
+
const page0Result = combined._rawResults[0];
|
|
2125
|
+
const page0Data = page0Result?.data;
|
|
2126
|
+
const page0UpdatedAt = page0Result?.dataUpdatedAt ?? 0;
|
|
2127
|
+
const hasPage0Data = page0Data !== void 0 && !page0Result?.isError;
|
|
2128
|
+
const hasSubsequentErrors = combined._rawResults.slice(1).some((q) => q?.isError && !q?.isFetching);
|
|
2129
|
+
if (!hasPage0Data || !hasSubsequentErrors || !page0Data?.continueCursor) return;
|
|
2130
|
+
const recoveryKey = buildRecoveryKey(state.pageKeys, page0Data.continueCursor, page0UpdatedAt);
|
|
2131
|
+
if (state.autoRecoveryAttempted === recoveryKey) return;
|
|
2132
|
+
const erroredPageKeys = state.pageKeys.filter((_, i) => i > 0 && combined._rawResults[i]?.isError);
|
|
2133
|
+
const itemsToRecover = erroredPageKeys.reduce((sum, key) => {
|
|
2134
|
+
return sum + (state.queries[key]?.args?.limit ?? limit ?? 20);
|
|
2135
|
+
}, 0);
|
|
2136
|
+
console.warn("[Pagination] Auto-recovering from stale cursors", {
|
|
2137
|
+
erroredPages: erroredPageKeys.length,
|
|
2138
|
+
itemsToRecover
|
|
2139
|
+
});
|
|
2140
|
+
setState((prev) => ({
|
|
2141
|
+
...prev,
|
|
2142
|
+
id: prev.id,
|
|
2143
|
+
nextPageKey: 2,
|
|
2144
|
+
pageKeys: [prev.pageKeys[0], 1],
|
|
2145
|
+
queries: {
|
|
2146
|
+
[prev.pageKeys[0]]: prev.queries[prev.pageKeys[0]],
|
|
2147
|
+
1: { args: {
|
|
2148
|
+
...argsObject,
|
|
2149
|
+
cursor: page0Data.continueCursor,
|
|
2150
|
+
limit: Math.min(itemsToRecover + (limit ?? 20), 500),
|
|
2151
|
+
__paginationId: prev.id
|
|
2152
|
+
} }
|
|
2153
|
+
},
|
|
2154
|
+
version: prev.version + 1,
|
|
2155
|
+
autoRecoveryAttempted: recoveryKey
|
|
2156
|
+
}));
|
|
2157
|
+
}, [
|
|
2158
|
+
combined.isFetchNextPageError,
|
|
2159
|
+
combined._rawResults,
|
|
2160
|
+
state.pageKeys,
|
|
2161
|
+
state.queries,
|
|
2162
|
+
state.autoRecoveryAttempted,
|
|
2163
|
+
argsObject,
|
|
2164
|
+
limit,
|
|
2165
|
+
setState
|
|
2166
|
+
]);
|
|
2167
|
+
useEffect(() => {
|
|
2168
|
+
if ((combined.status === "CanLoadMore" || combined.status === "Exhausted") && state.autoRecoveryAttempted) setState((prev) => ({
|
|
2169
|
+
...prev,
|
|
2170
|
+
autoRecoveryAttempted: void 0
|
|
2171
|
+
}));
|
|
2172
|
+
}, [
|
|
2173
|
+
combined.status,
|
|
2174
|
+
state.autoRecoveryAttempted,
|
|
2175
|
+
setState
|
|
2176
|
+
]);
|
|
2177
|
+
};
|
|
2178
|
+
/**
|
|
2179
|
+
* Internal infinite query hook using TanStack Query + convexQuery.
|
|
2180
|
+
* Each page gets:
|
|
2181
|
+
* - Convex WebSocket subscription (real-time reactivity)
|
|
2182
|
+
* - TanStack Query retry on timeout errors
|
|
2183
|
+
*
|
|
2184
|
+
* Use `useInfiniteQuery` for the public API with auth handling.
|
|
2185
|
+
*/
|
|
2186
|
+
const useInfiniteQueryInternal = (query, args, options) => {
|
|
2187
|
+
const { limit, enabled, placeholderData, ...queryOptions } = options;
|
|
2188
|
+
const { isLoading: isAuthLoading } = useSafeConvexAuth();
|
|
2189
|
+
const meta = useMeta();
|
|
2190
|
+
const queryClient = useQueryClient();
|
|
2191
|
+
const prefetchedFirstPage = useMemo(() => {
|
|
2192
|
+
const serverQueryKey = [
|
|
2193
|
+
"convexQuery",
|
|
2194
|
+
getFunctionName(query),
|
|
2195
|
+
{
|
|
2196
|
+
...args,
|
|
2197
|
+
cursor: null,
|
|
2198
|
+
limit
|
|
2199
|
+
}
|
|
2200
|
+
];
|
|
2201
|
+
return queryClient.getQueryData(serverQueryKey) ?? null;
|
|
2202
|
+
}, [
|
|
2203
|
+
query,
|
|
2204
|
+
JSON.stringify(args),
|
|
2205
|
+
limit,
|
|
2206
|
+
queryClient
|
|
2207
|
+
]);
|
|
2208
|
+
const skip = !prefetchedFirstPage && (isAuthLoading || enabled === false);
|
|
2209
|
+
const getPaginationState = useCallback((key) => {
|
|
2210
|
+
const queryKey = [PAGINATION_KEY_PREFIX, key];
|
|
2211
|
+
return queryClient.getQueryData(queryKey);
|
|
2212
|
+
}, [queryClient]);
|
|
2213
|
+
const setPaginationState = useCallback((key, state) => {
|
|
2214
|
+
const queryKey = [PAGINATION_KEY_PREFIX, key];
|
|
2215
|
+
queryClient.setQueryData(queryKey, state);
|
|
2216
|
+
}, [queryClient]);
|
|
2217
|
+
const argsObject = useMemo(() => skip ? {} : args, [skip, JSON.stringify(args)]);
|
|
2218
|
+
const storeKey = useMemo(() => JSON.stringify({
|
|
2219
|
+
query: getFunctionName(query),
|
|
2220
|
+
args: argsObject
|
|
2221
|
+
}), [query, argsObject]);
|
|
2222
|
+
const createInitialState = useCallback(() => {
|
|
2223
|
+
const id = getOrCreatePaginationId(storeKey);
|
|
2224
|
+
return {
|
|
2225
|
+
id,
|
|
2226
|
+
nextPageKey: 1,
|
|
2227
|
+
pageKeys: skip ? [] : [0],
|
|
2228
|
+
queries: skip ? {} : { 0: { args: {
|
|
2229
|
+
...argsObject,
|
|
2230
|
+
cursor: null,
|
|
2231
|
+
limit,
|
|
2232
|
+
__paginationId: id
|
|
2233
|
+
} } },
|
|
2234
|
+
version: 0
|
|
2235
|
+
};
|
|
2236
|
+
}, [
|
|
2237
|
+
storeKey,
|
|
2238
|
+
skip,
|
|
2239
|
+
argsObject,
|
|
2240
|
+
limit
|
|
2241
|
+
]);
|
|
2242
|
+
const prevArgsRef = useRef(null);
|
|
2243
|
+
const [state, setLocalState] = useState(() => {
|
|
2244
|
+
if (skip) return {
|
|
2245
|
+
id: 0,
|
|
2246
|
+
nextPageKey: 1,
|
|
2247
|
+
pageKeys: [],
|
|
2248
|
+
queries: {},
|
|
2249
|
+
version: 0
|
|
2250
|
+
};
|
|
2251
|
+
const existingState = getPaginationState(storeKey);
|
|
2252
|
+
if (existingState) return existingState;
|
|
2253
|
+
return createInitialState();
|
|
2254
|
+
});
|
|
2255
|
+
const setState = useCallback((updater) => {
|
|
2256
|
+
setLocalState((prev) => {
|
|
2257
|
+
const newState = typeof updater === "function" ? updater(prev) : updater;
|
|
2258
|
+
setPaginationState(storeKey, newState);
|
|
2259
|
+
return newState;
|
|
2260
|
+
});
|
|
2261
|
+
}, [storeKey, setPaginationState]);
|
|
2262
|
+
useEffect(() => {
|
|
2263
|
+
const prev = prevArgsRef.current;
|
|
2264
|
+
const isFirstRun = prev === null;
|
|
2265
|
+
const argsChanged = prev !== null && (prev.storeKey !== storeKey || prev.skip !== skip);
|
|
2266
|
+
const skipBecameFalse = prev?.skip && !skip;
|
|
2267
|
+
prevArgsRef.current = {
|
|
2268
|
+
storeKey,
|
|
2269
|
+
skip
|
|
2270
|
+
};
|
|
2271
|
+
if (skip) return;
|
|
2272
|
+
if (isFirstRun) {
|
|
2273
|
+
setPaginationState(storeKey, state);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (skipBecameFalse) {
|
|
2277
|
+
const existingState = getPaginationState(storeKey);
|
|
2278
|
+
if (existingState) {
|
|
2279
|
+
setLocalState(existingState);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const newState = createInitialState();
|
|
2283
|
+
setLocalState(newState);
|
|
2284
|
+
setPaginationState(storeKey, newState);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (argsChanged) {
|
|
2288
|
+
const existingState = getPaginationState(storeKey);
|
|
2289
|
+
if (existingState) {
|
|
2290
|
+
setLocalState(existingState);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
const newState = createInitialState();
|
|
2294
|
+
setLocalState(newState);
|
|
2295
|
+
setPaginationState(storeKey, newState);
|
|
2296
|
+
}
|
|
2297
|
+
}, [
|
|
2298
|
+
skip,
|
|
2299
|
+
storeKey,
|
|
2300
|
+
state,
|
|
2301
|
+
createInitialState,
|
|
2302
|
+
getPaginationState,
|
|
2303
|
+
setPaginationState
|
|
2304
|
+
]);
|
|
2305
|
+
const combined = useQueries({
|
|
2306
|
+
queries: useMemo(() => state.pageKeys.map((key, index) => {
|
|
2307
|
+
const pageArgs = state.queries[key]?.args;
|
|
2308
|
+
return {
|
|
2309
|
+
...convexQuery(query, pageArgs ? (({ __paginationId, ...rest }) => rest)(pageArgs) : "skip", meta),
|
|
2310
|
+
enabled: !skip && !!state.queries[key],
|
|
2311
|
+
structuralSharing: false,
|
|
2312
|
+
...queryOptions ?? {},
|
|
2313
|
+
...index === 0 && prefetchedFirstPage ? { initialData: prefetchedFirstPage } : {},
|
|
2314
|
+
...index === 0 && placeholderData ? { placeholderData: {
|
|
2315
|
+
page: placeholderData,
|
|
2316
|
+
isDone: false,
|
|
2317
|
+
continueCursor: null
|
|
2318
|
+
} } : {}
|
|
2319
|
+
};
|
|
2320
|
+
}), [
|
|
2321
|
+
query,
|
|
2322
|
+
state.pageKeys,
|
|
2323
|
+
state.queries,
|
|
2324
|
+
skip,
|
|
2325
|
+
meta,
|
|
2326
|
+
queryOptions,
|
|
2327
|
+
prefetchedFirstPage,
|
|
2328
|
+
placeholderData
|
|
2329
|
+
]),
|
|
2330
|
+
combine: (results) => {
|
|
2331
|
+
const allItems = [];
|
|
2332
|
+
const pages = [];
|
|
2333
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2334
|
+
let lastPage;
|
|
2335
|
+
let paginationStatus = "LoadingFirstPage";
|
|
2336
|
+
for (let i = 0; i < results.length; i++) {
|
|
2337
|
+
const pageQuery = results[i];
|
|
2338
|
+
if (pageQuery.isLoading || pageQuery.data === void 0) {
|
|
2339
|
+
paginationStatus = i === 0 ? "LoadingFirstPage" : "LoadingMore";
|
|
2340
|
+
break;
|
|
2341
|
+
}
|
|
2342
|
+
const page = pageQuery.data;
|
|
2343
|
+
lastPage = page;
|
|
2344
|
+
pages.push(page.page);
|
|
2345
|
+
for (const item of page.page) {
|
|
2346
|
+
const id = item._id || item.id;
|
|
2347
|
+
if (id && seenIds.has(id)) continue;
|
|
2348
|
+
if (id) seenIds.add(id);
|
|
2349
|
+
allItems.push(item);
|
|
2350
|
+
}
|
|
2351
|
+
paginationStatus = page.isDone ? "Exhausted" : "CanLoadMore";
|
|
2352
|
+
}
|
|
2353
|
+
const isPlaceholderData = results[0]?.isPlaceholderData ?? !!placeholderData;
|
|
2354
|
+
const isFetching = results.some((r) => r.isFetching);
|
|
2355
|
+
const dataUpdatedAt = Math.max(...results.map((r) => r.dataUpdatedAt ?? 0));
|
|
2356
|
+
return {
|
|
2357
|
+
...results[0] ?? {},
|
|
2358
|
+
data: allItems,
|
|
2359
|
+
dataUpdatedAt,
|
|
2360
|
+
lastPage,
|
|
2361
|
+
pages,
|
|
2362
|
+
status: paginationStatus,
|
|
2363
|
+
error: results.find((r) => r.isError)?.error ?? null,
|
|
2364
|
+
isError: results.some((r) => r.isError),
|
|
2365
|
+
isFetching,
|
|
2366
|
+
isFetchNextPageError: results.length > 1 && (results.at(-1)?.isError ?? false),
|
|
2367
|
+
isPlaceholderData,
|
|
2368
|
+
isRefetching: isFetching && allItems.length > 0 && !isPlaceholderData,
|
|
2369
|
+
_rawResults: results
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
useStaleCursorRecovery({
|
|
2374
|
+
argsObject,
|
|
2375
|
+
combined,
|
|
2376
|
+
limit,
|
|
2377
|
+
setState,
|
|
2378
|
+
state
|
|
2379
|
+
});
|
|
2380
|
+
useEffect(() => {
|
|
2381
|
+
for (let i = 0; i < combined._rawResults.length; i++) {
|
|
2382
|
+
const pageQuery = combined._rawResults[i];
|
|
2383
|
+
if (pageQuery.data) {
|
|
2384
|
+
const page = pageQuery.data;
|
|
2385
|
+
const pageKey = state.pageKeys[i];
|
|
2386
|
+
const pageState = state.queries[pageKey];
|
|
2387
|
+
if (page.splitCursor && pageState && !pageState.endCursor) {
|
|
2388
|
+
setState((prev) => {
|
|
2389
|
+
const currentPageState = prev.queries[pageKey];
|
|
2390
|
+
if (!currentPageState || currentPageState.endCursor) return prev;
|
|
2391
|
+
const newKey = prev.nextPageKey;
|
|
2392
|
+
const splitCursor = page.splitCursor;
|
|
2393
|
+
const splitPageArgs = {
|
|
2394
|
+
...argsObject,
|
|
2395
|
+
cursor: splitCursor,
|
|
2396
|
+
limit: currentPageState.args.limit,
|
|
2397
|
+
__paginationId: prev.id
|
|
2398
|
+
};
|
|
2399
|
+
const pageKeyIndex = prev.pageKeys.indexOf(pageKey);
|
|
2400
|
+
const newPageKeys = [...prev.pageKeys];
|
|
2401
|
+
newPageKeys.splice(pageKeyIndex + 1, 0, newKey);
|
|
2402
|
+
return {
|
|
2403
|
+
...prev,
|
|
2404
|
+
nextPageKey: newKey + 1,
|
|
2405
|
+
pageKeys: newPageKeys,
|
|
2406
|
+
queries: {
|
|
2407
|
+
...prev.queries,
|
|
2408
|
+
[pageKey]: {
|
|
2409
|
+
...currentPageState,
|
|
2410
|
+
endCursor: splitCursor
|
|
2411
|
+
},
|
|
2412
|
+
[newKey]: { args: splitPageArgs }
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
});
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}, [
|
|
2421
|
+
combined._rawResults,
|
|
2422
|
+
state.pageKeys,
|
|
2423
|
+
state.queries,
|
|
2424
|
+
argsObject,
|
|
2425
|
+
setState
|
|
2426
|
+
]);
|
|
2427
|
+
const loadMore = useCallback((pageLimit) => {
|
|
2428
|
+
if (combined.status !== "CanLoadMore" || !combined.lastPage?.continueCursor) return;
|
|
2429
|
+
setState((prev) => {
|
|
2430
|
+
const newKey = prev.nextPageKey;
|
|
2431
|
+
return {
|
|
2432
|
+
...prev,
|
|
2433
|
+
nextPageKey: newKey + 1,
|
|
2434
|
+
pageKeys: [...prev.pageKeys, newKey],
|
|
2435
|
+
queries: {
|
|
2436
|
+
...prev.queries,
|
|
2437
|
+
[newKey]: { args: {
|
|
2438
|
+
...argsObject,
|
|
2439
|
+
cursor: combined.lastPage.continueCursor,
|
|
2440
|
+
limit: pageLimit,
|
|
2441
|
+
__paginationId: prev.id
|
|
2442
|
+
} }
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
});
|
|
2446
|
+
}, [
|
|
2447
|
+
combined.status,
|
|
2448
|
+
combined.lastPage,
|
|
2449
|
+
setState,
|
|
2450
|
+
argsObject
|
|
2451
|
+
]);
|
|
2452
|
+
const { _rawResults, lastPage, ...result } = combined;
|
|
2453
|
+
const hasNextPage = combined.status === "CanLoadMore";
|
|
2454
|
+
const isFetchingNextPage = combined.status === "LoadingMore";
|
|
2455
|
+
return {
|
|
2456
|
+
...result,
|
|
2457
|
+
failureReason: combined.failureReason,
|
|
2458
|
+
error: combined.error instanceof Error ? combined.error : null,
|
|
2459
|
+
fetchNextPage: (n) => loadMore(n ?? limit),
|
|
2460
|
+
hasNextPage,
|
|
2461
|
+
isFetchingNextPage
|
|
2462
|
+
};
|
|
2463
|
+
};
|
|
2464
|
+
/**
|
|
2465
|
+
* Infinite query hook using cRPC-style options.
|
|
2466
|
+
* Accepts options from `crpc.posts.list.infiniteQueryOptions()`.
|
|
2467
|
+
*
|
|
2468
|
+
* @example
|
|
2469
|
+
* ```tsx
|
|
2470
|
+
* const crpc = useCRPC();
|
|
2471
|
+
* const { data, fetchNextPage } = useInfiniteQuery(
|
|
2472
|
+
* crpc.posts.list.infiniteQueryOptions({ userId }, { limit: 20 })
|
|
2473
|
+
* );
|
|
2474
|
+
* ```
|
|
2475
|
+
*/
|
|
2476
|
+
function useInfiniteQuery(infiniteOptions) {
|
|
2477
|
+
const query = infiniteOptions[FUNC_REF_SYMBOL];
|
|
2478
|
+
const onQueryUnauthorized = useAuthValue("onQueryUnauthorized");
|
|
2479
|
+
const { isLoading: isAuthLoading, isAuthenticated } = useSafeConvexAuth();
|
|
2480
|
+
const { queryKey: _queryKey, staleTime: _staleTime, refetchInterval: _refetchInterval, refetchOnMount: _refetchOnMount, refetchOnReconnect: _refetchOnReconnect, refetchOnWindowFocus: _refetchOnWindowFocus, enabled: factoryEnabled, meta, ...queryOptions } = infiniteOptions;
|
|
2481
|
+
const { queryName, args, limit, authType, skipUnauth } = meta;
|
|
2482
|
+
const skipUnauthFinal = skipUnauth ?? false;
|
|
2483
|
+
const isUnauthorized = authType === "required" && !isAuthLoading && !isAuthenticated;
|
|
2484
|
+
const shouldSkip = factoryEnabled === false || authType === "required" && isAuthLoading || authType === "required" && !isAuthenticated;
|
|
2485
|
+
const authError = useMemo(() => {
|
|
2486
|
+
if (isUnauthorized && !skipUnauthFinal) return new CRPCClientError({
|
|
2487
|
+
code: "UNAUTHORIZED",
|
|
2488
|
+
functionName: queryName
|
|
2489
|
+
});
|
|
2490
|
+
return null;
|
|
2491
|
+
}, [
|
|
2492
|
+
isUnauthorized,
|
|
2493
|
+
skipUnauthFinal,
|
|
2494
|
+
queryName
|
|
2495
|
+
]);
|
|
2496
|
+
useEffect(() => {
|
|
2497
|
+
if (isUnauthorized && !skipUnauthFinal) onQueryUnauthorized({ queryName });
|
|
2498
|
+
}, [
|
|
2499
|
+
isUnauthorized,
|
|
2500
|
+
skipUnauthFinal,
|
|
2501
|
+
queryName,
|
|
2502
|
+
onQueryUnauthorized
|
|
2503
|
+
]);
|
|
2504
|
+
const result = useInfiniteQueryInternal(query, args, {
|
|
2505
|
+
limit,
|
|
2506
|
+
...queryOptions,
|
|
2507
|
+
enabled: !shouldSkip
|
|
2508
|
+
});
|
|
2509
|
+
const authLoadingApplies = authType === "optional" || authType === "required";
|
|
2510
|
+
const isClientError = isCRPCClientError(result.error);
|
|
2511
|
+
const isSkippedUnauth = isUnauthorized && skipUnauthFinal;
|
|
2512
|
+
return {
|
|
2513
|
+
...result,
|
|
2514
|
+
data: isSkippedUnauth ? [] : result.data,
|
|
2515
|
+
pages: isSkippedUnauth ? [] : result.pages,
|
|
2516
|
+
...authError && {
|
|
2517
|
+
error: authError,
|
|
2518
|
+
isError: true
|
|
2519
|
+
},
|
|
2520
|
+
...isSkippedUnauth && { isPlaceholderData: false },
|
|
2521
|
+
isLoading: authLoadingApplies && isAuthLoading || !isClientError && !authError && !isSkippedUnauth && result.isLoading
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
//#endregion
|
|
2526
|
+
export { AUTH_SESSION_SYNC_GRACE_MS, AuthMutationError, AuthProvider, Authenticated, ConvexAuthBridge, ConvexProvider, ConvexProviderWithAuth, ConvexQueryClient, ConvexReactClient, FetchAccessTokenContext, MaybeAuthenticated, MaybeUnauthenticated, Unauthenticated, createAuthMutations, createCRPCContext, createCRPCOptionsProxy, createHttpProxy, createVanillaCRPCProxy, decodeJwtExp, getConvexQueryClientSingleton, getQueryClientSingleton, isAuthMutationError, isSessionSyncGraceActive, useAuth, useAuthGuard, useAuthState, useAuthStore, useAuthValue, useConvex, useConvexActionOptions, useConvexActionQueryOptions, useSafeConvexAuth as useConvexAuth, useSafeConvexAuth, useConvexAuthBridge, useConvexInfiniteQueryOptions, useConvexMutationOptions, useConvexQueryClient, useConvexQueryOptions, useFetchAccessToken, useFnMeta, useInfiniteQuery, useIsAuth, useMaybeAuth, useMeta, useUploadMutationOptions };
|