diff --git a/.changeset/slimy-hairs-lay.md b/.changeset/slimy-hairs-lay.md new file mode 100644 index 00000000..951d3ce1 --- /dev/null +++ b/.changeset/slimy-hairs-lay.md @@ -0,0 +1,7 @@ +--- +"@edge-runtime/ponyfill": patch +"@edge-runtime/primitives": patch +"@edge-runtime/vm": patch +--- + +Don't return `NodeJS.Timer` from `setTimeout` and `setInterval` diff --git a/packages/ponyfill/src/index.js b/packages/ponyfill/src/index.js index bae1e9cf..331f5958 100644 --- a/packages/ponyfill/src/index.js +++ b/packages/ponyfill/src/index.js @@ -26,6 +26,8 @@ function edge() { ReadableStreamDefaultReader, Request, Response, + setInterval, + setTimeout, structuredClone, SubtleCrypto, TextDecoder, diff --git a/packages/primitives/src/primitives/load.js b/packages/primitives/src/primitives/load.js index b9cd47fc..4dda9a73 100644 --- a/packages/primitives/src/primitives/load.js +++ b/packages/primitives/src/primitives/load.js @@ -97,6 +97,18 @@ export function load(scopedContext = {}) { }) assign(context, { console: consoleImpl.console }) + /** @type {import('../../type-definitions/timer')} */ + const timeoutImpl = requireWithFakeGlobalScope({ + context, + id: 'timeout.js', + sourceCode: injectSourceCode('./timer.js'), + scopedContext, + }) + assign(context, { + setTimeout: timeoutImpl.setTimeout, + setInterval: timeoutImpl.setInterval, + }) + /** @type {import('../../type-definitions/events')} */ const eventsImpl = requireWithFakeGlobalScope({ context, diff --git a/packages/primitives/src/primitives/timer.js b/packages/primitives/src/primitives/timer.js new file mode 100644 index 00000000..727ebc68 --- /dev/null +++ b/packages/primitives/src/primitives/timer.js @@ -0,0 +1,10 @@ +const timeoutProxy = new Proxy(setTimeout, { + apply: (target, thisArg, args) => { + const timeout = Reflect.apply(target, thisArg, args) + // Returns integer value of timeout ID + return timeout[Symbol.toPrimitive]() + }, +}) + +export { timeoutProxy as setTimeout } +export { timeoutProxy as setInterval } diff --git a/packages/primitives/type-definitions/index.d.ts b/packages/primitives/type-definitions/index.d.ts index 53a5a59e..efe5f74d 100644 --- a/packages/primitives/type-definitions/index.d.ts +++ b/packages/primitives/type-definitions/index.d.ts @@ -9,3 +9,4 @@ export * from './streams' export * from './text-encoding-streams' export * from './structured-clone' export * from './url' +export * from './timer' diff --git a/packages/primitives/type-definitions/timer.d.ts b/packages/primitives/type-definitions/timer.d.ts new file mode 100644 index 00000000..ac3742c6 --- /dev/null +++ b/packages/primitives/type-definitions/timer.d.ts @@ -0,0 +1,5 @@ +declare const _setTimeout: typeof Number +declare const _setInterval: typeof Number + +export { _setTimeout as setTimeout } +export { _setInterval as setInterval } diff --git a/packages/vm/src/edge-vm.ts b/packages/vm/src/edge-vm.ts index 8f2bc3d3..e8bc2aea 100644 --- a/packages/vm/src/edge-vm.ts +++ b/packages/vm/src/edge-vm.ts @@ -140,7 +140,7 @@ function patchInstanceOf(item: string, ctx: any) { } }) `, - ctx + ctx, ) } @@ -155,9 +155,9 @@ function registerUnhandledRejectionHandlers(handlers: RejectionHandler[]) { 'unhandledRejection', function invokeRejectionHandlers(reason, promise) { unhandledRejectionHandlers.forEach((handler) => - handler({ reason, promise }) + handler({ reason, promise }), ) - } + }, ) } unhandledRejectionHandlers = handlers @@ -294,6 +294,8 @@ export type EdgeContext = VMContext & { ReadableStreamDefaultReader: typeof EdgePrimitives.ReadableStreamDefaultReader Request: typeof EdgePrimitives.Request Response: typeof EdgePrimitives.Response + setTimeout: typeof EdgePrimitives.setTimeout + setInterval: typeof EdgePrimitives.setInterval structuredClone: typeof EdgePrimitives.structuredClone SubtleCrypto: typeof EdgePrimitives.SubtleCrypto TextDecoder: typeof EdgePrimitives.TextDecoder @@ -315,8 +317,6 @@ function addPrimitives(context: VMContext) { defineProperty(context, 'Symbol', { value: Symbol }) defineProperty(context, 'clearInterval', { value: clearInterval }) defineProperty(context, 'clearTimeout', { value: clearTimeout }) - defineProperty(context, 'setInterval', { value: setInterval }) - defineProperty(context, 'setTimeout', { value: setTimeout }) defineProperty(context, 'queueMicrotask', { value: queueMicrotask }) defineProperty(context, 'EdgeRuntime', { value: 'edge-runtime' }) @@ -383,6 +383,10 @@ function addPrimitives(context: VMContext) { // Console 'console', + + // Timers + 'setTimeout', + 'setInterval', ], }) @@ -404,7 +408,7 @@ function defineProperties( exports: Record enumerable?: string[] nonenumerable?: string[] - } + }, ) { for (const property of options.enumerable ?? []) { if (!options.exports[property]) { @@ -433,7 +437,7 @@ function defineProperties( * implemented in the provided context. */ function getTransferablePrimitivesFromContext( - context: Context + context: Context, ): Record<(typeof transferableConstructors)[number], unknown> { const keys = transferableConstructors.join(',') const stringifedObject = `({${keys}})` diff --git a/packages/vm/tests/edge-runtime.test.ts b/packages/vm/tests/edge-runtime.test.ts index f54eafd7..41d22b13 100644 --- a/packages/vm/tests/edge-runtime.test.ts +++ b/packages/vm/tests/edge-runtime.test.ts @@ -622,3 +622,21 @@ describe('Event handlers', () => { expect((runtime as any).__rejectionHandlers).toBeUndefined() }) }) + +describe('`Timers`', () => { + it.each(['setTimeout', 'setInterval'])( + '%s function should return integer', + async (f) => { + const runtime = new EdgeVM() + expect(() => { + runtime.evaluate(` + const timer = ${f}(() => {}, 1000); + timer.unref(); + `) + }).toThrow({ + name: 'Error', + message: `timer.unref is not a function`, + }) + }, + ) +})