Skip to content

Commit 9affa18

Browse files
authored
[dynamicIO] Avoid memory leak warning for hanging promises (#77480)
Node.js tries to help you avoid memory leaks by detecting when a lot of event listeners are set for a single AbortSignal. Next.js uses abort signal listeners to do cleanup on hanging promises and so this usage is not a memory leak however to avoid creating concern amongst users we need to work around the listener count limit. This is unfortunate b/c it is otherwise useless code to add but our alternative is to turn off this leak detection or raise the limit significantly, neither of which are great options since there may be other uses where this protection is useful to our users. In this change I implement the abort listening by a delegated event listener
1 parent 6b8a3cc commit 9affa18

File tree

1 file changed

+34
-14
lines changed

1 file changed

+34
-14
lines changed

packages/next/src/server/dynamic-rendering-utils.ts

+34-14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class HangingPromiseRejectionError extends Error {
2020
}
2121
}
2222

23+
type AbortListeners = Array<(err: unknown) => void>
24+
const abortListenersBySignal = new WeakMap<AbortSignal, AbortListeners>()
25+
2326
/**
2427
* This function constructs a promise that will never resolve. This is primarily
2528
* useful for dynamicIO where we use promise resolution timing to determine which
@@ -31,20 +34,37 @@ export function makeHangingPromise<T>(
3134
signal: AbortSignal,
3235
expression: string
3336
): Promise<T> {
34-
const hangingPromise = new Promise<T>((_, reject) => {
35-
signal.addEventListener(
36-
'abort',
37-
() => {
38-
reject(new HangingPromiseRejectionError(expression))
39-
},
40-
{ once: true }
41-
)
42-
})
43-
// We are fine if no one actually awaits this promise. We shouldn't consider this an unhandled rejection so
44-
// we attach a noop catch handler here to suppress this warning. If you actually await somewhere or construct
45-
// your own promise out of it you'll need to ensure you handle the error when it rejects.
46-
hangingPromise.catch(ignoreReject)
47-
return hangingPromise
37+
if (signal.aborted) {
38+
return Promise.reject(new HangingPromiseRejectionError(expression))
39+
} else {
40+
const hangingPromise = new Promise<T>((_, reject) => {
41+
const boundRejection = reject.bind(
42+
null,
43+
new HangingPromiseRejectionError(expression)
44+
)
45+
let currentListeners = abortListenersBySignal.get(signal)
46+
if (currentListeners) {
47+
currentListeners.push(boundRejection)
48+
} else {
49+
const listeners = [boundRejection]
50+
abortListenersBySignal.set(signal, listeners)
51+
signal.addEventListener(
52+
'abort',
53+
() => {
54+
for (let i = 0; i < listeners.length; i++) {
55+
listeners[i]()
56+
}
57+
},
58+
{ once: true }
59+
)
60+
}
61+
})
62+
// We are fine if no one actually awaits this promise. We shouldn't consider this an unhandled rejection so
63+
// we attach a noop catch handler here to suppress this warning. If you actually await somewhere or construct
64+
// your own promise out of it you'll need to ensure you handle the error when it rejects.
65+
hangingPromise.catch(ignoreReject)
66+
return hangingPromise
67+
}
4868
}
4969

5070
function ignoreReject() {}

0 commit comments

Comments
 (0)