211 lines
5.5 KiB
TypeScript
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(); |