diff --git a/.changeset/blue-candles-boil.md b/.changeset/blue-candles-boil.md new file mode 100644 index 0000000..5d2de1d --- /dev/null +++ b/.changeset/blue-candles-boil.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': patch +--- + +Cleanup AbortSignal listeners when they are not needed to avoid MaxListenersExceededWarning when migrating many migrations at once diff --git a/packages/cli/src/exec.ts b/packages/cli/src/exec.ts index 887f737..cac2a28 100644 --- a/packages/cli/src/exec.ts +++ b/packages/cli/src/exec.ts @@ -28,6 +28,8 @@ export const exec = async >( const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined; const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]); + aborter?.cancel(); + return [result, undefined]; } catch (error) { return [undefined, toError(error)]; @@ -40,27 +42,44 @@ export const exec = async >( * @param signal The abort signal to listen to * @param respite The time in milliseconds to wait before rejecting */ -const getAborter = async (signal: AbortSignal, respite = DEFAULT_RESPITE_SECONDS * 1000): Promise => { - return new Promise((_, reject) => { - if (signal.aborted) { - setTimeout( +const getAborter = ( + signal: AbortSignal, + respite = DEFAULT_RESPITE_SECONDS * 1000, +): PromiseLike & { cancel: () => void } => { + const cleanups: Array<() => void> = []; + + const aborter = new Promise((_, reject) => { + const abortListener = () => { + const timer = setTimeout( reject, respite, ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)), - ).unref(); + ); + timer.unref(); + cleanups.push(() => { + clearTimeout(timer); + }); + }; + + if (signal.aborted) { + abortListener(); return; } - signal.addEventListener( - 'abort', - () => { - setTimeout( - reject, - respite, - ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)), - ).unref(); - }, - { once: true }, - ); + signal.addEventListener('abort', abortListener, { once: true }); + + cleanups.push(() => { + signal.removeEventListener('abort', abortListener); + }); }); + + const cancel = () => { + for (const cleanup of cleanups) { + cleanup(); + } + + cleanups.length = 0; + }; + + return Object.assign(aborter, { cancel }); };