Skip to content

chore: child to parent singleton bleed - #14

Draft
brunozoric wants to merge 25 commits into
mainfrom
bruno/refactor/child-parent-singleton-bleed
Draft

chore: child to parent singleton bleed#14
brunozoric wants to merge 25 commits into
mainfrom
bruno/refactor/child-parent-singleton-bleed

Conversation

@brunozoric

@brunozoric brunozoric commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Singletons registered in a parent container are polluted by child containers. The fix is designed and pinned by failing tests; the implementation in src/Container.ts is not started.

Design: docs/2026-05-26-per-container-singleton-scoping-design.md (revision 2).

Issues

  1. Singleton bleed. resolveRegistration caches in the owning container but resolves dependencies from the requesting container. The first child to resolve a parent singleton writes its own view (extra { multiple: true } entries, overridden deps) into the parent's cache, and every other container gets it.
  2. Decorators stop at the owner. A decorator registered in a child never applies to a parent-owned service the child resolves. Decorators are collected from the owner's chain, not the requester's.
  3. Falsy cache check. if (existing) instead of if (existing !== undefined). Unreachable today since new always yields an object, but wrong on its face.
  4. No opt-in for shared instances. Once singletons are per-container, a SQL pool or HTTP client needs a way to stay one instance across the hierarchy.

Possible fixes

Pick one context container per resolution and use it for cache, deps and decorators:

Scope Context Result
Transient requester as today, plus requester-chain decorators
Singleton requester one instance per resolving container, cached in that container
Global (new) owner one instance per registration, cached in the owner, child registrations and decorators ignored
  • resolveRegistration: const context = scope === Global ? this : resolveFrom, then context.instances, context.resolveInternal(..., context), context.applyDecorators(..., context).
  • tryResolveFromCurrentContainer and resolveMultiple: call resolveFrom.applyDecorators instead of this.applyDecorators for instance and factory registrations.
  • LifetimeScope.Global and RegistrationBuilder.inGlobalScope().

Alternatives rejected in the spec: smart caching by dependency diff, fixing only { multiple: true }, walk-up cache for globals.

Open decisions

  • Child decorators applying to parent-owned transients, instances and factories is a behavior change. No existing test depends on the old behavior.
  • Global depending on Singleton: the singleton is cached in the owner, so child.resolve(G).s !== child.resolve(S). Spec needs to state this.
  • Per-request child containers will rebuild every parent singleton per request unless migrated to inGlobalScope(). This is the main migration cost.

Breaking change

child.resolve(X) === parent.resolve(X) no longer holds for singletons. Migrate shared resources to .inGlobalScope(). Major release, changeset still to add.

Tests

  • Failing, pinning the new contract (22): registry/registry.test.ts (2), singletonBleed.test.ts, childContainer/singletonCrossResolution.test.ts, singletonDecoratorChain.test.ts.
  • Will break when the fix lands (8): six in singletons.test.ts, one in registry/registry.test.ts, one in container.test.ts ("should resolve instance from parent container if not found in child container"). All assert cross-container identity.
  • Deferred until inGlobalScope() exists: globalScope.test.ts (8 scenarios listed in the spec).
  • containerToken.test.ts documents a separate inheritance limitation; unaffected.

Branch hygiene

🤖 Generated with Claude Code

@brunozoric brunozoric self-assigned this May 26, 2026
@brunozoric
brunozoric force-pushed the bruno/refactor/child-parent-singleton-bleed branch 2 times, most recently from de91da1 to 93b9f3f Compare June 25, 2026 08:17
brunozoric and others added 18 commits September 11, 2026 09:54
Describes the singleton bleed-through bug where child container
registrations pollute a parent's cached singleton, and the fix:
cache singletons per-resolving-container instead of per-owning-container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds singleton variants of all childContainer.test.ts scenarios
to ensure cross-resolution behavior is bulletproof under singleton scoping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add child decorator chain application to the singleton scoping fix
- Add ProductRegistry example with parent/child/grandchild showing
  before/after behavior with 6 products and 2 decorators
- Add registry.test.ts to impacted test list (found by code review)
- Add 6 additional test scenarios: decorator chain, resolution ordering,
  singleton dependency chains
- Document as breaking semantic change requiring semver bump

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6 tasks covering: failing tests, Container.ts fix, existing test
updates, singleton cross-resolution tests, decorator chain tests,
and documentation updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cold review findings: falsy singleton cache bug, reflect-metadata
side-effect in types.ts, circular { multiple: true } stack overflow,
dead prettier scripts, undocumented composite/resolveAll behavior.

