From 00332dc6b1ddcbe46f536355d3091558c9b53d01 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sat, 25 Oct 2025 17:51:44 -0400 Subject: [PATCH] Add Dependency Injection example and core interfaces. Implement Inject class for managing scopes and instances, along with InstanceProvider, IntegerConstantProvider, and StringConstantProvider for dependency resolution. --- examples/di-example.ts | 3 ++ src/di/IConfigurationLoader.ts | 3 ++ src/di/IInject.ts | 10 ++++ src/di/Inject.ts | 83 +++++++++++++++++++++++++++++++ src/di/InstanceProvider.ts | 47 +++++++++++++++++ src/di/IntegerConstantProvider.ts | 33 ++++++++++++ src/di/StringConstantProvider.ts | 33 ++++++++++++ 7 files changed, 212 insertions(+) create mode 100644 examples/di-example.ts create mode 100644 src/di/IConfigurationLoader.ts create mode 100644 src/di/IInject.ts create mode 100644 src/di/Inject.ts create mode 100644 src/di/InstanceProvider.ts create mode 100644 src/di/IntegerConstantProvider.ts create mode 100644 src/di/StringConstantProvider.ts diff --git a/examples/di-example.ts b/examples/di-example.ts new file mode 100644 index 0000000..7580ce2 --- /dev/null +++ b/examples/di-example.ts @@ -0,0 +1,3 @@ +import {DependencyManager} from '../src'; + +const dependencyManager = new DependencyManager(async path => await import(path)); diff --git a/src/di/IConfigurationLoader.ts b/src/di/IConfigurationLoader.ts new file mode 100644 index 0000000..6fe315d --- /dev/null +++ b/src/di/IConfigurationLoader.ts @@ -0,0 +1,3 @@ +export interface IConfigurationLoader { + load(name: string): Promise; +} diff --git a/src/di/IInject.ts b/src/di/IInject.ts new file mode 100644 index 0000000..e69b103 --- /dev/null +++ b/src/di/IInject.ts @@ -0,0 +1,10 @@ +import type IDisposable from '../lang/IDisposable'; +import {Type} from './Type'; + +export interface IInject extends IDisposable { + scope(): Promise; + defineInstance(type: string, instance: unknown): void; + instantiate(type: string): Promise; + start(): Promise; + getAvailableTypes(): Promise; +} diff --git a/src/di/Inject.ts b/src/di/Inject.ts new file mode 100644 index 0000000..c775a63 --- /dev/null +++ b/src/di/Inject.ts @@ -0,0 +1,83 @@ +import type {IDependencyManager} from './IDependencyManager'; +import type {IInject} from './IInject'; +import DisposableList from '../lang/DisposableList'; +import {InstanceProvider} from './InstanceProvider'; +import {Type} from './Type'; + +export class Inject implements IInject { + private readonly _dependencyManager: IDependencyManager; + private readonly _scope: DisposableList; + private readonly _endedCallbacks: (() => void)[]; + private _activeChildScope: Inject | null = null; + + constructor(dependencyManager: IDependencyManager) { + this._dependencyManager = dependencyManager; + this._scope = new DisposableList(); + this._endedCallbacks = []; + } + + public async scope(): Promise { + // Only one scope can be active at any given time + if (!this._activeChildScope) { + const scope = new Inject(this._dependencyManager); + this._activeChildScope = scope; + + // Unregister scope when it ends + scope._endedCallbacks.push(() => { + if (this._activeChildScope === scope) { + this._activeChildScope = null; + } + }); + + return scope; + } + + // Wait for the current scope to end + return new Promise(resolve => { + this._activeChildScope!._endedCallbacks.push(() => { + resolve(this.scope()); + }); + }); + } + + defineInstance(type: string, instance: unknown): void { + const disposable = this._dependencyManager.addProvider(new InstanceProvider(new Type(type), instance)); + this._scope.add(disposable); + } + + async instantiate(type: string): Promise { + const theType = new Type(type); + const provider = await this._dependencyManager.resolveProvider(theType); + return (await provider.provide(theType)) as T; + } + + async start(): Promise { + const types = await this._dependencyManager.getEagerTypes(); + + const instances = await Promise.all( + types.map(async type => { + const provider = await this._dependencyManager.resolveProvider(type); + return provider.provide(type); + }) + ); + + return instances; + } + + async getAvailableTypes(): Promise { + return this._dependencyManager.getTypes(); + } + + dispose(): void { + this._scope.dispose(); + + // Notify all listeners + for (const callback of this._endedCallbacks) { + callback(); + } + } + + toString(): string { + return 'Inject'; + } +} diff --git a/src/di/InstanceProvider.ts b/src/di/InstanceProvider.ts new file mode 100644 index 0000000..6897baf --- /dev/null +++ b/src/di/InstanceProvider.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2025 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ + +import {Type} from './Type'; +import type {IDependencyProvider} from './IDependencyProvider'; + +/** + * A dependency provider for pre-existing instances. + */ +export class InstanceProvider implements IDependencyProvider { + private readonly _type: Type; + private readonly _instance: unknown; + + constructor(type: Type, instance: unknown) { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + if (instance === null || instance === undefined) { + throw new Error('Must provide a valid instance'); + } + + this._type = type; + this._instance = instance; + } + + canProvide(type: Type): boolean { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + return type.equals(this._type); + } + + async provide(type: Type): Promise { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + return this._instance; + } + + toString(): string { + return 'InstanceProvider'; + } +} diff --git a/src/di/IntegerConstantProvider.ts b/src/di/IntegerConstantProvider.ts new file mode 100644 index 0000000..98aabc8 --- /dev/null +++ b/src/di/IntegerConstantProvider.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ + +import type {IDependencyProvider} from './IDependencyProvider'; +import {Type} from './Type'; +import {NamedType} from './NamedType'; + +/** + * A dependency provider for integer constants. + * Uses NamedType where the name is parsed as an integer. + */ +export class IntegerConstantProvider implements IDependencyProvider { + public canProvide(type: Type): boolean { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + return type instanceof NamedType && type.getType() === 'di/IntegerConstantProvider'; + } + + public async provide(type: Type): Promise { + if (!(type instanceof NamedType)) { + throw new Error('Must provide a NamedType'); + } + + return parseInt(type.getName(), 10); + } + + public toString(): string { + return 'IntegerConstantProvider'; + } +} diff --git a/src/di/StringConstantProvider.ts b/src/di/StringConstantProvider.ts new file mode 100644 index 0000000..35b6d47 --- /dev/null +++ b/src/di/StringConstantProvider.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ + +import type {IDependencyProvider} from './IDependencyProvider'; +import {Type} from './Type'; +import {NamedType} from './NamedType'; + +/** + * A dependency provider for string constants. + * Uses NamedType where the name is the string value. + */ +export class StringConstantProvider implements IDependencyProvider { + public canProvide(type: Type): boolean { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + return type instanceof NamedType && type.getType() === 'di/StringConstantProvider'; + } + + public async provide(type: Type): Promise { + if (!(type instanceof NamedType)) { + throw new Error('Must provide a NamedType'); + } + + return type.getName(); + } + + public toString(): string { + return 'StringConstantProvider'; + } +}