@travetto/di 6.0.1 → 7.0.0-rc.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/README.md +14 -14
- package/__index__.ts +2 -1
- package/package.json +3 -3
- package/src/decorator.ts +30 -57
- package/src/error.ts +2 -3
- package/src/registry/registry-adapter.ts +79 -0
- package/src/registry/registry-index.ts +279 -0
- package/src/registry/registry-resolver.ts +168 -0
- package/src/types.ts +47 -54
- package/support/test/suite.ts +14 -13
- package/src/registry.ts +0 -579
- package/support/dynamic.injection.ts +0 -108
- package/support/transformer.injectable.ts +0 -172
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ yarn add @travetto/di
|
|
|
16
16
|
[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) is a framework primitive. When used in conjunction with automatic file scanning, it provides for handling of application dependency wiring. Due to the nature of [Typescript](https://typescriptlang.org) and type erasure of interfaces, dependency injection only supports `class`es as a type signifier. The primary goal of dependency injection is to allow for separation of concerns of object creation and it's usage.
|
|
17
17
|
|
|
18
18
|
## Declaration
|
|
19
|
-
The [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
19
|
+
The [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) and [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L47) decorators provide the registration of dependencies. Dependency declaration revolves around exposing `class`es and subtypes thereof to provide necessary functionality. Additionally, the framework will utilize dependencies to satisfy contracts with various implementation.
|
|
20
20
|
|
|
21
21
|
**Code: Example Injectable**
|
|
22
22
|
```typescript
|
|
@@ -77,7 +77,7 @@ class SpecificService extends BaseService {
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
In this scenario, `SpecificService` is a valid candidate for `BaseService` due to the abstract inheritance. Sometimes, you may want to provide a slight variation to a dependency without extending a class. To this end, the [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
80
|
+
In this scenario, `SpecificService` is a valid candidate for `BaseService` due to the abstract inheritance. Sometimes, you may want to provide a slight variation to a dependency without extending a class. To this end, the [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L47) decorator denotes a `static` class method that produces an [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15).
|
|
81
81
|
|
|
82
82
|
**Code: Example InjectableFactory**
|
|
83
83
|
```typescript
|
|
@@ -125,12 +125,12 @@ class RuntimeService {
|
|
|
125
125
|
|
|
126
126
|
In this example, the enabled flag is specified in relationship to the deployment environment. When coupled with optional properties, and optional chaining, allows for seamless inclusion of optional dependencies at runtime.
|
|
127
127
|
|
|
128
|
-
**Note**: Other modules are able to provide aliases to [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
128
|
+
**Note**: Other modules are able to provide aliases to [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) that also provide additional functionality. For example, the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") module @Config or the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module @Controller decorator registers the associated class as an injectable element.
|
|
129
129
|
|
|
130
130
|
## Injection
|
|
131
|
-
Once all of your necessary dependencies are defined, now is the time to provide those [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
131
|
+
Once all of your necessary dependencies are defined, now is the time to provide those [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) instances to your code. There are three primary methods for injection:
|
|
132
132
|
|
|
133
|
-
The [@Inject](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
133
|
+
The [@Inject](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) decorator, which denotes a desire to inject a value directly. These will be set post construction.
|
|
134
134
|
|
|
135
135
|
**Code: Example Injectable with dependencies as Inject fields**
|
|
136
136
|
```typescript
|
|
@@ -148,7 +148,7 @@ class CustomService {
|
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
The [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
151
|
+
The [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) constructor params, which will be provided as the instance is being constructed.
|
|
152
152
|
|
|
153
153
|
**Code: Example Injectable with dependencies in constructor**
|
|
154
154
|
```typescript
|
|
@@ -170,7 +170,7 @@ class CustomService {
|
|
|
170
170
|
}
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
Via [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
173
|
+
Via [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L47) params, which are comparable to constructor params
|
|
174
174
|
|
|
175
175
|
**Code: Example InjectableFactory with parameters as dependencies**
|
|
176
176
|
```typescript
|
|
@@ -228,9 +228,9 @@ class Config {
|
|
|
228
228
|
```
|
|
229
229
|
|
|
230
230
|
## Non-Framework Dependencies
|
|
231
|
-
The module is built around the framework's management of class registration, and being able to decorate the code with [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
231
|
+
The module is built around the framework's management of class registration, and being able to decorate the code with [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) decorators. There may also be a desire to leverage external code and pull it into the dependency injection framework. This could easily be achieved using a wrapper class that is owned by the framework.
|
|
232
232
|
|
|
233
|
-
It is also possible to directly reference external types, and they will be converted into unique symbols. These symbols cannot be used manually, but can be leveraged using [@Inject](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#
|
|
233
|
+
It is also possible to directly reference external types, and they will be converted into unique symbols. These symbols cannot be used manually, but can be leveraged using [@Inject](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L15) decorators.
|
|
234
234
|
|
|
235
235
|
**Code: Example External Dependencies**
|
|
236
236
|
```typescript
|
|
@@ -268,18 +268,18 @@ class Service {
|
|
|
268
268
|
```
|
|
269
269
|
|
|
270
270
|
## Manual Invocation
|
|
271
|
-
Some times you will need to lookup a dependency dynamically, or you want to control the injection process at a more granular level. To achieve that you will need to directly access the [
|
|
271
|
+
Some times you will need to lookup a dependency dynamically, or you want to control the injection process at a more granular level. To achieve that you will need to directly access the [DependencyRegistryIndex](https://github.com/travetto/travetto/tree/main/module/di/src/registry/registry-index.ts#L19). The registry allows for requesting a dependency by class reference:
|
|
272
272
|
|
|
273
273
|
**Code: Example of Manual Lookup**
|
|
274
274
|
```typescript
|
|
275
|
-
import { Injectable,
|
|
275
|
+
import { Injectable, DependencyRegistryIndex } from '@travetto/di';
|
|
276
276
|
|
|
277
277
|
@Injectable()
|
|
278
278
|
class Complex { }
|
|
279
279
|
|
|
280
280
|
class ManualLookup {
|
|
281
281
|
async invoke() {
|
|
282
|
-
const complex = await
|
|
282
|
+
const complex = await DependencyRegistryIndex.getInstance(Complex);
|
|
283
283
|
return complex;
|
|
284
284
|
}
|
|
285
285
|
}
|
|
@@ -289,7 +289,7 @@ Additionally, support for interfaces (over class inheritance) is provided, but r
|
|
|
289
289
|
|
|
290
290
|
**Code: Example Interface Injection**
|
|
291
291
|
```typescript
|
|
292
|
-
import {
|
|
292
|
+
import { DependencyRegistryIndex, Inject, Injectable, InjectableFactory } from '@travetto/di';
|
|
293
293
|
import { toConcrete } from '@travetto/runtime';
|
|
294
294
|
|
|
295
295
|
/**
|
|
@@ -315,7 +315,7 @@ class SpecificService {
|
|
|
315
315
|
class ManualInvocationOfInterface {
|
|
316
316
|
@InjectableFactory()
|
|
317
317
|
static getCustomService(): Promise<ServiceContract> {
|
|
318
|
-
return
|
|
318
|
+
return DependencyRegistryIndex.getInstance(toConcrete<ServiceContract>());
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
```
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/di",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-rc.1",
|
|
4
4
|
"description": "Dependency registration/management and injection support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast-transformations",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"directory": "module/di"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^
|
|
30
|
+
"@travetto/registry": "^7.0.0-rc.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^
|
|
33
|
+
"@travetto/transformer": "^7.0.0-rc.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/decorator.ts
CHANGED
|
@@ -1,80 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Any, castTo, ClassInstance, getClass, type Class } from '@travetto/runtime';
|
|
2
|
+
import { CONSTRUCTOR_PROPERTY } from '@travetto/schema';
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { InjectableCandidate, ResolutionType } from './types.ts';
|
|
5
|
+
import { DependencyRegistryIndex } from './registry/registry-index.ts';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (args) {
|
|
9
|
-
if (Array.isArray(args)) {
|
|
10
|
-
for (const arg of args) {
|
|
11
|
-
if (typeof arg === 'symbol') {
|
|
12
|
-
out.qualifier = arg;
|
|
13
|
-
} else if (arg) {
|
|
14
|
-
Object.assign(out, arg);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
out = args;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return asFull(out);
|
|
22
|
-
}
|
|
7
|
+
const fromArg = <T extends { qualifier?: symbol }>(arg?: T | symbol): T =>
|
|
8
|
+
typeof arg === 'symbol' ? castTo({ qualifier: arg }) : (arg ?? castTo<T>({}));
|
|
23
9
|
|
|
24
10
|
/**
|
|
25
11
|
* Indicate that a class is able to be injected
|
|
26
|
-
*
|
|
27
|
-
* @
|
|
12
|
+
* @augments `@travetto/schema:Schema`
|
|
13
|
+
* @kind decorator
|
|
28
14
|
*/
|
|
29
|
-
export function Injectable(
|
|
30
|
-
return <T extends Class>(
|
|
31
|
-
|
|
32
|
-
...collapseConfig<Partial<InjectableConfig>>(first, ...args),
|
|
33
|
-
class: target
|
|
34
|
-
};
|
|
35
|
-
DependencyRegistry.registerClass(target, config);
|
|
36
|
-
return target;
|
|
15
|
+
export function Injectable(config?: Partial<InjectableCandidate> | symbol) {
|
|
16
|
+
return <T extends Class>(cls: T): void => {
|
|
17
|
+
DependencyRegistryIndex.getForRegister(cls).registerClass(fromArg(config));
|
|
37
18
|
};
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
export type InjectConfig = { qualifier?: symbol,
|
|
41
|
-
|
|
42
|
-
export function InjectArgs(configs?: InjectConfig[][]) {
|
|
43
|
-
return <T extends Class>(target: T): void => {
|
|
44
|
-
DependencyRegistry.registerConstructor(target, configs?.map(x => collapseConfig(...x)));
|
|
45
|
-
};
|
|
46
|
-
}
|
|
21
|
+
export type InjectConfig = { qualifier?: symbol, resolution?: ResolutionType };
|
|
47
22
|
|
|
48
23
|
/**
|
|
49
|
-
* Indicate that a field is able to be injected
|
|
50
|
-
*
|
|
24
|
+
* Indicate that a field or parameter is able to be injected
|
|
51
25
|
* @augments `@travetto/di:Inject`
|
|
26
|
+
* @augments `@travetto/schema:Input`
|
|
27
|
+
* @kind decorator
|
|
52
28
|
*/
|
|
53
|
-
export function Inject(
|
|
54
|
-
return (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
29
|
+
export function Inject(config?: InjectConfig | symbol) {
|
|
30
|
+
return (instanceOrCls: Class | ClassInstance, property?: string | symbol, idx?: number | PropertyDescriptor): void => {
|
|
31
|
+
const cfg = fromArg(config);
|
|
32
|
+
const cls = getClass(instanceOrCls);
|
|
33
|
+
const propertyKey = property ?? CONSTRUCTOR_PROPERTY;
|
|
34
|
+
if (typeof idx !== 'number') {
|
|
35
|
+
DependencyRegistryIndex.registerFieldMetadata(cls, propertyKey, cfg);
|
|
36
|
+
} else {
|
|
37
|
+
DependencyRegistryIndex.registerParameterMetadata(cls, propertyKey, idx, cfg);
|
|
61
38
|
}
|
|
62
39
|
};
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
/**
|
|
66
43
|
* Identifies a static method that is able to produce a dependency
|
|
67
|
-
*
|
|
68
|
-
* @
|
|
44
|
+
* @augments `@travetto/schema:Method`
|
|
45
|
+
* @kind decorator
|
|
69
46
|
*/
|
|
70
|
-
export function InjectableFactory(
|
|
71
|
-
return <T extends Class>(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
...config,
|
|
75
|
-
dependencies: config.dependencies?.map(x => Array.isArray(x) ? collapseConfig(...x) : collapseConfig(x)),
|
|
76
|
-
fn: descriptor.value!,
|
|
77
|
-
id: `${target.Ⲑid}#${property.toString()}`
|
|
47
|
+
export function InjectableFactory(config?: Partial<InjectableCandidate> | symbol) {
|
|
48
|
+
return <T extends Class>(cls: T, property: string | symbol, descriptor: TypedPropertyDescriptor<(...args: Any[]) => Any>): void => {
|
|
49
|
+
DependencyRegistryIndex.getForRegister(cls).registerFactory(property, fromArg(config), {
|
|
50
|
+
factory: (...params: unknown[]) => descriptor.value!.apply(cls, params),
|
|
78
51
|
});
|
|
79
52
|
};
|
|
80
53
|
}
|
package/src/error.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { AppError } from '@travetto/runtime';
|
|
2
|
-
import { ClassTarget } from './types.ts';
|
|
1
|
+
import { AppError, Class } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
function getName(symbol: symbol): string {
|
|
5
4
|
return symbol.toString().split(/[()]/g)[1];
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
export class InjectionError extends AppError {
|
|
9
|
-
constructor(message: string, target:
|
|
8
|
+
constructor(message: string, target: Class, qualifiers?: symbol[]) {
|
|
10
9
|
super(`${message}: [${target.Ⲑid}]${qualifiers ? `[${qualifiers.map(getName)}]` : ''}`, {
|
|
11
10
|
category: 'notfound',
|
|
12
11
|
details: {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RegistryAdapter } from '@travetto/registry';
|
|
2
|
+
import { Class, classConstruct, describeFunction, getAllEntries, safeAssign } from '@travetto/runtime';
|
|
3
|
+
import { CONSTRUCTOR_PROPERTY, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { InjectableConfig, getDefaultQualifier, InjectableCandidate } from '../types';
|
|
6
|
+
|
|
7
|
+
function combineInjectableCandidates<T extends InjectableCandidate>(base: T, ...override: Partial<T>[]): typeof base {
|
|
8
|
+
for (const o of override) {
|
|
9
|
+
safeAssign(base, o);
|
|
10
|
+
}
|
|
11
|
+
return base;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function combineClasses<T extends InjectableConfig>(base: T, ...override: Partial<T>[]): typeof base {
|
|
15
|
+
for (const o of override) {
|
|
16
|
+
Object.assign(base, {
|
|
17
|
+
...base,
|
|
18
|
+
...o,
|
|
19
|
+
candidates: {
|
|
20
|
+
...base.candidates,
|
|
21
|
+
...o.candidates,
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DependencyRegistryAdapter implements RegistryAdapter<InjectableConfig> {
|
|
29
|
+
#cls: Class;
|
|
30
|
+
#config: InjectableConfig;
|
|
31
|
+
|
|
32
|
+
constructor(cls: Class) {
|
|
33
|
+
this.#cls = cls;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
register(...data: Partial<InjectableConfig<unknown>>[]): InjectableConfig<unknown> {
|
|
37
|
+
this.#config ??= { class: this.#cls, candidates: {} };
|
|
38
|
+
return combineClasses(this.#config, ...data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
registerFactory(method: string | symbol, ...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
|
|
42
|
+
const { candidates } = this.register();
|
|
43
|
+
candidates[method] ??= {
|
|
44
|
+
class: this.#cls,
|
|
45
|
+
method,
|
|
46
|
+
enabled: true,
|
|
47
|
+
factory: undefined!,
|
|
48
|
+
candidateType: undefined!,
|
|
49
|
+
};
|
|
50
|
+
return combineInjectableCandidates(candidates[method], ...data);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerClass(...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
|
|
54
|
+
return this.registerFactory(CONSTRUCTOR_PROPERTY, ...data, {
|
|
55
|
+
factory: (...args: unknown[]) => classConstruct(this.#cls, args),
|
|
56
|
+
candidateType: this.#cls,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get(): InjectableConfig<unknown> {
|
|
61
|
+
return this.#config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finalize(): void {
|
|
65
|
+
for (const [k] of getAllEntries(this.#config.candidates)) {
|
|
66
|
+
const v = this.#config.candidates[k];
|
|
67
|
+
const candidateType = SchemaRegistryIndex.get(v.class).getMethodReturnType(k);
|
|
68
|
+
v.candidateType = candidateType;
|
|
69
|
+
v.qualifier ??= getDefaultQualifier(candidateType);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getCandidateConfigs(): InjectableCandidate[] {
|
|
74
|
+
const entries = getAllEntries(this.#config.candidates).map(([_, item]) => item);
|
|
75
|
+
return entries
|
|
76
|
+
.filter(item => (item.enabled ?? true) === true || (typeof item.enabled === 'function' && item.enabled()))
|
|
77
|
+
.filter(item => item.method !== CONSTRUCTOR_PROPERTY || !describeFunction(item.candidateType)?.abstract);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { ChangeEvent, RegistryIndex, RegistryIndexStore, Registry, RetargettingProxy } from '@travetto/registry';
|
|
2
|
+
import { AppError, castKey, castTo, Class, describeFunction, getParentClass, hasFunction, Runtime, TypedObject, Util } from '@travetto/runtime';
|
|
3
|
+
import { SchemaFieldConfig, SchemaParameterConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { Dependency, InjectableCandidate, InjectableClassMetadata, InjectableConfig, ResolutionType } from '../types';
|
|
6
|
+
import { DependencyRegistryAdapter } from './registry-adapter';
|
|
7
|
+
import { InjectionError } from '../error';
|
|
8
|
+
import { DependencyRegistryResolver } from './registry-resolver';
|
|
9
|
+
|
|
10
|
+
const MetadataSymbol = Symbol();
|
|
11
|
+
|
|
12
|
+
const hasPostConstruct = hasFunction<{ postConstruct: () => Promise<unknown> }>('postConstruct');
|
|
13
|
+
const hasPreDestroy = hasFunction<{ preDestroy: () => Promise<unknown> }>('preDestroy');
|
|
14
|
+
|
|
15
|
+
function readMetadata(item: { metadata?: Record<symbol, unknown> }): Dependency | undefined {
|
|
16
|
+
return castTo<Dependency | undefined>(item.metadata?.[MetadataSymbol]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class DependencyRegistryIndex implements RegistryIndex {
|
|
20
|
+
|
|
21
|
+
static #instance = Registry.registerIndex(DependencyRegistryIndex);
|
|
22
|
+
|
|
23
|
+
static getForRegister(cls: Class): DependencyRegistryAdapter {
|
|
24
|
+
return this.#instance.store.getForRegister(cls);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getInstance<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Promise<T> {
|
|
28
|
+
return this.#instance.getInstance(candidateType, qualifier, resolution);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getCandidates<T>(candidateType: Class<T>): InjectableCandidate<T>[] {
|
|
32
|
+
return this.#instance.getCandidates<T>(candidateType);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static getCandidateTypes<T>(candidateType: Class<T>): Class<T>[] {
|
|
36
|
+
return this.#instance.getCandidates(candidateType).map(c => c.candidateType);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static getInstances<T>(candidateType: Class<T>, predicate?: (cfg: InjectableCandidate<T>) => boolean): Promise<T[]> {
|
|
40
|
+
return this.#instance.getInstances<T>(candidateType, predicate);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static injectFields<T extends { constructor: Class<T> }>(o: T, cls = o.constructor): Promise<T> {
|
|
44
|
+
return this.#instance.injectFields(cls, o, cls);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static getOptional(cls: Class): InjectableConfig | undefined {
|
|
48
|
+
return this.#instance.store.getOptional(cls)?.get();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static registerClassMetadata(cls: Class, metadata: InjectableClassMetadata): void {
|
|
52
|
+
SchemaRegistryIndex.getForRegister(cls).registerMetadata<InjectableClassMetadata>(MetadataSymbol, metadata);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static registerParameterMetadata(cls: Class, method: string | symbol, index: number, metadata: Dependency): void {
|
|
56
|
+
SchemaRegistryIndex.getForRegister(cls).registerParameterMetadata(method, index, MetadataSymbol, metadata);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static registerFieldMetadata(cls: Class, field: string | symbol, metadata: Dependency): void {
|
|
60
|
+
SchemaRegistryIndex.getForRegister(cls).registerFieldMetadata(field, MetadataSymbol, metadata);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#proxies = new Map<string, Map<symbol | undefined, RetargettingProxy<unknown>>>();
|
|
64
|
+
#instances = new Map<Class, Map<symbol, unknown>>();
|
|
65
|
+
#instancePromises = new Map<Class, Map<symbol, Promise<unknown>>>();
|
|
66
|
+
#resolver = new DependencyRegistryResolver();
|
|
67
|
+
|
|
68
|
+
#proxyInstance<T>(target: Class<unknown>, qualifier: symbol, instance: T): T {
|
|
69
|
+
let proxy: RetargettingProxy<unknown>;
|
|
70
|
+
const targetId = target.Ⲑid;
|
|
71
|
+
|
|
72
|
+
if (!this.#proxies.has(targetId)) {
|
|
73
|
+
this.#proxies.set(targetId, new Map());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!this.#proxies.get(targetId)!.has(qualifier)) {
|
|
77
|
+
proxy = new RetargettingProxy(instance);
|
|
78
|
+
this.#proxies.get(targetId)!.set(qualifier, proxy);
|
|
79
|
+
console.debug('Registering proxy', { id: target.Ⲑid, qualifier: qualifier.toString() });
|
|
80
|
+
} else {
|
|
81
|
+
proxy = this.#proxies.get(targetId)!.get(qualifier)!;
|
|
82
|
+
proxy.setTarget(instance);
|
|
83
|
+
console.debug('Updating target', {
|
|
84
|
+
id: target.Ⲑid, qualifier: qualifier.toString(), instanceType: target.name
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return proxy.get();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#addClass(cls: Class, forceCreate: boolean = false): void {
|
|
92
|
+
const adapter = this.store.get(cls);
|
|
93
|
+
|
|
94
|
+
for (const config of adapter.getCandidateConfigs()) {
|
|
95
|
+
const parentClass = getParentClass(config.candidateType);
|
|
96
|
+
const parentConfig = parentClass ? this.store.getOptional(parentClass) : undefined;
|
|
97
|
+
const hasParentBase = (parentConfig || (parentClass && !!describeFunction(parentClass)?.abstract));
|
|
98
|
+
const baseParent = hasParentBase ? parentClass : undefined;
|
|
99
|
+
this.#resolver.registerClass(config, baseParent);
|
|
100
|
+
if (config.autoInject || forceCreate) {
|
|
101
|
+
// Don't wait
|
|
102
|
+
Util.queueMacroTask().then(() => {
|
|
103
|
+
this.getInstance(config.candidateType, config.qualifier);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#removeClass(cls: Class): void {
|
|
110
|
+
if (this.#instances.has(cls)) {
|
|
111
|
+
for (const [qualifier, config] of this.#resolver.getContainerEntries(cls)) {
|
|
112
|
+
try {
|
|
113
|
+
this.destroyInstance(config.candidateType, qualifier);
|
|
114
|
+
} catch { }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async #resolveDependencyValue(dependency: Dependency, input: SchemaFieldConfig | SchemaParameterConfig, src: Class): Promise<unknown> {
|
|
121
|
+
try {
|
|
122
|
+
const target = dependency.target ?? input.type;
|
|
123
|
+
return await this.getInstance(target, dependency.qualifier, dependency.resolution);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (input.required?.active === false && err instanceof InjectionError && err.category === 'notfound') {
|
|
126
|
+
return undefined;
|
|
127
|
+
} else {
|
|
128
|
+
if (err && err instanceof Error) {
|
|
129
|
+
err.message = `${err.message} via=${src.Ⲑid}[${input.name?.toString() ?? 'constructor'}]`;
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
store = new RegistryIndexStore(DependencyRegistryAdapter);
|
|
137
|
+
|
|
138
|
+
getConfig(cls: Class): InjectableConfig {
|
|
139
|
+
return this.store.get(cls).get();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process(events: ChangeEvent<Class>[]): void {
|
|
143
|
+
for (const ev of events) {
|
|
144
|
+
if ('prev' in ev) {
|
|
145
|
+
this.#removeClass(ev.prev);
|
|
146
|
+
}
|
|
147
|
+
if ('curr' in ev) {
|
|
148
|
+
this.#addClass(ev.curr, 'prev' in ev);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get all available candidates for a given type
|
|
155
|
+
*/
|
|
156
|
+
getCandidates<T>(candidateType: Class<T>): InjectableCandidate<T>[] {
|
|
157
|
+
return this.#resolver.getCandidateEntries(candidateType).map(([_, x]) => castTo<InjectableCandidate<T>>(x));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get candidate instances by target type, with an optional filter
|
|
162
|
+
*/
|
|
163
|
+
getInstances<T>(candidateType: Class<T>, predicate?: (cfg: InjectableCandidate<T>) => boolean): Promise<T[]> {
|
|
164
|
+
const inputs = this.getCandidates<T>(candidateType).filter(x => !predicate || predicate(x));
|
|
165
|
+
return Promise.all(inputs.map(l => this.getInstance<T>(l.class, l.qualifier)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Retrieve list dependencies
|
|
170
|
+
*/
|
|
171
|
+
async fetchDependencyParameters<T>(candidate: InjectableCandidate<T>): Promise<unknown[]> {
|
|
172
|
+
const inputs = SchemaRegistryIndex.has(candidate.class) ?
|
|
173
|
+
SchemaRegistryIndex.get(candidate.class).getMethod(candidate.method).parameters : [];
|
|
174
|
+
|
|
175
|
+
const promises = inputs
|
|
176
|
+
.map(input => this.#resolveDependencyValue(readMetadata(input) ?? {}, input, candidate.class));
|
|
177
|
+
|
|
178
|
+
return await Promise.all(promises);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Retrieve mapped dependencies
|
|
183
|
+
*/
|
|
184
|
+
async injectFields<T>(candidateType: Class, instance: T, srcClass: Class): Promise<T> {
|
|
185
|
+
const inputs = SchemaRegistryIndex.getOptional(candidateType)?.getFields() ?? {};
|
|
186
|
+
|
|
187
|
+
const promises = TypedObject.entries(inputs)
|
|
188
|
+
.filter(([k, input]) => readMetadata(input) !== undefined && (input.access !== 'readonly' && instance[castKey(k)] === undefined))
|
|
189
|
+
.map(async ([k, input]) => [k, await this.#resolveDependencyValue(readMetadata(input) ?? {}, input, srcClass)] as const);
|
|
190
|
+
|
|
191
|
+
const pairs = await Promise.all(promises);
|
|
192
|
+
|
|
193
|
+
for (const [k, v] of pairs) {
|
|
194
|
+
instance[castKey(k)] = castTo(v);
|
|
195
|
+
}
|
|
196
|
+
return instance;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Actually construct an instance while resolving the dependencies
|
|
201
|
+
*/
|
|
202
|
+
async construct<T>(candidateType: Class<T>, qualifier: symbol): Promise<T> {
|
|
203
|
+
const { candidate } = this.#resolver.resolveCandidate(candidateType, qualifier);
|
|
204
|
+
const targetType = candidate.candidateType;
|
|
205
|
+
const params = await this.fetchDependencyParameters(candidate);
|
|
206
|
+
const inst = await candidate.factory(...params);
|
|
207
|
+
|
|
208
|
+
// And auto-wire fields
|
|
209
|
+
await this.injectFields(targetType, inst, candidate.class);
|
|
210
|
+
|
|
211
|
+
// Run post construct, if it wasn't passed in, otherwise it was already created
|
|
212
|
+
if (hasPostConstruct(inst) && !params.includes(inst)) {
|
|
213
|
+
await inst.postConstruct();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const metadata = SchemaRegistryIndex.has(targetType) ?
|
|
217
|
+
SchemaRegistryIndex.get(targetType).getMetadata<InjectableClassMetadata>(MetadataSymbol) : undefined;
|
|
218
|
+
|
|
219
|
+
// Run post constructors
|
|
220
|
+
for (const op of Object.values(metadata?.postConstruct ?? {})) {
|
|
221
|
+
await op(inst);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Proxy if necessary
|
|
225
|
+
return Runtime.dynamic ? this.#proxyInstance(targetType, qualifier, inst) : inst;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get or create the instance
|
|
230
|
+
*/
|
|
231
|
+
async getInstance<T>(candidateType: Class<T>, requestedQualifier?: symbol, resolution?: ResolutionType): Promise<T> {
|
|
232
|
+
if (!candidateType) {
|
|
233
|
+
throw new AppError('Unable to get instance when target is undefined');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier, resolution);
|
|
237
|
+
|
|
238
|
+
if (!this.#instances.has(target)) {
|
|
239
|
+
this.#instances.set(target, new Map());
|
|
240
|
+
this.#instancePromises.set(target, new Map());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (this.#instancePromises.get(target)!.has(qualifier)) {
|
|
244
|
+
return castTo(this.#instancePromises.get(target)!.get(qualifier));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const instancePromise = this.construct(candidateType, qualifier);
|
|
248
|
+
this.#instancePromises.get(target)!.set(qualifier, instancePromise);
|
|
249
|
+
try {
|
|
250
|
+
const instance = await instancePromise;
|
|
251
|
+
this.#instances.get(target)!.set(qualifier, instance);
|
|
252
|
+
return instance;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
// Clear it out, don't save failed constructions
|
|
255
|
+
this.#instancePromises.get(target)!.delete(qualifier);
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Destroy an instance
|
|
262
|
+
*/
|
|
263
|
+
destroyInstance(candidateType: Class, requestedQualifier: symbol): void {
|
|
264
|
+
const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier);
|
|
265
|
+
|
|
266
|
+
const activeInstance = this.#instances.get(target)?.get(qualifier);
|
|
267
|
+
if (hasPreDestroy(activeInstance)) {
|
|
268
|
+
activeInstance.preDestroy();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.#resolver.removeClass(candidateType, qualifier);
|
|
272
|
+
this.#instances.get(target)?.delete(qualifier);
|
|
273
|
+
this.#instancePromises.get(target)?.delete(qualifier);
|
|
274
|
+
|
|
275
|
+
// May not exist
|
|
276
|
+
this.#proxies.get(target.Ⲑid)?.get(qualifier)?.setTarget(null);
|
|
277
|
+
console.debug('On uninstall', { id: target, qualifier: qualifier.toString(), classId: target });
|
|
278
|
+
}
|
|
279
|
+
}
|