6 tasks covering: cache fix, reflect-metadata cleanup, prettier
removal, circular depth guard, builder fluency, AGENTS.md docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds LifetimeScope.Global design (owner-context resolution, walk-up
cache lookup, downward sharing) and fixes 20 issues found across 4
review passes including incorrect assertions, missing breaking tests,
and table inaccuracies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a child container registers an additional plugin and resolves a
parent-registered singleton registry (with { multiple: true } deps),
the child's plugin leaks into the parent's cached singleton instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validates that a parent-registered singleton's cached instance is not
affected by later parent registrations when resolved from a child whose
own singleton is already cached.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AGENTS.md: add registry test directory to test structure, add singleton
  bleed-through to known issues
- Design spec: mark regression tests 1-2 as implemented/failing, renumber
  remaining planned tests
- Implementation plan: mark Task 1 complete with actual commit hashes and
  file paths
- PR description: split tests section into committed (failing) and planned,
  add current status section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Proves that registerInstance in a child container does not pollute
the parent's resolution — instance registrations are per-container
and isolated, unlike the singleton cache bleed bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the ContainerToken inheritance issue to AGENTS.md known issues,
test structure, and the singleton scoping design doc. Instance
registrations are isolated (no bleed), but child containers inherit
the parent's self-registration by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the 5 new ContainerToken tests to committed tests section,
current status, and test plan checklist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The rebase left both sides of the merge in pnpm-workspace.yaml: the new
settings block was added but the old one was never removed, leaving orphaned
indented lines dangling under frozenLockfile and duplicate keys. The result
was invalid YAML - pnpm could not load its configuration at all, so every
command failed on a fresh clone.

Rewrite the file as a single deduplicated block, validating every key against
pnpm 12.3.4 (which ships as a native binary and warns on unrecognized keys).

Tighten the settings while here:

- allowBuilds: esbuild true -> false. esbuild is not installed (it is only an
  optional peer of rslib; this repo builds via rspack), so this was granting
  lifecycle-script rights to a package absent from the tree. core-js and
  node-gyp are likewise pinned false so they cannot gain build rights if they
  ever appear transitively.
- autoInstallPeers: false. pnpm defaults this to true and silently installs
  missing peers as real packages; with strictPeerDependencies an unmet peer is
  now a hard error instead of an unreviewed dependency. This is recorded in the
  lockfile, hence the lockfile change - the package set is unchanged (493
  entries before and after), only peer-resolution bookkeeping differs.
- minimumReleaseAgeIgnoreMissingTime: false. Keeps a registry that cannot date
  its releases from bypassing both the age gate and trustPolicy.
- strictStorePkgContentCheck / trustLockfile / verifyDepsBeforeRun: pin store
  content verification, force lockfile re-verification against the supply-chain
  policies on every install, and refuse to run scripts against a node_modules
  that does not match the lockfile.
- saveExact: pin exact versions for anything added later.

No age-gate or trust exceptions are configured; this package has no first-party
dependencies that would need them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brunozoric
brunozoric force-pushed the bruno/refactor/child-parent-singleton-bleed branch from 8346446 to b95ae9b Compare September 11, 2026 08:24
brunozoric and others added 7 commits September 11, 2026 10:35
Revision 2 of the design. Records five review findings against the
first revision and the decision for each: the decorator chain walk
double-applied ancestor decorators (use resolveFrom.applyDecorators
once), transient decorator behavior contradicted the table (decorators
follow the requesting container on every non-global path), global scope
was order-dependent (cache in the owning container), the falsy cache
check (defensive only), and stale line/commit references. Adds a
branch-hygiene section for the engines/CI mismatch and stale
cache-key-collision material.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds 28 tests across three files that encode the revision 2 spec:
singletonBleed.test.ts, childContainer/singletonCrossResolution.test.ts
and singletonDecoratorChain.test.ts. 20 fail today because the fix is
not implemented; the two existing registry regression tests keep
failing as before. No passing test changed and no source changed.

Global scope tests are deferred until the inGlobalScope() API exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copies .claude/skills/handoff from webiny-js and rewrites it for this
repo: pnpm lint/build/test as the check gate, the docs table points at
AGENTS.md, docs/*-design.md, docs/superpowers/plans, pr/<branch>.md,
bugs/ and .changeset, handoff files go to docs/handoff/, and tests may
fail only when they pin a designed-but-unimplemented behavior.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ems in spec

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <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