@codefast/di 0.11.0 → 0.12.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +26 -12
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -2
  6. package/dist/container/container.js +107 -64
  7. package/dist/core/binding-builders.d.ts +114 -0
  8. package/dist/core/binding-declaration.d.ts +7 -0
  9. package/dist/core/binding-declaration.js +3 -2
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.js +5 -5
  17. package/dist/decorators/injectable.js +2 -2
  18. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +3 -3
  19. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  20. package/dist/{errors/errors.d.ts → errors.d.ts} +7 -6
  21. package/dist/{errors/errors.js → errors.js} +8 -7
  22. package/dist/index.d.ts +12 -10
  23. package/dist/index.js +6 -6
  24. package/dist/injection/descriptor.d.ts +1 -1
  25. package/dist/introspection/dependency-graph.d.ts +1 -1
  26. package/dist/introspection/dependency-graph.js +3 -4
  27. package/dist/introspection/explanation.d.ts +101 -0
  28. package/dist/introspection/explanation.js +151 -0
  29. package/dist/introspection/inspector.d.ts +9 -2
  30. package/dist/introspection/inspector.js +25 -20
  31. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  32. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +45 -28
  33. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  34. package/dist/lifecycle/{scope-manager.js → scopes.js} +8 -19
  35. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  36. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  37. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  38. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  39. package/dist/metadata/types.js +1 -0
  40. package/dist/metadata/verify.d.ts +43 -0
  41. package/dist/metadata/verify.js +133 -0
  42. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  43. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  44. package/dist/resolution/cache/activation-need.d.ts +1 -1
  45. package/dist/resolution/cache/activation-need.js +5 -1
  46. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  47. package/dist/resolution/cache/class-introspector.js +61 -159
  48. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  49. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  50. package/dist/resolution/context.d.ts +1 -1
  51. package/dist/resolution/context.js +1 -1
  52. package/dist/resolution/{path/resolution-path.js → path.js} +1 -1
  53. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  54. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  55. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  56. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  57. package/dist/resolution/resolver.d.ts +7 -7
  58. package/dist/resolution/resolver.js +308 -121
  59. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  60. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  61. package/dist/resolution/select/constraints.js +1 -1
  62. package/package.json +79 -71
  63. package/dist/core/binding-scope.d.ts +0 -12
  64. package/dist/core/binding-scope.js +0 -12
  65. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  66. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  67. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  68. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  69. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  70. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  71. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  72. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  73. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  74. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  75. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  76. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
  77. /package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,116 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1010](https://github.com/codefastlabs/codefast/pull/1010) **Breaking:** `resolveAsync`, `resolveOptionalAsync` and `resolveAllAsync` on a disposed container — or on a child of
8
+ one — now return a promise rejected with `DisposedContainerError` instead of throwing it, and `resolveAllAsync(token)`
9
+ likewise rejects when a `when()` predicate throws or an alias cycle is followed, so every async method reports its
10
+ failure through the promise it returns. Code that awaits these calls, or chains `.catch()`, behaves as before or better;
11
+ only a caller that wrapped the call itself in `try`/`catch` without awaiting it no longer sees the error there, and
12
+ should await it instead.
13
+
14
+ - [#995](https://github.com/codefastlabs/codefast/pull/995) Add `container.explain(token, options)`, which answers why a request selects the binding it does without instantiating
15
+ anything. It reads the container chain the way `resolve` does and decides by the same rules, so its answer is the
16
+ engine's own. It reports each registry it read with every candidate and its verdict (`eligible`, `slot-mismatch`,
17
+ `predicate-refused`, `collection-member`), the rule that decided (`sole-candidate`, `sole-predicate`, `most-criteria`,
18
+ `default-alias`, `ambiguous`), the binding the request ends on after every alias, and an `outcome` naming the error
19
+ `resolve` would throw when it ends on none. `when()` predicates run as they would in `resolve`, and `options.ancestors`
20
+ explains a request made from inside other resolutions, so a predicate that reads the parent sees the one it would.
21
+
22
+ - [#999](https://github.com/codefastlabs/codefast/pull/999) **Breaking:** a binding's slot is declared before its strategy —
23
+ `bind(Logger).whenNamed("file").to(FileLogger).singleton()`. `when()`, `whenNamed()`, `whenTagged()`, `whenDefault()`
24
+ and `many()` now come before `to*()` and are refused after it with `ChainAlreadyRegisteredError`, so a binding registers
25
+ once, in its final slot, and no later step moves it, displaces something else or brings back what it displaced. Scope
26
+ and lifecycle hooks stay after `to*()`. To migrate, move each chain's slot steps in front of its `to*()` call.
27
+
28
+ The registry no longer parks the bindings a re-slot displaced or restores them, so a bind with a name or a tag costs a
29
+ quarter to two fifths less. `BindingBuilder`, `ConstantBindingBuilder` and `AliasBindingBuilder` lose their `Names` type
30
+ parameter, since the slot is settled before they exist.
31
+
32
+ - [#1005](https://github.com/codefastlabs/codefast/pull/1005) **Breaking for deep subpath imports only** — the root entry `@codefast/di` is unchanged. `src/` now follows three rules,
33
+ and the published subpaths follow `src/` with no exception: a directory names a family of two or more modules and a lone
34
+ module sits flat at its layer; a directory carries the topic and a file its role, so no file repeats its directory's
35
+ name; and the exports map is the source tree, so the `strip` that kept the introspection modules at flat specifiers is
36
+ gone.
37
+
38
+ Moves: the `MetadataReader` verification leaves `resolution/cache/class-introspector` for `metadata/verify` — it was the
39
+ one value import pointing up from `metadata/` into `resolution/`; the `RESOLUTION_DIAGNOSTICS` channel leaves `errors/`
40
+ for `introspection/diagnostics`; the fluent chain's contract leaves `core/binding` for `core/binding-builders`, and the
41
+ class implementing it is `container/binding-chain`; `effectiveBindingScope` folds into `core/binding`.
42
+
43
+ Renamed specifiers: `./errors/errors` → `./errors`; `./errors/diagnostics` → `./introspection/diagnostics`;
44
+ `./ambient/active-container` → `./ambient-container`; `./container/binding-builders` → `./container/binding-chain`;
45
+ `./core/binding-scope` → gone (`effectiveBindingScope` is in `./core/binding`); `./injection/resolve-options` →
46
+ `./injection/dependency-slot`;
47
+ `./metadata/{metadata-keys,metadata-types,metadata-reader-token,symbol-metadata-reader,verifying-metadata-reader}` →
48
+ `./metadata/{keys,types,reader-token,symbol-reader,verifying-reader}`; `./lifecycle/{lifecycle-manager,scope-manager}` →
49
+ `./lifecycle/{hooks,scopes}`; `./decorators/{lifecycle-decorators,decorator-metadata}` →
50
+ `./decorators/{lifecycle,metadata-record}`; `./resolution/path/resolution-path` → `./resolution/path`;
51
+ `./resolution/plan/{instantiation-plan,plan-codegen}` → `./resolution/plan/{compiler,codegen}`;
52
+ `./resolution/select/binding-select` → `./resolution/select/candidates`; `./resolution/cache/binding-lookup-cache` →
53
+ `./resolution/cache/lookup`; `./{inspector,explanation,dependency-graph,graph-adapters/*}` →
54
+ `./introspection/{inspector,explanation,dependency-graph,graph-adapters/*}`. New: `./core/binding-builders`,
55
+ `./metadata/verify`.
56
+
57
+ `@codefast/di-testing` follows the one specifier it imports, `metadata/verifying-reader`.
58
+
59
+ ### Patch Changes
60
+
61
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A request with criteria that matches no slot is fast again when the container holds no alias: `resolveOptional()` of a
62
+ missing tagged slot no longer probes for a default-slot alias to forward to in a registry that never held one.
63
+
64
+ - [#1009](https://github.com/codefastlabs/codefast/pull/1009) `resolveAsync` no longer rejects with `AsyncResolutionError` when a later sibling reads a singleton or scoped binding
65
+ synchronously — a `toDynamic` factory's `ctx.resolve`, an `@inject` accessor, a top-level `resolve` — while the async
66
+ lane is still materialising it from an earlier sibling. A level of the async lane now yields only where a dependency or
67
+ hook really awaits, so an instance built without yielding is cached before the next sibling starts, and only a
68
+ materialisation that is genuinely pending is published in flight.
69
+
70
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scope refinement costs less at bind time. `singleton()`, `transient()` and `scoped()` now do nothing when the scope
71
+ does not change, and release only what exists when it does, so `bind(T).to(X).singleton()` straight after registration
72
+ no longer calls into the scope manager.
73
+
74
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) Every read on a child container is cheaper. The child's check that no ancestor was disposed is now one epoch compare
75
+ that V8 inlines into each entry point, the walk up the chain runs only after some container was disposed, and a new
76
+ child starts confirmed live, so a per-request child skips the walk on its first read.
77
+
78
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) Resolving a class binding for the first time is cheaper. The metadata cache now keeps one record per class, which a cold
79
+ resolve looks up once instead of reading a separate map for each fact, and a class with no constructor parameters is
80
+ built without allocating an empty argument list. This mostly speeds up containers that bind and resolve many class
81
+ singletons at startup.
82
+
83
+ - [#1001](https://github.com/codefastlabs/codefast/pull/1001) The first resolve of a class binding is cheaper. The class's parameter list, and the check that a subclass is not
84
+ dropping its base's declared dependencies, are settled once per class instead of once per instantiation, and the class
85
+ asked about last is answered without a map lookup, so a hundred singletons of one class bind and materialize about a
86
+ fifth faster.
87
+
88
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A root-level `resolveAll()` read in a loop over the same token, as an event bus does for every event, is cheaper: the
89
+ lookup cache keeps the last collection in front of its map, so a repeat read skips the map lookup.
90
+
91
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) A container that reads a collection once no longer builds a value list it never reads again. A root-level `resolveAll`
92
+ keeps its stable value list from the second read on, the way an instantiation plan compiles on repeat, so a fresh
93
+ container per request pays only for the members it resolves.
94
+
95
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A resolve that carries options on a root container gets back much of the speed 0.11.0 took from it. The check that
96
+ refuses a disposed chain grew in that release enough that V8 stopped inlining the lookup behind that entry point; a live
97
+ root now pays two field reads for it, and only a child reads the dispose epoch.
98
+
99
+ - [#998](https://github.com/codefastlabs/codefast/pull/998) A root container's `resolve(token, options)` no longer pays for the check a child makes that no ancestor was disposed.
100
+ Each container is created with the `resolve` of its role, so the root's never carries the child's path and the rule that
101
+ a disposed ancestor refuses its descendants stays as it was.
102
+
103
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scoped binding whose scope changes stops handing children the instance they cached before the change. A binding
104
+ registered on a parent, resolved from a child, then refined `scoped()` → `transient()` → `scoped()` gave that child its
105
+ old scoped instance back, because only the registering container's cache was cleared. Every container now files a scoped
106
+ instance under a key the binding replaces when it leaves `scoped`, so the cached instance is never found again.
107
+
108
+ - [#1003](https://github.com/codefastlabs/codefast/pull/1003) A teardown reads a class's `@preDestroy` methods once for a run of bindings of that class, instead of once per binding,
109
+ and names the token only when a hook fails.
110
+
111
+ - [#1002](https://github.com/codefastlabs/codefast/pull/1002) A rebind that owes no teardown allocates nothing for the binding it displaces, and a collection read skips its scan for
112
+ dangling aliases in a chain that never held one.
113
+
3
114
  ## 0.11.0
4
115
 
5
116
  ### Minor Changes
package/README.md CHANGED
@@ -169,8 +169,8 @@ constraints is its slot — think of a slot as a labelled variant of the token.
169
169
  `{ name }`:
170
170
 
171
171
  ```ts
172
- container.bind(LoggerToken).toConstantValue(consoleLogger).whenNamed("console");
173
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
172
+ container.bind(LoggerToken).whenNamed("console").toConstantValue(consoleLogger);
173
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
174
174
 
175
175
  container.resolve(LoggerToken, { name: "file" }); // → fileLogger
176
176
  ```
@@ -180,7 +180,7 @@ Declare the names on the token and they become checked, completable literals at
180
180
  ```ts
181
181
  const LoggerToken = token<Logger, "console" | "file">("app:Logger");
182
182
 
183
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
183
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
184
184
  container.resolve(LoggerToken, { name: "file" }); // { name: "fiel" } is a compile error
185
185
  ```
186
186
 
@@ -193,7 +193,7 @@ import { tag } from "@codefast/di";
193
193
 
194
194
  const Provider = tag<"s3" | "gcs">("app:provider");
195
195
 
196
- container.bind(StorageToken).to(S3Storage).whenTagged(Provider.of("s3"));
196
+ container.bind(StorageToken).whenTagged(Provider.of("s3")).to(S3Storage);
197
197
  container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
198
198
  ```
199
199
 
@@ -230,7 +230,7 @@ predicates ship from the root entry:
230
230
  | `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
231
231
 
232
232
  For the exact matching and most-specific-wins rules, see
233
- [`SPEC.md` → Slots and last-wins](./SPEC.md#slots-and-last-wins--the-exact-definition).
233
+ [the specification → Slots and last-wins](./spec/binding.md#slots-and-last-wins--the-exact-definition).
234
234
 
235
235
  ## Decorators
236
236
 
@@ -367,6 +367,18 @@ exits. Synchronous `using` isn't supported — `onDeactivation` may be async —
367
367
  token's bindings; and `container.generateDependencyGraph()` returns a JSON graph. Adapters render that graph for common
368
368
  viewers: `toDotGraph`, `toMermaidGraph`, `toCytoscapeGraph`, and `toReactFlowGraph`.
369
369
 
370
+ `container.explain(token, options)` answers why a request selects the binding it does, without instantiating anything:
371
+ every candidate each registry offered and what selection made of it, the rule that decided, and the binding it ends on.
372
+ It evaluates `when()` predicates the way `resolve` does, and `options.ancestors` explains a request made from inside
373
+ other resolutions, so a predicate that reads the parent sees the one it would:
374
+
375
+ ```ts
376
+ const explanation = container.explain(Logger, { ancestors: [Settlement] });
377
+
378
+ explanation.selected?.id; // the binding `resolve` would answer with
379
+ explanation.steps[0]?.rule; // "sole-predicate": the only candidate with a `when()` beat the rest
380
+ ```
381
+
370
382
  ## Modules
371
383
 
372
384
  A module is a reusable, stateless bundle of related bindings. Group them once, then load them into any container.
@@ -465,18 +477,20 @@ Every error extends `DiError` and carries a stable `code`, so you can branch on
465
477
 
466
478
  The full taxonomy — including `MissingMetadataError`, `InvalidMetadataError`, `RebindUnboundTokenError`,
467
479
  `AsyncModuleLoadError`, `InvalidBindingDeclarationError`, and the rest — is exported from the root entry and from
468
- `@codefast/di/errors/errors`.
480
+ `@codefast/di/errors`.
469
481
 
470
482
  ## Subpath exports
471
483
 
472
484
  Prefer the root entry: it re-exports the whole public API. Reach for a subpath only to trim a bundle, or to pull in
473
485
  something the root doesn't surface.
474
486
 
475
- Every module is also published as a subpath that mirrors the source layout: the model under `@codefast/di/core/*`
476
- (`core/token`, `core/tag`, `core/module`, …), errors under `errors/*`, the runtime under `container/*`, `injection/*`,
477
- `lifecycle/*`, and `resolution/*` (for example `@codefast/di/resolution/select/constraints`), and decorators and
478
- metadata under `decorators/*` and `metadata/*`. Introspection ships at flat specifiers: `@codefast/di/inspector`,
479
- `@codefast/di/dependency-graph`, and `@codefast/di/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
487
+ Every module is also published as a subpath that is its source path, with no exception: the model under
488
+ `@codefast/di/core/*` (`core/token`, `core/tag`, `core/module`, …), the error taxonomy at `@codefast/di/errors`, the
489
+ runtime under `container/*`, `injection/*`, `lifecycle/*`, and `resolution/*` (for example
490
+ `@codefast/di/resolution/select/constraints`), decorators and metadata under `decorators/*` and `metadata/*`, and
491
+ introspection under `introspection/*`: `@codefast/di/introspection/inspector`, `@codefast/di/introspection/explanation`,
492
+ `@codefast/di/introspection/dependency-graph`, and
493
+ `@codefast/di/introspection/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
480
494
 
481
495
  ## Benchmarks
482
496
 
@@ -487,7 +501,7 @@ alongside the method that produced them. Run it yourself rather than taking any
487
501
  ## Documentation
488
502
 
489
503
  - [Rendered docs on codefastlabs.com](https://codefastlabs.com/docs/di)
490
- - [`SPEC.md`](./SPEC.md) — the behavioural contract: public API, semantics, and errors.
504
+ - [`spec/`](./spec/README.md) — the behavioural contract: public API, semantics, and errors, one document per API area.
491
505
  - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — the internal shape and the invariants the hot paths depend on.
492
506
  - [`DECISIONS.md`](./DECISIONS.md) — why the API looks like this, and what it did not take from InversifyJS v8.
493
507
  - [`examples/`](./examples/README.md) — runnable examples from basic tokens to a multi-file Ports & Adapters app.
@@ -3,17 +3,18 @@
3
3
  *
4
4
  * @remarks One object plays every role: each builder step, the record the registry stores, and the
5
5
  * binding the resolver reads. The binding fields are declared first and in a fixed order, so every
6
- * binding in the process shares one hidden class; the return type of each step is what pins the
7
- * chain's order, so no runtime check has to.
6
+ * binding in the process shares one hidden class; the return type of each step pins the chain's
7
+ * order, slot before strategy, so the binding registers once and is never re-slotted.
8
8
  */
9
- import type { AliasBindingBuilder, Binding, BindingBuilder, BindingSlot, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding";
9
+ import type { Binding, BindingSlot } from "#core/binding";
10
+ import type { AliasBindingBuilder, BindingBuilder, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding-builders";
10
11
  import type { DeclaredBinding } from "#core/binding-declaration";
11
12
  import type { BindingRegistry } from "#core/registry";
12
13
  import type { BindingTag } from "#core/tag";
13
14
  import type { Token } from "#core/token";
14
15
  import type { ActivationHandler, BindingConstraint, BindingIdentifier, BindingKind, BindingScope, Constructor, DeactivationHandler, ResolutionContext, ResolutionFrame } from "#core/types";
15
16
  import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#injection/descriptor";
16
- import type { ScopeManager } from "#lifecycle/scope-manager";
17
+ import type { ScopeManager } from "#lifecycle/scopes";
17
18
  /**
18
19
  * Where a chain registers, and on whose behalf.
19
20
  *
@@ -26,24 +27,22 @@ export interface BindingRegistration {
26
27
  readonly registry: BindingRegistry;
27
28
  readonly scope: ScopeManager;
28
29
  readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
29
- /** Runs for a binding this registration displaces, instead of parking it for a later restore. */
30
+ /** Runs for a binding this registration displaces, deactivating it on the spot. */
30
31
  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
+ /** Notes a displaced binding so the container can still tear it down, when nothing deactivates it on the spot. */
32
33
  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;
35
34
  }
36
35
  /**
37
36
  * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
38
37
  *
39
38
  * @remarks The binding fields come first, in the order every binding shares, and the chain's own
40
- * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
41
- * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
42
- * or hook in place, a slot or predicate through the registry so its indexes follow.
39
+ * bookkeeping follows as a private field. The slot steps write the fields before anything registers;
40
+ * `to*()` fills the strategy in place and hands this object to the registry once, so a bind is one
41
+ * allocation and one add. A scope or hook afterwards writes the registered object in place.
43
42
  *
44
43
  * @since 0.5.0-canary.8
45
44
  */
46
- export declare class BindingChain<Value, Names extends string = string> implements AliasBindingBuilder<Names>, BindingBuilder<Value, Names>, BindToBuilder<Value, Names>, ConstantBindingBuilder<Value, Names>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
45
+ export declare class BindingChain<Value, Names extends string = string> implements AliasBindingBuilder, BindingBuilder<Value>, BindToBuilder<Value, Names>, ConstantBindingBuilder<Value>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
47
46
  #private;
48
47
  kind: BindingKind;
49
48
  readonly identifier: BindingIdentifier;
@@ -51,8 +50,8 @@ export declare class BindingChain<Value, Names extends string = string> implemen
51
50
  frame: ResolutionFrame | undefined;
52
51
  rootContext: ResolutionContext | undefined;
53
52
  activationStamp: number;
54
- registrationOrder: number;
55
53
  instance: unknown;
54
+ scopedCacheKey: BindingIdentifier;
56
55
  readonly token: Token<Value, Names> | Constructor<Value>;
57
56
  slot: BindingSlot;
58
57
  predicate: BindingConstraint | undefined;
@@ -65,26 +64,20 @@ export declare class BindingChain<Value, Names extends string = string> implemen
65
64
  activationHook: ActivationHandler<Value> | undefined;
66
65
  deactivationHook: DeactivationHandler<Value> | undefined;
67
66
  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
- */
67
+ /** Registers a declared module's bindings in list order, each a chain of its own, handled as `to*()` handles one. */
75
68
  static registerDeclared(declarations: ReadonlyArray<DeclaredBinding>, registration: BindingRegistration): void;
76
- to(type: Constructor<Value>): BindingBuilder<Value, Names>;
77
- toSelf(): BindingBuilder<Value, Names>;
78
- toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
79
- toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
80
- toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
69
+ to(type: Constructor<Value>): BindingBuilder<Value>;
70
+ toSelf(): BindingBuilder<Value>;
71
+ toConstantValue(value: Value): ConstantBindingBuilder<Value>;
72
+ toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
73
+ toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
81
74
  toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
82
75
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
83
- }) => Value, deps: Deps): BindingBuilder<Value, Names>;
76
+ }) => Value, deps: Deps): BindingBuilder<Value>;
84
77
  toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
85
78
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
86
- }) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
87
- toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
79
+ }) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
80
+ toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
88
81
  when(predicate: BindingConstraint): this;
