From 726d40b9a569586d1d12a652abb9bbf8c2e0e694 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sun, 1 Jun 2025 08:09:29 +0000 Subject: [PATCH] feat(lifecycle-component): enhance lifecycle management with unref support for timers and event listeners fix(lifecycle-component): store actual event handler for proper cleanup chore(meta): update certificate dates in meta.json --- certs/static-route/meta.json | 6 ++-- test/core/utils/test.lifecycle-component.ts | 2 +- ts/core/utils/enhanced-connection-pool.ts | 7 ++++- ts/core/utils/lifecycle-component.ts | 32 +++++++++++++++++---- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index dac82ca..602a705 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-08-29T18:29:48.329Z", - "issueDate": "2025-05-31T18:29:48.329Z", - "savedAt": "2025-05-31T18:29:48.330Z" + "expiryDate": "2025-08-30T08:04:36.897Z", + "issueDate": "2025-06-01T08:04:36.897Z", + "savedAt": "2025-06-01T08:04:36.897Z" } \ No newline at end of file diff --git a/test/core/utils/test.lifecycle-component.ts b/test/core/utils/test.lifecycle-component.ts index 3bccbdd..37c8604 100644 --- a/test/core/utils/test.lifecycle-component.ts +++ b/test/core/utils/test.lifecycle-component.ts @@ -249,4 +249,4 @@ tap.test('should not create timers when shutting down', async () => { expect(intervalFired).toBeFalse(); }); -tap.start(); \ No newline at end of file +export default tap.start(); \ No newline at end of file diff --git a/ts/core/utils/enhanced-connection-pool.ts b/ts/core/utils/enhanced-connection-pool.ts index 93cf328..260159c 100644 --- a/ts/core/utils/enhanced-connection-pool.ts +++ b/ts/core/utils/enhanced-connection-pool.ts @@ -403,7 +403,12 @@ export class EnhancedConnectionPool extends LifecycleComponent { const startTime = Date.now(); while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => { + const timer = setTimeout(resolve, 100); + if (typeof timer.unref === 'function') { + timer.unref(); + } + }); } // Destroy all connections diff --git a/ts/core/utils/lifecycle-component.ts b/ts/core/utils/lifecycle-component.ts index 0ee8b30..753dc6a 100644 --- a/ts/core/utils/lifecycle-component.ts +++ b/ts/core/utils/lifecycle-component.ts @@ -9,6 +9,7 @@ export abstract class LifecycleComponent { target: any; event: string; handler: Function; + actualHandler?: Function; // The actual handler registered (may be wrapped) once?: boolean; }> = []; private childComponents: Set = new Set(); @@ -21,7 +22,11 @@ export abstract class LifecycleComponent { protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout { if (this.isShuttingDown) { // Return a dummy timer if shutting down - return setTimeout(() => {}, 0); + const dummyTimer = setTimeout(() => {}, 0); + if (typeof dummyTimer.unref === 'function') { + dummyTimer.unref(); + } + return dummyTimer; } const wrappedHandler = () => { @@ -33,6 +38,12 @@ export abstract class LifecycleComponent { const timer = setTimeout(wrappedHandler, timeout); this.timers.add(timer); + + // Allow process to exit even with timer + if (typeof timer.unref === 'function') { + timer.unref(); + } + return timer; } @@ -42,7 +53,12 @@ export abstract class LifecycleComponent { protected setInterval(handler: Function, interval: number): NodeJS.Timeout { if (this.isShuttingDown) { // Return a dummy timer if shutting down - return setInterval(() => {}, interval); + const dummyTimer = setInterval(() => {}, interval); + if (typeof dummyTimer.unref === 'function') { + dummyTimer.unref(); + } + clearInterval(dummyTimer); // Clear immediately since we don't need it + return dummyTimer; } const wrappedHandler = () => { @@ -121,11 +137,12 @@ export abstract class LifecycleComponent { throw new Error('Target must support on() or addEventListener()'); } - // Store the original handler in our tracking (not the wrapped one) + // Store both the original handler and the actual handler registered this.listeners.push({ target, event, handler, + actualHandler, // The handler that was actually registered (may be wrapped) once: options?.once }); } @@ -208,12 +225,15 @@ export abstract class LifecycleComponent { this.intervals.clear(); // Remove all event listeners - for (const { target, event, handler } of this.listeners) { + for (const { target, event, handler, actualHandler } of this.listeners) { + // Use actualHandler if available (for wrapped handlers), otherwise use the original handler + const handlerToRemove = actualHandler || handler; + // All listeners need to be removed, including 'once' listeners that might not have fired if (typeof target.removeListener === 'function') { - target.removeListener(event, handler); + target.removeListener(event, handlerToRemove); } else if (typeof target.removeEventListener === 'function') { - target.removeEventListener(event, handler); + target.removeEventListener(event, handlerToRemove); } } this.listeners = [];