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.
Files changed (93) hide show
  1. package/bin/intent.js +3 -0
  2. package/dist/aggregate/index.d.ts +388 -0
  3. package/dist/aggregate/index.js +37 -0
  4. package/dist/api-entry-BckXqaLb.js +66 -0
  5. package/dist/auth/client/index.d.ts +37 -0
  6. package/dist/auth/client/index.js +217 -0
  7. package/dist/auth/config/index.d.ts +45 -0
  8. package/dist/auth/config/index.js +24 -0
  9. package/dist/auth/generated/index.d.ts +2 -0
  10. package/dist/auth/generated/index.js +3 -0
  11. package/dist/auth/http/index.d.ts +64 -0
  12. package/dist/auth/http/index.js +461 -0
  13. package/dist/auth/index.d.ts +221 -0
  14. package/dist/auth/index.js +1398 -0
  15. package/dist/auth/nextjs/index.d.ts +50 -0
  16. package/dist/auth/nextjs/index.js +81 -0
  17. package/dist/auth-store-Cljlmdmi.js +197 -0
  18. package/dist/builder-CBdG5W6A.js +1974 -0
  19. package/dist/caller-factory-cTXNvYdz.js +216 -0
  20. package/dist/cli.mjs +13264 -0
  21. package/dist/codegen-lF80HSWu.mjs +3416 -0
  22. package/dist/context-utils-HPC5nXzx.d.ts +17 -0
  23. package/dist/create-schema-odyF4kCy.js +156 -0
  24. package/dist/create-schema-orm-DOyiNDCx.js +246 -0
  25. package/dist/crpc/index.d.ts +105 -0
  26. package/dist/crpc/index.js +169 -0
  27. package/dist/customFunctions-C0voKmtx.js +144 -0
  28. package/dist/error-BZEnI7Sq.js +41 -0
  29. package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
  30. package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
  31. package/dist/http-types-DqJubRPJ.d.ts +292 -0
  32. package/dist/meta-utils-0Pu0Nrap.js +117 -0
  33. package/dist/middleware-BUybuv9n.d.ts +34 -0
  34. package/dist/middleware-C2qTZ3V7.js +84 -0
  35. package/dist/orm/index.d.ts +17 -0
  36. package/dist/orm/index.js +10713 -0
  37. package/dist/plugins/index.d.ts +2 -0
  38. package/dist/plugins/index.js +3 -0
  39. package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
  40. package/dist/procedure-caller-MWcxhQDv.js +349 -0
  41. package/dist/query-context-B8o6-8kC.js +1518 -0
  42. package/dist/query-context-CFZqIvD7.d.ts +42 -0
  43. package/dist/query-options-Dw7cOyXl.js +121 -0
  44. package/dist/ratelimit/index.d.ts +269 -0
  45. package/dist/ratelimit/index.js +856 -0
  46. package/dist/ratelimit/react/index.d.ts +76 -0
  47. package/dist/ratelimit/react/index.js +183 -0
  48. package/dist/react/index.d.ts +1284 -0
  49. package/dist/react/index.js +2526 -0
  50. package/dist/rsc/index.d.ts +276 -0
  51. package/dist/rsc/index.js +233 -0
  52. package/dist/runtime-CtvJPkur.js +2453 -0
  53. package/dist/server/index.d.ts +5 -0
  54. package/dist/server/index.js +6 -0
  55. package/dist/solid/index.d.ts +1221 -0
  56. package/dist/solid/index.js +2940 -0
  57. package/dist/transformer-DtDhR3Lc.js +194 -0
  58. package/dist/types-BTb_4BaU.d.ts +42 -0
  59. package/dist/types-BiJE7qxR.d.ts +4 -0
  60. package/dist/types-DEJpkIhw.d.ts +88 -0
  61. package/dist/types-HhO_R6pd.d.ts +213 -0
  62. package/dist/validators-B7oIJCAp.js +279 -0
  63. package/dist/validators-vzRKjBJC.d.ts +88 -0
  64. package/dist/watcher.mjs +96 -0
  65. package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
  66. package/package.json +107 -34
  67. package/skills/convex/SKILL.md +486 -0
  68. package/skills/convex/references/features/aggregates.md +353 -0
  69. package/skills/convex/references/features/auth-admin.md +446 -0
  70. package/skills/convex/references/features/auth-organizations.md +1141 -0
  71. package/skills/convex/references/features/auth-polar.md +579 -0
  72. package/skills/convex/references/features/auth.md +470 -0
  73. package/skills/convex/references/features/create-plugins.md +153 -0
  74. package/skills/convex/references/features/http.md +676 -0
  75. package/skills/convex/references/features/migrations.md +162 -0
  76. package/skills/convex/references/features/orm.md +1166 -0
  77. package/skills/convex/references/features/react.md +657 -0
  78. package/skills/convex/references/features/scheduling.md +267 -0
  79. package/skills/convex/references/features/testing.md +209 -0
  80. package/skills/convex/references/setup/auth.md +501 -0
  81. package/skills/convex/references/setup/biome.md +190 -0
  82. package/skills/convex/references/setup/doc-guidelines.md +145 -0
  83. package/skills/convex/references/setup/index.md +761 -0
  84. package/skills/convex/references/setup/next.md +116 -0
  85. package/skills/convex/references/setup/react.md +175 -0
  86. package/skills/convex/references/setup/server.md +473 -0
  87. package/skills/convex/references/setup/start.md +67 -0
  88. package/LICENSE +0 -21
  89. package/README.md +0 -0
  90. package/dist/index.d.mts +0 -5
  91. package/dist/index.d.mts.map +0 -1
  92. package/dist/index.mjs +0 -6
  93. 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 };