From f8d1ee3a94c81b36938ddd6cee07cdae9a3c0743 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Thu, 6 Feb 2025 05:08:19 +0100 Subject: [PATCH 1/2] fix: work around setTimeout memory leak, improve wrappers --- .../server/web/sandbox/resource-managers.ts | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/next/src/server/web/sandbox/resource-managers.ts b/packages/next/src/server/web/sandbox/resource-managers.ts index 29433fb81c097..95113069a1f02 100644 --- a/packages/next/src/server/web/sandbox/resource-managers.ts +++ b/packages/next/src/server/web/sandbox/resource-managers.ts @@ -1,10 +1,10 @@ -abstract class ResourceManager { +abstract class ResourceManager { private resources: T[] = [] - abstract create(resourceArgs: K): T + abstract create(resourceArgs: Args): T abstract destroy(resource: T): void - add(resourceArgs: K) { + add(resourceArgs: Args) { const resource = this.create(resourceArgs) this.resources.push(resource) return resource @@ -27,7 +27,7 @@ class IntervalsManager extends ResourceManager< > { create(args: Parameters) { // TODO: use the edge runtime provided `setInterval` instead - return setInterval(...args)[Symbol.toPrimitive]() + return webSetIntervalPolyfill(...args) } destroy(interval: number) { @@ -41,7 +41,7 @@ class TimeoutsManager extends ResourceManager< > { create(args: Parameters) { // TODO: use the edge runtime provided `setTimeout` instead - return setTimeout(...args)[Symbol.toPrimitive]() + return webSetTimeoutPolyfill(...args) } destroy(timeout: number) { @@ -49,5 +49,46 @@ class TimeoutsManager extends ResourceManager< } } +function webSetIntervalPolyfill( + callback: (...args: TArgs) => void, + ms?: number, + ...args: TArgs +): number { + return setInterval(() => { + // node's `setInterval` sets `this` to the `Timeout` instance it returned, + // but web `setInterval` always sets `this` to `window` + // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval#the_this_problem + return callback.apply(globalThis, args) + }, ms)[Symbol.toPrimitive]() +} + +function webSetTimeoutPolyfill( + callback: (...args: TArgs) => void, + ms?: number, + ...args: TArgs +): number { + const wrappedCallback = () => { + try { + // node's `setTimeout` sets `this` to the `Timeout` instance it returned, + // but web `setTimeout` always sets `this` to `window` + // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#the_this_problem + return callback.apply(globalThis, args) + } finally { + // On certain older node versions (<20.16.0, <22.4.0), + // a `setTimeout` whose Timeout was converted to a primitive will leak. + // See: https://github.com/nodejs/node/issues/53335 + // We can work around this by explicitly calling `clearTimeout` after the callback runs. + clearTimeout(timeout) + + // help the GC a bit by releasing any references we're holding here. + timeout = undefined! + callback = undefined! + args = undefined! + } + } + let timeout = setTimeout(wrappedCallback, ms) + return timeout[Symbol.toPrimitive]() +} + export const intervalsManager = new IntervalsManager() export const timeoutsManager = new TimeoutsManager() From c12aa66f3038f1886080f6e8b0fb1fd05b64d14a Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Thu, 6 Feb 2025 15:26:06 +0100 Subject: [PATCH 2/2] remove useless undefined stuff --- packages/next/src/server/web/sandbox/resource-managers.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/next/src/server/web/sandbox/resource-managers.ts b/packages/next/src/server/web/sandbox/resource-managers.ts index 95113069a1f02..7a51095d75cf6 100644 --- a/packages/next/src/server/web/sandbox/resource-managers.ts +++ b/packages/next/src/server/web/sandbox/resource-managers.ts @@ -79,14 +79,9 @@ function webSetTimeoutPolyfill( // See: https://github.com/nodejs/node/issues/53335 // We can work around this by explicitly calling `clearTimeout` after the callback runs. clearTimeout(timeout) - - // help the GC a bit by releasing any references we're holding here. - timeout = undefined! - callback = undefined! - args = undefined! } } - let timeout = setTimeout(wrappedCallback, ms) + const timeout = setTimeout(wrappedCallback, ms) return timeout[Symbol.toPrimitive]() }