@travetto/di 8.0.0-alpha.0 → 8.0.0-alpha.10

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#L17). 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": "8.0.0-alpha.0",
3
+ "version": "8.0.0-alpha.10",
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": "^8.0.0-alpha.0"
31
+ "@travetto/registry": "^8.0.0-alpha.10"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/transformer": "^8.0.0-alpha.0"
34
+ "@travetto/transformer": "^8.0.0-alpha.5"
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
  }
@@ -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,16 +1,14 @@
1
1
  import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
- import { RuntimeError, 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
-
12
- const hasPostConstruct = hasFunction<{ postConstruct: () => Promise<unknown> }>('postConstruct');
13
- const hasPreDestroy = hasFunction<{ preDestroy: () => Promise<unknown> }>('preDestroy');
11
+ const PostConstructionRan = Symbol();
14
12
 
15
13
  function readMetadata(item: { metadata?: Record<symbol, unknown> }): Dependency | undefined {
16
14
  return castTo<Dependency | undefined>(item.metadata?.[MetadataSymbol]);
@@ -24,6 +22,18 @@ export class DependencyRegistryIndex implements RegistryIndex {
24
22
  return this.#instance.store.getForRegister(cls);
25
23
  }
26
24
 
25
+ static registerPostConstruct<T>(cls: Class<T>, handler: PostConstructor<T>): void {
26
+ this.#instance.store.getForRegister(cls).register({ postConstruct: [castTo(handler)] });
27
+ }
28
+
29
+ static registerClass(cls: Class, ...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
30
+ return this.#instance.store.getForRegister(cls).registerClass(...data);
31
+ }
32
+
33
+ static registerFactory(cls: Class, method: string, ...data: Partial<InjectableCandidate<unknown>>[]): InjectableCandidate {
34
+ return this.#instance.store.getForRegister(cls).registerFactory(method, ...data);
35
+ }
36
+
27
37
  static getInstance<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Promise<T> {
28
38
  return this.#instance.getInstance(candidateType, qualifier, resolution);
29
39
  }
@@ -40,10 +50,6 @@ export class DependencyRegistryIndex implements RegistryIndex {
40
50
  return this.#instance.injectFields(cls, item, cls);
41
51
  }
42
52
 
43
- static registerClassMetadata(cls: Class, metadata: InjectableClassMetadata): void {
44
- SchemaRegistryIndex.getForRegister(cls).registerMetadata<InjectableClassMetadata>(MetadataSymbol, metadata);
45
- }
46
-
47
53
  static registerParameterMetadata(cls: Class, method: string, index: number, metadata: Dependency): void {
48
54
  SchemaRegistryIndex.getForRegister(cls).registerParameterMetadata(method, index, MetadataSymbol, metadata);
49
55
  }
@@ -52,7 +58,6 @@ export class DependencyRegistryIndex implements RegistryIndex {
52
58
  SchemaRegistryIndex.getForRegister(cls).registerFieldMetadata(field, MetadataSymbol, metadata);
53
59
  }
54
60
 
55
- #instances = new Map<Class, Map<symbol, unknown>>();
56
61
  #instancePromises = new Map<Class, Map<symbol, Promise<unknown>>>();
57
62
  #resolver = new DependencyRegistryResolver();
58
63
 
@@ -135,12 +140,12 @@ export class DependencyRegistryIndex implements RegistryIndex {
135
140
  /**
136
141
  * Retrieve mapped dependencies
137
142
  */
138
- async injectFields<T>(candidateType: Class, instance: T, srcClass: Class): Promise<T> {
143
+ async injectFields<T>(candidateType: Class, instance: T, sourceClass: Class): Promise<T> {
139
144
  const inputs = SchemaRegistryIndex.getOptional(candidateType)?.getFields() ?? {};
140
145
 
141
146
  const promises = TypedObject.entries(inputs)
142
147
  .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);
148
+ .map(async ([key, input]) => [key, await this.#resolveDependencyValue(readMetadata(input) ?? {}, input, sourceClass)] as const);
144
149
 
145
150
  const pairs = await Promise.all(promises);
146
151
 
@@ -155,76 +160,36 @@ export class DependencyRegistryIndex implements RegistryIndex {
155
160
  */
156
161
  async construct<T>(candidateType: Class<T>, qualifier: symbol): Promise<T> {
157
162
  const { candidate } = this.#resolver.resolveCandidate(candidateType, qualifier);
158
- const targetType = candidate.candidateType;
163
+ const { candidateType: targetType } = candidate;
159
164
  const params = await this.fetchDependencyParameters(candidate);
160
- const inst = await candidate.factory(...params);
165
+ const instance = await candidate.factory(...params);
161
166
 
162
167
  // 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
- }
168
+ await this.injectFields(targetType, instance, candidate.class);
169
169
 
170
- const metadata = SchemaRegistryIndex.has(targetType) ?
171
- SchemaRegistryIndex.get(targetType).getMetadata<InjectableClassMetadata>(MetadataSymbol) : undefined;
170
+ // Run post constructors if not run yet
171
+ const constructed: T & { [PostConstructionRan]?: boolean } = instance!;
172
+ if (!constructed[PostConstructionRan]) {
173
+ constructed[PostConstructionRan] = true;
172
174
 
173
- // Run post constructors
174
- for (const operation of Object.values(metadata?.postConstruct ?? {})) {
175
- await operation(inst);
175
+ // Run post constructors
176
+ const postConstruct = this.store.has(targetType) ? this.getConfig(targetType).postConstruct : [];
177
+ for (const { operation } of postConstruct) {
178
+ await operation.call(instance);
179
+ }
176
180
  }
177
181
 
178
- // Proxy if necessary
179
- return inst;
182
+ return instance;
180
183
  }
181
184
 
182
185
  /**
183
186
  * Get or create the instance
184
187
  */
185
188
  async getInstance<T>(candidateType: Class<T>, requestedQualifier?: symbol, resolution?: ResolutionType): Promise<T> {
186
- if (!candidateType) {
187
- throw new RuntimeError('Unable to get instance when target is undefined');
188
- }
189
-
190
189
  const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier, resolution);
191
190
 
192
- const instances = this.#instances.getOrInsert(target, new Map());
193
- const instancePromises = this.#instancePromises.getOrInsert(target, new Map());
194
-
195
- if (instancePromises.has(qualifier)) {
196
- return castTo(instancePromises.get(qualifier));
197
- }
198
-
199
- const instancePromise = this.construct(candidateType, qualifier);
200
- instancePromises.set(qualifier, instancePromise);
201
- try {
202
- const instance = await instancePromise;
203
- instances.set(qualifier, instance);
204
- return instance;
205
- } catch (error) {
206
- // Clear it out, don't save failed constructions
207
- instancePromises.delete(qualifier);
208
- throw error;
209
- }
210
- }
211
-
212
- /**
213
- * Destroy an instance
214
- */
215
- destroyInstance(candidateType: Class, requestedQualifier: symbol): void {
216
- const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier);
217
-
218
- const activeInstance = this.#instances.get(target)?.get(qualifier);
219
- if (hasPreDestroy(activeInstance)) {
220
- activeInstance.preDestroy();
221
- }
222
-
223
- this.#resolver.removeClass(candidateType, qualifier);
224
- this.#instances.get(target)?.delete(qualifier);
225
- this.#instancePromises.get(target)?.delete(qualifier);
226
-
227
- // May not exist
228
- console.debug('On uninstall', { id: target, qualifier: qualifier.toString(), classId: target });
191
+ return castTo(this.#instancePromises
192
+ .getOrInsert(target, new Map())
193
+ .getOrInsertComputed(qualifier, () => this.construct(target, qualifier)));
229
194
  }
230
195
  }
@@ -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';
@@ -122,6 +122,10 @@ export class DependencyRegistryResolver {
122
122
  * @param qualifier
123
123
  */
124
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
+
125
129
  const qualifiers = this.#byCandidateType.get(candidateType) ?? new Map<symbol, InjectableCandidate>();
126
130
 
127
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();