UNPKG

@travetto/di

Version:

Dependency registration/management and injection support.

566 lines (473 loc) 17.7 kB
import { Class, Runtime, asConstructable, castTo, classConstruct, describeFunction, asFull, castKey, TypedFunction, hasFunction } from '@travetto/runtime'; import { MetadataRegistry, RootRegistry, ChangeEvent } from '@travetto/registry'; import { Dependency, InjectableConfig, ClassTarget, InjectableFactoryConfig } from './types'; import { InjectionError } from './error'; import { AutoCreateTarget } from './internal/types'; type TargetId = string; type ClassId = string; export type Resolved<T> = { config: InjectableConfig<T>, qualifier: symbol, id: string }; export type ResolutionType = 'strict' | 'loose' | 'any'; const PrimaryCandidateSymbol = Symbol.for('@travetto/di:primary'); const hasPostConstruct = hasFunction<{ postConstruct: () => Promise<unknown> }>('postConstruct'); const hasPreDestroy = hasFunction<{ preDestroy: () => Promise<unknown> }>('preDestroy'); /** * Dependency registry */ class $DependencyRegistry extends MetadataRegistry<InjectableConfig> { pendingFinalize: Class[] = []; defaultSymbols = new Set<symbol>(); instances = new Map<TargetId, Map<symbol, unknown>>(); instancePromises = new Map<TargetId, Map<symbol, Promise<unknown>>>(); factories = new Map<TargetId, Map<Class, InjectableConfig>>(); targetToClass = new Map<TargetId, Map<symbol, string>>(); classToTarget = new Map<ClassId, Map<symbol, TargetId>>(); constructor() { super(RootRegistry); } /** * Resolve the target given a qualifier * @param target * @param qualifier */ resolveTarget<T>(target: ClassTarget<T>, qualifier?: symbol, resolution?: ResolutionType): Resolved<T> { const qualifiers = this.targetToClass.get(target.Ⲑid) ?? new Map<symbol, string>(); let cls: string | undefined; if (qualifier && qualifiers.has(qualifier)) { cls = qualifiers.get(qualifier); } else { const resolved = [...qualifiers.keys()]; if (!qualifier) { // If primary found if (qualifiers.has(PrimaryCandidateSymbol)) { qualifier = PrimaryCandidateSymbol; } else { // If there is only one default symbol const filtered = resolved.filter(x => !!x).filter(x => this.defaultSymbols.has(x)); if (filtered.length === 1) { qualifier = filtered[0]; } else if (filtered.length > 1) { // If dealing with sub types, prioritize exact matches const exact = this .getCandidateTypes(castTo<Class>(target)) .filter(x => x.class === target); if (exact.length === 1) { qualifier = exact[0].qualifier; } else { if (resolution === 'any') { qualifier = filtered[0]; } else { throw new InjectionError('Dependency has multiple candidates', target, filtered); } } } } } if (!qualifier) { throw new InjectionError('Dependency not found', target); } else if (!qualifiers.has(qualifier)) { if (!this.defaultSymbols.has(qualifier) && resolution === 'loose') { console.debug('Unable to find specific dependency, falling back to general instance', { qualifier, target: target.Ⲑid }); return this.resolveTarget(target); } throw new InjectionError('Dependency not found', target, [qualifier]); } else { cls = qualifiers.get(qualifier!)!; } } const config: InjectableConfig<T> = castTo(this.get(cls!)); return { qualifier, config, id: (config.factory ? config.target : config.class).Ⲑid }; } /** * Retrieve all dependencies */ async fetchDependencies(managed: InjectableConfig, deps?: Dependency[]): Promise<unknown[]> { if (!deps || !deps.length) { return []; } const promises = deps.map(async x => { try { return await this.getInstance(x.target, x.qualifier, x.resolution); } catch (err) { if (x.optional && err instanceof InjectionError && err.category === 'notfound') { return undefined; } else { if (err && err instanceof Error) { err.message = `${err.message} via=${managed.class.Ⲑid}`; } throw err; } } }); return await Promise.all(promises); } /** * Resolve all field dependencies */ async resolveFieldDependencies<T>(config: InjectableConfig<T>, instance: T): Promise<void> { const keys = Object.keys(config.dependencies.fields ?? {}) .filter(k => instance[castKey<T>(k)] === undefined); // Filter out already set ones // And auto-wire if (keys.length) { const deps = await this.fetchDependencies(config, keys.map(x => config.dependencies.fields[x])); for (let i = 0; i < keys.length; i++) { instance[castKey<T>(keys[i])] = castTo(deps[i]); } } } /** * Actually construct an instance while resolving the dependencies */ async construct<T>(target: ClassTarget<T>, qualifier: symbol): Promise<T> { const managed = this.resolveTarget(target, qualifier).config; // Only fetch constructor values const consValues = await this.fetchDependencies(managed, managed.dependencies.cons); // Create instance const inst = managed.factory ? managed.factory(...consValues) : classConstruct(managed.class, consValues); // And auto-wire fields await this.resolveFieldDependencies(managed, inst); // If factory with field properties on the sub class if (managed.factory) { const resolved = this.get(asConstructable(inst).constructor); if (resolved) { await this.resolveFieldDependencies(resolved, inst); } } // Run post construct, if it wasn't passed in, otherwise it was already created if (hasPostConstruct(inst) && !consValues.includes(inst)) { await inst.postConstruct(); } return inst; } /** * Create the instance */ async createInstance<T>(target: ClassTarget<T>, qualifier: symbol): Promise<T> { const classId = this.resolveTarget(target, qualifier).id; if (!this.instances.has(classId)) { this.instances.set(classId, new Map()); this.instancePromises.set(classId, new Map()); } if (this.instancePromises.get(classId)!.has(qualifier)) { return castTo(this.instancePromises.get(classId)!.get(qualifier)); } const instancePromise = this.construct(target, qualifier); this.instancePromises.get(classId)!.set(qualifier, instancePromise); try { const instance = await instancePromise; this.instances.get(classId)!.set(qualifier, instance); return instance; } catch (err) { // Clear it out, don't save failed constructions this.instancePromises.get(classId)!.delete(qualifier); throw err; } } /** * Destroy an instance */ destroyInstance(cls: Class, qualifier: symbol): void { const classId = cls.Ⲑid; const activeInstance = this.instances.get(classId)!.get(qualifier); if (hasPreDestroy(activeInstance)) { activeInstance.preDestroy(); } this.defaultSymbols.delete(qualifier); this.instances.get(classId)!.delete(qualifier); this.instancePromises.get(classId)!.delete(qualifier); this.classToTarget.get(classId)!.delete(qualifier); console.debug('On uninstall', { id: classId, qualifier: qualifier.toString(), classId }); } override async init(): Promise<void> { await super.init(); if (Runtime.dynamic) { const { DependencyRegistration } = await import('../support/dynamic.injection'); DependencyRegistration.init(this); } // Allow for auto-creation for (const cfg of await this.getCandidateTypes(AutoCreateTarget)) { await this.getInstance(cfg.class, cfg.qualifier); } } /** * Handle initial installation for the entire registry */ override initialInstall(): Class[] { const finalizing = this.pendingFinalize; this.pendingFinalize = []; for (const cls of finalizing) { this.install(cls, { type: 'added', curr: cls }); } return []; } /** * Register a cls as pending */ createPending(cls: Class): Partial<InjectableConfig> { if (!this.resolved) { this.pendingFinalize.push(cls); } return { class: cls, enabled: true, target: cls, interfaces: [], dependencies: { fields: {}, cons: [] } }; } /** * Get an instance by type and qualifier */ async getInstance<T>(target: ClassTarget<T>, qual?: symbol, resolution?: ResolutionType): Promise<T> { this.verifyInitialized(); const { id: classId, qualifier } = this.resolveTarget(target, qual, resolution); if (!this.instances.has(classId) || !this.instances.get(classId)!.has(qualifier)) { await this.createInstance(target, qualifier); // Wait for proxy } return castTo(this.instances.get(classId)!.get(qualifier)); } /** * Get all available candidate types for the target */ getCandidateTypes<T, U = T>(target: Class<U>): InjectableConfig<T>[] { const qualifiers = this.targetToClass.get(target.Ⲑid)!; const uniqueQualifiers = qualifiers ? Array.from(new Set(qualifiers.values())) : []; return castTo(uniqueQualifiers.map(id => this.get(id))); } /** * Get candidate instances by target type, with an optional filter */ getCandidateInstances<T>(target: Class, predicate?: (cfg: InjectableConfig<T>) => boolean): Promise<T[]> { const inputs = this.getCandidateTypes<T>(target).filter(x => !predicate || predicate(x)); return Promise.all(inputs.map(l => this.getInstance<T>(l.class, l.qualifier))); } /** * Register a constructor with dependencies */ registerConstructor<T>(cls: Class<T>, dependencies?: Dependency[]): void { const conf = this.getOrCreatePending(cls); conf.dependencies!.cons = dependencies; } /** * Register a property as a dependency */ registerProperty<T>(cls: Class<T>, field: string, dependency: Dependency): void { const conf = this.getOrCreatePending(cls); conf.dependencies!.fields[field] = dependency; } /** * Register a class */ registerClass<T>(cls: Class<T>, pConfig: Partial<InjectableConfig<T>> = {}): void { const config = this.getOrCreatePending(pConfig.class ?? cls); config.enabled = pConfig.enabled ?? config.enabled; config.class = cls; config.qualifier = pConfig.qualifier ?? config.qualifier ?? Symbol.for(cls.Ⲑid); if (pConfig.interfaces) { config.interfaces?.push(...pConfig.interfaces); } if (pConfig.primary !== undefined) { config.primary = pConfig.primary; } if (pConfig.factory) { config.factory = pConfig.factory ?? config.factory; } if (pConfig.target) { config.target = pConfig.target; } if (pConfig.dependencies) { config.dependencies = { ...pConfig.dependencies, fields: { ...pConfig.dependencies.fields } }; } } /** * Register a factory configuration */ registerFactory(config: Omit<InjectableFactoryConfig, 'qualifier'> & { id: string; qualifier?: undefined | symbol; fn: TypedFunction; }): void { const finalConfig: Partial<InjectableConfig> = {}; finalConfig.enabled = config.enabled ?? true; finalConfig.factory = config.fn; finalConfig.target = config.target; finalConfig.qualifier = config.qualifier; if (!finalConfig.qualifier) { finalConfig.qualifier = Symbol.for(config.id); } if (config.primary !== undefined) { finalConfig.primary = config.primary; } finalConfig.dependencies = { fields: {} }; if (config.dependencies) { finalConfig.dependencies.cons = config.dependencies; } // Create mock cls for DI purposes const fnClass = class { static Ⲑid = config.id; }; finalConfig.class = fnClass; this.registerClass(fnClass, finalConfig); const srcClassId = config.src.Ⲑid; if (!this.factories.has(srcClassId)) { this.factories.set(srcClassId, new Map()); } this.factories.get(srcClassId)!.set(fnClass, asFull(finalConfig)); } /** * On Install event */ override onInstall<T>(cls: Class<T>, e: ChangeEvent<Class<T>>): void { super.onInstall(cls, e); const classId = cls.Ⲑid; // Install factories separate from classes if (this.factories.has(classId)) { for (const fact of this.factories.get(classId)!.keys()) { this.onInstall(fact, e); } } } /** * Handle installing a class */ onInstallFinalize<T>(cls: Class<T>): InjectableConfig<T> { const classId = cls.Ⲑid; const config: InjectableConfig<T> = castTo(this.getOrCreatePending(cls)); if (!(typeof config.enabled === 'boolean' ? config.enabled : config.enabled())) { return config; // Do not setup if disabled } // Allow for the factory to fulfill the target let parentClass: Function = config.factory ? config.target : Object.getPrototypeOf(cls); if (config.factory) { while (describeFunction(Object.getPrototypeOf(parentClass))?.abstract) { parentClass = Object.getPrototypeOf(parentClass); } if (!this.targetToClass.has(classId)) { this.targetToClass.set(classId, new Map()); } // Make explicitly discoverable as self this.targetToClass.get(classId)?.set(config.qualifier, classId); } const parentConfig = this.get(parentClass.Ⲑid); if (parentConfig) { config.dependencies.fields = { ...parentConfig.dependencies!.fields, ...config.dependencies.fields }; // collect interfaces config.interfaces = [ ...parentConfig.interfaces, ...config.interfaces ]; // Inherit cons deps if no constructor defined if (config.dependencies.cons === undefined) { config.dependencies.cons = parentConfig.dependencies.cons; } } if (describeFunction(cls)?.abstract) { // Skip out early, only needed to inherit return config; } if (!this.classToTarget.has(classId)) { this.classToTarget.set(classId, new Map()); } const targetClassId = config.target.Ⲑid; if (!this.targetToClass.has(targetClassId)) { this.targetToClass.set(targetClassId, new Map()); } if (config.qualifier === Symbol.for(classId)) { this.defaultSymbols.add(config.qualifier); } this.targetToClass.get(targetClassId)!.set(config.qualifier, classId); this.classToTarget.get(classId)!.set(config.qualifier, targetClassId); // If aliased for (const el of config.interfaces) { const elClassId = el.Ⲑid; if (!this.targetToClass.has(elClassId)) { this.targetToClass.set(elClassId, new Map()); } this.targetToClass.get(elClassId)!.set(config.qualifier, classId); this.classToTarget.get(classId)!.set(Symbol.for(elClassId), elClassId); if (config.primary && (classId === targetClassId || config.factory)) { this.targetToClass.get(elClassId)!.set(PrimaryCandidateSymbol, classId); } } // If targeting self (default @Injectable behavior) if ((classId === targetClassId || config.factory) && (parentConfig || describeFunction(parentClass)?.abstract)) { const parentId = parentClass.Ⲑid; if (!this.targetToClass.has(parentId)) { this.targetToClass.set(parentId, new Map()); } if (config.primary) { this.targetToClass.get(parentId)!.set(PrimaryCandidateSymbol, classId); } this.targetToClass.get(parentId)!.set(config.qualifier, classId); this.classToTarget.get(classId)!.set(config.qualifier, parentId); } if (config.primary) { if (!this.targetToClass.has(classId)) { this.targetToClass.set(classId, new Map()); } this.targetToClass.get(classId)!.set(PrimaryCandidateSymbol, classId); if (config.factory) { this.targetToClass.get(targetClassId)!.set(PrimaryCandidateSymbol, classId); } // Register primary if only one interface provided and no parent config if (config.interfaces.length === 1 && !parentConfig) { const [primaryInterface] = config.interfaces; const primaryClassId = primaryInterface.Ⲑid; if (!this.targetToClass.has(primaryClassId)) { this.targetToClass.set(primaryClassId, new Map()); } this.targetToClass.get(primaryClassId)!.set(PrimaryCandidateSymbol, classId); } } return config; } /** * Handle uninstalling a class */ override onUninstallFinalize(cls: Class): void { const classId = cls.Ⲑid; if (!this.classToTarget.has(classId)) { return; } if (this.instances.has(classId)) { for (const qualifier of this.classToTarget.get(classId)!.keys()) { this.destroyInstance(cls, qualifier); } } } /** * Inject fields into instance */ async injectFields<T extends { constructor: Class<T> }>(o: T, cls = o.constructor): Promise<void> { this.verifyInitialized(); // Compute fields to be auto-wired return await this.resolveFieldDependencies(this.get(cls), o); } /** * Execute the run method of a given class */ async runInstance<T extends { run(..._args: unknown[]): unknown }>( cls: Class<T>, ...args: Parameters<T['run']> ): Promise<Awaited<ReturnType<T['run']>>> { await RootRegistry.init(); const inst = await this.getInstance<T>(cls); return castTo<Awaited<ReturnType<T['run']>>>(inst.run(...args)); } } export const DependencyRegistry = new $DependencyRegistry();