@push.rocks/smartproxy 19.5.4 → 19.5.6

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.
Files changed (62) hide show
  1. package/dist_ts/core/utils/async-utils.d.ts +81 -0
  2. package/dist_ts/core/utils/async-utils.js +216 -0
  3. package/dist_ts/core/utils/binary-heap.d.ts +73 -0
  4. package/dist_ts/core/utils/binary-heap.js +193 -0
  5. package/dist_ts/core/utils/enhanced-connection-pool.d.ts +110 -0
  6. package/dist_ts/core/utils/enhanced-connection-pool.js +320 -0
  7. package/dist_ts/core/utils/fs-utils.d.ts +144 -0
  8. package/dist_ts/core/utils/fs-utils.js +252 -0
  9. package/dist_ts/core/utils/index.d.ts +6 -2
  10. package/dist_ts/core/utils/index.js +7 -3
  11. package/dist_ts/core/utils/lifecycle-component.d.ts +59 -0
  12. package/dist_ts/core/utils/lifecycle-component.js +195 -0
  13. package/dist_ts/core/utils/socket-utils.d.ts +28 -0
  14. package/dist_ts/core/utils/socket-utils.js +77 -0
  15. package/dist_ts/forwarding/handlers/http-handler.js +7 -4
  16. package/dist_ts/forwarding/handlers/https-passthrough-handler.js +14 -55
  17. package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +52 -40
  18. package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +31 -43
  19. package/dist_ts/plugins.d.ts +2 -1
  20. package/dist_ts/plugins.js +3 -2
  21. package/dist_ts/proxies/http-proxy/certificate-manager.d.ts +15 -0
  22. package/dist_ts/proxies/http-proxy/certificate-manager.js +49 -2
  23. package/dist_ts/proxies/http-proxy/connection-pool.js +4 -19
  24. package/dist_ts/proxies/http-proxy/http-proxy.js +3 -7
  25. package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +10 -0
  26. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +53 -43
  27. package/dist_ts/proxies/smart-proxy/cert-store.js +22 -20
  28. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +35 -9
  29. package/dist_ts/proxies/smart-proxy/connection-manager.js +243 -189
  30. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +13 -2
  31. package/dist_ts/proxies/smart-proxy/port-manager.js +3 -3
  32. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +35 -4
  33. package/package.json +2 -2
  34. package/readme.hints.md +96 -1
  35. package/readme.plan.md +1135 -221
  36. package/readme.problems.md +167 -83
  37. package/ts/core/utils/async-utils.ts +275 -0
  38. package/ts/core/utils/binary-heap.ts +225 -0
  39. package/ts/core/utils/enhanced-connection-pool.ts +420 -0
  40. package/ts/core/utils/fs-utils.ts +270 -0
  41. package/ts/core/utils/index.ts +6 -2
  42. package/ts/core/utils/lifecycle-component.ts +231 -0
  43. package/ts/core/utils/socket-utils.ts +96 -0
  44. package/ts/forwarding/handlers/http-handler.ts +7 -3
  45. package/ts/forwarding/handlers/https-passthrough-handler.ts +13 -62
  46. package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +58 -46
  47. package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +38 -53
  48. package/ts/plugins.ts +2 -1
  49. package/ts/proxies/http-proxy/certificate-manager.ts +52 -1
  50. package/ts/proxies/http-proxy/connection-pool.ts +3 -16
  51. package/ts/proxies/http-proxy/http-proxy.ts +2 -5
  52. package/ts/proxies/nftables-proxy/nftables-proxy.ts +64 -79
  53. package/ts/proxies/smart-proxy/cert-store.ts +26 -20
  54. package/ts/proxies/smart-proxy/connection-manager.ts +277 -197
  55. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +15 -1
  56. package/ts/proxies/smart-proxy/port-manager.ts +2 -2
  57. package/ts/proxies/smart-proxy/route-connection-handler.ts +39 -4
  58. package/readme.plan2.md +0 -764
  59. package/ts/common/eventUtils.ts +0 -34
  60. package/ts/common/types.ts +0 -91
  61. package/ts/core/utils/event-system.ts +0 -376
  62. package/ts/core/utils/event-utils.ts +0 -25
@@ -3,22 +3,45 @@ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.
3
3
  import { SecurityManager } from './security-manager.js';
4
4
  import { TimeoutManager } from './timeout-manager.js';