89
82
  whenNamed(name: Names): this;
90
83
  whenTagged(criterion: BindingTag): this;
@@ -1,17 +1,17 @@
1
- import { clearBindingFrame, NO_ACTIVATION_STAMP, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, UNREGISTERED_ORDER, withSlotCriterion, } from "#core/binding";
1
+ import { clearBindingFrame, NO_ACTIVATION_STAMP, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, 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
- import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#errors/errors";
5
+ import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#errors";
6
6
  import { normalizeToDescriptor } from "#injection/descriptor";
7
7
  // ── BindingChain ─────────────────────────────────────────────────────────────────────────────────────────────────────
8
8
  /**
9
9
  * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
10
10
  *
11
11
  * @remarks The binding fields come first, in the order every binding shares, and the chain's own
12
- * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
13
- * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
14
- * or hook in place, a slot or predicate through the registry so its indexes follow.
12
+ * bookkeeping follows as a private field. The slot steps write the fields before anything registers;
13
+ * `to*()` fills the strategy in place and hands this object to the registry once, so a bind is one
14
+ * allocation and one add. A scope or hook afterwards writes the registered object in place.
15
15
  *
16
16
  * @since 0.5.0-canary.8
17
17
  */
@@ -23,8 +23,8 @@ export class BindingChain {
23
23
  frame = undefined;
24
24
  rootContext = undefined;
25
25
  activationStamp = NO_ACTIVATION_STAMP;
26
- registrationOrder = UNREGISTERED_ORDER;
27
26
  instance = NO_INSTANCE;
27
+ scopedCacheKey = this.identifier;
28
28
  token;
29
29
  slot = DEFAULT_BINDING_SLOT;
30
30
  predicate = undefined;
@@ -38,10 +38,6 @@ export class BindingChain {
38
38
  deactivationHook = undefined;
39
39
  // The chain. Set by the first `to*()`, which is the only one a chain accepts.
40
40
  #isRegistered = false;
41
- // Allocated only by a chain that actually displaces something — most never do.
42
- #displacedByChain;
43
- // Registry version after this chain's last write — a mismatch means someone else wrote in between.
44
- #versionAfterLastWrite = -1;
45
41
  #registration;
46
42
  constructor(token, registration) {
47
43
  this.token = token;
@@ -51,13 +47,13 @@ export class BindingChain {
51
47
  get #binding() {
52
48
  return this;
53
49
  }
54
- /** Loud failure for a refinement before `to*()`. */
50
+ /** Loud failure for a scope, a hook or `id()` before `to*()`. */
55
51
  #requireRegistered() {
56
52
  if (!this.#isRegistered) {
57
53
  throw new ChainNotRegisteredError(tokenName(this.token));
58
54
  }
59
55
  }
60
- /** Loud failure for a second `to*()`, raised before the first one's fields can be overwritten. */
56
+ /** Loud failure for a slot step or a second `to*()` once registered, raised before a field is overwritten. */
61
57
  #requireUnregistered() {
62
58
  if (this.#isRegistered) {
63
59
  throw new ChainAlreadyRegisteredError(tokenName(this.token));
@@ -67,32 +63,22 @@ export class BindingChain {
67
63
  this.kind = kind;
68
64
  this.scope = scope;
69
65
  this.#isRegistered = true;
70
- // A fresh registration parks nothing yet and restores nothing: one add, then the version.
71
66
  const registration = this.#registration;
72
- const registry = registration.registry;
73
- const displaced = registry.add(this.#binding);
67
+ const displaced = registration.registry.add(this.#binding);
74
68
  if (displaced !== undefined) {
75
69
  if (registration.deactivateDisplaced !== undefined) {
76
70
  registration.deactivateDisplaced(displaced);
77
71
  }
78
72
  else {
79
- this.#displacedByChain = [displaced];
80
73
  registration.onDisplaced?.(displaced);
81
74
  }
82
75
  }
83
76
  if (registration.moduleBindingIds !== undefined) {
84
77
  registration.moduleBindingIds.push(this.identifier);
85
78
  }
86
- this.#versionAfterLastWrite = registry.version;
87
79
  return this;
88
80
  }
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
- */
81
+ /** Registers a declared module's bindings in list order, each a chain of its own, handled as `to*()` handles one. */
96
82
  static registerDeclared(declarations, registration) {
97
83
  const { registry, moduleBindingIds, deactivateDisplaced, onDisplaced } = registration;
98
84
  for (let index = 0; index < declarations.length; index += 1) {
@@ -177,102 +163,67 @@ export class BindingChain {
177
163
  this.target = target;
178
164
  return this.#register("alias", "transient");
179
165
  }
180
- // ── Refinement ─────────────────────────────────────────────────────────────────────────────────────────────────────
181
- /**
182
- * Whether this chain provably still owns the registry's last write with nothing parked.
183
- *
184
- * @remarks When it does, a refinement rewrites the live binding in place; otherwise `#reslot`
185
- * re-checks liveness and restores what the new shape frees.
186
- */
187
- get #isProvablyLive() {
188
- return this.#registration.registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined;
189
- }
190
- /** Stamps this chain's last-write version after a mutation it made in place. */
191
- #recordWrite() {
192
- this.#versionAfterLastWrite = this.#registration.registry.version;
193
- }
194
- /** Bumps the registry for an index-neutral mutation (scope, hook) and records this chain's write. */
195
- #touchAndRecordWrite() {
196
- this.#registration.registry.touch();
197
- this.#recordWrite();
198
- }
199
- // Slot and predicate are what the registry indexes on, so a re-slot takes the binding out of the
200
- // registry, rewrites the two fields while it is out, and registers it again — same object, same id.
201
- #reslot(slot, predicate) {
202
- this.#requireRegistered();
203
- this.#commit(() => {
204
- this.slot = slot;
205
- this.predicate = predicate;
206
- // The frame reports the slot, so a resolve before this call memoized the previous one.
207
- clearBindingFrame(this.#binding);
208
- });
209
- return this;
210
- }
211
- #withScope(scope) {
212
- this.#requireRegistered();
213
- if (this.scope !== scope) {
214
- // An instance cached under the old scope must not survive the change — a later flip back
215
- // to that scope would resurrect it.
216
- this.#registration.scope.deleteSingleton(this.#binding);
217
- this.#registration.scope.deleteScoped(this.identifier);
218
- this.scope = scope;
219
- }
220
- // The frame reports the scope, so a resolve before this call memoized the previous one.
221
- clearBindingFrame(this.#binding);
222
- this.#touchAndRecordWrite();
223
- return this;
224
- }
225
- // SPEC calls a candidate a binding that passes *all* of a chain's predicates, and the chain type
226
- // reads as refinement, so a second `when()` narrows rather than replaces.
166
+ // ── Slot ───────────────────────────────────────────────────────────────────────────────────────────────────────────
167
+ // SPEC calls a candidate a binding that passes *all* of a chain's predicates, so a second `when()`
168
+ // narrows rather than replaces.
227
169
  when(predicate) {
228
- this.#requireRegistered();
170
+ this.#requireUnregistered();
229
171
  const previous = this.predicate;
230
172
  // The composite carries both sides' requirements, so validate() still sees them.
231
- const narrowed = previous === undefined
232
- ? predicate
233
- : mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
234
- // The slot is unchanged, so a provably-live binding just takes the predicate in place.
235
- if (this.#isProvablyLive) {
236
- this.#registration.registry.setPredicate(this.#binding, narrowed);
237
- this.#recordWrite();
238
- return this;
239
- }
240
- return this.#reslot(this.slot, narrowed);
173
+ this.predicate =
174
+ previous === undefined
175
+ ? predicate
176
+ : mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
177
+ return this;
241
178
  }
242
179
  whenNamed(name) {
243
180
  return this.whenTagged(slotName.of(name));
244
181
  }
245
182
  whenTagged(criterion) {
246
- this.#requireRegistered();
183
+ this.#requireUnregistered();
247
184
  if (this.isMany) {
248
185
  throw new ManyBindingSlotError(tokenName(this.token));
249
186
  }
250
- return this.#reslot(withSlotCriterion(this.slot, criterion), this.predicate);
187
+ this.slot = withSlotCriterion(this.slot, criterion);
188
+ return this;
251
189
  }
252
190
  many() {
253
- this.#requireRegistered();
191
+ this.#requireUnregistered();
254
192
  if (this.slot.tags.length !== 0) {
255
193
  throw new ManyBindingSlotError(tokenName(this.token));
256
194
  }
257
- if (this.isMany) {
258
- return this;
259
- }
260
- // A provably-live binding takes membership in place; the registry only moves it out of the lone
261
- // map. Otherwise `#commit` re-checks liveness and restores what the freed slot lets back in.
262
- if (this.#isProvablyLive) {
263
- this.#registration.registry.setMany(this.#binding);
264
- this.#recordWrite();
265
- return this;
266
- }
267
- this.#commit(() => {
268
- this.isMany = true;
269
- });
195
+ this.isMany = true;
270
196
  return this;
271
197
  }
272
198
  whenDefault() {
273
- // The default slot is what a fresh registration already has, so there is nothing to re-slot —
274
- // but an unregistered chain must fail here exactly as it does in every other refinement.
199
+ // The default slot is what a fresh chain already has; the check keeps it a slot step like the others.
200
+ this.#requireUnregistered();
201
+ return this;
202
+ }
203
+ // ── Scope and lifecycle ────────────────────────────────────────────────────────────────────────────────────────────
204
+ #withScope(scope) {
275
205
  this.#requireRegistered();
206
+ // Nothing a cache, a frame or the version answers for depends on a scope that did not change.
207
+ if (this.scope === scope) {
208
+ return this;
209
+ }
210
+ // An instance cached under the old scope must not survive the change — a later flip back to that
211
+ // scope would resurrect it. A child's scoped cache is out of reach here, so leaving `scoped` retires
212
+ // the key every child filed its instance under.
213
+ const registration = this.#registration;
214
+ if (this.instance !== NO_INSTANCE) {
215
+ registration.scope.deleteSingleton(this.#binding);
216
+ }
217
+ if (this.scope === "scoped") {
218
+ registration.scope.deleteScoped(this.scopedCacheKey);
219
+ this.scopedCacheKey = generateBindingId();
220
+ }
221
+ this.scope = scope;
222
+ // The frame reports the scope, so a resolve before this call memoized the previous one.
223
+ if (this.frame !== undefined || this.rootContext !== undefined) {
224
+ clearBindingFrame(this.#binding);
225
+ }
226
+ registration.registry.touch();
276
227
  return this;
277
228
  }
278
229
  singleton() {
@@ -287,78 +238,17 @@ export class BindingChain {
287
238
  onActivation(fn) {
288
239
  this.#requireRegistered();
289
240
  this.activationHook = fn;
290
- this.#touchAndRecordWrite();
241
+ this.#registration.registry.touch();
291
242
  return this;
292
243
  }
293
244
  onDeactivation(fn) {
294
245
  this.#requireRegistered();
295
246
  this.deactivationHook = fn;
296
- this.#touchAndRecordWrite();
247
+ this.#registration.registry.touch();
297
248
  return this;
298
249
  }
299
250
  id() {
300
251
  this.#requireRegistered();
301
252
  return this.identifier;
302
253
  }
303
- // ── Registry ───────────────────────────────────────────────────────────────────────────────────────────────────────
304
- /**
305
- * Registers this binding; with `rewrite` given, first takes the live binding out, rewrites it, and
306
- * registers it again.
307
- *
308
- * @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
309
- * the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
310
- * chain settles, then get restored.
311
- */
312
- #commit(rewrite) {
313
- const { registry, moduleBindingIds } = this.#registration;
314
- const registered = this.#binding;
315
- if (rewrite !== undefined) {
316
- // A registry someone else wrote since this chain's last write invalidates the parked
317
- // snapshot: restoring it could undo an unbind or shadow a newer binding.
318
- if (registry.version !== this.#versionAfterLastWrite) {
319
- this.#displacedByChain = undefined;
320
- }
321
- if (!registry.reslot(registered, rewrite)) {
322
- // The chain's binding is no longer live (unbound or displaced) — a refinement must not
323
- // resurrect it, so the chain goes inert against the registry.
324
- this.#displacedByChain = undefined;
325
- this.#versionAfterLastWrite = registry.version;
326
- return;
327
- }
328
- }
329
- else {
330
- // Each `to*()` starts its own registration, so anything a previous one displaced is not this
331
- // registration's to restore.
332
- this.#displacedByChain = undefined;
333
- }
334
- const displaced = registry.add(registered);
335
- if (displaced !== undefined) {
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
- }
343
- }
344
- if (rewrite !== undefined && this.#displacedByChain !== undefined) {
345
- this.#restoreNonConflicting(this.#displacedByChain);
346
- }
347
- if (rewrite === undefined && moduleBindingIds !== undefined) {
348
- moduleBindingIds.push(this.identifier);
349
- }
350
- this.#versionAfterLastWrite = registry.version;
351
- }
352
- #restoreNonConflicting(displaced) {
353
- for (let index = displaced.length - 1; index >= 0; index -= 1) {
354
- const candidate = displaced[index];
355
- // A restore must never displace: a slot that has been re-occupied keeps its occupant, and
356
- // the candidate stays parked.
357
- if (!this.#registration.registry.hasSlotOccupant(candidate)) {
358
- this.#registration.registry.add(candidate);
359
- this.#registration.onRestored?.(candidate);
360
- displaced.splice(index, 1);
361
- }
362
- }
363
- }
364
254
  }