@codefast/di 0.8.0 → 0.8.1

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 (218) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +242 -159
  3. package/dist/ambient/active-container.d.ts +1 -2
  4. package/dist/ambient/active-container.js +1 -2
  5. package/dist/container/binding-builders.d.ts +1 -2
  6. package/dist/container/binding-builders.js +1 -2
  7. package/dist/container/container.d.ts +1 -2
  8. package/dist/container/container.js +1 -2
  9. package/dist/core/binding-scope.d.ts +1 -2
  10. package/dist/core/binding-scope.js +1 -2
  11. package/dist/core/binding.d.ts +1 -2
  12. package/dist/core/binding.js +1 -2
  13. package/dist/core/constraint-requirement.d.ts +1 -2
  14. package/dist/core/constraint-requirement.js +1 -2
  15. package/dist/core/constructor-type.d.ts +1 -2
  16. package/dist/core/constructor-type.js +1 -2
  17. package/dist/core/map-upsert.d.ts +1 -2
  18. package/dist/core/map-upsert.js +1 -2
  19. package/dist/core/module.d.ts +1 -2
  20. package/dist/core/module.js +1 -2
  21. package/dist/core/registry.d.ts +1 -2
  22. package/dist/core/registry.js +1 -2
  23. package/dist/core/tag.d.ts +1 -2
  24. package/dist/core/tag.js +1 -2
  25. package/dist/core/token.d.ts +1 -2
  26. package/dist/core/token.js +1 -2
  27. package/dist/core/types.d.ts +1 -2
  28. package/dist/core/types.js +1 -2
  29. package/dist/decorators/inject.d.ts +1 -2
  30. package/dist/decorators/inject.js +1 -2
  31. package/dist/decorators/injectable.d.ts +1 -2
  32. package/dist/decorators/injectable.js +1 -2
  33. package/dist/decorators/lifecycle-decorators.d.ts +1 -2
  34. package/dist/decorators/lifecycle-decorators.js +1 -2
  35. package/dist/errors/diagnostics.d.ts +1 -2
  36. package/dist/errors/diagnostics.js +1 -2
  37. package/dist/errors/errors.d.ts +1 -2
  38. package/dist/errors/errors.js +1 -2
  39. package/dist/index.d.ts +1 -2
  40. package/dist/index.js +1 -2
  41. package/dist/injection/descriptor.d.ts +1 -2
  42. package/dist/injection/descriptor.js +1 -2
  43. package/dist/injection/resolve-options.d.ts +1 -2
  44. package/dist/injection/resolve-options.js +1 -2
  45. package/dist/introspection/dependency-graph.d.ts +1 -2
  46. package/dist/introspection/dependency-graph.js +1 -2
  47. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -2
  48. package/dist/introspection/graph-adapters/cytoscape.js +1 -2
  49. package/dist/introspection/graph-adapters/dot.d.ts +1 -2
  50. package/dist/introspection/graph-adapters/dot.js +1 -2
  51. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -2
  52. package/dist/introspection/graph-adapters/mermaid.js +1 -2
  53. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -2
  54. package/dist/introspection/graph-adapters/reactflow.js +1 -2
  55. package/dist/introspection/inspector.d.ts +1 -2
  56. package/dist/introspection/inspector.js +1 -2
  57. package/dist/lifecycle/lifecycle-manager.d.ts +1 -2
  58. package/dist/lifecycle/lifecycle-manager.js +1 -2
  59. package/dist/lifecycle/scope-manager.d.ts +1 -2
  60. package/dist/lifecycle/scope-manager.js +1 -2
  61. package/dist/metadata/metadata-keys.d.ts +1 -2
  62. package/dist/metadata/metadata-keys.js +1 -2
  63. package/dist/metadata/metadata-reader-token.d.ts +1 -2
  64. package/dist/metadata/metadata-reader-token.js +1 -2
  65. package/dist/metadata/metadata-types.d.ts +1 -2
  66. package/dist/metadata/metadata-types.js +1 -2
  67. package/dist/metadata/symbol-metadata-reader.d.ts +1 -2
  68. package/dist/metadata/symbol-metadata-reader.js +1 -2
  69. package/dist/metadata/verifying-metadata-reader.d.ts +1 -2
  70. package/dist/metadata/verifying-metadata-reader.js +1 -2
  71. package/dist/resolution/cache/activation-need.d.ts +1 -2
  72. package/dist/resolution/cache/activation-need.js +1 -2
  73. package/dist/resolution/cache/binding-lookup-cache.d.ts +1 -2
  74. package/dist/resolution/cache/binding-lookup-cache.js +1 -2
  75. package/dist/resolution/cache/class-introspector.d.ts +1 -2
  76. package/dist/resolution/cache/class-introspector.js +1 -2
  77. package/dist/resolution/context.d.ts +1 -2
  78. package/dist/resolution/context.js +1 -2
  79. package/dist/resolution/path/resolution-path.d.ts +1 -2
  80. package/dist/resolution/path/resolution-path.js +1 -2
  81. package/dist/resolution/plan/instantiation-plan.d.ts +1 -2
  82. package/dist/resolution/plan/instantiation-plan.js +1 -2
  83. package/dist/resolution/resolver.d.ts +1 -2
  84. package/dist/resolution/resolver.js +1 -2
  85. package/dist/resolution/select/binding-select.d.ts +1 -2
  86. package/dist/resolution/select/binding-select.js +1 -2
  87. package/dist/resolution/select/constraints.d.ts +1 -2
  88. package/dist/resolution/select/constraints.js +1 -2
  89. package/package.json +6 -58
  90. package/dist/ambient/active-container.d.ts.map +0 -1
  91. package/dist/ambient/active-container.js.map +0 -1
  92. package/dist/container/binding-builders.d.ts.map +0 -1
  93. package/dist/container/binding-builders.js.map +0 -1
  94. package/dist/container/container.d.ts.map +0 -1
  95. package/dist/container/container.js.map +0 -1
  96. package/dist/core/binding-scope.d.ts.map +0 -1
  97. package/dist/core/binding-scope.js.map +0 -1
  98. package/dist/core/binding.d.ts.map +0 -1
  99. package/dist/core/binding.js.map +0 -1
  100. package/dist/core/constraint-requirement.d.ts.map +0 -1
  101. package/dist/core/constraint-requirement.js.map +0 -1
  102. package/dist/core/constructor-type.d.ts.map +0 -1
  103. package/dist/core/constructor-type.js.map +0 -1
  104. package/dist/core/map-upsert.d.ts.map +0 -1
  105. package/dist/core/map-upsert.js.map +0 -1
  106. package/dist/core/module.d.ts.map +0 -1
  107. package/dist/core/module.js.map +0 -1
  108. package/dist/core/registry.d.ts.map +0 -1
  109. package/dist/core/registry.js.map +0 -1
  110. package/dist/core/tag.d.ts.map +0 -1
  111. package/dist/core/tag.js.map +0 -1
  112. package/dist/core/token.d.ts.map +0 -1
  113. package/dist/core/token.js.map +0 -1
  114. package/dist/core/types.d.ts.map +0 -1
  115. package/dist/core/types.js.map +0 -1
  116. package/dist/decorators/inject.d.ts.map +0 -1
  117. package/dist/decorators/inject.js.map +0 -1
  118. package/dist/decorators/injectable.d.ts.map +0 -1
  119. package/dist/decorators/injectable.js.map +0 -1
  120. package/dist/decorators/lifecycle-decorators.d.ts.map +0 -1
  121. package/dist/decorators/lifecycle-decorators.js.map +0 -1
  122. package/dist/errors/diagnostics.d.ts.map +0 -1
  123. package/dist/errors/diagnostics.js.map +0 -1
  124. package/dist/errors/errors.d.ts.map +0 -1
  125. package/dist/errors/errors.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/injection/descriptor.d.ts.map +0 -1
  129. package/dist/injection/descriptor.js.map +0 -1
  130. package/dist/injection/resolve-options.d.ts.map +0 -1
  131. package/dist/injection/resolve-options.js.map +0 -1
  132. package/dist/introspection/dependency-graph.d.ts.map +0 -1
  133. package/dist/introspection/dependency-graph.js.map +0 -1
  134. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +0 -1
  135. package/dist/introspection/graph-adapters/cytoscape.js.map +0 -1
  136. package/dist/introspection/graph-adapters/dot.d.ts.map +0 -1
  137. package/dist/introspection/graph-adapters/dot.js.map +0 -1
  138. package/dist/introspection/graph-adapters/mermaid.d.ts.map +0 -1
  139. package/dist/introspection/graph-adapters/mermaid.js.map +0 -1
  140. package/dist/introspection/graph-adapters/reactflow.d.ts.map +0 -1
  141. package/dist/introspection/graph-adapters/reactflow.js.map +0 -1
  142. package/dist/introspection/inspector.d.ts.map +0 -1
  143. package/dist/introspection/inspector.js.map +0 -1
  144. package/dist/lifecycle/lifecycle-manager.d.ts.map +0 -1
  145. package/dist/lifecycle/lifecycle-manager.js.map +0 -1
  146. package/dist/lifecycle/scope-manager.d.ts.map +0 -1
  147. package/dist/lifecycle/scope-manager.js.map +0 -1
  148. package/dist/metadata/metadata-keys.d.ts.map +0 -1
  149. package/dist/metadata/metadata-keys.js.map +0 -1
  150. package/dist/metadata/metadata-reader-token.d.ts.map +0 -1
  151. package/dist/metadata/metadata-reader-token.js.map +0 -1
  152. package/dist/metadata/metadata-types.d.ts.map +0 -1
  153. package/dist/metadata/metadata-types.js.map +0 -1
  154. package/dist/metadata/symbol-metadata-reader.d.ts.map +0 -1
  155. package/dist/metadata/symbol-metadata-reader.js.map +0 -1
  156. package/dist/metadata/verifying-metadata-reader.d.ts.map +0 -1
  157. package/dist/metadata/verifying-metadata-reader.js.map +0 -1
  158. package/dist/resolution/cache/activation-need.d.ts.map +0 -1
  159. package/dist/resolution/cache/activation-need.js.map +0 -1
  160. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +0 -1
  161. package/dist/resolution/cache/binding-lookup-cache.js.map +0 -1
  162. package/dist/resolution/cache/class-introspector.d.ts.map +0 -1
  163. package/dist/resolution/cache/class-introspector.js.map +0 -1
  164. package/dist/resolution/context.d.ts.map +0 -1
  165. package/dist/resolution/context.js.map +0 -1
  166. package/dist/resolution/path/resolution-path.d.ts.map +0 -1
  167. package/dist/resolution/path/resolution-path.js.map +0 -1
  168. package/dist/resolution/plan/instantiation-plan.d.ts.map +0 -1
  169. package/dist/resolution/plan/instantiation-plan.js.map +0 -1
  170. package/dist/resolution/resolver.d.ts.map +0 -1
  171. package/dist/resolution/resolver.js.map +0 -1
  172. package/dist/resolution/select/binding-select.d.ts.map +0 -1
  173. package/dist/resolution/select/binding-select.js.map +0 -1
  174. package/dist/resolution/select/constraints.d.ts.map +0 -1
  175. package/dist/resolution/select/constraints.js.map +0 -1
  176. package/src/ambient/active-container.ts +0 -69
  177. package/src/container/binding-builders.ts +0 -337
  178. package/src/container/container.ts +0 -972
  179. package/src/core/binding-scope.ts +0 -15
  180. package/src/core/binding.ts +0 -505
  181. package/src/core/constraint-requirement.ts +0 -94
  182. package/src/core/constructor-type.ts +0 -18
  183. package/src/core/map-upsert.ts +0 -53
  184. package/src/core/module.ts +0 -117
  185. package/src/core/registry.ts +0 -319
  186. package/src/core/tag.ts +0 -212
  187. package/src/core/token.ts +0 -31
  188. package/src/core/types.ts +0 -149
  189. package/src/decorators/inject.ts +0 -101
  190. package/src/decorators/injectable.ts +0 -134
  191. package/src/decorators/lifecycle-decorators.ts +0 -43
  192. package/src/errors/diagnostics.ts +0 -45
  193. package/src/errors/errors.ts +0 -499
  194. package/src/index.ts +0 -139
  195. package/src/injection/descriptor.ts +0 -218
  196. package/src/injection/resolve-options.ts +0 -201
  197. package/src/introspection/dependency-graph.ts +0 -296
  198. package/src/introspection/graph-adapters/cytoscape.ts +0 -78
  199. package/src/introspection/graph-adapters/dot.ts +0 -32
  200. package/src/introspection/graph-adapters/mermaid.ts +0 -61
  201. package/src/introspection/graph-adapters/reactflow.ts +0 -84
  202. package/src/introspection/inspector.ts +0 -134
  203. package/src/lifecycle/lifecycle-manager.ts +0 -272
  204. package/src/lifecycle/scope-manager.ts +0 -158
  205. package/src/metadata/metadata-keys.ts +0 -28
  206. package/src/metadata/metadata-reader-token.ts +0 -10
  207. package/src/metadata/metadata-types.ts +0 -57
  208. package/src/metadata/symbol-metadata-reader.ts +0 -126
  209. package/src/metadata/verifying-metadata-reader.ts +0 -41
  210. package/src/resolution/cache/activation-need.ts +0 -87
  211. package/src/resolution/cache/binding-lookup-cache.ts +0 -169
  212. package/src/resolution/cache/class-introspector.ts +0 -256
  213. package/src/resolution/context.ts +0 -331
  214. package/src/resolution/path/resolution-path.ts +0 -167
  215. package/src/resolution/plan/instantiation-plan.ts +0 -592
  216. package/src/resolution/resolver.ts +0 -1654
  217. package/src/resolution/select/binding-select.ts +0 -207
  218. package/src/resolution/select/constraints.ts +0 -146
