@codefast/di 0.5.0 → 0.6.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.
- package/CHANGELOG.md +712 -0
- package/README.md +119 -30
- package/dist/ambient/active-container.d.ts +37 -0
- package/dist/ambient/active-container.d.ts.map +1 -0
- package/dist/ambient/active-container.js +41 -0
- package/dist/ambient/active-container.js.map +1 -0
- package/dist/container/binding-builders.d.ts +12 -8
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +62 -31
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts +33 -19
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +281 -118
- package/dist/container/container.js.map +1 -1
- package/dist/{resolution → core}/binding-scope.d.ts +2 -2
- package/dist/core/binding-scope.d.ts.map +1 -0
- package/dist/core/binding-scope.js.map +1 -0
- package/dist/{binding.d.ts → core/binding.d.ts} +70 -15
- package/dist/core/binding.d.ts.map +1 -0
- package/dist/{binding.js → core/binding.js} +27 -12
- package/dist/core/binding.js.map +1 -0
- package/dist/core/constraint-requirement.d.ts +53 -0
- package/dist/core/constraint-requirement.d.ts.map +1 -0
- package/dist/core/constraint-requirement.js +62 -0
- package/dist/core/constraint-requirement.js.map +1 -0
- package/dist/{constructor-type.d.ts → core/constructor-type.d.ts} +1 -1
- package/dist/core/constructor-type.d.ts.map +1 -0
- package/dist/core/constructor-type.js.map +1 -0
- package/dist/{module.d.ts → core/module.d.ts} +5 -5
- package/dist/core/module.d.ts.map +1 -0
- package/dist/{module.js → core/module.js} +4 -4
- package/dist/core/module.js.map +1 -0
- package/dist/{registry.d.ts → core/registry.d.ts} +27 -7
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/{registry.js → core/registry.js} +125 -52
- package/dist/core/registry.js.map +1 -0
- package/dist/core/tag.d.ts +80 -0
- package/dist/core/tag.d.ts.map +1 -0
- package/dist/core/tag.js +96 -0
- package/dist/core/tag.js.map +1 -0
- package/dist/{token.d.ts → core/token.d.ts} +2 -6
- package/dist/core/token.d.ts.map +1 -0
- package/dist/core/token.js +13 -0
- package/dist/core/token.js.map +1 -0
- package/dist/{types.d.ts → core/types.d.ts} +27 -21
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/decorators/inject.d.ts +5 -49
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js +34 -104
- package/dist/decorators/inject.js.map +1 -1
- package/dist/decorators/injectable.d.ts +25 -5
- package/dist/decorators/injectable.d.ts.map +1 -1
- package/dist/decorators/injectable.js +2 -3
- package/dist/decorators/injectable.js.map +1 -1
- package/dist/decorators/lifecycle-decorators.d.ts +4 -2
- package/dist/decorators/lifecycle-decorators.d.ts.map +1 -1
- package/dist/decorators/lifecycle-decorators.js +19 -25
- package/dist/decorators/lifecycle-decorators.js.map +1 -1
- package/dist/{resolution → errors}/diagnostics.d.ts +2 -0
- package/dist/errors/diagnostics.d.ts.map +1 -0
- package/dist/errors/diagnostics.js.map +1 -0
- package/dist/{errors.d.ts → errors/errors.d.ts} +89 -4
- package/dist/errors/errors.d.ts.map +1 -0
- package/dist/{errors.js → errors/errors.js} +129 -7
- package/dist/errors/errors.js.map +1 -0
- package/dist/index.d.ts +21 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/injection/descriptor.d.ts +79 -0
- package/dist/injection/descriptor.d.ts.map +1 -0
- package/dist/injection/descriptor.js +118 -0
- package/dist/injection/descriptor.js.map +1 -0
- package/dist/{resolution → injection}/resolve-options.d.ts +21 -11
- package/dist/injection/resolve-options.d.ts.map +1 -0
- package/dist/injection/resolve-options.js +101 -0
- package/dist/injection/resolve-options.js.map +1 -0
- package/dist/introspection/dependency-graph.d.ts +16 -5
- package/dist/introspection/dependency-graph.d.ts.map +1 -1
- package/dist/introspection/dependency-graph.js +138 -57
- package/dist/introspection/dependency-graph.js.map +1 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts +6 -3
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -1
- package/dist/introspection/graph-adapters/cytoscape.js +3 -0
- package/dist/introspection/graph-adapters/cytoscape.js.map +1 -1
- package/dist/introspection/graph-adapters/dot.d.ts.map +1 -1
- package/dist/introspection/graph-adapters/dot.js +12 -5
- package/dist/introspection/graph-adapters/dot.js.map +1 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts +9 -0
- package/dist/introspection/graph-adapters/mermaid.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/mermaid.js +47 -0
- package/dist/introspection/graph-adapters/mermaid.js.map +1 -0
- package/dist/introspection/graph-adapters/reactflow.d.ts +6 -3
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -1
- package/dist/introspection/graph-adapters/reactflow.js +5 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +1 -1
- package/dist/introspection/inspector.d.ts +4 -4
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +10 -6
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/lifecycle/lifecycle-manager.d.ts +27 -0
- package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
- package/dist/{resolution/lifecycle.js → lifecycle/lifecycle-manager.js} +39 -9
- package/dist/lifecycle/lifecycle-manager.js.map +1 -0
- package/dist/lifecycle/scope-manager.d.ts +48 -0
- package/dist/lifecycle/scope-manager.d.ts.map +1 -0
- package/dist/lifecycle/scope-manager.js +135 -0
- package/dist/lifecycle/scope-manager.js.map +1 -0
- package/dist/metadata/metadata-reader-token.d.ts +1 -1
- package/dist/metadata/metadata-reader-token.d.ts.map +1 -1
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/metadata/metadata-reader-token.js.map +1 -1
- package/dist/metadata/metadata-types.d.ts +9 -9
- package/dist/metadata/metadata-types.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts +19 -6
- package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.js +89 -9
- package/dist/metadata/symbol-metadata-reader.js.map +1 -1
- package/dist/metadata/verifying-metadata-reader.d.ts +16 -0
- package/dist/metadata/verifying-metadata-reader.d.ts.map +1 -0
- package/dist/metadata/verifying-metadata-reader.js +32 -0
- package/dist/metadata/verifying-metadata-reader.js.map +1 -0
- package/dist/resolution/{activation-need.d.ts → cache/activation-need.d.ts} +5 -5
- package/dist/resolution/cache/activation-need.d.ts.map +1 -0
- package/dist/resolution/cache/activation-need.js.map +1 -0
- package/dist/resolution/{binding-lookup-cache.d.ts → cache/binding-lookup-cache.d.ts} +9 -5
- package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/{binding-lookup-cache.js → cache/binding-lookup-cache.js} +55 -0
- package/dist/resolution/cache/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/cache/class-introspector.d.ts +66 -0
- package/dist/resolution/cache/class-introspector.d.ts.map +1 -0
- package/dist/resolution/cache/class-introspector.js +194 -0
- package/dist/resolution/cache/class-introspector.js.map +1 -0
- package/dist/resolution/context.d.ts +84 -0
- package/dist/resolution/context.d.ts.map +1 -0
- package/dist/resolution/{environment.js → context.js} +54 -80
- package/dist/resolution/context.js.map +1 -0
- package/dist/resolution/path/resolution-path.d.ts +94 -0
- package/dist/resolution/path/resolution-path.d.ts.map +1 -0
- package/dist/resolution/path/resolution-path.js +117 -0
- package/dist/resolution/path/resolution-path.js.map +1 -0
- package/dist/resolution/{instantiation-plan.d.ts → plan/instantiation-plan.d.ts} +37 -7
- package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -0
- package/dist/resolution/plan/instantiation-plan.js +419 -0
- package/dist/resolution/plan/instantiation-plan.js.map +1 -0
- package/dist/resolution/resolver.d.ts +28 -28
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +581 -239
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/select/binding-select.d.ts +34 -0
- package/dist/resolution/select/binding-select.d.ts.map +1 -0
- package/dist/resolution/select/binding-select.js +175 -0
- package/dist/resolution/select/binding-select.js.map +1 -0
- package/dist/resolution/{constraints.d.ts → select/constraints.d.ts} +13 -12
- package/dist/resolution/select/constraints.d.ts.map +1 -0
- package/dist/resolution/{constraints.js → select/constraints.js} +33 -22
- package/dist/resolution/select/constraints.js.map +1 -0
- package/package.json +160 -20
- package/src/ambient/active-container.ts +65 -0
- package/src/container/binding-builders.ts +79 -44
- package/src/container/container.ts +357 -170
- package/src/{resolution → core}/binding-scope.ts +2 -2
- package/src/{binding.ts → core/binding.ts} +101 -39
- package/src/core/constraint-requirement.ts +94 -0
- package/src/{constructor-type.ts → core/constructor-type.ts} +1 -1
- package/src/{module.ts → core/module.ts} +10 -10
- package/src/core/registry.ts +365 -0
- package/src/core/tag.ts +158 -0
- package/src/{token.ts → core/token.ts} +2 -14
- package/src/{types.ts → core/types.ts} +37 -34
- package/src/decorators/inject.ts +39 -175
- package/src/decorators/injectable.ts +40 -10
- package/src/decorators/lifecycle-decorators.ts +22 -34
- package/src/{resolution → errors}/diagnostics.ts +2 -0
- package/src/{errors.ts → errors/errors.ts} +152 -8
- package/src/index.ts +43 -15
- package/src/injection/descriptor.ts +206 -0
- package/src/injection/resolve-options.ts +144 -0
- package/src/introspection/dependency-graph.ts +223 -63
- package/src/introspection/graph-adapters/cytoscape.ts +9 -3
- package/src/introspection/graph-adapters/dot.ts +13 -5
- package/src/introspection/graph-adapters/mermaid.ts +61 -0
- package/src/introspection/graph-adapters/reactflow.ts +11 -4
- package/src/introspection/inspector.ts +16 -12
- package/src/{resolution/lifecycle.ts → lifecycle/lifecycle-manager.ts} +68 -32
- package/src/lifecycle/scope-manager.ts +156 -0
- package/src/metadata/metadata-reader-token.ts +2 -2
- package/src/metadata/metadata-types.ts +9 -9
- package/src/metadata/symbol-metadata-reader.ts +95 -18
- package/src/metadata/verifying-metadata-reader.ts +41 -0
- package/src/resolution/{activation-need.ts → cache/activation-need.ts} +8 -8
- package/src/resolution/{binding-lookup-cache.ts → cache/binding-lookup-cache.ts} +63 -5
- package/src/resolution/cache/class-introspector.ts +251 -0
- package/src/resolution/{environment.ts → context.ts} +84 -145
- package/src/resolution/path/resolution-path.ts +167 -0
- package/src/resolution/plan/instantiation-plan.ts +615 -0
- package/src/resolution/resolver.ts +663 -313
- package/src/resolution/select/binding-select.ts +210 -0
- package/src/resolution/select/constraints.ts +130 -0
- package/dist/binding.d.ts.map +0 -1
- package/dist/binding.js.map +0 -1
- package/dist/constructor-type.d.ts.map +0 -1
- package/dist/constructor-type.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/module.d.ts.map +0 -1
- package/dist/module.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/resolution/activation-need.d.ts.map +0 -1
- package/dist/resolution/activation-need.js.map +0 -1
- package/dist/resolution/binding-lookup-cache.d.ts.map +0 -1
- package/dist/resolution/binding-lookup-cache.js.map +0 -1
- package/dist/resolution/binding-scope.d.ts.map +0 -1
- package/dist/resolution/binding-scope.js.map +0 -1
- package/dist/resolution/binding-select.d.ts +0 -23
- package/dist/resolution/binding-select.d.ts.map +0 -1
- package/dist/resolution/binding-select.js +0 -121
- package/dist/resolution/binding-select.js.map +0 -1
- package/dist/resolution/class-introspector.d.ts +0 -27
- package/dist/resolution/class-introspector.d.ts.map +0 -1
- package/dist/resolution/class-introspector.js +0 -60
- package/dist/resolution/class-introspector.js.map +0 -1
- package/dist/resolution/constraints.d.ts.map +0 -1
- package/dist/resolution/constraints.js.map +0 -1
- package/dist/resolution/diagnostics.d.ts.map +0 -1
- package/dist/resolution/diagnostics.js.map +0 -1
- package/dist/resolution/environment.d.ts +0 -94
- package/dist/resolution/environment.d.ts.map +0 -1
- package/dist/resolution/environment.js.map +0 -1
- package/dist/resolution/instantiation-plan.d.ts.map +0 -1
- package/dist/resolution/instantiation-plan.js +0 -183
- package/dist/resolution/instantiation-plan.js.map +0 -1
- package/dist/resolution/lifecycle.d.ts +0 -23
- package/dist/resolution/lifecycle.d.ts.map +0 -1
- package/dist/resolution/lifecycle.js.map +0 -1
- package/dist/resolution/resolution-path.d.ts +0 -98
- package/dist/resolution/resolution-path.d.ts.map +0 -1
- package/dist/resolution/resolution-path.js +0 -98
- package/dist/resolution/resolution-path.js.map +0 -1
- package/dist/resolution/resolve-options.d.ts.map +0 -1
- package/dist/resolution/resolve-options.js +0 -56
- package/dist/resolution/resolve-options.js.map +0 -1
- package/dist/resolution/scope.d.ts +0 -28
- package/dist/resolution/scope.d.ts.map +0 -1
- package/dist/resolution/scope.js +0 -88
- package/dist/resolution/scope.js.map +0 -1
- package/dist/token.d.ts.map +0 -1
- package/dist/token.js +0 -22
- package/dist/token.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/src/registry.ts +0 -282
- package/src/resolution/binding-select.ts +0 -154
- package/src/resolution/class-introspector.ts +0 -74
- package/src/resolution/constraints.ts +0 -121
- package/src/resolution/instantiation-plan.ts +0 -292
- package/src/resolution/resolution-path.ts +0 -156
- package/src/resolution/resolve-options.ts +0 -89
- package/src/resolution/scope.ts +0 -105
- /package/dist/{resolution → core}/binding-scope.js +0 -0
- /package/dist/{constructor-type.js → core/constructor-type.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{resolution → errors}/diagnostics.js +0 -0
- /package/dist/resolution/{activation-need.js → cache/activation-need.js} +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import type { Binding } from "#/core/binding";
|
|
2
|
+
import { bindingSlotEquals, bindingSlotToString } from "#/core/binding";
|
|
3
|
+
import type { BindingTag } from "#/core/tag";
|
|
4
|
+
import type { Token } from "#/core/token";
|
|
5
|
+
import type { BindingIdentifier, Constructor, DependencyKey } from "#/core/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @since 0.3.16-canary.0
|
|
9
|
+
*/
|
|
10
|
+
export class BindingRegistry {
|
|
11
|
+
// Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
|
|
12
|
+
#version = 0;
|
|
13
|
+
// Map from token key -> array of bindings (order matters for last-wins)
|
|
14
|
+
readonly #bindings = new Map<DependencyKey, Array<Binding>>();
|
|
15
|
+
// Fast lookup by binding ID
|
|
16
|
+
readonly #byId = new Map<BindingIdentifier, Binding>();
|
|
17
|
+
// Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
|
|
18
|
+
#simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
|
|
19
|
+
// Fast path for one default slot binding with no predicate
|
|
20
|
+
readonly #fastDefault = new Map<DependencyKey, Binding>();
|
|
21
|
+
// Fast lookup for a slot carrying exactly one tag — keyed by the interned criterion itself, so
|
|
22
|
+
// the (key, value) pair is one hash rather than two. Unallocated until a tagged binding lands.
|
|
23
|
+
#simpleTagged: Map<DependencyKey, Map<BindingTag, Binding>> | undefined;
|
|
24
|
+
// Name-less slots with two or more tags, bucketed by their FIRST criterion. A matching slot's
|
|
25
|
+
// every tag is in the request, so walking the request's buckets finds each candidate exactly once.
|
|
26
|
+
#multiTagged: Map<DependencyKey, Map<BindingTag, Array<Binding>>> | undefined;
|
|
27
|
+
|
|
28
|
+
// Set on the first constant registered and never cleared. Teardown only needs the negative answer
|
|
29
|
+
// to be exact, and that is what lets a container holding no constant skip its sweep entirely.
|
|
30
|
+
#heldConstantBinding = false;
|
|
31
|
+
|
|
32
|
+
/** Monotonic version — increments on every mutation. */
|
|
33
|
+
get version(): number {
|
|
34
|
+
return this.#version;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
|
|
38
|
+
get hasHeldConstantBinding(): boolean {
|
|
39
|
+
return this.#heldConstantBinding;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registers a mutation the indexes don't care about (a fluent chain refining scope or an
|
|
44
|
+
* activation hook in place), so version-stamped resolver caches still invalidate.
|
|
45
|
+
*/
|
|
46
|
+
touch(): void {
|
|
47
|
+
this.#version += 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds or replaces a binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
52
|
+
*
|
|
53
|
+
* @remarks The binding is stored by reference — it must come from `createBinding`, which is
|
|
54
|
+
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
55
|
+
*/
|
|
56
|
+
add(binding: Binding): Binding | undefined {
|
|
57
|
+
this.#version += 1;
|
|
58
|
+
if (binding.kind === "constant") {
|
|
59
|
+
this.#heldConstantBinding = true;
|
|
60
|
+
}
|
|
61
|
+
const key: DependencyKey = binding.token;
|
|
62
|
+
// Copy-on-write: a selection may be walking the current list inside a `when()` predicate, so
|
|
63
|
+
// mutation replaces the array and never splices one that has been handed out.
|
|
64
|
+
const bindingsForToken = this.#bindings.get(key);
|
|
65
|
+
|
|
66
|
+
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
67
|
+
let displacedBinding: Binding | undefined;
|
|
68
|
+
let nextBindings: Array<Binding>;
|
|
69
|
+
if (bindingsForToken === undefined) {
|
|
70
|
+
nextBindings = [binding];
|
|
71
|
+
} else {
|
|
72
|
+
if (!isPurePredicateBinding(binding)) {
|
|
73
|
+
const existingIndex = bindingsForToken.findIndex(
|
|
74
|
+
(candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
75
|
+
);
|
|
76
|
+
if (existingIndex !== -1) {
|
|
77
|
+
displacedBinding = bindingsForToken[existingIndex]!;
|
|
78
|
+
this.#byId.delete(displacedBinding.id);
|
|
79
|
+
this.#deindexSimpleNamedBinding(key, displacedBinding);
|
|
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.#indexSimpleNamedBinding(key, binding);
|
|
93
|
+
this.#indexSimpleTaggedBinding(key, binding);
|
|
94
|
+
this.#indexMultiTaggedBinding(key, binding);
|
|
95
|
+
this.#refreshFastDefaultForToken(key);
|
|
96
|
+
return displacedBinding;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Remove all bindings for a token. Returns removed bindings. */
|
|
100
|
+
removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
|
|
101
|
+
this.#version += 1;
|
|
102
|
+
const key: DependencyKey = token;
|
|
103
|
+
const bindingsForToken = this.#bindings.get(key) ?? [];
|
|
104
|
+
this.#bindings.delete(key);
|
|
105
|
+
this.#simpleNamed?.delete(key);
|
|
106
|
+
this.#simpleTagged?.delete(key);
|
|
107
|
+
this.#multiTagged?.delete(key);
|
|
108
|
+
this.#fastDefault.delete(key);
|
|
109
|
+
for (const binding of bindingsForToken) {
|
|
110
|
+
this.#byId.delete(binding.id);
|
|
111
|
+
}
|
|
112
|
+
return bindingsForToken;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Remove a specific binding by ID. Returns the removed binding or undefined. */
|
|
116
|
+
removeById(id: BindingIdentifier): Binding | undefined {
|
|
117
|
+
const binding = this.#byId.get(id);
|
|
118
|
+
if (binding === undefined) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
this.#version += 1;
|
|
122
|
+
this.#byId.delete(id);
|
|
123
|
+
const key: DependencyKey = binding.token;
|
|
124
|
+
const bindingsForToken = this.#bindings.get(key);
|
|
125
|
+
if (bindingsForToken !== undefined) {
|
|
126
|
+
const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
|
|
127
|
+
// Copy-on-write, like `add`: a walk holding the current array must not lose its place.
|
|
128
|
+
const remaining = bindingIndex === -1 ? bindingsForToken : bindingsForToken.toSpliced(bindingIndex, 1);
|
|
129
|
+
this.#deindexSimpleNamedBinding(key, binding);
|
|
130
|
+
this.#deindexSimpleTaggedBinding(key, binding);
|
|
131
|
+
this.#deindexMultiTaggedBinding(key, binding);
|
|
132
|
+
if (remaining.length === 0) {
|
|
133
|
+
this.#bindings.delete(key);
|
|
134
|
+
this.#simpleNamed?.delete(key);
|
|
135
|
+
this.#simpleTagged?.delete(key);
|
|
136
|
+
this.#multiTagged?.delete(key);
|
|
137
|
+
this.#fastDefault.delete(key);
|
|
138
|
+
} else {
|
|
139
|
+
this.#bindings.set(key, remaining);
|
|
140
|
+
this.#refreshFastDefaultForToken(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return binding;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Get all bindings for a token. */
|
|
147
|
+
getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
148
|
+
return this.#bindings.get(token) ?? [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get binding by ID. */
|
|
152
|
+
getById(id: BindingIdentifier): Binding | undefined {
|
|
153
|
+
return this.#byId.get(id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Check if any binding exists for token. */
|
|
157
|
+
has(token: Token<unknown> | Constructor): boolean {
|
|
158
|
+
const key: DependencyKey = token;
|
|
159
|
+
const list = this.#bindings.get(key);
|
|
160
|
+
return list !== undefined && list.length > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** All bindings in the registry. */
|
|
164
|
+
allBindings(): ReadonlyArray<Binding> {
|
|
165
|
+
const allBindings: Array<Binding> = [];
|
|
166
|
+
for (const bindingsForToken of this.#bindings.values()) {
|
|
167
|
+
allBindings.push(...bindingsForToken);
|
|
168
|
+
}
|
|
169
|
+
return allBindings;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Remove all bindings. Returns all removed. */
|
|
173
|
+
clear(): ReadonlyArray<Binding> {
|
|
174
|
+
this.#version += 1;
|
|
175
|
+
const all = this.allBindings();
|
|
176
|
+
this.#bindings.clear();
|
|
177
|
+
this.#byId.clear();
|
|
178
|
+
this.#simpleNamed?.clear();
|
|
179
|
+
this.#simpleTagged?.clear();
|
|
180
|
+
this.#multiTagged?.clear();
|
|
181
|
+
this.#fastDefault.clear();
|
|
182
|
+
return all;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
|
|
186
|
+
hasSlotOccupant(binding: Binding): boolean {
|
|
187
|
+
if (isPurePredicateBinding(binding)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const candidates = this.#bindings.get(binding.token);
|
|
191
|
+
if (candidates === undefined) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return candidates.some(
|
|
195
|
+
(candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
200
|
+
return this.#simpleNamed?.get(token)?.get(name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The binding indexed under one criterion.
|
|
205
|
+
*
|
|
206
|
+
* @remarks Exact, with no re-check: criteria are interned, so a `Map` keyed by the pair answers by
|
|
207
|
+
* identity — where a value-keyed map answered by SameValueZero and parted from `Object.is` on ±0.
|
|
208
|
+
*/
|
|
209
|
+
getSimpleTagged(token: Token<unknown> | Constructor, criterion: BindingTag): Binding | undefined {
|
|
210
|
+
return this.#simpleTagged?.get(token)?.get(criterion);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The multi-tag bindings whose slot's first criterion is `criterion`.
|
|
215
|
+
*
|
|
216
|
+
* @remarks A prefilter, not an answer: a bucket member's remaining tags still have to be matched
|
|
217
|
+
* against the request — first-criterion bucketing only guarantees each candidate appears once.
|
|
218
|
+
*/
|
|
219
|
+
getMultiTagged(token: Token<unknown> | Constructor, criterion: BindingTag): ReadonlyArray<Binding> | undefined {
|
|
220
|
+
return this.#multiTagged?.get(token)?.get(criterion);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
|
|
224
|
+
return this.#fastDefault.get(token);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Summarize available slot strings for a token (for error messages). */
|
|
228
|
+
availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
|
|
229
|
+
const bindingsForToken = this.#bindings.get(token) ?? [];
|
|
230
|
+
return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
234
|
+
const criterion = simpleTagOf(binding);
|
|
235
|
+
if (criterion === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const byCriterion = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<BindingTag, Binding>());
|
|
239
|
+
byCriterion.set(criterion, binding);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
243
|
+
const criterion = simpleTagOf(binding);
|
|
244
|
+
if (criterion === undefined) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const byCriterion = this.#simpleTagged?.get(tokenKey);
|
|
248
|
+
if (byCriterion === undefined) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (byCriterion.get(criterion)?.id === binding.id) {
|
|
252
|
+
byCriterion.delete(criterion);
|
|
253
|
+
if (byCriterion.size === 0) {
|
|
254
|
+
this.#simpleTagged!.delete(tokenKey);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#indexMultiTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
260
|
+
const firstCriterion = multiTagFirstOf(binding);
|
|
261
|
+
if (firstCriterion === undefined) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const buckets = (this.#multiTagged ??= new Map()).getOrInsert(tokenKey, new Map<BindingTag, Array<Binding>>());
|
|
265
|
+
buckets.getOrInsert(firstCriterion, []).push(binding);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#deindexMultiTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
269
|
+
const firstCriterion = multiTagFirstOf(binding);
|
|
270
|
+
if (firstCriterion === undefined) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const bucket = this.#multiTagged?.get(tokenKey)?.get(firstCriterion);
|
|
274
|
+
if (bucket === undefined) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const bindingIndex = bucket.findIndex((candidate) => candidate.id === binding.id);
|
|
278
|
+
// Spliced in place: nothing walks a bucket while user code runs — candidates are gathered
|
|
279
|
+
// into their own array before any predicate is evaluated.
|
|
280
|
+
if (bindingIndex !== -1) {
|
|
281
|
+
bucket.splice(bindingIndex, 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
286
|
+
const name = simpleNameOf(binding);
|
|
287
|
+
if (name === undefined) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
|
|
291
|
+
bindingsByName.set(name, binding);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
295
|
+
const name = simpleNameOf(binding);
|
|
296
|
+
if (name === undefined) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
300
|
+
if (bindingsByName === undefined) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (bindingsByName.get(name)?.id === binding.id) {
|
|
304
|
+
bindingsByName.delete(name);
|
|
305
|
+
if (bindingsByName.size === 0) {
|
|
306
|
+
this.#simpleNamed!.delete(tokenKey);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
312
|
+
const bindingsForToken = this.#bindings.get(tokenKey);
|
|
313
|
+
const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
|
|
314
|
+
if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
|
|
315
|
+
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.#fastDefault.delete(tokenKey);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Whether the deferred named-slot index has had to be built. */
|
|
322
|
+
get isNamedIndexBuilt(): boolean {
|
|
323
|
+
return this.#simpleNamed !== undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Whether the deferred tagged-slot index has had to be built. */
|
|
327
|
+
get isTaggedIndexBuilt(): boolean {
|
|
328
|
+
return this.#simpleTagged !== undefined;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The name a binding is indexed under, or `undefined` when its slot is more than a plain name. */
|
|
333
|
+
function simpleNameOf(binding: Binding): string | undefined {
|
|
334
|
+
const { name, tags } = binding.slot;
|
|
335
|
+
return name !== undefined && tags.length === 0 ? name : undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The tag a binding is indexed under, or `undefined` when its slot is more than one plain tag.
|
|
340
|
+
*
|
|
341
|
+
* @remarks Carries predicate-bearing bindings too, exactly as the name index does: every lane that
|
|
342
|
+
* reads this index already re-checks what it finds, so an indexed hit was never unconditional.
|
|
343
|
+
*/
|
|
344
|
+
function simpleTagOf(binding: Binding): BindingTag | undefined {
|
|
345
|
+
const { name, tags } = binding.slot;
|
|
346
|
+
return name === undefined && tags.length === 1 ? tags[0] : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The first criterion a multi-tag slot is bucketed under, or `undefined` for any other shape. */
|
|
350
|
+
function multiTagFirstOf(binding: Binding): BindingTag | undefined {
|
|
351
|
+
const { name, tags } = binding.slot;
|
|
352
|
+
return name === undefined && tags.length >= 2 ? tags[0] : undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A binding nothing has to be matched against: the default slot, no predicate. */
|
|
356
|
+
function isDefaultSlotBinding(binding: Binding): boolean {
|
|
357
|
+
const { name, tags } = binding.slot;
|
|
358
|
+
return name === undefined && tags.length === 0 && binding.predicate === undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** A predicate with no slot constraint: last-wins does not apply to it. */
|
|
362
|
+
function isPurePredicateBinding(binding: Binding): boolean {
|
|
363
|
+
const { name, tags } = binding.slot;
|
|
364
|
+
return binding.predicate !== undefined && name === undefined && tags.length === 0;
|
|
365
|
+
}
|
package/src/core/tag.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
|
|
63
|
+
let tagKeyCounter = -1;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Distinguishes `-0` from `+0` in the intern cache.
|
|
67
|
+
*
|
|
68
|
+
* @remarks A `Map` key compares by SameValueZero, which holds `+0` and `-0` equal, while tag values
|
|
69
|
+
* compare by `Object.is`, which does not. Interning them to one object would make the
|
|
70
|
+
* two indistinguishable everywhere downstream, so the negative one is cached under this instead.
|
|
71
|
+
*/
|
|
72
|
+
const NEGATIVE_ZERO_KEY: unique symbol = Symbol("di:tag-negative-zero");
|
|
73
|
+
|
|
74
|
+
function internKeyFor(value: unknown): unknown {
|
|
75
|
+
return value === 0 && Object.is(value, -0) ? NEGATIVE_ZERO_KEY : value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
|
|
80
|
+
*
|
|
81
|
+
* @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
|
|
82
|
+
* value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const Region = tag<"eu" | "us">("region");
|
|
87
|
+
* container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
|
|
88
|
+
* container.resolve(Storage, { tag: Region.of("eu") });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @since 0.6.0
|
|
92
|
+
*/
|
|
93
|
+
export function tag<Value = unknown>(name: string): TagKey<Value> {
|
|
94
|
+
tagKeyCounter += 1;
|
|
95
|
+
const id = tagKeyCounter;
|
|
96
|
+
const mask = (1 << (id % MASK_WIDTH)) as TagKeyMask;
|
|
97
|
+
const interned = new Map<unknown, BindingTag<Value>>();
|
|
98
|
+
// One-entry cache in front of the intern map: an inline `.of()` at a call site usually repeats
|
|
99
|
+
// one value, and `Object.is` is the slot contract's own comparison, so a hit is exact — ±0 stay
|
|
100
|
+
// split and `NaN` hits itself, with no `internKeyFor` detour.
|
|
101
|
+
let lastValue: Value | undefined;
|
|
102
|
+
let lastPair: BindingTag<Value> | undefined;
|
|
103
|
+
|
|
104
|
+
const key: TagKey<Value> = {
|
|
105
|
+
name,
|
|
106
|
+
id,
|
|
107
|
+
mask,
|
|
108
|
+
of(value: Value): BindingTag<Value> {
|
|
109
|
+
if (lastPair !== undefined && Object.is(value, lastValue)) {
|
|
110
|
+
return lastPair;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cacheKey = internKeyFor(value);
|
|
114
|
+
const existing = interned.get(cacheKey);
|
|
115
|
+
|
|
116
|
+
if (existing !== undefined) {
|
|
117
|
+
lastValue = value;
|
|
118
|
+
lastPair = existing;
|
|
119
|
+
|
|
120
|
+
return existing;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const pair = { key, value, mask } as BindingTag<Value>;
|
|
124
|
+
|
|
125
|
+
interned.set(cacheKey, pair);
|
|
126
|
+
lastValue = value;
|
|
127
|
+
lastPair = pair;
|
|
128
|
+
|
|
129
|
+
return pair;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return key;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The key set a list of criteria covers.
|
|
138
|
+
*
|
|
139
|
+
* @since 0.6.0
|
|
140
|
+
*/
|
|
141
|
+
export function tagKeyMaskOf(tags: ReadonlyArray<BindingTag>): TagKeyMask {
|
|
142
|
+
let mask = NO_TAG_KEYS;
|
|
143
|
+
|
|
144
|
+
for (let index = 0; index < tags.length; index += 1) {
|
|
145
|
+
mask = (mask | tags[index]!.mask) as TagKeyMask;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return mask;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether a request carrying `requestMask` covers every key in `slotMask`.
|
|
153
|
+
*
|
|
154
|
+
* @since 0.6.0
|
|
155
|
+
*/
|
|
156
|
+
export function coversTagKeys(requestMask: TagKeyMask, slotMask: TagKeyMask): boolean {
|
|
157
|
+
return (requestMask & slotMask) === slotMask;
|
|
158
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Constructor } from "#/constructor-type";
|
|
1
|
+
import type { Constructor } from "#/core/constructor-type";
|
|
2
2
|
|
|
3
3
|
declare const TOKEN_BRAND: unique symbol;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @since 0.3.16-canary.0
|
|
7
7
|
*/
|
|
8
|
-
export interface Token<Value> {
|
|
8
|
+
export interface Token<out Value> {
|
|
9
9
|
readonly name: string;
|
|
10
10
|
readonly [TOKEN_BRAND]: Value;
|
|
11
11
|
}
|
|
@@ -23,15 +23,3 @@ export function token<Value>(name: string): Token<Value> {
|
|
|
23
23
|
export function tokenName(dependency: Token<unknown> | Constructor): string {
|
|
24
24
|
return dependency.name;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @since 0.3.16-canary.0
|
|
29
|
-
*/
|
|
30
|
-
export function isToken(value: unknown): value is Token<unknown> {
|
|
31
|
-
return (
|
|
32
|
-
typeof value === "object" &&
|
|
33
|
-
value !== null &&
|
|
34
|
-
"name" in value &&
|
|
35
|
-
typeof (value as Record<string, unknown>)["name"] === "string"
|
|
36
|
-
);
|
|
37
|
-
}
|