portok 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/portokd.mjs ADDED
@@ -0,0 +1,793 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Portok Daemon - Zero-downtime deployment proxy
5
+ * Routes traffic through a stable port to internal app instances
6
+ * with health-gated switching, connection draining, and auto-rollback.
7
+ *
8
+ * Performance optimizations:
9
+ * - FAST_PATH mode for maximum throughput in benchmarks
10
+ * - Keep-alive agent for upstream connections (critical for throughput)
11
+ * - No URL parsing in hot path
12
+ * - Minimal event listeners per request
13
+ * - Connection header stripping for proper keep-alive upstream
14
+ */
15
+
16
+ import http from 'node:http';
17
+ import fs from 'node:fs';
18
+ import crypto from 'node:crypto';
19
+ import httpProxy from 'http-proxy';
20
+
21
+ // =============================================================================
22
+ // Configuration
23
+ // =============================================================================
24
+
25
+ // Instance name for multi-instance deployments
26
+ const instanceName = process.env.INSTANCE_NAME || 'default';
27
+
28
+ // Derive default state file from instance name if not explicitly set
29
+ const defaultStateFile = process.env.STATE_FILE
30
+ || (instanceName !== 'default' ? `/var/lib/portok/${instanceName}.json` : './portok-state.json');
31
+
32
+ const config = {
33
+ instanceName,
34
+ listenPort: parseInt(process.env.LISTEN_PORT || '3000', 10),
35
+ initialTargetPort: parseInt(process.env.INITIAL_TARGET_PORT || '0', 10),
36
+ stateFile: defaultStateFile,
37
+ healthPath: process.env.HEALTH_PATH || '/health',
38
+ healthTimeoutMs: parseInt(process.env.HEALTH_TIMEOUT_MS || '5000', 10),
39
+ drainMs: parseInt(process.env.DRAIN_MS || '30000', 10),
40
+ rollbackWindowMs: parseInt(process.env.ROLLBACK_WINDOW_MS || '60000', 10),
41
+ rollbackCheckEveryMs: parseInt(process.env.ROLLBACK_CHECK_EVERY_MS || '5000', 10),
42
+ rollbackFailThreshold: parseInt(process.env.ROLLBACK_FAIL_THRESHOLD || '3', 10),
43
+ adminToken: process.env.ADMIN_TOKEN || '',
44
+ adminAllowlist: (process.env.ADMIN_ALLOWLIST || '127.0.0.1,::1,::ffff:127.0.0.1').split(',').map(s => s.trim()),
45
+ adminUnixSocket: process.env.ADMIN_UNIX_SOCKET || '',
46
+
47
+ // Performance tuning: Upstream keep-alive settings
48
+ // CRITICAL: Without keep-alive, every request opens a new TCP connection
49
+ upstreamKeepAlive: process.env.UPSTREAM_KEEPALIVE !== '0',
50
+ upstreamMaxSockets: parseInt(process.env.UPSTREAM_MAX_SOCKETS || '1024', 10),
51
+ upstreamKeepAliveMsecs: parseInt(process.env.UPSTREAM_KEEPALIVE_MSECS || '1000', 10),
52
+
53
+ // Server timeout settings
54
+ serverKeepAliveTimeout: parseInt(process.env.SERVER_KEEPALIVE_TIMEOUT || '5000', 10),
55
+ serverHeadersTimeout: parseInt(process.env.SERVER_HEADERS_TIMEOUT || '6000', 10),
56
+
57
+ // Proxy settings
58
+ enableXfwd: process.env.ENABLE_XFWD !== '0', // Default ON for standard proxy behavior
59
+
60
+ // FAST_PATH mode: Maximum performance for benchmarks
61
+ // Disables: statusCounters, rolling RPS, lastProxyError capture
62
+ // Keeps: totalRequests, inflight, proxyErrors
63
+ fastPath: process.env.FAST_PATH === '1',
64
+
65
+ // Debug mode: Track upstream socket creation
66
+ debugUpstream: process.env.DEBUG_UPSTREAM === '1',
67
+
68
+ // Error handling
69
+ verboseErrors: process.env.VERBOSE_ERRORS === '1',
70
+ };
71
+
72
+ // Logging helper - only for admin/startup, NEVER in hot path
73
+ function log(category, message) {
74
+ const prefix = config.instanceName !== 'default' ? `[${config.instanceName}]` : '';
75
+ console.log(`${prefix}[${category}] ${message}`);
76
+ }
77
+
78
+ function logError(category, message) {
79
+ const prefix = config.instanceName !== 'default' ? `[${config.instanceName}]` : '';
80
+ console.error(`${prefix}[${category}] ${message}`);
81
+ }
82
+
83
+ // Validate required config
84
+ if (!config.initialTargetPort && !fs.existsSync(config.stateFile)) {
85
+ logError('config', 'INITIAL_TARGET_PORT is required when no state file exists');
86
+ process.exit(1);
87
+ }
88
+
89
+ if (!config.adminToken) {
90
+ logError('config', 'ADMIN_TOKEN is required');
91
+ process.exit(1);
92
+ }
93
+
94
+ // =============================================================================
95
+ // State Management
96
+ // =============================================================================
97
+
98
+ const state = {
99
+ activePort: config.initialTargetPort,
100
+ previousPort: null,
101
+ drainUntil: null,
102
+ lastSwitch: {
103
+ from: null,
104
+ to: null,
105
+ at: null,
106
+ reason: null,
107
+ id: null,
108
+ },
109
+ };
110
+
111
+ // Load state from file if exists
112
+ function loadState() {
113
+ try {
114
+ if (fs.existsSync(config.stateFile)) {
115
+ const data = JSON.parse(fs.readFileSync(config.stateFile, 'utf-8'));
116
+ state.activePort = data.activePort || config.initialTargetPort;
117
+ state.previousPort = data.previousPort || null;
118
+ state.lastSwitch = data.lastSwitch || state.lastSwitch;
119
+ log('state', `Loaded state from ${config.stateFile}, activePort=${state.activePort}`);
120
+ }
121
+ } catch (err) {
122
+ logError('state', `Failed to load state file: ${err.message}`);
123
+ }
124
+ }
125
+
126
+ // Save state to file atomically
127
+ function saveState() {
128
+ try {
129
+ const tempFile = `${config.stateFile}.tmp`;
130
+ const data = JSON.stringify({
131
+ activePort: state.activePort,
132
+ previousPort: state.previousPort,
133
+ lastSwitch: state.lastSwitch,
134
+ }, null, 2);
135
+ fs.writeFileSync(tempFile, data, 'utf-8');
136
+ fs.renameSync(tempFile, config.stateFile);
137
+ log('state', `Saved state to ${config.stateFile}`);
138
+ } catch (err) {
139
+ logError('state', `Failed to save state: ${err.message}`);
140
+ }
141
+ }
142
+
143
+ loadState();
144
+
145
+ // =============================================================================
146
+ // Metrics
147
+ // =============================================================================
148
+
149
+ const metrics = {
150
+ startedAt: Date.now(),
151
+ inflight: 0,
152
+ inflightMax: 0,
153
+ totalRequests: 0,
154
+ totalProxyErrors: 0,
155
+ // These are only updated when NOT in FAST_PATH mode
156
+ statusCounters: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
157
+ requestTimestamps: [], // Only used when NOT in FAST_PATH
158
+ health: {
159
+ activePortOk: true,
160
+ lastCheckedAt: null,
161
+ consecutiveFails: 0,
162
+ },
163
+ lastProxyError: null,
164
+ // Debug: upstream socket tracking
165
+ upstreamSocketsCreated: 0,
166
+ };
167
+
168
+ // Rolling RPS calculation - ONLY called from /__metrics endpoint, not hot path
169
+ function getRollingRps60() {
170
+ if (config.fastPath) return 0;
171
+ const now = Date.now();
172
+ const cutoff = now - 60000;
173
+ let count = 0;
174
+ for (let i = metrics.requestTimestamps.length - 1; i >= 0; i--) {
175
+ if (metrics.requestTimestamps[i] >= cutoff) count++;
176
+ else break;
177
+ }
178
+ return Math.round((count / 60) * 100) / 100;
179
+ }
180
+
181
+ // Record request timestamp - uses push only, cleanup done periodically
182
+ function recordRequest() {
183
+ metrics.requestTimestamps.push(Date.now());
184
+ }
185
+
186
+ // Periodic cleanup of old timestamps (every 10 seconds)
187
+ if (!config.fastPath) {
188
+ setInterval(() => {
189
+ const cutoff = Date.now() - 60000;
190
+ // Find first index >= cutoff using binary-ish scan from start
191
+ let removeCount = 0;
192
+ while (removeCount < metrics.requestTimestamps.length &&
193
+ metrics.requestTimestamps[removeCount] < cutoff) {
194
+ removeCount++;
195
+ }
196
+ if (removeCount > 0) {
197
+ metrics.requestTimestamps.splice(0, removeCount);
198
+ }
199
+ }, 10000);
200
+ }
201
+
202
+ function incrementStatusCounter(statusCode) {
203
+ if (statusCode >= 200 && statusCode < 300) metrics.statusCounters['2xx']++;
204
+ else if (statusCode >= 300 && statusCode < 400) metrics.statusCounters['3xx']++;
205
+ else if (statusCode >= 400 && statusCode < 500) metrics.statusCounters['4xx']++;
206
+ else if (statusCode >= 500) metrics.statusCounters['5xx']++;
207
+ }
208
+
209
+ // =============================================================================
210
+ // Socket Tracking for Connection Draining
211
+ // =============================================================================
212
+
213
+ // Map<Socket, number> - just store the port number for speed
214
+ const socketPortMap = new Map();
215
+
216
+ function getSocketPort(socket) {
217
+ const port = socketPortMap.get(socket);
218
+ return port !== undefined ? port : state.activePort;
219
+ }
220
+
221
+ // =============================================================================
222
+ // Upstream Keep-Alive Agent
223
+ // =============================================================================
224
+
225
+ // CRITICAL for performance: Shared HTTP agent for upstream connections
226
+ const upstreamAgent = new http.Agent({
227
+ keepAlive: config.upstreamKeepAlive,
228
+ maxSockets: config.upstreamMaxSockets,
229
+ keepAliveMsecs: config.upstreamKeepAliveMsecs,
230
+ scheduling: 'fifo', // Better for keep-alive reuse
231
+ });
232
+
233
+ // Debug: Track socket creation
234
+ if (config.debugUpstream) {
235
+ const originalCreateConnection = upstreamAgent.createConnection.bind(upstreamAgent);
236
+ upstreamAgent.createConnection = function(options, callback) {
237
+ metrics.upstreamSocketsCreated++;
238
+ return originalCreateConnection(options, callback);
239
+ };
240
+ }
241
+
242
+ // =============================================================================
243
+ // Health Check
244
+ // =============================================================================
245
+
246
+ async function checkHealth(port, timeout = config.healthTimeoutMs) {
247
+ return new Promise((resolve) => {
248
+ const req = http.request({
249
+ hostname: '127.0.0.1',
250
+ port,
251
+ path: config.healthPath,
252
+ method: 'GET',
253
+ timeout,
254
+ }, (res) => {
255
+ // Consume response to free socket
256
+ res.resume();
257
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
258
+ });
259
+
260
+ req.on('error', () => resolve(false));
261
+ req.on('timeout', () => {
262
+ req.destroy();
263
+ resolve(false);
264
+ });
265
+ req.end();
266
+ });
267
+ }
268
+
269
+ // =============================================================================
270
+ // Auto Rollback Monitor
271
+ // =============================================================================
272
+
273
+ let rollbackInterval = null;
274
+
275
+ function startRollbackMonitor(newPort, previousPort, isRollback = false) {
276
+ if (isRollback) {
277
+ log('rollback', 'Skipping rollback monitor for rollback operation');
278
+ return;
279
+ }
280
+
281
+ if (rollbackInterval) {
282
+ clearInterval(rollbackInterval);
283
+ rollbackInterval = null;
284
+ }
285
+
286
+ let consecutiveFails = 0;
287
+ const endTime = Date.now() + config.rollbackWindowMs;
288
+
289
+ log('rollback', `Starting monitor for port ${newPort}, window=${config.rollbackWindowMs}ms`);
290
+
291
+ rollbackInterval = setInterval(async () => {
292
+ if (Date.now() > endTime) {
293
+ log('rollback', 'Monitor window expired, port is stable');
294
+ clearInterval(rollbackInterval);
295
+ rollbackInterval = null;
296
+ return;
297
+ }
298
+
299
+ if (state.activePort !== newPort) {
300
+ log('rollback', 'Port changed externally, stopping monitor');
301
+ clearInterval(rollbackInterval);
302
+ rollbackInterval = null;
303
+ return;
304
+ }
305
+
306
+ const healthy = await checkHealth(newPort);
307
+ metrics.health.lastCheckedAt = Date.now();
308
+
309
+ if (!healthy) {
310
+ consecutiveFails++;
311
+ metrics.health.consecutiveFails = consecutiveFails;
312
+ metrics.health.activePortOk = false;
313
+ log('rollback', `Health check failed (${consecutiveFails}/${config.rollbackFailThreshold})`);
314
+
315
+ if (consecutiveFails >= config.rollbackFailThreshold) {
316
+ log('rollback', `Threshold reached, rolling back to port ${previousPort}`);
317
+ clearInterval(rollbackInterval);
318
+ rollbackInterval = null;
319
+ await performSwitch(previousPort, 'auto-rollback', true);
320
+ }
321
+ } else {
322
+ consecutiveFails = 0;
323
+ metrics.health.consecutiveFails = 0;
324
+ metrics.health.activePortOk = true;
325
+ }
326
+ }, config.rollbackCheckEveryMs);
327
+ }
328
+
329
+ // =============================================================================
330
+ // Switching Logic
331
+ // =============================================================================
332
+
333
+ async function performSwitch(newPort, reason = 'manual', isRollback = false) {
334
+ const previousPort = state.activePort;
335
+
336
+ state.previousPort = previousPort;
337
+ state.activePort = newPort;
338
+ state.drainUntil = Date.now() + config.drainMs;
339
+ state.lastSwitch = {
340
+ from: previousPort,
341
+ to: newPort,
342
+ at: Date.now(),
343
+ reason,
344
+ id: crypto.randomUUID(),
345
+ };
346
+
347
+ saveState();
348
+
349
+ log('switch', `Switched from port ${previousPort} to ${newPort} (reason: ${reason})`);
350
+
351
+ startRollbackMonitor(newPort, previousPort, isRollback);
352
+
353
+ setTimeout(() => {
354
+ if (state.drainUntil && state.drainUntil <= Date.now()) {
355
+ state.drainUntil = null;
356
+ log('drain', 'Drain period ended');
357
+ }
358
+ }, config.drainMs);
359
+
360
+ return state.lastSwitch;
361
+ }
362
+
363
+ // =============================================================================
364
+ // Security: Token Validation & Rate Limiting
365
+ // =============================================================================
366
+
367
+ function timingSafeEqual(a, b) {
368
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
369
+ const bufA = Buffer.from(a);
370
+ const bufB = Buffer.from(b);
371
+ if (bufA.length !== bufB.length) {
372
+ crypto.timingSafeEqual(bufA, bufA);
373
+ return false;
374
+ }
375
+ return crypto.timingSafeEqual(bufA, bufB);
376
+ }
377
+
378
+ function validateAdminToken(req) {
379
+ const token = req.headers['x-admin-token'];
380
+ return timingSafeEqual(token || '', config.adminToken);
381
+ }
382
+
383
+ function isAllowedIP(req) {
384
+ const remoteAddr = req.socket.remoteAddress || '';
385
+ return config.adminAllowlist.some(allowed => {
386
+ if (remoteAddr === `::ffff:${allowed}`) return true;
387
+ return remoteAddr === allowed;
388
+ });
389
+ }
390
+
391
+ const rateLimitMap = new Map();
392
+ const RATE_LIMIT_MAX = parseInt(process.env.ADMIN_RATE_LIMIT || '10', 10);
393
+ const RATE_LIMIT_WINDOW = 60000;
394
+
395
+ function checkRateLimit(ip) {
396
+ const now = Date.now();
397
+ const cutoff = now - RATE_LIMIT_WINDOW;
398
+
399
+ let timestamps = rateLimitMap.get(ip) || [];
400
+ timestamps = timestamps.filter(t => t > cutoff);
401
+
402
+ if (timestamps.length >= RATE_LIMIT_MAX) {
403
+ rateLimitMap.set(ip, timestamps);
404
+ return false;
405
+ }
406
+
407
+ timestamps.push(now);
408
+ rateLimitMap.set(ip, timestamps);
409
+ return true;
410
+ }
411
+
412
+ setInterval(() => {
413
+ const now = Date.now();
414
+ const cutoff = now - RATE_LIMIT_WINDOW;
415
+ for (const [ip, timestamps] of rateLimitMap.entries()) {
416
+ const filtered = timestamps.filter(t => t > cutoff);
417
+ if (filtered.length === 0) {
418
+ rateLimitMap.delete(ip);
419
+ } else {
420
+ rateLimitMap.set(ip, filtered);
421
+ }
422
+ }
423
+ }, 60000);
424
+
425
+ // =============================================================================
426
+ // Admin Endpoints
427
+ // =============================================================================
428
+
429
+ function sendJSON(res, statusCode, data) {
430
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
431
+ res.end(JSON.stringify(data));
432
+ }
433
+
434
+ // Extract pathname without URL object
435
+ function getPathname(url) {
436
+ const qIdx = url.indexOf('?');
437
+ return qIdx === -1 ? url : url.slice(0, qIdx);
438
+ }
439
+
440
+ // Extract query param without URL parsing
441
+ function getQueryParam(url, param) {
442
+ const qIdx = url.indexOf('?');
443
+ if (qIdx === -1) return null;
444
+ const query = url.slice(qIdx + 1);
445
+ const regex = new RegExp(`(?:^|&)${param}=([^&]*)`);
446
+ const match = query.match(regex);
447
+ return match ? match[1] : null;
448
+ }
449
+
450
+ async function handleAdminRequest(req, res) {
451
+ const pathname = getPathname(req.url);
452
+
453
+ if (!isAllowedIP(req)) {
454
+ sendJSON(res, 403, { error: 'Forbidden: IP not allowed' });
455
+ return true;
456
+ }
457
+
458
+ if (!validateAdminToken(req)) {
459
+ sendJSON(res, 401, { error: 'Unauthorized: Invalid or missing x-admin-token' });
460
+ return true;
461
+ }
462
+
463
+ const clientIP = req.socket.remoteAddress || 'unknown';
464
+ if (!checkRateLimit(clientIP)) {
465
+ sendJSON(res, 429, { error: 'Too Many Requests: Rate limit exceeded' });
466
+ return true;
467
+ }
468
+
469
+ if (pathname === '/__status' && req.method === 'GET') {
470
+ sendJSON(res, 200, {
471
+ instanceName: config.instanceName,
472
+ activePort: state.activePort,
473
+ drainUntil: state.drainUntil,
474
+ lastSwitch: state.lastSwitch,
475
+ });
476
+ return true;
477
+ }
478
+
479
+ if (pathname === '/__metrics' && req.method === 'GET') {
480
+ const metricsData = {
481
+ startedAt: new Date(metrics.startedAt).toISOString(),
482
+ inflight: metrics.inflight,
483
+ inflightMax: metrics.inflightMax,
484
+ totalRequests: metrics.totalRequests,
485
+ totalProxyErrors: metrics.totalProxyErrors,
486
+ statusCounters: metrics.statusCounters,
487
+ rollingRps60: getRollingRps60(),
488
+ health: {
489
+ ...metrics.health,
490
+ lastCheckedAt: metrics.health.lastCheckedAt
491
+ ? new Date(metrics.health.lastCheckedAt).toISOString()
492
+ : null,
493
+ },
494
+ lastProxyError: metrics.lastProxyError,
495
+ fastPath: config.fastPath,
496
+ };
497
+
498
+ // Include upstream debug info if enabled
499
+ if (config.debugUpstream) {
500
+ metricsData.upstreamSocketsCreated = metrics.upstreamSocketsCreated;
501
+ metricsData.upstreamAgentSockets = {
502
+ freeSockets: Object.keys(upstreamAgent.freeSockets).length,
503
+ sockets: Object.keys(upstreamAgent.sockets).length,
504
+ };
505
+ }
506
+
507
+ sendJSON(res, 200, metricsData);
508
+ return true;
509
+ }
510
+
511
+ if (pathname === '/__switch' && req.method === 'POST') {
512
+ const portStr = getQueryParam(req.url, 'port');
513
+ const port = parseInt(portStr, 10);
514
+
515
+ if (!port || port < 1 || port > 65535) {
516
+ sendJSON(res, 400, { error: 'Invalid port: must be 1-65535' });
517
+ return true;
518
+ }
519
+
520
+ log('switch', `Health checking port ${port}...`);
521
+ const healthy = await checkHealth(port);
522
+
523
+ if (!healthy) {
524
+ sendJSON(res, 409, {
525
+ error: 'Health check failed',
526
+ message: `Port ${port} did not respond with 2xx at ${config.healthPath}`,
527
+ });
528
+ return true;
529
+ }
530
+
531
+ const result = await performSwitch(port, 'manual', false);
532
+ sendJSON(res, 200, {
533
+ success: true,
534
+ message: `Switched to port ${port}`,
535
+ switch: result,
536
+ });
537
+ return true;
538
+ }
539
+
540
+ if (pathname === '/__health' && req.method === 'GET') {
541
+ const healthy = await checkHealth(state.activePort);
542
+ metrics.health.activePortOk = healthy;
543
+ metrics.health.lastCheckedAt = Date.now();
544
+
545
+ if (healthy) {
546
+ sendJSON(res, 200, {
547
+ healthy: true,
548
+ activePort: state.activePort,
549
+ checkedAt: new Date(metrics.health.lastCheckedAt).toISOString(),
550
+ });
551
+ } else {
552
+ sendJSON(res, 503, {
553
+ healthy: false,
554
+ activePort: state.activePort,
555
+ checkedAt: new Date(metrics.health.lastCheckedAt).toISOString(),
556
+ });
557
+ }
558
+ return true;
559
+ }
560
+
561
+ return false;
562
+ }
563
+
564
+ // =============================================================================
565
+ // Proxy Server
566
+ // =============================================================================
567
+
568
+ const proxy = httpProxy.createProxyServer({
569
+ ws: true,
570
+ xfwd: config.enableXfwd,
571
+ // These are critical for performance:
572
+ changeOrigin: false, // Not needed for localhost
573
+ selfHandleResponse: false, // Let http-proxy handle response
574
+ followRedirects: false, // Don't follow redirects
575
+ });
576
+
577
+ // CRITICAL: Remove connection header to ensure upstream keep-alive works
578
+ // Without this, "connection: close" from client breaks upstream reuse
579
+ proxy.on('proxyReq', (proxyReq, req, res, options) => {
580
+ // Remove connection header - let agent manage keep-alive
581
+ proxyReq.removeHeader('connection');
582
+ // Ensure we don't forward hop-by-hop headers that break keep-alive
583
+ proxyReq.removeHeader('proxy-connection');
584
+ });
585
+
586
+ // Proxy error handler - minimal work in hot path
587
+ proxy.on('error', (err, req, res) => {
588
+ metrics.totalProxyErrors++;
589
+
590
+ // Only capture error details if NOT in FAST_PATH
591
+ if (!config.fastPath) {
592
+ metrics.lastProxyError = {
593
+ message: err.message,
594
+ timestamp: Date.now(),
595
+ targetPort: state.activePort,
596
+ };
597
+ }
598
+
599
+ // Only log if verbose enabled
600
+ if (config.verboseErrors) {
601
+ logError('proxy', `${err.message}\n${err.stack}`);
602
+ }
603
+
604
+ // Fast 502 response
605
+ if (res && !res.headersSent && typeof res.writeHead === 'function') {
606
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
607
+ res.end('Bad Gateway');
608
+ }
609
+ });
610
+
611
+ // Track response status codes - ONLY if NOT in FAST_PATH mode
612
+ if (!config.fastPath) {
613
+ proxy.on('proxyRes', (proxyRes) => {
614
+ incrementStatusCounter(proxyRes.statusCode);
615
+ });
616
+ }
617
+
618
+ // =============================================================================
619
+ // HTTP Request Handler - ULTRA OPTIMIZED HOT PATH
620
+ // =============================================================================
621
+
622
+ // Pre-calculate admin prefix bytes for fastest check
623
+ const SLASH = 47; // '/'
624
+ const UNDERSCORE = 95; // '_'
625
+
626
+ function handleRequest(req, res) {
627
+ const url = req.url;
628
+
629
+ // Fast admin check: is url starting with '/__' ?
630
+ if (url && url.length > 2 &&
631
+ url.charCodeAt(0) === SLASH &&
632
+ url.charCodeAt(1) === UNDERSCORE &&
633
+ url.charCodeAt(2) === UNDERSCORE) {
634
+ // Admin path - async handling
635
+ handleAdminRequest(req, res).then(handled => {
636
+ if (!handled) {
637
+ proxyRequestFast(req, res);
638
+ }
639
+ });
640
+ return;
641
+ }
642
+
643
+ // Hot path: proxy immediately
644
+ proxyRequestFast(req, res);
645
+ }
646
+
647
+ // Ultra-fast proxy function - minimal overhead
648
+ function proxyRequestFast(req, res) {
649
+ // Essential metrics only
650
+ metrics.totalRequests++;
651
+ metrics.inflight++;
652
+
653
+ // Track max inflight (cheap comparison)
654
+ if (metrics.inflight > metrics.inflightMax) {
655
+ metrics.inflightMax = metrics.inflight;
656
+ }
657
+
658
+ // Rolling RPS tracking - ONLY if not FAST_PATH
659
+ if (!config.fastPath) {
660
+ recordRequest();
661
+ }
662
+
663
+ // Get target port (for drain support)
664
+ const targetPort = getSocketPort(req.socket);
665
+
666
+ // Track inflight with protection against double-decrement
667
+ // Use a simple flag on the response object
668
+ res._inflightDecremented = false;
669
+
670
+ const onFinish = () => {
671
+ if (!res._inflightDecremented) {
672
+ res._inflightDecremented = true;
673
+ metrics.inflight--;
674
+ }
675
+ };
676
+
677
+ // Use 'once' to auto-remove listener after first call
678
+ res.once('finish', onFinish);
679
+ res.once('close', onFinish);
680
+
681
+ // Proxy with keep-alive agent
682
+ proxy.web(req, res, {
683
+ target: { host: '127.0.0.1', port: targetPort },
684
+ agent: upstreamAgent,
685
+ });
686
+ }
687
+
688
+ // WebSocket upgrade handler
689
+ function handleUpgrade(req, socket, head) {
690
+ const targetPort = getSocketPort(socket);
691
+
692
+ // No logging in hot path
693
+ proxy.ws(req, socket, head, {
694
+ target: { host: '127.0.0.1', port: targetPort },
695
+ agent: upstreamAgent,
696
+ });
697
+ }
698
+
699
+ // =============================================================================
700
+ // HTTP Server
701
+ // =============================================================================
702
+
703
+ const server = http.createServer(handleRequest);
704
+ server.on('upgrade', handleUpgrade);
705
+
706
+ // Configure server for high concurrency
707
+ server.keepAliveTimeout = config.serverKeepAliveTimeout;
708
+ server.headersTimeout = config.serverHeadersTimeout;
709
+ server.maxHeadersCount = 100; // Reasonable limit
710
+
711
+ // Track socket connections for draining - minimal overhead
712
+ server.on('connection', (socket) => {
713
+ socketPortMap.set(socket, state.activePort);
714
+ socket.once('close', () => socketPortMap.delete(socket));
715
+ });
716
+
717
+ // =============================================================================
718
+ // Server Startup
719
+ // =============================================================================
720
+
721
+ server.listen(config.listenPort, () => {
722
+ log('portokd', `Listening on port ${config.listenPort}`);
723
+ log('portokd', `Instance: ${config.instanceName}`);
724
+ log('portokd', `Proxying to 127.0.0.1:${state.activePort}`);
725
+ log('portokd', `Health path: ${config.healthPath}`);
726
+ log('portokd', `State file: ${config.stateFile}`);
727
+ log('portokd', `Admin endpoints: /__status, /__metrics, /__switch, /__health`);
728
+ log('portokd', `Keep-alive: ${config.upstreamKeepAlive ? 'enabled' : 'disabled'}, maxSockets=${config.upstreamMaxSockets}`);
729
+ if (config.fastPath) {
730
+ log('portokd', 'FAST_PATH mode: Minimal metrics for maximum throughput');
731
+ }
732
+ if (config.debugUpstream) {
733
+ log('portokd', 'DEBUG_UPSTREAM mode: Tracking upstream socket creation');
734
+ }
735
+ });
736
+
737
+ // Optional: Admin Unix socket
738
+ if (config.adminUnixSocket) {
739
+ try {
740
+ fs.unlinkSync(config.adminUnixSocket);
741
+ } catch (e) {
742
+ // Ignore if doesn't exist
743
+ }
744
+
745
+ const adminServer = http.createServer(async (req, res) => {
746
+ req.socket.remoteAddress = '127.0.0.1';
747
+ if (req.url && req.url.charCodeAt(0) === SLASH &&
748
+ req.url.charCodeAt(1) === UNDERSCORE &&
749
+ req.url.charCodeAt(2) === UNDERSCORE) {
750
+ await handleAdminRequest(req, res);
751
+ } else {
752
+ sendJSON(res, 404, { error: 'Not found' });
753
+ }
754
+ });
755
+
756
+ adminServer.listen(config.adminUnixSocket, () => {
757
+ log('portokd', `Admin socket listening on ${config.adminUnixSocket}`);
758
+ fs.chmodSync(config.adminUnixSocket, 0o600);
759
+ });
760
+ }
761
+
762
+ // Graceful shutdown
763
+ process.on('SIGTERM', () => {
764
+ log('portokd', 'Received SIGTERM, shutting down...');
765
+ upstreamAgent.destroy();
766
+ server.close(() => {
767
+ log('portokd', 'Server closed');
768
+ process.exit(0);
769
+ });
770
+ });
771
+
772
+ process.on('SIGINT', () => {
773
+ log('portokd', 'Received SIGINT, shutting down...');
774
+ upstreamAgent.destroy();
775
+ server.close(() => {
776
+ log('portokd', 'Server closed');
777
+ process.exit(0);
778
+ });
779
+ });
780
+
781
+ // Export for testing
782
+ export {
783
+ config,
784
+ state,
785
+ metrics,
786
+ checkHealth,
787
+ performSwitch,
788
+ validateAdminToken,
789
+ isAllowedIP,
790
+ checkRateLimit,
791
+ server,
792
+ upstreamAgent,
793
+ };