import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js'; /** * Manages timeouts and inactivity tracking for connections */ export class TimeoutManager { constructor(private settings: IPortProxySettings) {} /** * Ensure timeout values don't exceed Node.js max safe integer */ public ensureSafeTimeout(timeout: number): number { const MAX_SAFE_TIMEOUT = 2147483647; // Maximum safe value (2^31 - 1) return Math.min(Math.floor(timeout), MAX_SAFE_TIMEOUT); } /** * Generate a slightly randomized timeout to prevent thundering herd */ public randomizeTimeout(baseTimeout: number, variationPercent: number = 5): number { const safeBaseTimeout = this.ensureSafeTimeout(baseTimeout); const variation = safeBaseTimeout * (variationPercent / 100); return this.ensureSafeTimeout( safeBaseTimeout + Math.floor(Math.random() * variation * 2) - variation ); } /** * Update connection activity timestamp */ public updateActivity(record: IConnectionRecord): void { record.lastActivity = Date.now(); // Clear any inactivity warning if (record.inactivityWarningIssued) { record.inactivityWarningIssued = false; } } /** * Calculate effective inactivity timeout based on connection type */ public getEffectiveInactivityTimeout(record: IConnectionRecord): number { let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default // For immortal keep-alive connections, use an extremely long timeout if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { return Number.MAX_SAFE_INTEGER; } // For extended keep-alive connections, apply multiplier if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { const multiplier = this.settings.keepAliveInactivityMultiplier || 6; effectiveTimeout = effectiveTimeout * multiplier; } return this.ensureSafeTimeout(effectiveTimeout); } /** * Calculate effective max lifetime based on connection type */ public getEffectiveMaxLifetime(record: IConnectionRecord): number { // Use domain-specific timeout if available const baseTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime || 86400000; // 24 hours default // For immortal keep-alive connections, use an extremely long lifetime if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { return Number.MAX_SAFE_INTEGER; } // For extended keep-alive connections, use the extended lifetime setting if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { return this.ensureSafeTimeout( this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000 // 7 days default ); } // Apply randomization if enabled if (this.settings.enableRandomizedTimeouts) { return this.randomizeTimeout(baseTimeout); } return this.ensureSafeTimeout(baseTimeout); } /** * Setup connection timeout * @returns The cleanup timer */ public setupConnectionTimeout( record: IConnectionRecord, onTimeout: (record: IConnectionRecord, reason: string) => void ): NodeJS.Timeout { // Clear any existing timer if (record.cleanupTimer) { clearTimeout(record.cleanupTimer); } // Calculate effective timeout const effectiveLifetime = this.getEffectiveMaxLifetime(record); // Set up the timeout const timer = setTimeout(() => { // Call the provided callback onTimeout(record, 'connection_timeout'); }, effectiveLifetime); // Make sure timeout doesn't keep the process alive if (timer.unref) { timer.unref(); } return timer; } /** * Check for inactivity on a connection * @returns Object with check results */ public checkInactivity(record: IConnectionRecord): { isInactive: boolean; shouldWarn: boolean; inactivityTime: number; effectiveTimeout: number; } { // Skip for connections with inactivity check disabled if (this.settings.disableInactivityCheck) { return { isInactive: false, shouldWarn: false, inactivityTime: 0, effectiveTimeout: 0 }; } // Skip for immortal keep-alive connections if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { return { isInactive: false, shouldWarn: false, inactivityTime: 0, effectiveTimeout: 0 }; } const now = Date.now(); const inactivityTime = now - record.lastActivity; const effectiveTimeout = this.getEffectiveInactivityTimeout(record); // Check if inactive const isInactive = inactivityTime > effectiveTimeout; // For keep-alive connections, we should warn first const shouldWarn = record.hasKeepAlive && isInactive && !record.inactivityWarningIssued; return { isInactive, shouldWarn, inactivityTime, effectiveTimeout }; } /** * Apply socket timeout settings */ public applySocketTimeouts(record: IConnectionRecord): void { // Skip for immortal keep-alive connections if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { // Disable timeouts completely for immortal connections record.incoming.setTimeout(0); if (record.outgoing) { record.outgoing.setTimeout(0); } return; } // Apply normal timeouts const timeout = this.ensureSafeTimeout(this.settings.socketTimeout || 3600000); // 1 hour default record.incoming.setTimeout(timeout); if (record.outgoing) { record.outgoing.setTimeout(timeout); } } }