Skip to content

chore: child to parent singleton bleed - #14

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

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

Conversation

@brunozoric

@brunozoric brunozoric commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Issue

A singleton registered in a parent container is cached in the parent but built from the view of whichever container resolves it first. If a child resolves it first, the child's registrations ({ multiple: true } entries, overridden dependencies) end up in the parent's cached instance and every other container gets them.

Root cause in resolveRegistration: cache in this.instances, dependencies from resolveFrom. Two related gaps: decorators registered in a child never apply to a parent-owned service the child resolves, and there is no scope for "one instance shared by the whole hierarchy" once singletons stop leaking.

Pinned by 22 failing tests. Implementation not started.

Possible solutions

Pick one context container per resolution and use it for cache, dependencies and decorators. Two ways to name the result:

A. Redefine Singleton, add Global (current spec). inSingletonScope() becomes one instance per resolving container; inGlobalScope() is one instance per registration, built and cached in the owner. Isolation by default. Breaks child.resolve(X) === parent.resolve(X); major release.

B. Keep Singleton, add ContainerScoped. inSingletonScope() keeps shared identity but is built from the owner's view, which alone removes the bleed. New inContainerScope() gives the per-container instance. Matches tsyringe, Autofac and Microsoft DI naming. Patch for the fix, minor for the new scope.

Same resolveRegistration change either way; only the meaning of the existing keyword differs. Decision pending.

Docs

  • Design: docs/2026-05-26-per-container-singleton-scoping-design.md (revision 2, open items at the end)
  • Prior art: docs/2026-09-14-scoping-prior-art.md

🤖 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 8 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>
Add docs/2026-09-14-scoping-prior-art.md covering tsyringe, InversifyJS,
NestJS, Microsoft DI, Autofac, Spring, Guice, Symfony, Laravel and shaku,
framed around whose view builds a shared instance and where it is cached.

Record two new open items in the design spec: scope naming (every surveyed
container keeps Singleton meaning the shared instance and names the
per-container behavior separately) and the unguarded captive-dependency
case. Mirror both in the PR description, link the survey from AGENTS.md,
and mark the implementation plan as stale pending the naming decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brunozoric and others added 3 commits September 14, 2026 09:31
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