diff --git a/packages/toolkit/src/listenerMiddleware/index.ts b/packages/toolkit/src/listenerMiddleware/index.ts index 049fa7cb1f..a30a2ab148 100644 --- a/packages/toolkit/src/listenerMiddleware/index.ts +++ b/packages/toolkit/src/listenerMiddleware/index.ts @@ -39,10 +39,10 @@ import { } from './exceptions' import { runTask, - promisifyAbortSignal, validateActive, createPause, createDelay, + raceWithSignal, } from './task' export { TaskAbortError } from './exceptions' export type { @@ -138,9 +138,9 @@ const createTakePattern = ( // Placeholder unsubscribe function until the listener is added let unsubscribe: UnsubscribeListener = () => {} - const tuplePromise = new Promise<[AnyAction, S, S]>((resolve) => { + const tuplePromise = new Promise<[AnyAction, S, S]>((resolve, reject) => { // Inside the Promise, we synchronously add the listener. - unsubscribe = startListening({ + let stopListening = startListening({ predicate: predicate as any, effect: (action, listenerApi): void => { // One-shot listener that cleans up as soon as the predicate passes @@ -153,10 +153,13 @@ const createTakePattern = ( ]) }, }) + unsubscribe = () => { + stopListening() + reject() + } }) const promises: (Promise | Promise<[AnyAction, S, S]>)[] = [ - promisifyAbortSignal(signal), tuplePromise, ] @@ -167,7 +170,7 @@ const createTakePattern = ( } try { - const output = await Promise.race(promises) + const output = await raceWithSignal(signal, Promise.race(promises)) validateActive(signal) return output diff --git a/packages/toolkit/src/listenerMiddleware/task.ts b/packages/toolkit/src/listenerMiddleware/task.ts index ca57aad422..271b7918e5 100644 --- a/packages/toolkit/src/listenerMiddleware/task.ts +++ b/packages/toolkit/src/listenerMiddleware/task.ts @@ -1,6 +1,6 @@ import { TaskAbortError } from './exceptions' import type { AbortSignalWithReason, TaskResult } from './types' -import { addAbortSignalListener, catchRejection } from './utils' +import { addAbortSignalListener, catchRejection, noop } from './utils' /** * Synchronously raises {@link TaskAbortError} if the task tied to the input `signal` has been cancelled. @@ -15,24 +15,29 @@ export const validateActive = (signal: AbortSignal): void => { } /** - * Returns a promise that will reject {@link TaskAbortError} if the task is cancelled. - * @param signal - * @returns + * Generates a race between the promise(s) and the AbortSignal + * This avoids `Promise.race()`-related memory leaks: + * https://github.com/nodejs/node/issues/17469#issuecomment-349794909 */ -export const promisifyAbortSignal = ( - signal: AbortSignalWithReason -): Promise => { - return catchRejection( - new Promise((_, reject) => { - const notifyRejection = () => reject(new TaskAbortError(signal.reason)) +export function raceWithSignal( + signal: AbortSignalWithReason, + promise: Promise +): Promise { + let cleanup = noop + return new Promise((resolve, reject) => { + const notifyRejection = () => reject(new TaskAbortError(signal.reason)) + + if (signal.aborted) { + notifyRejection() + return + } - if (signal.aborted) { - notifyRejection() - } else { - addAbortSignalListener(signal, notifyRejection) - } - }) - ) + cleanup = addAbortSignalListener(signal, notifyRejection) + promise.finally(() => cleanup()).then(resolve, reject) + }).finally(() => { + // after this point, replace `cleanup` with a noop, so there is no reference to `signal` any more + cleanup = noop + }) } /** @@ -73,7 +78,7 @@ export const runTask = async ( export const createPause = (signal: AbortSignal) => { return (promise: Promise): Promise => { return catchRejection( - Promise.race([promisifyAbortSignal(signal), promise]).then((output) => { + raceWithSignal(signal, promise).then((output) => { validateActive(signal) return output }) diff --git a/packages/toolkit/src/listenerMiddleware/utils.ts b/packages/toolkit/src/listenerMiddleware/utils.ts index 3f86348776..f52e49b154 100644 --- a/packages/toolkit/src/listenerMiddleware/utils.ts +++ b/packages/toolkit/src/listenerMiddleware/utils.ts @@ -28,6 +28,7 @@ export const addAbortSignalListener = ( callback: (evt: Event) => void ) => { abortSignal.addEventListener('abort', callback, { once: true }) + return () => abortSignal.removeEventListener('abort', callback) } /**