@codefast/di 0.8.1 → 0.10.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 (56) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/LICENSE +1 -1
  3. package/README.md +29 -20
  4. package/dist/ambient/active-container.d.ts +13 -0
  5. package/dist/ambient/active-container.js +24 -0
  6. package/dist/container/binding-builders.d.ts +42 -20
  7. package/dist/container/binding-builders.js +155 -87
  8. package/dist/container/container.d.ts +10 -10
  9. package/dist/container/container.js +55 -23
  10. package/dist/core/binding.d.ts +55 -57
  11. package/dist/core/binding.js +11 -37
  12. package/dist/core/constraint-requirement.d.ts +19 -4
  13. package/dist/core/constraint-requirement.js +19 -9
  14. package/dist/core/registry.d.ts +47 -4
  15. package/dist/core/registry.js +361 -159
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/tag.d.ts +2 -2
  19. package/dist/core/tag.js +2 -2
  20. package/dist/core/token.d.ts +24 -4
  21. package/dist/core/token.js +1 -1
  22. package/dist/core/types.d.ts +15 -6
  23. package/dist/decorators/inject.d.ts +1 -1
  24. package/dist/decorators/inject.js +6 -4
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +33 -1
  27. package/dist/errors/errors.js +44 -3
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +1 -1
  30. package/dist/injection/descriptor.d.ts +16 -8
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +6 -0
  33. package/dist/injection/resolve-options.js +16 -0
  34. package/dist/introspection/dependency-graph.js +10 -5
  35. package/dist/introspection/inspector.d.ts +3 -1
  36. package/dist/introspection/inspector.js +10 -24
  37. package/dist/lifecycle/lifecycle-manager.js +10 -8
  38. package/dist/lifecycle/scope-manager.js +1 -1
  39. package/dist/metadata/metadata-reader-token.js +1 -1
  40. package/dist/resolution/cache/activation-need.d.ts +2 -0
  41. package/dist/resolution/cache/activation-need.js +13 -6
  42. package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
  43. package/dist/resolution/cache/binding-lookup-cache.js +96 -12
  44. package/dist/resolution/cache/class-introspector.d.ts +1 -1
  45. package/dist/resolution/cache/class-introspector.js +28 -14
  46. package/dist/resolution/context.d.ts +8 -8
  47. package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
  48. package/dist/resolution/plan/instantiation-plan.js +156 -65
  49. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  50. package/dist/resolution/plan/plan-codegen.js +185 -0
  51. package/dist/resolution/resolver.d.ts +14 -4
  52. package/dist/resolution/resolver.js +375 -134
  53. package/dist/resolution/select/binding-select.js +12 -7
  54. package/dist/resolution/select/constraints.d.ts +7 -4
  55. package/dist/resolution/select/constraints.js +34 -13
  56. package/package.json +14 -40