@@ -1,117 +0,0 @@
1
- import type { BindToBuilder } from "#/core/binding";
2
- import type { Token } from "#/core/token";
3
- import type { Constructor } from "#/core/types";
4
-
5
- // ── Branded types (runtime symbols for branding) ─────────────────────────────────────────────────────────────────────
6
-
7
- const SYNC_MODULE_BRAND: unique symbol = Symbol("di:sync-module");
8
- const ASYNC_MODULE_BRAND: unique symbol = Symbol("di:async-module");
9
-
10
- /**
11
- * Key for the module's setup callback. A symbol (not exported from the package root)
12
- * keeps the container-only member out of consumer-facing autocomplete entirely.
13
- *
14
- * @since 0.5.0-canary.7
15
- */
16
- export const MODULE_SETUP: unique symbol = Symbol("di:module-setup");
17
-
18
- /**
19
- * A named, reusable group of bindings a container applies synchronously via `load()`.
20
- *
21
- * @since 0.3.16-canary.0
22
- */
23
- export interface SyncModule {
24
- readonly name: string;
25
- readonly [SYNC_MODULE_BRAND]: true;
26
- readonly [MODULE_SETUP]: (builder: ModuleBuilder) => void;
27
- }
28
-
29
- /**
30
- * A named group of bindings whose setup is async, applied via `loadAsync()`.
31
- *
32
- * @since 0.3.16-canary.0
33
- */
34
- export interface AsyncModule {
35
- readonly name: string;
36
- readonly [ASYNC_MODULE_BRAND]: true;
37
- readonly [MODULE_SETUP]: (builder: AsyncModuleBuilder) => Promise<void>;
38
- }
39
-
40
- // ── Builder interfaces ───────────────────────────────────────────────────────────────────────────────────────────────
41
-
42
- /**
43
- * The binding surface a sync module's setup callback receives.
44
- *
45
- * @since 0.3.16-canary.0
46
- */
47
- export interface ModuleBuilder {
48
- bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
49
- import(...modules: Array<SyncModule>): void;
50
- }
51
-
52
- /**
53
- * The binding surface an async module's setup callback receives; its `import` accepts async modules too.
54
- *
55
- * @since 0.3.16-canary.0
56
- */
57
- export interface AsyncModuleBuilder {
58
- bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
59
- import(...modules: Array<SyncModule | AsyncModule>): void;
60
- }
61
-
62
- // ── Static factories ─────────────────────────────────────────────────────────────────────────────────────────────────
63
-
64
- /**
65
- * The companion factory that creates `SyncModule` values.
66
- *
67
- * @since 0.3.16-canary.0
68
- */
69
- export const SyncModule = {
70
- create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
71
- return {
72
- name,
73
- [SYNC_MODULE_BRAND]: true as const,
74
- [MODULE_SETUP]: setup,
75
- };
76
- },
77
- };
78
-
79
- /**
80
- * The companion factory that creates `AsyncModule` values.
81
- *
82
- * @since 0.3.16-canary.0
83
- */
84
- export const AsyncModule = {
85
- create(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
86
- return {
87
- name,
88
- [ASYNC_MODULE_BRAND]: true as const,
89
- [MODULE_SETUP]: setup,
90
- };
91
- },
92
- };
93
-
94
- // ── Module — unified API ─────────────────────────────────────────────────────────────────────────────────────────────
95
-
96
- /**
97
- * The unified module factory — `create` for sync modules, `createAsync` for async ones.
98
- *
99
- * @since 0.3.16-canary.0
100
- */
101
- export const Module = {
102
- create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
103
- return SyncModule.create(name, setup);
104
- },
105
- createAsync(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
106
- return AsyncModule.create(name, setup);
107
- },
108
- };
109
-
110
- /**
111
- * Narrows a module union to `SyncModule` by checking its brand.
112
- *
113
- * @since 0.3.16-canary.0
114
- */
115
- export function isSyncModule(module: SyncModule | AsyncModule): module is SyncModule {
116
- return (module as Partial<SyncModule>)[SYNC_MODULE_BRAND] === true;
117
- }
@@ -1,319 +0,0 @@
1
- import type { Binding } from "#/core/binding";
2
- import { bindingSlotEquals, bindingSlotToString } from "#/core/binding";
3
- import { getOrInsert } from "#/core/map-upsert";
4
- import type { BindingTag } from "#/core/tag";
5
- import type { Token } from "#/core/token";
6
- import type { BindingIdentifier, Constructor, DependencyKey } from "#/core/types";
7
-
8
- /**
9
- * One container's binding store, indexed by token, binding id, and slot for fast lookup.
10
- *
11
- * @since 0.3.16-canary.0
12
- */
13
- export class BindingRegistry {
14
- // Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
15
- #version = 0;
16
- // Map from token key -> array of bindings (order matters for last-wins)
17
- readonly #bindings = new Map<DependencyKey, Array<Binding>>();
18
- // Fast lookup by binding ID
19
- readonly #byId = new Map<BindingIdentifier, Binding>();
20
- // Fast path for one default slot binding with no predicate
21
- readonly #fastDefault = new Map<DependencyKey, Binding>();
22
- // Fast lookup for a slot carrying exactly one criterion (a lone name folds here too) — keyed by
23
- // the interned criterion itself, so the pair is one hash. Unallocated until such a slot lands.
24
- #simpleTagged: Map<DependencyKey, Map<BindingTag, Binding>> | undefined;
25
- // Slots with two or more criteria, bucketed by their FIRST criterion. A matching slot's every
26
- // criterion is in the request, so walking the request's buckets finds each candidate exactly once.
27
- #multiTagged: Map<DependencyKey, Map<BindingTag, Array<Binding>>> | undefined;
28
-
29
- // Set on the first constant registered and never cleared. Teardown only needs the negative answer
30
- // to be exact, and that is what lets a container holding no constant skip its sweep entirely.
31
- #heldConstantBinding = false;
32
-
33
- /** Monotonic version — increments on every mutation. */
34
- get version(): number {
35
- return this.#version;
36
- }
37
-
38
- /** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
39
- get hasHeldConstantBinding(): boolean {
40
- return this.#heldConstantBinding;
41
- }
42
-
43
- /**
44
- * Registers a mutation the indexes don't care about (a fluent chain refining scope or an
45
- * activation hook in place), so version-stamped resolver caches still invalidate.
46
- */
47
- touch(): void {
48
- this.#version += 1;
49
- }
50
-
51
- /**
52
- * Adds or replaces a binding using slot-aware last-wins. Returns the displaced binding, if any.
53
- *
54
- * @remarks The binding is stored by reference — it must come from `createBinding`, which is
55
- * what guarantees the single hidden class the resolver's hot reads depend on.
56
- */
57
- add(binding: Binding): Binding | undefined {
58
- this.#version += 1;
59
- if (binding.kind === "constant") {
60
- this.#heldConstantBinding = true;
61
- }
62
- const key: DependencyKey = binding.token;
63
- // Copy-on-write: a selection may be walking the current list inside a `when()` predicate, so
64
- // mutation replaces the array and never splices one that has been handed out.
65
- const bindingsForToken = this.#bindings.get(key);
66
-
67
- // Only apply last-wins for slot-based bindings (not predicate-only)
68
- let displacedBinding: Binding | undefined;
69
- let nextBindings: Array<Binding>;
70
- if (bindingsForToken === undefined) {
71
- nextBindings = [binding];
72
- } else {
73
- if (!isPurePredicateBinding(binding)) {
74
- const existingIndex = bindingsForToken.findIndex(
75
- (candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
76
- );
77
- if (existingIndex !== -1) {
78
- displacedBinding = bindingsForToken[existingIndex]!;
79
- this.#byId.delete(displacedBinding.id);
80
- this.#deindexSimpleTaggedBinding(key, displacedBinding);
81
- this.#deindexMultiTaggedBinding(key, displacedBinding);
82
- }
83
- }
84
- nextBindings =
85
- displacedBinding === undefined
86
- ? [...bindingsForToken, binding]
87
- : [...bindingsForToken.filter((candidate) => candidate !== displacedBinding), binding];
88
- }
89
-
90
- this.#bindings.set(key, nextBindings);
91
- this.#byId.set(binding.id, binding);
92
- this.#indexSimpleTaggedBinding(key, binding);
93
- this.#indexMultiTaggedBinding(key, binding);
94
- this.#refreshFastDefaultForToken(key);
95
- return displacedBinding;
96
- }
97
-
98
- /** Remove all bindings for a token. Returns removed bindings. */
99
- removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
100
- this.#version += 1;
101
- const key: DependencyKey = token;
102
- const bindingsForToken = this.#bindings.get(key) ?? [];
103
- this.#bindings.delete(key);
104
- this.#simpleTagged?.delete(key);
105
- this.#multiTagged?.delete(key);
106
- this.#fastDefault.delete(key);
107
- for (const binding of bindingsForToken) {
108
- this.#byId.delete(binding.id);
109
- }
110
- return bindingsForToken;
111
- }
112
-
113
- /** Remove a specific binding by ID. Returns the removed binding or undefined. */
114
- removeById(id: BindingIdentifier): Binding | undefined {
115
- const binding = this.#byId.get(id);
116
- if (binding === undefined) {
117
- return undefined;
118
- }
119
- this.#version += 1;
120
- this.#byId.delete(id);
121
- const key: DependencyKey = binding.token;
122
- const bindingsForToken = this.#bindings.get(key);
123
- if (bindingsForToken !== undefined) {
124
- const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
125
- // Copy-on-write, like `add`: a walk holding the current array must not lose its place.
126
- const remaining = bindingIndex === -1 ? bindingsForToken : bindingsForToken.toSpliced(bindingIndex, 1);
127
- this.#deindexSimpleTaggedBinding(key, binding);
128
- this.#deindexMultiTaggedBinding(key, binding);
129
- if (remaining.length === 0) {
130
- this.#bindings.delete(key);
131
- this.#simpleTagged?.delete(key);
132
- this.#multiTagged?.delete(key);
133
- this.#fastDefault.delete(key);
134
- } else {
135
- this.#bindings.set(key, remaining);
136
- this.#refreshFastDefaultForToken(key);
137
- }
138
- }
139
- return binding;
140
- }
141
-
142
- /** Get all bindings for a token. */
143
- getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
144
- return this.#bindings.get(token) ?? [];
145
- }
146
-
147
- /** Get binding by ID. */
148
- getById(id: BindingIdentifier): Binding | undefined {
149
- return this.#byId.get(id);
150
- }
151
-
152
- /** Check if any binding exists for token. */
153
- has(token: Token<unknown> | Constructor): boolean {
154
- const key: DependencyKey = token;
155
- const list = this.#bindings.get(key);
156
- return list !== undefined && list.length > 0;
157
- }
158
-
159
- /** All bindings in the registry. */
160
- allBindings(): ReadonlyArray<Binding> {
161
- const allBindings: Array<Binding> = [];
162
- for (const bindingsForToken of this.#bindings.values()) {
163
- allBindings.push(...bindingsForToken);
164
- }
165
- return allBindings;
166
- }
167
-
168
- /** Remove all bindings. Returns all removed. */
169
- clear(): ReadonlyArray<Binding> {
170
- this.#version += 1;
171
- const all = this.allBindings();
172
- this.#bindings.clear();
173
- this.#byId.clear();
174
- this.#simpleTagged?.clear();
175
- this.#multiTagged?.clear();
176
- this.#fastDefault.clear();
177
- return all;
178
- }
179
-
180
- /** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
181
- hasSlotOccupant(binding: Binding): boolean {
182
- if (isPurePredicateBinding(binding)) {
183
- return false;
184
- }
185
- const candidates = this.#bindings.get(binding.token);
186
- if (candidates === undefined) {
187
- return false;
188
- }
189
- return candidates.some(
190
- (candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
191
- );
192
- }
193
-
194
- /**
195
- * The binding indexed under one criterion.
196
- *
197
- * @remarks Exact, with no re-check: criteria are interned, so a `Map` keyed by the pair answers by
198
- * identity — where a value-keyed map answered by SameValueZero and parted from `Object.is` on ±0.
199
- */
200
- getSimpleTagged(token: Token<unknown> | Constructor, criterion: BindingTag): Binding | undefined {
201
- return this.#simpleTagged?.get(token)?.get(criterion);
202
- }
203
-
204
- /**
205
- * The multi-tag bindings whose slot's first criterion is `criterion`.
206
- *
207
- * @remarks A prefilter, not an answer: a bucket member's remaining tags still have to be matched
208
- * against the request — first-criterion bucketing only guarantees each candidate appears once.
209
- */
210
- getMultiTagged(token: Token<unknown> | Constructor, criterion: BindingTag): ReadonlyArray<Binding> | undefined {
211
- return this.#multiTagged?.get(token)?.get(criterion);
212
- }
213
-
214
- getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
215
- return this.#fastDefault.get(token);
216
- }
217
-
218
- /** Summarize available slot strings for a token (for error messages). */
219
- availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
220
- const bindingsForToken = this.#bindings.get(token) ?? [];
221
- return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
222
- }
223
-
224
- #indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
225
- const criterion = simpleTagOf(binding);
226
- if (criterion === undefined) {
227
- return;
228
- }
229
- this.#simpleTagged ??= new Map();
230
- const byCriterion = getOrInsert(this.#simpleTagged, tokenKey, new Map<BindingTag, Binding>());
231
- byCriterion.set(criterion, binding);
232
- }
233
-
234
- #deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
235
- const criterion = simpleTagOf(binding);
236
- if (criterion === undefined) {
237
- return;
238
- }
239
- const byCriterion = this.#simpleTagged?.get(tokenKey);
240
- if (byCriterion === undefined) {
241
- return;
242
- }
243
- if (byCriterion.get(criterion)?.id === binding.id) {
244
- byCriterion.delete(criterion);
245
- if (byCriterion.size === 0) {
246
- this.#simpleTagged!.delete(tokenKey);
247
- }
248
- }
249
- }
250
-
251
- #indexMultiTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
252
- const firstCriterion = multiTagFirstOf(binding);
253
- if (firstCriterion === undefined) {
254
- return;
255
- }
256
- this.#multiTagged ??= new Map();
257
- const buckets = getOrInsert(this.#multiTagged, tokenKey, new Map<BindingTag, Array<Binding>>());
258
- getOrInsert(buckets, firstCriterion, []).push(binding);
259
- }
260
-
261
- #deindexMultiTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
262
- const firstCriterion = multiTagFirstOf(binding);
263
- if (firstCriterion === undefined) {
264
- return;
265
- }
266
- const bucket = this.#multiTagged?.get(tokenKey)?.get(firstCriterion);
267
- if (bucket === undefined) {
268
- return;
269
- }
270
- const bindingIndex = bucket.findIndex((candidate) => candidate.id === binding.id);
271
- // Spliced in place: nothing walks a bucket while user code runs — candidates are gathered
272
- // into their own array before any predicate is evaluated.
273
- if (bindingIndex !== -1) {
274
- bucket.splice(bindingIndex, 1);
275
- }
276
- }
277
-
278
- #refreshFastDefaultForToken(tokenKey: DependencyKey): void {
279
- const bindingsForToken = this.#bindings.get(tokenKey);
280
- const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
281
- if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
282
- this.#fastDefault.set(tokenKey, onlyBinding);
283
- return;
284
- }
285
- this.#fastDefault.delete(tokenKey);
286
- }
287
-
288
- /** Whether the deferred tagged-slot index has had to be built. */
289
- get isTaggedIndexBuilt(): boolean {
290
- return this.#simpleTagged !== undefined;
291
- }
292
- }
293
-
294
- /**
295
- * The criterion a binding is indexed under, or `undefined` when its slot carries more than one.
296
- *
297
- * @remarks Carries predicate-bearing bindings too: every lane that reads this index already
298
- * re-checks what it finds, so an indexed hit was never unconditional.
299
- */
300
- function simpleTagOf(binding: Binding): BindingTag | undefined {
301
- const { tags } = binding.slot;
302
- return tags.length === 1 ? tags[0] : undefined;
303
- }
304
-
305
- /** The first criterion a multi-criterion slot is bucketed under, or `undefined` for any other shape. */
306
- function multiTagFirstOf(binding: Binding): BindingTag | undefined {
307
- const { tags } = binding.slot;
308
- return tags.length >= 2 ? tags[0] : undefined;
309
- }
310
-
311
- /** A binding nothing has to be matched against: the default slot, no predicate. */
312
- function isDefaultSlotBinding(binding: Binding): boolean {
313
- return binding.slot.tags.length === 0 && binding.predicate === undefined;
314
- }
315
-
316
- /** A predicate with no slot constraint: last-wins does not apply to it. */
317
- function isPurePredicateBinding(binding: Binding): boolean {
318
- return binding.predicate !== undefined && binding.slot.tags.length === 0;
319
- }
package/src/core/tag.ts DELETED
@@ -1,212 +0,0 @@
1
- /** Tag keys and the interned pairs they mint — the one way a slot criterion is built. */
2
-
3
- declare const TAG_KEY_BRAND: unique symbol;
4
- declare const BINDING_TAG_BRAND: unique symbol;
5
-
6
- /**
7
- * How many distinct tag keys one mask bit is shared by.
8
- *
9
- * @remarks Ids past this wrap, so two keys can share a bit. The mask is a prefilter only — a shared
10
- * bit costs a false positive that the identity comparison then rejects, never a false negative.
11
- */
12
- const MASK_WIDTH = 32;
13
-
14
- /**
15
- * A slot's or a request's tag key set, as one word.
16
- *
17
- * @remarks A slot matches only if the request carries every key the slot declares, which is
18
- * `(requestMask & slotMask) === slotMask` — the whole subset test, before any value is read. Keys
19
- * are prefiltered rather than values indexed because a key set fits in a word and a value set does not.
20
- *
21
- * @since 0.6.0
22
- */
23
- export type TagKeyMask = number & { readonly [TAG_KEY_BRAND]: "mask" };
24
-
25
- /**
26
- * The empty key set: what an untagged slot and an untagged request both carry.
27
- *
28
- * @since 0.6.0
29
- */
30
- export const NO_TAG_KEYS = 0 as TagKeyMask;
31
-
32
- /**
33
- * One `[key, value]` criterion, interned so equal criteria are the same object.
34
- *
35
- * @remarks Only {@link TagKey.of} mints one, which is what makes identity a sound stand-in for the
36
- * `Object.is` comparison the slot contract requires. Never build one by hand — the matcher and the
37
- * registry index both read identity, so a hand-built criterion matches nothing.
38
- *
39
- * @since 0.6.0
40
- */
41
- export interface BindingTag<Value = unknown> {
42
- readonly key: TagKey<Value>;
43
- readonly value: Value;
44
- /** This pair's key mask, copied off the key so the matcher reads one field. */
45
- readonly mask: TagKeyMask;
46
- readonly [BINDING_TAG_BRAND]: true;
47
- }
48
-
49
- /**
50
- * A named tag key, and the factory for its criteria.
51
- *
52
- * @since 0.6.0
53
- */
54
- export interface TagKey<Value = unknown> {
55
- readonly name: string;
56
- /** Process-monotonic, so a key set is a mask and the registry can index on a number. */
57
- readonly id: number;
58
- readonly mask: TagKeyMask;
59
- /** The criterion for one value, interned: the same value always yields the same object. */
60
- of(value: Value): BindingTag<Value>;
61
- /**
62
- * The interned criterion for a value, or `undefined` when none was ever minted.
63
- *
64
- * @remarks Reading without minting is what keeps a request-side value from being retained for
65
- * the process lifetime: a value no binding ever declared has no criterion, so a lookup can
66
- * answer "no match" without inserting one.
67
- */
68
- peek(value: Value): BindingTag<Value> | undefined;
69
- }
70
-
71
- let tagKeyCounter = -1;
72
-
73
- /**
74
- * Distinguishes `-0` from `+0` in the intern cache.
75
- *
76
- * @remarks A `Map` key compares by SameValueZero, which holds `+0` and `-0` equal, while tag values
77
- * compare by `Object.is`, which does not. Interning them to one object would make the
78
- * two indistinguishable everywhere downstream, so the negative one is cached under this instead.
79
- */
80
- const NEGATIVE_ZERO_KEY: unique symbol = Symbol("di:tag-negative-zero");
81
-
82
- function internKeyFor(value: unknown): unknown {
83
- return value === 0 && Object.is(value, -0) ? NEGATIVE_ZERO_KEY : value;
84
- }
85
-
86
- /**
87
- * Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
88
- *
89
- * @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
90
- * value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
91
- *
92
- * @example
93
- * ```ts
94
- * const Region = tag<"eu" | "us">("region");
95
- * container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
96
- * container.resolve(Storage, { tag: Region.of("eu") });
97
- * ```
98
- *
99
- * @since 0.6.0
100
- */
101
- export function tag<Value = unknown>(name: string): TagKey<Value> {
102
- tagKeyCounter += 1;
103
- const id = tagKeyCounter;
104
- const mask = (1 << (id % MASK_WIDTH)) as TagKeyMask;
105
- const interned = new Map<unknown, BindingTag<Value>>();
106
- // One-entry cache in front of the intern map: an inline `.of()` at a call site usually repeats
107
- // one value, and `Object.is` is the slot contract's own comparison, so a hit is exact — ±0 stay
108
- // split and `NaN` hits itself, with no `internKeyFor` detour.
109
- let lastValue: Value | undefined;
110
- let lastPair: BindingTag<Value> | undefined;
111
-
112
- // The miss paths live outside `of()`/`peek()` so the hot wrappers stay small enough to inline.
113
- const internPair = (value: Value): BindingTag<Value> => {
114
- const cacheKey = internKeyFor(value);
115
- const existing = interned.get(cacheKey);
116
-
117
- if (existing !== undefined) {
118
- lastValue = value;
119
- lastPair = existing;
120
-
121
- return existing;
122
- }
123
-
124
- const pair = { key, value, mask } as BindingTag<Value>;
125
-
126
- interned.set(cacheKey, pair);
127
- lastValue = value;
128
- lastPair = pair;
129
-
130
- return pair;
131
- };
132
- const peekInterned = (value: Value): BindingTag<Value> | undefined => interned.get(internKeyFor(value));
133
-
134
- const key: TagKey<Value> = {
135
- name,
136
- id,
137
- mask,
138
- of(value: Value): BindingTag<Value> {
139
- if (lastPair !== undefined && Object.is(value, lastValue)) {
140
- return lastPair;
141
- }
142
- return internPair(value);
143
- },
144
- peek(value: Value): BindingTag<Value> | undefined {
145
- if (lastPair !== undefined && Object.is(value, lastValue)) {
146
- return lastPair;
147
- }
148
- return peekInterned(value);
149
- },
150
- };
151
-
152
- return key;
153
- }
154
-
155
- /**
156
- * The reserved key a slot's name is a criterion of.
157
- *
158
- * @remarks `whenNamed(n)` and a request's `name` are sugar for `slotName.of(n)`, so one selection
159
- * model serves both spellings — a name takes part in key masks, indexes and specificity like any
160
- * criterion. What reserves the key is its identity; diagnostics render its criteria as `name:<value>`.
161
- *
162
- * @since 0.8.0
163
- */
164
- export const slotName: TagKey<string> = tag<string>("di:name");
165
-
166
- // One-entry front for the reserved key's read: a name's criterion never changes once minted, so a
167
- // hit is sound forever, and a miss is never cached so a later `whenNamed` bind is still seen.
168
- let lastPeekedName: string | undefined;
169
- let lastPeekedCriterion: BindingTag<string> | undefined;
170
-
171
- /**
172
- * The reserved criterion for a name, or `undefined` while no binding has declared it.
173
- *
174
- * @remarks Reads without minting, so a request-side name no binding declared is never retained.
175
- *
176
- * @since 0.8.0
177
- */
178
- export function slotNameCriterionOf(name: string): BindingTag<string> | undefined {
179
- if (name === lastPeekedName) {
180
- return lastPeekedCriterion;
181
- }
182
- const criterion = slotName.peek(name);
183
- if (criterion !== undefined) {
184
- lastPeekedName = name;
185
- lastPeekedCriterion = criterion;
186
- }
187
- return criterion;
188
- }
189
-
190
- /**
191
- * The key set a list of criteria covers.
192
- *
193
- * @since 0.6.0
194
- */
195
- export function tagKeyMaskOf(tags: ReadonlyArray<BindingTag>): TagKeyMask {
196
- let mask = NO_TAG_KEYS;
197
-
198
- for (let index = 0; index < tags.length; index += 1) {
199
- mask = (mask | tags[index]!.mask) as TagKeyMask;
200
- }
201
-
202
- return mask;
203
- }
204
-
205
- /**
206
- * Whether a request carrying `requestMask` covers every key in `slotMask`.
207
- *
208
- * @since 0.6.0
209
- */
210
- export function coversTagKeys(requestMask: TagKeyMask, slotMask: TagKeyMask): boolean {
211
- return (requestMask & slotMask) === slotMask;
212
- }
package/src/core/token.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { Constructor } from "#/core/constructor-type";
2
-
3
- declare const TOKEN_BRAND: unique symbol;
4
-
5
- /**
6
- * A branded identifier carrying the value type its bindings resolve to.
7
- *
8
- * @since 0.3.16-canary.0
9
- */
10
- export interface Token<out Value> {
11
- readonly name: string;
12
- readonly [TOKEN_BRAND]: Value;
13
- }
14
-
15
- /**
16
- * Creates a named `Token` for the given value type.
17
- *
18
- * @since 0.3.16-canary.0
19
- */
20
- export function token<Value>(name: string): Token<Value> {
21
- return { name } as Token<Value>;
22
- }
23
-
24
- /**
25
- * Returns the display name of a token or class used as a dependency key.
26
- *
27
- * @since 0.3.16-canary.0
28
- */
29
- export function tokenName(dependency: Token<unknown> | Constructor): string {
30
- return dependency.name;
31
- }