kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
216 lines (212 loc) • 7.61 kB
JavaScript
import { i as getFunctionType, n as getFuncRef, t as buildMetaIndex } from "./meta-utils-D9K4fICl.js";
import { s as getTransformer } from "./transformer-C6pGVHqx.js";
import { fetchAction, fetchMutation, fetchQuery } from "convex/nextjs";
//#region src/server/caller.ts
function createRecursiveProxy(api, path, createOpts) {
return new Proxy(() => {}, {
get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
return createRecursiveProxy(api, [...path, prop], createOpts);
},
apply(_target, _thisArg, argsList) {
const funcRef = getFuncRef(api, path);
const args = createOpts.transformer.input.serialize(argsList[0] ?? {});
const callerOpts = argsList[1];
const fnType = getFunctionType(path, createOpts.meta);
if (fnType === "query") return createOpts.fetchQuery(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result));
if (fnType === "mutation") return createOpts.fetchMutation(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result));
return createOpts.fetchAction(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result));
}
});
}
/**
* Create a server caller for direct data fetching without React Query.
*
* This is detached from React Query cache - data is NOT available in client components.
* Use for server-only data that doesn't need to be shared with the client.
*
* @example
* ```tsx
* // src/lib/convex/rsc.tsx
* export const caller = createServerCaller(api, {
* fetchQuery: fetchAuthQuery,
* fetchMutation: fetchAuthMutation,
* });
*
* // app/page.tsx (RSC)
* const posts = await caller.posts.list();
* return <div>{posts?.length} posts</div>;
* ```
*/
function createServerCaller(api, opts) {
return createRecursiveProxy(api, [], {
...opts,
transformer: getTransformer(opts.transformer)
});
}
//#endregion
//#region src/server/lazy-caller.ts
/**
* Lazy caller that creates context on each procedure invocation.
* Matches tRPC's appRouter.createCaller(createTRPCContext) pattern.
*/
function traverseCaller(caller, path) {
let target = caller;
for (const key of path) target = target[key];
return target;
}
function createRecursiveProxyWithLazyContext(api, path, createContext) {
return new Proxy(() => {}, {
get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
if (path.length === 0 && prop === "isAuth") return async () => {
return (await createContext()).isAuthenticated;
};
if (path.length === 0 && prop === "isUnauth") return async () => {
return !(await createContext()).isAuthenticated;
};
if (path.length === 0 && prop === "getToken") return async () => {
return (await createContext()).token;
};
return createRecursiveProxyWithLazyContext(api, [...path, prop], createContext);
},
async apply(_target, _thisArg, argsList) {
try {
getFuncRef(api, path);
} catch (error) {
const displayPath = path.length > 0 ? path.join(".") : "<root>";
throw new Error(`Invalid caller path: ${displayPath}`, { cause: error });
}
return traverseCaller((await createContext()).caller, path)(argsList[0] ?? {}, argsList[1]);
}
});
}
/**
* Create a lazy caller that creates context on each procedure invocation.
* Matches tRPC's `appRouter.createCaller(createTRPCContext)` pattern.
*
* @example
* ```ts
* // server.ts
* const { createContext, createCaller } = createCallerFactory({...});
*
* // rsc.tsx
* const createRSCContext = cache(async () => {
* const heads = await headers();
* return createContext({ headers: heads });
* });
* export const caller = createCaller(createRSCContext);
*
* // app/page.tsx - single call! Context created lazily
* const posts = await caller.posts.list();
* ```
*/
function createLazyCaller(api, createContext) {
return createRecursiveProxyWithLazyContext(api, [], createContext);
}
//#endregion
//#region src/server/caller-factory.ts
/** biome-ignore-all lint/suspicious/noExplicitAny: lib */
/**
* Framework-agnostic caller factory.
* getToken is passed as a parameter so framework auth helpers stay decoupled.
*/
const CONVEX_SITE_URL_RE = /\.convex\.site(?=\/|$)/;
const getArgsAndOptions = (args, token, url) => [args[0], {
token,
url
}];
const parseConvexSiteUrl = (url) => {
if (!url) throw new Error("CONVEX_SITE_URL is not set. This must be set in the environment.");
if (url.endsWith(".convex.cloud")) throw new Error(`CONVEX_SITE_URL should end in .convex.site, not .convex.cloud. Currently set to ${url}.`);
return url;
};
const getConvexUrl = (siteUrl, convexUrl) => {
if (convexUrl) return convexUrl;
return siteUrl.replace(CONVEX_SITE_URL_RE, ".convex.cloud");
};
/**
* Framework-agnostic caller factory.
*
* @example
* ```ts
* const { createContext, createCaller } = createCallerFactory({
* api,
* convexSiteUrl: env.NEXT_PUBLIC_CONVEX_SITE_URL,
* auth: { getToken },
* });
* ```
*/
const noAuthGetToken = () => Promise.resolve({ token: void 0 });
function createCallerFactory(opts) {
const siteUrl = parseConvexSiteUrl(opts.convexSiteUrl);
const convexUrl = getConvexUrl(siteUrl, opts.convexUrl);
const getToken = opts.auth?.getToken ?? noAuthGetToken;
const isUnauthorized = opts.auth?.isUnauthorized;
const crpcMeta = buildMetaIndex(opts.api);
const callWithTokenAndRetry = async (fn, tokenResult, headers) => {
const shouldRetryWithFreshToken = !!opts.auth && !tokenResult.isFresh;
try {
return await fn(tokenResult.token);
} catch (error) {
if (!shouldRetryWithFreshToken) {
if (isUnauthorized?.(error)) return null;
throw error;
}
const newToken = await getToken(siteUrl, headers, {
...opts,
forceRefresh: true
});
try {
return await fn(newToken.token);
} catch (retryError) {
if (isUnauthorized?.(retryError)) return null;
throw retryError;
}
}
};
const createContext = async (reqOpts) => {
const tokenResult = await getToken(siteUrl, reqOpts.headers, opts);
const fetchAuthQuery = async (query, args, callerOpts) => {
if (callerOpts?.skipUnauth && !tokenResult.token) return null;
return callWithTokenAndRetry((token) => {
const argsAndOptions = getArgsAndOptions([args], token, convexUrl);
return fetchQuery(query, argsAndOptions[0], argsAndOptions[1]);
}, tokenResult, reqOpts.headers);
};
const fetchAuthMutation = async (mutation, args, callerOpts) => {
if (callerOpts?.skipUnauth && !tokenResult.token) return null;
return callWithTokenAndRetry((token) => {
const argsAndOptions = getArgsAndOptions([args], token, convexUrl);
return fetchMutation(mutation, argsAndOptions[0], argsAndOptions[1]);
}, tokenResult, reqOpts.headers);
};
const fetchAuthAction = async (action, args, callerOpts) => {
if (callerOpts?.skipUnauth && !tokenResult.token) return null;
return callWithTokenAndRetry((token) => {
const argsAndOptions = getArgsAndOptions([args], token, convexUrl);
return fetchAction(action, argsAndOptions[0], argsAndOptions[1]);
}, tokenResult, reqOpts.headers);
};
return {
caller: createServerCaller(opts.api, {
fetchAction: fetchAuthAction,
fetchMutation: fetchAuthMutation,
fetchQuery: fetchAuthQuery,
meta: crpcMeta,
transformer: opts.transformer
}),
isAuthenticated: !!tokenResult.token,
token: tokenResult.token
};
};
const createCaller = (ctxFn) => createLazyCaller(opts.api, ctxFn);
return {
createCaller,
createContext
};
}
//#endregion
export { createLazyCaller as n, createServerCaller as r, createCallerFactory as t };