package/CHANGELOG.md CHANGED
@@ -1,5 +1,169 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`3621384`](https://github.com/codefastlabs/codefast/commit/36213842e7e64284dfb0c5b0d651368debf20a13) Thanks [@thevuong](https://github.com/thevuong)! - `BindingIdentifier` is a branded number, minted from a process-wide counter, instead of a branded string. The id stays
8
+ opaque — obtained from `.id()`, handed back to `unbind(id)`, reported by `inspect()` and the dependency graph — and a
9
+ plain bind no longer allocates a string for it. Code that treated the id as a string (interpolating, parsing or storing
10
+ it as text) must treat it as a number.
11
+
12
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`67c7eb4`](https://github.com/codefastlabs/codefast/commit/67c7eb4a63cee26c8b3d4954965a5fdacbfe3533) Thanks [@thevuong](https://github.com/thevuong)! - The fluent chain `bind()` returns is now the binding it registers: one object is every builder step, the registry's
13
+ record and what the resolver reads, so a plain bind is one allocation instead of three and a copy of every field. Two
14
+ names on the `Binding` shapes change to make room for the builder's methods — `id` is `identifier`, and the hook fields
15
+ are `activationHook` / `deactivationHook` — and a chain registers exactly once: a second `to*()` throws
16
+ `ChainAlreadyRegisteredError` instead of minting a second binding.
17
+
18
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`7f4efc2`](https://github.com/codefastlabs/codefast/commit/7f4efc2faf96ffdd4279241c9bc83aba53bc64af) Thanks [@thevuong](https://github.com/thevuong)! - `resolveAll`, `resolveAllAsync` and the value an `injectAll()` dependency delivers are `ReadonlyArray`s, and a
19
+ root-level, options-less `resolveAll` hands out the engine's own list — the same array on every call while no registry
20
+ in the chain has changed — instead of a copy per read. That list is kept while every member is a hook-free constant or a
21
+ hook-free singleton whose instance is cached, so a collection of singleton handlers is one lookup per read. Callers that
22
+ mutated the returned array spread it first; a constructor parameter typed `Array<T>` for an `injectAll` dependency
23
+ becomes `ReadonlyArray<T>`.
24
+
25
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`6980f8e`](https://github.com/codefastlabs/codefast/commit/6980f8ee5b804b17946ac959a9581f207e7ad008) Thanks [@thevuong](https://github.com/thevuong)! - `many()` marks a binding as a collection member: several members of one token coexist on the default slot, `resolveAll`
26
+ returns every member, a single `resolve` never selects one, and membership replaces slot last-wins. A member keeps the
27
+ default slot (`ManyBindingSlotError` otherwise) and may carry `when()` predicates. It is the intended form of a strategy
28
+ set, where a predicate that always passes used to stand in — and it is what lets a root-level collection of constants be
29
+ served from its memo without evaluating anything. `BindingSnapshot` gains `isMany`.
30
+
31
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`a56390f`](https://github.com/codefastlabs/codefast/commit/a56390f79bb62e6e33ecb79cbd158ad1349d7bee) Thanks [@thevuong](https://github.com/thevuong)! - A root-level `resolveAll` with no options memoizes its candidate list until any registry in the chain changes, and its
32
+ value list while every member is a hook-free constant with no activation hook anywhere in the chain. The contract
33
+ already required `when()` predicates to be pure; the engine now relies on it there, evaluating a predicate once per
34
+ container state for that read instead of on every call. Reads carrying options, or made from inside a factory, are
35
+ unchanged.
36
+
37
+ ### Patch Changes
38
+
39
+ - [#863](https://github.com/codefastlabs/codefast/pull/863) [`ccc5ed1`](https://github.com/codefastlabs/codefast/commit/ccc5ed193f15d9edc7c3e6475a6bfda22e2c6319) Thanks [@thevuong](https://github.com/thevuong)! - Rewrite `benchmarks/di/RESULTS.md` as a single machine-derived snapshot on `@codefast/di` 0.9.0 (full profile,
40
+ GC-exposed, interleaved) instead of an accreted dated ledger, weighting wins and losses equally so the page shows where
41
+ the engine is slower. The snapshot records that `@codefast/di` loses the aggregate to ditox (0.79× median) and the
42
+ geomean to injection-js (0.56×), and breaks down each loss as a real deficit or a by-design work difference — the
43
+ `resolve-all-strategies` collapse (0.03× at N=100) is the rivals returning a memoized collection where `@codefast/di`
44
+ re-gathers per op.
45
+
46
+ Assert the A/B method in `BENCH_GUIDE.md`: because `run.ts` rebuilds `packages/di/dist` from `src` unconditionally
47
+ before spawning, swapping the source is the one method, and swapping `dist` directly is a demoted escape hatch that must
48
+ use the child entries. Repoint the `packages/di/ARCHITECTURE.md` tag-chain-walk-memo reference from `RESULTS.md` to the
49
+ package `CHANGELOG.md`, where that A/B now lives.
50
+
51
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`85adfd0`](https://github.com/codefastlabs/codefast/commit/85adfd004920f99ec8552745b7726e03e971cbb2) Thanks [@thevuong](https://github.com/thevuong)! - Construct an accessor-injected class without the two per-instantiation allocations it used to pay: the ambient
52
+ resolution handed to its accessors is built once per resolver for the lent root stack, and the construction itself runs
53
+ inside the ambient scope directly instead of through a wrapping closure.
54
+
55
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`d541dee`](https://github.com/codefastlabs/codefast/commit/d541dee97719a3151dd4daa9675846cb11aac82b) Thanks [@thevuong](https://github.com/thevuong)! - Compile an accessor-injected class as a plan root instead of declining the plan: its constructor parameters compile as
56
+ usual and construction runs through the host, which puts the class's frame on the resolution path and the container
57
+ ambient before constructing, so an accessor that cycles back is still reported as a `CircularDependencyError`. Below a
58
+ plan's root such a class stays an escape.
59
+
60
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`793f5d0`](https://github.com/codefastlabs/codefast/commit/793f5d0a13092144b2ca83ec23142e68011cd5ca) Thanks [@thevuong](https://github.com/thevuong)! - Route every member of a `resolveAllAsync` collection that is a transient factory with no activation and no request
61
+ options through the same non-`async` lane a single `resolveAsync` already takes, so a fan-out costs one factory promise
62
+ per member instead of an async state machine on top of each.
63
+
64
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`7a00e30`](https://github.com/codefastlabs/codefast/commit/7a00e306de98a829385e3238095333f094a843f7) Thanks [@thevuong](https://github.com/thevuong)! - Build collections in linear time. A token's binding list now appends in place and is replaced only on removal or
65
+ displacement, with every selection walk reading its starting length first, so a predicate that binds mid-walk is still
66
+ invisible to that walk; and a bare `when()` rewrites the binding's predicate in place — moving a lone binding into a
67
+ record — instead of re-registering it, whenever the chain owns the registry's last write and has nothing parked. A
68
+ hundred `when()` bindings on one token no longer copy the list a hundred times.
69
+
70
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`3613886`](https://github.com/codefastlabs/codefast/commit/36138863480e0fa48ca522ad56fa1ee98a528307) Thanks [@thevuong](https://github.com/thevuong)! - Defer every per-container allocation a resolve can happen without. The resolver builds its plan compiler and plan maps
71
+ on the first plan request, the lookup cache allocates its memo maps only once a second distinct token or tag appears in
72
+ one cache generation, the activation-need memo is allocated by the first answer its early returns cannot give, and the
73
+ registry allocates its record map on the first bind. A per-request child that is created, asked one parent-owned token
74
+ and disposed now allocates none of them; the registry's fast-default map stays eager so the first read of every
75
+ synchronous resolve is still a bare `Map.get`.
76
+
77
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`b823fc8`](https://github.com/codefastlabs/codefast/commit/b823fc89c09fb200d0d9262f16f34f32cd27a63e) Thanks [@thevuong](https://github.com/thevuong)! - Price the two lanes the full pass showed paying for the rewrite: a request for an unbound or record-less token reads the
78
+ record map alone once its lone-map probe has missed, instead of probing the lone map again, and a root container's
79
+ chain-summed versions are read as its own version instead of through the epoch memo, so a `resolveAll` over a root no
80
+ longer pays a compare and a stamp per candidate.
81
+
82
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`5fbc3de`](https://github.com/codefastlabs/codefast/commit/5fbc3dea835987f8eb857529a5a6b1293ac523b6) Thanks [@thevuong](https://github.com/thevuong)! - A request carrying a name and one tag is answered from a memoized lookup of the exact two-criterion slot, in either
83
+ declaration order, instead of a scan of the token's bindings; a predicate on that binding still runs, and a name no
84
+ binding has declared is a miss at once.
85
+
86
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`b46c0cd`](https://github.com/codefastlabs/codefast/commit/b46c0cd9473a662f294231796c4be2a206c83033) Thanks [@thevuong](https://github.com/thevuong)! - `resolveOptional` with no options answers a lone default binding in its own registry on the lane a plain `resolve`
87
+ takes, and a root that keeps no records answers a miss without the selection walk; a generated `toResolved` plan checks
88
+ its factory's result for a promise inline instead of through a call.
89
+
90
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`0506f04`](https://github.com/codefastlabs/codefast/commit/0506f046e9f797086708c186f14f96f13026a8bd) Thanks [@thevuong](https://github.com/thevuong)! - A compiled instantiation plan that keeps running — sync or async — is generated as a function of its own through the
91
+ `Function` constructor and takes the closure's place, so its call sites carry feedback for one plan only instead of for
92
+ every plan the process has compiled; below the threshold a plan stays a closure, so a cold container or a per-request
93
+ child never compiles one. A runtime whose Content Security Policy refuses the constructor keeps every plan a closure and
94
+ behaves identically. `RESOLUTION_DIAGNOSTICS` reports `generatedPlanCount`.
95
+
96
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`e3e9257`](https://github.com/codefastlabs/codefast/commit/e3e92571dc64f8eb48006b3310d4334ea61cc149) Thanks [@thevuong](https://github.com/thevuong)! - Keep the common token — one default-slot binding, no predicate — in the registry's fast-default map alone, and give a
97
+ record (binding list plus tagged indexes) only to a token that carries a second binding, a tagged slot or a predicate,
98
+ moving it back when the record shrinks to the default slot. A plain bind is now one map write and one binding object;
99
+ `getFastDefault()` is unchanged, a bare `Map.get` on that map, and `has()`/`hasOwn()` without criteria answer from a
100
+ registry presence probe instead of materialising the token's list. `RESOLUTION_DIAGNOSTICS` reports the record map under
101
+ `registry.records`.
102
+
103
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`414d495`](https://github.com/codefastlabs/codefast/commit/414d495c6db58106e64ba5f74880e12bd85c2459) Thanks [@thevuong](https://github.com/thevuong)! - The binding registry keeps one record per token — its list and its two tagged indexes — in a single map, and builds the
104
+ binding-id index only on the first id-keyed operation. A bind into a fresh token is one record and one map write where
105
+ it was three map writes; `getFastDefault()` keeps its own map so the warm resolve lane still answers from one bare
106
+ `Map.get`. Measured paired and alternating against the previous layout: the bind path runs at roughly 1.6× on the
107
+ 128-binding registration row and the warm resolve rows hold at parity.
108
+
109
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`3db3447`](https://github.com/codefastlabs/codefast/commit/3db3447bfe87b4aaf5f48113cc91b19537eefeb0) Thanks [@thevuong](https://github.com/thevuong)! - Register a binding into a token's record in constant time. Last-wins displacement asked the record's list who occupied
110
+ the incoming binding's slot, walking every binding already on the token; the default slot now has its own index entry
111
+ beside the two tagged ones, so an add finds and displaces exactly its occupant without the walk. A token that grows a
112
+ large collection member by member — the fan-out cold path — was quadratic to build and is now linear: per-bind cost
113
+ stays flat instead of climbing with the collection size, and the `resolve-all-cold` rows move from a loss to a win
114
+ against the decorator-free rivals. The slot index is dropped in the same step a binding leaves its slot — `many()`
115
+ turning it into a member, a predicate making it predicate-only — so a later add never displaces a binding that has
116
+ already moved on.
117
+
118
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`8bb19e3`](https://github.com/codefastlabs/codefast/commit/8bb19e3ab11c4176f307765321e431cfa94d37bb) Thanks [@thevuong](https://github.com/thevuong)! - Answer a request the slot indexes decline with an allocation-free first pass over the token's candidates: one slot match
119
+ with no predicate is returned outright and no match is a clean miss, without building a constraint context, a display
120
+ name or a candidate array. A second match, or a predicate on a match, still goes through full selection, so specificity
121
+ and ambiguity are decided exactly as before.
122
+
123
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`5e57752`](https://github.com/codefastlabs/codefast/commit/5e57752b2fcad8a1c44e5bff61d5f91f7cdb27da) Thanks [@thevuong](https://github.com/thevuong)! - A container's class-metadata caches are keyed by the metadata reader and shared by every container that reads through
124
+ it, so a child inheriting its parent's reader resolves a class the parent already met without reading its metadata
125
+ again; the activation-need cache is built by the first interpreted resolve that asks for it instead of with the
126
+ container.
127
+
128
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`bee69af`](https://github.com/codefastlabs/codefast/commit/bee69af3688a67234ecd113159ca7e767e14699a) Thanks [@thevuong](https://github.com/thevuong)! - Memoize the lookup cache's chain-summed registry version against a process-wide state epoch that every registry mutation
129
+ and activation-hook registration advances, and let a root container read both chain sums as its own version. A resolve
130
+ from a deep child no longer walks its whole ancestor chain on every lookup while nothing has changed; the first read
131
+ after any change re-sums exactly as before. The activation sum stays a walk: the memo that would serve it costs fields
132
+ on the resolver that the warm transient class lane pays for.
133
+
134
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`f78726e`](https://github.com/codefastlabs/codefast/commit/f78726e00b9c388ceb03f7fad116593d7b752a94) Thanks [@thevuong](https://github.com/thevuong)! - A request carrying one criterion that the token's tagged index does not hold is answered as a miss for that container
135
+ without scanning its bindings, and walks to the parent directly.
136
+
137
+ - [#866](https://github.com/codefastlabs/codefast/pull/866) [`5536e62`](https://github.com/codefastlabs/codefast/commit/5536e6208ab58955fb551866c3b9703e7f2c55e0) Thanks [@thevuong](https://github.com/thevuong)! - Allocate the deactivation-pair list only when an unbind, rebind or teardown actually owes a deactivation; the hot-swap
138
+ shape, a binding nothing ever cached, hands back a shared empty list instead.
139
+
140
+ ## 0.9.0
141
+
142
+ ### Minor Changes
143
+
144
+ - [#847](https://github.com/codefastlabs/codefast/pull/847) [`d0b794c`](https://github.com/codefastlabs/codefast/commit/d0b794c047344c4040b5641202c259d72a0ea48c) Thanks [@thevuong](https://github.com/thevuong)! - `Token` gains a second type parameter, `Names extends string = string`, declaring the slot names its bindings may use:
145
+ `token<Logger, "console" | "file">("Logger")`. `whenNamed`, and `name` in `ResolveOptions` and `InjectOptions`, narrow
146
+ to it, so a misspelt name is a compile error and the IDE completes the declared names at every bind and request site.
147
+ `Names` is a covariant phantom that defaults to `string`, so existing tokens, class keys and internal `Token<unknown>`
148
+ lanes are unchanged; a new `SlotNamesOf<Key>` type reads the set back.
149
+
150
+ **Breaking:** `whenParentNamed` and `whenAnyAncestorNamed` now take the parent token first —
151
+ `whenParentNamed(Database, "primary")` — and match only when that frame resolves that token at that slot. A slot name is
152
+ a label on one token's bindings, so the token is part of the question and is what types the name; a label shared across
153
+ tokens is what a tag key is for. `validate()` checks the name on that token's bindings and `UnreachableConstraintError`
154
+ carries the new `requiredTokenName`. The reserved criterion handed to a `…Tagged` helper
155
+ (`whenParentTagged(slotName.of("x"))`) is now validated too, as it is the same bare string.
156
+
157
+ Display names follow one rule everywhere the package speaks — spelled like the TS symbol they stand for, under the
158
+ owner's namespace: `token<Logger>("app:Logger")`, `Module.create("app:Infra", …)`, `tag("app:cacheTier")`. The package's
159
+ own `MetadataReaderToken` now prints as `di:MetadataReader`, beside the reserved `di:name` key. SPEC gains a normative
160
+ "Display names" section stating the rule and its enforcement.
161
+
162
+ ### Patch Changes
163
+
164
+ - [#827](https://github.com/codefastlabs/codefast/pull/827) [`0984174`](https://github.com/codefastlabs/codefast/commit/0984174df148a7cffcd09b837bdde1922f38f24e) Thanks [@thevuong](https://github.com/thevuong)! - `package.json` now carries `homepage` and `bugs`, so npm links the package README and the issue tracker the way the
165
+ other `@codefast/*` packages already do.
166
+
3
167
  ## 0.8.1
4
168
 
5
169
  ### Patch Changes
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 CodeFast Labs
3
+ Copyright (c) 2024 Codefast Labs
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -48,7 +48,7 @@ interface Logger {
48
48
  info(message: string): void;
49
49
  }
50
50
 
51
- const LoggerToken = token<Logger>("Logger");
51
+ const LoggerToken = token<Logger>("app:Logger");
52
52
 
53
53
  @injectable([LoggerToken])
54
54
  class CheckoutService {
@@ -86,7 +86,7 @@ and resolve against. Tokens compare by reference, so declare each one once and r
86
86
  ```ts
87
87
  import { token } from "@codefast/di";
88
88
 
89
- const DbToken = token<Database>("Database");
89
+ const DbToken = token<Database>("app:Database");
90
90
  ```
91
91
 
92
92
  A class constructor works as a key too: `container.bind(UserService).toSelf()`, then `container.resolve(UserService)`.
@@ -170,6 +170,15 @@ container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
170
170
  container.resolve(LoggerToken, { name: "file" }); // → fileLogger
171
171
  ```
172
172
 
173
+ Declare the names on the token and they become checked, completable literals at every bind and request site:
174
+
175
+ ```ts
176
+ const LoggerToken = token<Logger, "console" | "file">("app:Logger");
177
+
178
+ container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
179
+ container.resolve(LoggerToken, { name: "file" }); // { name: "fiel" } is a compile error
180
+ ```
181
+
173
182
  **Tagged — for typed, collision-proof keys.** A criterion is a `[key, value]` pair. Declare the key once with
174
183
  `tag<Value>(name)`, then mint a criterion with `key.of(value)`. The bind site and the resolve site share the same typed
175
184
  key: a key declared `tag<"s3" | "gcs">` refuses any other value, so the two sites can't drift apart.
@@ -177,7 +186,7 @@ key: a key declared `tag<"s3" | "gcs">` refuses any other value, so the two site
177
186
  ```ts
178
187
  import { tag } from "@codefast/di";
179
188
 
180
- const Provider = tag<"s3" | "gcs">("provider");
189
+ const Provider = tag<"s3" | "gcs">("app:provider");
181
190
 
182
191
  container.bind(StorageToken).to(S3Storage).whenTagged(Provider.of("s3"));
183
192
  container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
@@ -202,18 +211,18 @@ container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
202
211
  resolving — pass a predicate to `.when(ctx => boolean)`. It runs at resolve time, after slot matching. These ready-made
203
212
  predicates ship from the root entry:
204
213
 
205
- | Predicate | Matches when |
206
- | ------------------------------------ | ------------------------------------------------------ |
207
- | `whenParentIs(token)` | the direct parent resolves `token` |
208
- | `whenNoParentIs(token)` | there is no parent, or it resolves a different token |
209
- | `whenAnyAncestorIs(token)` | some ancestor resolves `token` |
210
- | `whenNoAncestorIs(token)` | no ancestor resolves `token` |
211
- | `whenParentNamed(name)` | the parent's slot carries that name |
212
- | `whenAnyAncestorNamed(name)` | some ancestor's slot carries that name |
213
- | `whenParentTagged(criterion)` | the parent's slot carries that criterion |
214
- | `whenAnyAncestorTagged(criterion)` | some ancestor's slot carries that criterion |
215
- | `whenParentTaggedAll(criteria)` | the parent's slot carries all criteria in the array |
216
- | `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
214
+ | Predicate | Matches when |
215
+ | ------------------------------------ | ------------------------------------------------------- |
216
+ | `whenParentIs(token)` | the direct parent resolves `token` |
217
+ | `whenNoParentIs(token)` | there is no parent, or it resolves a different token |
218
+ | `whenAnyAncestorIs(token)` | some ancestor resolves `token` |
219
+ | `whenNoAncestorIs(token)` | no ancestor resolves `token` |
220
+ | `whenParentNamed(token, name)` | the parent resolves `token` at the slot named `name` |
221
+ | `whenAnyAncestorNamed(token, name)` | some ancestor resolves `token` at the slot named `name` |
222
+ | `whenParentTagged(criterion)` | the parent's slot carries that criterion |
223
+ | `whenAnyAncestorTagged(criterion)` | some ancestor's slot carries that criterion |
224
+ | `whenParentTaggedAll(criteria)` | the parent's slot carries all criteria in the array |
225
+ | `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
217
226
 
218
227
  For the exact matching and most-specific-wins rules, see [`SPEC.md` → Slots and last-wins](./SPEC.md#slot-matching).
219
228
 
@@ -358,11 +367,11 @@ A module is a reusable, stateless bundle of related bindings. Group them once, t
358
367
  ```ts
359
368
  import { Container, Module } from "@codefast/di";
360
369
 
361
- const InfrastructureModule = Module.create("Infra", (api) => {
370
+ const InfrastructureModule = Module.create("app:Infra", (api) => {
362
371
  api.bind(LoggerToken).toConstantValue(console);
363
372
  });
364
373
 
365
- const AppModule = Module.create("App", (api) => {
374
+ const AppModule = Module.create("app:Root", (api) => {
366
375
  api.import(InfrastructureModule);
367
376
  api.bind(UserRepository).toSelf().singleton();
368
377
  });
@@ -407,9 +416,9 @@ metadata under `decorators/*` and `metadata/*`. Introspection ships at flat spec
407
416
 
408
417
  ## Benchmarks
409
418
 
410
- A first-party benchmark suite lives in the monorepo, at [`benchmarks/di-inversify`](../../benchmarks/di-inversify). It
411
- runs the same workloads through `@codefast/di`, InversifyJS, Awilix, and tsyringe, and its `RESULTS.md` ledger records
412
- the numbers alongside the method that produced them. Run it yourself rather than taking any figure on faith.
419
+ A first-party benchmark suite lives in the monorepo, at [`benchmarks/di`](../../benchmarks/di). It runs the same
420
+ workloads through `@codefast/di`, InversifyJS, Awilix, and tsyringe, and its `RESULTS.md` ledger records the numbers
421
+ alongside the method that produced them. Run it yourself rather than taking any figure on faith.
413
422
 
414
423
  ## Documentation
415
424
 
@@ -1,5 +1,6 @@
1
1
  /** The container an `@inject` accessor initializer resolves from when it has no other handle. */
2
2
  import type { Container } from "#/container/container";
3
+ import type { ConstructorInvocation } from "#/core/constructor-type";
3
4
  import type { Token } from "#/core/token";
4
5
  import type { Constructor, ResolveOptions } from "#/core/types";
5
6
  /**
@@ -31,6 +32,18 @@ export declare function runWithAmbientResolution<Result>(container: Container, r
31
32
  *
32
33
  * @since 0.3.16-canary.0
33
34
  */
35
+ /**
36
+ * Constructs `target` with `container` and `resolution` ambient for the duration of its constructor.
37
+ *
38
+ * @remarks The construction is written out rather than wrapped in a closure: an accessor-injected
39
+ * class pays this on every instantiation, and the closure was one of two allocations it paid for.
40
+ *
41
+ * @since 0.10.0
42
+ */
43
+ export declare function constructWithAmbientResolution(container: Container, resolution: AmbientResolution, target: ConstructorInvocation, deps: ReadonlyArray<unknown>): unknown;
44
+ /**
45
+ * @since 0.10.0
46
+ */
34
47
  export declare function getActiveContainer(): Container | undefined;
35
48
  /**
36
49
  * The path-continuing resolution for the construction in flight, when the engine installed one.
@@ -31,6 +31,30 @@ export function runWithAmbientResolution(container, resolution, fn) {
31
31
  *
32
32
  * @since 0.3.16-canary.0
33
33
  */
34
+ /**
35
+ * Constructs `target` with `container` and `resolution` ambient for the duration of its constructor.
36
+ *
37
+ * @remarks The construction is written out rather than wrapped in a closure: an accessor-injected
38
+ * class pays this on every instantiation, and the closure was one of two allocations it paid for.
39
+ *
40
+ * @since 0.10.0
41
+ */
42
+ export function constructWithAmbientResolution(container, resolution, target, deps) {
43
+ const previousContainer = activeContainer;
44
+ const previousResolution = activeResolution;
45
+ activeContainer = container;
46
+ activeResolution = resolution;
47
+ try {
48
+ return new target(...deps);
49
+ }
50
+ finally {
51
+ activeContainer = previousContainer;
52
+ activeResolution = previousResolution;
53
+ }
54
+ }
55
+ /**
56
+ * @since 0.10.0
57
+ */
34
58
  export function getActiveContainer() {
35
59
  return activeContainer;
36
60
  }
@@ -1,15 +1,17 @@
1
1
  /**
2
- * The fluent chain `bind()` returns: it registers the binding and refines it in place.
2
+ * The fluent chain `bind()` returns, which is also the binding it registers.
3
3
  *
4
- * @remarks One object plays every role in the chain; the return type of each step is what pins the
5
- * order, so no runtime check has to.
4
+ * @remarks One object plays every role: each builder step, the record the registry stores, and the
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
8
  */
7
- import type { AliasBindingBuilder, BindingBuilder, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#/core/binding";
9
+ import type { AliasBindingBuilder, BindingBuilder, BindingSlot, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#/core/binding";
8
10
  import type { BindingRegistry } from "#/core/registry";
9
11
  import type { BindingTag } from "#/core/tag";
10
12
  import type { Token } from "#/core/token";
11
- import type { ActivationHandler, BindingConstraint, BindingIdentifier, Constructor, DeactivationHandler, ResolutionContext } from "#/core/types";
12
- import type { InjectableDependency, ResolvedDependencyValue } from "#/injection/descriptor";
13
+ import type { ActivationHandler, BindingConstraint, BindingIdentifier, BindingKind, BindingScope, Constructor, DeactivationHandler, ResolutionContext, ResolutionFrame } from "#/core/types";
14
+ import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#/injection/descriptor";
13
15
  import type { ScopeManager } from "#/lifecycle/scope-manager";
14
16
  /**
15
17
  * Where a chain registers, and on whose behalf.
@@ -25,30 +27,50 @@ export interface BindingRegistration {
25
27
  readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
26
28
  }
27
29
  /**
28
- * The one builder behind `bind()` and every `to*()` return type. Each interface exposes only the
29
- * calls that are legal at that point in the chain; the runtime object is shared because every
30
- * refinement is the same operation — narrow the registered binding, keep its id.
30
+ * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
31
+ *
32
+ * @remarks The binding fields come first, in the order every binding shares, and the chain's own
33
+ * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
34
+ * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
35
+ * or hook in place, a slot or predicate through the registry so its indexes follow.
31
36
  *
32
37
  * @since 0.5.0-canary.8
33
38
  */
34
- export declare class BindingChain<Value> implements AliasBindingBuilder, BindingBuilder<Value>, BindToBuilder<Value>, ConstantBindingBuilder<Value>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
39
+ 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> {
35
40
  #private;
36
- constructor(token: Token<Value> | Constructor<Value>, registration: BindingRegistration);
37
- to(type: Constructor<Value>): BindingBuilder<Value>;
38
- toSelf(): BindingBuilder<Value>;
39
- toConstantValue(value: Value): ConstantBindingBuilder<Value>;
40
- toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
41
- toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
41
+ kind: BindingKind;
42
+ readonly identifier: BindingIdentifier;
43
+ inFlight: boolean;
44
+ frame: ResolutionFrame | undefined;
45
+ instance: unknown;
46
+ readonly token: Token<Value, Names> | Constructor<Value>;
47
+ slot: BindingSlot;
48
+ predicate: BindingConstraint | undefined;
49
+ isMany: boolean;
50
+ scope: BindingScope;
51
+ target: unknown;
52
+ factory: unknown;
53
+ deps: ReadonlyArray<InjectionDescriptor> | undefined;
54
+ value: unknown;
55
+ activationHook: ActivationHandler<Value> | undefined;
56
+ deactivationHook: DeactivationHandler<Value> | undefined;
57
+ constructor(token: Token<Value, Names> | Constructor<Value>, registration: BindingRegistration);
58
+ to(type: Constructor<Value>): BindingBuilder<Value, Names>;
59
+ toSelf(): BindingBuilder<Value, Names>;
60
+ toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
61
+ toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
62
+ toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
42
63
  toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
43
64
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
44
- }) => Value, deps: Deps): BindingBuilder<Value>;
65
+ }) => Value, deps: Deps): BindingBuilder<Value, Names>;
45
66
  toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
46
67
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
47
- }) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
48
- toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
68
+ }) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
69
+ toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
49
70
  when(predicate: BindingConstraint): this;
50
- whenNamed(name: string): this;
71
+ whenNamed(name: Names): this;
51
72
  whenTagged(criterion: BindingTag): this;
73
+ many(): this;
52
74
  whenDefault(): this;
53
75
  singleton(): SingletonBindingBuilder<Value>;
54
76
  transient(): TransientBindingBuilder<Value>;