Skip to content

feat!: add inContainerScope and build singletons from the registering container - #19

Merged
adrians5j merged 4 commits into
mainfrom
adrian/container-scope
Oct 9, 2026
Merged

adrians5j merged 4 commits into
mainfrom
adrian/container-scope

Conversation

@adrians5j

@adrians5j adrians5j commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Implements option B from #14: keep inSingletonScope(), fix how singletons are built, and add a per-container lifetime as a new method.

Changes

  1. New inContainerScope(). One instance per container that resolves it, built from that container's view and cached in it. Register it once in a parent, and every child container gets its own instance, thrown away with the child. In Webiny that's one instance per request, because every request runs in its own child container.

    root.register(RequestCacheImpl).inContainerScope();
    
    request1.resolve(RequestCache) === request1.resolve(RequestCache); // true
    request1.resolve(RequestCache) === request2.resolve(RequestCache); // false
  2. Singletons are built from the registering container's view (the bleed fix). Before, a parent-registered singleton was cached in the parent but took its dependencies and decorators from whichever child resolved it first, and everyone got that child's version. Now its dependencies and decorators come from the container that holds the registration, so it's the same instance no matter who resolves it first.

  3. Decorators follow the resolving container, for transient, container-scoped, registerInstance and factory registrations. A child's decorators now apply to registrations that live in a parent. Singletons are the exception: they keep the registering container's decorators, because a child's decorator would otherwise be baked into the instance everyone shares. This is F2 in the chore: child to parent singleton bleed #14 design doc.

  4. A child decorator on a parent's singleton throws (Bruno's suggestion). That decorator can never apply, so resolving the singleton from that child now fails with Cannot apply decorator X to Y, instead of skipping the decorator silently. The check runs at resolve time, so a child that registers its own implementation of the abstraction can still decorate it.

In Container.ts, applyDecorators now collects decorators from the resolving container. resolveRegistration picks the container to build from (the registering container for singletons, the resolving one otherwise) and the cache to use (none, the registering container's, or the resolving container's).

Breaking changes (major: 2.0.0)

  1. Singletons (change 2). A parent-registered singleton that depends on something registered only in a child container will no longer see it:

    • an optional dependency resolves to undefined;
    • a required one throws "No registration found".

    To migrate, register the dependency where the singleton is registered, or switch the singleton to inContainerScope().

  2. Decorators (change 3). A child's decorators now apply to parent-registered transient, instance and factory registrations, where they didn't before.

  3. Child decorators on singletons (change 4). Resolving a parent's singleton from a child that decorated it now throws, where it used to skip the decorator silently.

Why major and not minor: Webiny's packages depend on @webiny/di through ^1.1.0 ranges. A minor release would reach existing projects on any fresh install or lockfile refresh, without the Webiny-side fix (Cognito, below). A major release means nothing upgrades by accident.

Release order:

  1. Release @webiny/di 2.0.0 from this PR.
  2. In webiny-js: the Cognito fix plus bumping every @webiny/di range to ^2.0.0, in one PR.
  3. In webiny-js: fold the per-request storage features back into one feature per package, using inContainerScope().

Tests

The tests are adapted from Bruno's #14:

  • containerScope.test.ts takes his per-container scenarios (siblings, grandchildren, resolution order, dependency chains) and runs them against inContainerScope(). New cases cover the shared singleton: no leak when a child resolves first, one instance for siblings and parent, a child's dependency override not reaching it, and a singleton registered in a child.
  • decoratorChain.test.ts takes his decorator-chain scenarios under container scope, plus his transient, instance, factory and resolveAll cases. New cases show that a shared singleton ignores child decorators but keeps ancestor ones, and resolves decorator dependencies from the registering container.

Results:

  • 24 of the 26 new tests fail on main. The 2 that pass cover decorators registered above the owner, which already worked.
  • The decorator tests also cover the new error: it fires when the singleton is already cached, the parent still resolves fine, and a child with its own implementation can decorate it.
  • All 74 existing tests still pass unchanged, for 102 in total. pnpm lint and pnpm build are clean.
  • CodeRabbit (CLI) found nothing.

Checked against Webiny

I put this build into a webiny-js checkout on current next, which has webiny/webiny-js#5933 merged:

  • the full build type-checks;
  • the DDB API handler tests pass (5/5, including the real root/request container setup);
  • api-core passes (267);
  • the CMS DDB storage suite passes (43);
  • the full api-headless-cms suite passes (1,041 tests);
  • api-aco passes (124 tests).

I reran all of these after adding change 4, and nothing in Webiny trips the new error.

Known follow-up in Webiny: CognitoIdentityProvider is a root singleton that gets its config from an extension, registered in the request container, as an optional dependency. With change 2, it would silently get undefined. No test covers it, so this needs a Webiny fix before or together with the upgrade.

Not in this PR

From the design discussion, still open:

  • a captive-dependency check (a singleton depending on something container-scoped).

Supersedes the implementation part of #14. Its docs, specs and prior-art write-up stay there, and its tests were adapted here, with Bruno as co-author on the commit.

Workspace: wby-next11 · STORAGE PER REQ

🤖 Generated with Claude Code

adrians5j and others added 3 commits October 8, 2026 23:28
…container

Adds a third lifetime, inContainerScope(): one instance per container that
resolves it, built from that container's view and cached in it. Registered
once in a parent, it gives every child container (one per request, for
example) its own instance.

Singletons are now built from the view of the container that holds the
registration. Before, a parent-registered singleton took its dependencies and
decorators from whichever child resolved it first, and that version was
cached in the parent for everyone (the "bleed" pinned by #14).

Decorators are now collected from the resolving container for transient,
container-scoped, instance and factory registrations, so a child's
decorators apply to registrations that live in a parent. Singletons keep
the registering container's decorators.

Tests are adapted from #14: its per-container expectations now use
inContainerScope(), and new cases cover the shared singleton.

Co-authored-by: Bruno Zorić <bruno@webiny.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Singletons are now built from the registering container's view, and child
decorators reach parent registrations. Webiny packages depend on @webiny/di
through ^1 ranges, so a minor release would reach existing projects without
the fixes they need on their side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adrians5j adrians5j changed the title feat: add inContainerScope and build singletons from the registering container feat!: add inContainerScope and build singletons from the registering container Oct 9, 2026
A singleton only takes decorators from the container that registered it and
its parents, so a decorator registered in a child container for it could
never apply. Resolving the singleton from that child now throws instead of
skipping the decorator silently. A child that registers its own
implementation can still decorate it.

Suggested by Bruno Zorić.

Co-authored-by: Bruno Zorić <bruno@webiny.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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