diff --git a/src/di/DependencyProvider.ts b/src/di/DependencyProvider.ts new file mode 100644 index 0000000..176eb3f --- /dev/null +++ b/src/di/DependencyProvider.ts @@ -0,0 +1,84 @@ +import type IDependencyProvider from './IDependencyProvider'; +import type {Milliseconds} from '../types/Units'; +import type IDependencyManager from './IDependencyManager'; +import {Lifecycle} from './Lifecycle'; +import Type from './Type'; + +interface IStartable { + start(): Promise | void; +} + +const SERVICE_START_TIMEOUT: Milliseconds = 30_000; // 30 seconds + +function isStartable(obj: unknown): obj is IStartable { + return obj !== null && typeof obj === 'object' && 'start' in obj && typeof (obj as IStartable).start === 'function'; +} + +export default class DependencyProvider implements IDependencyProvider { + private readonly _dependencyManager: IDependencyManager; + private readonly _type: Type; + private readonly _instanceType: Type; + private readonly _lifecycle: Lifecycle; + private _instancePromise?: Promise; + + constructor(dependencyManager: IDependencyManager, type: Type, instanceType: Type, lifecycle: Lifecycle) { + this._dependencyManager = dependencyManager; + this._type = type; + this._instanceType = instanceType; + this._lifecycle = lifecycle; + } + + public canProvide(type: Type): boolean { + if (!(type instanceof Type)) { + throw new Error('Must provide a valid type'); + } + + return type.equals(this._type) && this._type.equals(type); + } + + public async provide(type: Type): Promise { + if (!this.canProvide(type)) { + throw new Error(`Provider for ${this._type} cannot provide ${type}`); + } + + if (this._instancePromise) { + return this._instancePromise; + } + + const instancePromise = this._createInstance(); + + if (this._lifecycle === Lifecycle.Singleton || this._lifecycle === Lifecycle.Service) { + this._instancePromise = instancePromise; + } + + return instancePromise; + } + + private async _createInstance(): Promise { + const instance = await this._dependencyManager.instantiateType(this._instanceType); + + if (this._lifecycle === Lifecycle.Service) { + if (!isStartable(instance)) { + throw new Error(`Instance for [${this._type.toString()}] is missing start() method`); + } + + await this._startWithTimeout(instance, SERVICE_START_TIMEOUT); + } + + return instance; + } + + private async _startWithTimeout(instance: IStartable, timeout: number): Promise { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`Timed out starting [${this._instanceType}]`)); + }, timeout); + }); + + await Promise.race([instance.start(), timeoutPromise]); + } + + public toString(): string { + return `DependencyProvider[${this._type}]`; + } +} diff --git a/src/di/Lifecycle.ts b/src/di/Lifecycle.ts new file mode 100644 index 0000000..d2155d5 --- /dev/null +++ b/src/di/Lifecycle.ts @@ -0,0 +1,37 @@ +import assertUnreachable from '../lang/assertUnreachable'; + +export enum Lifecycle { + Singleton = 0, + Service = 1, + Instance = 2 +} + +export type LifecycleType = 'signleton' | 'service' | 'instance'; + +export class LifecycleMapping { + public static convertLifecycleToLifecycleType(lifecycle: Lifecycle): LifecycleType { + switch (lifecycle) { + case Lifecycle.Singleton: + return 'signleton'; + case Lifecycle.Service: + return 'service'; + case Lifecycle.Instance: + return 'instance'; + default: + return assertUnreachable(lifecycle); + } + } + + public static convertLifecycleTypeToLifecycle(lifecycleType: LifecycleType): Lifecycle { + switch (lifecycleType) { + case 'signleton': + return Lifecycle.Singleton; + case 'service': + return Lifecycle.Service; + case 'instance': + return Lifecycle.Instance; + default: + return assertUnreachable(lifecycleType); + } + } +} diff --git a/src/di/NamedType.ts b/src/di/NamedType.ts index 6379125..dfe4cb9 100644 --- a/src/di/NamedType.ts +++ b/src/di/NamedType.ts @@ -12,8 +12,12 @@ export default class NamedType extends Type { return this._name; } - public override equals(other: Type) { - return super.equals(other) && other instanceof Type && this.getName() === (other as NamedType).getName(); + public override equals(other: Type): boolean { + return ( + super.equals(other) && + other instanceof NamedType && + this.getName() === other.getName() + ); } public overridetoURN(): string { diff --git a/src/lang/assertUnreachable.ts b/src/lang/assertUnreachable.ts new file mode 100644 index 0000000..f3d23b4 --- /dev/null +++ b/src/lang/assertUnreachable.ts @@ -0,0 +1,3 @@ +export default function assertUnreachable(x: never): never { + throw new Error(`Unreachable code: [${x}]`); +} diff --git a/src/types/Units.ts b/src/types/Units.ts new file mode 100644 index 0000000..94ccbf5 --- /dev/null +++ b/src/types/Units.ts @@ -0,0 +1,12 @@ +export type Seconds = number; +export type Milliseconds = number; +export type Microseconds = number; +export type Nanoseconds = number; + +export type Bytes = number; +export type Kilobytes = number; +export type Megabytes = number; +export type Gigabytes = number; +export type Terabytes = number; +export type Petabytes = number; +export type Exabytes = number;