fix(refactor): remove deprecated Port80Handler and related utilities
- Deleted event-utils.ts which contained deprecated Port80Handler and its subscribers. - Updated index.ts to remove the export of event-utils. - Refactored ConnectionManager to extend LifecycleComponent for better resource management. - Added BinaryHeap implementation for efficient priority queue operations. - Introduced EnhancedConnectionPool for managing pooled connections with lifecycle management. - Implemented LifecycleComponent to manage timers and event listeners automatically. - Added comprehensive tests for BinaryHeap and LifecycleComponent to ensure functionality.
This commit is contained in:
225
ts/core/utils/binary-heap.ts
Normal file
225
ts/core/utils/binary-heap.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* A binary heap implementation for efficient priority queue operations
|
||||
* Supports O(log n) insert and extract operations
|
||||
*/
|
||||
export class BinaryHeap<T> {
|
||||
private heap: T[] = [];
|
||||
private keyMap?: Map<string, number>; // For efficient key-based lookups
|
||||
|
||||
constructor(
|
||||
private compareFn: (a: T, b: T) => number,
|
||||
private extractKey?: (item: T) => string
|
||||
) {
|
||||
if (extractKey) {
|
||||
this.keyMap = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current size of the heap
|
||||
*/
|
||||
public get size(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the heap is empty
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this.heap.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek at the top element without removing it
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
return this.heap[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new item into the heap
|
||||
* O(log n) time complexity
|
||||
*/
|
||||
public insert(item: T): void {
|
||||
const index = this.heap.length;
|
||||
this.heap.push(item);
|
||||
|
||||
if (this.keyMap && this.extractKey) {
|
||||
const key = this.extractKey(item);
|
||||
this.keyMap.set(key, index);
|
||||
}
|
||||
|
||||
this.bubbleUp(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the top element from the heap
|
||||
* O(log n) time complexity
|
||||
*/
|
||||
public extract(): T | undefined {
|
||||
if (this.heap.length === 0) return undefined;
|
||||
if (this.heap.length === 1) {
|
||||
const item = this.heap.pop()!;
|
||||
if (this.keyMap && this.extractKey) {
|
||||
this.keyMap.delete(this.extractKey(item));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
const result = this.heap[0];
|
||||
const lastItem = this.heap.pop()!;
|
||||
this.heap[0] = lastItem;
|
||||
|
||||
if (this.keyMap && this.extractKey) {
|
||||
this.keyMap.delete(this.extractKey(result));
|
||||
this.keyMap.set(this.extractKey(lastItem), 0);
|
||||
}
|
||||
|
||||
this.bubbleDown(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an element that matches the predicate
|
||||
* O(n) time complexity for search, O(log n) for extraction
|
||||
*/
|
||||
public extractIf(predicate: (item: T) => boolean): T | undefined {
|
||||
const index = this.heap.findIndex(predicate);
|
||||
if (index === -1) return undefined;
|
||||
|
||||
return this.extractAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an element by its key (if extractKey was provided)
|
||||
* O(log n) time complexity
|
||||
*/
|
||||
public extractByKey(key: string): T | undefined {
|
||||
if (!this.keyMap || !this.extractKey) {
|
||||
throw new Error('extractKey function must be provided to use key-based extraction');
|
||||
}
|
||||
|
||||
const index = this.keyMap.get(key);
|
||||
if (index === undefined) return undefined;
|
||||
|
||||
return this.extractAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists in the heap
|
||||
* O(1) time complexity
|
||||
*/
|
||||
public hasKey(key: string): boolean {
|
||||
if (!this.keyMap) return false;
|
||||
return this.keyMap.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all elements as an array (does not modify heap)
|
||||
* O(n) time complexity
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return [...this.heap];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the heap
|
||||
*/
|
||||
public clear(): void {
|
||||
this.heap = [];
|
||||
if (this.keyMap) {
|
||||
this.keyMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract element at specific index
|
||||
*/
|
||||
private extractAt(index: number): T {
|
||||
const item = this.heap[index];
|
||||
|
||||
if (this.keyMap && this.extractKey) {
|
||||
this.keyMap.delete(this.extractKey(item));
|
||||
}
|
||||
|
||||
if (index === this.heap.length - 1) {
|
||||
this.heap.pop();
|
||||
return item;
|
||||
}
|
||||
|
||||
const lastItem = this.heap.pop()!;
|
||||
this.heap[index] = lastItem;
|
||||
|
||||
if (this.keyMap && this.extractKey) {
|
||||
this.keyMap.set(this.extractKey(lastItem), index);
|
||||
}
|
||||
|
||||
// Try bubbling up first
|
||||
const parentIndex = Math.floor((index - 1) / 2);
|
||||
if (parentIndex >= 0 && this.compareFn(this.heap[index], this.heap[parentIndex]) < 0) {
|
||||
this.bubbleUp(index);
|
||||
} else {
|
||||
this.bubbleDown(index);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubble up element at given index to maintain heap property
|
||||
*/
|
||||
private bubbleUp(index: number): void {
|
||||
while (index > 0) {
|
||||
const parentIndex = Math.floor((index - 1) / 2);
|
||||
|
||||
if (this.compareFn(this.heap[index], this.heap[parentIndex]) >= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.swap(index, parentIndex);
|
||||
index = parentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubble down element at given index to maintain heap property
|
||||
*/
|
||||
private bubbleDown(index: number): void {
|
||||
const length = this.heap.length;
|
||||
|
||||
while (true) {
|
||||
const leftChild = 2 * index + 1;
|
||||
const rightChild = 2 * index + 2;
|
||||
let smallest = index;
|
||||
|
||||
if (leftChild < length &&
|
||||
this.compareFn(this.heap[leftChild], this.heap[smallest]) < 0) {
|
||||
smallest = leftChild;
|
||||
}
|
||||
|
||||
if (rightChild < length &&
|
||||
this.compareFn(this.heap[rightChild], this.heap[smallest]) < 0) {
|
||||
smallest = rightChild;
|
||||
}
|
||||
|
||||
if (smallest === index) break;
|
||||
|
||||
this.swap(index, smallest);
|
||||
index = smallest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap two elements in the heap
|
||||
*/
|
||||
private swap(i: number, j: number): void {
|
||||
const temp = this.heap[i];
|
||||
this.heap[i] = this.heap[j];
|
||||
this.heap[j] = temp;
|
||||
|
||||
if (this.keyMap && this.extractKey) {
|
||||
this.keyMap.set(this.extractKey(this.heap[i]), i);
|
||||
this.keyMap.set(this.extractKey(this.heap[j]), j);
|
||||
}
|
||||
}
|
||||
}
|
420
ts/core/utils/enhanced-connection-pool.ts
Normal file
420
ts/core/utils/enhanced-connection-pool.ts
Normal file
@ -0,0 +1,420 @@
|
||||
import { LifecycleComponent } from './lifecycle-component.js';
|
||||
import { BinaryHeap } from './binary-heap.js';
|
||||
import { AsyncMutex } from './async-utils.js';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* Interface for pooled connection
|
||||
*/
|
||||
export interface IPooledConnection<T> {
|
||||
id: string;
|
||||
connection: T;
|
||||
createdAt: number;
|
||||
lastUsedAt: number;
|
||||
useCount: number;
|
||||
inUse: boolean;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the connection pool
|
||||
*/
|
||||
export interface IConnectionPoolOptions<T> {
|
||||
minSize?: number;
|
||||
maxSize?: number;
|
||||
acquireTimeout?: number;
|
||||
idleTimeout?: number;
|
||||
maxUseCount?: number;
|
||||
validateOnAcquire?: boolean;
|
||||
validateOnReturn?: boolean;
|
||||
queueTimeout?: number;
|
||||
connectionFactory: () => Promise<T>;
|
||||
connectionValidator?: (connection: T) => Promise<boolean>;
|
||||
connectionDestroyer?: (connection: T) => Promise<void>;
|
||||
onConnectionError?: (error: Error, connection?: T) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for queued acquire request
|
||||
*/
|
||||
interface IAcquireRequest<T> {
|
||||
id: string;
|
||||
priority: number;
|
||||
timestamp: number;
|
||||
resolve: (connection: IPooledConnection<T>) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeoutHandle?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced connection pool with priority queue, backpressure, and lifecycle management
|
||||
*/
|
||||
export class EnhancedConnectionPool<T> extends LifecycleComponent {
|
||||
private readonly options: Required<Omit<IConnectionPoolOptions<T>, 'connectionValidator' | 'connectionDestroyer' | 'onConnectionError'>> & Pick<IConnectionPoolOptions<T>, 'connectionValidator' | 'connectionDestroyer' | 'onConnectionError'>;
|
||||
private readonly availableConnections: IPooledConnection<T>[] = [];
|
||||
private readonly activeConnections: Map<string, IPooledConnection<T>> = new Map();
|
||||
private readonly waitQueue: BinaryHeap<IAcquireRequest<T>>;
|
||||
private readonly mutex = new AsyncMutex();
|
||||
private readonly eventEmitter = new EventEmitter();
|
||||
|
||||
private connectionIdCounter = 0;
|
||||
private requestIdCounter = 0;
|
||||
private isClosing = false;
|
||||
|
||||
// Metrics
|
||||
private metrics = {
|
||||
connectionsCreated: 0,
|
||||
connectionsDestroyed: 0,
|
||||
connectionsAcquired: 0,
|
||||
connectionsReleased: 0,
|
||||
acquireTimeouts: 0,
|
||||
validationFailures: 0,
|
||||
queueHighWaterMark: 0,
|
||||
};
|
||||
|
||||
constructor(options: IConnectionPoolOptions<T>) {
|
||||
super();
|
||||
|
||||
this.options = {
|
||||
minSize: 0,
|
||||
maxSize: 10,
|
||||
acquireTimeout: 30000,
|
||||
idleTimeout: 300000, // 5 minutes
|
||||
maxUseCount: Infinity,
|
||||
validateOnAcquire: true,
|
||||
validateOnReturn: false,
|
||||
queueTimeout: 60000,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Initialize priority queue (higher priority = extracted first)
|
||||
this.waitQueue = new BinaryHeap<IAcquireRequest<T>>(
|
||||
(a, b) => b.priority - a.priority || a.timestamp - b.timestamp,
|
||||
(item) => item.id
|
||||
);
|
||||
|
||||
// Start maintenance cycle
|
||||
this.startMaintenance();
|
||||
|
||||
// Initialize minimum connections
|
||||
this.initializeMinConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize minimum number of connections
|
||||
*/
|
||||
private async initializeMinConnections(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < this.options.minSize; i++) {
|
||||
promises.push(
|
||||
this.createConnection()
|
||||
.then(conn => {
|
||||
this.availableConnections.push(conn);
|
||||
})
|
||||
.catch(err => {
|
||||
if (this.options.onConnectionError) {
|
||||
this.options.onConnectionError(err);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start maintenance timer for idle connection cleanup
|
||||
*/
|
||||
private startMaintenance(): void {
|
||||
this.setInterval(() => {
|
||||
this.performMaintenance();
|
||||
}, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform maintenance tasks
|
||||
*/
|
||||
private async performMaintenance(): Promise<void> {
|
||||
await this.mutex.runExclusive(async () => {
|
||||
const now = Date.now();
|
||||
const toRemove: IPooledConnection<T>[] = [];
|
||||
|
||||
// Check for idle connections beyond minimum size
|
||||
for (let i = this.availableConnections.length - 1; i >= 0; i--) {
|
||||
const conn = this.availableConnections[i];
|
||||
|
||||
// Keep minimum connections
|
||||
if (this.availableConnections.length <= this.options.minSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove idle connections
|
||||
if (now - conn.lastUsedAt > this.options.idleTimeout) {
|
||||
toRemove.push(conn);
|
||||
this.availableConnections.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy idle connections
|
||||
for (const conn of toRemove) {
|
||||
await this.destroyConnection(conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a connection from the pool
|
||||
*/
|
||||
public async acquire(priority: number = 0, timeout?: number): Promise<IPooledConnection<T>> {
|
||||
if (this.isClosing) {
|
||||
throw new Error('Connection pool is closing');
|
||||
}
|
||||
|
||||
return this.mutex.runExclusive(async () => {
|
||||
// Try to get an available connection
|
||||
const connection = await this.tryAcquireConnection();
|
||||
if (connection) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Check if we can create a new connection
|
||||
const totalConnections = this.availableConnections.length + this.activeConnections.size;
|
||||
if (totalConnections < this.options.maxSize) {
|
||||
try {
|
||||
const newConnection = await this.createConnection();
|
||||
return this.checkoutConnection(newConnection);
|
||||
} catch (err) {
|
||||
// Fall through to queue if creation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Add to wait queue
|
||||
return this.queueAcquireRequest(priority, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to acquire an available connection
|
||||
*/
|
||||
private async tryAcquireConnection(): Promise<IPooledConnection<T> | null> {
|
||||
while (this.availableConnections.length > 0) {
|
||||
const connection = this.availableConnections.shift()!;
|
||||
|
||||
// Check if connection exceeded max use count
|
||||
if (connection.useCount >= this.options.maxUseCount) {
|
||||
await this.destroyConnection(connection);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate connection if required
|
||||
if (this.options.validateOnAcquire && this.options.connectionValidator) {
|
||||
try {
|
||||
const isValid = await this.options.connectionValidator(connection.connection);
|
||||
if (!isValid) {
|
||||
this.metrics.validationFailures++;
|
||||
await this.destroyConnection(connection);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
this.metrics.validationFailures++;
|
||||
await this.destroyConnection(connection);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return this.checkoutConnection(connection);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout a connection for use
|
||||
*/
|
||||
private checkoutConnection(connection: IPooledConnection<T>): IPooledConnection<T> {
|
||||
connection.inUse = true;
|
||||
connection.lastUsedAt = Date.now();
|
||||
connection.useCount++;
|
||||
|
||||
this.activeConnections.set(connection.id, connection);
|
||||
this.metrics.connectionsAcquired++;
|
||||
|
||||
this.eventEmitter.emit('acquire', connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an acquire request
|
||||
*/
|
||||
private queueAcquireRequest(priority: number, timeout?: number): Promise<IPooledConnection<T>> {
|
||||
return new Promise<IPooledConnection<T>>((resolve, reject) => {
|
||||
const request: IAcquireRequest<T> = {
|
||||
id: `req-${this.requestIdCounter++}`,
|
||||
priority,
|
||||
timestamp: Date.now(),
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
|
||||
// Set timeout
|
||||
const timeoutMs = timeout || this.options.queueTimeout;
|
||||
request.timeoutHandle = this.setTimeout(() => {
|
||||
if (this.waitQueue.extractByKey(request.id)) {
|
||||
this.metrics.acquireTimeouts++;
|
||||
reject(new Error(`Connection acquire timeout after ${timeoutMs}ms`));
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
this.waitQueue.insert(request);
|
||||
this.metrics.queueHighWaterMark = Math.max(
|
||||
this.metrics.queueHighWaterMark,
|
||||
this.waitQueue.size
|
||||
);
|
||||
|
||||
this.eventEmitter.emit('enqueue', { queueSize: this.waitQueue.size });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a connection back to the pool
|
||||
*/
|
||||
public async release(connection: IPooledConnection<T>): Promise<void> {
|
||||
return this.mutex.runExclusive(async () => {
|
||||
if (!connection.inUse || !this.activeConnections.has(connection.id)) {
|
||||
throw new Error('Connection is not active');
|
||||
}
|
||||
|
||||
this.activeConnections.delete(connection.id);
|
||||
connection.inUse = false;
|
||||
connection.lastUsedAt = Date.now();
|
||||
this.metrics.connectionsReleased++;
|
||||
|
||||
// Check if connection should be destroyed
|
||||
if (connection.useCount >= this.options.maxUseCount) {
|
||||
await this.destroyConnection(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate on return if required
|
||||
if (this.options.validateOnReturn && this.options.connectionValidator) {
|
||||
try {
|
||||
const isValid = await this.options.connectionValidator(connection.connection);
|
||||
if (!isValid) {
|
||||
await this.destroyConnection(connection);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await this.destroyConnection(connection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are waiting requests
|
||||
const request = this.waitQueue.extract();
|
||||
if (request) {
|
||||
this.clearTimeout(request.timeoutHandle!);
|
||||
request.resolve(this.checkoutConnection(connection));
|
||||
this.eventEmitter.emit('dequeue', { queueSize: this.waitQueue.size });
|
||||
} else {
|
||||
// Return to available pool
|
||||
this.availableConnections.push(connection);
|
||||
this.eventEmitter.emit('release', connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connection
|
||||
*/
|
||||
private async createConnection(): Promise<IPooledConnection<T>> {
|
||||
const rawConnection = await this.options.connectionFactory();
|
||||
|
||||
const connection: IPooledConnection<T> = {
|
||||
id: `conn-${this.connectionIdCounter++}`,
|
||||
connection: rawConnection,
|
||||
createdAt: Date.now(),
|
||||
lastUsedAt: Date.now(),
|
||||
useCount: 0,
|
||||
inUse: false,
|
||||
};
|
||||
|
||||
this.metrics.connectionsCreated++;
|
||||
this.eventEmitter.emit('create', connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a connection
|
||||
*/
|
||||
private async destroyConnection(connection: IPooledConnection<T>): Promise<void> {
|
||||
try {
|
||||
if (this.options.connectionDestroyer) {
|
||||
await this.options.connectionDestroyer(connection.connection);
|
||||
}
|
||||
|
||||
this.metrics.connectionsDestroyed++;
|
||||
this.eventEmitter.emit('destroy', connection);
|
||||
} catch (err) {
|
||||
if (this.options.onConnectionError) {
|
||||
this.options.onConnectionError(err as Error, connection.connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current pool statistics
|
||||
*/
|
||||
public getStats() {
|
||||
return {
|
||||
available: this.availableConnections.length,
|
||||
active: this.activeConnections.size,
|
||||
waiting: this.waitQueue.size,
|
||||
total: this.availableConnections.length + this.activeConnections.size,
|
||||
...this.metrics,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to pool events
|
||||
*/
|
||||
public on(event: string, listener: Function): void {
|
||||
this.addEventListener(this.eventEmitter, event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the pool and cleanup resources
|
||||
*/
|
||||
protected async onCleanup(): Promise<void> {
|
||||
this.isClosing = true;
|
||||
|
||||
// Clear the wait queue
|
||||
while (!this.waitQueue.isEmpty()) {
|
||||
const request = this.waitQueue.extract();
|
||||
if (request) {
|
||||
this.clearTimeout(request.timeoutHandle!);
|
||||
request.reject(new Error('Connection pool is closing'));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for active connections to be released (with timeout)
|
||||
const timeout = 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Destroy all connections
|
||||
const allConnections = [
|
||||
...this.availableConnections,
|
||||
...this.activeConnections.values(),
|
||||
];
|
||||
|
||||
await Promise.all(allConnections.map(conn => this.destroyConnection(conn)));
|
||||
|
||||
this.availableConnections.length = 0;
|
||||
this.activeConnections.clear();
|
||||
}
|
||||
}
|
@ -1,376 +0,0 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type {
|
||||
ICertificateData,
|
||||
ICertificateFailure,
|
||||
ICertificateExpiring
|
||||
} from '../models/common-types.js';
|
||||
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
||||
import { Port80HandlerEvents } from '../models/common-types.js';
|
||||
|
||||
/**
|
||||
* Standardized event names used throughout the system
|
||||
*/
|
||||
export enum ProxyEvents {
|
||||
// Certificate events
|
||||
CERTIFICATE_ISSUED = 'certificate:issued',
|
||||
CERTIFICATE_RENEWED = 'certificate:renewed',
|
||||
CERTIFICATE_FAILED = 'certificate:failed',
|
||||
CERTIFICATE_EXPIRING = 'certificate:expiring',
|
||||
|
||||
// Component lifecycle events
|
||||
COMPONENT_STARTED = 'component:started',
|
||||
COMPONENT_STOPPED = 'component:stopped',
|
||||
|
||||
// Connection events
|
||||
CONNECTION_ESTABLISHED = 'connection:established',
|
||||
CONNECTION_CLOSED = 'connection:closed',
|
||||
CONNECTION_ERROR = 'connection:error',
|
||||
|
||||
// Request events
|
||||
REQUEST_RECEIVED = 'request:received',
|
||||
REQUEST_COMPLETED = 'request:completed',
|
||||
REQUEST_ERROR = 'request:error',
|
||||
|
||||
// Route events
|
||||
ROUTE_MATCHED = 'route:matched',
|
||||
ROUTE_UPDATED = 'route:updated',
|
||||
ROUTE_ERROR = 'route:error',
|
||||
|
||||
// Security events
|
||||
SECURITY_BLOCKED = 'security:blocked',
|
||||
SECURITY_BREACH_ATTEMPT = 'security:breach-attempt',
|
||||
|
||||
// TLS events
|
||||
TLS_HANDSHAKE_STARTED = 'tls:handshake-started',
|
||||
TLS_HANDSHAKE_COMPLETED = 'tls:handshake-completed',
|
||||
TLS_HANDSHAKE_FAILED = 'tls:handshake-failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Component types for event metadata
|
||||
*/
|
||||
export enum ComponentType {
|
||||
SMART_PROXY = 'smart-proxy',
|
||||
NETWORK_PROXY = 'network-proxy',
|
||||
NFTABLES_PROXY = 'nftables-proxy',
|
||||
PORT80_HANDLER = 'port80-handler',
|
||||
CERTIFICATE_MANAGER = 'certificate-manager',
|
||||
ROUTE_MANAGER = 'route-manager',
|
||||
CONNECTION_MANAGER = 'connection-manager',
|
||||
TLS_MANAGER = 'tls-manager',
|
||||
SECURITY_MANAGER = 'security-manager'
|
||||
}
|
||||
|
||||
/**
|
||||
* Base event data interface
|
||||
*/
|
||||
export interface IEventData {
|
||||
timestamp: number;
|
||||
componentType: ComponentType;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Certificate event data
|
||||
*/
|
||||
export interface ICertificateEventData extends IEventData, ICertificateData {
|
||||
isRenewal?: boolean;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Certificate failure event data
|
||||
*/
|
||||
export interface ICertificateFailureEventData extends IEventData, ICertificateFailure {}
|
||||
|
||||
/**
|
||||
* Certificate expiring event data
|
||||
*/
|
||||
export interface ICertificateExpiringEventData extends IEventData, ICertificateExpiring {}
|
||||
|
||||
/**
|
||||
* Component lifecycle event data
|
||||
*/
|
||||
export interface IComponentEventData extends IEventData {
|
||||
name: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection event data
|
||||
*/
|
||||
export interface IConnectionEventData extends IEventData {
|
||||
connectionId: string;
|
||||
clientIp: string;
|
||||
serverIp?: string;
|
||||
port: number;
|
||||
isTls?: boolean;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request event data
|
||||
*/
|
||||
export interface IRequestEventData extends IEventData {
|
||||
connectionId: string;
|
||||
requestId: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
statusCode?: number;
|
||||
duration?: number;
|
||||
routeId?: string;
|
||||
routeName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route event data
|
||||
*/
|
||||
export interface IRouteEventData extends IEventData {
|
||||
route: IRouteConfig;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Security event data
|
||||
*/
|
||||
export interface ISecurityEventData extends IEventData {
|
||||
clientIp: string;
|
||||
reason: string;
|
||||
routeId?: string;
|
||||
routeName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS event data
|
||||
*/
|
||||
export interface ITlsEventData extends IEventData {
|
||||
connectionId: string;
|
||||
domain?: string;
|
||||
clientIp: string;
|
||||
tlsVersion?: string;
|
||||
cipherSuite?: string;
|
||||
sniHostname?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger interface for event system
|
||||
*/
|
||||
export interface IEventLogger {
|
||||
info: (message: string, ...args: any[]) => void;
|
||||
warn: (message: string, ...args: any[]) => void;
|
||||
error: (message: string, ...args: any[]) => void;
|
||||
debug?: (message: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler type
|
||||
*/
|
||||
export type EventHandler<T> = (data: T) => void;
|
||||
|
||||
/**
|
||||
* Helper class to standardize event emission and handling
|
||||
* across all system components
|
||||
*/
|
||||
export class EventSystem {
|
||||
private emitter: plugins.EventEmitter;
|
||||
private componentType: ComponentType;
|
||||
private componentId: string;
|
||||
private logger?: IEventLogger;
|
||||
|
||||
constructor(
|
||||
componentType: ComponentType,
|
||||
componentId: string = '',
|
||||
logger?: IEventLogger
|
||||
) {
|
||||
this.emitter = new plugins.EventEmitter();
|
||||
this.componentType = componentType;
|
||||
this.componentId = componentId;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a certificate issued event
|
||||
*/
|
||||
public emitCertificateIssued(data: Omit<ICertificateEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: ICertificateEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.info?.(`Certificate issued for ${data.domain}`);
|
||||
this.emitter.emit(ProxyEvents.CERTIFICATE_ISSUED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a certificate renewed event
|
||||
*/
|
||||
public emitCertificateRenewed(data: Omit<ICertificateEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: ICertificateEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.info?.(`Certificate renewed for ${data.domain}`);
|
||||
this.emitter.emit(ProxyEvents.CERTIFICATE_RENEWED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a certificate failed event
|
||||
*/
|
||||
public emitCertificateFailed(data: Omit<ICertificateFailureEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: ICertificateFailureEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.error?.(`Certificate issuance failed for ${data.domain}: ${data.error}`);
|
||||
this.emitter.emit(ProxyEvents.CERTIFICATE_FAILED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a certificate expiring event
|
||||
*/
|
||||
public emitCertificateExpiring(data: Omit<ICertificateExpiringEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: ICertificateExpiringEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.warn?.(`Certificate expiring for ${data.domain} in ${data.daysRemaining} days`);
|
||||
this.emitter.emit(ProxyEvents.CERTIFICATE_EXPIRING, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a component started event
|
||||
*/
|
||||
public emitComponentStarted(name: string, version?: string): void {
|
||||
const eventData: IComponentEventData = {
|
||||
name,
|
||||
version,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.info?.(`Component ${name} started${version ? ` (v${version})` : ''}`);
|
||||
this.emitter.emit(ProxyEvents.COMPONENT_STARTED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a component stopped event
|
||||
*/
|
||||
public emitComponentStopped(name: string): void {
|
||||
const eventData: IComponentEventData = {
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.info?.(`Component ${name} stopped`);
|
||||
this.emitter.emit(ProxyEvents.COMPONENT_STOPPED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a connection established event
|
||||
*/
|
||||
public emitConnectionEstablished(data: Omit<IConnectionEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: IConnectionEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.debug?.(`Connection ${data.connectionId} established from ${data.clientIp} on port ${data.port}`);
|
||||
this.emitter.emit(ProxyEvents.CONNECTION_ESTABLISHED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a connection closed event
|
||||
*/
|
||||
public emitConnectionClosed(data: Omit<IConnectionEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: IConnectionEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.debug?.(`Connection ${data.connectionId} closed`);
|
||||
this.emitter.emit(ProxyEvents.CONNECTION_CLOSED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a route matched event
|
||||
*/
|
||||
public emitRouteMatched(data: Omit<IRouteEventData, 'timestamp' | 'componentType' | 'componentId'>): void {
|
||||
const eventData: IRouteEventData = {
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
componentType: this.componentType,
|
||||
componentId: this.componentId
|
||||
};
|
||||
|
||||
this.logger?.debug?.(`Route matched: ${data.route.name || data.route.id || 'unnamed'}`);
|
||||
this.emitter.emit(ProxyEvents.ROUTE_MATCHED, eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
public on<T>(event: ProxyEvents, handler: EventHandler<T>): void {
|
||||
this.emitter.on(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event once
|
||||
*/
|
||||
public once<T>(event: ProxyEvents, handler: EventHandler<T>): void {
|
||||
this.emitter.once(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
public off<T>(event: ProxyEvents, handler: EventHandler<T>): void {
|
||||
this.emitter.off(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Port80Handler events to standard proxy events
|
||||
*/
|
||||
public subscribePort80HandlerEvents(handler: any): void {
|
||||
handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
|
||||
this.emitCertificateIssued({
|
||||
...data,
|
||||
isRenewal: false,
|
||||
source: 'port80handler'
|
||||
});
|
||||
});
|
||||
|
||||
handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
|
||||
this.emitCertificateRenewed({
|
||||
...data,
|
||||
isRenewal: true,
|
||||
source: 'port80handler'
|
||||
});
|
||||
});
|
||||
|
||||
handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (data: ICertificateFailure) => {
|
||||
this.emitCertificateFailed(data);
|
||||
});
|
||||
|
||||
handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data: ICertificateExpiring) => {
|
||||
this.emitCertificateExpiring(data);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Port80Handler has been removed - use SmartCertManager instead
|
||||
import { Port80HandlerEvents } from '../models/common-types.js';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export { Port80HandlerEvents };
|
||||
|
||||
/**
|
||||
* @deprecated Use SmartCertManager instead
|
||||
*/
|
||||
export interface IPort80HandlerSubscribers {
|
||||
onCertificateIssued?: (data: any) => void;
|
||||
onCertificateRenewed?: (data: any) => void;
|
||||
onCertificateFailed?: (data: any) => void;
|
||||
onCertificateExpiring?: (data: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use SmartCertManager instead
|
||||
*/
|
||||
export function subscribeToPort80Handler(
|
||||
handler: any,
|
||||
subscribers: IPort80HandlerSubscribers
|
||||
): void {
|
||||
console.warn('subscribeToPort80Handler is deprecated - use SmartCertManager instead');
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
* Core utility functions
|
||||
*/
|
||||
|
||||
export * from './event-utils.js';
|
||||
export * from './validation-utils.js';
|
||||
export * from './ip-utils.js';
|
||||
export * from './template-utils.js';
|
||||
@ -10,8 +9,10 @@ export * from './route-manager.js';
|
||||
export * from './route-utils.js';
|
||||
export * from './security-utils.js';
|
||||
export * from './shared-security-manager.js';
|
||||
export * from './event-system.js';
|
||||
export * from './websocket-utils.js';
|
||||
export * from './logger.js';
|
||||
export * from './async-utils.js';
|
||||
export * from './fs-utils.js';
|
||||
export * from './lifecycle-component.js';
|
||||
export * from './binary-heap.js';
|
||||
export * from './enhanced-connection-pool.js';
|
||||
|
213
ts/core/utils/lifecycle-component.ts
Normal file
213
ts/core/utils/lifecycle-component.ts
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Base class for components that need proper resource lifecycle management
|
||||
* Provides automatic cleanup of timers and event listeners to prevent memory leaks
|
||||
*/
|
||||
export abstract class LifecycleComponent {
|
||||
private timers: Set<NodeJS.Timeout> = new Set();
|
||||
private intervals: Set<NodeJS.Timeout> = new Set();
|
||||
private listeners: Array<{
|
||||
target: any;
|
||||
event: string;
|
||||
handler: Function;
|
||||
once?: boolean;
|
||||
}> = [];
|
||||
private childComponents: Set<LifecycleComponent> = new Set();
|
||||
protected isShuttingDown = false;
|
||||
private cleanupPromise?: Promise<void>;
|
||||
|
||||
/**
|
||||
* Create a managed setTimeout that will be automatically cleaned up
|
||||
*/
|
||||
protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
|
||||
if (this.isShuttingDown) {
|
||||
// Return a dummy timer if shutting down
|
||||
return setTimeout(() => {}, 0);
|
||||
}
|
||||
|
||||
const wrappedHandler = () => {
|
||||
this.timers.delete(timer);
|
||||
if (!this.isShuttingDown) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(wrappedHandler, timeout);
|
||||
this.timers.add(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a managed setInterval that will be automatically cleaned up
|
||||
*/
|
||||
protected setInterval(handler: Function, interval: number): NodeJS.Timeout {
|
||||
if (this.isShuttingDown) {
|
||||
// Return a dummy timer if shutting down
|
||||
return setInterval(() => {}, interval);
|
||||
}
|
||||
|
||||
const wrappedHandler = () => {
|
||||
if (!this.isShuttingDown) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setInterval(wrappedHandler, interval);
|
||||
this.intervals.add(timer);
|
||||
|
||||
// Allow process to exit even with timer
|
||||
if (typeof timer.unref === 'function') {
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a managed timeout
|
||||
*/
|
||||
protected clearTimeout(timer: NodeJS.Timeout): void {
|
||||
clearTimeout(timer);
|
||||
this.timers.delete(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a managed interval
|
||||
*/
|
||||
protected clearInterval(timer: NodeJS.Timeout): void {
|
||||
clearInterval(timer);
|
||||
this.intervals.delete(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a managed event listener that will be automatically removed on cleanup
|
||||
*/
|
||||
protected addEventListener(
|
||||
target: any,
|
||||
event: string,
|
||||
handler: Function,
|
||||
options?: { once?: boolean }
|
||||
): void {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both EventEmitter and DOM-style event targets
|
||||
if (typeof target.on === 'function') {
|
||||
if (options?.once) {
|
||||
target.once(event, handler);
|
||||
} else {
|
||||
target.on(event, handler);
|
||||
}
|
||||
} else if (typeof target.addEventListener === 'function') {
|
||||
target.addEventListener(event, handler, options);
|
||||
} else {
|
||||
throw new Error('Target must support on() or addEventListener()');
|
||||
}
|
||||
|
||||
this.listeners.push({
|
||||
target,
|
||||
event,
|
||||
handler,
|
||||
once: options?.once
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific event listener
|
||||
*/
|
||||
protected removeEventListener(target: any, event: string, handler: Function): void {
|
||||
// Remove from target
|
||||
if (typeof target.removeListener === 'function') {
|
||||
target.removeListener(event, handler);
|
||||
} else if (typeof target.removeEventListener === 'function') {
|
||||
target.removeEventListener(event, handler);
|
||||
}
|
||||
|
||||
// Remove from our tracking
|
||||
const index = this.listeners.findIndex(
|
||||
l => l.target === target && l.event === event && l.handler === handler
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a child component that should be cleaned up when this component is cleaned up
|
||||
*/
|
||||
protected registerChildComponent(component: LifecycleComponent): void {
|
||||
this.childComponents.add(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a child component
|
||||
*/
|
||||
protected unregisterChildComponent(component: LifecycleComponent): void {
|
||||
this.childComponents.delete(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to implement component-specific cleanup logic
|
||||
*/
|
||||
protected async onCleanup(): Promise<void> {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all managed resources
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
// Return existing cleanup promise if already cleaning up
|
||||
if (this.cleanupPromise) {
|
||||
return this.cleanupPromise;
|
||||
}
|
||||
|
||||
this.cleanupPromise = this.performCleanup();
|
||||
return this.cleanupPromise;
|
||||
}
|
||||
|
||||
private async performCleanup(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// First, clean up child components
|
||||
const childCleanupPromises: Promise<void>[] = [];
|
||||
for (const child of this.childComponents) {
|
||||
childCleanupPromises.push(child.cleanup());
|
||||
}
|
||||
await Promise.all(childCleanupPromises);
|
||||
this.childComponents.clear();
|
||||
|
||||
// Clear all timers
|
||||
for (const timer of this.timers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.timers.clear();
|
||||
|
||||
// Clear all intervals
|
||||
for (const timer of this.intervals) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
this.intervals.clear();
|
||||
|
||||
// Remove all event listeners
|
||||
for (const { target, event, handler } of this.listeners) {
|
||||
// All listeners need to be removed, including 'once' listeners that might not have fired
|
||||
if (typeof target.removeListener === 'function') {
|
||||
target.removeListener(event, handler);
|
||||
} else if (typeof target.removeEventListener === 'function') {
|
||||
target.removeEventListener(event, handler);
|
||||
}
|
||||
}
|
||||
this.listeners = [];
|
||||
|
||||
// Call subclass cleanup
|
||||
await this.onCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the component is shutting down
|
||||
*/
|
||||
protected isShuttingDownState(): boolean {
|
||||
return this.isShuttingDown;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user