Skip to content

feat: add Container.resolveImplementation - #17

Merged
adrians5j merged 2 commits into
mainfrom
adrian/resolve-implementation
Sep 10, 2026
Merged

adrians5j merged 2 commits into
mainfrom
adrian/resolve-implementation

Conversation

@adrians5j

@adrians5j adrians5j commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Adds Container.resolveImplementation(implementation) — resolve one implementation without registering it.

Why

When several implementations share an abstraction, there is no way to resolve a specific one. resolve() cannot say which you mean, and resolveAll() constructs all of them.

That is a real shape, not a hypothetical. In webiny-js every HTTP route is registered under one HttpRouteHandler abstraction. The router matches a request to a route and then wants to build only that one — building all of them would drag in each route's whole dependency graph, which for a static-asset request means constructing the GraphQL engine, every contextual schema and the AI provider before discovering it wanted none of them.

resolveWithDependencies half-covers this, but has two problems:

  • The caller supplies the dependencies. Having no other source, webiny-js reads them out of Metadata itself and casts to Dependencies<T> — container plumbing leaking into a consumer.
  • It is the only resolve path that never calls applyDecorators. resolveInternal, resolveRegistration and resolveMultiple all decorate; this one does not. So anything resolved through it is silently undecoratable, which cost real time to diagnose downstream.

What

const route = container.resolveImplementation(MatchedRouteImpl);

Takes the class alone and reads its dependencies from its own metadata. Otherwise identical to resolve(): dependencies come from the container, and decorators registered for the abstraction are applied, because it goes through resolveRegistration like every other path.

Nothing is registered and nothing is cached.

The one gotcha

It does not consult registrations, so a registered singleton is not shared:

container.register(Impl).inSingletonScope();

container.resolve(Thing);              // the singleton, same instance every time
container.resolveImplementation(Impl); // a FRESH instance, not the singleton

That follows from what the method is for — you are asking for this class, not for whatever is registered under its abstraction — and there is no registration to key a singleton on. But it means the two are not interchangeable, so prefer resolve() whenever the abstraction can identify what you want.

It is also why this is a separate method rather than an overload of resolve(). Discriminating an Abstraction from a constructor at runtime is trivial, but folding them together would make container.resolve(X) honour a singleton or not depending on whether X happened to be the abstraction or the class — two call sites that read identically, one sharing an instance and one not. The distinct name is doing useful work: it tells you you have left the polymorphic path.

A test pins this behaviour, so it cannot quietly change without someone deciding to.

register() and resolveImplementation() need the same metadata reads and the same composite / decorator / missing-abstraction checks, so those moved into a shared private describeImplementation. register()'s behaviour is unchanged; the existing suite covers that.

Tests

12 new, covering: resolves without registering, dependencies from metadata, picking one of several implementations sharing an abstraction, building only the requested one, decorators applied (own container and parent container), dependencies resolved from the container it was called on, transient instances, the singleton divergence above, and the three error cases (not an implementation, a decorator, a composite).

Full suite 74 passing, no type errors, pnpm lint clean, builds.

I also ran a throwaway test reproducing the webiny-js router case end to end — many routes on one abstraction, a decorator registered, resolve the matched one — and confirmed it builds only that route and applies the decorator. Not included here, since it belongs in that repo.

Downstream

webiny-js currently works around the missing method by resolving in a throwaway child container whose only registration is the wanted route. That works, and its tests pin it, but it allocates a container per matched route per request and leans on child-before-parent lookup order rather than a stated contract. With this method it becomes one line.

Note the latest published version is 1.0.2 while main is 22 commits ahead, so a release is needed before anything downstream can use this.

adrians5j and others added 2 commits September 9, 2026 22:19
Resolves one implementation without registering it, for when the abstraction
cannot identify which one is wanted: several implementations share an
abstraction, `resolve()` cannot say which, and `resolveAll()` would construct all
of them.

    const route = container.resolveImplementation(MatchedRouteImpl);

`resolveWithDependencies` already covered part of this, but it has two problems.
The caller must supply the dependencies, so it reads them out of `Metadata`
itself — plumbing that belongs in the container. And it is the only resolve path
that never calls `applyDecorators`, so anything resolved through it is silently
undecoratable. `resolveImplementation` takes the class alone, reads the
dependencies off its own metadata, and goes through `resolveRegistration` like
every other path, so decorators apply.

Nothing is registered and nothing is cached; the instance is transient, because
there is no registration to key a singleton on.

`register()` and `resolveImplementation()` need the same metadata reads and the
same composite/decorator/missing-abstraction checks, so those move into a shared
`describeImplementation`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The behaviour follows from the method not consulting registrations, but it is
the one way resolve() and resolveImplementation() are NOT interchangeable, so it
belongs in the doc comment and the changeset rather than being inferred.

Adds a test pinning it, so a later change cannot quietly make the two agree
without someone deciding to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adrians5j
adrians5j marked this pull request as draft September 9, 2026 20:59
@adrians5j
adrians5j marked this pull request as ready for review September 10, 2026 09:18
@adrians5j
adrians5j merged commit 6bb91b2 into main Sep 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant