Add DependencyProvider and Lifecycle classes, along with utility types for time and data units. Implemented assertUnreachable for exhaustive checks in Lifecycle mapping.

This commit is contained in:
2025-10-25 17:30:25 -04:00
parent b209d710b8
commit 975a543027
5 changed files with 142 additions and 2 deletions

View File

@@ -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> | 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<unknown>;
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<unknown> {
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<unknown> {
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<void> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Timed out starting [${this._instanceType}]`));
}, timeout);
});
await Promise.race([instance.start(), timeoutPromise]);
}
public toString(): string {
return `DependencyProvider[${this._type}]`;
}
}

37
src/di/Lifecycle.ts Normal file
View File

@@ -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);
}
}
}

View File

@@ -12,8 +12,12 @@ export default class NamedType extends Type {
return this._name; return this._name;
} }
public override equals(other: Type) { public override equals(other: Type): boolean {
return super.equals(other) && other instanceof Type && this.getName() === (other as NamedType).getName(); return (
super.equals(other) &&
other instanceof NamedType &&
this.getName() === other.getName()
);
} }
public overridetoURN(): string { public overridetoURN(): string {

View File

@@ -0,0 +1,3 @@
export default function assertUnreachable(x: never): never {
throw new Error(`Unreachable code: [${x}]`);
}

12
src/types/Units.ts Normal file
View File

@@ -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;