@push.rocks/smartproxy 19.5.3 → 19.5.5

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