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
This commit is contained in:
Philipp Kunz 2025-06-01 08:09:29 +00:00
parent cacc88797a
commit 726d40b9a5
4 changed files with 36 additions and 11 deletions

View File

@ -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"
}

View File

@ -249,4 +249,4 @@ tap.test('should not create timers when shutting down', async () => {
expect(intervalFired).toBeFalse();
});
tap.start();
export default tap.start();

View File

@ -403,7 +403,12 @@ export class EnhancedConnectionPool<T> 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

View File

@ -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<LifecycleComponent> = 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 = [];