5
5
  import { logger } from '../../core/utils/logger.js';
6
+ import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
7
+ import { cleanupSocket } from '../../core/utils/socket-utils.js';
6
8
 
7
9
  /**
8
- * Manages connection lifecycle, tracking, and cleanup
10
+ * Manages connection lifecycle, tracking, and cleanup with performance optimizations
9
11
  */
10
- export class ConnectionManager {
12
+ export class ConnectionManager extends LifecycleComponent {
11
13
  private connectionRecords: Map<string, IConnectionRecord> = new Map();
12
14
  private terminationStats: {
13
15
  incoming: Record<string, number>;
14
16
  outgoing: Record<string, number>;
15
17
  } = { incoming: {}, outgoing: {} };
18
+
19
+ // Performance optimization: Track connections needing inactivity check
20
+ private nextInactivityCheck: Map<string, number> = new Map();
21
+
22
+ // Connection limits
23
+ private readonly maxConnections: number;
24
+ private readonly cleanupBatchSize: number = 100;
25
+
26
+ // Cleanup queue for batched processing
27
+ private cleanupQueue: Set<string> = new Set();
28
+ private cleanupTimer: NodeJS.Timeout | null = null;
16
29
 
17
30
  constructor(
18
31
  private settings: ISmartProxyOptions,
19
32
  private securityManager: SecurityManager,
20
33
  private timeoutManager: TimeoutManager
21
- ) {}
34
+ ) {
35
+ super();
36
+
37
+ // Set reasonable defaults for connection limits
38
+ this.maxConnections = settings.defaults?.security?.maxConnections || 10000;
39
+
40
+ // Start inactivity check timer if not disabled
41
+ if (!settings.disableInactivityCheck) {
42
+ this.startInactivityCheckTimer();
43
+ }
44
+ }
22
45
 
23
46
  /**
24
47
  * Generate a unique connection ID
@@ -31,17 +54,29 @@ export class ConnectionManager {
31
54
  /**
32
55
  * Create and track a new connection
33
56
  */
34
- public createConnection(socket: plugins.net.Socket): IConnectionRecord {
57
+ public createConnection(socket: plugins.net.Socket): IConnectionRecord | null {
58
+ // Enforce connection limit
59
+ if (this.connectionRecords.size >= this.maxConnections) {
60
+ logger.log('warn', `Connection limit reached (${this.maxConnections}). Rejecting new connection.`, {
61
+ currentConnections: this.connectionRecords.size,
62
+ maxConnections: this.maxConnections,
63
+ component: 'connection-manager'
64
+ });
65
+ socket.destroy();
66
+ return null;
67
+ }
68
+
35
69
  const connectionId = this.generateConnectionId();
36
70
  const remoteIP = socket.remoteAddress || '';
37
71
  const localPort = socket.localPort || 0;
72
+ const now = Date.now();
38
73
 
39
74
  const record: IConnectionRecord = {
40
75
  id: connectionId,
41
76
  incoming: socket,
42
77
  outgoing: null,
43
- incomingStartTime: Date.now(),
44
- lastActivity: Date.now(),
78
+ incomingStartTime: now,
79
+ lastActivity: now,
45
80
  connectionClosed: false,
46
81
  pendingData: [],
47
82
  pendingDataSize: 0,
@@ -70,6 +105,42 @@ export class ConnectionManager {
70
105
  public trackConnection(connectionId: string, record: IConnectionRecord): void {
71
106
  this.connectionRecords.set(connectionId, record);
72
107
  this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
108
+
109
+ // Schedule inactivity check
110
+ if (!this.settings.disableInactivityCheck) {
111
+ this.scheduleInactivityCheck(connectionId, record);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Schedule next inactivity check for a connection
117
+ */
118
+ private scheduleInactivityCheck(connectionId: string, record: IConnectionRecord): void {
119
+ let timeout = this.settings.inactivityTimeout!;
120
+
121
+ if (record.hasKeepAlive) {
122
+ if (this.settings.keepAliveTreatment === 'immortal') {
123
+ // Don't schedule check for immortal connections
124
+ return;
125
+ } else if (this.settings.keepAliveTreatment === 'extended') {
126
+ const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
127
+ timeout = timeout * multiplier;
128
+ }
129
+ }
130
+
131
+ const checkTime = Date.now() + timeout;
132
+ this.nextInactivityCheck.set(connectionId, checkTime);
133
+ }
134
+
135
+ /**
136
+ * Start the inactivity check timer
137
+ */
138
+ private startInactivityCheckTimer(): void {
139
+ // Check every 30 seconds for connections that need inactivity check
140
+ this.setInterval(() => {
141
+ this.performOptimizedInactivityCheck();
142
+ }, 30000);
143
+ // Note: LifecycleComponent's setInterval already calls unref()
73
144
  }
74
145
 
75
146
  /**
@@ -98,18 +169,65 @@ export class ConnectionManager {
98
169
  */
99
170
  public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
100
171
  if (this.settings.enableDetailedLogging) {
101
- logger.log('info', `Connection cleanup initiated`, { connectionId: record.id, remoteIP: record.remoteIP, reason, component: 'connection-manager' });
172
+ logger.log('info', `Connection cleanup initiated`, {
173
+ connectionId: record.id,
174
+ remoteIP: record.remoteIP,
175
+ reason,
176
+ component: 'connection-manager'
177
+ });
102
178
  }
103
179
 
104
- if (
105
- record.incomingTerminationReason === null ||
106
- record.incomingTerminationReason === undefined
107
- ) {
180
+ if (record.incomingTerminationReason == null) {
108
181
  record.incomingTerminationReason = reason;
109
182
  this.incrementTerminationStat('incoming', reason);
110
183
  }
111
184
 
112
- this.cleanupConnection(record, reason);
185
+ // Add to cleanup queue for batched processing
186
+ this.queueCleanup(record.id);
187
+ }
188
+
189
+ /**
190
+ * Queue a connection for cleanup
191
+ */
192
+ private queueCleanup(connectionId: string): void {
193
+ this.cleanupQueue.add(connectionId);
194
+
195
+ // Process immediately if queue is getting large
196
+ if (this.cleanupQueue.size >= this.cleanupBatchSize) {
197
+ this.processCleanupQueue();
198
+ } else if (!this.cleanupTimer) {
199
+ // Otherwise, schedule batch processing
200
+ this.cleanupTimer = this.setTimeout(() => {
201
+ this.processCleanupQueue();
202
+ }, 100);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Process the cleanup queue in batches
208
+ */
209
+ private processCleanupQueue(): void {
210
+ if (this.cleanupTimer) {
211
+ this.clearTimeout(this.cleanupTimer);
212
+ this.cleanupTimer = null;
213
+ }
214
+
215
+ const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
216
+ this.cleanupQueue.clear();
217
+
218
+ for (const connectionId of toCleanup) {
219
+ const record = this.connectionRecords.get(connectionId);
220
+ if (record) {
221
+ this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
222
+ }
223
+ }
224
+
225
+ // If there are more in queue, schedule next batch
226
+ if (this.cleanupQueue.size > 0) {
227
+ this.cleanupTimer = this.setTimeout(() => {
228
+ this.processCleanupQueue();
229
+ }, 10);
230
+ }
113
231
  }
114
232
 
115
233
  /**
@@ -119,6 +237,9 @@ export class ConnectionManager {
119
237
  if (!record.connectionClosed) {
120
238
  record.connectionClosed = true;
121
239
 
240
+ // Remove from inactivity check
241
+ this.nextInactivityCheck.delete(record.id);
242
+
122
243
  // Track connection termination
123
244
  this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
124
245
 
@@ -127,29 +248,41 @@ export class ConnectionManager {
127
248
  record.cleanupTimer = undefined;
128
249
  }
129
250
 
130
- // Detailed logging data
251
+ // Calculate metrics once
131
252
  const duration = Date.now() - record.incomingStartTime;
132
- const bytesReceived = record.bytesReceived;
133
- const bytesSent = record.bytesSent;
253
+ const logData = {
254
+ connectionId: record.id,
255
+ remoteIP: record.remoteIP,
256
+ localPort: record.localPort,
257
+ reason,
258
+ duration: plugins.prettyMs(duration),
259
+ bytes: { in: record.bytesReceived, out: record.bytesSent },
260
+ tls: record.isTLS,
261
+ keepAlive: record.hasKeepAlive,
262
+ usingNetworkProxy: record.usingNetworkProxy,
263
+ domainSwitches: record.domainSwitches || 0,
264
+ component: 'connection-manager'
265
+ };
134
266
 
135
267
  // Remove all data handlers to make sure we clean up properly
136
268
  if (record.incoming) {
137
269
  try {
138
- // Remove our safe data handler
139
270
  record.incoming.removeAllListeners('data');
140
- // Reset the handler references
141
271
  record.renegotiationHandler = undefined;
142
272
  } catch (err) {
143
- logger.log('error', `Error removing data handlers for connection ${record.id}: ${err}`, { connectionId: record.id, error: err, component: 'connection-manager' });
273
+ logger.log('error', `Error removing data handlers: ${err}`, {
274
+ connectionId: record.id,
275
+ error: err,
276
+ component: 'connection-manager'
277
+ });
144
278
  }
145
279
  }
146
280
 
147
- // Handle incoming socket
148
- this.cleanupSocket(record, 'incoming', record.incoming);
281
+ // Handle socket cleanup without delay
282
+ cleanupSocket(record.incoming, `${record.id}-incoming`);
149
283
 
150
- // Handle outgoing socket
151
284
  if (record.outgoing) {
152
- this.cleanupSocket(record, 'outgoing', record.outgoing);
285
+ cleanupSocket(record.outgoing, `${record.id}-outgoing`);
153
286
  }
154
287
 
155
288
  // Clear pendingData to avoid memory leaks
@@ -162,28 +295,13 @@ export class ConnectionManager {
162
295
  // Log connection details
163
296
  if (this.settings.enableDetailedLogging) {
164
297
  logger.log('info',
165
- `Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}). ` +
166
- `Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
167
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
168
- `${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
169
- `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`,
170
- {
171
- connectionId: record.id,
172
- remoteIP: record.remoteIP,
173
- localPort: record.localPort,
174
- reason,
175
- duration: plugins.prettyMs(duration),
176
- bytes: { in: bytesReceived, out: bytesSent },
177
- tls: record.isTLS,
178
- keepAlive: record.hasKeepAlive,
179
- usingNetworkProxy: record.usingNetworkProxy,
180
- domainSwitches: record.domainSwitches || 0,
181
- component: 'connection-manager'
182
- }
298
+ `Connection terminated: ${record.remoteIP}:${record.localPort} (${reason}) - ` +
299
+ `${plugins.prettyMs(duration)}, IN: ${record.bytesReceived}B, OUT: ${record.bytesSent}B`,
300
+ logData
183
301
  );
184
302
  } else {
185
303
  logger.log('info',
186
- `Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`,
304
+ `Connection terminated: ${record.remoteIP} (${reason}). Active: ${this.connectionRecords.size}`,
187
305
  {
188
306
  connectionId: record.id,
189
307
  remoteIP: record.remoteIP,
@@ -196,40 +314,6 @@ export class ConnectionManager {
196
314
  }
197
315
  }
198
316
 
199
- /**
200
- * Helper method to clean up a socket
201
- */
202
- private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
203
- try {
204
- if (!socket.destroyed) {
205
- // Try graceful shutdown first, then force destroy after a short timeout
206
- socket.end();
207
- const socketTimeout = setTimeout(() => {
208
- try {
209
- if (!socket.destroyed) {
210
- socket.destroy();
211
- }
212
- } catch (err) {
213
- logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
214
- }
215
- }, 1000);
216
-
217
- // Ensure the timeout doesn't block Node from exiting
218
- if (socketTimeout.unref) {
219
- socketTimeout.unref();
220
- }
221
- }
222
- } catch (err) {
223
- logger.log('error', `Error closing ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
224
- try {
225
- if (!socket.destroyed) {
226
- socket.destroy();
227
- }
228
- } catch (destroyErr) {
229
- logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${destroyErr}`, { connectionId: record.id, side, error: destroyErr, component: 'connection-manager' });
230
- }
231
- }
232
- }
233
317
 
234
318
  /**
235
319
  * Creates a generic error handler for incoming or outgoing sockets
@@ -238,49 +322,44 @@ export class ConnectionManager {
238
322
  return (err: Error) => {
239
323
  const code = (err as any).code;
240
324
  let reason = 'error';
241
-
325
+
242
326
  const now = Date.now();
243
327
  const connectionDuration = now - record.incomingStartTime;
244
328
  const lastActivityAge = now - record.lastActivity;
245
329
 
246
- if (code === 'ECONNRESET') {
247
- reason = 'econnreset';
248
- logger.log('warn', `ECONNRESET on ${side} connection from ${record.remoteIP}. Error: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
249
- connectionId: record.id,
250
- side,
251
- remoteIP: record.remoteIP,
252
- error: err.message,
253
- duration: plugins.prettyMs(connectionDuration),
254
- lastActivity: plugins.prettyMs(lastActivityAge),
255
- component: 'connection-manager'
256
- });
257
- } else if (code === 'ETIMEDOUT') {
258
- reason = 'etimedout';
259
- logger.log('warn', `ETIMEDOUT on ${side} connection from ${record.remoteIP}. Error: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
260
- connectionId: record.id,
261
- side,
262
- remoteIP: record.remoteIP,
263
- error: err.message,
264
- duration: plugins.prettyMs(connectionDuration),
265
- lastActivity: plugins.prettyMs(lastActivityAge),
266
- component: 'connection-manager'
267
- });
268
- } else {
269
- logger.log('error', `Error on ${side} connection from ${record.remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
270
- connectionId: record.id,
271
- side,
272
- remoteIP: record.remoteIP,
273
- error: err.message,
274
- duration: plugins.prettyMs(connectionDuration),
275
- lastActivity: plugins.prettyMs(lastActivityAge),
276
- component: 'connection-manager'
277
- });
330
+ // Update activity tracking
331
+ if (side === 'incoming') {
332
+ record.lastActivity = now;
333
+ this.scheduleInactivityCheck(record.id, record);
334
+ }
335
+
336
+ const errorData = {
337
+ connectionId: record.id,
338
+ side,
339
+ remoteIP: record.remoteIP,
340
+ error: err.message,
341
+ duration: plugins.prettyMs(connectionDuration),
342
+ lastActivity: plugins.prettyMs(lastActivityAge),
343
+ component: 'connection-manager'
344
+ };
345
+
346
+ switch (code) {
347
+ case 'ECONNRESET':
348
+ reason = 'econnreset';
349
+ logger.log('warn', `ECONNRESET on ${side}: ${record.remoteIP}`, errorData);
350
+ break;
351
+ case 'ETIMEDOUT':
352
+ reason = 'etimedout';
353
+ logger.log('warn', `ETIMEDOUT on ${side}: ${record.remoteIP}`, errorData);
354
+ break;
355
+ default:
356
+ logger.log('error', `Error on ${side}: ${record.remoteIP} - ${err.message}`, errorData);
278
357
  }
279
358
 
280
- if (side === 'incoming' && record.incomingTerminationReason === null) {
359
+ if (side === 'incoming' && record.incomingTerminationReason == null) {
281
360
  record.incomingTerminationReason = reason;
282
361
  this.incrementTerminationStat('incoming', reason);
283
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
362
+ } else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
284
363
  record.outgoingTerminationReason = reason;
285
364
  this.incrementTerminationStat('outgoing', reason);
286
365
  }
@@ -303,13 +382,12 @@ export class ConnectionManager {
303
382
  });
304
383
  }
305
384
 
306
- if (side === 'incoming' && record.incomingTerminationReason === null) {
385
+ if (side === 'incoming' && record.incomingTerminationReason == null) {
307
386
  record.incomingTerminationReason = 'normal';
308
387
  this.incrementTerminationStat('incoming', 'normal');
309
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
388
+ } else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
310
389
  record.outgoingTerminationReason = 'normal';
311
390
  this.incrementTerminationStat('outgoing', 'normal');
312
- // Record the time when outgoing socket closed.
313
391
  record.outgoingClosedTime = Date.now();
314
392
  }
315
393
 
@@ -332,26 +410,29 @@ export class ConnectionManager {
332
410
  }
333
411
 
334
412
  /**
335
- * Check for stalled/inactive connections
413
+ * Optimized inactivity check - only checks connections that are due
336
414
  */
337
- public performInactivityCheck(): void {
415
+ private performOptimizedInactivityCheck(): void {
338
416
  const now = Date.now();
339
- const connectionIds = [...this.connectionRecords.keys()];
417
+ const connectionsToCheck: string[] = [];
340
418
 
341
- for (const id of connectionIds) {
342
- const record = this.connectionRecords.get(id);
343
- if (!record) continue;
344
-
345
- // Skip inactivity check if disabled or for immortal keep-alive connections
346
- if (
347
- this.settings.disableInactivityCheck ||
348
- (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
349
- ) {
419
+ // Find connections that need checking
420
+ for (const [connectionId, checkTime] of this.nextInactivityCheck) {
421
+ if (checkTime <= now) {
422
+ connectionsToCheck.push(connectionId);
423
+ }
424
+ }
425
+
426
+ // Process only connections that need checking
427
+ for (const connectionId of connectionsToCheck) {
428
+ const record = this.connectionRecords.get(connectionId);
429
+ if (!record || record.connectionClosed) {
430
+ this.nextInactivityCheck.delete(connectionId);
350
431
  continue;
351
432
  }
352
433
 
353
434
  const inactivityTime = now - record.lastActivity;
354
-
435
+
355
436
  // Use extended timeout for extended-treatment keep-alive connections
356
437
  let effectiveTimeout = this.settings.inactivityTimeout!;
357
438
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
@@ -359,37 +440,37 @@ export class ConnectionManager {
359
440
  effectiveTimeout = effectiveTimeout * multiplier;
360
441
  }
361
442
 
362
- if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
443
+ if (inactivityTime > effectiveTimeout) {
363
444
  // For keep-alive connections, issue a warning first
364
445
  if (record.hasKeepAlive && !record.inactivityWarningIssued) {
365
- logger.log('warn', `Keep-alive connection ${id} from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. Will close in 10 minutes if no activity.`, {
366
- connectionId: id,
446
+ logger.log('warn', `Keep-alive connection inactive: ${record.remoteIP}`, {
447
+ connectionId,
367
448
  remoteIP: record.remoteIP,
368
449
  inactiveFor: plugins.prettyMs(inactivityTime),
369
- closureWarning: '10 minutes',
370
450
  component: 'connection-manager'
371
451
  });
372
452
 
373
- // Set warning flag and add grace period
374
453
  record.inactivityWarningIssued = true;
375
- record.lastActivity = now - (effectiveTimeout - 600000);
454
+
455
+ // Reschedule check for 10 minutes later
456
+ this.nextInactivityCheck.set(connectionId, now + 600000);
376
457
 
377
458
  // Try to stimulate activity with a probe packet
378
459
  if (record.outgoing && !record.outgoing.destroyed) {
379
460
  try {
380
461
  record.outgoing.write(Buffer.alloc(0));
381
-
382
- if (this.settings.enableDetailedLogging) {
383
- logger.log('info', `Sent probe packet to test keep-alive connection ${id}`, { connectionId: id, component: 'connection-manager' });
384
- }
385
462
  } catch (err) {
386
- logger.log('error', `Error sending probe packet to connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
463
+ logger.log('error', `Error sending probe packet: ${err}`, {
464
+ connectionId,
465
+ error: err,
466
+ component: 'connection-manager'
467
+ });
387
468
  }
388
469
  }
389
470
  } else {
390
- // For non-keep-alive or after warning, close the connection
391
- logger.log('warn', `Closing inactive connection ${id} from ${record.remoteIP} (inactive for ${plugins.prettyMs(inactivityTime)}, keep-alive: ${record.hasKeepAlive ? 'Yes' : 'No'})`, {
392
- connectionId: id,
471
+ // Close the connection
472
+ logger.log('warn', `Closing inactive connection: ${record.remoteIP}`, {
473
+ connectionId,
393
474
  remoteIP: record.remoteIP,
394
475
  inactiveFor: plugins.prettyMs(inactivityTime),
395
476
  hasKeepAlive: record.hasKeepAlive,
@@ -397,15 +478,9 @@ export class ConnectionManager {
397
478
  });
398
479
  this.cleanupConnection(record, 'inactivity');
399
480
  }
400
- } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
401
- // If activity detected after warning, clear the warning
402
- if (this.settings.enableDetailedLogging) {
403
- logger.log('info', `Connection ${id} activity detected after inactivity warning`, {
404
- connectionId: id,
405
- component: 'connection-manager'
406
- });
407
- }
408
- record.inactivityWarningIssued = false;
481
+ } else {
482
+ // Reschedule next check
483
+ this.scheduleInactivityCheck(connectionId, record);
409
484
  }
410
485
 
411
486
  // Parity check: if outgoing socket closed and incoming remains active
@@ -415,8 +490,8 @@ export class ConnectionManager {
415
490
  !record.connectionClosed &&
416
491
  now - record.outgoingClosedTime > 120000
417
492
  ) {
418
- logger.log('warn', `Parity check: Connection ${id} from ${record.remoteIP} has incoming socket still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing socket closed`, {
419
- connectionId: id,
493
+ logger.log('warn', `Parity check failed: ${record.remoteIP}`, {
494
+ connectionId,
420
495
  remoteIP: record.remoteIP,
421
496
  timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
422
497
  component: 'connection-manager'
@@ -426,68 +501,73 @@ export class ConnectionManager {
426
501
  }
427
502
  }
428
503
 
504
+ /**
505
+ * Legacy method for backward compatibility
506
+ */
507
+ public performInactivityCheck(): void {
508
+ this.performOptimizedInactivityCheck();
509
+ }
510
+
429
511
  /**
430
512
  * Clear all connections (for shutdown)
431
513
  */
432
- public clearConnections(): void {
433
- // Create a copy of the keys to avoid modification during iteration
434
- const connectionIds = [...this.connectionRecords.keys()];
514
+ public async clearConnections(): Promise<void> {
515
+ // Delegate to LifecycleComponent's cleanup
516
+ await this.cleanup();
517
+ }
518
+
519
+ /**
520
+ * Override LifecycleComponent's onCleanup method
521
+ */
522
+ protected async onCleanup(): Promise<void> {
435
523
 
436
- // First pass: End all connections gracefully
437
- for (const id of connectionIds) {
438
- const record = this.connectionRecords.get(id);
439
- if (record) {
524
+ // Process connections in batches to avoid blocking
525
+ const connections = Array.from(this.connectionRecords.values());
526
+ const batchSize = 100;
527
+ let index = 0;
528
+
529
+ const processBatch = () => {
530
+ const batch = connections.slice(index, index + batchSize);
531
+
532
+ for (const record of batch) {
440
533
  try {
441
- // Clear any timers
442
534
  if (record.cleanupTimer) {
443
535
  clearTimeout(record.cleanupTimer);
444
536
  record.cleanupTimer = undefined;
445
537
  }
446
538
 
447
- // End sockets gracefully
448
- if (record.incoming && !record.incoming.destroyed) {
449
- record.incoming.end();
539
+ // Immediate destruction using socket-utils
540
+ if (record.incoming) {
541
+ cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`);
450
542
  }
451
543
 
452
- if (record.outgoing && !record.outgoing.destroyed) {
453
- record.outgoing.end();
544
+ if (record.outgoing) {
545
+ cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`);
454
546
  }
455
547
  } catch (err) {
456
- logger.log('error', `Error during graceful end of connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
457
- }
458
- }
459
- }
460
-
461
- // Short delay to allow graceful ends to process
462
- setTimeout(() => {
463
- // Second pass: Force destroy everything
464
- for (const id of connectionIds) {
465
- const record = this.connectionRecords.get(id);
466
- if (record) {
467
- try {
468
- // Remove all listeners to prevent memory leaks
469
- if (record.incoming) {
470
- record.incoming.removeAllListeners();
471
- if (!record.incoming.destroyed) {
472
- record.incoming.destroy();
473
- }
474
- }
475
-
476
- if (record.outgoing) {
477
- record.outgoing.removeAllListeners();
478
- if (!record.outgoing.destroyed) {
479
- record.outgoing.destroy();
480
- }
481
- }
482
- } catch (err) {
483
- logger.log('error', `Error during forced destruction of connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
484
- }
548
+ logger.log('error', `Error during connection cleanup: ${err}`, {
549
+ connectionId: record.id,
550
+ error: err,
551
+ component: 'connection-manager'
552
+ });
485
553
  }
486
554
  }
487
555
 
488
- // Clear all maps
489
- this.connectionRecords.clear();
490
- this.terminationStats = { incoming: {}, outgoing: {} };
491
- }, 100);
556
+ index += batchSize;
557
+
558
+ // Continue with next batch if needed
559
+ if (index < connections.length) {
560
+ setImmediate(processBatch);
561
+ } else {
562
+ // Clear all maps
563
+ this.connectionRecords.clear();
564
+ this.nextInactivityCheck.clear();
565
+ this.cleanupQueue.clear();
566
+ this.terminationStats = { incoming: {}, outgoing: {} };
567
+ }
568
+ };
569
+
570
+ // Start batch processing
571
+ setImmediate(processBatch);
492
572
  }
493
573
  }