Compare commits
7 Commits
b209d710b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3429df650f | |||
| 5d9b77ef7d | |||
| 1710043b74 | |||
| 00332dc6b1 | |||
| 0de4b9314a | |||
| e73c6e75e9 | |||
| 975a543027 |
0
examples/cluster.json
Normal file
0
examples/cluster.json
Normal file
32
examples/di-example.ts
Normal file
32
examples/di-example.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {ConfigurationReader, DependencyManager, JSONConfigurationLoader, Type} from '../src/di';
|
||||
import type {IMessageQueue} from '../src/example/IMessageQueue';
|
||||
|
||||
async function main() {
|
||||
const moduleLoader = async (path: string) => await import(`../${path}.ts`);
|
||||
const dependencyManager = new DependencyManager(moduleLoader);
|
||||
const configurationReader = new ConfigurationReader(dependencyManager, new JSONConfigurationLoader());
|
||||
|
||||
await configurationReader.load('./examples/di.json');
|
||||
|
||||
// Instantiate all eager types
|
||||
const eagerTypes = await dependencyManager.getEagerTypes();
|
||||
|
||||
for (const type of eagerTypes) {
|
||||
await dependencyManager.instantiateType(type);
|
||||
}
|
||||
|
||||
// Resolve the MessageQueue
|
||||
const messageQueueType = new Type('src/example/IMessageQueue');
|
||||
const messageQueue = await dependencyManager.instantiateType<IMessageQueue>(messageQueueType);
|
||||
|
||||
messageQueue.subscribe('test-topic', (message: string) => {
|
||||
console.log('[MessageQueue] [Listener] Received message:', message);
|
||||
});
|
||||
|
||||
// Use the service
|
||||
await messageQueue.publish('test-topic', 'Hello, DI!');
|
||||
|
||||
console.log('[MessageQueue] [Publisher] Message published successfully!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
61
examples/di.json
Normal file
61
examples/di.json
Normal file
@@ -0,0 +1,61 @@
|
||||
[
|
||||
{
|
||||
"name": "connection",
|
||||
"class": "src/env/ConfigurationProvider",
|
||||
"lifecycle": "singleton"
|
||||
},
|
||||
{
|
||||
"name": "mq.heartbeat.interval",
|
||||
"class": "src/env/ConfigurationProvider",
|
||||
"lifecycle": "singleton"
|
||||
},
|
||||
{
|
||||
"name": "mq.heartbeat.timeout",
|
||||
"class": "src/env/ConfigurationProvider",
|
||||
"lifecycle": "singleton"
|
||||
},
|
||||
{
|
||||
"name": "environment.version",
|
||||
"class": "src/env/ConfigurationProvider",
|
||||
"lifecycle": "singleton"
|
||||
},
|
||||
{
|
||||
"class": "src/example/Connection",
|
||||
"instanceType": "src/example/IConnection",
|
||||
"lifecycle": "singleton",
|
||||
"inject": [
|
||||
{
|
||||
"name": "connection",
|
||||
"class": "src/env/ConfigurationProvider"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"class": "src/example/MessageQueue",
|
||||
"instanceType": "src/example/IMessageQueue",
|
||||
"inject": ["src/example/IConnection"],
|
||||
"eager": true
|
||||
},
|
||||
{
|
||||
"class": "src/health/HealthCheck",
|
||||
"inject": [
|
||||
{
|
||||
"name": "environment.version",
|
||||
"class": "src/env/ConfigurationProvider"
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"class": "src/env/ConfigurationProvider"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"class": "src/health/HttpHealthCheck",
|
||||
"inject": [
|
||||
{
|
||||
"name": "environment.version",
|
||||
"class": "src/env/ConfigurationProvider"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "platform-ts",
|
||||
"module": "index.ts",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --max-warnings 0 ./src",
|
||||
"prelint:fix": "bun run format",
|
||||
"lint:fix": "eslint --fix ./src"
|
||||
"lint:fix": "eslint --fix ./src",
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "0.13.0",
|
||||
@@ -23,5 +25,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "2.30.1"
|
||||
}
|
||||
}
|
||||
|
||||
81
scripts/Runner.js
Normal file
81
scripts/Runner.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {spawn} from 'child_process';
|
||||
|
||||
const node = process.execPath;
|
||||
const nodeDir = path.dirname(node);
|
||||
let npm = path.join(nodeDir, 'npm.cmd');
|
||||
|
||||
if (!fs.existsSync(npm)) {
|
||||
const alternateNpm = path.join(nodeDir, 'npm');
|
||||
|
||||
if (fs.existsSync(alternateNpm)) {
|
||||
npm = alternateNpm;
|
||||
}
|
||||
}
|
||||
|
||||
class Runner {
|
||||
_node = process.execPath;
|
||||
_nodeDir = path.dirname(this._node);
|
||||
_npm;
|
||||
|
||||
constructor() {
|
||||
this._npm = path.join(this._nodeDir, 'npm.cmd');
|
||||
|
||||
if (!fs.existsSync(this._npm)) {
|
||||
const alternateNpm = path.join(this._nodeDir, 'npm');
|
||||
|
||||
if (fs.existsSync(alternateNpm)) {
|
||||
this._npm = alternateNpm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runCommands(commands, done) {
|
||||
let command = commands[0];
|
||||
|
||||
if (typeof command === 'string') {
|
||||
command = command.split(' ');
|
||||
}
|
||||
|
||||
if (command.length > 0) {
|
||||
if (command[0] === 'npm') {
|
||||
command[0] = this._npm;
|
||||
} else if (command[0] === 'node') {
|
||||
command[0] = this._node;
|
||||
}
|
||||
}
|
||||
|
||||
this.run(command, () => {
|
||||
if (commands.length > 1) {
|
||||
this.runCommands(commands.slice(1), done);
|
||||
} else {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(command, next) {
|
||||
const childProcess = spawn(command[0], command.slice(1), {stdio: 'inherit'});
|
||||
|
||||
childProcess.on('error', error => {
|
||||
console.error('Error [%o]', error);
|
||||
|
||||
process.exit(40);
|
||||
});
|
||||
|
||||
childProcess.on('close', code => {
|
||||
if (code !== 0) {
|
||||
console.error('Command [%o] exited with code [%d]', command, code);
|
||||
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Runner();
|
||||
5
scripts/npm-install.js
Normal file
5
scripts/npm-install.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import runner from './Runner.js';
|
||||
|
||||
runner.runCommands(['node --version', 'npm --version', 'npm install --no-save'], () => {
|
||||
process.exit(0);
|
||||
});
|
||||
3
scripts/version.js
Normal file
3
scripts/version.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import packageJSON from '../package.json' with {type: 'json'};
|
||||
|
||||
process.stdout.write(packageJSON.version);
|
||||
105
src/di/ConfigurationReader.ts
Normal file
105
src/di/ConfigurationReader.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type {IDependencyManager} from './IDependencyManager';
|
||||
import type {IConfigurationLoader} from './IConfigurationLoader';
|
||||
import {Type} from './Type';
|
||||
import {NamedType} from './NamedType';
|
||||
import type {LifecycleType} from './Lifecycle';
|
||||
import {DependencyProvider} from './DependencyProvider';
|
||||
import {LifecycleMapping} from './Lifecycle';
|
||||
import type {IDependencyDefinition, IServiceDefinition} from './IServiceDefinition';
|
||||
|
||||
export class ConfigurationReader {
|
||||
private readonly _dependencyManager: IDependencyManager;
|
||||
private readonly _configurationLoader: IConfigurationLoader;
|
||||
|
||||
constructor(dependencyManager: IDependencyManager, configurationLoader: IConfigurationLoader) {
|
||||
if (!dependencyManager) {
|
||||
throw new Error('Dependency manager is required');
|
||||
}
|
||||
|
||||
if (!configurationLoader) {
|
||||
throw new Error('Configuration loader is required');
|
||||
}
|
||||
|
||||
this._dependencyManager = dependencyManager;
|
||||
this._configurationLoader = configurationLoader;
|
||||
}
|
||||
|
||||
public async load(path: string): Promise<void> {
|
||||
const definitions = await this._configurationLoader.load(path);
|
||||
|
||||
if (!Array.isArray(definitions) || definitions.length === 0) {
|
||||
throw new Error('Definitions must be an array and not empty');
|
||||
}
|
||||
|
||||
for (const [idx, definition] of definitions.entries()) {
|
||||
if (definition.include && typeof definition.include === 'string') {
|
||||
const importDefinitions = await this._configurationLoader.load(definition.include);
|
||||
|
||||
if (!Array.isArray(importDefinitions)) {
|
||||
throw new Error('Import definitions must be an array');
|
||||
}
|
||||
|
||||
for (const importDefinition of importDefinitions) {
|
||||
await this.load(importDefinition);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!definition.class || typeof definition.class !== 'string') {
|
||||
throw new Error(`Definition must define a class (index: [${idx}], definition [${JSON.stringify(definition)}]`);
|
||||
}
|
||||
|
||||
const type = this.getType(definition);
|
||||
const dependencies: Type[] = [];
|
||||
|
||||
if (definition.inject) {
|
||||
if (!Array.isArray(definition.inject)) {
|
||||
throw new Error(`Injection must be an array (index: ${idx}, definition: ${JSON.stringify(definition)})`);
|
||||
}
|
||||
|
||||
for (const dependency of definition.inject) {
|
||||
let depType: Type;
|
||||
|
||||
if (typeof dependency === 'object') {
|
||||
depType = this.getType(dependency as IDependencyDefinition);
|
||||
} else {
|
||||
depType = new Type(dependency);
|
||||
}
|
||||
|
||||
dependencies.push(depType);
|
||||
}
|
||||
}
|
||||
|
||||
this._dependencyManager.defineDependencies(type, dependencies);
|
||||
|
||||
// Only create a provider if there's a separate instance type or special lifecycle
|
||||
if (definition.instanceType || definition.lifecycle === 'singleton' || definition.lifecycle === 'service') {
|
||||
const instanceType = definition.instanceType ? new Type(definition.instanceType) : type;
|
||||
const lifecycle: LifecycleType = definition.lifecycle || 'instance';
|
||||
const provider = new DependencyProvider(this._dependencyManager, type, instanceType, LifecycleMapping.convertLifecycleTypeToLifecycle(lifecycle));
|
||||
this._dependencyManager.addProvider(provider);
|
||||
}
|
||||
|
||||
if (definition.eager) {
|
||||
this._dependencyManager.addEagerType(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getType(definition: IDependencyDefinition): Type {
|
||||
if (!definition.class || typeof definition.class !== 'string') {
|
||||
throw new Error(`Definition must define a class (${JSON.stringify(definition)})`);
|
||||
}
|
||||
|
||||
if (definition.name !== undefined) {
|
||||
return new NamedType(definition.name, definition.class);
|
||||
}
|
||||
|
||||
return new Type(definition.class);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'ConfigurationReader';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type IDependencyProvider from './IDependencyProvider';
|
||||
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';
|
||||
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 {
|
||||
export class DependencyManager implements IDependencyManager {
|
||||
private readonly _moduleLoader: ModuleLoader;
|
||||
private readonly _types: Type[];
|
||||
private readonly _eagerTypes: Type[];
|
||||
@@ -99,7 +99,7 @@ export default class DependencyManager implements IDependencyManager {
|
||||
return candidates[0] as IDependencyProvider;
|
||||
}
|
||||
|
||||
public async instantiateType(type: Type): Promise<unknown> {
|
||||
public async instantiateType<T = unknown>(type: Type): Promise<T> {
|
||||
if (!(type instanceof Type)) {
|
||||
throw new Error('Type must be an instance of Type');
|
||||
}
|
||||
@@ -107,7 +107,8 @@ export default class DependencyManager implements IDependencyManager {
|
||||
const key = type.toURN();
|
||||
|
||||
// Check for circular dependencies
|
||||
if (this._pending.includes(type)) {
|
||||
const isAlreadyPending = this._pending.some(t => t.equals(type));
|
||||
if (isAlreadyPending) {
|
||||
const pendingChain = this._pending.map(t => t.toString()).join(' -> ');
|
||||
throw new Error(`Failed to resolve ${type} due to circular dependency: ${pendingChain}`);
|
||||
}
|
||||
@@ -115,6 +116,16 @@ export default class DependencyManager implements IDependencyManager {
|
||||
this._pending.push(type);
|
||||
|
||||
try {
|
||||
// Check if there's a provider for this type (only if not already being instantiated)
|
||||
const providers = this._providers.filter(provider => provider.canProvide(type));
|
||||
if (providers.length > 0) {
|
||||
if (providers.length > 1) {
|
||||
const candidateNames = providers.map(c => c.toString()).join(', ');
|
||||
throw new Error(`Multiple providers for ${type}: ${candidateNames}`);
|
||||
}
|
||||
return await providers[0].provide(type) as T;
|
||||
}
|
||||
|
||||
// Get dependencies for this type
|
||||
const dependencies = this._injections.get(key) || [];
|
||||
|
||||
@@ -135,7 +146,16 @@ export default class DependencyManager implements IDependencyManager {
|
||||
|
||||
// Load the module
|
||||
const module = await this._moduleLoader(type.getType());
|
||||
const Constructor = 'default' in module ? module.default : module;
|
||||
let Constructor;
|
||||
|
||||
if ('default' in module) {
|
||||
Constructor = module.default;
|
||||
} else {
|
||||
// Handle named exports - extract class name from type path
|
||||
const typePath = type.getType();
|
||||
const className = typePath.split('/').pop();
|
||||
Constructor = module[className as string];
|
||||
}
|
||||
|
||||
if (!Constructor) {
|
||||
throw new Error(`Could not load type "${type.getType()}"`);
|
||||
@@ -144,11 +164,6 @@ export default class DependencyManager implements IDependencyManager {
|
||||
// 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
|
||||
|
||||
92
src/di/DependencyProvider.ts
Normal file
92
src/di/DependencyProvider.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type {IDependencyProvider} from './IDependencyProvider';
|
||||
import type {Milliseconds} from '../types/Units';
|
||||
import type {IDependencyManager} from './IDependencyManager';
|
||||
import {Lifecycle} from './Lifecycle';
|
||||
import {Type} from './Type';
|
||||
import {NamedType} from './NamedType';
|
||||
|
||||
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 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');
|
||||
}
|
||||
|
||||
// Only provide the instance type to avoid circular dependencies
|
||||
// when the provider tries to instantiate its own concrete type
|
||||
return type.equals(this._instanceType);
|
||||
}
|
||||
|
||||
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> {
|
||||
// If _type is a NamedType, instantiate the base type to avoid circular dependency
|
||||
const typeToInstantiate = this._type instanceof NamedType
|
||||
? new Type(this._type.getType())
|
||||
: this._type;
|
||||
|
||||
const instance = await this._dependencyManager.instantiateType(typeToInstantiate);
|
||||
|
||||
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}]`;
|
||||
}
|
||||
}
|
||||
3
src/di/IConfigurationLoader.ts
Normal file
3
src/di/IConfigurationLoader.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IConfigurationLoader {
|
||||
load(name: string): Promise<unknown>;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type Type from './Type';
|
||||
import type {Type} from './Type';
|
||||
import type IDisposable from '../lang/IDisposable';
|
||||
import type IDependencyProvider from './IDependencyProvider';
|
||||
import type {IDependencyProvider} from './IDependencyProvider';
|
||||
|
||||
export default interface IDependencyManager {
|
||||
export interface IDependencyManager {
|
||||
defineDependencies(type: Type, dependencies: Type[]): IDisposable;
|
||||
getDependencies(type: Type): Type[] | undefined;
|
||||
addProvider(provider: IDependencyProvider): IDisposable;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Type from './Type';
|
||||
import {Type} from './Type';
|
||||
|
||||
export default interface IDependencyProvider {
|
||||
export interface IDependencyProvider {
|
||||
canProvide(type: Type): boolean;
|
||||
provide(type: Type): Promise<unknown>;
|
||||
toString(): string;
|
||||
|
||||
10
src/di/IInject.ts
Normal file
10
src/di/IInject.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type IDisposable from '../lang/IDisposable';
|
||||
import {Type} from './Type';
|
||||
|
||||
export interface IInject extends IDisposable {
|
||||
scope(): Promise<IInject>;
|
||||
defineInstance(type: string, instance: unknown): void;
|
||||
instantiate<T = unknown>(type: string): Promise<T>;
|
||||
start(): Promise<unknown[]>;
|
||||
getAvailableTypes(): Promise<Type[]>;
|
||||
}
|
||||
16
src/di/IServiceDefinition.ts
Normal file
16
src/di/IServiceDefinition.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {LifecycleType} from './Lifecycle';
|
||||
|
||||
export interface IDependencyDefinition {
|
||||
class?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IServiceDefinition {
|
||||
class: string;
|
||||
name?: string;
|
||||
inject?: (string | IDependencyDefinition)[];
|
||||
instanceType?: string;
|
||||
lifecycle?: LifecycleType;
|
||||
eager?: boolean;
|
||||
include?: string;
|
||||
}
|
||||
83
src/di/Inject.ts
Normal file
83
src/di/Inject.ts
Normal file
@@ -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<IInject> {
|
||||
// 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<IInject>(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<T = unknown>(type: string): Promise<T> {
|
||||
const theType = new Type(type);
|
||||
const provider = await this._dependencyManager.resolveProvider(theType);
|
||||
return (await provider.provide(theType)) as T;
|
||||
}
|
||||
|
||||
async start(): Promise<unknown[]> {
|
||||
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<Type[]> {
|
||||
return this._dependencyManager.getTypes();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._scope.dispose();
|
||||
|
||||
// Notify all listeners
|
||||
for (const callback of this._endedCallbacks) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'Inject';
|
||||
}
|
||||
}
|
||||
47
src/di/InstanceProvider.ts
Normal file
47
src/di/InstanceProvider.ts
Normal file
@@ -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<unknown> {
|
||||
if (!(type instanceof Type)) {
|
||||
throw new Error('Must provide a valid type');
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'InstanceProvider';
|
||||
}
|
||||
}
|
||||
33
src/di/IntegerConstantProvider.ts
Normal file
33
src/di/IntegerConstantProvider.ts
Normal file
@@ -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<number> {
|
||||
if (!(type instanceof NamedType)) {
|
||||
throw new Error('Must provide a NamedType');
|
||||
}
|
||||
|
||||
return parseInt(type.getName(), 10);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'IntegerConstantProvider';
|
||||
}
|
||||
}
|
||||
12
src/di/JSONConfigurationLoader.ts
Normal file
12
src/di/JSONConfigurationLoader.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type {IConfigurationLoader} from './IConfigurationLoader';
|
||||
import fs from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
export class JSONConfigurationLoader implements IConfigurationLoader {
|
||||
async load(path: string): Promise<object> {
|
||||
const filePath = resolve(path);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
return JSON.parse(fileContent);
|
||||
}
|
||||
}
|
||||
37
src/di/Lifecycle.ts
Normal file
37
src/di/Lifecycle.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import assertUnreachable from '../lang/assertUnreachable';
|
||||
|
||||
export enum Lifecycle {
|
||||
Singleton = 0,
|
||||
Service = 1,
|
||||
Instance = 2
|
||||
}
|
||||
|
||||
export type LifecycleType = 'singleton' | 'service' | 'instance';
|
||||
|
||||
export class LifecycleMapping {
|
||||
public static convertLifecycleToLifecycleType(lifecycle: Lifecycle): LifecycleType {
|
||||
switch (lifecycle) {
|
||||
case Lifecycle.Singleton:
|
||||
return 'singleton';
|
||||
case Lifecycle.Service:
|
||||
return 'service';
|
||||
case Lifecycle.Instance:
|
||||
return 'instance';
|
||||
default:
|
||||
return assertUnreachable(lifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertLifecycleTypeToLifecycle(lifecycleType: LifecycleType): Lifecycle {
|
||||
switch (lifecycleType) {
|
||||
case 'singleton':
|
||||
return Lifecycle.Singleton;
|
||||
case 'service':
|
||||
return Lifecycle.Service;
|
||||
case 'instance':
|
||||
return Lifecycle.Instance;
|
||||
default:
|
||||
return assertUnreachable(lifecycleType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Type from './Type';
|
||||
import {Type} from './Type';
|
||||
|
||||
export default class NamedType extends Type {
|
||||
export class NamedType extends Type {
|
||||
private readonly _name: string;
|
||||
|
||||
constructor(name: string, type: string) {
|
||||
@@ -12,15 +12,15 @@ 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 {
|
||||
public override toURN(): string {
|
||||
return `urn:namedtype:${super.getType()}#${this._name}`;
|
||||
}
|
||||
|
||||
public overridetoString(): string {
|
||||
public override toString(): string {
|
||||
return `${super.getType()}#${this._name}`;
|
||||
}
|
||||
}
|
||||
|
||||
33
src/di/StringConstantProvider.ts
Normal file
33
src/di/StringConstantProvider.ts
Normal file
@@ -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<string> {
|
||||
if (!(type instanceof NamedType)) {
|
||||
throw new Error('Must provide a NamedType');
|
||||
}
|
||||
|
||||
return type.getName();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'StringConstantProvider';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export default class Type {
|
||||
export class Type {
|
||||
private _type: string;
|
||||
|
||||
constructor(type: string) {
|
||||
@@ -10,7 +10,8 @@ export default class Type {
|
||||
}
|
||||
|
||||
public equals(other: Type): boolean {
|
||||
return other instanceof Type && this.getType() === other.getType();
|
||||
// Ensure Type only equals Type, not subclasses like NamedType
|
||||
return other instanceof Type && other.constructor === this.constructor && this.getType() === other.getType();
|
||||
}
|
||||
|
||||
public toURN() {
|
||||
|
||||
16
src/di/index.ts
Normal file
16
src/di/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type {IDependencyProvider} from './IDependencyProvider';
|
||||
export type {IDependencyManager} from './IDependencyManager';
|
||||
export type {default as IDisposable} from '../lang/IDisposable';
|
||||
export type {LifecycleType} from './Lifecycle';
|
||||
export type {IInject} from './IInject';
|
||||
export type {IConfigurationLoader} from './IConfigurationLoader';
|
||||
export {Type} from './Type';
|
||||
export {NamedType} from './NamedType';
|
||||
export {DependencyManager} from './DependencyManager';
|
||||
export {DependencyProvider} from './DependencyProvider';
|
||||
export {InstanceProvider} from './InstanceProvider';
|
||||
export {IntegerConstantProvider} from './IntegerConstantProvider';
|
||||
export {StringConstantProvider} from './StringConstantProvider';
|
||||
export {Inject} from './Inject';
|
||||
export {ConfigurationReader} from './ConfigurationReader';
|
||||
export {JSONConfigurationLoader} from './JSONConfigurationLoader';
|
||||
56
src/env/ConfigurationProvider.ts
vendored
Normal file
56
src/env/ConfigurationProvider.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import type {IDependencyProvider} from '../di/IDependencyProvider';
|
||||
import {NamedType} from '../di/NamedType';
|
||||
import {Type} from '../di/Type';
|
||||
|
||||
export class ConfigurationProvider implements IDependencyProvider {
|
||||
private readonly _config: Record<string, unknown>;
|
||||
|
||||
constructor() {
|
||||
this._config = {
|
||||
connection: 'localhost:5672',
|
||||
mq: {
|
||||
heartbeat: {
|
||||
interval: 30000,
|
||||
timeout: 60000
|
||||
}
|
||||
},
|
||||
environment: {
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public canProvide(type: Type): boolean {
|
||||
if (type instanceof NamedType) {
|
||||
return type.getType() === 'src/env/ConfigurationProvider';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async provide(type: NamedType): Promise<unknown> {
|
||||
const name = type.getName();
|
||||
|
||||
if (name === '') {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
const props = name.split('.');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let current: any = this._config;
|
||||
|
||||
for (const prop of props) {
|
||||
if (!(prop in current)) {
|
||||
throw new Error(`Config path not found: ${name}`);
|
||||
}
|
||||
current = current[prop];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'ConfigurationProvider';
|
||||
}
|
||||
}
|
||||
32
src/env/RuntimeConfigurationProvider.ts
vendored
Normal file
32
src/env/RuntimeConfigurationProvider.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import {type IDependencyProvider, Type} from '../di';
|
||||
import packageJSON from '../../package.json' with {type: 'json'};
|
||||
|
||||
export class RuntimeConfigurationProvider implements IDependencyProvider {
|
||||
public canProvide(type: Type): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public async provide(type: Type): Promise<unknown> {
|
||||
const runtimeConfig = {
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
version: packageJSON.version,
|
||||
server: {},
|
||||
client: {}
|
||||
};
|
||||
const serverApp = process.env.SERVER_APP;
|
||||
|
||||
if (serverApp) {
|
||||
runtimeConfig.server = {app: serverApp};
|
||||
}
|
||||
|
||||
const clientApp = process.env.CLIENT_APP;
|
||||
|
||||
if (clientApp) {
|
||||
runtimeConfig.client = {app: clientApp};
|
||||
}
|
||||
|
||||
return runtimeConfig;
|
||||
}
|
||||
public toString(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
27
src/example/Connection.ts
Normal file
27
src/example/Connection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type {IConnection} from './IConnection';
|
||||
|
||||
export class Connection implements IConnection {
|
||||
private readonly _connectionString: string;
|
||||
|
||||
constructor(connectionString: string) {
|
||||
console.log(`[Connection] Created with: ${connectionString}`);
|
||||
this._connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
console.log(`[Connection] Connecting with ${this._connectionString}...`);
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
console.log('[Connection] Connected.');
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
console.log('[Connection] Disconnecting...');
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
console.log('[Connection] Disconnected.');
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'Connection';
|
||||
}
|
||||
}
|
||||
5
src/example/IConnection.ts
Normal file
5
src/example/IConnection.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IConnection {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
toString(): string;
|
||||
}
|
||||
4
src/example/IMessageQueue.ts
Normal file
4
src/example/IMessageQueue.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IMessageQueue {
|
||||
publish(topic: string, message: string): Promise<void>;
|
||||
subscribe(topic: string, handler: (message: string) => void): Promise<void>;
|
||||
}
|
||||
30
src/example/MessageQueue.ts
Normal file
30
src/example/MessageQueue.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type {IMessageQueue} from './IMessageQueue';
|
||||
import type {IConnection} from './IConnection';
|
||||
|
||||
export class MessageQueue implements IMessageQueue {
|
||||
private readonly _connection: IConnection;
|
||||
|
||||
constructor(connection: IConnection) {
|
||||
if (!connection) {
|
||||
throw new Error('Connection is required');
|
||||
}
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
public async publish(topic: string, message: string): Promise<void> {
|
||||
await this._connection.connect();
|
||||
console.log(`Publishing to [${topic}]: ${message}`);
|
||||
await this._connection.disconnect();
|
||||
}
|
||||
|
||||
public async subscribe(topic: string, handler: (message: string) => void): Promise<void> {
|
||||
await this._connection.connect();
|
||||
console.log(`Subscribing to [${topic}]`);
|
||||
// In a real implementation, you'd keep the connection open and listen for messages.
|
||||
// Here we'll just simulate receiving one message.
|
||||
setTimeout(() => {
|
||||
handler('Hello from the queue!');
|
||||
this._connection.disconnect();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
143
src/health/HealthCheck.ts
Normal file
143
src/health/HealthCheck.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import moment from 'moment';
|
||||
import type IDisposable from '../lang/IDisposable';
|
||||
import {Events} from '../lang/Events';
|
||||
|
||||
const checkForRecoverabilityTimeout = moment.duration(30, 'seconds');
|
||||
|
||||
interface IHealthCheckable {
|
||||
checkForRecoverability(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class HealthCheck {
|
||||
private readonly _environment: string;
|
||||
private readonly _app: string;
|
||||
private readonly _version: string;
|
||||
private readonly _regionName: string;
|
||||
private readonly _zone: string;
|
||||
private readonly _checks: IHealthCheckable[];
|
||||
private readonly _since: string;
|
||||
private readonly _events: Events;
|
||||
private _status: string;
|
||||
private _enabled: boolean;
|
||||
|
||||
constructor(environment: string, app: string, version: string, regionName: string, zone: string, ...checkForRecoverability: IHealthCheckable[]) {
|
||||
this._environment = environment;
|
||||
this._app = app;
|
||||
this._version = version;
|
||||
this._regionName = regionName;
|
||||
this._zone = zone;
|
||||
this._checks = checkForRecoverability;
|
||||
this._since = moment.utc().toISOString();
|
||||
this._status = 'starting';
|
||||
this._events = new Events();
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._status = 'starting';
|
||||
this._events.emit('starting');
|
||||
this._events.emit('status-changed');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public getInfo() {
|
||||
return {
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
region: this._regionName,
|
||||
zone: this._zone,
|
||||
since: this._since
|
||||
};
|
||||
}
|
||||
|
||||
public async checkHealth() {
|
||||
if (this._status !== 'ok') {
|
||||
return {
|
||||
status: this._status,
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
this._checks.map(async check => {
|
||||
const isHealthy = await check.checkForRecoverability();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.warn('Health check failed [%s]', check);
|
||||
} else {
|
||||
console.debug('Health check passed [%s]', check);
|
||||
}
|
||||
|
||||
return isHealthy;
|
||||
})
|
||||
);
|
||||
|
||||
const success = results.every(result => result === true);
|
||||
|
||||
if (success === true) {
|
||||
let status = this._status;
|
||||
|
||||
if (status === 'ok' && !this._enabled) {
|
||||
status = 'disabled';
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
}
|
||||
|
||||
this._events.emit('health-check-failed');
|
||||
|
||||
return {
|
||||
status: 'health-check-failed',
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
} catch (e) {
|
||||
this._events.emit('health-check-failed');
|
||||
|
||||
console.warn('Health check failed', e);
|
||||
|
||||
return {
|
||||
status: 'health-check-failed',
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone,
|
||||
reason: (e as Error).message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public on(event: string, listener: (...args: unknown[]) => void | Promise<void>): IDisposable {
|
||||
return this._events.on(event, listener);
|
||||
}
|
||||
|
||||
public markReady() {
|
||||
this._status = 'ok';
|
||||
this._events.emit('ok');
|
||||
this._events.emit('status-changed');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
137
src/health/HttpHealthCheck.ts
Normal file
137
src/health/HttpHealthCheck.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type {Duration} from 'moment';
|
||||
import moment from 'moment';
|
||||
import {Agent as HttpAgent, request as httpRequest} from 'http';
|
||||
import {Agent as HttpsAgent, request as httpsRequest} from 'https';
|
||||
|
||||
interface IHttpHealthCheckConfig {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: string;
|
||||
keepalive?: boolean;
|
||||
headers?: Record<string, string>;
|
||||
reconnectTimeout?: string;
|
||||
requestTimeout?: string;
|
||||
}
|
||||
|
||||
export class HttpHealthCheck {
|
||||
private readonly _protocol: string;
|
||||
private readonly _hostname: string;
|
||||
private readonly _port: string;
|
||||
private readonly _keepalive: boolean;
|
||||
private readonly _headers: Record<string, string>;
|
||||
private readonly _connectTimeout: Duration;
|
||||
private readonly _requestTimeout: Duration;
|
||||
|
||||
constructor(config: IHttpHealthCheckConfig) {
|
||||
this._protocol = config.protocol;
|
||||
this._hostname = config.hostname;
|
||||
this._port = config.port;
|
||||
this._keepalive = config.keepalive ?? false;
|
||||
this._headers = config.headers ?? {};
|
||||
this._connectTimeout = moment.duration(config.reconnectTimeout ?? '30s');
|
||||
this._requestTimeout = moment.duration(config.requestTimeout ?? '30s');
|
||||
}
|
||||
|
||||
public checkForRecoverability(): Promise<boolean> {
|
||||
const method = 'GET';
|
||||
const path = '/';
|
||||
const isHttps = this._protocol === 'https';
|
||||
const Agent = isHttps ? HttpsAgent : HttpAgent;
|
||||
const request = isHttps ? httpsRequest : httpRequest;
|
||||
|
||||
const options = {
|
||||
hostname: this._hostname,
|
||||
port: this._port,
|
||||
path,
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'Health-Check/1.0',
|
||||
Connection: 'close'
|
||||
},
|
||||
timeout: this._connectTimeout.asMilliseconds(),
|
||||
agent: new Agent({
|
||||
keepAlive: this._keepalive,
|
||||
maxSockets: 1,
|
||||
maxFreeSockets: 0
|
||||
}),
|
||||
rejectUnauthorized: !isHttps
|
||||
};
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const req = request(options, res => {
|
||||
let isHealthy = false;
|
||||
|
||||
switch (res.statusCode) {
|
||||
case 200:
|
||||
case 301:
|
||||
case 404: {
|
||||
const allHeadersMatch = Object.keys(this._headers).every(headerKey => res.headers[headerKey] === this._headers[headerKey]);
|
||||
|
||||
if (allHeadersMatch) {
|
||||
isHealthy = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fall through to default case
|
||||
console.warn(
|
||||
'HTTP health check to host [%s]:[%s] for [%s] [%s] failed - headers mismatch. Code [%s] headers [%j]',
|
||||
this._hostname,
|
||||
this._port,
|
||||
method,
|
||||
path,
|
||||
res.statusCode,
|
||||
res.headers
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'HTTP health check to host [%s]:[%s] for [%s] [%s] failed with code [%s] and headers [%j]',
|
||||
this._hostname,
|
||||
this._port,
|
||||
method,
|
||||
path,
|
||||
res.statusCode,
|
||||
res.headers
|
||||
);
|
||||
}
|
||||
|
||||
res.on('data', () => {
|
||||
// Ignore response data
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(isHealthy);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e: Error & {code?: string}) => {
|
||||
reject(e);
|
||||
|
||||
if (e.code) {
|
||||
console.error('HTTP health check failed: %s', e.code);
|
||||
} else {
|
||||
console.error('HTTP health check failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
req.setTimeout(this._requestTimeout.asMilliseconds(), () => {
|
||||
const err = new Error('HTTP health check timed out') as Error & {code?: string};
|
||||
err.code = 'ETIMEDOUT';
|
||||
|
||||
reject(err);
|
||||
|
||||
console.warn('HTTP health check timed out');
|
||||
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `HttpHealthCheck[protocol=[${this._protocol}],hostname=[${this._hostname}],port=[${this._port}]]`;
|
||||
}
|
||||
}
|
||||
10
src/index.ts
Normal file
10
src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Main exports for platform-ts library
|
||||
export * from './di';
|
||||
export * from './env/ConfigurationProvider';
|
||||
export * from './health/HealthCheck';
|
||||
export * from './health/HttpHealthCheck';
|
||||
export * from './lang/Events';
|
||||
export {default as Disposable} from './lang/Disposable';
|
||||
export {default as DisposableList} from './lang/DisposableList';
|
||||
export type {default as IDisposable} from './lang/IDisposable';
|
||||
export {default as assertUnreachable} from './lang/assertUnreachable';
|
||||
@@ -1,4 +1,4 @@
|
||||
import IDisposable from './IDisposable';
|
||||
import type IDisposable from './IDisposable';
|
||||
|
||||
export default class Disposable implements IDisposable {
|
||||
private readonly _cleanup: () => void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import IDisposable from './IDisposable';
|
||||
import type IDisposable from './IDisposable';
|
||||
|
||||
export default class DisposableList implements IDisposable {
|
||||
private readonly _list: IDisposable[];
|
||||
@@ -15,7 +15,7 @@ export default class DisposableList implements IDisposable {
|
||||
while (this._list.length) {
|
||||
const disposable = this._list.shift();
|
||||
|
||||
disposable.dispose();
|
||||
disposable!.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
src/lang/Events.ts
Normal file
53
src/lang/Events.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {EventEmitter} from 'events';
|
||||
import type IDisposable from './IDisposable';
|
||||
import Disposable from './Disposable';
|
||||
|
||||
type EventListener = (...args: unknown[]) => void | Promise<void>;
|
||||
|
||||
export class Events {
|
||||
private readonly _events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
this._events = new EventEmitter();
|
||||
}
|
||||
|
||||
public on(event: string, listener: EventListener): IDisposable {
|
||||
const wrappedListener = async (...eventArguments: unknown[]) => {
|
||||
try {
|
||||
await listener.apply(this, eventArguments);
|
||||
} catch (e) {
|
||||
console.error('Failed to process event [%s] with listener [%s]', event, listener, e);
|
||||
}
|
||||
};
|
||||
|
||||
this._events.on(event, wrappedListener);
|
||||
|
||||
return new Disposable(() => {
|
||||
this._events.removeListener(event, wrappedListener);
|
||||
});
|
||||
}
|
||||
|
||||
public once(event: string, listener: EventListener): IDisposable {
|
||||
const wrappedListener = async (...eventArguments: unknown[]) => {
|
||||
try {
|
||||
await listener.apply(this, eventArguments);
|
||||
} catch (e) {
|
||||
console.error('Failed to process event [%s] with listener [%s]', event, listener, e);
|
||||
}
|
||||
};
|
||||
|
||||
this._events.once(event, wrappedListener);
|
||||
|
||||
return new Disposable(() => {
|
||||
this._events.removeListener(event, wrappedListener);
|
||||
});
|
||||
}
|
||||
|
||||
public emit(event: string, ...eventArguments: unknown[]): boolean {
|
||||
return this._events.emit(event, ...eventArguments);
|
||||
}
|
||||
|
||||
public listenerCount(event: string): number {
|
||||
return this._events.listenerCount(event);
|
||||
}
|
||||
}
|
||||
3
src/lang/assertUnreachable.ts
Normal file
3
src/lang/assertUnreachable.ts
Normal 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
12
src/types/Units.ts
Normal 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;
|
||||
@@ -7,12 +7,18 @@
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./dist/types",
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"noEmit": false,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
@@ -25,5 +31,7 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/**/*", "test/**/*", "scripts/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user