Repository navigation
fix(core): release the throwaway wrapper of each create() call - #18137
Merged
kamilmysliwiec merged 1 commit intoOct 13, 2026
Merged
kamilmysliwiec merged 1 commit into
kamilmysliwiec merged 1 commit into
Conversation
moduleRef.create() builds a new InstanceWrapper for every call and passes it to the injector as the inquirer. Each dependency then keeps that wrapper in its dependency tree parent set, so it is never collected. A transient dependency also keeps the instance it built for the call in its transient map, under the wrapper's id, so every transient instance stays alive and lifecycle hooks keep running on it. create() now detaches its wrapper once it settles: the wrapper leaves the parent set of each constructor and property dependency, and the transient instances keyed by its id, nested ones included, are removed.
4 of 12 tasks
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.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
moduleRef.create()builds a newInstanceWrapperon every call and passes it to the injector as the inquirer (module-ref.ts). Resolving its constructor and property dependencies runsregisterDependencyTreeParent, which adds that wrapper todependencyTreeParents. That's a module-level WeakMap keyed by the dependency's wrapper, but the dependency lives as long as the app and the value is a strong Set. So every throwaway wrapper, and itsmetatypewith it, is never collected.Transient dependencies make it worse. Each one gets a
transientMapentry keyed by the throwaway wrapper's id (<wrapper id>:<transient id>for a nested one) holding its instance under STATIC_CONTEXT. Every transient instance built for acreate()call stays alive, andgetStaticTransientInstances()keeps returning it, which is the list the lifecycle hooks walk.A plain Node script against the compiled package shows it with a real
NestFactory.createApplicationContext: 200 warm-up + 20 000create()calls, heap measured after gc. On master:All 3 runs matched (B/call moved by a few bytes).
Issue Number: N/A
Steps to reproduce
npx vitest run packages/core/test/injector/module-ref.spec.tson master with the fix reverted (the tests in "when the created class has dependencies")create()settles, its wrapper is gone from the dependencies' parent sets and their transient mapsWhat is the new behavior?
instantiateClassnow callswrapper.detachFromDependencies()in a.finallyon the promise it returns, so it runs on resolve and on reject. I put it there because the caller resumes onresolve(instance)inside the injector callback, beforeloadInstancesettles.detachFromDependencies()removes the wrapper from each constructor and property dependency's parent set, then deletes thetransientMapkeys equal to its id or starting with<id>:, recursing through transient dependencies. Entries other inquirers own are left alone, and the instancecreate()returns is the same as before. Same probe on the fix:Does this PR introduce a breaking change?
Other information
The parent registry came in with 1b8c8b0 (propagate dependency tree cache resets) and went module-private in 3ed595e. Nothing removed entries for a wrapper that only lives for one
create()call, which is the gap this closes.Left as is: a transient dependency's
rootInquirerstill points at the lastcreate()wrapper. That's one per provider, so it stays bounded.module-ref.spec.ts16/16,instance-wrapper.spec.ts+module-ref.spec.ts123/123, prettier and oxlint clean on the touched files.Full suite: 4176 passed / 4 failed on the branch, 4172 / 4 on master. The same 4 fail on both (the logger NO_COLOR case, a discovery-service useFactory case, testing-module-builder compile, and the real-ioredis client-redis retry test).