@travetto/di 6.0.1 → 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.
@@ -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 ClassTarget<T = unknown> = Class<T> | Function;
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 Core<T = unknown> {
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: ClassTarget<T>;
17
+ target?: Class<T>;
15
18
  /**
16
19
  * Qualifier symbol
17
20
  */
18
- qualifier: symbol;
21
+ qualifier?: symbol;
19
22
  }
20
23
 
21
24
  /**
22
- * State of a Dependency Target
25
+ * Injectable candidate
23
26
  */
24
- interface CoreTarget<T = unknown> extends Core<T> {
27
+ export interface InjectableCandidate<T = unknown> {
25
28
  /**
26
- * Is this injectable enabled
29
+ * Reference for the class
27
30
  */
28
- enabled?: boolean | (() => boolean);
31
+ class: Class<T>;
29
32
  /**
30
- * Is this the primary instance
33
+ * Method that is injectable on class
31
34
  */
32
- primary?: boolean;
35
+ method: string | symbol;
33
36
  /**
34
- * Should the target be auto-created
37
+ * Method handle
35
38
  */
36
- autoCreate?: boolean;
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
- * Whether or not the dependency is optional
41
+ * The type of the candidate
45
42
  */
46
- optional?: boolean;
47
-
43
+ candidateType: Class;
48
44
  /**
49
- * Whether or not resolution of dependency should be flexible,
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
- class: Class<T>;
47
+ enabled?: boolean | (() => boolean);
63
48
  /**
64
- * Factory function for the injectable
49
+ * Is this the primary instance
65
50
  */
66
- factory?: (...args: unknown[]) => T;
51
+ primary?: boolean;
67
52
  /**
68
- * List of dependencies as fields or as constructor arguments
53
+ * Should the target be constructed on startup
69
54
  */
70
- dependencies: {
71
- cons?: Dependency[];
72
- fields: Record<string, Dependency>;
73
- };
55
+ autoInject?: boolean;
74
56
  /**
75
- * List of interface types
57
+ * Actual reference to a Class
76
58
  */
77
- interfaces: Class[];
59
+ target?: Class;
78
60
  /**
79
- * Post construct handlers
61
+ * Qualifier symbol
80
62
  */
81
- postConstruct: Record<string | symbol, PostConstructHandler<unknown>>;
63
+ qualifier?: symbol;
82
64
  }
83
65
 
84
66
  /**
85
- * Factory configuration
67
+ * Full injectable configuration for a class
86
68
  */
87
- export interface InjectableFactoryConfig<T = unknown> extends CoreTarget<T> {
69
+ export interface InjectableConfig<T = unknown> {
88
70
  /**
89
- * Src of the factory method
71
+ * Reference for the class
90
72
  */
91
- src: Class<T>;
73
+ class: Class<T>;
92
74
  /**
93
- * List of all dependencies as function arguments
75
+ * Candidates that are injectable
94
76
  */
95
- dependencies?: Dependency[];
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
+ };
@@ -1,21 +1,22 @@
1
- import { castTo, Class, ClassInstance } from '@travetto/runtime';
2
- import { RootRegistry } from '@travetto/registry';
3
- import { SuiteRegistry } from '@travetto/test';
1
+ import { Class } from '@travetto/runtime';
2
+ import { Registry } from '@travetto/registry';
3
+ import { SuiteRegistryIndex } from '@travetto/test';
4
4
 
5
- import { DependencyRegistry } from '../../src/registry.ts';
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 (target: Class) => {
12
- SuiteRegistry.registerPendingListener(
13
- target,
14
- async function (this: unknown) {
15
- await RootRegistry.init();
16
- await DependencyRegistry.injectFields(castTo<ClassInstance>(this), target);
17
- },
18
- 'beforeEach'
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
  }