UNPKG

@travetto/di

Version:

Dependency registration/management and injection support.

232 lines (186 loc) 9.31 kB
import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry'; import { AppError, castKey, castTo, type Class, describeFunction, getParentClass, hasFunction, TypedObject } from '@travetto/runtime'; import { type SchemaFieldConfig, type SchemaParameterConfig, SchemaRegistryIndex } from '@travetto/schema'; import type { Dependency, InjectableCandidate, InjectableClassMetadata, InjectableConfig, ResolutionType } from '../types.ts'; import { DependencyRegistryAdapter } from './registry-adapter.ts'; import { InjectionError } from '../error.ts'; import { DependencyRegistryResolver } from './registry-resolver.ts'; const MetadataSymbol = Symbol(); const hasPostConstruct = hasFunction<{ postConstruct: () => Promise<unknown> }>('postConstruct'); const hasPreDestroy = hasFunction<{ preDestroy: () => Promise<unknown> }>('preDestroy'); function readMetadata(item: { metadata?: Record<symbol, unknown> }): Dependency | undefined { return castTo<Dependency | undefined>(item.metadata?.[MetadataSymbol]); } export class DependencyRegistryIndex implements RegistryIndex { static #instance = Registry.registerIndex(DependencyRegistryIndex); static getForRegister(cls: Class): DependencyRegistryAdapter { return this.#instance.store.getForRegister(cls); } static getInstance<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Promise<T> { return this.#instance.getInstance(candidateType, qualifier, resolution); } static getCandidates<T>(candidateType: Class<T>): InjectableCandidate<T>[] { return this.#instance.getCandidates<T>(candidateType); } static getInstances<T>(candidateType: Class<T>, predicate?: (config: InjectableCandidate<T>) => boolean): Promise<T[]> { return this.#instance.getInstances<T>(candidateType, predicate); } static injectFields<T extends { constructor: Class<T> }>(item: T, cls = item.constructor): Promise<T> { return this.#instance.injectFields(cls, item, cls); } static registerClassMetadata(cls: Class, metadata: InjectableClassMetadata): void { SchemaRegistryIndex.getForRegister(cls).registerMetadata<InjectableClassMetadata>(MetadataSymbol, metadata); } static registerParameterMetadata(cls: Class, method: string, index: number, metadata: Dependency): void { SchemaRegistryIndex.getForRegister(cls).registerParameterMetadata(method, index, MetadataSymbol, metadata); } static registerFieldMetadata(cls: Class, field: string, metadata: Dependency): void { SchemaRegistryIndex.getForRegister(cls).registerFieldMetadata(field, MetadataSymbol, metadata); } #instances = new Map<Class, Map<symbol, unknown>>(); #instancePromises = new Map<Class, Map<symbol, Promise<unknown>>>(); #resolver = new DependencyRegistryResolver(); async #resolveDependencyValue(dependency: Dependency, input: SchemaFieldConfig | SchemaParameterConfig, cls: Class): Promise<unknown> { try { const target = dependency.target ?? input.type; return await this.getInstance(target, dependency.qualifier, dependency.resolution); } catch (error) { if (input.required?.active === false && error instanceof InjectionError && error.category === 'notfound') { return undefined; } else { if (error && error instanceof Error) { error.message = `${error.message} via=${cls.Ⲑid}[${input.name?.toString() ?? 'constructor'}]`; } throw error; } } } store = new RegistryIndexStore(DependencyRegistryAdapter); /** @private */ constructor(source: unknown) { Registry.validateConstructor(source); } getConfig(cls: Class): InjectableConfig { return this.store.get(cls).get(); } onCreate(cls: Class): void { const adapter = this.store.get(cls); for (const config of adapter.getCandidateConfigs()) { const parentClass = getParentClass(config.candidateType); const parentConfig = parentClass ? this.store.getOptional(parentClass) : undefined; const hasParentBase = (parentConfig || (parentClass && !!describeFunction(parentClass)?.abstract)); const baseParent = hasParentBase ? parentClass : undefined; this.#resolver.registerClass(config, baseParent); } } // Setup instances after change set complete onChangeSetComplete(classes: Class[]): void { for (const cls of classes) { const adapter = this.store.get(cls); for (const config of adapter.getCandidateConfigs()) { if (config.autoInject) { this.getInstance(config.candidateType, config.qualifier); } } } } /** * Get all available candidates for a given type */ getCandidates<T>(candidateType: Class<T>): InjectableCandidate<T>[] { return this.#resolver.getCandidateEntries(candidateType).map(([_, candidate]) => castTo<InjectableCandidate<T>>(candidate)); } /** * Get candidate instances by target type, with an optional filter */ getInstances<T>(candidateType: Class<T>, predicate?: (config: InjectableCandidate<T>) => boolean): Promise<T[]> { const inputs = this.getCandidates<T>(candidateType).filter(candidate => !predicate || predicate(candidate)); return Promise.all(inputs.map(candidate => this.getInstance<T>(candidate.class, candidate.qualifier))); } /** * Retrieve list dependencies */ async fetchDependencyParameters<T>(candidate: InjectableCandidate<T>): Promise<unknown[]> { const inputs = SchemaRegistryIndex.has(candidate.class) ? SchemaRegistryIndex.get(candidate.class).getMethod(candidate.method).parameters : []; const promises = inputs .map(input => this.#resolveDependencyValue(readMetadata(input) ?? {}, input, candidate.class)); return await Promise.all(promises); } /** * Retrieve mapped dependencies */ async injectFields<T>(candidateType: Class, instance: T, srcClass: Class): Promise<T> { const inputs = SchemaRegistryIndex.getOptional(candidateType)?.getFields() ?? {}; const promises = TypedObject.entries(inputs) .filter(([key, input]) => readMetadata(input) !== undefined && (input.access !== 'readonly' && instance[castKey(key)] === undefined)) .map(async ([key, input]) => [key, await this.#resolveDependencyValue(readMetadata(input) ?? {}, input, srcClass)] as const); const pairs = await Promise.all(promises); for (const [key, value] of pairs) { instance[castKey(key)] = castTo(value); } return instance; } /** * Actually construct an instance while resolving the dependencies */ async construct<T>(candidateType: Class<T>, qualifier: symbol): Promise<T> { const { candidate } = this.#resolver.resolveCandidate(candidateType, qualifier); const targetType = candidate.candidateType; const params = await this.fetchDependencyParameters(candidate); const inst = await candidate.factory(...params); // And auto-wire fields await this.injectFields(targetType, inst, candidate.class); // Run post construct, if it wasn't passed in, otherwise it was already created if (hasPostConstruct(inst) && !params.includes(inst)) { await inst.postConstruct(); } const metadata = SchemaRegistryIndex.has(targetType) ? SchemaRegistryIndex.get(targetType).getMetadata<InjectableClassMetadata>(MetadataSymbol) : undefined; // Run post constructors for (const operation of Object.values(metadata?.postConstruct ?? {})) { await operation(inst); } // Proxy if necessary return inst; } /** * Get or create the instance */ async getInstance<T>(candidateType: Class<T>, requestedQualifier?: symbol, resolution?: ResolutionType): Promise<T> { if (!candidateType) { throw new AppError('Unable to get instance when target is undefined'); } const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier, resolution); if (!this.#instances.has(target)) { this.#instances.set(target, new Map()); this.#instancePromises.set(target, new Map()); } if (this.#instancePromises.get(target)!.has(qualifier)) { return castTo(this.#instancePromises.get(target)!.get(qualifier)); } const instancePromise = this.construct(candidateType, qualifier); this.#instancePromises.get(target)!.set(qualifier, instancePromise); try { const instance = await instancePromise; this.#instances.get(target)!.set(qualifier, instance); return instance; } catch (error) { // Clear it out, don't save failed constructions this.#instancePromises.get(target)!.delete(qualifier); throw error; } } /** * Destroy an instance */ destroyInstance(candidateType: Class, requestedQualifier: symbol): void { const { target, qualifier } = this.#resolver.resolveCandidate(candidateType, requestedQualifier); const activeInstance = this.#instances.get(target)?.get(qualifier); if (hasPreDestroy(activeInstance)) { activeInstance.preDestroy(); } this.#resolver.removeClass(candidateType, qualifier); this.#instances.get(target)?.delete(qualifier); this.#instancePromises.get(target)?.delete(qualifier); // May not exist console.debug('On uninstall', { id: target, qualifier: qualifier.toString(), classId: target }); } }