import type {IDependencyProvider} from './IDependencyProvider'; import type IDisposable from '../lang/IDisposable'; import Disposable from '../lang/Disposable'; import {Type} from './Type'; import type {IDependencyManager} from './IDependencyManager'; type Constructor = new (...args: unknown[]) => T; type ModuleLoader = (modulePath: string) => Promise; export class DependencyManager implements IDependencyManager { private readonly _moduleLoader: ModuleLoader; private readonly _types: Type[]; private readonly _eagerTypes: Type[]; private readonly _providers: IDependencyProvider[]; private readonly _injections: Map; private readonly _pending: Type[]; constructor(moduleLoader: ModuleLoader) { if (typeof moduleLoader !== 'function') { throw new Error('Module loader must be a function'); } this._moduleLoader = moduleLoader; this._types = []; this._eagerTypes = []; this._providers = []; this._injections = new Map(); this._pending = []; } public defineDependencies(type: Type, dependencies: Type[]): IDisposable { if (!(type instanceof Type)) { throw new Error('Type must be a Type'); } if (!Array.isArray(dependencies)) { throw new Error('Dependencies must be an array'); } for (const dependency of dependencies) { if (!(dependency instanceof Type)) { throw new Error('Dependency must be a Type'); } } const key = type.toURN(); if (this._injections.has(key)) { throw new Error(`Dependencies already defined for type [${type.toString()}]`); } this._injections.set(key, dependencies); this._types.push(type); console.debug(`Dependencies for [${type}] defined as [${dependencies}]`); return new Disposable(() => this._injections.delete(key)); } public getDependencies(type: Type): Type[] | undefined { if (!(type instanceof Type)) { throw new Error('Type must be an instance of Type'); } return this._injections.get(type.toURN()); } public addProvider(provider: IDependencyProvider): IDisposable { if (!this.isProvider(provider)) { throw new Error('Provider must implement IDependencyProvider'); } this._providers.push(provider); return new Disposable(() => { const index = this._providers.indexOf(provider); if (index !== -1) { this._providers.splice(index, 1); } }); } public async resolveProvider(type: Type): Promise { if (!(type instanceof Type)) { throw new Error('Type must be an instance of Type'); } const candidates = this._providers.filter(provider => provider.canProvide(type)); if (candidates.length === 0) { throw new Error(`No provider for [${type.toString()}]`); } if (candidates.length > 1) { const candidateNames = candidates.map(c => c.toString()).join(', '); throw new Error(`Multiple providers for ${type}: ${candidateNames}`); } return candidates[0] as IDependencyProvider; } public async instantiateType(type: Type): Promise { if (!(type instanceof Type)) { throw new Error('Type must be an instance of Type'); } const key = type.toURN(); // Check for circular dependencies if (this._pending.includes(type)) { const pendingChain = this._pending.map(t => t.toString()).join(' -> '); throw new Error(`Failed to resolve ${type} due to circular dependency: ${pendingChain}`); } this._pending.push(type); try { // Get dependencies for this type const dependencies = this._injections.get(key) || []; // Resolve all dependencies const resolvedDependencies = await Promise.all( dependencies.map(async dependency => { const provider = await this.resolveProvider(dependency); let instance = await provider.provide(dependency); // Handle nested providers if (this.isProvider(instance)) { instance = await (instance as IDependencyProvider).provide(dependency); } return instance; }) ); // Load the module const module = await this._moduleLoader(type.getType()); const Constructor = 'default' in module ? module.default : module; if (!Constructor) { throw new Error(`Could not load type "${type.getType()}"`); } // Instantiate with resolved dependencies const instance = this.injectDependencies(Constructor, resolvedDependencies); // Handle if the instance is itself a provider if (this.isProvider(instance)) { return await (instance as IDependencyProvider).provide(type); } return instance; } finally { // Remove from pending stack const index = this._pending.indexOf(type); if (index !== -1) { this._pending.splice(index, 1); } } } public addEagerType(type: Type): void { if (!(type instanceof Type)) { throw new Error('Type must be an instance of Type'); } this._eagerTypes.push(type); } public async getEagerTypes(): Promise { return Promise.resolve([...this._eagerTypes]); } public async getTypes(): Promise { return Promise.resolve([...this._types]); } private isProvider(obj: unknown): obj is IDependencyProvider { return ( obj !== null && typeof obj === 'object' && 'canProvide' in obj && 'provide' in obj && typeof (obj as IDependencyProvider).canProvide === 'function' && typeof (obj as IDependencyProvider).provide === 'function' ); } private injectDependencies(Constructor: Constructor, dependencies: unknown[]): unknown { if (dependencies.length === 0) { return new Constructor(); } return new Constructor(...dependencies); } }