export enum ServiceLifetime { Singleton = 'singleton', Transient = 'transient', Scoped = 'scoped' } export interface ServiceDescriptor { name: string factory: () => T | Promise lifetime: ServiceLifetime dependencies?: string[] onDispose?: (instance: T) => void | Promise } export type ServiceFactory = () => T | Promise export class ServiceContainer { private descriptors = new Map() private singletonInstances = new Map() private scopedInstances = new Map>() private resolutionStack: string[] = [] private disposed = false register(name: string, factory: ServiceFactory): void register(descriptor: ServiceDescriptor): void register(nameOrDescriptor: string | ServiceDescriptor, factory?: ServiceFactory): void { this.ensureNotDisposed() if (typeof nameOrDescriptor === 'string') { const descriptor: ServiceDescriptor = { name: nameOrDescriptor, factory: factory!, lifetime: ServiceLifetime.Singleton } this.descriptors.set(nameOrDescriptor, descriptor) } else { this.descriptors.set(nameOrDescriptor.name, nameOrDescriptor) } } async get(name: string): Promise { this.ensureNotDisposed() return this.resolveInternal(name, null) } getSync(name: string): T { this.ensureNotDisposed() const descriptor = this.descriptors.get(name) if (!descriptor) { throw new Error(`Service '${name}' not registered`) } if (descriptor.lifetime === ServiceLifetime.Singleton) { if (this.singletonInstances.has(name)) { return this.singletonInstances.get(name) as T } } const result = descriptor.factory() if (result instanceof Promise) { throw new Error( `Service '${name}' has an async factory but getSync() was called. Use get() instead.` ) } if (descriptor.lifetime === ServiceLifetime.Singleton) { this.singletonInstances.set(name, result) } return result as T } createScope(scopeId: string): ServiceScope { this.ensureNotDisposed() return new ServiceScope(this, scopeId) } has(name: string): boolean { return this.descriptors.has(name) } async dispose(): Promise { if (this.disposed) { return } for (const [name, instance] of this.singletonInstances) { const descriptor = this.descriptors.get(name) if (descriptor?.onDispose) { try { await descriptor.onDispose(instance) } catch (error) { console.error(`Error disposing service '${name}':`, error) } } } for (const [, scopeMap] of this.scopedInstances) { for (const [name, instance] of scopeMap) { const descriptor = this.descriptors.get(name) if (descriptor?.onDispose) { try { await descriptor.onDispose(instance) } catch (error) { console.error(`Error disposing scoped service '${name}':`, error) } } } } this.singletonInstances.clear() this.scopedInstances.clear() this.descriptors.clear() this.resolutionStack = [] this.disposed = true } clear(): void { this.singletonInstances.clear() this.scopedInstances.clear() this.descriptors.clear() this.resolutionStack = [] } isDisposed(): boolean { return this.disposed } private async resolveInternal(name: string, scopeId: string | null): Promise { if (this.resolutionStack.includes(name)) { const cycle = [...this.resolutionStack, name].join(' -> ') throw new Error(`Circular dependency detected: ${cycle}`) } const descriptor = this.descriptors.get(name) if (!descriptor) { throw new Error(`Service '${name}' not registered`) } if (descriptor.lifetime === ServiceLifetime.Singleton) { if (this.singletonInstances.has(name)) { return this.singletonInstances.get(name) as T } this.resolutionStack.push(name) try { const instance = await descriptor.factory() this.singletonInstances.set(name, instance) return instance as T } finally { this.resolutionStack.pop() } } if (descriptor.lifetime === ServiceLifetime.Scoped) { if (!scopeId) { throw new Error( `Scoped service '${name}' cannot be resolved outside of a scope. Use createScope() first.` ) } let scopeMap = this.scopedInstances.get(scopeId) if (!scopeMap) { scopeMap = new Map() this.scopedInstances.set(scopeId, scopeMap) } if (scopeMap.has(name)) { return scopeMap.get(name) as T } this.resolutionStack.push(name) try { const instance = await descriptor.factory() scopeMap.set(name, instance) return instance as T } finally { this.resolutionStack.pop() } } this.resolutionStack.push(name) try { const instance = await descriptor.factory() return instance as T } finally { this.resolutionStack.pop() } } private ensureNotDisposed(): void { if (this.disposed) { throw new Error('ServiceContainer has been disposed') } } } export class ServiceScope { private container: ServiceContainer private scopeId: string private disposed = false constructor(container: ServiceContainer, scopeId: string) { this.container = container this.scopeId = scopeId } async get(name: string): Promise { if (this.disposed) { throw new Error('ServiceScope has been disposed') } return this.container['resolveInternal'](name, this.scopeId) } async dispose(): Promise { if (this.disposed) { return } const scopeMap = this.container['scopedInstances'].get(this.scopeId) if (scopeMap) { for (const [name, instance] of scopeMap) { const descriptor = this.container['descriptors'].get(name) if (descriptor?.onDispose) { try { await descriptor.onDispose(instance) } catch (error) { console.error(`Error disposing scoped service '${name}':`, error) } } } this.container['scopedInstances'].delete(this.scopeId) } this.disposed = true } isDisposed(): boolean { return this.disposed } }