@travetto/di
Version:
Dependency registration/management and injection support.
232 lines (186 loc) • 9.31 kB
text/typescript
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 });
}
}