Skip to content

Commit d9374c7

Browse files
authored
Merge pull request #3021 from reduxjs/pr/fix-signal-race-memleak
2 parents 59899f4 + 54bbb32 commit d9374c7

File tree

3 files changed

+32
-23
lines changed

3 files changed

+32
-23
lines changed

packages/toolkit/src/listenerMiddleware/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ import {
3939
} from './exceptions'
4040
import {
4141
runTask,
42-
promisifyAbortSignal,
4342
validateActive,
4443
createPause,
4544
createDelay,
45+
raceWithSignal,
4646
} from './task'
4747
export { TaskAbortError } from './exceptions'
4848
export type {
@@ -138,9 +138,9 @@ const createTakePattern = <S>(
138138
// Placeholder unsubscribe function until the listener is added
139139
let unsubscribe: UnsubscribeListener = () => {}
140140

141-
const tuplePromise = new Promise<[AnyAction, S, S]>((resolve) => {
141+
const tuplePromise = new Promise<[AnyAction, S, S]>((resolve, reject) => {
142142
// Inside the Promise, we synchronously add the listener.
143-
unsubscribe = startListening({
143+
let stopListening = startListening({
144144
predicate: predicate as any,
145145
effect: (action, listenerApi): void => {
146146
// One-shot listener that cleans up as soon as the predicate passes
@@ -153,10 +153,13 @@ const createTakePattern = <S>(
153153
])
154154
},
155155
})
156+
unsubscribe = () => {
157+
stopListening()
158+
reject()
159+
}
156160
})
157161

158162
const promises: (Promise<null> | Promise<[AnyAction, S, S]>)[] = [
159-
promisifyAbortSignal(signal),
160163
tuplePromise,
161164
]
162165

@@ -167,7 +170,7 @@ const createTakePattern = <S>(
167170
}
168171

169172
try {
170-
const output = await Promise.race(promises)
173+
const output = await raceWithSignal(signal, Promise.race(promises))
171174

172175
validateActive(signal)
173176
return output

packages/toolkit/src/listenerMiddleware/task.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TaskAbortError } from './exceptions'
22
import type { AbortSignalWithReason, TaskResult } from './types'
3-
import { addAbortSignalListener, catchRejection } from './utils'
3+
import { addAbortSignalListener, catchRejection, noop } from './utils'
44

55
/**
66
* 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 => {
1515
}
1616

1717
/**
18-
* Returns a promise that will reject {@link TaskAbortError} if the task is cancelled.
19-
* @param signal
20-
* @returns
18+
* Generates a race between the promise(s) and the AbortSignal
19+
* This avoids `Promise.race()`-related memory leaks:
20+
* https://github.com/nodejs/node/issues/17469#issuecomment-349794909
2121
*/
22-
export const promisifyAbortSignal = (
23-
signal: AbortSignalWithReason<string>
24-
): Promise<never> => {
25-
return catchRejection(
26-
new Promise<never>((_, reject) => {
27-
const notifyRejection = () => reject(new TaskAbortError(signal.reason))
22+
export function raceWithSignal<T>(
23+
signal: AbortSignalWithReason<string>,
24+
promise: Promise<T>
25+
): Promise<T> {
26+
let cleanup = noop
27+
return new Promise<T>((resolve, reject) => {
28+
const notifyRejection = () => reject(new TaskAbortError(signal.reason))
29+
30+
if (signal.aborted) {
31+
notifyRejection()
32+
return
33+
}
2834

29-
if (signal.aborted) {
30-
notifyRejection()
31-
} else {
32-
addAbortSignalListener(signal, notifyRejection)
33-
}
34-
})
35-
)
35+
cleanup = addAbortSignalListener(signal, notifyRejection)
36+
promise.finally(() => cleanup()).then(resolve, reject)
37+
}).finally(() => {
38+
// after this point, replace `cleanup` with a noop, so there is no reference to `signal` any more
39+
cleanup = noop
40+
})
3641
}
3742

3843
/**
@@ -73,7 +78,7 @@ export const runTask = async <T>(
7378
export const createPause = <T>(signal: AbortSignal) => {
7479
return (promise: Promise<T>): Promise<T> => {
7580
return catchRejection(
76-
Promise.race([promisifyAbortSignal(signal), promise]).then((output) => {
81+
raceWithSignal(signal, promise).then((output) => {
7782
validateActive(signal)
7883
return output
7984
})

packages/toolkit/src/listenerMiddleware/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const addAbortSignalListener = (
2828
callback: (evt: Event) => void
2929
) => {
3030
abortSignal.addEventListener('abort', callback, { once: true })
31+
return () => abortSignal.removeEventListener('abort', callback)
3132
}
3233

3334
/**

0 commit comments

Comments
 (0)