LMS-BGN/src/core/di/DIContainer.ts

211 lines
5.5 KiB
TypeScript

import { IService } from '../interfaces';
/**
* Service factory function
*/
export type ServiceFactory<T = any> = () => T | Promise<T>;
/**
* Service registration options
*/
export interface ServiceRegistration<T = any> {
factory: ServiceFactory<T>;
singleton?: boolean;
dependencies?: string[];
}
/**
* Dependency injection container
*/
export class DIContainer {
private services = new Map<string, ServiceRegistration>();
private singletons = new Map<string, any>();
private resolving = new Set<string>();
// Track pending resolutions to allow concurrent callers to await the same promise
private pendingResolutions = new Map<string, Promise<any>>();
/**
* Register a service with the container
*/
register<T>(token: string, factory: ServiceFactory<T>, options?: Omit<ServiceRegistration<T>, 'factory'>): void {
this.services.set(token, {
factory,
singleton: options?.singleton ?? false,
dependencies: options?.dependencies ?? []
});
}
/**
* Register a singleton service
*/
singleton<T>(token: string, factory: ServiceFactory<T>, dependencies?: string[]): void {
this.register(token, factory, { singleton: true, dependencies });
}
/**
* Register a transient service
*/
transient<T>(token: string, factory: ServiceFactory<T>, dependencies?: string[]): void {
this.register(token, factory, { singleton: false, dependencies });
}
/**
* Register an existing instance as a singleton
*/
registerInstance<T>(token: string, instance: T): void {
this.singletons.set(token, instance);
// Also register a factory that returns the instance
this.register(token, () => instance, { singleton: true });
}
/**
* Resolve a service from the container
*/
async resolve<T>(token: string): Promise<T> {
// Return singleton if already created (prefer fast path)
if (this.singletons.has(token)) {
return this.singletons.get(token);
}
// If a resolution is already in progress for this token, share the promise
if (this.pendingResolutions.has(token)) {
return await this.pendingResolutions.get(token) as T;
}
// Check for circular dependencies (true recursion within a single resolution chain)
if (this.resolving.has(token)) {
throw new Error(`Circular dependency detected: ${token}`);
}
const registration = this.services.get(token);
if (!registration) {
throw new Error(`Service not found: ${token}`);
}
this.resolving.add(token);
// Create a shared promise for this resolution so concurrent callers can await it
const resolutionPromise = (async () => {
try {
// Resolve dependencies first
const dependencies: any[] = [];
if (registration.dependencies) {
for (const dep of registration.dependencies) {
dependencies.push(await this.resolve(dep));
}
}
// Create service instance
const instance = await registration.factory();
// Initialize service if it implements IService
if (this.isService(instance)) {
await instance.initialize?.();
}
// Store singleton
if (registration.singleton) {
this.singletons.set(token, instance);
}
return instance as T;
} finally {
this.resolving.delete(token);
this.pendingResolutions.delete(token);
}
})();
this.pendingResolutions.set(token, resolutionPromise);
return await resolutionPromise;
}
/**
* Resolve service synchronously (for already initialized singletons)
*/
resolveSync<T>(token: string): T {
if (this.singletons.has(token)) {
return this.singletons.get(token);
}
throw new Error(`Service not available synchronously: ${token}`);
}
/**
* Check if service is registered
*/
has(token: string): boolean {
return this.services.has(token);
}
/**
* Get all registered service tokens
*/
getRegisteredServices(): string[] {
return Array.from(this.services.keys());
}
/**
* Clear all services and singletons
*/
async clear(): Promise<void> {
// Destroy all singleton services
const entries = Array.from(this.singletons.entries());
for (const [token, instance] of entries) {
if (this.isService(instance)) {
await instance.destroy?.();
}
}
this.services.clear();
this.singletons.clear();
this.resolving.clear();
}
/**
* Remove a specific service
*/
async remove(token: string): Promise<void> {
// Destroy singleton if exists
if (this.singletons.has(token)) {
const instance = this.singletons.get(token);
if (this.isService(instance)) {
await instance.destroy?.();
}
this.singletons.delete(token);
}
this.services.delete(token);
}
/**
* Health check for all services
*/
async healthCheck(): Promise<Record<string, boolean>> {
const health: Record<string, boolean> = {};
const entries = Array.from(this.singletons.entries());
for (const [token, instance] of entries) {
if (this.isService(instance)) {
try {
health[token] = await instance.isHealthy?.() ?? true;
} catch (error) {
health[token] = false;
}
} else {
health[token] = true;
}
}
return health;
}
/**
* Check if object implements IService interface
*/
private isService(obj: any): obj is IService {
return obj && typeof obj === 'object' && 'id' in obj && 'name' in obj;
}
}
// Global container instance
export const container = new DIContainer();