feat: defered execution of requests. Respond to client early, but continue handling the request
Describe the feature you'd like to request
I'd like to send responses to clients, but then continue handling their request in Next.js. For example, user finalizes his order -> waits for confirm -> gets 200, but my code continues and sends emails + analytics without slowing down the user. Yes, I can use a queue service with worker, but it's easier this way to do half sync, and half async work.
Describe the solution you'd like to see
I've implemented it for myself, tested and would like to document it.
The main point is the after() function from 'next/server', when using the next.js tRPC adapter fetchRequestHandler. Code inside the after() is executed by Next.js after the request has been sent.
The below code is non-exaustive. I want you to get the idea, NOT exact implementation, because my approach might not be the best for maintenance. However, I've tested and it works.
Usage within a procedure:
const testRouter = createTRPCRouter({
deferedHello: publicProcedure.query(async ({ctx}) => {
ctx.defer?.(sleep(2000).then(() => console.log('deferred: hello chad'))); //runs after client response send
return 'Hello chad'
}),
hello: publicProcedure.query(() => 'Hello chad')
})route.ts:
//Technical debt: tRPC only has 2 HTTP verbs - GET & POST. We don't expect our API to be public for now.
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/root';
import { after } from 'next/server';
const handler = (req: Request) => {
/** Hover over `defer` below for docs */
const deferedTasks: Promise<unknown>[] = []
const res = fetchRequestHandler({
endpoint: '/v1/api/trpc',
req,
router: appRouter,
createContext: async () => ({ defer: (p) => deferedTasks.push(p) })
})
after(res.then(() => Promise.allSettled(deferedTasks))) //Next.js will return to client, but continue handling the defered tasks.
return res
};
export { handler as GET, handler as POST };tRPC context:
type TRPCContext = {
/**
* @description What if I want to return early to client, but continue handling the request?
* I implemented 'defer()' as an abstraction for runtime, instead of calling 'after()' from next.js inside every procedure. If we change the server, we'd have to change every 'after()' call, while here we change the equivalent in 1 place.
* And No, I can't use a middleware for this. Middleware must return before sending the request.
* @warning ctx.defer() don't work in server-side calls
* @example ctx.defer?.(await metrics.send()) // only in client -> server RPCs.
* @example ctx.defer?.(await analytics.send()) // only in client -> server RPCs.
* @example ctx.defer?.(await email.send()) // only in client -> server RPCs.
*/
defer?: (task: Promise<unknown>) => unknown //return response early to client and continue handling the request.
};
//
const t = initTRPC.context<TRPCContext>().create()Describe alternate solutions
Use a proper queue service like RabbitMQ.
Additional information
No response
Contributing
- ♂️ Yes, I'd be down to file a PR implementing this feature!
Source: trpc/trpc