-
TLDR
This is not taken in account with experimental app dir support, as the Therefore, I am suffering a bug when a query that just uses IntroductionExperimental tRPC's NextJS AppRouter layout support involves caching of query inputs, and is implemented in the next way: trpc/packages/next/src/app-dir/links/nextCache.ts Lines 34 to 43 in b72c7aa ProblemWhile it works for most of the cases, problems arise when your query logic depends on trpc's Here's an example implementation of a protected procedure taken from the tRPC's documentation: import { initTRPC, TRPCError } from '@trpc/server';
export const t = initTRPC.context<Context>().create();
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(async function isAuthed(
opts,
) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
// ^?
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
// ^?
},
});
});
t.router({
// this is accessible for everyone
hello: t.procedure
.input(z.string().nullish())
.query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`),
admin: t.router({
// this is accessible only to admins
secret: protectedProcedure.query((opts) => {
return {
secret: 'sauce',
};
}),
}),
}); As you can see, the In this particular case, which is taken from the docs, the consequence is that the first user querying the WorkaroundI was forced to always pass Proposed SolutionAllow a user to explicitly state the State the cache key in the queryThe revamped docs example could look like this: import { initTRPC, TRPCError } from '@trpc/server';
export const t = initTRPC.context<Context>().create();
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(async function isAuthed(
opts,
) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
// ^?
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
// ^?
},
});
});
t.router({
// this is accessible for everyone
hello: t.procedure
.input(z.string().nullish())
.query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`)
// explicitly state data to use as `cacheKey`
.cacheKey((opts) => [opts.input, opts.ctx.user?.name]),
admin: t.router({
// this is accessible only to admins
secret: protectedProcedure.query((opts) => {
return {
secret: 'sauce',
};
}),
}),
}); State the cache key in the middlewareThe revamped docs example could look like this: import { initTRPC, TRPCError } from '@trpc/server';
export const t = initTRPC.context<Context>().create();
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(async function isAuthed(
opts,
) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
// ^?
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
// ^?
},
// state which values from the context you want to cache
cacheKey: (ctx) => ([ctx.user])
});
});
t.router({
// this is accessible for everyone
hello: t.procedure
.input(z.string().nullish())
.query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`)
admin: t.router({
// this is accessible only to admins
secret: protectedProcedure.query((opts) => {
return {
secret: 'sauce',
};
}),
}),
}); If you're read till the end, you're the hero, thank you. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
Okay, here's another FAR simpler idea. Let the user define the context values to cache inside of Going to try this out on my specific setup, but similar has to be done in all the places where type NextCacheLinkOptions<TRouter extends AnyRouter> = {
router: TRouter;
createContext: () => Promise<inferRouterContext<TRouter>>;
+ /** define values from the context that must be passed to `generateCacheTag` */
+ cacheContext?: (ctx: inferRouterContext<TRouter>) => any[];
/** how many seconds the cache should hold before revalidating */
revalidate?: number | false;
} & TransformerOptions<inferRootTypes<TRouter>>;
// ts-prune-ignore-next
export function experimental_nextCacheLink<TRouter extends AnyRouter>(
opts: NextCacheLinkOptions<TRouter>,
): TRPCLink<TRouter> {
const transformer = getTransformer(opts.transformer);
return () =>
({ op }) =>
observable((observer) => {
const { path, input, type, context } = op;
- const cacheTag = generateCacheTag(path, input);
- // Let per-request revalidate override global revalidate
- const requestRevalidate =
- typeof context['revalidate'] === 'number' ||
- context['revalidate'] === false
- ? context['revalidate']
- : undefined;
- const revalidate = requestRevalidate ?? opts.revalidate ?? false;
const promise = opts
.createContext()
.then(async (ctx) => {
+ const cacheTag = generateCacheTag(path, input, opts.cacheContext?.(ctx));
+ // Let per-request revalidate override global revalidate
+ const requestRevalidate =
+ typeof context['revalidate'] === 'number' ||
+ context['revalidate'] === false
+ ? context['revalidate']
+ : undefined;
+ const revalidate = requestRevalidate ?? opts.revalidate ?? false;
const callProc = async (_cachebuster: string) => {
// // _cachebuster is not used by us but to make sure
// // that calls with different tags are properly separated
// // @link https://github.com/trpc/trpc/issues/4622
const procedureResult = await callProcedure({
procedures: opts.router._def.procedures,
path,
getRawInput: async () => input,
ctx: ctx,
type,
}); And the -export function generateCacheTag(procedurePath: string, input: any, context?: any) {
- return input
- ? `${procedurePath}?input=${JSON.stringify(input)}&context=${JSON.stringify(context)}`
- : procedurePath;
-}
+export function generateCacheTag(procedurePath: string, input: any, context?: any) {
+ return `${procedurePath}?input=${JSON.stringify(input ?? {})}&context=${JSON.stringify(context ?? {})}`
+} |
Beta Was this translation helpful? Give feedback.
-
Moved to #5455 |
Beta Was this translation helpful? Give feedback.
Moved to #5455