From 17e7f8babd3239804936d9700403cb52f0db71a9 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sat, 25 Oct 2025 17:14:24 -0400 Subject: [PATCH] Add Dependency Injection Framework with IDependencyManager, IDependencyProvider, Type, and NamedType Classes --- src/di/DependencyManager.ts | 206 ++++++++++++++++++++++++++++++++++ src/di/IDependencyManager.ts | 14 +++ src/di/IDependencyProvider.ts | 7 ++ src/di/NamedType.ts | 30 +++++ src/di/Type.ts | 23 ++++ 5 files changed, 280 insertions(+) create mode 100644 src/di/DependencyManager.ts create mode 100644 src/di/IDependencyManager.ts create mode 100644 src/di/IDependencyProvider.ts create mode 100644 src/di/NamedType.ts create mode 100644 src/di/Type.ts diff --git a/src/di/DependencyManager.ts b/src/di/DependencyManager.ts new file mode 100644 index 0000000..7e3e744 --- /dev/null +++ b/src/di/DependencyManager.ts @@ -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 = new (...args: unknown[]) => T; +type ModuleLoader = (modulePath: string) => Promise; + +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; + 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 { + return this.resolve(type); + } + + public async instantiateType(type: Type): Promise { + 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 { + return Promise.resolve([...this._eagerTypes]); + } + + public async getTypes(): Promise { + 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); + } +} diff --git a/src/di/IDependencyManager.ts b/src/di/IDependencyManager.ts new file mode 100644 index 0000000..0229d9a --- /dev/null +++ b/src/di/IDependencyManager.ts @@ -0,0 +1,14 @@ +import type Type from "./Type"; +import type IDisposable from "../lang/IDisposable"; +import type IDependencyProvider from "./IDependencyProvider"; + +export default interface IDependencyManager { + defineDependencies(type: Type, dependencies: Type[]): IDisposable; + getDependencies(type: Type): Type[] | undefined; + addProvider(provider: IDependencyProvider): IDisposable; + resolveProvider(type: Type): Promise; + instantiateType(type: Type): Promise; + addEagerType(type: Type): void; + getEagerTypes(): Promise; + getTypes(): Promise; +} diff --git a/src/di/IDependencyProvider.ts b/src/di/IDependencyProvider.ts new file mode 100644 index 0000000..ed54328 --- /dev/null +++ b/src/di/IDependencyProvider.ts @@ -0,0 +1,7 @@ +import Type from "./Type"; + +export default interface IDependencyProvider { + canProvide(type: Type): boolean; + provide(type: Type): Promise; + toString(): string; +} diff --git a/src/di/NamedType.ts b/src/di/NamedType.ts new file mode 100644 index 0000000..06e686e --- /dev/null +++ b/src/di/NamedType.ts @@ -0,0 +1,30 @@ +import Type from "./Type"; + +export default class NamedType extends Type { + private readonly _name: string; + + constructor(name: string, type: string) { + super(type); + this._name = name; + } + + public getName(): string { + return this._name; + } + + public override equals(other: Type) { + return ( + super.equals(other) && + other instanceof Type && + this.getName() === (other as NamedType).getName() + ); + } + + public overridetoURN(): string { + return `urn:namedtype:${super.getType()}#${this._name}`; + } + + public overridetoString(): string { + return `${super.getType()}#${this._name}`; + } +} diff --git a/src/di/Type.ts b/src/di/Type.ts new file mode 100644 index 0000000..571553b --- /dev/null +++ b/src/di/Type.ts @@ -0,0 +1,23 @@ +export default class Type { + private _type: string; + + constructor(type: string) { + this._type = type; + } + + public getType(): string { + return this._type; + } + + public equals(other: Type): boolean { + return other instanceof Type && this.getType() === other.getType(); + } + + public toURN() { + return `urn:type:${this._type}`; + } + + public toString() { + return `Type|${this._type}`; + } +}