import { given } from "@nivinjoseph/n-defensive"; import { InvalidOperationException, ObjectDisposedException } from "@nivinjoseph/n-exception"; import { BaseScope } from "./base-scope.js"; import { ChildScope } from "./child-scope.js"; import { ComponentInstaller } from "./component-installer.js"; import { ComponentRegistry } from "./component-registry.js"; import { Lifestyle } from "./lifestyle.js"; import { Registry } from "./registry.js"; import { ReservedKeys } from "./reserved-keys.js"; import { Scope } from "./scope.js"; import { ScopeType } from "./scope-type.js"; /** * The main IoC container class that manages component registration and scope creation. * This is the entry point for using the dependency injection framework. */ export class Container extends BaseScope implements Registry { private readonly _installers = new Array(); private _myDisposePromise: Promise | null = null; /** * Creates a new instance of the Container. */ public constructor() { super(ScopeType.Root, new ComponentRegistry(), null); } /** * Registers a component with transient lifecycle. * Creates a new instance each time the component is resolved. * @param key - The unique identifier for the component * @param component - The component class or function to register * @param aliases - Optional aliases for the component * @returns The container instance for method chaining */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type public registerTransient(key: string, component: Function, ...aliases: Array): Registry { this._register(key, component, Lifestyle.Transient, ...aliases); return this; } /** * Registers a component with scoped lifecycle. * Creates one instance per scope. * @param key - The unique identifier for the component * @param component - The component class or function to register * @param aliases - Optional aliases for the component * @returns The container instance for method chaining */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type public registerScoped(key: string, component: Function, ...aliases: Array): Registry { this._register(key, component, Lifestyle.Scoped, ...aliases); return this; } /** * Registers a component with singleton lifecycle. * Creates one instance for the entire container. * @param key - The unique identifier for the component * @param component - The component class or function to register * @param aliases - Optional aliases for the component * @returns The container instance for method chaining */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type public registerSingleton(key: string, component: Function, ...aliases: Array): Registry { this._register(key, component, Lifestyle.Singleton, ...aliases); return this; } /** * Registers a pre-created instance. * Uses the provided instance for all resolutions. * @param key - The unique identifier for the component * @param instance - The pre-created instance to register * @param aliases - Optional aliases for the component * @returns The container instance for method chaining */ public registerInstance(key: string, instance: object, ...aliases: Array): Registry { this._register(key, instance, Lifestyle.Instance, ...aliases); return this; } /** * Installs components using an installer. * @param componentInstaller - The installer to use * @returns The container instance for method chaining * @throws ObjectDisposedException if the container is disposed * @throws InvalidOperationException if the container is bootstrapped */ public install(componentInstaller: ComponentInstaller): Container { if (this.isDisposed) throw new ObjectDisposedException(this); if (this.isBootstrapped) throw new InvalidOperationException("install after bootstrap"); given(componentInstaller, "componentInstaller").ensureHasValue(); this._installers.push(componentInstaller); return this; } /** * Creates a new scope for dependency resolution. * @returns A new scope instance * @throws ObjectDisposedException if the container is disposed * @throws InvalidOperationException if the container is not bootstrapped */ public createScope(): Scope { if (this.isDisposed) throw new ObjectDisposedException(this); if (!this.isBootstrapped) throw new InvalidOperationException("createScope before bootstrap"); return new ChildScope(this.componentRegistry, this); } /** * Bootstraps the container. * Verifies all registrations and prepares the container for use. * @throws ObjectDisposedException if the container is disposed * @throws InvalidOperationException if the container is already bootstrapped */ public async bootstrap(): Promise { if (this.isDisposed) throw new ObjectDisposedException(this); if (this.isBootstrapped) throw new InvalidOperationException("bootstrap after bootstrap"); await this._installers.forEachAsync((t) => t.install(this), 1); this.componentRegistry.verifyRegistrations(); super.bootstrapInternal(); } /** * Disposes the container and its resources. * Cleans up all scopes and releases resources. * @returns A promise that resolves when disposal is complete */ public override dispose(): Promise { if (this._myDisposePromise == null) this._myDisposePromise = super.dispose().then(() => this.componentRegistry.dispose()); return this._myDisposePromise; } /** * Removes a component registration. * @param key - The component key to deregister * @throws ObjectDisposedException if the container is disposed * @throws InvalidOperationException if the container is bootstrapped */ public deregister(key: string): void { if (this.isDisposed) throw new ObjectDisposedException(this); if (this.isBootstrapped) throw new InvalidOperationException("deregister after bootstrap"); given(key, "key").ensureHasValue(); this.componentRegistry.deregister(key); } /** * Internal method to register a component with the specified lifecycle. * @param key - The unique identifier for the component * @param component - The component class, function, or instance to register * @param lifestyle - The lifecycle to use for the component * @param aliases - Optional aliases for the component * @throws ObjectDisposedException if the container is disposed * @throws InvalidOperationException if the container is bootstrapped */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type private _register(key: string, component: Function | object, lifestyle: Lifestyle, ...aliases: Array): void { if (this.isDisposed) throw new ObjectDisposedException(this); if (this.isBootstrapped) throw new InvalidOperationException("register after bootstrap"); const isReserved = (value: string): boolean => { const normalized = value.trim().toLowerCase(); return ReservedKeys.all.some(r => r.toLowerCase() === normalized); }; given(key, "key").ensureHasValue().ensureIsString() .ensure(t => t.trim().length > 0, "cannot be empty or whitespace") .ensure(t => !isReserved(t), "cannot use reserved key"); given(component, "component").ensureHasValue(); given(lifestyle, "lifestyle").ensureHasValue().ensureIsNumber(); given(aliases, "aliases").ensureHasValue().ensureIsArray() .ensure(t => t.every(u => typeof u === "string"), "alias must be a string") .ensure(t => t.every(u => u.trim().length > 0), "alias cannot be empty or whitespace") .ensure(t => t.every(u => u.trim() !== key.trim()), "alias cannot be the same as key") .ensure(t => t.every(u => !isReserved(u)), "alias cannot use reserved key") .ensure(t => t.length === t.map(u => u.trim()).distinct().length, "duplicates detected"); this.componentRegistry.register(key, component, lifestyle, ...aliases); } }