diff --git a/packages/next/src/server/dynamic-rendering-utils.ts b/packages/next/src/server/dynamic-rendering-utils.ts index 3a816fb01089a..b72865c19d985 100644 --- a/packages/next/src/server/dynamic-rendering-utils.ts +++ b/packages/next/src/server/dynamic-rendering-utils.ts @@ -10,17 +10,44 @@ export function makeHangingPromise( expression: string ): Promise { const hangingPromise = new Promise((_, reject) => { - signal.addEventListener('abort', () => { + function abort() { reject( new Error( `During prerendering, ${expression} rejects when the prerender is complete. Typically these errors are handled by React but if you move ${expression} to a different context by using \`setTimeout\`, \`unstable_after\`, or similar functions you may observe this error and you should handle it in that context.` ) ) - }) + } + + if (signal.aborted) { + abort() + return + } + + let listeners = listenersForSignal.get(signal) + if (listeners) { + listeners.push(abort) + } else { + listeners = [abort] + listenersForSignal.set(signal, listeners) + signal.addEventListener('abort', onAbort.bind(null, listeners), { + once: true, + }) + } }) // We are fine if no one actually awaits this promise. We shouldn't consider this an unhandled rejection so // we attach a noop catch handler here to suppress this warning. If you actually await somewhere or construct // your own promise out of it you'll need to ensure you handle the error when it rejects. - hangingPromise.catch(() => {}) + hangingPromise.catch(ignoreReject) return hangingPromise } + +function ignoreReject() {} + +function onAbort(listeners: Array) { + for (let i = 0; i < listeners.length; i++) { + listeners[i]() + } +} + +type AbortListener = () => void +const listenersForSignal = new WeakMap>() diff --git a/test/e2e/app-dir/dynamic-io/dynamic-io.test.ts b/test/e2e/app-dir/dynamic-io/dynamic-io.test.ts index c4998caa6bc71..130bce6577621 100644 --- a/test/e2e/app-dir/dynamic-io/dynamic-io.test.ts +++ b/test/e2e/app-dir/dynamic-io/dynamic-io.test.ts @@ -20,6 +20,12 @@ describe('dynamic-io', () => { expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) + if (!isNextDev) { + it('should not warn about potential memory leak for even listeners on AbortSignal', async () => { + expect(next.cliOutput).not.toMatch('MaxListenersExceededWarning') + }) + } + it('should prerender fully static pages', async () => { let $ = await next.render$('/cases/static', {}) if (isNextDev) {