Add Dependency Injection Framework with IDependencyManager, IDependencyProvider, Type, and NamedType Classes

This commit is contained in:
2025-10-25 17:14:24 -04:00
parent 2c4a886e27
commit 17e7f8babd
5 changed files with 280 additions and 0 deletions

206
src/di/DependencyManager.ts Normal file
View File

@@ -0,0 +1,206 @@
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<T = unknown> = new (...args: unknown[]) => T;
type ModuleLoader = (modulePath: string) => Promise<Constructor | { default: Constructor }>;
export default class DependencyManager implements IDependencyManager {
private readonly _moduleLoader: ModuleLoader;
private readonly _types: Type[];
private readonly _eagerTypes: Type[];
private readonly _providers: IDependencyProvider[];
private readonly _injections: Map<string, Type[]>;
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<IDependencyProvider> {
return this.resolve(type);
}
public async instantiateType(type: Type): Promise<unknown> {
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<Type[]> {
return Promise.resolve([...this._eagerTypes]);
}
public async getTypes(): Promise<Type[]> {
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 resolve(type: Type): IDependencyProvider {
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;
}
private injectDependencies(Constructor: Constructor, dependencies: unknown[]): unknown {
if (dependencies.length === 0) {
return new Constructor();
}
return new Constructor(...dependencies);
}
}