@codefast/di 0.3.16-canary.2 → 0.3.16-canary.3
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 +8 -0
- package/dist/binding.d.mts +24 -24
- package/dist/constraints.d.mts +3 -3
- package/dist/container.mjs +82 -150
- package/dist/decorators/inject.d.mts +3 -2
- package/dist/decorators/inject.mjs +13 -40
- package/dist/decorators/injectable.mjs +4 -12
- package/dist/decorators/lifecycle-decorators.mjs +11 -72
- 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 +3 -3
- 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/registry.mjs +3 -22
- package/dist/resolve-options.d.mts +2 -2
- package/dist/resolve-options.mjs +10 -8
- package/dist/resolver.d.mts +5 -0
- package/dist/resolver.mjs +15 -21
- package/dist/types.d.mts +10 -4
- package/package.json +39 -13
- package/src/binding-scope.ts +26 -0
- package/src/binding-select.ts +167 -0
- package/src/binding.ts +281 -0
- package/src/constraints.ts +149 -0
- package/src/constructor-type.ts +19 -0
- package/src/container.ts +1213 -0
- package/src/decorators/inject.ts +233 -0
- package/src/decorators/injectable.ts +85 -0
- package/src/decorators/lifecycle-decorators.ts +55 -0
- package/src/dependency-graph.ts +116 -0
- package/src/environment.ts +232 -0
- package/src/errors.ts +262 -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 +133 -0
- package/src/lifecycle.ts +238 -0
- package/src/metadata/metadata-keys.ts +25 -0
- package/src/metadata/metadata-reader-token.ts +8 -0
- package/src/metadata/metadata-types.ts +53 -0
- package/src/metadata/symbol-metadata-reader.ts +57 -0
- package/src/module.ts +93 -0
- package/src/registry.ts +241 -0
- package/src/resolve-options.ts +42 -0
- package/src/resolver.ts +1837 -0
- package/src/scope.ts +77 -0
- package/src/token.ts +40 -0
- package/src/types.ts +145 -0
- package/dist/graph-adapters/types.d.mts +0 -2
- package/dist/graph-adapters/types.mjs +0 -1
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Foundation types
|
|
2
|
+
export type {
|
|
3
|
+
ActivationHandler,
|
|
4
|
+
BindingIdentifier,
|
|
5
|
+
BindingKind,
|
|
6
|
+
BindingScope,
|
|
7
|
+
BindingTag,
|
|
8
|
+
ConstraintContext,
|
|
9
|
+
Constructor,
|
|
10
|
+
DependencyKey,
|
|
11
|
+
DeactivationHandler,
|
|
12
|
+
ResolutionFrame,
|
|
13
|
+
ResolveOptions,
|
|
14
|
+
ResolutionContext,
|
|
15
|
+
TokenValue,
|
|
16
|
+
} from "#/types";
|
|
17
|
+
|
|
18
|
+
// Token
|
|
19
|
+
export { token, tokenName, isToken } from "#/token";
|
|
20
|
+
export type { Token } from "#/token";
|
|
21
|
+
|
|
22
|
+
// Binding builders — types only
|
|
23
|
+
export type {
|
|
24
|
+
AliasBindingBuilder,
|
|
25
|
+
BindToBuilder,
|
|
26
|
+
BindingBuilder,
|
|
27
|
+
ConstantBindingBuilder,
|
|
28
|
+
ScopedBindingBuilder,
|
|
29
|
+
SingletonBindingBuilder,
|
|
30
|
+
SingletonLifecycleBuilder,
|
|
31
|
+
SlotConstrainedBuilder,
|
|
32
|
+
TransientBindingBuilder,
|
|
33
|
+
} from "#/binding";
|
|
34
|
+
|
|
35
|
+
// Container
|
|
36
|
+
export { Container } from "#/container";
|
|
37
|
+
export type { Container as ContainerInterface, ContainerStatic } from "#/container";
|
|
38
|
+
|
|
39
|
+
export { effectiveBindingScope } from "#/binding-scope";
|
|
40
|
+
export { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolve-options";
|
|
41
|
+
|
|
42
|
+
// Introspection types
|
|
43
|
+
export type { BindingSnapshot, ContainerSnapshot } from "#/inspector";
|
|
44
|
+
|
|
45
|
+
// Graph types
|
|
46
|
+
export type { ContainerGraphJson, GraphEdge, GraphNode, GraphOptions } from "#/dependency-graph";
|
|
47
|
+
|
|
48
|
+
// Module
|
|
49
|
+
export { AsyncModule, isSyncModule, Module, SyncModule } from "#/module";
|
|
50
|
+
export type { AsyncModuleBuilder, ModuleBuilder } from "#/module";
|
|
51
|
+
|
|
52
|
+
// Decorators
|
|
53
|
+
export { inject, injectAll, isInjectionDescriptor, optional } from "#/decorators/inject";
|
|
54
|
+
export type { InjectionDescriptor, InjectOptions } from "#/decorators/inject";
|
|
55
|
+
export { injectable } from "#/decorators/injectable";
|
|
56
|
+
export type { InjectableDependency, InjectableOptions } from "#/decorators/injectable";
|
|
57
|
+
export { postConstruct, preDestroy } from "#/decorators/lifecycle-decorators";
|
|
58
|
+
|
|
59
|
+
// Auto-register
|
|
60
|
+
export { createAutoRegisterRegistry } from "#/decorators/injectable";
|
|
61
|
+
export type { AutoRegisterRegistry } from "#/decorators/injectable";
|
|
62
|
+
|
|
63
|
+
// MetadataReader
|
|
64
|
+
export { MetadataReaderToken } from "#/metadata/metadata-reader-token";
|
|
65
|
+
export type { MetadataReader, MutableLifecycleMetadata } from "#/metadata/metadata-types";
|
|
66
|
+
|
|
67
|
+
// Constraints — contextual injection predicates for .when()
|
|
68
|
+
export {
|
|
69
|
+
whenAnyAncestorIs,
|
|
70
|
+
whenAnyAncestorNamed,
|
|
71
|
+
whenAnyAncestorTagged,
|
|
72
|
+
whenAnyAncestorTaggedAll,
|
|
73
|
+
whenNoAncestorIs,
|
|
74
|
+
whenNoParentIs,
|
|
75
|
+
whenParentIs,
|
|
76
|
+
whenParentNamed,
|
|
77
|
+
whenParentTagged,
|
|
78
|
+
whenParentTaggedAll,
|
|
79
|
+
} from "#/constraints";
|
|
80
|
+
|
|
81
|
+
// Errors
|
|
82
|
+
export {
|
|
83
|
+
AmbiguousBindingError,
|
|
84
|
+
AsyncActivationError,
|
|
85
|
+
AsyncDeactivationError,
|
|
86
|
+
AsyncModuleLoadError,
|
|
87
|
+
AsyncResolutionError,
|
|
88
|
+
CircularDependencyError,
|
|
89
|
+
DiError,
|
|
90
|
+
DisposedContainerError,
|
|
91
|
+
InternalError,
|
|
92
|
+
MissingContainerContextError,
|
|
93
|
+
MissingMetadataError,
|
|
94
|
+
MissingScopeContextError,
|
|
95
|
+
NoMatchingBindingError,
|
|
96
|
+
RebindUnboundTokenError,
|
|
97
|
+
ScopeViolationError,
|
|
98
|
+
SyncDisposalNotSupportedError,
|
|
99
|
+
TokenNotBoundError,
|
|
100
|
+
} from "#/errors";
|
|
101
|
+
export type { ScopeViolationDetails } from "#/errors";
|
package/src/inspector.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Binding } from "#/binding";
|
|
2
|
+
import type {
|
|
3
|
+
BindingIdentifier,
|
|
4
|
+
BindingKind,
|
|
5
|
+
BindingScope,
|
|
6
|
+
BindingTag,
|
|
7
|
+
ConstraintContext,
|
|
8
|
+
Constructor,
|
|
9
|
+
ResolveOptions,
|
|
10
|
+
} from "#/types";
|
|
11
|
+
import type { Token } from "#/token";
|
|
12
|
+
import type { BindingRegistry } from "#/registry";
|
|
13
|
+
import type { ScopeManager } from "#/scope";
|
|
14
|
+
import { tokenName } from "#/token";
|
|
15
|
+
import { selectBinding } from "#/binding-select";
|
|
16
|
+
import { effectiveBindingScope } from "#/binding-scope";
|
|
17
|
+
|
|
18
|
+
// ── Public types ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @since 0.3.16-canary.0
|
|
22
|
+
*/
|
|
23
|
+
export interface BindingSnapshot {
|
|
24
|
+
readonly tokenName: string;
|
|
25
|
+
readonly kind: BindingKind;
|
|
26
|
+
readonly scope: BindingScope;
|
|
27
|
+
readonly slot: {
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
readonly tags: ReadonlyArray<BindingTag>;
|
|
30
|
+
};
|
|
31
|
+
readonly id: BindingIdentifier;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @since 0.3.16-canary.0
|
|
36
|
+
*/
|
|
37
|
+
export interface ContainerSnapshot {
|
|
38
|
+
readonly ownBindings: ReadonlyArray<BindingSnapshot>;
|
|
39
|
+
readonly cachedSingletonCount: number;
|
|
40
|
+
readonly hasParent: boolean;
|
|
41
|
+
readonly isDisposed: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Inspector ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @since 0.3.16-canary.0
|
|
48
|
+
*/
|
|
49
|
+
export class Inspector {
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly _registry: BindingRegistry,
|
|
52
|
+
private readonly _scope: ScopeManager,
|
|
53
|
+
private readonly _hasParent: boolean,
|
|
54
|
+
private readonly _isDisposed: () => boolean,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
inspect(): ContainerSnapshot {
|
|
58
|
+
const snapshots = this.allBindingSnapshots();
|
|
59
|
+
return {
|
|
60
|
+
ownBindings: snapshots,
|
|
61
|
+
cachedSingletonCount: this._scope.getAllSingletons().size,
|
|
62
|
+
hasParent: this._hasParent,
|
|
63
|
+
isDisposed: this._isDisposed(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
|
|
68
|
+
const bindings = this._registry.getAll(token);
|
|
69
|
+
return bindings.map((binding) => this._toSnapshot(binding));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
has(
|
|
73
|
+
token: Token<unknown> | Constructor,
|
|
74
|
+
hint?: ResolveOptions,
|
|
75
|
+
parentHas?: () => boolean,
|
|
76
|
+
): boolean {
|
|
77
|
+
const bindings = this._registry.getAll(token);
|
|
78
|
+
if (bindings.length > 0) {
|
|
79
|
+
if (hint !== undefined) {
|
|
80
|
+
if (
|
|
81
|
+
selectBinding(bindings, hint, this._makeHintContext(hint), tokenName(token)) !== undefined
|
|
82
|
+
) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return parentHas?.() ?? false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
hasOwn(token: Token<unknown> | Constructor, hint?: ResolveOptions): boolean {
|
|
93
|
+
const bindings = this._registry.getAll(token);
|
|
94
|
+
if (bindings.length === 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (hint !== undefined) {
|
|
98
|
+
return (
|
|
99
|
+
selectBinding(bindings, hint, this._makeHintContext(hint), tokenName(token)) !== undefined
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private _makeHintContext(hint: ResolveOptions): ConstraintContext {
|
|
106
|
+
return {
|
|
107
|
+
resolutionPath: [],
|
|
108
|
+
resolutionStack: [],
|
|
109
|
+
parent: undefined,
|
|
110
|
+
ancestors: [],
|
|
111
|
+
currentResolveHint: hint,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private allBindingSnapshots(): ReadonlyArray<BindingSnapshot> {
|
|
116
|
+
return this._registry.allBindings().map((binding) => this._toSnapshot(binding));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _toSnapshot(binding: Binding): BindingSnapshot {
|
|
120
|
+
const scope = effectiveBindingScope(binding);
|
|
121
|
+
const slot: BindingSnapshot["slot"] =
|
|
122
|
+
binding.slot.name !== undefined
|
|
123
|
+
? { name: binding.slot.name, tags: binding.slot.tags }
|
|
124
|
+
: { tags: binding.slot.tags };
|
|
125
|
+
return {
|
|
126
|
+
tokenName: tokenName(binding.token),
|
|
127
|
+
kind: binding.kind,
|
|
128
|
+
scope,
|
|
129
|
+
slot,
|
|
130
|
+
id: binding.id,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationHandler,
|
|
3
|
+
Constructor,
|
|
4
|
+
DeactivationHandler,
|
|
5
|
+
ResolutionContext,
|
|
6
|
+
} from "#/types";
|
|
7
|
+
import type { Token } from "#/token";
|
|
8
|
+
import type { Binding } from "#/binding";
|
|
9
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
10
|
+
import { AsyncActivationError, AsyncDeactivationError } from "#/errors";
|
|
11
|
+
import { tokenName } from "#/token";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @since 0.3.16-canary.0
|
|
15
|
+
*/
|
|
16
|
+
export class LifecycleManager {
|
|
17
|
+
// Container-level activation/deactivation hooks per token
|
|
18
|
+
private readonly _activationHooks = new Map<
|
|
19
|
+
Token<unknown> | Constructor,
|
|
20
|
+
Array<ActivationHandler<unknown>>
|
|
21
|
+
>();
|
|
22
|
+
private readonly _deactivationHooks = new Map<
|
|
23
|
+
Token<unknown> | Constructor,
|
|
24
|
+
Array<DeactivationHandler<unknown>>
|
|
25
|
+
>();
|
|
26
|
+
private _activationVersion = 0;
|
|
27
|
+
|
|
28
|
+
registerActivation<const Value>(
|
|
29
|
+
token: Token<Value> | Constructor<Value>,
|
|
30
|
+
handler: ActivationHandler<Value>,
|
|
31
|
+
): void {
|
|
32
|
+
this._activationVersion += 1;
|
|
33
|
+
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
34
|
+
const list = this._activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
35
|
+
list.push(handler as ActivationHandler<unknown>);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
|
|
39
|
+
if (this._activationHooks.size === 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const list = this._activationHooks.get(token as Token<unknown> | Constructor);
|
|
43
|
+
return list !== undefined && list.length > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get activationVersion(): number {
|
|
47
|
+
return this._activationVersion;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
registerDeactivation<const Value>(
|
|
51
|
+
token: Token<Value> | Constructor<Value>,
|
|
52
|
+
handler: DeactivationHandler<Value>,
|
|
53
|
+
): void {
|
|
54
|
+
const list = this._deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
55
|
+
list.push(handler as DeactivationHandler<unknown>);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async runActivation<const Value>(
|
|
59
|
+
resolutionContext: ResolutionContext,
|
|
60
|
+
binding: Binding<Value>,
|
|
61
|
+
instance: Value,
|
|
62
|
+
metadataReader: MetadataReader,
|
|
63
|
+
): Promise<Value> {
|
|
64
|
+
let activatedInstance: Value = instance;
|
|
65
|
+
|
|
66
|
+
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
67
|
+
if (binding.kind === "class") {
|
|
68
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
69
|
+
if (lifecycle?.postConstruct && lifecycle.postConstruct.length > 0) {
|
|
70
|
+
for (const methodName of lifecycle.postConstruct) {
|
|
71
|
+
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
72
|
+
if (typeof method === "function") {
|
|
73
|
+
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
74
|
+
if (hookResult instanceof Promise) {
|
|
75
|
+
await hookResult;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. per-binding onActivation
|
|
83
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
84
|
+
const activationResult = binding.onActivation(resolutionContext, activatedInstance);
|
|
85
|
+
activatedInstance =
|
|
86
|
+
activationResult instanceof Promise ? await activationResult : activationResult;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. container-level onActivation
|
|
90
|
+
const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
|
|
91
|
+
if (containerHooks !== undefined) {
|
|
92
|
+
for (const hook of containerHooks) {
|
|
93
|
+
const activationResult = hook(resolutionContext, activatedInstance);
|
|
94
|
+
activatedInstance = (
|
|
95
|
+
activationResult instanceof Promise ? await activationResult : activationResult
|
|
96
|
+
) as Value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return activatedInstance;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
runActivationSync<const Value>(
|
|
104
|
+
resolutionContext: ResolutionContext,
|
|
105
|
+
binding: Binding<Value>,
|
|
106
|
+
instance: Value,
|
|
107
|
+
metadataReader: MetadataReader,
|
|
108
|
+
): Value {
|
|
109
|
+
let activatedInstance: Value = instance;
|
|
110
|
+
|
|
111
|
+
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
112
|
+
if (binding.kind === "class") {
|
|
113
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
114
|
+
if (lifecycle?.postConstruct && lifecycle.postConstruct.length > 0) {
|
|
115
|
+
for (const methodName of lifecycle.postConstruct) {
|
|
116
|
+
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
117
|
+
if (typeof method === "function") {
|
|
118
|
+
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
119
|
+
if (hookResult instanceof Promise) {
|
|
120
|
+
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. per-binding onActivation (must be sync)
|
|
128
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
129
|
+
const activationResult = binding.onActivation(resolutionContext, activatedInstance);
|
|
130
|
+
if (activationResult instanceof Promise) {
|
|
131
|
+
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
132
|
+
}
|
|
133
|
+
activatedInstance = activationResult;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. container-level onActivation (must be sync)
|
|
137
|
+
const tokenDisplayName = tokenName(binding.token);
|
|
138
|
+
const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
|
|
139
|
+
if (containerHooks !== undefined) {
|
|
140
|
+
for (const hook of containerHooks) {
|
|
141
|
+
const activationResult = hook(resolutionContext, activatedInstance);
|
|
142
|
+
if (activationResult instanceof Promise) {
|
|
143
|
+
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
144
|
+
}
|
|
145
|
+
activatedInstance = activationResult as Value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return activatedInstance;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async runDeactivation<const Value>(
|
|
153
|
+
binding: Binding<Value>,
|
|
154
|
+
instance: Value,
|
|
155
|
+
metadataReader: MetadataReader,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
158
|
+
|
|
159
|
+
// 1. container-level onDeactivation
|
|
160
|
+
const containerHooks = this._deactivationHooks.get(tokenKey);
|
|
161
|
+
if (containerHooks !== undefined) {
|
|
162
|
+
for (const hook of containerHooks) {
|
|
163
|
+
const hookResult = hook(instance);
|
|
164
|
+
if (hookResult instanceof Promise) {
|
|
165
|
+
await hookResult;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 2. per-binding onDeactivation
|
|
171
|
+
if (binding.kind !== "alias" && binding.onDeactivation !== undefined) {
|
|
172
|
+
const hookResult = binding.onDeactivation(instance);
|
|
173
|
+
if (hookResult instanceof Promise) {
|
|
174
|
+
await hookResult;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. @preDestroy() — all methods in declaration order
|
|
179
|
+
if (binding.kind === "class") {
|
|
180
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
181
|
+
if (lifecycle?.preDestroy && lifecycle.preDestroy.length > 0) {
|
|
182
|
+
for (const methodName of lifecycle.preDestroy) {
|
|
183
|
+
const method = (instance as Record<string, unknown>)[methodName];
|
|
184
|
+
if (typeof method === "function") {
|
|
185
|
+
const hookResult = (method as () => unknown).call(instance);
|
|
186
|
+
if (hookResult instanceof Promise) {
|
|
187
|
+
await hookResult;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
runDeactivationSync<const Value>(
|
|
196
|
+
binding: Binding<Value>,
|
|
197
|
+
instance: Value,
|
|
198
|
+
metadataReader: MetadataReader,
|
|
199
|
+
): void {
|
|
200
|
+
const tokenDisplayName = tokenName(binding.token);
|
|
201
|
+
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
202
|
+
|
|
203
|
+
// 1. container-level onDeactivation
|
|
204
|
+
const containerHooks = this._deactivationHooks.get(tokenKey);
|
|
205
|
+
if (containerHooks !== undefined) {
|
|
206
|
+
for (const hook of containerHooks) {
|
|
207
|
+
const hookResult = hook(instance);
|
|
208
|
+
if (hookResult instanceof Promise) {
|
|
209
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 2. per-binding onDeactivation
|
|
215
|
+
if (binding.kind !== "alias" && binding.onDeactivation !== undefined) {
|
|
216
|
+
const hookResult = binding.onDeactivation(instance);
|
|
217
|
+
if (hookResult instanceof Promise) {
|
|
218
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3. @preDestroy()
|
|
223
|
+
if (binding.kind === "class") {
|
|
224
|
+
const lifecycle = metadataReader.getLifecycleMetadata(binding.target);
|
|
225
|
+
if (lifecycle?.preDestroy && lifecycle.preDestroy.length > 0) {
|
|
226
|
+
for (const methodName of lifecycle.preDestroy) {
|
|
227
|
+
const method = (instance as Record<string, unknown>)[methodName];
|
|
228
|
+
if (typeof method === "function") {
|
|
229
|
+
const hookResult = (method as () => unknown).call(instance);
|
|
230
|
+
if (hookResult instanceof Promise) {
|
|
231
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -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 { token } from "#/token";
|
|
2
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
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,53 @@
|
|
|
1
|
+
import type { Constructor } from "#/types";
|
|
2
|
+
import type { Token } from "#/token";
|
|
3
|
+
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
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
|
+
):
|
|
51
|
+
| ReadonlyArray<{ readonly key: string | symbol; readonly descriptor: InjectionDescriptor }>
|
|
52
|
+
| undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Constructor } from "#/types";
|
|
2
|
+
import type {
|
|
3
|
+
ConstructorMetadata,
|
|
4
|
+
LifecycleMetadata,
|
|
5
|
+
MetadataReader,
|
|
6
|
+
} from "#/metadata/metadata-types";
|
|
7
|
+
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
8
|
+
import {
|
|
9
|
+
INJECT_ACCESSOR_KEY,
|
|
10
|
+
INJECTABLE_KEY,
|
|
11
|
+
LIFECYCLE_KEY,
|
|
12
|
+
METADATA_SYMBOL,
|
|
13
|
+
} from "#/metadata/metadata-keys";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @since 0.3.16-canary.0
|
|
17
|
+
*/
|
|
18
|
+
export class SymbolMetadataReader implements MetadataReader {
|
|
19
|
+
private _getMetadataRecord(
|
|
20
|
+
target: Constructor,
|
|
21
|
+
key: string | symbol,
|
|
22
|
+
): Record<string | symbol, unknown> | undefined {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, METADATA_SYMBOL);
|
|
24
|
+
if (descriptor === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const record = descriptor.value as Record<string | symbol, unknown> | null | undefined;
|
|
28
|
+
if (!record || typeof record !== "object" || !Object.hasOwn(record, key)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
|
|
35
|
+
const record = this._getMetadataRecord(target, INJECTABLE_KEY);
|
|
36
|
+
return record?.[INJECTABLE_KEY] as ConstructorMetadata | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getLifecycleMetadata(target: Constructor): LifecycleMetadata | undefined {
|
|
40
|
+
const record = this._getMetadataRecord(target, LIFECYCLE_KEY);
|
|
41
|
+
return record?.[LIFECYCLE_KEY] as LifecycleMetadata | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getAccessorMetadata(
|
|
45
|
+
target: Constructor,
|
|
46
|
+
): Array<{ key: string | symbol; descriptor: InjectionDescriptor }> | undefined {
|
|
47
|
+
const record = this._getMetadataRecord(target, INJECT_ACCESSOR_KEY);
|
|
48
|
+
return record?.[INJECT_ACCESSOR_KEY] as
|
|
49
|
+
| Array<{ key: string | symbol; descriptor: InjectionDescriptor }>
|
|
50
|
+
| undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @since 0.3.16-canary.0
|
|
56
|
+
*/
|
|
57
|
+
export const defaultMetadataReader = new SymbolMetadataReader();
|