Add moment.js as a dependency, enhance TypeScript configuration to exclude scripts, and introduce new examples for dependency injection and health checks.

This commit is contained in:
2025-10-26 09:39:46 -04:00
parent 5d9b77ef7d
commit 3429df650f
26 changed files with 868 additions and 30 deletions

0
examples/cluster.json Normal file
View File

View File

@@ -1,3 +1,32 @@
import {DependencyManager} from '../src'; import {ConfigurationReader, DependencyManager, JSONConfigurationLoader, Type} from '../src/di';
import type {IMessageQueue} from '../src/example/IMessageQueue';
const dependencyManager = new DependencyManager(async path => await import(path)); 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
View 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"
}
]
}
]

View File

@@ -25,5 +25,8 @@
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "5.9.3" "typescript": "5.9.3"
},
"dependencies": {
"moment": "2.30.1"
} }
} }

81
scripts/Runner.js Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
import packageJSON from '../package.json' with {type: 'json'};
process.stdout.write(packageJSON.version);

View 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';
}
}

View File

@@ -99,7 +99,7 @@ export class DependencyManager implements IDependencyManager {
return candidates[0] as IDependencyProvider; 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)) { if (!(type instanceof Type)) {
throw new Error('Type must be an instance of Type'); throw new Error('Type must be an instance of Type');
} }
@@ -107,7 +107,8 @@ export class DependencyManager implements IDependencyManager {
const key = type.toURN(); const key = type.toURN();
// Check for circular dependencies // 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(' -> '); const pendingChain = this._pending.map(t => t.toString()).join(' -> ');
throw new Error(`Failed to resolve ${type} due to circular dependency: ${pendingChain}`); throw new Error(`Failed to resolve ${type} due to circular dependency: ${pendingChain}`);
} }
@@ -115,6 +116,16 @@ export class DependencyManager implements IDependencyManager {
this._pending.push(type); this._pending.push(type);
try { 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 // Get dependencies for this type
const dependencies = this._injections.get(key) || []; const dependencies = this._injections.get(key) || [];
@@ -135,7 +146,16 @@ export class DependencyManager implements IDependencyManager {
// Load the module // Load the module
const module = await this._moduleLoader(type.getType()); 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) { if (!Constructor) {
throw new Error(`Could not load type "${type.getType()}"`); throw new Error(`Could not load type "${type.getType()}"`);
@@ -144,11 +164,6 @@ export class DependencyManager implements IDependencyManager {
// Instantiate with resolved dependencies // Instantiate with resolved dependencies
const instance = this.injectDependencies(Constructor, resolvedDependencies); 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; return instance;
} finally { } finally {
// Remove from pending stack // Remove from pending stack

View File

@@ -3,6 +3,7 @@ import type {Milliseconds} from '../types/Units';
import type {IDependencyManager} from './IDependencyManager'; import type {IDependencyManager} from './IDependencyManager';
import {Lifecycle} from './Lifecycle'; import {Lifecycle} from './Lifecycle';
import {Type} from './Type'; import {Type} from './Type';
import {NamedType} from './NamedType';
interface IStartable { interface IStartable {
start(): Promise<void> | void; start(): Promise<void> | void;
@@ -33,7 +34,9 @@ export class DependencyProvider implements IDependencyProvider {
throw new Error('Must provide a valid type'); throw new Error('Must provide a valid type');
} }
return type.equals(this._type) && this._type.equals(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> { public async provide(type: Type): Promise<unknown> {
@@ -55,7 +58,12 @@ export class DependencyProvider implements IDependencyProvider {
} }
private async _createInstance(): Promise<unknown> { private async _createInstance(): Promise<unknown> {
const instance = await this._dependencyManager.instantiateType(this._instanceType); // 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 (this._lifecycle === Lifecycle.Service) {
if (!isStartable(instance)) { if (!isStartable(instance)) {

View 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;
}

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

View File

@@ -6,13 +6,13 @@ export enum Lifecycle {
Instance = 2 Instance = 2
} }
export type LifecycleType = 'signleton' | 'service' | 'instance'; export type LifecycleType = 'singleton' | 'service' | 'instance';
export class LifecycleMapping { export class LifecycleMapping {
public static convertLifecycleToLifecycleType(lifecycle: Lifecycle): LifecycleType { public static convertLifecycleToLifecycleType(lifecycle: Lifecycle): LifecycleType {
switch (lifecycle) { switch (lifecycle) {
case Lifecycle.Singleton: case Lifecycle.Singleton:
return 'signleton'; return 'singleton';
case Lifecycle.Service: case Lifecycle.Service:
return 'service'; return 'service';
case Lifecycle.Instance: case Lifecycle.Instance:
@@ -24,7 +24,7 @@ export class LifecycleMapping {
public static convertLifecycleTypeToLifecycle(lifecycleType: LifecycleType): Lifecycle { public static convertLifecycleTypeToLifecycle(lifecycleType: LifecycleType): Lifecycle {
switch (lifecycleType) { switch (lifecycleType) {
case 'signleton': case 'singleton':
return Lifecycle.Singleton; return Lifecycle.Singleton;
case 'service': case 'service':
return Lifecycle.Service; return Lifecycle.Service;

View File

@@ -10,7 +10,8 @@ export class Type {
} }
public equals(other: Type): boolean { 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() { public toURN() {

16
src/di/index.ts Normal file
View 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
View 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
View 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
View 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';
}
}

View File

@@ -0,0 +1,5 @@
export interface IConnection {
connect(): Promise<void>;
disconnect(): Promise<void>;
toString(): string;
}

View File

@@ -0,0 +1,4 @@
export interface IMessageQueue {
publish(topic: string, message: string): Promise<void>;
subscribe(topic: string, handler: (message: string) => void): Promise<void>;
}

View 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
View 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;
}
}

View 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}]]`;
}
}

View File

@@ -1,14 +1,10 @@
export type {IDependencyProvider} from './di/IDependencyProvider'; // Main exports for platform-ts library
export type {IDependencyManager} from './di/IDependencyManager'; 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 type {default as IDisposable} from './lang/IDisposable';
export type {LifecycleType} from './di/Lifecycle'; export {default as assertUnreachable} from './lang/assertUnreachable';
export type {IInject} from './di/IInject';
export type {IConfigurationLoader} from './di/IConfigurationLoader';
export {Type} from './di/Type';
export {NamedType} from './di/NamedType';
export {DependencyManager} from './di/DependencyManager';
export {DependencyProvider} from './di/DependencyProvider';
export {InstanceProvider} from './di/InstanceProvider';
export {IntegerConstantProvider} from './di/IntegerConstantProvider';
export {StringConstantProvider} from './di/StringConstantProvider';
export {Inject} from './di/Inject';

53
src/lang/Events.ts Normal file
View 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);
}
}

View File

@@ -33,5 +33,5 @@
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules/**/*", "test/**/*"] "exclude": ["node_modules/**/*", "test/**/*", "scripts/**/*"]
} }