UNPKG

@miracledevs/paradigm-web-di

Version:

A minimal dependency injection library for the web

151 lines (120 loc) 5.55 kB
import { ObjectType, getObjectTypeName } from "./object-type"; import { DependencyLifeTime } from "./dependency-life-time"; import { DependencyDescriptor } from "./dependency-descriptor"; import { DependencyContainer } from "./dependency-container"; export class DependencyCollection { private static _globalCollection: DependencyCollection; public static get globalCollection(): DependencyCollection { if (!this._globalCollection) this._globalCollection = new DependencyCollection(); return this._globalCollection; } private readonly _registeredTypes: Map<ObjectType, DependencyDescriptor>; constructor() { this._registeredTypes = new Map<ObjectType, DependencyDescriptor>(); this.register(DependencyContainer as any, DependencyLifeTime.Transient); } register<T = any>(objectType: ObjectType<T>, lifeTime: DependencyLifeTime, dependencies?: ObjectType[], instance?: T): void { if (this.contains(objectType)) throw new Error(`The type ${getObjectTypeName(objectType)} is already registered.`); if (instance && lifeTime !== DependencyLifeTime.Singleton) throw new Error(`Only the singletons can be registered with an existing instance.`); this._registeredTypes.set(objectType, new DependencyDescriptor(lifeTime || DependencyLifeTime.Transient, dependencies || [], instance || null)); } registerTransient<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], ): void { this.register(objectType, DependencyLifeTime.Transient, dependencies); } registerScoped<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], ): void { this.register(objectType, DependencyLifeTime.Scoped, dependencies); } registerSingleton<T = any>(objectType: ObjectType<T>, dependencies?: ObjectType[], instance?: T): void { this.register(objectType, DependencyLifeTime.Singleton, dependencies, instance); } get(objectType: ObjectType): DependencyDescriptor { if (!this._registeredTypes.has(objectType)) throw new Error(`The type ${getObjectTypeName(objectType)} is not registered.`); return this._registeredTypes.get(objectType) as DependencyDescriptor; } contains(objectType: ObjectType): boolean { return this._registeredTypes.has(objectType); } buildContainer(validate: boolean = false): DependencyContainer { if (validate) { let errors = ''; for (const registeredType of this._registeredTypes.keys()) { try { this.validateCircularDependency(registeredType); this.validateDependencyRegistration(registeredType); this.validateScopedOnSingletons(registeredType); } catch (e) { errors += ` - ${(e instanceof Error? e.message : e)}\n`; } } if (errors.length > 0) { throw new Error("Errors found on the dependency configuration:\n" + errors); } } return DependencyContainer.createFromCollection(this, "root"); } private validateDependencyRegistration<T>(objectType: ObjectType<T>): void { const descriptor = this.get(objectType); for (const dependencyType of descriptor.dependencies) { if (!this.contains(dependencyType)) { throw new Error(`The type '${getObjectTypeName(objectType)}' depends on the type '${getObjectTypeName(dependencyType)}' but is not registered.`); } this.validateDependencyRegistration(dependencyType); } } private validateCircularDependency<T>(objectType: ObjectType<T>, hierarchy?: ObjectType[]): void { if (!hierarchy) hierarchy = []; const descriptor = this.get(objectType); const dependencies = descriptor.dependencies; for (const dependencyType of dependencies) { if (!this.contains(dependencyType)) continue; if (hierarchy.find(x => x === dependencyType)) { throw new Error(`Circular dependency found in service type '${getObjectTypeName(hierarchy[0])}': ${hierarchy.map(x => getObjectTypeName(x)).join(" -> ")} -> ${getObjectTypeName(dependencyType)}.`); } const newHierarchy = [...hierarchy]; newHierarchy.push(dependencyType); this.validateCircularDependency(dependencyType, newHierarchy); } } private validateScopedOnSingletons<T>(objectType: ObjectType<T>): void { const descriptor = this.get(objectType); for (const dependencyType of descriptor.dependencies) { const dependencyDescriptor = this.get(dependencyType); if (descriptor.lifeTime === DependencyLifeTime.Singleton && dependencyDescriptor.lifeTime === DependencyLifeTime.Scoped) { throw new Error(`Cannot consume scoped type '${getObjectTypeName(dependencyType)}' from singleton '${getObjectTypeName(objectType)}'.`); } this.validateScopedOnSingletons(dependencyType); } } }