@travetto/di 7.1.4 → 8.0.0-alpha.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 +1 -1
- package/package.json +3 -3
- package/src/decorator.ts +12 -2
- package/src/error.ts +2 -2
- package/src/registry/registry-adapter.ts +11 -10
- package/src/registry/registry-index.ts +31 -70
- package/src/registry/registry-resolver.ts +6 -5
- package/src/types.ts +12 -5
- package/support/test/suite.ts +15 -8
package/README.md
CHANGED
|
@@ -268,7 +268,7 @@ 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 [DependencyRegistryIndex](https://github.com/travetto/travetto/tree/main/module/di/src/registry/registry-index.ts#
|
|
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#L16). The registry allows for requesting a dependency by class reference:
|
|
272
272
|
|
|
273
273
|
**Code: Example of Manual Lookup**
|
|
274
274
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/di",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dependency registration/management and injection support.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"directory": "module/di"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/registry": "^
|
|
31
|
+
"@travetto/registry": "^8.0.0-alpha.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/transformer": "^
|
|
34
|
+
"@travetto/transformer": "^8.0.0-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/decorator.ts
CHANGED
|
@@ -15,7 +15,7 @@ const fromInput = <T extends { qualifier?: symbol }>(input?: T | symbol): T =>
|
|
|
15
15
|
*/
|
|
16
16
|
export function Injectable(input?: Partial<InjectableCandidate> | symbol) {
|
|
17
17
|
return <T extends Class>(cls: T): void => {
|
|
18
|
-
DependencyRegistryIndex.
|
|
18
|
+
DependencyRegistryIndex.registerClass(cls, fromInput(input));
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -47,8 +47,18 @@ export function Inject(input?: InjectConfig | symbol) {
|
|
|
47
47
|
*/
|
|
48
48
|
export function InjectableFactory(input?: Partial<InjectableCandidate> | symbol) {
|
|
49
49
|
return <T extends Class>(cls: T, property: string, descriptor: TypedPropertyDescriptor<(...args: Any[]) => Any>): void => {
|
|
50
|
-
DependencyRegistryIndex.
|
|
50
|
+
DependencyRegistryIndex.registerFactory(cls, property, fromInput(input), {
|
|
51
51
|
factory: (...params: unknown[]) => descriptor.value!.apply(cls, params),
|
|
52
52
|
});
|
|
53
53
|
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register post construction handler
|
|
58
|
+
* @kind decorator
|
|
59
|
+
*/
|
|
60
|
+
export function PostConstruct(priority: number = 10) {
|
|
61
|
+
return (instance: ClassInstance, property: string, descriptor: TypedPropertyDescriptor<() => Any>): void => {
|
|
62
|
+
DependencyRegistryIndex.registerPostConstruct(getClass(instance), { operation: descriptor.value!, priority });
|
|
63
|
+
};
|
|
54
64
|
}
|
package/src/error.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RuntimeError, type Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
function getName(symbol: symbol): string {
|
|
4
4
|
return symbol.toString().split(/[()]/g)[1];
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export class InjectionError extends
|
|
7
|
+
export class InjectionError extends RuntimeError {
|
|
8
8
|
constructor(message: string, target: Class, qualifiers?: symbol[]) {
|
|
9
9
|
super(`${message}: [${target.Ⲑid}]${qualifiers ? `[${qualifiers.map(getName)}]` : ''}`, {
|
|
10
10
|
category: 'notfound',
|
|
@@ -13,14 +13,12 @@ function combineInjectableCandidates<T extends InjectableCandidate>(base: T, ...
|
|
|
13
13
|
|
|
14
14
|
function combineClasses<T extends InjectableConfig>(base: T, ...overrides: Partial<T>[]): typeof base {
|
|
15
15
|
for (const override of overrides) {
|
|
16
|
-
|
|
17
|
-
...base,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
});
|
|
16
|
+
if (override.candidates) {
|
|
17
|
+
base.candidates = { ...base.candidates, ...override.candidates };
|
|
18
|
+
}
|
|
19
|
+
if (override.postConstruct) {
|
|
20
|
+
base.postConstruct = [...base.postConstruct, ...override.postConstruct];
|
|
21
|
+
}
|
|
24
22
|
}
|
|
25
23
|
return base;
|
|
26
24
|
}
|
|
@@ -34,7 +32,7 @@ export class DependencyRegistryAdapter implements RegistryAdapter<InjectableConf
|
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
register(...data: Partial<InjectableConfig<unknown>>[]): InjectableConfig<unknown> {
|
|
37
|
-
this.#config ??= { class: this.#cls, candidates: {} };
|
|
35
|
+
this.#config ??= { class: this.#cls, candidates: {}, postConstruct: [] };
|
|
38
36
|
return combineClasses(this.#config, ...data);
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -61,13 +59,16 @@ export class DependencyRegistryAdapter implements RegistryAdapter<InjectableConf
|
|
|
61
59
|
return this.#config;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
finalize(): void {
|
|
62
|
+
finalize(parent?: InjectableConfig): void {
|
|
65
63
|
for (const method of Object.keys(this.#config.candidates)) {
|
|
66
64
|
const candidate = this.#config.candidates[method];
|
|
67
65
|
const candidateType = SchemaRegistryIndex.get(candidate.class).getMethodReturnType(method);
|
|
68
66
|
candidate.candidateType = candidateType;
|
|
69
67
|
candidate.qualifier ??= getDefaultQualifier(candidateType);
|
|
70
68
|
}
|
|
69
|
+
// Inherit post construct from parent
|
|
70
|
+
this.#config.postConstruct = [...(parent?.postConstruct ?? []), ...this.#config.postConstruct]
|
|
71
|
+
.sort((a, b) => (a.priority ?? 1) - (b.priority ?? 1));
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
getCandidateConfigs(): InjectableCandidate[] {
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { castKey, castTo, type Class, describeFunction, getParentClass, TypedObject } from '@travetto/runtime';
|
|
3
3
|
import { type SchemaFieldConfig, type SchemaParameterConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import type { Dependency, InjectableCandidate,
|
|
5
|
+
import type { Dependency, InjectableCandidate, InjectableConfig, PostConstructor, ResolutionType } from '../types.ts';
|
|
6
6
|
import { DependencyRegistryAdapter } from './registry-adapter.ts';
|
|
7
7
|
import { InjectionError } from '../error.ts';
|
|
8
8
|
import { DependencyRegistryResolver } from './registry-resolver.ts';
|
|
9
9
|
|
|
10
10
|
const MetadataSymbol = Symbol();
|
|
11
11
|
|
|
12
|
-
const hasPostConstruct = hasFunction<{ postConstruct: () => Promise<unknown> }>('postConstruct');
|
|
13
|
-
const hasPreDestroy = hasFunction<{ preDestroy: () => Promise<unknown> }>('preDestroy');
|
|
14
|
-
|
|
15
12
|
function readMetadata(item: { metadata?: Record<symbol, unknown> }): Dependency | undefined {
|
|
16
13
|
return castTo<Dependency | undefined>(item.metadata?.[MetadataSymbol]);
|
|
17
14
|
}
|
|
@@ -24,6 +21,18 @@ export class DependencyRegistryIndex implements RegistryIndex {
|
|
|
24
21
|
return this.#instance.store.getForRegister(cls);
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
static registerPostConstruct<T>(cls: Class<T>, handler: PostConstructor<T>): void {
|
|
25
|
+
this.#instance.store.getForRegister(cls).register({ postConstruct: [castTo(handler)] });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static registerClass(cls: Class, ...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
|
|
29
|
+
return this.#instance.store.getForRegister(cls).registerClass(...data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static registerFactory(cls: Class, method: string, ...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
|
|
33
|
+
return this.#instance.store.getForRegister(cls).registerFactory(method, ...data);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
static getInstance<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Promise<T> {
|
|
28
37
|
return this.#instance.getInstance(candidateType, qualifier, resolution);
|
|
29
38
|
}
|
|
@@ -40,10 +49,6 @@ export class DependencyRegistryIndex implements RegistryIndex {
|
|
|
40
49
|
return this.#instance.injectFields(cls, item, cls);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
static registerClassMetadata(cls: Class, metadata: InjectableClassMetadata): void {
|
|
44
|
-
SchemaRegistryIndex.getForRegister(cls).registerMetadata<InjectableClassMetadata>(MetadataSymbol, metadata);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
52
|
static registerParameterMetadata(cls: Class, method: string, index: number, metadata: Dependency): void {
|
|
48
53
|
SchemaRegistryIndex.getForRegister(cls).registerParameterMetadata(method, index, MetadataSymbol, metadata);
|
|
49
54
|
}
|
|
@@ -52,7 +57,6 @@ export class DependencyRegistryIndex implements RegistryIndex {
|
|
|
52
57
|
SchemaRegistryIndex.getForRegister(cls).registerFieldMetadata(field, MetadataSymbol, metadata);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
#instances = new Map<Class, Map<symbol, unknown>>();
|
|
56
60
|
#instancePromises = new Map<Class, Map<symbol, Promise<unknown>>>();
|
|
57
61
|
#resolver = new DependencyRegistryResolver();
|
|
58
62
|
|
|
@@ -135,12 +139,12 @@ export class DependencyRegistryIndex implements RegistryIndex {
|
|
|
135
139
|
/**
|
|
136
140
|
* Retrieve mapped dependencies
|
|
137
141
|
*/
|
|
138
|
-
async injectFields<T>(candidateType: Class, instance: T,
|
|
142
|
+
async injectFields<T>(candidateType: Class, instance: T, sourceClass: Class): Promise<T> {
|
|
139
143
|
const inputs = SchemaRegistryIndex.getOptional(candidateType)?.getFields() ?? {};
|
|
140
144
|
|
|
141
145
|
const promises = TypedObject.entries(inputs)
|
|
142
146
|
.filter(([key, input]) => readMetadata(input) !== undefined && (input.access !== 'readonly' && instance[castKey(key)] === undefined))
|
|
143
|
-
.map(async ([key, input]) => [key, await this.#resolveDependencyValue(readMetadata(input) ?? {}, input,
|
|
147
|
+
.map(async ([key, input]) => [key, await this.#resolveDependencyValue(readMetadata(input) ?? {}, input, sourceClass)] as const);
|
|
144
148
|
|
|
145
149
|
const pairs = await Promise.all(promises);
|
|
146
150
|
|
|
@@ -155,78 +159,35 @@ export class DependencyRegistryIndex implements RegistryIndex {
|
|
|
155
159
|
*/
|
|
156
160
|
async construct<T>(candidateType: Class<T>, qualifier: symbol): Promise<T> {
|
|
157
161
|
const { candidate } = this.#resolver.resolveCandidate(candidateType, qualifier);
|
|
158
|
-
const targetType = candidate
|
|
162
|
+
const { candidateType: targetType } = candidate;
|
|
159
163
|
const params = await this.fetchDependencyParameters(candidate);
|
|
160
|
-
const
|
|
164
|
+
const instance = await candidate.factory(...params);
|
|
161
165
|
|
|
162
166
|
// And auto-wire fields
|
|
163
|
-
await this.injectFields(targetType,
|
|
164
|
-
|
|
165
|
-
// Run post construct, if it wasn't passed in, otherwise it was already created
|
|
166
|
-
if (hasPostConstruct(inst) && !params.includes(inst)) {
|
|
167
|
-
await inst.postConstruct();
|
|
168
|
-
}
|
|
167
|
+
await this.injectFields(targetType, instance, candidate.class);
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Run post constructors if output is not already as a dependency
|
|
170
|
+
const isParameterTargetType = params.find(param => typeof param === 'object' && param !== null && param.constructor === targetType);
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
if (!isParameterTargetType) {
|
|
173
|
+
// Run post constructors
|
|
174
|
+
const postConstruct = this.store.has(targetType) ? this.getConfig(targetType).postConstruct : [];
|
|
175
|
+
for (const { operation } of postConstruct) {
|
|
176
|
+
await operation.call(instance);
|
|
177
|
+
}
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
|
|
179
|
-
return inst;
|
|
180
|
+
return instance;
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
/**
|
|
183
184
|
* Get or create the instance
|
|
184
185
|
*/
|
|
185
186
|
async getInstance<T>(candidateType: Class<T>, requestedQualifier?: symbol, resolution?: ResolutionType): Promise<T> {
|
|
186
|
-
if (!candidateType) {
|
|
187
|
-
throw new AppError('Unable to get instance when target is undefined');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
187
|
const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier, resolution);
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (this.#instancePromises.get(target)!.has(qualifier)) {
|
|
198
|
-
return castTo(this.#instancePromises.get(target)!.get(qualifier));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const instancePromise = this.construct(candidateType, qualifier);
|
|
202
|
-
this.#instancePromises.get(target)!.set(qualifier, instancePromise);
|
|
203
|
-
try {
|
|
204
|
-
const instance = await instancePromise;
|
|
205
|
-
this.#instances.get(target)!.set(qualifier, instance);
|
|
206
|
-
return instance;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
// Clear it out, don't save failed constructions
|
|
209
|
-
this.#instancePromises.get(target)!.delete(qualifier);
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Destroy an instance
|
|
216
|
-
*/
|
|
217
|
-
destroyInstance(candidateType: Class, requestedQualifier: symbol): void {
|
|
218
|
-
const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier);
|
|
219
|
-
|
|
220
|
-
const activeInstance = this.#instances.get(target)?.get(qualifier);
|
|
221
|
-
if (hasPreDestroy(activeInstance)) {
|
|
222
|
-
activeInstance.preDestroy();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.#resolver.removeClass(candidateType, qualifier);
|
|
226
|
-
this.#instances.get(target)?.delete(qualifier);
|
|
227
|
-
this.#instancePromises.get(target)?.delete(qualifier);
|
|
228
|
-
|
|
229
|
-
// May not exist
|
|
230
|
-
console.debug('On uninstall', { id: target, qualifier: qualifier.toString(), classId: target });
|
|
189
|
+
return castTo(this.#instancePromises
|
|
190
|
+
.getOrInsert(target, new Map())
|
|
191
|
+
.getOrInsertComputed(qualifier, () => this.construct(target, qualifier)));
|
|
231
192
|
}
|
|
232
193
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
1
|
+
import { SchemaRegistryIndex, UnknownType } from '@travetto/schema';
|
|
2
2
|
import { castTo, type Class } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { getDefaultQualifier, type InjectableCandidate, PrimaryCandidateSymbol, type ResolutionType } from '../types.ts';
|
|
@@ -7,10 +7,7 @@ import { InjectionError } from '../error.ts';
|
|
|
7
7
|
type Resolved<T> = { candidate: InjectableCandidate<T>, qualifier: symbol, target: Class };
|
|
8
8
|
|
|
9
9
|
function setInMap<T>(map: Map<Class, Map<typeof key, T>>, cls: Class, key: symbol | string, dest: T): void {
|
|
10
|
-
|
|
11
|
-
map.set(cls, new Map());
|
|
12
|
-
}
|
|
13
|
-
map.get(cls)!.set(key, dest);
|
|
10
|
+
map.getOrInsert(cls, new Map()).set(key, dest);
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
export class DependencyRegistryResolver {
|
|
@@ -125,6 +122,10 @@ export class DependencyRegistryResolver {
|
|
|
125
122
|
* @param qualifier
|
|
126
123
|
*/
|
|
127
124
|
resolveCandidate<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Resolved<T> {
|
|
125
|
+
if (!candidateType) {
|
|
126
|
+
throw new InjectionError('Unable to resolve candidate when target is undefined', UnknownType, qualifier ? [qualifier] : undefined);
|
|
127
|
+
}
|
|
128
|
+
|
|
128
129
|
const qualifiers = this.#byCandidateType.get(candidateType) ?? new Map<symbol, InjectableCandidate>();
|
|
129
130
|
|
|
130
131
|
let config: InjectableCandidate;
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface InjectableCandidate<T = unknown> {
|
|
|
63
63
|
qualifier?: symbol;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Post Construct Handler */
|
|
67
|
+
export type PostConstructor<T> = {
|
|
68
|
+
operation: ((this: T) => Promise<unknown> | unknown);
|
|
69
|
+
priority: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
66
73
|
/**
|
|
67
74
|
* Full injectable configuration for a class
|
|
68
75
|
*/
|
|
@@ -75,14 +82,14 @@ export interface InjectableConfig<T = unknown> {
|
|
|
75
82
|
* Candidates that are injectable
|
|
76
83
|
*/
|
|
77
84
|
candidates: Record<string, InjectableCandidate>;
|
|
85
|
+
/**
|
|
86
|
+
* Post construct methods to run after construction of the instance
|
|
87
|
+
*/
|
|
88
|
+
postConstruct: PostConstructor<T>[];
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export function getDefaultQualifier(cls: Class): symbol {
|
|
81
92
|
return Symbol.for(cls.Ⲑid);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
export const PrimaryCandidateSymbol = Symbol();
|
|
85
|
-
|
|
86
|
-
export type InjectableClassMetadata = {
|
|
87
|
-
postConstruct: Record<string, (<T>(inst: T) => Promise<void>)>;
|
|
88
|
-
};
|
|
95
|
+
export const PrimaryCandidateSymbol = Symbol();
|
package/support/test/suite.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Class } from '@travetto/runtime';
|
|
2
2
|
import { Registry } from '@travetto/registry';
|
|
3
|
-
import { SuiteRegistryIndex } from '@travetto/test';
|
|
3
|
+
import { SuiteRegistryIndex, type SuitePhaseHandler } from '@travetto/test';
|
|
4
4
|
|
|
5
5
|
import { DependencyRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
6
6
|
|
|
7
|
+
class ModelSuiteHandler<T extends object> implements SuitePhaseHandler<T> {
|
|
8
|
+
target: Class;
|
|
9
|
+
constructor(target: Class<T>) {
|
|
10
|
+
this.target = target;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async beforeEach(instance: T) {
|
|
14
|
+
await Registry.init();
|
|
15
|
+
await DependencyRegistryIndex.injectFields(instance, this.target);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Registers a suite as injectable
|
|
9
21
|
* @kind decorator
|
|
@@ -11,12 +23,7 @@ import { DependencyRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
|
11
23
|
export function InjectableSuite() {
|
|
12
24
|
return (cls: Class) => {
|
|
13
25
|
SuiteRegistryIndex.getForRegister(cls).register({
|
|
14
|
-
|
|
15
|
-
async function (this: unknown) {
|
|
16
|
-
await Registry.init();
|
|
17
|
-
await DependencyRegistryIndex.injectFields(this, cls);
|
|
18
|
-
},
|
|
19
|
-
]
|
|
26
|
+
phaseHandlers: [new ModelSuiteHandler(cls)]
|
|
20
27
|
});
|
|
21
28
|
};
|
|
22
29
|
}
|