kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,555 lines (1,539 loc) • 85.4 kB
JavaScript
'use client';
import { D as CRPCClientError, E as writeAuthSessionFallbackToken, O as defaultIsUnauthorized, S as clearAuthSessionFallback, T as writeAuthSessionFallbackData, _ 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, k as isCRPCClientError, 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, x as useSafeConvexAuth, y as useIsAuth } from "../auth-store-CwGbvP_s.js";
import { ConvexProvider, ConvexReactClient, ConvexReactClient as ConvexReactClient$1, useAction, useConvex, useMutation } from "convex/react";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { jsx } from "react/jsx-runtime";
import { getFunctionName } from "convex/server";
import { notifyManager, skipToken, useQueries, useQueryClient } from "@tanstack/react-query";
import { hashKey } from "@tanstack/query-core";
import { convexToJson } from "convex/values";
import { ConvexHttpClient } from "convex/browser";
//#region src/shared/meta-utils.ts
const metaCache = /* @__PURE__ */ new WeakMap();
const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function isFunctionType(value) {
return value === "query" || value === "mutation" || value === "action";
}
function isMetaScalar(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function extractLeafMeta(value) {
const type = value.type;
if (!isFunctionType(type)) return;
const result = { type };
for (const [key, entry] of Object.entries(value)) {
if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
if (entry === void 0) continue;
if (isMetaScalar(entry)) result[key] = entry;
}
return result;
}
function getHttpRoutes(api) {
const routes = api._http;
if (!isRecord(routes)) return;
const normalized = {};
for (const [routeKey, routeValue] of Object.entries(routes)) {
if (!isRecord(routeValue)) continue;
const routePath = routeValue.path;
const routeMethod = routeValue.method;
if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
path: routePath,
method: routeMethod
};
}
return normalized;
}
/**
* Build a metadata index from merged API leaves.
* Supports both generated `api` objects and plain metadata fixtures.
*/
function buildMetaIndex(api) {
const cached = metaCache.get(api);
if (cached) return cached;
const meta = {};
const httpRoutes = getHttpRoutes(api);
if (httpRoutes) meta._http = httpRoutes;
const walk = (node, path) => {
for (const [key, value] of Object.entries(node)) {
if (key.startsWith("_")) continue;
if (!isRecord(value)) continue;
const leafMeta = extractLeafMeta(value);
if (leafMeta) {
if (path.length === 0) continue;
const namespace = path.join("/");
meta[namespace] ??= {};
meta[namespace][key] = leafMeta;
continue;
}
walk(value, [...path, key]);
}
};
walk(api, []);
metaCache.set(api, meta);
return meta;
}
/**
* Get a function reference from the API object by traversing the path.
*/
function getFuncRef(api, path) {
let current = api;
for (const key of path) if (current && typeof current === "object") {
const next = current[key];
if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
current = next;
} else throw new Error(`Invalid path: ${path.join(".")}`);
if (current && typeof current === "object") {
const maybeFunctionRef = current.functionRef;
if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
}
if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
return current;
}
/**
* Get function type from meta using path.
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
*
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
* @param meta - The meta object from codegen
* @returns Function type or 'query' as default
*/
function getFunctionType(path, source) {
if (path.length < 2) return "query";
const meta = buildMetaIndex(source);
const fnName = path.at(-1);
const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
return "query";
}
/**
* Get function metadata from meta using path.
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
*
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
* @param meta - The meta object from codegen
* @returns Function metadata or undefined
*/
function getFunctionMeta(path, source) {
if (path.length < 2) return;
const meta = buildMetaIndex(source);
const fnName = path.at(-1);
return meta[path.slice(0, -1).join("/")]?.[fnName];
}
//#endregion
//#region src/crpc/http-types.ts
/** HTTP client error */
var HttpClientError = class extends Error {
name = "HttpClientError";
code;
status;
procedureName;
constructor(opts) {
super(opts.message ?? `${opts.code}: ${opts.procedureName}`);
this.code = opts.code;
this.status = opts.status;
this.procedureName = opts.procedureName;
}
};
//#endregion
//#region src/crpc/http-client.ts
/**
* HTTP Client Helpers
*
* Framework-agnostic utilities for executing HTTP requests
* against Convex HTTP endpoints.
*/
/** Reserved keys that are not part of JSON body */
const RESERVED_KEYS = new Set([
"params",
"searchParams",
"form",
"fetch",
"init",
"headers"
]);
/**
* Replace URL path parameters with actual values.
* e.g., '/users/:id' with { id: '123' } -> '/users/123'
*/
function replaceUrlParam(url, params) {
return url.replace(/:(\w+)/g, (_, key) => {
const value = params[key];
return value !== void 0 ? encodeURIComponent(value) : `:${key}`;
});
}
/**
* Build URLSearchParams from query object.
* Handles array values as multiple params with same key (like Hono).
*/
function buildSearchParams(query) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) for (const v of value) params.append(key, v);
else if (value !== void 0 && value !== null) params.append(key, value);
return params;
}
/**
* Hono-style HTTP request executor.
* Processes args in the same way as Hono's ClientRequestImpl.fetch().
*/
async function executeHttpRequest(opts) {
const { method, path } = opts.route;
const args = opts.args ?? {};
let rBody;
let cType;
if (args.form) {
const form = new FormData();
for (const [k, v] of Object.entries(args.form)) if (Array.isArray(v)) for (const v2 of v) form.append(k, v2);
else form.append(k, v);
rBody = form;
} else {
const jsonBody = {};
for (const [key, value] of Object.entries(args)) if (!RESERVED_KEYS.has(key) && value !== void 0) jsonBody[key] = value;
if (Object.keys(jsonBody).length > 0) {
rBody = JSON.stringify(opts.transformer.input.serialize(jsonBody));
cType = "application/json";
}
}
const argsClientOpts = {};
if (args.fetch) argsClientOpts.fetch = args.fetch;
if (args.init) argsClientOpts.init = args.init;
if (args.headers) argsClientOpts.headers = args.headers;
const mergedClientOpts = {
...opts.clientOpts,
...argsClientOpts
};
const resolvedBaseHeaders = typeof opts.baseHeaders === "function" ? await opts.baseHeaders() : opts.baseHeaders;
const headerValues = { ...typeof mergedClientOpts.headers === "function" ? await mergedClientOpts.headers() : mergedClientOpts.headers };
if (cType) headerValues["Content-Type"] = cType;
const finalHeaders = {};
if (resolvedBaseHeaders) {
for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
}
Object.assign(finalHeaders, headerValues);
let url = opts.convexSiteUrl + path;
if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
if (args.searchParams) {
const queryString = buildSearchParams(args.searchParams).toString();
if (queryString) url = `${url}?${queryString}`;
}
const methodUpperCase = method.toUpperCase();
const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
body: setBody ? rBody : void 0,
method: methodUpperCase,
headers: finalHeaders,
...mergedClientOpts.init
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: {
code: "UNKNOWN",
message: response.statusText
} }));
const errorCode = errorData?.error?.code || "UNKNOWN";
const errorMessage = errorData?.error?.message || response.statusText;
throw new HttpClientError({
code: errorCode,
status: response.status,
procedureName: opts.procedureName,
message: errorMessage
});
}
if (response.headers.get("content-length") === "0" || response.status === 204) return;
if ((response.headers.get("content-type") || "").includes("application/json")) return opts.transformer.output.deserialize(await response.json());
return response.text();
}
//#endregion
//#region src/crpc/transformer.ts
const isPlainObject = (value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
const CODEC_MARKER_KEY = "__crpc";
const CODEC_MARKER_VALUE = 1;
const CODEC_TAG_KEY = "t";
const CODEC_VALUE_KEY = "v";
const hasOnlyCodecPayloadKeys = (value) => {
let keyCount = 0;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
keyCount += 1;
if (key !== CODEC_MARKER_KEY && key !== CODEC_TAG_KEY && key !== CODEC_VALUE_KEY) return false;
if (keyCount > 3) return false;
}
return keyCount === 3;
};
/**
* Date wire tag (Convex-style reserved key).
*/
const DATE_CODEC_TAG = "$date";
/**
* Built-in Date codec.
*/
const dateWireCodec = {
tag: DATE_CODEC_TAG,
isType: (value) => value instanceof Date,
encode: (value) => value.getTime(),
decode: (value) => {
if (typeof value !== "number") return value;
return new Date(value);
}
};
/**
* Build a recursive tagged transformer from codecs.
*/
const createTaggedTransformer = (codecs) => {
const codecByTag = /* @__PURE__ */ new Map();
for (const codec of codecs) {
if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`);
if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`);
codecByTag.set(codec.tag, codec);
}
const serialize = (value) => {
for (const codec of codecs) if (codec.isType(value)) return {
[CODEC_MARKER_KEY]: CODEC_MARKER_VALUE,
[CODEC_TAG_KEY]: codec.tag,
[CODEC_VALUE_KEY]: serialize(codec.encode(value))
};
if (Array.isArray(value)) {
let result;
for (let index = 0; index < value.length; index += 1) {
const item = value[index];
const serialized = serialize(item);
if (serialized !== item) {
if (!result) result = value.slice();
result[index] = serialized;
}
}
return result ?? value;
}
if (isPlainObject(value)) {
let result;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const nested = value[key];
const serialized = serialize(nested);
if (serialized !== nested) {
if (!result) result = { ...value };
result[key] = serialized;
}
}
return result ?? value;
}
return value;
};
const deserialize = (value) => {
if (Array.isArray(value)) {
let result;
for (let index = 0; index < value.length; index += 1) {
const item = value[index];
const deserialized = deserialize(item);
if (deserialized !== item) {
if (!result) result = value.slice();
result[index] = deserialized;
}
}
return result ?? value;
}
if (isPlainObject(value)) {
const marker = value[CODEC_MARKER_KEY];
const tag = value[CODEC_TAG_KEY];
if (marker === CODEC_MARKER_VALUE && typeof tag === "string" && CODEC_VALUE_KEY in value && hasOnlyCodecPayloadKeys(value)) {
const codec = codecByTag.get(tag);
if (codec) return codec.decode(deserialize(value[CODEC_VALUE_KEY]));
}
let result;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const nested = value[key];
const deserialized = deserialize(nested);
if (deserialized !== nested) {
if (!result) result = { ...value };
result[key] = deserialized;
}
}
return result ?? value;
}
return value;
};
return {
serialize,
deserialize
};
};
/**
* Default cRPC transformer (Date-enabled).
*/
const defaultCRPCTransformer = createTaggedTransformer([dateWireCodec]);
const DEFAULT_COMBINED_TRANSFORMER = {
input: defaultCRPCTransformer,
output: defaultCRPCTransformer
};
/**
* Normalize transformer config to split input/output shape.
*/
const normalizeCustomTransformer = (transformer) => {
if (!transformer) return;
if ("input" in transformer && "output" in transformer) return transformer;
return {
input: transformer,
output: transformer
};
};
/**
* Compose user transformer with default Date transformer.
*
* Date transformer is always active:
* - serialize: user -> default(Date)
* - deserialize: default(Date) -> user
*/
const composeWithDefault = (transformer) => {
if (!transformer) return defaultCRPCTransformer;
return {
serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)),
deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value))
};
};
const transformerCache = /* @__PURE__ */ new WeakMap();
/**
* Normalize transformer config to split input/output shape.
* User transformers are additive and always composed with default Date handling.
*/
const getTransformer = (transformer) => {
if (!transformer) return DEFAULT_COMBINED_TRANSFORMER;
const cacheKey = transformer;
const cached = transformerCache.get(cacheKey);
if (cached) return cached;
const custom = normalizeCustomTransformer(transformer);
const resolved = {
input: composeWithDefault(custom?.input),
output: composeWithDefault(custom?.output)
};
transformerCache.set(cacheKey, resolved);
return resolved;
};
//#endregion
//#region src/react/http-proxy.ts
/**
* Create a recursive proxy for HTTP routes with TanStack Query integration.
*
* Terminal methods:
* - GET endpoints: `queryOptions`, `queryKey`
* - POST/PUT/PATCH/DELETE: `mutationOptions`, `mutationKey`
*/
function createRecursiveHttpProxy(opts, path = []) {
return new Proxy(() => {}, { get(_, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
const routeKey = path.join(".");
const route = opts.routes[routeKey];
if (prop === "query") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
};
}
if (prop === "mutate") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
};
}
if (prop === "queryOptions") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
return (args, queryOpts) => ({
...queryOpts,
queryKey: [
"httpQuery",
routeKey,
args
],
queryFn: async () => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
}
});
}
if (prop === "queryKey") return (args) => {
return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
"httpQuery",
routeKey,
args
] : ["httpQuery", routeKey];
};
if (prop === "queryFilter") return (args, filters) => {
const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
return {
...filters,
queryKey: hasArgs ? [
"httpQuery",
routeKey,
args
] : ["httpQuery", routeKey]
};
};
if (prop === "mutationOptions") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return (mutationOpts) => ({
...mutationOpts,
mutationKey: ["httpMutation", routeKey],
mutationFn: async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
}
});
}
if (prop === "mutationKey") return () => ["httpMutation", routeKey];
return createRecursiveHttpProxy(opts, [...path, prop]);
} });
}
/**
* Create an HTTP proxy with TanStack Query integration.
*
* Returns a proxy that provides:
* - `queryOptions` for GET endpoints (no subscription)
* - `mutationOptions` for POST/PUT/PATCH/DELETE endpoints
*
* @example
* ```ts
* const httpProxy = createHttpProxy<AppRouter>({
* convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
* routes: httpRoutes,
* });
*
* // GET endpoint
* const opts = httpProxy.todos.get.queryOptions({ id: '123' });
* const { data } = useQuery(opts);
*
* // POST endpoint
* const mutation = useMutation(httpProxy.todos.create.mutationOptions());
* await mutation.mutateAsync({ title: 'New todo' });
* ```
*/
function createHttpProxy(opts) {
const transformer = getTransformer(opts.transformer);
return createRecursiveHttpProxy({
convexSiteUrl: opts.convexSiteUrl,
routes: opts.routes,
headers: opts.headers,
fetch: opts.fetch,
onError: opts.onError,
transformer
});
}
//#endregion
//#region src/crpc/query-options.ts
/**
* Query options factory for Convex query function subscriptions.
* Requires `convexQueryClient.queryFn()` set as the default `queryFn` globally.
*/
function convexQuery(funcRef, args, meta, opts) {
const finalArgs = args ?? {};
const isSkip = finalArgs === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = meta?.[namespace]?.[fnName]?.auth;
const skipUnauth = opts?.skipUnauth;
return {
queryKey: [
"convexQuery",
funcName,
isSkip ? "skip" : finalArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...isSkip ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: true
}
};
}
/**
* Query options factory for Convex action functions.
* Actions are NOT reactive - they follow normal TanStack Query semantics.
*
* @example
* ```ts
* useQuery(convexAction(api.ai.generate, { prompt }))
* ```
*
* @example With additional options (use spread):
* ```ts
* useQuery({
* ...convexAction(api.files.process, { fileId }),
* staleTime: 60_000
* });
* ```
*/
function convexAction(funcRef, args, meta, opts) {
const finalArgs = args ?? {};
const isSkip = finalArgs === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = meta?.[namespace]?.[fnName]?.auth;
const skipUnauth = opts?.skipUnauth;
return {
queryKey: [
"convexAction",
funcName,
isSkip ? {} : finalArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...isSkip ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: false
}
};
}
/**
* Infinite query options factory for paginated Convex queries.
* Server-safe (non-hook) - can be used in RSC.
*
* Uses flat { cursor, limit } input like tRPC.
*/
function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
const { limit, skipUnauth, enabled, ...queryOptions } = opts;
const finalArgs = args === "skip" ? {} : args;
const isSkip = args === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = (meta?.[namespace]?.[fnName])?.auth;
const firstPageArgs = {
...finalArgs,
cursor: null,
limit
};
const finalEnabled = enabled === false || isSkip ? false : void 0;
return {
queryKey: [
"convexQuery",
funcName,
firstPageArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...queryOptions,
...finalEnabled === false ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: true,
queryName: funcName,
args: finalArgs,
limit
}
};
}
//#endregion
//#region src/crpc/types.ts
/** Symbol key for attaching FunctionReference to options (non-serializable) */
const FUNC_REF_SYMBOL = Symbol.for("convex.funcRef");
//#endregion
//#region src/internal/auth.ts
/** Get auth type from meta for a function */
function getAuthType(meta, funcName) {
const [namespace, fnName] = funcName.split(":");
return meta?.[namespace]?.[fnName]?.auth;
}
/** Hook to compute auth-based skip logic for queries */
function useAuthSkip(funcRef, opts) {
const { isAuthenticated, isLoading: isAuthLoading } = useSafeConvexAuth();
const authType = getAuthType(useMeta(), getFunctionName(funcRef));
const authLoadingApplies = authType === "optional" || authType === "required";
return {
authType,
isAuthLoading,
isAuthenticated,
shouldSkip: opts?.enabled === false || authLoadingApplies && isAuthLoading || authType === "required" && !isAuthenticated && !isAuthLoading || !isAuthenticated && !isAuthLoading && !!opts?.skipUnauth
};
}
//#endregion
//#region src/internal/query-key.ts
/**
* Shared query key utilities for Convex + TanStack Query.
* This file has NO React dependencies so it can be imported in both
* server (RSC) and client contexts.
*/
/**
* Check if query key is for a Convex query function.
* Format: ['convexQuery', 'namespace:functionName', { args }]
*/
function isConvexQuery(queryKey) {
return queryKey.length >= 2 && queryKey[0] === "convexQuery";
}
/**
* Check if query key is for a Convex action function.
* Format: ['convexAction', 'namespace:functionName', { args }]
*/
function isConvexAction$1(queryKey) {
return queryKey.length >= 2 && queryKey[0] === "convexAction";
}
/**
* Create stable hash for Convex query keys.
* Uses Convex's JSON serialization for consistent argument hashing.
*/
function hashConvexQuery(queryKey) {
const [, funcName, args] = queryKey;
return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
}
/**
* Create stable hash for Convex action keys.
* Uses Convex's JSON serialization for consistent argument hashing.
*/
function hashConvexAction(queryKey) {
const [, funcName, args] = queryKey;
return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
}
//#endregion
//#region src/internal/hash.ts
/**
* Create a hash function for TanStack Query that handles Convex keys.
*/
function createHashFn(fallback = hashKey) {
return (queryKey) => {
if (isConvexQuery(queryKey)) return hashConvexQuery(queryKey);
if (isConvexAction$1(queryKey)) return hashConvexAction(queryKey);
return fallback(queryKey);
};
}
//#endregion
//#region src/react/use-query-options.ts
/** biome-ignore-all lint/suspicious/noExplicitAny: Convex type compatibility */
/**
* Query options factories for Convex functions.
* Forked from @convex-dev/react-query to support auth-aware error handling.
*/
const EMPTY_ARGS = {};
const hashConvexOptionsKey = createHashFn();
const MAX_STABLE_ARGS = 500;
const stableArgsByHash = /* @__PURE__ */ new Map();
function getStableArgsByHash(hash, args) {
if (stableArgsByHash.has(hash)) {
const stableArgs = stableArgsByHash.get(hash);
stableArgsByHash.delete(hash);
stableArgsByHash.set(hash, stableArgs);
return stableArgs;
}
stableArgsByHash.set(hash, args);
if (stableArgsByHash.size > MAX_STABLE_ARGS) {
const oldestHash = stableArgsByHash.keys().next().value;
if (oldestHash !== void 0) stableArgsByHash.delete(oldestHash);
}
return args;
}
function useStableQueryArgs(prefix, funcRef, args) {
const resolvedArgs = args === skipToken || args == null ? EMPTY_ARGS : args;
const argsHash = hashConvexOptionsKey([
prefix,
getFunctionName(funcRef),
resolvedArgs
]);
const value = useMemo(() => getStableArgsByHash(argsHash, resolvedArgs), [argsHash, resolvedArgs]);
return useMemo(() => ({
hash: argsHash,
value
}), [argsHash, value]);
}
/**
* Hook that returns query options for use with useQuery.
* Handles skipUnauth by setting enabled: false when unauthorized.
*
* @example
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }));
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, userId ? { id: userId } : skipToken));
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { skipUnauth: true }));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { enabled: !!id, placeholderData: [] }));
* ```
*/
function useConvexQueryOptions(funcRef, args, options) {
const isSkipped = args === skipToken;
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
const { authType, shouldSkip } = useAuthSkip(funcRef, {
enabled: isSkipped ? false : enabled,
skipUnauth: options?.skipUnauth
});
const stableArgs = useStableQueryArgs("convexQuery", funcRef, isSkipped ? EMPTY_ARGS : args);
const baseOptions = useMemo(() => convexQuery(funcRef, stableArgs.value), [funcRef, stableArgs]);
return useMemo(() => {
const { skipUnauth: _, subscribe, ...queryOptions } = options ?? {};
return {
...baseOptions,
...queryOptions,
enabled: isSkipped ? false : !shouldSkip,
meta: {
...baseOptions.meta,
authType,
subscribe: subscribe !== false
}
};
}, [
authType,
baseOptions,
isSkipped,
options,
shouldSkip
]);
}
/**
* Hook that returns infinite query options for use with useInfiniteQuery.
* Handles auth type detection from meta and skipUnauth.
*
* @example
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20 })
* );
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, userId ? { userId } : skipToken, { limit: 20 })
* );
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20, skipUnauth: true })
* );
* ```
*/
function useConvexInfiniteQueryOptions(funcRef, args, opts) {
const meta = useMeta();
const isSkipped = args === skipToken;
const enabledOpt = typeof opts.enabled === "function" ? void 0 : opts.enabled;
const { authType, shouldSkip } = useAuthSkip(funcRef, {
enabled: isSkipped ? false : enabledOpt,
skipUnauth: opts.skipUnauth
});
const enabled = isSkipped || shouldSkip ? false : enabledOpt;
const baseOptions = convexInfiniteQueryOptions(funcRef, isSkipped ? {} : args, {
...opts,
enabled
}, meta);
return {
...baseOptions,
meta: {
...baseOptions.meta,
authType
}
};
}
/**
* Hook that returns query options for using an action as a one-shot query.
* Actions don't support WebSocket subscriptions - they're one-time calls.
*
* @example
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }));
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, id ? { id } : skipToken));
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }, { skipUnauth: true }));
* ```
*/
function useConvexActionQueryOptions(action, args, options) {
const isSkipped = args === skipToken;
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
const { shouldSkip } = useAuthSkip(action, {
enabled: isSkipped ? false : enabled,
skipUnauth: options?.skipUnauth
});
const stableArgs = useStableQueryArgs("convexAction", action, isSkipped ? EMPTY_ARGS : args);
const baseOptions = useMemo(() => convexAction(action, stableArgs.value), [action, stableArgs]);
return useMemo(() => {
const { skipUnauth: _, ...queryOptions } = options ?? {};
return {
...baseOptions,
...queryOptions,
enabled: isSkipped ? false : !shouldSkip
};
}, [
baseOptions,
isSkipped,
options,
shouldSkip
]);
}
/**
* Hook that returns mutation options for use with useMutation.
* Wraps the Convex mutation with auth guard logic.
*
* @example
* ```tsx
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update, {
* onSuccess: () => toast.success('Updated!'),
* }));
* ```
*/
function useConvexMutationOptions(mutation, options, transformer) {
const guard = useAuthGuard();
const getMeta = useFnMeta();
const name = getFunctionName(mutation);
const [namespace, fnName] = name.split(":");
const authType = getMeta(namespace, fnName)?.auth;
const convexMutation = useMutation(mutation);
const resolvedTransformer = getTransformer(transformer);
return {
...options,
mutationFn: async (args) => {
if (authType === "required" && guard()) throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: name
});
return convexMutation(resolvedTransformer.input.serialize(args));
}
};
}
/**
* Hook that returns action options for use with useMutation.
* Wraps the Convex action with auth guard logic.
*
* @example
* ```tsx
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate, {
* onSuccess: (data) => console.info(data),
* }));
* ```
*/
function useConvexActionOptions(action, options, transformer) {
const guard = useAuthGuard();
const getMeta = useFnMeta();
const name = getFunctionName(action);
const [namespace, fnName] = name.split(":");
const authType = getMeta(namespace, fnName)?.auth;
const convexAction = useAction(action);
const resolvedTransformer = getTransformer(transformer);
return {
...options,
mutationFn: async (args) => {
if (authType === "required" && guard()) throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: name
});
return convexAction(resolvedTransformer.input.serialize(args));
}
};
}
/**
* Hook that returns upload mutation options for use with useMutation.
* Generates a presigned URL, then uploads the file directly to storage.
*
* @example
* ```tsx
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl));
* mutate({ file, ...otherArgs });
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl, {
* onSuccess: (result) => console.info('Uploaded:', result.key),
* }));
* ```
*/
function useUploadMutationOptions(generateUrlMutation, options) {
const generateUrl = useMutation(generateUrlMutation);
return {
...options,
mutationFn: async ({ file, ...args }) => {
const result = await generateUrl(args);
const { url } = result;
const response = await fetch(url, {
body: file,
headers: { "Content-Type": file.type },
method: "PUT"
});
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
return result;
}
};
}
//#endregion
//#region src/react/proxy.ts
/**
* CRPC Recursive Proxy
*
* Creates a tRPC-like proxy that wraps Convex API functions with
* TanStack Query options builders.
*
* @example
* ```ts
* const crpc = createCRPCOptionsProxy(api);
* const opts = crpc.user.get.queryOptions({ id: '123' });
* const { data } = useQuery(opts);
* ```
*/
/** Get query key prefix based on function type */
function getQueryKeyPrefix(path, meta) {
if (getFunctionType(path, meta) === "action") return "convexAction";
return "convexQuery";
}
/**
* Create a recursive proxy that accumulates path segments.
*/
function createRecursiveProxy(api, path, meta, transformer) {
return new Proxy(() => {}, { get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
if (prop === "queryOptions") return (args = {}, opts) => {
const funcRef = getFuncRef(api, path);
if (getFunctionType(path, meta) === "action") return useConvexActionQueryOptions(funcRef, args, opts);
return useConvexQueryOptions(funcRef, args, opts);
};
if (prop === "staticQueryOptions") return (args = {}, opts) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const finalArgs = args === skipToken ? "skip" : args;
if (fnType === "action") return convexAction(funcRef, finalArgs, meta, opts);
return convexQuery(funcRef, finalArgs, meta, opts);
};
if (prop === "queryKey") return (args = {}) => {
const funcName = getFunctionName(getFuncRef(api, path));
return [
getQueryKeyPrefix(path, meta),
funcName,
args
];
};
if (prop === "queryFilter") return (args, filters) => {
const funcName = getFunctionName(getFuncRef(api, path));
const prefix = getQueryKeyPrefix(path, meta);
return {
...filters,
queryKey: [
prefix,
funcName,
args
]
};
};
if (prop === "infiniteQueryOptions") return (args = {}, opts = {}) => {
const funcRef = getFuncRef(api, path);
const options = useConvexInfiniteQueryOptions(funcRef, args, opts);
Object.defineProperty(options, FUNC_REF_SYMBOL, {
value: funcRef,
enumerable: false,
configurable: false
});
return options;
};
if (prop === "infiniteQueryKey") return (args) => {
return [
"convexQuery",
getFunctionName(getFuncRef(api, path)),
args ?? {}
];
};
if (prop === "meta" && path.length >= 2) return getFunctionMeta(path, meta);
if (prop === "mutationKey") return () => {
return ["convexMutation", getFunctionName(getFuncRef(api, path))];
};
if (prop === "mutationOptions") return (opts) => {
const funcRef = getFuncRef(api, path);
if (getFunctionType(path, meta) === "action") return useConvexActionOptions(funcRef, opts, transformer);
return useConvexMutationOptions(funcRef, opts, transformer);
};
return createRecursiveProxy(api, [...path, prop], meta, transformer);
} });
}
/**
* Create a CRPC proxy for a Convex API object.
*
* The proxy provides a tRPC-like interface for accessing Convex functions
* with TanStack Query options builders.
*
* @param api - The Convex API object (from `@convex/api`)
* @param meta - Generated function metadata for runtime type detection
* @returns A typed proxy with queryOptions/mutationOptions methods
*
* @example
* ```tsx
* import { api } from '@convex/api';
*
* // Usually you should use createCRPCContext({ api }) instead.
* // createCRPCOptionsProxy is a low-level helper.
* const crpc = createCRPCOptionsProxy(api, {} as any);
*
* function MyComponent() {
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
* const { mutate } = useMutation(crpc.user.update.mutationOptions());
* }
* ```
*/
function createCRPCOptionsProxy(api, meta, transformer) {
return createRecursiveProxy(api, [], meta, transformer);
}
//#endregion
//#region src/react/vanilla-client.ts
/**
* Create a recursive proxy for vanilla (direct) calls.
*/
function createRecursiveVanillaProxy(api, path, meta, convexClient, transformer) {
return new Proxy(() => {}, { get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
if (prop === "query") return async (args = {}) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const wireArgs = transformer.input.serialize(args);
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
return transformer.output.deserialize(await convexClient.query(funcRef, wireArgs));
};
if (prop === "watchQuery") return (args = {}, opts) => {
const funcRef = getFuncRef(api, path);
return convexClient.watchQuery(funcRef, transformer.input.serialize(args), opts);
};
if (prop === "mutate") return async (args = {}) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const wireArgs = transformer.input.serialize(args);
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
return transformer.output.deserialize(await convexClient.mutation(funcRef, wireArgs));
};
return createRecursiveVanillaProxy(api, [...path, prop], meta, convexClient, transformer);
} });
}
/**
* Create a vanilla CRPC proxy for direct procedural calls.
*
* The proxy provides a tRPC-like interface for imperative Convex function calls.
*
* @param api - The Convex API object (from `@convex/api`)
* @param meta - Generated function metadata for runtime type detection
* @param convexClient - The ConvexReactClient instance
* @returns A typed proxy with query/mutate methods
*
* @example
* ```tsx
* const client = createVanillaCRPCProxy(api, meta, convexClient);
*
* // Direct calls (no React Query)
* const user = await client.user.get.query({ id });
* await client.user.update.mutate({ id, name: 'test' });
* ```
*/
function createVanillaCRPCProxy(api, meta, convexClient, transformer) {
return createRecursiveVanillaProxy(api, [], meta, convexClient, getTransformer(transformer));
}
//#endregion
//#region src/react/context.tsx
const ConvexQueryClientContext = createContext(null);
/** Access ConvexQueryClient (e.g., for logout cleanup) */
const useConvexQueryClient = () => useContext(ConvexQueryClientContext);
const MetaContext = createContext(void 0);
/**
* Hook to access the meta object from context.
* Returns undefined if meta was not provided to createCRPCContext.
*/
function useMeta() {
return useContext(MetaContext);
}
/**
* Hook to get auth type for a function from meta.
*/
function useFnMeta() {
const meta = useMeta();
return (namespace, fnName) => meta?.[namespace]?.[fnName];
}
/**
* Create CRPC context, provider, and hooks for a Convex API.
*
* @param options - Configuration object containing api and optional HTTP settings
* @returns Object with CRPCProvider, useCRPC, and useCRPCClient
*
* @example
* ```tsx
* // lib/crpc.ts
* import { api } from '@convex/api';
* import { createCRPCContext } from 'kitcn/react';
*
* // Works for both regular Convex functions and generated HTTP router types
* export const { useCRPC } = createCRPCContext({
* api,
* convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
* });
*
* // components/user-profile.tsx
* function UserProfile({ id }) {
* const crpc = useCRPC();
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
*
* // HTTP endpoints (if configured)
* const { data: httpData } = useQuery(crpc.http.todos.get.queryOptions({ id }));
* }
* ```
*/
function createCRPCContext(options) {
const { api, ...httpOptions } = options;
const meta = buildMetaIndex(api);
const CRPCProxyContext = createContext(null);
const VanillaClientContext = createContext(null);
const HttpProxyContext = createContext(void 0);
/** Inner provider */
function CRPCProviderInner({ children, convexClient, convexQueryClient }) {
const authStore = useAuthStore();
const token = useAuthValue("token");
const isAuthenticated = useAuthValue("isAuthenticated");
const previousAuthRef = useRef(null);
const fetchAccessToken = useFetchAccessToken();
useEffect(() => {
const previous = previousAuthRef.current;
const tokenReady = token === null || decodeJwtExp(token) !== null;
previousAuthRef.current = {
isAuthenticated,
token
};
if (!previous) return;
if (tokenReady && (previous.token !== token || previous.isAuthenticated !== isAuthenticated)) convexQueryClient.resetAuthQueries();
}, [
convexQueryClient,
isAuthenticated,
token
]);
const httpProxy = useMemo(() => {
if (!httpOptions.convexSiteUrl || !meta._http) return;
return createHttpProxy({
convexSiteUrl: httpOptions.convexSiteUrl,
routes: meta._http,
headers: async () => {
const token = authStore.get("token");
const expiresAt = authStore.get("expiresAt");
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
if (token && expiresAt && timeRemaining >= 6e4) return {
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
Authorization: `Bearer ${token}`
};
if (fetchAccessToken) {
const newToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt });
if (newToken) return {
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
Authorization: `Bearer ${newToken}`
};
}
return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
},
fetch: httpOptions.fetch,
onError: httpOptions.onError,
transformer: options.transformer
});
}, [authStore, fetchAccessToken]);
const proxy = useMemo(() => createCRPCOptionsProxy(api, meta, options.transformer), []);
const vanillaClient = useMemo(() => createVanillaCRPCProxy(api, meta, convexClient, options.transformer), [convexClient]);
return /* @__PURE__ */ jsx(ConvexQueryClientContext.Provider, {
value: convexQueryClient,
children: /* @__PURE__ */ jsx(MetaContext.Provider, {
value: meta,
children: /* @__PURE__ */ jsx(VanillaClientContext.Provider, {
value: vanillaClient,
children: /* @__PURE__ */ jsx(HttpProxyContext.Provider, {
value: httpProxy,
children: /* @__PURE__ */ jsx(CRPCProxyContext.Provider, {
value: proxy,
children
})
})
})
})
});
}
/**
* Provider component that wraps the app with CRPC context.
* For auth, wrap with ConvexAuthProvider (or AuthProvider) above this.
*/
function CRPCProvider({ children, convexClient, convexQueryClient }) {
return /* @__PURE__ */ jsx(CRPCProviderInner, {
convexClient,
convexQueryClient,
children
});
}
/**
* Hook to access the CRPC proxy for building query/mutation options.
*
* @returns The typed CRPC proxy (with http namespace if configured)
* @throws If used outside of CRPCProvider
*
* @example
* ```tsx
* const crpc = useCRPC();
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
*
* // HTTP endpoints (if configured)
* const { data: httpData } = useQuery(crpc.http.todos.get.queryOptions({ id }));
* ```
*/
function useCRPC() {
const ctx = useContext(CRPCProxyContext);
const httpProxy = useContext(HttpProxyContext);
if (!ctx) throw new Error("useCRPC must be used within CRPCProvider");
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
if (prop === "http") return httpProxy;
return Reflect.get(target, prop);
} });
return ctx;
}
/**
* Hook to access the vanilla CRPC client for direct procedural calls.
*
* @returns The typed VanillaCRPCClient for direct .query()/.mutate() calls
* @throws If used outside of CRPCProvider
*
* @example
* ```tsx
* const client = useCRPCClient();
*
* // Direct calls (no React Query)
* const user = await client.user.get.query({ id });
* await client.user.update.mutate({ id, name: 'test' });
*
* // HTTP endpoints (if configured)
* const todos = await client.http.todos.list.queryOptions({});
* ```
*/
function useCRPCClient() {
const ctx = useContext(VanillaClientContext);
const httpProxy = useContext(HttpProxyContext);
if (!ctx) throw new Error("useCRPCClient must be used within CRPCProvider");
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
if (prop === "http") return httpProxy;
return Reflect.get(target, prop);
} });
return ctx;
}
return {
CRPCProvider,
useCRPC,
useCRPCClient
};
}
//#endregion
//#region src/crpc/auth-error.ts
/**
* Auth Mutation Error
*
* Framework-agnostic error class for Better Auth mutations.
*/
/**
* Error thrown when a Better Auth mutation fails.
* Contains the original error details from Better Auth.
*/
var AuthMutationError = class extends Error {
/** Error code from Better Auth (e.g., 'INVALID_PASSWORD', 'EMAIL_ALREADY_REGISTERED') */
code;
/** HTTP status code */
status;
/** HTTP status text */
statusText;
constructor(authError) {
super(authError.message || authError.statusText);
this.name = "AuthMutationError";
this.code = authError.code;
this.status = authError.status;
this.statusText = authError.statusText;
}
};
/**
* Type guard to check if an error is an AuthMutationError.
*/
function isAuthMutationError(error) {
return error instanceof AuthMutationError;
}
//#endregion
//#region src/react/auth-mutations.ts
/** Poll until JWT token exists (auth complete) (max 5s) */
const waitForAuth = async (store, timeout = 5e3) => {
const start = Date.now();
while (Date.now() - start < timeout) {
if (store.get("token")) return true;
await new Promise((r) => setTimeout(r, 50));
}
return false;
};
const authStateTimeoutError = () => new AuthMutationError({
code: "AUTH_STATE_TIMEOUT",
message: "Authentication did not complete. Try again.",
status: 401,
statusText: "UNAUTHORIZED"
});
const ensureAuth = async (store) => {
if (await waitForAuth(store)) return;
throw authStateTimeoutError();
};
const readReturnedToken = (value) => {
if (!value || typeof value !== "object") return null;
const record = value;
if (typeof record.token === "string" && record.token.length > 0) return record.token;
return readReturnedToken(record.data) ?? readReturnedToken(record.session);
};
const seedReturnedToken = (store, value) => {
const token = readReturnedToken(value);
if (!token) return;
store.set("token", token);
store.set("expiresAt", decodeJwtExp(token));
store.set("sessionSyncGraceUntil", Date.now() + AUTH_SESSION_SYNC_GRACE_MS);
if (decodeJwtExp(token) === null) writeAuthSessionFallbackToken(token);
};
const toAuthMutationError = (error) => new AuthMutationError({
code: error?.code,
message: error?.message,
status: error?.status ?? 500,
statusText: error?.statusText ?? "AUTH_ERROR"
});
const callAuthMethod = async (method, ...args) => {
return method(...args);
};
const syncSessionAtom = (authClient, sessionData) => {
const sessionAtom = authClient.$store?.atoms?.session;
if (typeof sessionAtom?.get !== "function" || typeof sessionAtom.set !== "function") return;
const current = sessionAtom.get();
sessionAtom.set({
data: sessionData,
error: null,
isPending: false,
isRefetching: false,
refetch: current?.refetch ?? (async () => {})
});
};
const hydrateReturnedSession = async (authClient, value) => {
const token = readReturnedToken(value);
if (!token || typeof authClient.getSession !== "function") return;
const session = await callAuthMethod(authClient.getSession, { fetchOptions: {
credentials: "omit",
headers: { Authorization: `Bearer ${token}` }
} });
if (session?.data) {
syncSessionAtom(authClient, session.data);
writeAuthSessionFallbackData(session.data);
}
};
const withDisabledSessionSignal = (args) => {
const record = args && typeof args === "object" ? args : {};
return {
...record,
fetchOptions: {
...record.fetchOptions,
disableSignal: true
}
};
};
function createAuthMutations(authClient) {
const useSignOutMutationOptions = ((options) => {
const convexQueryClient = useConvexQueryClient();
const authStoreApi = useAuthStore();
return {
...options,
mutationFn: async (args) => {
const signOut = authClient.signOut;
if (typeof signOut !== "function") throw new Error("Auth client does not expose signOut");
authStoreApi.set("isAuthenticated", false);
convexQueryClient?.unsubscribeAuthQueries();
const res = await callAuthMethod(signOut, args);
if (res?.error) throw toAuthMutationError(res.error);
authStoreApi.set("token", null);
authStoreApi.set("expiresAt", null);
authStoreApi.set("sessionSyncGraceUntil", null);
clearAuthSessionFallback();
await convexQueryClient?.resetAuthQueries();
return res;
}
};
});
const useSignInSocialMutationOptions = ((options)