Peform
Peform7d ago

trpc caching procedure calls for queries in the same batch

Hello, I am using nextjs and trpc server components, using trpc to prefetch queries. All of the requests on this page are protected by a procedure called protectedGuildPermissionsProcedure which essentially checks that the user has the right permissions to access the data that is returned.
await Promise.all([
api.guild.get.prefetch({ guildId }),
api.guild.channels.prefetch({ guildId }),
api.guild.roles.prefetch({ guildId }),
api.applicationPanel.list.prefetch({ guildId }),
api.application.applicationExists.prefetch({ guildId }),
]);
await Promise.all([
api.guild.get.prefetch({ guildId }),
api.guild.channels.prefetch({ guildId }),
api.guild.roles.prefetch({ guildId }),
api.applicationPanel.list.prefetch({ guildId }),
api.application.applicationExists.prefetch({ guildId }),
]);
procedure:
export const guildProcedure = protectedProcedure
.input(z.object({ guildId: z.string() }))
.use(async ({ ctx, next, input }) => {
const hasPermissions = await someExpensiveOperationToCheckPermissions(input.guildId)
if(!hasPermissions) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have permission to access this guild' });
}

return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
});
export const guildProcedure = protectedProcedure
.input(z.object({ guildId: z.string() }))
.use(async ({ ctx, next, input }) => {
const hasPermissions = await someExpensiveOperationToCheckPermissions(input.guildId)
if(!hasPermissions) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have permission to access this guild' });
}

return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
});
Is there some way of telling TRPC to cache the response from the procedure for all batched requests so wont do the same permission check, even though it has already been done? As it currently stands, this expensive procedure will run 5 times, one for each api request that is prefetched. Yes, I could cache this in redis (which I do) but if there is no cache to quickly see if they have permissions each query will have to run the expensive function and it could make the page take up to 30 seconds to load. I'm thinking I could use some sort of redis lock, but it still seems a little inefficient having 5 queries that run at he same time (same trpc batch) all getting the same data from redis?
5 Replies
Nick
Nick6d ago
If you put the check in createContext then it will run once per batch. Alternatively you could use a singleton to de-duplicate and cache the checks but be careful to key it appropriately by user if you end up horizontally scaling then the redis idea would probably be the way to go, but no need for extra infra for a single node
Peform
PeformOP5d ago
Hi thank you for the response. I am already using redis, which does help once the data is cached. and the redis lock could help if there is no data cached, But, it still seems a little redundant to fetch data from redis multiple times for requests in the same batch, so I am curious about the createContext method. When you say put the check inside of my create context, are you talking about the trpc context which has the db connection, users session etc?
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth();
const correlationId = uuid4();

return {
db,
session,
stripe,
correlationId,
...opts,
};
};
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth();
const correlationId = uuid4();

return {
db,
session,
stripe,
correlationId,
...opts,
};
};
only some of my endpoints do this "expensive check", but all endpoints require the context I which I have above ^. Would the solution to this be creating multiple contexts for each type procedure? (maybe its possible to chain contexts?) if i were to make new contexts for each procedure, im unsure how id be able to get the required data from the request in order to pass into my permission checking function
Nick
Nick5d ago
I would put a cache in createContext and then have a middleware call it. That way the middleware instances rely on the same promise for each batch
Peform
PeformOP5d ago
apologies but i dont quite understand this, do you know of any example available which demonstrates how to do this? if not, what exactly do you mean by putting a cache inside of createContext?
Nick
Nick5d ago
Just roll something so you store the first promise and return it to all other callers

Did you find this page helpful?