@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 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#L19). The registry allows for requesting a dependency by class reference:
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": "7.1.4",
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": "^7.1.4"
31
+ "@travetto/registry": "^8.0.0-alpha.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/transformer": "^7.1.3"
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.getForRegister(cls).registerClass(fromInput(input));
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.getForRegister(cls).registerFactory(property, fromInput(input), {
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 { AppError, type Class } from '@travetto/runtime';
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 AppError {
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
- Object.assign(base, {
17
- ...base,
18
- ...override,
19
- candidates: {
20
- ...base.candidates,
21
- ...override.candidates,
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 { AppError, castKey, castTo, type Class, describeFunction, getParentClass, hasFunction, TypedObject } from '@travetto/runtime';
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, InjectableClassMetadata, InjectableConfig, ResolutionType } from '../types.ts';
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, srcClass: Class): Promise<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, srcClass)] as const);
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.candidateType;
162
+ const { candidateType: targetType } = candidate;
159
163
  const params = await this.fetchDependencyParameters(candidate);
160
- const inst = await candidate.factory(...params);
164
+ const instance = await candidate.factory(...params);
161
165
 
162
166
  // And auto-wire fields
163
- await this.injectFields(targetType, inst, candidate.class);
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
- const metadata = SchemaRegistryIndex.has(targetType) ?
171
- SchemaRegistryIndex.get(targetType).getMetadata<InjectableClassMetadata>(MetadataSymbol) : undefined;
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
- // Run post constructors
174
- for (const operation of Object.values(metadata?.postConstruct ?? {})) {
175
- await operation(inst);
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
- // Proxy if necessary
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
- if (!this.#instances.has(target)) {
193
- this.#instances.set(target, new Map());
194
- this.#instancePromises.set(target, new Map());
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
- if (!map.has(cls)) {
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();
@@ -1,9 +1,21 @@
1
- import type { Class } from '@travetto/runtime';
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
- beforeEach: [
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
  }