diff --git a/packages/next/src/server/web/sandbox/resource-managers.ts b/packages/next/src/server/web/sandbox/resource-managers.ts index 29433fb81c097..7a51095d75cf6 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,41 @@ 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) + } + } + const timeout = setTimeout(wrappedCallback, ms) + return timeout[Symbol.toPrimitive]() +} + export const intervalsManager = new IntervalsManager() export const timeoutsManager = new TimeoutsManager()