Add Dependency Injection Framework with IDependencyManager, IDependencyProvider, Type, and NamedType Classes
This commit is contained in:
206
src/di/DependencyManager.ts
Normal file
206
src/di/DependencyManager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/di/IDependencyManager.ts
Normal file
14
src/di/IDependencyManager.ts
Normal file
@@ -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<IDependencyProvider>;
|
||||||
|
instantiateType(type: Type): Promise<unknown>;
|
||||||
|
addEagerType(type: Type): void;
|
||||||
|
getEagerTypes(): Promise<Type[]>;
|
||||||
|
getTypes(): Promise<Type[]>;
|
||||||
|
}
|
||||||
7
src/di/IDependencyProvider.ts
Normal file
7
src/di/IDependencyProvider.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Type from "./Type";
|
||||||
|
|
||||||
|
export default interface IDependencyProvider {
|
||||||
|
canProvide(type: Type): boolean;
|
||||||
|
provide(type: Type): Promise<unknown>;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
30
src/di/NamedType.ts
Normal file
30
src/di/NamedType.ts
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/di/Type.ts
Normal file
23
src/di/Type.ts
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user