fix(cli): cleanup AbortSignal event listeners to avoid MaxListenersExceededWarning

This commit is contained in:
Joakim Carlstein 2024-02-12 20:56:02 +01:00 committed by Joakim Carlstein
parent ae9e8b1b04
commit 57a099169e
2 changed files with 40 additions and 16 deletions

View file

@ -0,0 +1,5 @@
---
'@emigrate/cli': patch
---
Cleanup AbortSignal listeners when they are not needed to avoid MaxListenersExceededWarning when migrating many migrations at once

View file

@ -28,6 +28,8 @@ export const exec = async <Return extends Promise<any>>(
const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined; const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined;
const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]); const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]);
aborter?.cancel();
return [result, undefined]; return [result, undefined];
} catch (error) { } catch (error) {
return [undefined, toError(error)]; return [undefined, toError(error)];
@ -40,27 +42,44 @@ export const exec = async <Return extends Promise<any>>(
* @param signal The abort signal to listen to * @param signal The abort signal to listen to
* @param respite The time in milliseconds to wait before rejecting * @param respite The time in milliseconds to wait before rejecting
*/ */
const getAborter = async (signal: AbortSignal, respite = DEFAULT_RESPITE_SECONDS * 1000): Promise<never> => { const getAborter = (
return new Promise((_, reject) => { signal: AbortSignal,
if (signal.aborted) { respite = DEFAULT_RESPITE_SECONDS * 1000,
setTimeout( ): PromiseLike<never> & { cancel: () => void } => {
const cleanups: Array<() => void> = [];
const aborter = new Promise<never>((_, reject) => {
const abortListener = () => {
const timer = setTimeout(
reject, reject,
respite, respite,
ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)), ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)),
).unref(); );
timer.unref();
cleanups.push(() => {
clearTimeout(timer);
});
};
if (signal.aborted) {
abortListener();
return; return;
} }
signal.addEventListener( signal.addEventListener('abort', abortListener, { once: true });
'abort',
() => { cleanups.push(() => {
setTimeout( signal.removeEventListener('abort', abortListener);
reject, });
respite,
ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)),
).unref();
},
{ once: true },
);
}); });
const cancel = () => {
for (const cleanup of cleanups) {
cleanup();
}
cleanups.length = 0;
};
return Object.assign(aborter, { cancel });
}; };