Skip to content

Fix/scan pass teardown - #21

Merged
bebrasmell merged 2 commits into
mainfrom
fix/scan-pass-teardown
Aug 16, 2026
Merged

Fix/scan pass teardown#21
bebrasmell merged 2 commits into
mainfrom
fix/scan-pass-teardown

Conversation

@bebrasmell

Copy link
Copy Markdown
Contributor

No description provided.

The ES2015 claim was never achievable: `globalThis` is read at module
scope, and no build target can lower a runtime global. Rather than
document that gap, close it and state a floor that holds everywhere.

- pin the build to `target: 'es2020'`, and write the sources to the same
  level so the JSR package, which publishes `src/` rather than the
  bundle, asks no more of consumers than npm does
- rewrite the last two `??=` operators (ES2021), which were the only
  reason JSR sat a level above the bundle
- stop reaching for `performance` unguarded. That global only arrived in
  Node.js 16, so a diagnostic measurement had quietly made 16 the real
  floor; it now falls back to `Date.now()` where absent
- README states ES2020 / Node.js 14+ and lists what sets it

No engine loses support. With no explicit target esbuild had been
emitting `esnext`, so every bundle released so far carried ES2022 class
static blocks and this one is strictly more portable than any of them.
Leaving ES2017 also brings async injection back to native emit rather
than a generator.
Discovering a factory's dependencies means running it, and that dry run
executes the constructor body for real. Anything it sets up outside the
container survives the throwaway instance, but `LifecycleRef` resolved to
the shape-shifter during the scan, so `beforeDestroy` silently registered
nothing and even correct code could not undo it.

Measured: a constructor that registers one teardown hook is called twice
and the hook runs once. A service that subscribes to a module-scoped
source therefore leaks exactly one subscription per provider, for good,
whatever the author writes.

The scan now hands out a scratch lifecycle instead — every other token
stays a shape-shifter — and tears it down as the scan ends, because the
instance that registered those hooks is discarded at that same moment and
nothing will ever be able to run them later. End to end against
@illuma/signals: a service subscribing to a module-scoped signal used to
leave 2 listeners while alive and 1 after destroy; it is now 1 and 0.

The trade-off, stated plainly: teardown registered during a scan now runs
at `provide()` time. That is right for a constructor tearing down what it
itself set up, and early for one registering the teardown of a shared
resource it did not create. The asymmetry it removes — setup running
twice, teardown once — is the defect itself.

Also resolves `LifecycleRef.destroyed` to `false` during a scan rather
than to a truthy shape-shifter, which is the truthful answer: the
container being scanned is not destroyed.
@bebrasmell
bebrasmell merged commit 560d1c7 into main Aug 16, 2026
2 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