UNPKG

@travetto/di

Version:

Dependency registration/management and injection support.

170 lines (141 loc) 6.06 kB
import { SchemaRegistryIndex } from '@travetto/schema'; import { castTo, type Class } from '@travetto/runtime'; import { getDefaultQualifier, type InjectableCandidate, PrimaryCandidateSymbol, type ResolutionType } from '../types.ts'; import { InjectionError } from '../error.ts'; type Resolved<T> = { candidate: InjectableCandidate<T>, qualifier: symbol, target: Class }; function setInMap<T>(map: Map<Class, Map<typeof key, T>>, cls: Class, key: symbol | string, dest: T): void { if (!map.has(cls)) { map.set(cls, new Map()); } map.get(cls)!.set(key, dest); } export class DependencyRegistryResolver { /** * Default symbols */ #defaultSymbols = new Set<symbol>(); /** * Maps from the requested type to the candidates */ #byCandidateType = new Map<Class, Map<symbol, InjectableCandidate>>(); /** * Maps from inbound class file) to the candidates */ #byContainerType = new Map<Class, Map<symbol, InjectableCandidate>>(); #resolveQualifier<T>(type: Class<T>, resolution?: ResolutionType): symbol | undefined { const qualifiers = this.#byCandidateType.get(type) ?? new Map<symbol, InjectableCandidate>(); const resolved = [...qualifiers.keys()]; // If primary found if (qualifiers.has(PrimaryCandidateSymbol)) { return PrimaryCandidateSymbol; } else { const filtered = resolved .filter(qualifier => !!qualifier) .filter(qualifier => this.#defaultSymbols.has(qualifier)); // If there is only one default symbol if (filtered.length === 1) { return filtered[0]; } else if (filtered.length > 1) { // If dealing with sub types, prioritize exact matches const exact = this.getCandidateEntries(type) .map(([_, candidate]) => candidate) .filter(candidate => candidate.candidateType === type); if (exact.length === 1) { return exact[0].qualifier; } else { if (resolution === 'any') { return filtered[0]; } else { throw new InjectionError('Dependency has multiple candidates', type, filtered); } } } } } /** * Register a class with the dependency resolver */ registerClass(config: InjectableCandidate, baseParent?: Class): void { const candidateType = config.candidateType; const target = config.target ?? candidateType; const isSelfTarget = target === candidateType; const qualifier = config.qualifier ?? getDefaultQualifier(candidateType); // Record qualifier if its the default for the class if (config.qualifier === getDefaultQualifier(candidateType)) { this.#defaultSymbols.add(config.qualifier); } // Register inbound config by method and class setInMap(this.#byContainerType, config.class, config.method, config); setInMap(this.#byCandidateType, target, qualifier, config); setInMap(this.#byCandidateType, candidateType, qualifier, config); // Track interface aliases as targets const interfaces = SchemaRegistryIndex.has(candidateType) ? SchemaRegistryIndex.get(candidateType).get().interfaces : []; for (const type of interfaces) { setInMap(this.#byCandidateType, type, qualifier, config); } // If targeting self (default @Injectable behavior) if (isSelfTarget && baseParent) { setInMap(this.#byCandidateType, baseParent, qualifier, config); } // Registry primary candidates if (config.primary) { if (baseParent) { setInMap(this.#byCandidateType, baseParent, PrimaryCandidateSymbol, config); } // Register primary for self setInMap(this.#byCandidateType, target, PrimaryCandidateSymbol, config); // Register primary if only one interface provided and no parent config if (interfaces.length === 1 && (!baseParent || !this.#byContainerType.has(baseParent))) { const [primaryInterface] = interfaces; setInMap(this.#byCandidateType, primaryInterface, PrimaryCandidateSymbol, config); } else if (isSelfTarget) { // Register primary for all interfaces if self targeting for (const type of interfaces) { setInMap(this.#byCandidateType, type, PrimaryCandidateSymbol, config); } } } } /** * Resolve the target given a qualifier * @param candidateType * @param qualifier */ resolveCandidate<T>(candidateType: Class<T>, qualifier?: symbol, resolution?: ResolutionType): Resolved<T> { const qualifiers = this.#byCandidateType.get(candidateType) ?? new Map<symbol, InjectableCandidate>(); let config: InjectableCandidate; if (qualifier && qualifiers.has(qualifier)) { config = qualifiers.get(qualifier)!; } else { qualifier ??= this.#resolveQualifier(candidateType, resolution); if (!qualifier) { throw new InjectionError('Dependency not found', candidateType); } 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: candidateType.Ⲑid }); return this.resolveCandidate(candidateType); } throw new InjectionError('Dependency not found', candidateType, [qualifier]); } else { config = qualifiers.get(qualifier!)!; } } return { candidate: castTo(config), qualifier, target: (config.target ?? config.candidateType) }; } removeClass(cls: Class, qualifier: symbol): void { this.#defaultSymbols.delete(qualifier); this.#byCandidateType.get(cls)!.delete(qualifier); this.#byContainerType.get(cls)!.delete(qualifier); } getCandidateEntries(candidateType: Class): [symbol, InjectableCandidate][] { return [...this.#byCandidateType.get(candidateType)?.entries() ?? []]; } getContainerEntries(containerType: Class): [symbol, InjectableCandidate][] { return [...this.#byContainerType.get(containerType)?.entries() ?? []]; } }