@travetto/di 6.0.0 → 7.0.0-rc.0
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 +288 -0
- package/src/registry/registry-resolver.ts +175 -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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
2
|
+
import { castTo, Class } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
import { getDefaultQualifier, InjectableCandidate, PrimaryCandidateSymbol, ResolutionType } from '../types';
|
|
5
|
+
import { InjectionError } from '../error';
|
|
6
|
+
|
|
7
|
+
type Resolved<T> = { candidate: InjectableCandidate<T>, qualifier: symbol, targetId: string };
|
|
8
|
+
type ClassId = string;
|
|
9
|
+
|
|
10
|
+
function setInMap<T>(map: Map<string, Map<typeof key, T>>, src: string, key: symbol | string, dest: T): void {
|
|
11
|
+
if (!map.has(src)) {
|
|
12
|
+
map.set(src, new Map());
|
|
13
|
+
}
|
|
14
|
+
map.get(src)!.set(key, dest);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class DependencyRegistryResolver {
|
|
18
|
+
/**
|
|
19
|
+
* Default symbols
|
|
20
|
+
*/
|
|
21
|
+
#defaultSymbols = new Set<symbol>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maps from the requested type id to the candidates
|
|
25
|
+
*/
|
|
26
|
+
#byCandidateType = new Map<ClassId, Map<symbol, InjectableCandidate>>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps from inbound class id (file) to the candidates
|
|
30
|
+
*/
|
|
31
|
+
#byContainerType = new Map<ClassId, Map<symbol, InjectableCandidate>>();
|
|
32
|
+
|
|
33
|
+
#resolveQualifier<T>(type: Class<T>, resolution?: ResolutionType): symbol | undefined {
|
|
34
|
+
const qualifiers = this.#byCandidateType.get(type.Ⲑid) ?? new Map<symbol, InjectableCandidate>();
|
|
35
|
+
|
|
36
|
+
const resolved = [...qualifiers.keys()];
|
|
37
|
+
|
|
38
|
+
// If primary found
|
|
39
|
+
if (qualifiers.has(PrimaryCandidateSymbol)) {
|
|
40
|
+
return PrimaryCandidateSymbol;
|
|
41
|
+
} else {
|
|
42
|
+
const filtered = resolved.filter(x => !!x).filter(x => this.#defaultSymbols.has(x));
|
|
43
|
+
// If there is only one default symbol
|
|
44
|
+
if (filtered.length === 1) {
|
|
45
|
+
return filtered[0];
|
|
46
|
+
} else if (filtered.length > 1) {
|
|
47
|
+
// If dealing with sub types, prioritize exact matches
|
|
48
|
+
const exact = this.getCandidateEntries(type)
|
|
49
|
+
.map(([_, x]) => x)
|
|
50
|
+
.filter(x => x.candidateType === type);
|
|
51
|
+
|
|
52
|
+
if (exact.length === 1) {
|
|
53
|
+
return exact[0].qualifier;
|
|
54
|
+
} else {
|
|
55
|
+
if (resolution === 'any') {
|
|
56
|
+
return filtered[0];
|
|
57
|
+
} else {
|
|
58
|
+
throw new InjectionError('Dependency has multiple candidates', type, filtered);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a class with the dependency resolver
|
|
67
|
+
*/
|
|
68
|
+
registerClass(config: InjectableCandidate, baseParentId?: string): void {
|
|
69
|
+
const candidateType = config.candidateType;
|
|
70
|
+
const candidateClassId = candidateType.Ⲑid;
|
|
71
|
+
const target = config.target ?? candidateType;
|
|
72
|
+
|
|
73
|
+
const targetClassId = target.Ⲑid;
|
|
74
|
+
const isSelfTarget = candidateClassId === targetClassId;
|
|
75
|
+
const qualifier = config.qualifier ?? getDefaultQualifier(candidateType);
|
|
76
|
+
|
|
77
|
+
// Record qualifier if its the default for the class
|
|
78
|
+
if (config.qualifier === getDefaultQualifier(candidateType)) {
|
|
79
|
+
this.#defaultSymbols.add(config.qualifier);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Register inbound config by method and class
|
|
83
|
+
setInMap(this.#byContainerType, config.class.Ⲑid, config.method, config);
|
|
84
|
+
|
|
85
|
+
setInMap(this.#byCandidateType, targetClassId, qualifier, config);
|
|
86
|
+
setInMap(this.#byCandidateType, candidateClassId, qualifier, config);
|
|
87
|
+
|
|
88
|
+
// Track interface aliases as targets
|
|
89
|
+
const interfaces = SchemaRegistryIndex.has(candidateType) ?
|
|
90
|
+
SchemaRegistryIndex.get(candidateType).get().interfaces : [];
|
|
91
|
+
|
|
92
|
+
for (const { Ⲑid: interfaceId } of interfaces) {
|
|
93
|
+
setInMap(this.#byCandidateType, interfaceId, qualifier, config);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If targeting self (default @Injectable behavior)
|
|
97
|
+
if (isSelfTarget && baseParentId) {
|
|
98
|
+
setInMap(this.#byCandidateType, baseParentId, qualifier, config);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Registry primary candidates
|
|
102
|
+
if (config.primary) {
|
|
103
|
+
if (baseParentId) {
|
|
104
|
+
setInMap(this.#byCandidateType, baseParentId, PrimaryCandidateSymbol, config);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Register primary for self
|
|
108
|
+
setInMap(this.#byCandidateType, targetClassId, PrimaryCandidateSymbol, config);
|
|
109
|
+
|
|
110
|
+
// Register primary if only one interface provided and no parent config
|
|
111
|
+
if (interfaces.length === 1 && (!baseParentId || !this.#byContainerType.has(baseParentId))) {
|
|
112
|
+
const [primaryInterface] = interfaces;
|
|
113
|
+
const primaryClassId = primaryInterface.Ⲑid;
|
|
114
|
+
setInMap(this.#byCandidateType, primaryClassId, PrimaryCandidateSymbol, config);
|
|
115
|
+
} else if (isSelfTarget) {
|
|
116
|
+
// Register primary for all interfaces if self targeting
|
|
117
|
+
for (const { Ⲑid: interfaceId } of interfaces) {
|
|
118
|
+
setInMap(this.#byCandidateType, interfaceId, PrimaryCandidateSymbol, config);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the target given a qualifier
|
|
126
|
+
* @param candidateType
|
|
127
|
+
* @param qualifier
|
|
128
|
+
*/
|
|
129
|
+
resolveCandidate<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Resolved<T> {
|
|
130
|
+
const qualifiers = this.#byCandidateType.get(candidateType.Ⲑid) ?? new Map<symbol, InjectableCandidate>();
|
|
131
|
+
|
|
132
|
+
let config: InjectableCandidate;
|
|
133
|
+
|
|
134
|
+
if (qualifier && qualifiers.has(qualifier)) {
|
|
135
|
+
config = qualifiers.get(qualifier)!;
|
|
136
|
+
} else {
|
|
137
|
+
qualifier ??= this.#resolveQualifier(candidateType, resolution);
|
|
138
|
+
|
|
139
|
+
if (!qualifier) {
|
|
140
|
+
throw new InjectionError('Dependency not found', candidateType);
|
|
141
|
+
} else if (!qualifiers.has(qualifier)) {
|
|
142
|
+
if (!this.#defaultSymbols.has(qualifier) && resolution === 'loose') {
|
|
143
|
+
console.debug('Unable to find specific dependency, falling back to general instance', { qualifier, target: candidateType.Ⲑid });
|
|
144
|
+
return this.resolveCandidate(candidateType);
|
|
145
|
+
}
|
|
146
|
+
throw new InjectionError('Dependency not found', candidateType, [qualifier]);
|
|
147
|
+
} else {
|
|
148
|
+
config = qualifiers.get(qualifier!)!;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
candidate: castTo(config),
|
|
154
|
+
qualifier,
|
|
155
|
+
targetId: (config.target ?? config.candidateType).Ⲑid
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
removeClass(cls: Class, qualifier: symbol): void {
|
|
160
|
+
const classId = cls.Ⲑid;
|
|
161
|
+
this.#defaultSymbols.delete(qualifier);
|
|
162
|
+
this.#byCandidateType.get(classId)!.delete(qualifier);
|
|
163
|
+
this.#byContainerType.get(classId)!.delete(qualifier);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getCandidateEntries(candidateType: Class): [symbol, InjectableCandidate][] {
|
|
167
|
+
const candidateTypeId = candidateType.Ⲑid;
|
|
168
|
+
return [...this.#byCandidateType.get(candidateTypeId)?.entries() ?? []];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getContainerEntries(containerType: Class): [symbol, InjectableCandidate][] {
|
|
172
|
+
const containerTypeId = containerType.Ⲑid;
|
|
173
|
+
return [...this.#byContainerType.get(containerTypeId)?.entries() ?? []];
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,96 +1,89 @@
|
|
|
1
1
|
import { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
export type PostConstructHandler<T> = (value: T) => (void | Promise<void>);
|
|
3
|
+
export type ResolutionType = 'strict' | 'loose' | 'any';
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* State of a Dependency
|
|
9
7
|
*/
|
|
10
|
-
interface
|
|
8
|
+
export interface Dependency<T = unknown> {
|
|
9
|
+
/**
|
|
10
|
+
* Whether or not resolution of dependency should be flexible,
|
|
11
|
+
* or should be strict. Default is strict.
|
|
12
|
+
*/
|
|
13
|
+
resolution?: ResolutionType;
|
|
11
14
|
/**
|
|
12
15
|
* Actual reference to a Class
|
|
13
16
|
*/
|
|
14
|
-
target
|
|
17
|
+
target?: Class<T>;
|
|
15
18
|
/**
|
|
16
19
|
* Qualifier symbol
|
|
17
20
|
*/
|
|
18
|
-
qualifier
|
|
21
|
+
qualifier?: symbol;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
25
|
+
* Injectable candidate
|
|
23
26
|
*/
|
|
24
|
-
interface
|
|
27
|
+
export interface InjectableCandidate<T = unknown> {
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
29
|
+
* Reference for the class
|
|
27
30
|
*/
|
|
28
|
-
|
|
31
|
+
class: Class<T>;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* Method that is injectable on class
|
|
31
34
|
*/
|
|
32
|
-
|
|
35
|
+
method: string | symbol;
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
37
|
+
* Method handle
|
|
35
38
|
*/
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* State of a Dependency
|
|
41
|
-
*/
|
|
42
|
-
export interface Dependency<T = unknown> extends Core<T> {
|
|
39
|
+
factory: (...args: unknown[]) => T | Promise<T>;
|
|
43
40
|
/**
|
|
44
|
-
*
|
|
41
|
+
* The type of the candidate
|
|
45
42
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
candidateType: Class;
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
50
|
-
* or should be strict. Default is strict.
|
|
51
|
-
*/
|
|
52
|
-
resolution?: 'loose' | 'strict';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Injectable configuration
|
|
57
|
-
*/
|
|
58
|
-
export interface InjectableConfig<T = unknown> extends CoreTarget<T> {
|
|
59
|
-
/**
|
|
60
|
-
* Reference for the class
|
|
45
|
+
* Is this injectable enabled
|
|
61
46
|
*/
|
|
62
|
-
|
|
47
|
+
enabled?: boolean | (() => boolean);
|
|
63
48
|
/**
|
|
64
|
-
*
|
|
49
|
+
* Is this the primary instance
|
|
65
50
|
*/
|
|
66
|
-
|
|
51
|
+
primary?: boolean;
|
|
67
52
|
/**
|
|
68
|
-
*
|
|
53
|
+
* Should the target be constructed on startup
|
|
69
54
|
*/
|
|
70
|
-
|
|
71
|
-
cons?: Dependency[];
|
|
72
|
-
fields: Record<string, Dependency>;
|
|
73
|
-
};
|
|
55
|
+
autoInject?: boolean;
|
|
74
56
|
/**
|
|
75
|
-
*
|
|
57
|
+
* Actual reference to a Class
|
|
76
58
|
*/
|
|
77
|
-
|
|
59
|
+
target?: Class;
|
|
78
60
|
/**
|
|
79
|
-
*
|
|
61
|
+
* Qualifier symbol
|
|
80
62
|
*/
|
|
81
|
-
|
|
63
|
+
qualifier?: symbol;
|
|
82
64
|
}
|
|
83
65
|
|
|
84
66
|
/**
|
|
85
|
-
*
|
|
67
|
+
* Full injectable configuration for a class
|
|
86
68
|
*/
|
|
87
|
-
export interface
|
|
69
|
+
export interface InjectableConfig<T = unknown> {
|
|
88
70
|
/**
|
|
89
|
-
*
|
|
71
|
+
* Reference for the class
|
|
90
72
|
*/
|
|
91
|
-
|
|
73
|
+
class: Class<T>;
|
|
92
74
|
/**
|
|
93
|
-
*
|
|
75
|
+
* Candidates that are injectable
|
|
94
76
|
*/
|
|
95
|
-
|
|
96
|
-
}
|
|
77
|
+
candidates: Record<string | symbol, InjectableCandidate>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getDefaultQualifier(cls: Class): symbol {
|
|
81
|
+
return Symbol.for(cls.Ⲑid);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export const PrimaryCandidateSymbol = Symbol();
|
|
86
|
+
|
|
87
|
+
export type InjectableClassMetadata = {
|
|
88
|
+
postConstruct: Record<string | symbol, (<T>(inst: T) => Promise<void>)>;
|
|
89
|
+
};
|
package/support/test/suite.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Class } from '@travetto/runtime';
|
|
2
|
+
import { Registry } from '@travetto/registry';
|
|
3
|
+
import { SuiteRegistryIndex } from '@travetto/test';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DependencyRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Registers a suite as injectable
|
|
9
|
+
* @kind decorator
|
|
9
10
|
*/
|
|
10
11
|
export function InjectableSuite() {
|
|
11
|
-
return (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
12
|
+
return (cls: Class) => {
|
|
13
|
+
SuiteRegistryIndex.getForRegister(cls).register({
|
|
14
|
+
beforeEach: [
|
|
15
|
+
async function (this: unknown) {
|
|
16
|
+
await Registry.init();
|
|
17
|
+
await DependencyRegistryIndex.injectFields(this, cls);
|
|
18
|
+
},
|
|
19
|
+
]
|
|
20
|
+
});
|
|
20
21
|
};
|
|
21
22
|
}
|