@codefast/di 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +265 -0
- package/README.md +69 -6
- package/dist/ambient/active-container.d.ts +7 -2
- package/dist/ambient/active-container.js +6 -1
- package/dist/container/binding-builders.d.ts +19 -1
- package/dist/container/binding-builders.js +75 -16
- package/dist/container/container.js +224 -47
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding.d.ts +57 -5
- package/dist/core/binding.js +48 -1
- package/dist/core/module.d.ts +7 -4
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +11 -7
- package/dist/core/registry.js +122 -38
- package/dist/core/state-epoch.d.ts +18 -1
- package/dist/core/state-epoch.js +17 -0
- package/dist/core/tag.js +1 -1
- package/dist/decorators/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.js +2 -1
- package/dist/decorators/injectable.js +3 -1
- package/dist/decorators/lifecycle-decorators.js +8 -2
- package/dist/errors/errors.d.ts +77 -3
- package/dist/errors/errors.js +93 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.js +6 -4
- package/dist/introspection/dependency-graph.d.ts +7 -2
- package/dist/introspection/dependency-graph.js +46 -23
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.js +6 -9
- package/dist/lifecycle/lifecycle-manager.js +14 -2
- package/dist/lifecycle/scope-manager.js +28 -10
- package/dist/metadata/verifying-metadata-reader.d.ts +4 -3
- package/dist/metadata/verifying-metadata-reader.js +28 -6
- package/dist/resolution/async-fan-out.d.ts +12 -0
- package/dist/resolution/async-fan-out.js +26 -0
- package/dist/resolution/cache/activation-need.d.ts +0 -1
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +0 -7
- package/dist/resolution/cache/binding-lookup-cache.js +30 -17
- package/dist/resolution/cache/class-introspector.d.ts +11 -0
- package/dist/resolution/cache/class-introspector.js +18 -0
- package/dist/resolution/context.d.ts +15 -23
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +89 -38
- package/dist/resolution/plan/instantiation-plan.js +61 -21
- package/dist/resolution/plan/plan-codegen.d.ts +7 -4
- package/dist/resolution/plan/plan-codegen.js +60 -32
- package/dist/resolution/resolver.d.ts +4 -5
- package/dist/resolution/resolver.js +288 -258
- package/package.json +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,270 @@
|
|
|
1
1
|
# @codefast/di
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A default-slot alias is a transparent pointer: a request whose criteria no slot of the alias's own token matches is
|
|
8
|
+
forwarded, criteria and all, to its target — `resolve(AbstractLogger, { name: "file" })` reaches `Logger`'s `"file"`
|
|
9
|
+
binding. An exact slot on the alias's token still wins, the nearest container still answers first, and `has` agrees;
|
|
10
|
+
`resolveAll` keeps filtering by the alias token's own slots.
|
|
11
|
+
|
|
12
|
+
- [#896](https://github.com/codefastlabs/codefast/pull/896) Retire the async cascade lane: every `resolveAsync` now runs on the branch lane, so a factory's context answers from its
|
|
13
|
+
own ancestors before and after an `await`. A `whenParentIs` binding requested after an `await` is now selected as the
|
|
14
|
+
sync lane selects it, `ctx.graph.resolutionPath` names the level's own ancestors, and a cycle formed entirely from
|
|
15
|
+
post-`await` edges is named from the true root. A transient class or factory root with a statically visible graph is
|
|
16
|
+
answered by the compiled async plan. `AsyncCascadeContext` and `resolveAsyncFromCascade` are removed from
|
|
17
|
+
`resolution/context`; `ResolutionDiagnostics.builtSubsystems` reports `resolver.asyncRootLevel` once the async lane has
|
|
18
|
+
been entered.
|
|
19
|
+
|
|
20
|
+
- [#937](https://github.com/codefastlabs/codefast/pull/937) Add declared modules: `Module.fromBindings(name, [binding(key, definition), …])` (also `SyncModule.fromBindings`) builds
|
|
21
|
+
a `SyncModule` from a list, each `binding()` stating one binding with the fluent chain's own vocabulary — `to`,
|
|
22
|
+
`toSelf: true`, `toConstantValue`, `toDynamic`, `toDynamicAsync`, `toResolved` / `toResolvedAsync` with `deps`,
|
|
23
|
+
`toAlias`, then `whenNamed`, `whenTagged` (one criterion or several), `when`, `many: true`, `scope` and the lifecycle
|
|
24
|
+
hooks. A definition is typed exactly as the chain is — the key alone decides the value type and the slot names, and the
|
|
25
|
+
compiler rejects a second strategy, a scope on a constant or an alias, a hook on an alias, `onDeactivation` off a
|
|
26
|
+
singleton, and a slot on a collection member. `binding()` checks the same rules at runtime for plain JavaScript and
|
|
27
|
+
throws the new `InvalidBindingDeclarationError` (`INVALID_BINDING_DECLARATION`, with `tokenName` and `reason`), or
|
|
28
|
+
`ManyBindingSlotError` / `SelfBindingRequiresClassError` as the chain does.
|
|
29
|
+
|
|
30
|
+
A declared module loads, unloads, ref-counts and imports exactly as the equivalent `Module.create` setup would, and
|
|
31
|
+
registers each binding already in its final shape, so a binding with a slot, a scope or a hook skips the chain steps a
|
|
32
|
+
fluent setup pays on every load. Nothing a resolve reads changes. `SyncModule`'s `MODULE_SETUP` member now holds either
|
|
33
|
+
the setup callback or the declarations.
|
|
34
|
+
|
|
35
|
+
- [#925](https://github.com/codefastlabs/codefast/pull/925) A constant that a plain last-wins `bind()` displaces now still runs its `onDeactivation` hook at `dispose()`. A
|
|
36
|
+
displaced constant left the registry — so the dispose sweep, which walks the registry, never reached it — and, carrying
|
|
37
|
+
no cached instance, it was absent from the singleton cache too, falling between the two. The container now records a
|
|
38
|
+
displaced constant that still owes a deactivation and drains it at dispose; a refinement that restores the binding (a
|
|
39
|
+
`when*()` that moves the winner off the slot) takes it back out of that set, so nothing deactivates twice. The
|
|
40
|
+
park-and-restore behaviour of `bind()` is otherwise unchanged.
|
|
41
|
+
|
|
42
|
+
- [#931](https://github.com/codefastlabs/codefast/pull/931) Disposing a container now reaches its descendants. A child whose parent (or any ancestor) has been disposed is itself
|
|
43
|
+
disposed: `isDisposed` reads `true`, and every `resolve`, `has` and mutation is refused with `DisposedContainerError`,
|
|
44
|
+
including the child's own bindings — previously a child kept building transients and constants from a torn-down chain,
|
|
45
|
+
and `child.has` and `child.resolve` disagreed. The guard keeps the resolve path cheap: a root still reads only its own
|
|
46
|
+
`#disposed` field, and a child adds one call-free compare of a dedicated dispose-epoch cell against the epoch at which
|
|
47
|
+
its chain was last confirmed live — the ancestor walk runs only after some container in the process is disposed. The
|
|
48
|
+
dispose epoch is separate from the state epoch, so a per-request child's dispose does not invalidate the chain-version
|
|
49
|
+
memo that keeps deep resolves cheap, and `createChild()` is untouched. `resolveAsync` refuses a disposed chain with a
|
|
50
|
+
synchronous throw at the entry, matching how it already guarded a self-disposed container, and `dispose()` stays ungated
|
|
51
|
+
so an `await using` child of a disposed ancestor still tears down cleanly.
|
|
52
|
+
|
|
53
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) `generateDependencyGraph({ includeParent: true })` follows resolution's own walk up the chain: every ancestor's bindings
|
|
54
|
+
join the graph, a single dependency connects to the nearest container holding a binding its slot matches — a child
|
|
55
|
+
binding the request cannot select no longer hides the parent's — and an `injectAll` fans out across the whole chain.
|
|
56
|
+
`buildDependencyGraph` takes the ancestor registries, nearest first, in place of the parent registry.
|
|
57
|
+
|
|
58
|
+
- [#921](https://github.com/codefastlabs/codefast/pull/921) A subclass that inherits declared constructor dependencies but declares none of its own — the natural
|
|
59
|
+
`class Derived extends Base {}` with an implicit constructor — is now rejected with `MissingMetadataError` instead of
|
|
60
|
+
being built with `undefined` arguments. Constructor metadata is opt-in per class, so an implicit constructor previously
|
|
61
|
+
slipped through the `target.length === 0` check and produced a silently-broken instance. The error names the base and
|
|
62
|
+
how many dependencies it declares. A subclass whose own `@injectable([])` declares zero deps is unaffected — it is built
|
|
63
|
+
with zero arguments, as declared.
|
|
64
|
+
|
|
65
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) Decorators report a runtime without `Symbol.metadata` as `MissingDecoratorMetadataError` at the declaration, naming the
|
|
66
|
+
decorator and the fix (`(Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata")` in a module
|
|
67
|
+
imported first), instead of failing with a bare `TypeError` on the first metadata write. TypeScript compiles
|
|
68
|
+
`context.metadata` to `undefined` on such a runtime; the library does not install the symbol itself, since the package
|
|
69
|
+
declares no side effects.
|
|
70
|
+
|
|
71
|
+
- [#926](https://github.com/codefastlabs/codefast/pull/926) Module `unload` is now symmetric with `import`, and `unbindAll` resets module bookkeeping. An `import` inside a module's
|
|
72
|
+
setup incremented the imported module's ref-count but nothing ever decremented it, so `unload(A)` left a module `A`
|
|
73
|
+
imported behind bound forever — the container now records each module's imports and, when a module's own ref-count
|
|
74
|
+
reaches 0, releases the imports it took, unbinding an imported module only when its ref-count reaches 0. And
|
|
75
|
+
`unbindAll()` / `unbindAllAsync()` now clear the module ref-count, binding-id and import tables after deactivation, so a
|
|
76
|
+
module loaded before `unbindAll` can be `load()`ed again instead of being silently skipped as already-loaded.
|
|
77
|
+
|
|
78
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A sync `rebind()` whose old binding owes an async deactivation now fails exactly like a sync `unbind()` whatever shape
|
|
79
|
+
the token has: `rebind()` itself throws `AsyncDeactivationError`, the old bindings are removed, and nothing is
|
|
80
|
+
committed. A lone default binding used to let `rebind()` pass and then throw from `.to*()` after the replacement was
|
|
81
|
+
already registered.
|
|
82
|
+
|
|
83
|
+
- [#929](https://github.com/codefastlabs/codefast/pull/929) `runWithContainer` is now typed as synchronous-only. The ambient container lives in a module-level variable restored in
|
|
84
|
+
a `finally`, so it lasts only the callback's synchronous run — it does not survive an `await`. A callback returning a
|
|
85
|
+
`Promise` was silently accepted and lost the context; its return type is now
|
|
86
|
+
`Result extends Promise<unknown> ? never : Result`, so an async callback resolves to `never`.
|
|
87
|
+
`MissingContainerContextError` now says the context does not survive an `await`, and the TSDoc, README and SPEC state
|
|
88
|
+
the sync-only constraint. The engine internals are unchanged — they only ever ran synchronous callbacks.
|
|
89
|
+
|
|
90
|
+
- [#894](https://github.com/codefastlabs/codefast/pull/894) Four size thresholds are gone, each replaced by the algorithm it approximated: alias chains fold exactly to any length
|
|
91
|
+
(`ALIAS_HOP_LIMIT` is removed from `resolution/cache/binding-lookup-cache`); a generated plan is one statement per node
|
|
92
|
+
and inlines to any depth; every synchronous lane checks a cycle by the binding's in-flight flag at any depth
|
|
93
|
+
(`RESOLUTION_SET_THRESHOLD` and `enterResolutionPath` are removed from `resolution/path/resolution-path`); a
|
|
94
|
+
multi-criterion request is selected by one scan at any binding count. `PLAN_CODEGEN_THRESHOLD` is 1024, the measured
|
|
95
|
+
break-even of generating a plan against running its closure.
|
|
96
|
+
|
|
97
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) `UnreachableLifecycleHookError` carries a `reason` — `"unbound"` or `"no-deactivatable-binding"` — and an
|
|
98
|
+
`onDeactivation` hook on a token whose every binding is transient or scoped is reported as that, rather than as a token
|
|
99
|
+
nothing is bound to.
|
|
100
|
+
|
|
101
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) `validate()` checks an `optional()` dependency whenever it is bound, so a singleton capturing a bound transient or
|
|
102
|
+
scoped dependency through `optional()` is a `ScopeViolationError`; an optional dependency that is not bound still
|
|
103
|
+
imposes nothing.
|
|
104
|
+
|
|
105
|
+
- [#976](https://github.com/codefastlabs/codefast/pull/976) `engines.node` is now `>=24.0.0`, up from `>=22.12.0`, and Node 22 is no longer supported. Node 24.0.0 is the first
|
|
106
|
+
release with explicit resource management built in (`using`, `await using`, `DisposableStack`, `AsyncDisposableStack`,
|
|
107
|
+
`SuppressedError`) and all of ES2025, so the packages use both as the platform ships them instead of shimming them for
|
|
108
|
+
an older line, and the CI matrix runs the unit suite on 24.0.0 itself. Move to Node 24, or stay on the current minor
|
|
109
|
+
while a deployment still runs Node 22.
|
|
110
|
+
|
|
111
|
+
### Patch Changes
|
|
112
|
+
|
|
113
|
+
- [#977](https://github.com/codefastlabs/codefast/pull/977) The README states the browser floor: Chrome and Edge 136, Firefox 136, and Safari 18.4 or later, the first releases that
|
|
114
|
+
ship every ES2025 builtin. `@codefast/di`'s README also says what a browser program without explicit resource management
|
|
115
|
+
does: it keeps `skipLibCheck` on and calls `dispose()` instead of `await using`.
|
|
116
|
+
|
|
117
|
+
- [#922](https://github.com/codefastlabs/codefast/pull/922) A lifecycle hook that returns a rejecting `Promise` when reached from a synchronous lane no longer crashes the process.
|
|
118
|
+
`runActivationSync` and `runDeactivationSync` call each hook before they can tell it is async, so on discovering a
|
|
119
|
+
returned `Promise` they now adopt its rejection (a no-op `.catch`) before throwing `AsyncActivationError` /
|
|
120
|
+
`AsyncDeactivationError` — a rejecting `@postConstruct`, `onActivation`, `onDeactivation` or `@preDestroy` can no longer
|
|
121
|
+
become an unhandled rejection that ends the process. The hook has still run; retry on `resolveAsync` / `unbindAsync` to
|
|
122
|
+
await it properly.
|
|
123
|
+
|
|
124
|
+
- [#896](https://github.com/codefastlabs/codefast/pull/896) An async level with several dependencies, and a `resolveAllAsync` collection, report the first failing dependency in
|
|
125
|
+
declaration order — the order the sync lanes report — instead of whichever rejection happened to settle first. Every
|
|
126
|
+
dependency still starts before anything is reported. A node with one dependency awaits it directly, with no fan-out to
|
|
127
|
+
settle.
|
|
128
|
+
|
|
129
|
+
- [#924](https://github.com/codefastlabs/codefast/pull/924) `child.has(token)` no longer throws when a parent container is disposed but the child is not. `has` recursed through the
|
|
130
|
+
parent's **public** `has`, which asserts the container is live, so a live child that could still `resolve` a
|
|
131
|
+
parent-owned token threw `DisposedContainerError` from `has` alone — breaking the `if (c.has(token)) c.resolve(token)`
|
|
132
|
+
guard on a healthy container. It now recurses through an internal `#hasInChain` that reads the parent's registry
|
|
133
|
+
directly, keeping the disposed-guard on the public entry point of the container being called.
|
|
134
|
+
|
|
135
|
+
- [#894](https://github.com/codefastlabs/codefast/pull/894) The cold path allocates only what it uses: a container builds its lookup memo, class introspector, context pools and
|
|
136
|
+
lone map on first use; a root's plan is compiled on the request that repeats it, so a container that resolves a root
|
|
137
|
+
once never compiles; a binding's activation need is stamped on the binding instead of memoized in a per-resolver map
|
|
138
|
+
(`ResolutionDiagnostics.builtSubsystems` no longer lists `resolver.activationNeedMemo`); teardown clears instances
|
|
139
|
+
without splicing the singleton list or pairing each binding; a rebind of a lone token is one registration whose
|
|
140
|
+
displaced binding is deactivated on the spot; a fresh registration is one probe and one write; a chain's own `.many()`
|
|
141
|
+
re-slots without probing the registry; each error class names itself with a literal.
|
|
142
|
+
|
|
143
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A reader passed through `ContainerOptions.metadataReader` now outranks a `MetadataReaderToken` binding for that
|
|
144
|
+
container's children too, at every depth, as it already did for the container itself; a token binding still reaches the
|
|
145
|
+
children of a container given no reader.
|
|
146
|
+
|
|
147
|
+
- [#920](https://github.com/codefastlabs/codefast/pull/920) `resolveOptional`, `resolveOptionalAsync`, `resolveAll` and `resolveAllAsync` no longer throw when they reach an alias
|
|
148
|
+
whose chain ends at a token nothing matches. An alias is a transparent pointer, so a dangling chain is the same miss the
|
|
149
|
+
target itself would be: the optional lanes return `undefined`, and a dangling alias member is skipped from a collection
|
|
150
|
+
rather than failing the whole fan-out. A required `resolve` / `resolveAsync` still throws, and an alias **cycle** still
|
|
151
|
+
throws `CircularDependencyError` — a cycle has no absent reading.
|
|
152
|
+
|
|
153
|
+
- [#976](https://github.com/codefastlabs/codefast/pull/976) The README states what a program needs for the declarations' disposal members: the explicit resource management types,
|
|
154
|
+
which no numbered `lib` declares before ES2027. `@types/node` 24 or later loads them, and so does `ESNext.Disposable` in
|
|
155
|
+
`lib`. Without either, TypeScript 7 fails inside `container.d.ts` with TS2550 under `skipLibCheck: false`, and at
|
|
156
|
+
`await using` with TS2318. `@codefast/di-testing` drops a `/// <reference lib="esnext.disposable" />` that TypeScript 7
|
|
157
|
+
stripped from its emitted declarations anyway, and takes the lib from its `tsconfig.json` instead.
|
|
158
|
+
|
|
159
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A dependency slot declaring `tags: []` states no criterion, as a binding slot already did:
|
|
160
|
+
`injectionSlotToResolveOptions` answers `undefined` for it, and such a dependency no longer escapes a compiled plan.
|
|
161
|
+
|
|
162
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A `MetadataReader` passed through `ContainerOptions` is asked about a class once for the life of the reader: its answers
|
|
163
|
+
are memoized per class, and every container handed the same reader shares one verifying wrapper. Lifecycle metadata used
|
|
164
|
+
to be read on every activation, and each root container asked again.
|
|
165
|
+
|
|
166
|
+
- [#894](https://github.com/codefastlabs/codefast/pull/894) The React Flow graph export lays its nodes out on a square grid derived from the node count rather than a fixed five
|
|
167
|
+
columns.
|
|
168
|
+
|
|
169
|
+
- [#934](https://github.com/codefastlabs/codefast/pull/934) `MissingMetadataError` for a subclass that inherits declared constructor dependencies no longer advises giving it an
|
|
170
|
+
explicit constructor, which is rejected the same way. It points at `@injectable([...deps])` — `@injectable([])` for a
|
|
171
|
+
class that takes none — or a `toDynamic()`/`toResolved()` binding.
|
|
172
|
+
|
|
173
|
+
- [#894](https://github.com/codefastlabs/codefast/pull/894) Every resolution lane now answers a graph identically. A per-request child that misses a token its parent's bindings all
|
|
174
|
+
declined reports `NoMatchingBindingError` as the parent does, not `TokenNotBoundError`; a sibling on the async
|
|
175
|
+
interpreted lane is selected against its own path rather than the earlier sibling's frame; a dynamic factory that
|
|
176
|
+
resolves its own token from its synchronous prefix is reported as a cycle before it runs a second time.
|
|
177
|
+
|
|
178
|
+
- [#905](https://github.com/codefastlabs/codefast/pull/905) The chain-versioned lookup memo answers a repeated token from a method small enough for its hot callers to inline: the
|
|
179
|
+
hit is the whole of `defaultEntry`, and everything that fills the memo is the miss. An alias resolve, a parent-owned
|
|
180
|
+
resolve from a child and every other lookup that reaches the memo pays one inlined compare where it paid a call.
|
|
181
|
+
|
|
182
|
+
- [#938](https://github.com/codefastlabs/codefast/pull/938) A constant with an `onDeactivation` hook that a bind inside a module load displaces now runs that hook at `dispose()`,
|
|
183
|
+
as it already did when a plain `container.bind()` displaced it. A module's chains registered without the container's
|
|
184
|
+
displacement bookkeeping, so the displaced constant left the registry untracked and nothing ever tore it down; this held
|
|
185
|
+
for `Module.create` setups and declared modules (`Module.fromBindings`) alike. Unloading the displacing module still
|
|
186
|
+
does not restore the displaced binding — its deactivation stays owed to `dispose()`.
|
|
187
|
+
|
|
188
|
+
- [#918](https://github.com/codefastlabs/codefast/pull/918) A `{ name, tag }` request now selects the same binding as its `{ tags: [slotName.of(n), tag] }` spelling, closing two
|
|
189
|
+
selection bugs. A name that has been interned nowhere in the process no longer makes the request a miss: the request's
|
|
190
|
+
criteria can still be a superset of a slot's, so an index miss falls through to the scan and the parent walk instead of
|
|
191
|
+
returning early — the answer no longer depends on whether unrelated code has ever called `whenNamed` with that string.
|
|
192
|
+
And the name-plus-tag fast lane now declines to full selection whenever the token carries any `when()` predicate
|
|
193
|
+
candidate, so the more-specific rule's predicate step is honoured on this lane as on every other.
|
|
194
|
+
|
|
195
|
+
- [#896](https://github.com/codefastlabs/codefast/pull/896) Every binding predicate now reads one `ConstraintContext` shape: the shared root context, the context a selection builds
|
|
196
|
+
over a live path, an async level's prefix and the inspector's probe are all `DefaultConstraintContext`, exported from
|
|
197
|
+
`resolution/context`. A predicate's call site stays monomorphic, and the per-selection object literal with its eager
|
|
198
|
+
`ancestors` slice is gone — `ancestors` is now sliced on first read, as `ctx.graph` already did.
|
|
199
|
+
|
|
200
|
+
- [#916](https://github.com/codefastlabs/codefast/pull/916) `LEARNING.md` is removed, and with it the Learning page the docs site rendered for this package. What a shape is and
|
|
201
|
+
what it guarantees stays in `ARCHITECTURE.md`, the behavioural contract in `SPEC.md`, and usage in `README.md`, whose
|
|
202
|
+
document list no longer points at the removed file.
|
|
203
|
+
|
|
204
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) `resolveAll` returns a token's bindings in registration order however a chain refines its own binding. Binding a default
|
|
205
|
+
and then a named, tagged, member or predicate binding used to list the later one first, because the refinement re-added
|
|
206
|
+
what it had displaced behind it; each binding now keeps the place its first registration gave it.
|
|
207
|
+
|
|
208
|
+
- [#902](https://github.com/codefastlabs/codefast/pull/902) A transient factory root resolved with `resolveAsync()` is handed one resolution context per binding, built on the first
|
|
209
|
+
resolve and reused by every later one — its path is its own frame alone and the request carries no options, so the
|
|
210
|
+
context is a function of the binding. The root allocated an array and a context per resolve; it allocates nothing now,
|
|
211
|
+
and a concurrent root reads the same, correct, path after an `await`.
|
|
212
|
+
|
|
213
|
+
- [#919](https://github.com/codefastlabs/codefast/pull/919) A `NoMatchingBindingError`'s "Available slots" diagnostic no longer throws when a bound slot carries a tag value that
|
|
214
|
+
cannot be stringified — a bigint, a null-prototype object, or one whose `toString` throws. `bindingSlotToString` (and
|
|
215
|
+
the dependency-graph edge label) now render such a value as `<unprintable>` through a shared `stringifyTagValue` guard,
|
|
216
|
+
so the real `NoMatchingBindingError` surfaces instead of a masking `TypeError`, matching the guard the request-side
|
|
217
|
+
diagnostic already had.
|
|
218
|
+
|
|
219
|
+
- [#932](https://github.com/codefastlabs/codefast/pull/932) `SPEC.md` carries the behavioural contract and nothing else. The source-tree listing, the copied export barrel, the
|
|
220
|
+
build configuration and the roadmap of already-shipped work are gone — a reader can now rename any file under `src/`
|
|
221
|
+
without contradicting the spec. `Public API` states the surface as a rule instead of a second copy of `index.ts`, which
|
|
222
|
+
had already drifted from it, and `Scope and requirements` states the runtime, module format and TypeScript floor a
|
|
223
|
+
consumer must bring. New `DECISIONS.md` takes the background and the whole InversifyJS v8 comparison, `README.md` takes
|
|
224
|
+
the testing patterns with the `MetadataReader` example corrected to `Container.create({ metadataReader })`, and
|
|
225
|
+
`CONTRIBUTING.md` takes the packaging and decorator-toolchain notes.
|
|
226
|
+
|
|
227
|
+
Checking the spec against the source turned up four public names it never described: `BindingSnapshot.isMany`, the
|
|
228
|
+
`DependencySlot` that both `InjectionDescriptor` and `ParamMetadata` extend, and `bindingSlotToResolveOptions`, which
|
|
229
|
+
turns a snapshot's slot back into the `ResolveOptions` that selects it. All three are specified now, and the claim that
|
|
230
|
+
`toAlias()` returns the one builder with no type parameter is corrected — it has no _value_ type parameter, but it does
|
|
231
|
+
carry the token's slot names.
|
|
232
|
+
|
|
233
|
+
- [#927](https://github.com/codefastlabs/codefast/pull/927) `@postConstruct` / `@preDestroy` on a symbol-keyed method now fail at the declaration with the new
|
|
234
|
+
`SymbolKeyedLifecycleError`, instead of a misleading `InvalidMetadataError` at resolve that blamed a `MetadataReader`
|
|
235
|
+
the caller never configured. The lifecycle reader keys methods by their string name, so a symbol-keyed method could
|
|
236
|
+
never be found again; the decorator now rejects it up front, where the mistake is.
|
|
237
|
+
|
|
238
|
+
- [#957](https://github.com/codefastlabs/codefast/pull/957) `MissingDecoratorMetadataError` suggests installing `Symbol.metadata` as
|
|
239
|
+
`(Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata")`, which type-checks in a `.ts` file; the
|
|
240
|
+
bare `Symbol.metadata ??= …` it quoted failed with TS2540, since TypeScript declares the symbol `readonly`.
|
|
241
|
+
|
|
242
|
+
- [#933](https://github.com/codefastlabs/codefast/pull/933) A rejected async `onActivation` on a transient `toDynamic` binding — per-binding or container-level — no longer surfaces
|
|
243
|
+
as an unhandled rejection: the sync resolve still throws `AsyncActivationError`, and the hook's promise is adopted as it
|
|
244
|
+
already was on every other lane.
|
|
245
|
+
|
|
246
|
+
- [#965](https://github.com/codefastlabs/codefast/pull/965) The documented TypeScript floor is now 7, up from 5.9: TypeScript 7 is the one compiler that type-checks the package and
|
|
247
|
+
emits its published declarations, so it is the floor they support. `README.md`, `SPEC.md` and `DECISIONS.md` state it;
|
|
248
|
+
no code or declaration changed.
|
|
249
|
+
|
|
250
|
+
- [#940](https://github.com/codefastlabs/codefast/pull/940) `unbind(id)` and `unbindAsync(id)` now tear down a binding that a later last-wins bind displaced: its cached singleton
|
|
251
|
+
is deactivated, and a displaced constant runs its `onDeactivation`, at that call instead of waiting for `dispose()`. The
|
|
252
|
+
id lookup asked only the registry, which a displaced binding had already left, so the call did nothing. The unbind also
|
|
253
|
+
invalidates what the displacing chain holds parked, so a later `when*()` refinement of that chain can no longer restore
|
|
254
|
+
the unbound binding.
|
|
255
|
+
|
|
256
|
+
- [#939](https://github.com/codefastlabs/codefast/pull/939) Unloading a module now tears down a binding it registered that a later last-wins bind displaced: a displaced cached
|
|
257
|
+
singleton is deactivated, and a displaced constant runs its `onDeactivation`, at that `unload()`/`unloadAsync()` instead
|
|
258
|
+
of waiting for `dispose()`. Unload looked each of the module's bindings up in the registry, which a displaced binding
|
|
259
|
+
had already left, so it was skipped. The unload also invalidates what the displacing chain holds parked, so a later
|
|
260
|
+
`when*()` refinement of that chain can no longer restore a binding of the unloaded module.
|
|
261
|
+
|
|
262
|
+
- [#923](https://github.com/codefastlabs/codefast/pull/923) `validate()` now reports a container-level `onDeactivation` hook that can never run. The builder type blocks
|
|
263
|
+
`onDeactivation` on `scoped`/`transient` bindings, but `container.onDeactivation(token, handler)` takes any token with
|
|
264
|
+
no such gate, so a hook keyed to a token whose every binding is scoped or transient used to pass validation silently.
|
|
265
|
+
`validate()` now raises `UnreachableLifecycleHookError` for it, reusing the mechanism that already catches a hook on an
|
|
266
|
+
unbound token. A token that also has a singleton or constant binding, and any `onActivation` hook, stay valid.
|
|
267
|
+
|
|
3
268
|
## 0.10.1
|
|
4
269
|
|
|
5
270
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -35,9 +35,14 @@ its keep as the dependency graph grows.
|
|
|
35
35
|
pnpm add @codefast/di
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`@codefast/di`
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
`@codefast/di` runs on Node.js 24 or later and in Chrome and Edge 136, Firefox 136, or Safari 18.4 or later
|
|
39
|
+
([support policy](../../SUPPORT.md#browsers)). It requires TypeScript 7 or later, with native Stage 3 decorators. Leave
|
|
40
|
+
`experimentalDecorators` off — it's off by default. Its declarations use explicit resource management, which no numbered
|
|
41
|
+
`lib` declares yet, so your program needs those types: `@types/node` 24 or later loads them, and any other program adds
|
|
42
|
+
`ESNext.Disposable` to `lib` once its runtime ships explicit resource management. A browser program that targets Safari,
|
|
43
|
+
which has not shipped it, keeps `skipLibCheck` on and calls `dispose()` instead of `await using`. The package is
|
|
44
|
+
published on 0.x and versioned on its own track: breaking changes ship as minor versions, so pin the minor version when
|
|
45
|
+
you need stability.
|
|
41
46
|
|
|
42
47
|
## Quick start
|
|
43
48
|
|
|
@@ -282,7 +287,8 @@ const byHand = runWithContainer(container, () => new Controller());
|
|
|
282
287
|
|
|
283
288
|
An accessor resolves from the container that's constructing the instance. When something else owns the `new` — a router,
|
|
284
289
|
an ORM, a test helper — open that context with `runWithContainer`. Without one, the accessor throws
|
|
285
|
-
`MissingContainerContextError`. `getActiveContainer()` reads the open context.
|
|
290
|
+
`MissingContainerContextError`. `getActiveContainer()` reads the open context. `runWithContainer` is **synchronous
|
|
291
|
+
only**: the context does not survive an `await`, so a callback returning a `Promise` resolves to `never`.
|
|
286
292
|
|
|
287
293
|
### Auto-registration
|
|
288
294
|
|
|
@@ -384,6 +390,62 @@ const container = Container.fromModules(AppModule);
|
|
|
384
390
|
`Container.fromModulesAsync` or `container.loadAsync`. Modules are ref-counted: loading one twice counts once, and
|
|
385
391
|
`unload` removes its bindings only when the count reaches zero.
|
|
386
392
|
|
|
393
|
+
A module with no logic of its own can be written as a list instead. Each `binding()` states one binding with the fluent
|
|
394
|
+
chain's own vocabulary, is typed exactly as the chain is, and is checked where it is written:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { binding, Module } from "@codefast/di";
|
|
398
|
+
|
|
399
|
+
const InfrastructureModule = Module.fromBindings("app:Infra", [
|
|
400
|
+
binding(LoggerToken, { toConstantValue: console }),
|
|
401
|
+
binding(UserRepository, { toSelf: true, scope: "singleton" }),
|
|
402
|
+
binding(CacheToken, { toDynamic: () => new Map(), whenNamed: "session", scope: "scoped" }),
|
|
403
|
+
]);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
It loads exactly as the equivalent `Module.create` would, and never slower: each binding registers in its final shape,
|
|
407
|
+
so one with a slot, a scope or a hook skips the chain steps a fluent setup pays on every load. A declared module mixes
|
|
408
|
+
into a fluent one with `api.import(...)`.
|
|
409
|
+
|
|
410
|
+
## Testing
|
|
411
|
+
|
|
412
|
+
Give every test its own container and dispose it afterwards. A container shared across tests carries cached singletons
|
|
413
|
+
from one into the next.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
let container: Container;
|
|
417
|
+
|
|
418
|
+
beforeEach(() => {
|
|
419
|
+
container = Container.create();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
afterEach(async () => {
|
|
423
|
+
await container.dispose();
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
To swap one binding out of a module you already loaded, bind it at a **child** container — the child has no binding of
|
|
428
|
+
its own yet, and resolution prefers the child over its parent:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const testContainer = Container.fromModules(AppModule).createChild();
|
|
432
|
+
testContainer.bind(DbToken).toConstantValue(fakeDatabase);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`rebind()` is for replacing a binding that already exists in the **same** container — it throws
|
|
436
|
+
`RebindUnboundTokenError` when the token is only bound at a parent, so it is the wrong tool for the override above.
|
|
437
|
+
|
|
438
|
+
To construct a class the container was never told about — plain JavaScript, generated code, a class you cannot decorate
|
|
439
|
+
— supply the metadata yourself. Pass the reader to `Container.create`: a reader bound through `MetadataReaderToken` on
|
|
440
|
+
the container you are about to use is read too late, because the resolver was built with the container.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
const container = Container.create({ metadataReader: customReader });
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
For unit tests where every collaborator should be a mock, [`@codefast/di-testing`](../di-testing/README.md) builds the
|
|
447
|
+
mocks from the class's own declared dependencies, through a real container.
|
|
448
|
+
|
|
387
449
|
## Errors
|
|
388
450
|
|
|
389
451
|
Every error extends `DiError` and carries a stable `code`, so you can branch on the code rather than the message:
|
|
@@ -402,7 +464,8 @@ Every error extends `DiError` and carries a stable `code`, so you can branch on
|
|
|
402
464
|
| `DisposedContainerError` | `DISPOSED_CONTAINER` | A disposed container is used |
|
|
403
465
|
|
|
404
466
|
The full taxonomy — including `MissingMetadataError`, `InvalidMetadataError`, `RebindUnboundTokenError`,
|
|
405
|
-
`AsyncModuleLoadError`, and the rest — is exported from the root entry and from
|
|
467
|
+
`AsyncModuleLoadError`, `InvalidBindingDeclarationError`, and the rest — is exported from the root entry and from
|
|
468
|
+
`@codefast/di/errors/errors`.
|
|
406
469
|
|
|
407
470
|
## Subpath exports
|
|
408
471
|
|
|
@@ -426,7 +489,7 @@ alongside the method that produced them. Run it yourself rather than taking any
|
|
|
426
489
|
- [Rendered docs on codefastlabs.com](https://codefastlabs.com/docs/di)
|
|
427
490
|
- [`SPEC.md`](./SPEC.md) — the behavioural contract: public API, semantics, and errors.
|
|
428
491
|
- [`ARCHITECTURE.md`](./ARCHITECTURE.md) — the internal shape and the invariants the hot paths depend on.
|
|
429
|
-
- [`
|
|
492
|
+
- [`DECISIONS.md`](./DECISIONS.md) — why the API looks like this, and what it did not take from InversifyJS v8.
|
|
430
493
|
- [`examples/`](./examples/README.md) — runnable examples from basic tokens to a multi-file Ports & Adapters app.
|
|
431
494
|
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — the package workflow: build, exports mirror, tests, and the perf guard.
|
|
432
495
|
- [`CHANGELOG.md`](./CHANGELOG.md) — release history.
|
|
@@ -16,11 +16,16 @@ export interface AmbientResolution {
|
|
|
16
16
|
resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Runs a callback with the given container installed as the ambient one and returns its result.
|
|
19
|
+
* Runs a synchronous callback with the given container installed as the ambient one and returns its result.
|
|
20
|
+
*
|
|
21
|
+
* @remarks The context lives in a module-level variable restored in a `finally`, so it lasts only the
|
|
22
|
+
* synchronous run of `fn` — it does not survive an `await`. A callback returning a `Promise` is a
|
|
23
|
+
* mistake the return type flags: it resolves to `never`, and the ambient context would already be torn
|
|
24
|
+
* down by the time the promise settled.
|
|
20
25
|
*
|
|
21
26
|
* @since 0.3.16-canary.0
|
|
22
27
|
*/
|
|
23
|
-
export declare function runWithContainer<Result>(container: Container, fn: () => Result): Result;
|
|
28
|
+
export declare function runWithContainer<Result>(container: Container, fn: () => Result): Result extends Promise<unknown> ? never : Result;
|
|
24
29
|
/**
|
|
25
30
|
* Engine-internal variant of {@link runWithContainer} that also installs a path-continuing resolver.
|
|
26
31
|
*
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
let activeContainer;
|
|
2
2
|
let activeResolution;
|
|
3
3
|
/**
|
|
4
|
-
* Runs a callback with the given container installed as the ambient one and returns its result.
|
|
4
|
+
* Runs a synchronous callback with the given container installed as the ambient one and returns its result.
|
|
5
|
+
*
|
|
6
|
+
* @remarks The context lives in a module-level variable restored in a `finally`, so it lasts only the
|
|
7
|
+
* synchronous run of `fn` — it does not survive an `await`. A callback returning a `Promise` is a
|
|
8
|
+
* mistake the return type flags: it resolves to `never`, and the ambient context would already be torn
|
|
9
|
+
* down by the time the promise settled.
|
|
5
10
|
*
|
|
6
11
|
* @since 0.3.16-canary.0
|
|
7
12
|
*/
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* binding in the process shares one hidden class; the return type of each step is what pins the
|
|
7
7
|
* chain's order, so no runtime check has to.
|
|
8
8
|
*/
|
|
9
|
-
import type { AliasBindingBuilder, BindingBuilder, BindingSlot, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding";
|
|
9
|
+
import type { AliasBindingBuilder, Binding, BindingBuilder, BindingSlot, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding";
|
|
10
|
+
import type { DeclaredBinding } from "#core/binding-declaration";
|
|
10
11
|
import type { BindingRegistry } from "#core/registry";
|
|
11
12
|
import type { BindingTag } from "#core/tag";
|
|
12
13
|
import type { Token } from "#core/token";
|
|
@@ -25,6 +26,12 @@ export interface BindingRegistration {
|
|
|
25
26
|
readonly registry: BindingRegistry;
|
|
26
27
|
readonly scope: ScopeManager;
|
|
27
28
|
readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
|
|
29
|
+
/** Runs for a binding this registration displaces, instead of parking it for a later restore. */
|
|
30
|
+
readonly deactivateDisplaced?: ((binding: Binding) => void) | undefined;
|
|
31
|
+
/** Notes a parked displaced binding so the container can still tear it down if no restore reclaims it. */
|
|
32
|
+
readonly onDisplaced?: ((binding: Binding) => void) | undefined;
|
|
33
|
+
/** Undoes {@link BindingRegistration.onDisplaced} when a refinement restores the binding to the registry. */
|
|
34
|
+
readonly onRestored?: ((binding: Binding) => void) | undefined;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
37
|
* The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
|
|
@@ -42,6 +49,9 @@ export declare class BindingChain<Value, Names extends string = string> implemen
|
|
|
42
49
|
readonly identifier: BindingIdentifier;
|
|
43
50
|
inFlight: boolean;
|
|
44
51
|
frame: ResolutionFrame | undefined;
|
|
52
|
+
rootContext: ResolutionContext | undefined;
|
|
53
|
+
activationStamp: number;
|
|
54
|
+
registrationOrder: number;
|
|
45
55
|
instance: unknown;
|
|
46
56
|
readonly token: Token<Value, Names> | Constructor<Value>;
|
|
47
57
|
slot: BindingSlot;
|
|
@@ -55,6 +65,14 @@ export declare class BindingChain<Value, Names extends string = string> implemen
|
|
|
55
65
|
activationHook: ActivationHandler<Value> | undefined;
|
|
56
66
|
deactivationHook: DeactivationHandler<Value> | undefined;
|
|
57
67
|
constructor(token: Token<Value, Names> | Constructor<Value>, registration: BindingRegistration);
|
|
68
|
+
/**
|
|
69
|
+
* Registers a declared module's bindings in list order, each a chain of its own that no step
|
|
70
|
+
* will ever refine.
|
|
71
|
+
*
|
|
72
|
+
* @remarks A displaced binding is handled as `to*()` handles it, never parked: nothing refines the
|
|
73
|
+
* chain, so nothing could restore it.
|
|
74
|
+
*/
|
|
75
|
+
static registerDeclared(declarations: ReadonlyArray<DeclaredBinding>, registration: BindingRegistration): void;
|
|
58
76
|
to(type: Constructor<Value>): BindingBuilder<Value, Names>;
|
|
59
77
|
toSelf(): BindingBuilder<Value, Names>;
|
|
60
78
|
toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { clearBindingFrame,
|
|
1
|
+
import { clearBindingFrame, NO_ACTIVATION_STAMP, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, UNREGISTERED_ORDER, withSlotCriterion, } from "#core/binding";
|
|
2
2
|
import { mergingConstraintRequirements } from "#core/constraint-requirement";
|
|
3
3
|
import { slotName } from "#core/tag";
|
|
4
4
|
import { tokenName } from "#core/token";
|
|
5
5
|
import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#errors/errors";
|
|
6
6
|
import { normalizeToDescriptor } from "#injection/descriptor";
|
|
7
|
-
/** One criterion per key: re-tagging the same key replaces it rather than asking for both values. */
|
|
8
|
-
function updateSlotTag(slot, criterion) {
|
|
9
|
-
const tags = [...slot.tags];
|
|
10
|
-
const existingIndex = tags.findIndex((existing) => existing.key === criterion.key);
|
|
11
|
-
if (existingIndex === -1) {
|
|
12
|
-
tags.push(criterion);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
tags[existingIndex] = criterion;
|
|
16
|
-
}
|
|
17
|
-
return createBindingSlot(tags);
|
|
18
|
-
}
|
|
19
7
|
// ── BindingChain ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
20
8
|
/**
|
|
21
9
|
* The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
|
|
@@ -33,6 +21,9 @@ export class BindingChain {
|
|
|
33
21
|
identifier = generateBindingId();
|
|
34
22
|
inFlight = false;
|
|
35
23
|
frame = undefined;
|
|
24
|
+
rootContext = undefined;
|
|
25
|
+
activationStamp = NO_ACTIVATION_STAMP;
|
|
26
|
+
registrationOrder = UNREGISTERED_ORDER;
|
|
36
27
|
instance = NO_INSTANCE;
|
|
37
28
|
token;
|
|
38
29
|
slot = DEFAULT_BINDING_SLOT;
|
|
@@ -76,9 +67,70 @@ export class BindingChain {
|
|
|
76
67
|
this.kind = kind;
|
|
77
68
|
this.scope = scope;
|
|
78
69
|
this.#isRegistered = true;
|
|
79
|
-
|
|
70
|
+
// A fresh registration parks nothing yet and restores nothing: one add, then the version.
|
|
71
|
+
const registration = this.#registration;
|
|
72
|
+
const registry = registration.registry;
|
|
73
|
+
const displaced = registry.add(this.#binding);
|
|
74
|
+
if (displaced !== undefined) {
|
|
75
|
+
if (registration.deactivateDisplaced !== undefined) {
|
|
76
|
+
registration.deactivateDisplaced(displaced);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.#displacedByChain = [displaced];
|
|
80
|
+
registration.onDisplaced?.(displaced);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (registration.moduleBindingIds !== undefined) {
|
|
84
|
+
registration.moduleBindingIds.push(this.identifier);
|
|
85
|
+
}
|
|
86
|
+
this.#versionAfterLastWrite = registry.version;
|
|
80
87
|
return this;
|
|
81
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Registers a declared module's bindings in list order, each a chain of its own that no step
|
|
91
|
+
* will ever refine.
|
|
92
|
+
*
|
|
93
|
+
* @remarks A displaced binding is handled as `to*()` handles it, never parked: nothing refines the
|
|
94
|
+
* chain, so nothing could restore it.
|
|
95
|
+
*/
|
|
96
|
+
static registerDeclared(declarations, registration) {
|
|
97
|
+
const { registry, moduleBindingIds, deactivateDisplaced, onDisplaced } = registration;
|
|
98
|
+
for (let index = 0; index < declarations.length; index += 1) {
|
|
99
|
+
const declaration = declarations[index];
|
|
100
|
+
const binding = BindingChain.#fromDeclaration(declaration, registration);
|
|
101
|
+
const displaced = registry.add(binding);
|
|
102
|
+
if (displaced !== undefined) {
|
|
103
|
+
if (deactivateDisplaced === undefined) {
|
|
104
|
+
onDisplaced?.(displaced);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
deactivateDisplaced(displaced);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
moduleBindingIds?.push(binding.identifier);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A registered chain in the shape a declaration's steps would have left.
|
|
115
|
+
*
|
|
116
|
+
* @remarks Typed `unknown` like the declaration it copies, so the value type is never asserted here.
|
|
117
|
+
*/
|
|
118
|
+
static #fromDeclaration(declaration, registration) {
|
|
119
|
+
const chain = new BindingChain(declaration.token, registration);
|
|
120
|
+
chain.kind = declaration.kind;
|
|
121
|
+
chain.slot = declaration.slot;
|
|
122
|
+
chain.predicate = declaration.predicate;
|
|
123
|
+
chain.isMany = declaration.isMany;
|
|
124
|
+
chain.scope = declaration.scope;
|
|
125
|
+
chain.target = declaration.target;
|
|
126
|
+
chain.factory = declaration.factory;
|
|
127
|
+
chain.deps = declaration.deps;
|
|
128
|
+
chain.value = declaration.value;
|
|
129
|
+
chain.activationHook = declaration.activationHook;
|
|
130
|
+
chain.deactivationHook = declaration.deactivationHook;
|
|
131
|
+
chain.#isRegistered = true;
|
|
132
|
+
return chain.#binding;
|
|
133
|
+
}
|
|
82
134
|
// ── Registration ───────────────────────────────────────────────────────────────────────────────────────────────────
|
|
83
135
|
to(type) {
|
|
84
136
|
this.#requireUnregistered();
|
|
@@ -195,7 +247,7 @@ export class BindingChain {
|
|
|
195
247
|
if (this.isMany) {
|
|
196
248
|
throw new ManyBindingSlotError(tokenName(this.token));
|
|
197
249
|
}
|
|
198
|
-
return this.#reslot(
|
|
250
|
+
return this.#reslot(withSlotCriterion(this.slot, criterion), this.predicate);
|
|
199
251
|
}
|
|
200
252
|
many() {
|
|
201
253
|
this.#requireRegistered();
|
|
@@ -281,7 +333,13 @@ export class BindingChain {
|
|
|
281
333
|
}
|
|
282
334
|
const displaced = registry.add(registered);
|
|
283
335
|
if (displaced !== undefined) {
|
|
284
|
-
(this.#
|
|
336
|
+
if (this.#registration.deactivateDisplaced !== undefined && rewrite === undefined) {
|
|
337
|
+
this.#registration.deactivateDisplaced(displaced);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
(this.#displacedByChain ??= []).push(displaced);
|
|
341
|
+
this.#registration.onDisplaced?.(displaced);
|
|
342
|
+
}
|
|
285
343
|
}
|
|
286
344
|
if (rewrite !== undefined && this.#displacedByChain !== undefined) {
|
|
287
345
|
this.#restoreNonConflicting(this.#displacedByChain);
|
|
@@ -298,6 +356,7 @@ export class BindingChain {
|
|
|
298
356
|
// the candidate stays parked.
|
|
299
357
|
if (!this.#registration.registry.hasSlotOccupant(candidate)) {
|
|
300
358
|
this.#registration.registry.add(candidate);
|
|
359
|
+
this.#registration.onRestored?.(candidate);
|
|
301
360
|
displaced.splice(index, 1);
|
|
302
361
|
}
|
|
303
362
|
}
|