import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { SmartProxy } from '../ts/index.js'; import * as plugins from '../ts/plugins.js'; tap.test('keepalive support - verify keepalive connections are properly handled', async (tools) => { console.log('\n=== KeepAlive Support Test ==='); console.log('Purpose: Verify that keepalive connections are not prematurely cleaned up'); // Create a simple echo backend const echoBackend = net.createServer((socket) => { socket.on('data', (data) => { // Echo back received data try { socket.write(data); } catch (err) { // Ignore write errors during shutdown } }); socket.on('error', (err) => { // Ignore errors from backend sockets console.log(`Backend socket error (expected during cleanup): ${err.code}`); }); }); await new Promise((resolve) => { echoBackend.listen(9998, () => { console.log('✓ Echo backend started on port 9998'); resolve(); }); }); // Test 1: Standard keepalive treatment console.log('\n--- Test 1: Standard KeepAlive Treatment ---'); const proxy1 = new SmartProxy({ routes: [{ name: 'keepalive-route', match: { ports: 8590 }, action: { type: 'forward', target: { host: 'localhost', port: 9998 } } }], keepAlive: true, keepAliveTreatment: 'standard', inactivityTimeout: 5000, // 5 seconds for faster testing enableDetailedLogging: false, }); await proxy1.start(); console.log('✓ Proxy with standard keepalive started on port 8590'); // Create a keepalive connection const client1 = net.connect(8590, 'localhost'); // Add error handler to prevent unhandled errors client1.on('error', (err) => { console.log(`Client1 error (expected during cleanup): ${err.code}`); }); await new Promise((resolve) => { client1.on('connect', () => { console.log('Client connected'); client1.setKeepAlive(true, 1000); resolve(); }); }); // Send initial data client1.write('Hello keepalive\n'); // Wait for echo await new Promise((resolve) => { client1.once('data', (data) => { console.log(`Received echo: ${data.toString().trim()}`); resolve(); }); }); // Check connection is marked as keepalive const cm1 = (proxy1 as any).connectionManager; const connections1 = cm1.getConnections(); let keepAliveCount = 0; for (const [id, record] of connections1) { if (record.hasKeepAlive) { keepAliveCount++; console.log(`KeepAlive connection ${id}: hasKeepAlive=${record.hasKeepAlive}`); } } expect(keepAliveCount).toEqual(1); // Wait to ensure it's not cleaned up prematurely await plugins.smartdelay.delayFor(6000); const afterWaitCount1 = cm1.getConnectionCount(); console.log(`Connections after 6s wait: ${afterWaitCount1}`); expect(afterWaitCount1).toEqual(1); // Should still be connected // Send more data to keep it alive client1.write('Still alive\n'); // Clean up test 1 client1.destroy(); await proxy1.stop(); await plugins.smartdelay.delayFor(500); // Wait for port to be released // Test 2: Extended keepalive treatment console.log('\n--- Test 2: Extended KeepAlive Treatment ---'); const proxy2 = new SmartProxy({ routes: [{ name: 'keepalive-extended', match: { ports: 8591 }, action: { type: 'forward', target: { host: 'localhost', port: 9998 } } }], keepAlive: true, keepAliveTreatment: 'extended', keepAliveInactivityMultiplier: 6, inactivityTimeout: 2000, // 2 seconds base, 12 seconds with multiplier enableDetailedLogging: false, }); await proxy2.start(); console.log('✓ Proxy with extended keepalive started on port 8591'); const client2 = net.connect(8591, 'localhost'); // Add error handler to prevent unhandled errors client2.on('error', (err) => { console.log(`Client2 error (expected during cleanup): ${err.code}`); }); await new Promise((resolve) => { client2.on('connect', () => { console.log('Client connected with extended timeout'); client2.setKeepAlive(true, 1000); resolve(); }); }); // Send initial data client2.write('Extended keepalive\n'); // Check connection const cm2 = (proxy2 as any).connectionManager; await plugins.smartdelay.delayFor(1000); const connections2 = cm2.getConnections(); for (const [id, record] of connections2) { console.log(`Extended connection ${id}: hasKeepAlive=${record.hasKeepAlive}, treatment=extended`); } // Wait 3 seconds (would timeout with standard treatment) await plugins.smartdelay.delayFor(3000); const midWaitCount = cm2.getConnectionCount(); console.log(`Connections after 3s (base timeout exceeded): ${midWaitCount}`); expect(midWaitCount).toEqual(1); // Should still be connected due to extended treatment // Clean up test 2 client2.destroy(); await proxy2.stop(); await plugins.smartdelay.delayFor(500); // Wait for port to be released // Test 3: Immortal keepalive treatment console.log('\n--- Test 3: Immortal KeepAlive Treatment ---'); const proxy3 = new SmartProxy({ routes: [{ name: 'keepalive-immortal', match: { ports: 8592 }, action: { type: 'forward', target: { host: 'localhost', port: 9998 } } }], keepAlive: true, keepAliveTreatment: 'immortal', inactivityTimeout: 1000, // 1 second - should be ignored for immortal enableDetailedLogging: false, }); await proxy3.start(); console.log('✓ Proxy with immortal keepalive started on port 8592'); const client3 = net.connect(8592, 'localhost'); // Add error handler to prevent unhandled errors client3.on('error', (err) => { console.log(`Client3 error (expected during cleanup): ${err.code}`); }); await new Promise((resolve) => { client3.on('connect', () => { console.log('Client connected with immortal treatment'); client3.setKeepAlive(true, 1000); resolve(); }); }); // Send initial data client3.write('Immortal connection\n'); // Wait well beyond normal timeout await plugins.smartdelay.delayFor(5000); const cm3 = (proxy3 as any).connectionManager; const immortalCount = cm3.getConnectionCount(); console.log(`Immortal connections after 5s inactivity: ${immortalCount}`); expect(immortalCount).toEqual(1); // Should never timeout // Verify zombie detection doesn't affect immortal connections console.log('\n--- Verifying zombie detection respects keepalive ---'); // Manually trigger inactivity check cm3.performOptimizedInactivityCheck(); await plugins.smartdelay.delayFor(1000); const afterCheckCount = cm3.getConnectionCount(); console.log(`Connections after manual inactivity check: ${afterCheckCount}`); expect(afterCheckCount).toEqual(1); // Should still be alive // Clean up client3.destroy(); await proxy3.stop(); // Close backend and wait for it to fully close await new Promise((resolve) => { echoBackend.close(() => { console.log('Echo backend closed'); resolve(); }); }); console.log('\n✓ All keepalive tests passed:'); console.log(' - Standard treatment works correctly'); console.log(' - Extended treatment applies multiplier'); console.log(' - Immortal treatment never times out'); console.log(' - Zombie detection respects keepalive settings'); }); tap.start();