@travetto/di
Version:
Dependency registration/management and injection support.
170 lines (141 loc) • 6.06 kB
text/typescript
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() ?? []];
}
}