{{ message }}
Repository navigation
perf(nest): create the standard handler once instead of per request - #2291
Merged
Merged
Conversation
…d codec `ImplementInterceptor` used to build a new `StandardHandler` (and re-init every plugin) on each request. It now builds the handler once, with a `NestHandlerCodec` that resolves the procedure and NestJS route params stored in AsyncLocalStorage for the current request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Move `NestHandlerCodec` into `implement.ts` without exporting it, drop its standalone unit tests, and cover its no-stored-procedure branch through the public API with a routing interceptor that calls `next` outside the request's async context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Replace the codec's AsyncLocalStorage with a private symbol key on the handler context, the same pattern the rethrow and hibernation plugins use. On Node < 24 an AsyncLocalStorage adds promise-hook overhead to every promise in the process once it has run, and its store is silently lost when `next` is deferred through another async context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Contributor
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused Nest integration refactor that reuses one configured StandardHandler and moves procedure resolution into a private codec while preserving the existing request pipeline. The changed context-propagation edge case is covered by an integration test, with no schema, security, billing, or deployment impact. You can add or adjust custom eligibility rules. Learn more. |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/mcp
@orpc/msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
ImplementInterceptorused to build a newStandardHandleron every request, which re-ran every plugin'sinit(sorting plugins and re-wrapping interceptors) each time. The handler is now created once in the interceptor's constructor and reused across requests.Changes
NestHandlerCodec(private, inimplement.ts): extendsOpenAPIHandlerCodecCoreand implementsStandardHandlerCodec. NestJS has already routed the request, soresolveProceduredoesn't match the URL. It reads the procedure and the NestJS route params from a private symbol key on the handler context.codec.withMatch(context, { procedure, params }). This copies the resolved context and never mutates the one from the module config, so concurrent requests can't interfere.initnow runs once at app startup instead of on every request. A plugin whoseinitthrows now fails startup instead of failing each request.Why a context symbol rather than AsyncLocalStorage
AsyncLocalStoragerelies on promise hooks there. Once it has run once, it slows every promise in the process; a micro-benchmark of 3M awaits on Node 22 went from 186ms to 552ms. Node 24+ (AsyncContextFrame) doesn't have this cost.nextthrough another async context.RethrowHandlerPluginandHibernationHandlerPluginalready pass per-request state through a symbol key in the context.Tests
packages/nest/srcstays at 100% coverage.🤖 Generated with Claude Code
https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj