@codefast/di 0.3.16-canary.2 → 0.4.0-canary.4
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 +16 -0
- package/README.md +5 -17
- package/dist/binding.d.mts +24 -24
- package/dist/constraints.d.mts +3 -3
- package/dist/container.d.mts +3 -3
- package/dist/container.mjs +89 -157
- package/dist/decorators/inject.d.mts +3 -2
- package/dist/decorators/inject.mjs +14 -41
- package/dist/decorators/injectable.mjs +4 -12
- package/dist/decorators/lifecycle-decorators.mjs +13 -74
- package/dist/dependency-graph.d.mts +1 -1
- package/dist/dependency-graph.mjs +6 -4
- package/dist/graph-adapters/cytoscape.d.mts +12 -12
- package/dist/graph-adapters/cytoscape.mjs +7 -7
- package/dist/graph-adapters/reactflow.d.mts +15 -15
- package/dist/graph-adapters/reactflow.mjs +6 -9
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/dist/inspector.d.mts +3 -2
- package/dist/inspector.mjs +7 -10
- package/dist/lifecycle.mjs +2 -12
- package/dist/metadata/metadata-keys.d.mts +8 -33
- package/dist/metadata/metadata-keys.mjs +8 -21
- package/dist/metadata/metadata-types.d.mts +5 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
- package/dist/metadata/symbol-metadata-reader.mjs +11 -33
- package/dist/module.mjs +1 -1
- package/dist/registry.mjs +3 -22
- package/dist/resolve-options.d.mts +2 -2
- package/dist/resolve-options.mjs +10 -8
- package/dist/resolver.d.mts +6 -1
- package/dist/resolver.mjs +15 -21
- package/dist/types.d.mts +10 -4
- package/package.json +40 -14
- package/src/binding-scope.ts +26 -0
- package/src/binding-select.ts +158 -0
- package/src/binding.ts +277 -0
- package/src/constraints.ts +121 -0
- package/src/constructor-type.ts +19 -0
- package/src/container.ts +1135 -0
- package/src/decorators/inject.ts +222 -0
- package/src/decorators/injectable.ts +85 -0
- package/src/decorators/lifecycle-decorators.ts +51 -0
- package/src/dependency-graph.ts +116 -0
- package/src/environment.ts +207 -0
- package/src/errors.ts +260 -0
- package/src/graph-adapters/cytoscape.ts +64 -0
- package/src/graph-adapters/dot.ts +22 -0
- package/src/graph-adapters/reactflow.ts +58 -0
- package/src/index.ts +101 -0
- package/src/inspector.ts +125 -0
- package/src/lifecycle.ts +217 -0
- package/src/metadata/metadata-keys.ts +25 -0
- package/src/metadata/metadata-reader-token.ts +8 -0
- package/src/metadata/metadata-types.ts +51 -0
- package/src/metadata/symbol-metadata-reader.ts +45 -0
- package/src/module.ts +93 -0
- package/src/registry.ts +232 -0
- package/src/resolve-options.ts +42 -0
- package/src/resolver.ts +1609 -0
- package/src/scope.ts +77 -0
- package/src/token.ts +40 -0
- package/src/types.ts +123 -0
- package/dist/graph-adapters/types.d.mts +0 -2
- package/dist/graph-adapters/types.mjs +0 -1
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Binding } from "#/binding";
|
|
2
|
+
import { AsyncActivationError, AsyncDeactivationError } from "#/errors";
|
|
3
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
4
|
+
import type { Token } from "#/token";
|
|
5
|
+
import { tokenName } from "#/token";
|
|
6
|
+
import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionContext } from "#/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @since 0.3.16-canary.0
|
|
10
|
+
*/
|
|
11
|
+
export class LifecycleManager {
|
|
12
|
+
// Container-level activation/deactivation hooks per token
|
|
13
|
+
private readonly _activationHooks = new Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>>();
|
|
14
|
+
private readonly _deactivationHooks = new Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>>();
|
|
15
|
+
private _activationVersion = 0;
|
|
16
|
+
|
|
17
|
+
registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
|
|
18
|
+
this._activationVersion += 1;
|
|
19
|
+
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
20
|
+
const list = this._activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
21
|
+
list.push(handler as ActivationHandler<unknown>);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
|
|
25
|
+
if (this._activationHooks.size === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const list = this._activationHooks.get(token as Token<unknown> | Constructor);
|
|
29
|
+
return list !== undefined && list.length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get activationVersion(): number {
|
|
33
|
+
return this._activationVersion;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerDeactivation<const Value>(
|
|
37
|
+
token: Token<Value> | Constructor<Value>,
|
|
38
|
+
handler: DeactivationHandler<Value>,
|
|
39
|
+
): void {
|
|
40
|
+
const list = this._deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
41
|
+
list.push(handler as DeactivationHandler<unknown>);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async runActivation<const Value>(
|
|
45
|
+
resolutionContext: ResolutionContext,
|
|
46
|
+
binding: Binding<Value>,
|
|
47
|
+
instance: Value,
|
|
48
|
+
metadataReader: MetadataReader,
|
|
49
|
+
): Promise<Value> {
|
|
50
|
+
let activatedInstance: Value = instance;
|
|
51
|
+
|
|
52
|
+
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
53
|
+
if (binding.kind === "class") {
|
|
54
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
55
|
+
if (lifecycle?.postConstruct && lifecycle.postConstruct.length > 0) {
|
|
56
|
+
for (const methodName of lifecycle.postConstruct) {
|
|
57
|
+
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
58
|
+
if (typeof method === "function") {
|
|
59
|
+
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
60
|
+
if (hookResult instanceof Promise) {
|
|
61
|
+
await hookResult;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. per-binding onActivation
|
|
69
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
70
|
+
const activationResult = binding.onActivation(resolutionContext, activatedInstance);
|
|
71
|
+
activatedInstance = activationResult instanceof Promise ? await activationResult : activationResult;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. container-level onActivation
|
|
75
|
+
const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
|
|
76
|
+
if (containerHooks !== undefined) {
|
|
77
|
+
for (const hook of containerHooks) {
|
|
78
|
+
const activationResult = hook(resolutionContext, activatedInstance);
|
|
79
|
+
activatedInstance = (activationResult instanceof Promise ? await activationResult : activationResult) as Value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return activatedInstance;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
runActivationSync<const Value>(
|
|
87
|
+
resolutionContext: ResolutionContext,
|
|
88
|
+
binding: Binding<Value>,
|
|
89
|
+
instance: Value,
|
|
90
|
+
metadataReader: MetadataReader,
|
|
91
|
+
): Value {
|
|
92
|
+
let activatedInstance: Value = instance;
|
|
93
|
+
|
|
94
|
+
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
95
|
+
if (binding.kind === "class") {
|
|
96
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
97
|
+
if (lifecycle?.postConstruct && lifecycle.postConstruct.length > 0) {
|
|
98
|
+
for (const methodName of lifecycle.postConstruct) {
|
|
99
|
+
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
100
|
+
if (typeof method === "function") {
|
|
101
|
+
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
102
|
+
if (hookResult instanceof Promise) {
|
|
103
|
+
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2. per-binding onActivation (must be sync)
|
|
111
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
112
|
+
const activationResult = binding.onActivation(resolutionContext, activatedInstance);
|
|
113
|
+
if (activationResult instanceof Promise) {
|
|
114
|
+
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
115
|
+
}
|
|
116
|
+
activatedInstance = activationResult;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3. container-level onActivation (must be sync)
|
|
120
|
+
const tokenDisplayName = tokenName(binding.token);
|
|
121
|
+
const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
|
|
122
|
+
if (containerHooks !== undefined) {
|
|
123
|
+
for (const hook of containerHooks) {
|
|
124
|
+
const activationResult = hook(resolutionContext, activatedInstance);
|
|
125
|
+
if (activationResult instanceof Promise) {
|
|
126
|
+
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
127
|
+
}
|
|
128
|
+
activatedInstance = activationResult as Value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return activatedInstance;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async runDeactivation<const Value>(
|
|
136
|
+
binding: Binding<Value>,
|
|
137
|
+
instance: Value,
|
|
138
|
+
metadataReader: MetadataReader,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
141
|
+
|
|
142
|
+
// 1. container-level onDeactivation
|
|
143
|
+
const containerHooks = this._deactivationHooks.get(tokenKey);
|
|
144
|
+
if (containerHooks !== undefined) {
|
|
145
|
+
for (const hook of containerHooks) {
|
|
146
|
+
const hookResult = hook(instance);
|
|
147
|
+
if (hookResult instanceof Promise) {
|
|
148
|
+
await hookResult;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. per-binding onDeactivation
|
|
154
|
+
if (binding.kind !== "alias" && binding.onDeactivation !== undefined) {
|
|
155
|
+
const hookResult = binding.onDeactivation(instance);
|
|
156
|
+
if (hookResult instanceof Promise) {
|
|
157
|
+
await hookResult;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. @preDestroy() — all methods in declaration order
|
|
162
|
+
if (binding.kind === "class") {
|
|
163
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
164
|
+
if (lifecycle?.preDestroy && lifecycle.preDestroy.length > 0) {
|
|
165
|
+
for (const methodName of lifecycle.preDestroy) {
|
|
166
|
+
const method = (instance as Record<string, unknown>)[methodName];
|
|
167
|
+
if (typeof method === "function") {
|
|
168
|
+
const hookResult = (method as () => unknown).call(instance);
|
|
169
|
+
if (hookResult instanceof Promise) {
|
|
170
|
+
await hookResult;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runDeactivationSync<const Value>(binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): void {
|
|
179
|
+
const tokenDisplayName = tokenName(binding.token);
|
|
180
|
+
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
181
|
+
|
|
182
|
+
// 1. container-level onDeactivation
|
|
183
|
+
const containerHooks = this._deactivationHooks.get(tokenKey);
|
|
184
|
+
if (containerHooks !== undefined) {
|
|
185
|
+
for (const hook of containerHooks) {
|
|
186
|
+
const hookResult = hook(instance);
|
|
187
|
+
if (hookResult instanceof Promise) {
|
|
188
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. per-binding onDeactivation
|
|
194
|
+
if (binding.kind !== "alias" && binding.onDeactivation !== undefined) {
|
|
195
|
+
const hookResult = binding.onDeactivation(instance);
|
|
196
|
+
if (hookResult instanceof Promise) {
|
|
197
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 3. @preDestroy()
|
|
202
|
+
if (binding.kind === "class") {
|
|
203
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
204
|
+
if (lifecycle?.preDestroy && lifecycle.preDestroy.length > 0) {
|
|
205
|
+
for (const methodName of lifecycle.preDestroy) {
|
|
206
|
+
const method = (instance as Record<string, unknown>)[methodName];
|
|
207
|
+
if (typeof method === "function") {
|
|
208
|
+
const hookResult = (method as () => unknown).call(instance);
|
|
209
|
+
if (hookResult instanceof Promise) {
|
|
210
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.3.16-canary.0
|
|
3
|
+
*/
|
|
4
|
+
export const INJECTABLE_KEY: unique symbol = Symbol("di:injectable");
|
|
5
|
+
/**
|
|
6
|
+
* @since 0.3.16-canary.0
|
|
7
|
+
*/
|
|
8
|
+
export const LIFECYCLE_KEY: unique symbol = Symbol("di:lifecycle");
|
|
9
|
+
/**
|
|
10
|
+
* @since 0.3.16-canary.0
|
|
11
|
+
*/
|
|
12
|
+
export const INJECT_ACCESSOR_KEY: unique symbol = Symbol("di:inject-accessor");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The well-known symbol used by TC39 Stage 3 decorator transforms to store class metadata.
|
|
16
|
+
*
|
|
17
|
+
* `Symbol.metadata` is defined natively once the runtime ships the full TC39 decorator
|
|
18
|
+
* proposal. Until then (current Node.js / browsers), Babel and esbuild both fall back to
|
|
19
|
+
* `Symbol.for("Symbol.metadata")` — a global-registry symbol with the same string key.
|
|
20
|
+
* Resolving it here once keeps the reader and the decorator transforms in sync regardless
|
|
21
|
+
* of which path is taken.
|
|
22
|
+
*
|
|
23
|
+
* @since 0.3.16-canary.0
|
|
24
|
+
*/
|
|
25
|
+
export const METADATA_SYMBOL: symbol = Symbol.metadata ?? Symbol.for("Symbol.metadata");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
2
|
+
import { token } from "#/token";
|
|
3
|
+
import type { Token } from "#/token";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @since 0.3.16-canary.0
|
|
7
|
+
*/
|
|
8
|
+
export const MetadataReaderToken: Token<MetadataReader> = token<MetadataReader>("MetadataReader");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
2
|
+
import type { Token } from "#/token";
|
|
3
|
+
import type { Constructor } from "#/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @since 0.3.16-canary.0
|
|
7
|
+
*/
|
|
8
|
+
export interface ParamMetadata {
|
|
9
|
+
readonly index: number;
|
|
10
|
+
readonly token: Token<unknown> | Constructor;
|
|
11
|
+
readonly optional: boolean;
|
|
12
|
+
readonly multi: boolean;
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
readonly tags?: ReadonlyArray<readonly [string, unknown]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @since 0.3.16-canary.0
|
|
19
|
+
*/
|
|
20
|
+
export interface ConstructorMetadata {
|
|
21
|
+
readonly params: ReadonlyArray<ParamMetadata>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @since 0.3.16-canary.0
|
|
26
|
+
*/
|
|
27
|
+
export interface LifecycleMetadata {
|
|
28
|
+
readonly postConstruct: ReadonlyArray<string>;
|
|
29
|
+
readonly preDestroy: ReadonlyArray<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mutable buckets used while aggregating decorator metadata (same keys as {@link LifecycleMetadata}).
|
|
34
|
+
*
|
|
35
|
+
* @since 0.3.16-canary.0
|
|
36
|
+
*/
|
|
37
|
+
export interface MutableLifecycleMetadata {
|
|
38
|
+
postConstruct: Array<string>;
|
|
39
|
+
preDestroy: Array<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @since 0.3.16-canary.0
|
|
44
|
+
*/
|
|
45
|
+
export interface MetadataReader {
|
|
46
|
+
getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined;
|
|
47
|
+
getLifecycleMetadata(target: Constructor): LifecycleMetadata | undefined;
|
|
48
|
+
getAccessorMetadata?(
|
|
49
|
+
target: Constructor,
|
|
50
|
+
): ReadonlyArray<{ readonly key: string | symbol; readonly descriptor: InjectionDescriptor }> | undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
2
|
+
import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#/metadata/metadata-keys";
|
|
3
|
+
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#/metadata/metadata-types";
|
|
4
|
+
import type { Constructor } from "#/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @since 0.3.16-canary.0
|
|
8
|
+
*/
|
|
9
|
+
export class SymbolMetadataReader implements MetadataReader {
|
|
10
|
+
private _getMetadataRecord(target: Constructor, key: string | symbol): Record<string | symbol, unknown> | undefined {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, METADATA_SYMBOL);
|
|
12
|
+
if (descriptor === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const record = descriptor.value as Record<string | symbol, unknown> | null | undefined;
|
|
16
|
+
if (!record || typeof record !== "object" || !Object.hasOwn(record, key)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return record;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
|
|
23
|
+
const record = this._getMetadataRecord(target, INJECTABLE_KEY);
|
|
24
|
+
return record?.[INJECTABLE_KEY] as ConstructorMetadata | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getLifecycleMetadata(target: Constructor): LifecycleMetadata | undefined {
|
|
28
|
+
const record = this._getMetadataRecord(target, LIFECYCLE_KEY);
|
|
29
|
+
return record?.[LIFECYCLE_KEY] as LifecycleMetadata | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getAccessorMetadata(
|
|
33
|
+
target: Constructor,
|
|
34
|
+
): Array<{ key: string | symbol; descriptor: InjectionDescriptor }> | undefined {
|
|
35
|
+
const record = this._getMetadataRecord(target, INJECT_ACCESSOR_KEY);
|
|
36
|
+
return record?.[INJECT_ACCESSOR_KEY] as
|
|
37
|
+
| Array<{ key: string | symbol; descriptor: InjectionDescriptor }>
|
|
38
|
+
| undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @since 0.3.16-canary.0
|
|
44
|
+
*/
|
|
45
|
+
export const defaultMetadataReader = new SymbolMetadataReader();
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { BindToBuilder } from "#/binding";
|
|
2
|
+
import type { Token } from "#/token";
|
|
3
|
+
import type { Constructor } from "#/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
|
+
* @since 0.3.16-canary.0
|
|
12
|
+
*/
|
|
13
|
+
export interface SyncModule {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly [SYNC_MODULE_BRAND]: true;
|
|
16
|
+
readonly _setup: (builder: ModuleBuilder) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @since 0.3.16-canary.0
|
|
21
|
+
*/
|
|
22
|
+
export interface AsyncModule {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly [ASYNC_MODULE_BRAND]: true;
|
|
25
|
+
readonly _setup: (builder: AsyncModuleBuilder) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Builder interfaces ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @since 0.3.16-canary.0
|
|
32
|
+
*/
|
|
33
|
+
export interface ModuleBuilder {
|
|
34
|
+
bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
35
|
+
import(...modules: Array<SyncModule>): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @since 0.3.16-canary.0
|
|
40
|
+
*/
|
|
41
|
+
export interface AsyncModuleBuilder {
|
|
42
|
+
bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
43
|
+
import(...modules: Array<SyncModule | AsyncModule>): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Static factories ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @since 0.3.16-canary.0
|
|
50
|
+
*/
|
|
51
|
+
export const SyncModule = {
|
|
52
|
+
create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
[SYNC_MODULE_BRAND]: true as const,
|
|
56
|
+
_setup: setup,
|
|
57
|
+
} as SyncModule;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @since 0.3.16-canary.0
|
|
63
|
+
*/
|
|
64
|
+
export const AsyncModule = {
|
|
65
|
+
create(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
[ASYNC_MODULE_BRAND]: true as const,
|
|
69
|
+
_setup: setup,
|
|
70
|
+
} as AsyncModule;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ── Module — unified API ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @since 0.3.16-canary.0
|
|
78
|
+
*/
|
|
79
|
+
export const Module = {
|
|
80
|
+
create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
|
|
81
|
+
return SyncModule.create(name, setup);
|
|
82
|
+
},
|
|
83
|
+
createAsync(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
|
|
84
|
+
return AsyncModule.create(name, setup);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @since 0.3.16-canary.0
|
|
90
|
+
*/
|
|
91
|
+
export function isSyncModule(module: SyncModule | AsyncModule): module is SyncModule {
|
|
92
|
+
return (module as SyncModule)[SYNC_MODULE_BRAND];
|
|
93
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Binding } from "#/binding";
|
|
2
|
+
import { bindingSlotEquals, bindingSlotToString } from "#/binding";
|
|
3
|
+
import type { Token } from "#/token";
|
|
4
|
+
import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @since 0.3.16-canary.0
|
|
8
|
+
*/
|
|
9
|
+
export class BindingRegistry {
|
|
10
|
+
// Map from token key -> array of bindings (order matters for last-wins)
|
|
11
|
+
private readonly _bindings = new Map<DependencyKey, Array<Binding>>();
|
|
12
|
+
// Fast lookup by binding ID
|
|
13
|
+
private readonly _byId = new Map<BindingIdentifier, Binding>();
|
|
14
|
+
// Fast lookup for slot { name, tags: [] }
|
|
15
|
+
private readonly _simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
|
|
16
|
+
// Fast path for one default slot binding with no predicate
|
|
17
|
+
private readonly _fastDefault = new Map<DependencyKey, Binding>();
|
|
18
|
+
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
|
|
19
|
+
private readonly _simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
|
|
20
|
+
|
|
21
|
+
/** Add or replace binding using slot-aware last-wins. */
|
|
22
|
+
add(binding: Binding): void {
|
|
23
|
+
const key = binding.token as DependencyKey;
|
|
24
|
+
// ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
|
|
25
|
+
const bindingsForToken = this._bindings.getOrInsert(key, []);
|
|
26
|
+
|
|
27
|
+
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
28
|
+
if (!this._isPurePredicateBinding(binding)) {
|
|
29
|
+
const existingIndex = bindingsForToken.findIndex(
|
|
30
|
+
(candidate) => !this._isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
31
|
+
);
|
|
32
|
+
if (existingIndex !== -1) {
|
|
33
|
+
const replacedBinding = bindingsForToken[existingIndex]!;
|
|
34
|
+
this._byId.delete(replacedBinding.id);
|
|
35
|
+
bindingsForToken.splice(existingIndex, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
bindingsForToken.push(binding);
|
|
40
|
+
this._byId.set(binding.id, binding);
|
|
41
|
+
this._indexSimpleNamedBinding(key, binding);
|
|
42
|
+
this._indexSimpleTaggedBinding(key, binding);
|
|
43
|
+
this._refreshFastDefaultForToken(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Remove all bindings for a token. Returns removed bindings. */
|
|
47
|
+
removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
|
|
48
|
+
const key = token as DependencyKey;
|
|
49
|
+
const bindingsForToken = this._bindings.get(key) ?? [];
|
|
50
|
+
this._bindings.delete(key);
|
|
51
|
+
this._simpleNamed.delete(key);
|
|
52
|
+
this._simpleTagged.delete(key);
|
|
53
|
+
this._fastDefault.delete(key);
|
|
54
|
+
for (const binding of bindingsForToken) {
|
|
55
|
+
this._byId.delete(binding.id);
|
|
56
|
+
}
|
|
57
|
+
return bindingsForToken;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Remove a specific binding by ID. Returns the removed binding or undefined. */
|
|
61
|
+
removeById(id: BindingIdentifier): Binding | undefined {
|
|
62
|
+
const binding = this._byId.get(id);
|
|
63
|
+
if (binding === undefined) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
this._byId.delete(id);
|
|
67
|
+
const key = binding.token as DependencyKey;
|
|
68
|
+
const bindingsForToken = this._bindings.get(key);
|
|
69
|
+
if (bindingsForToken !== undefined) {
|
|
70
|
+
const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
|
|
71
|
+
if (bindingIndex !== -1) {
|
|
72
|
+
bindingsForToken.splice(bindingIndex, 1);
|
|
73
|
+
}
|
|
74
|
+
this._deindexSimpleNamedBinding(key, binding);
|
|
75
|
+
this._deindexSimpleTaggedBinding(key, binding);
|
|
76
|
+
if (bindingsForToken.length === 0) {
|
|
77
|
+
this._bindings.delete(key);
|
|
78
|
+
this._simpleNamed.delete(key);
|
|
79
|
+
this._simpleTagged.delete(key);
|
|
80
|
+
this._fastDefault.delete(key);
|
|
81
|
+
} else {
|
|
82
|
+
this._refreshFastDefaultForToken(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return binding;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get all bindings for a token. */
|
|
89
|
+
getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
90
|
+
return this._bindings.get(token as DependencyKey) ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Get binding by ID. */
|
|
94
|
+
getById(id: BindingIdentifier): Binding | undefined {
|
|
95
|
+
return this._byId.get(id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if any binding exists for token. */
|
|
99
|
+
has(token: Token<unknown> | Constructor): boolean {
|
|
100
|
+
const key = token as DependencyKey;
|
|
101
|
+
const list = this._bindings.get(key);
|
|
102
|
+
return list !== undefined && list.length > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** All bindings in the registry. */
|
|
106
|
+
allBindings(): ReadonlyArray<Binding> {
|
|
107
|
+
const allBindings: Array<Binding> = [];
|
|
108
|
+
for (const bindingsForToken of this._bindings.values()) {
|
|
109
|
+
allBindings.push(...bindingsForToken);
|
|
110
|
+
}
|
|
111
|
+
return allBindings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Remove all bindings. Returns all removed. */
|
|
115
|
+
clear(): ReadonlyArray<Binding> {
|
|
116
|
+
const all = this.allBindings();
|
|
117
|
+
this._bindings.clear();
|
|
118
|
+
this._byId.clear();
|
|
119
|
+
this._simpleNamed.clear();
|
|
120
|
+
this._simpleTagged.clear();
|
|
121
|
+
this._fastDefault.clear();
|
|
122
|
+
return all;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
126
|
+
return this._simpleNamed.get(token as DependencyKey)?.get(name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
|
|
130
|
+
return this._simpleTagged
|
|
131
|
+
.get(token as DependencyKey)
|
|
132
|
+
?.get(tagKey)
|
|
133
|
+
?.get(tagValue);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
|
|
137
|
+
return this._fastDefault.get(token as DependencyKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Summarize available slot strings for a token (for error messages). */
|
|
141
|
+
availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
|
|
142
|
+
const bindingsForToken = this._bindings.get(token as DependencyKey) ?? [];
|
|
143
|
+
return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
147
|
+
const slot = binding.slot;
|
|
148
|
+
if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const [tagKey, tagValue] = slot.tags[0]!;
|
|
152
|
+
const byTagKey = this._simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
153
|
+
const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
|
|
154
|
+
byTagValue.set(tagValue, binding);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private _deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
158
|
+
const slot = binding.slot;
|
|
159
|
+
if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const [tagKey, tagValue] = slot.tags[0]!;
|
|
163
|
+
const byTagKey = this._simpleTagged.get(tokenKey);
|
|
164
|
+
if (byTagKey === undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const byTagValue = byTagKey.get(tagKey);
|
|
168
|
+
if (byTagValue === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const current = byTagValue.get(tagValue);
|
|
172
|
+
if (current?.id === binding.id) {
|
|
173
|
+
byTagValue.delete(tagValue);
|
|
174
|
+
if (byTagValue.size === 0) {
|
|
175
|
+
byTagKey.delete(tagKey);
|
|
176
|
+
if (byTagKey.size === 0) {
|
|
177
|
+
this._simpleTagged.delete(tokenKey);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private _isPurePredicateBinding(binding: Binding): boolean {
|
|
184
|
+
const slot = binding.slot;
|
|
185
|
+
const hasPredicate = binding.predicate !== undefined;
|
|
186
|
+
const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
|
|
187
|
+
// Pure predicate = has predicate but no slot constraint (name/tags)
|
|
188
|
+
return hasPredicate && !hasConstraint;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private _indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
192
|
+
const slot = binding.slot;
|
|
193
|
+
if (slot.name === undefined || slot.tags.length > 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const bindingsByName = this._simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
|
|
197
|
+
bindingsByName.set(slot.name, binding);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
201
|
+
const slot = binding.slot;
|
|
202
|
+
if (slot.name === undefined || slot.tags.length > 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const bindingsByName = this._simpleNamed.get(tokenKey);
|
|
206
|
+
if (bindingsByName === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const currentBinding = bindingsByName.get(slot.name);
|
|
210
|
+
if (currentBinding?.id === binding.id) {
|
|
211
|
+
bindingsByName.delete(slot.name);
|
|
212
|
+
if (bindingsByName.size === 0) {
|
|
213
|
+
this._simpleNamed.delete(tokenKey);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private _refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
219
|
+
const bindingsForToken = this._bindings.get(tokenKey);
|
|
220
|
+
if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
|
|
221
|
+
this._fastDefault.delete(tokenKey);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const onlyBinding = bindingsForToken[0]!;
|
|
225
|
+
const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
|
|
226
|
+
if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
|
|
227
|
+
this._fastDefault.delete(tokenKey);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this._fastDefault.set(tokenKey, onlyBinding);
|
|
231
|
+
}
|
|
232
|
+
}
|