From aef241cb967046778ef1d52e0d5b8332de8e9dc9 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 16 Jan 2025 11:24:32 -0500 Subject: [PATCH] add node:timers module --- src/node/BUILD.bazel | 1 + src/node/internal/internal_timers.ts | 320 ++++++++++++++++++ src/node/internal/internal_timers_promises.ts | 57 ++++ src/node/timers.ts | 28 ++ src/node/timers/promises.ts | 3 + src/node/tsconfig.json | 1 + src/workerd/api/node/BUILD.bazel | 6 + .../api/node/tests/timers-nodejs-test.js | 24 ++ .../api/node/tests/timers-nodejs-test.wd-test | 15 + 9 files changed, 455 insertions(+) create mode 100644 src/node/internal/internal_timers.ts create mode 100644 src/node/internal/internal_timers_promises.ts create mode 100644 src/node/timers.ts create mode 100644 src/node/timers/promises.ts create mode 100644 src/workerd/api/node/tests/timers-nodejs-test.js create mode 100644 src/workerd/api/node/tests/timers-nodejs-test.wd-test diff --git a/src/node/BUILD.bazel b/src/node/BUILD.bazel index c9da6ef4736..80eba20636d 100644 --- a/src/node/BUILD.bazel +++ b/src/node/BUILD.bazel @@ -17,6 +17,7 @@ wd_ts_bundle( "stream/*.js", "path/*.ts", "util/*.ts", + "timers/*.ts", ]), schema_id = "0xbcc8f57c63814005", tsconfig_json = "tsconfig.json", diff --git a/src/node/internal/internal_timers.ts b/src/node/internal/internal_timers.ts new file mode 100644 index 00000000000..35bb29c3f60 --- /dev/null +++ b/src/node/internal/internal_timers.ts @@ -0,0 +1,320 @@ +import { validateFunction, validateNumber } from 'node-internal:validators'; +import { ERR_OUT_OF_RANGE } from 'node-internal:internal_errors'; + +// Timeout values > TIMEOUT_MAX are set to 1. +const TIMEOUT_MAX = 2 ** 31 - 1; + +let clearTimeoutImpl: (obj: Timeout) => void; +let clearImmediateImpl: (obj: Immediate) => void; + +function getTimerDuration(msecs: unknown, name: string): number { + validateNumber(msecs, name); + if (msecs < 0 || !Number.isFinite(msecs)) { + throw new ERR_OUT_OF_RANGE(name, 'a non-negative finite number', msecs); + } + + // Ensure that msecs fits into signed int32 + if (msecs > TIMEOUT_MAX) { + return TIMEOUT_MAX; + } + + return msecs; +} + +class Timeout { + #timer: string | number | this | undefined; + #callback: (...args: unknown[]) => unknown; + #after: number; + #args: unknown[]; + #isRepeat: boolean; + #isRefed: boolean; + + public constructor( + callback: (...args: unknown[]) => unknown, + after?: number, + args: unknown[] = [], + isRepeat: boolean = false, + isRefed: boolean = false + ) { + if (after === undefined) { + after = 1; + } else { + after *= 1; // Coalesce to number or NaN + } + + this.#callback = callback; + this.#after = after; + this.#args = args; + this.#isRepeat = isRepeat; + this.#isRefed = isRefed; + this.#constructTimer(); + } + + #constructTimer(): void { + if (this.#isRepeat) { + // @ts-expect-error TS2322 Due to difference between Node.js and globals + this.#timer = globalThis.setInterval( + () => this.#callback(...this.#args), + this.#after + ); + } else { + // @ts-expect-error TS2322 Due to difference between Node.js and globals + this.#timer = globalThis.setTimeout( + () => this.#callback(...this.#args), + this.#after + ); + } + } + + #clearTimeout(): void { + if (this.#isRepeat) { + globalThis.clearInterval(this.#timer); + } + { + globalThis.clearTimeout(this.#timer); + } + } + + public refresh(): this { + this.#clearTimeout(); + this.#constructTimer(); + return this; + } + + public unref(): this { + // TODO(soon): Implement this + this.#isRefed = false; + return this; + } + + public ref(): this { + // TODO(soon): Implement this + this.#isRefed = true; + return this; + } + + public hasRef(): boolean { + return this.#isRefed; + } + + public close(): this { + this.#clearTimeout(); + return this; + } + + public [Symbol.dispose](): void { + this.#clearTimeout(); + } + + public [Symbol.toPrimitive](): number { + // @ts-expect-error TS2322 Timer is actually an ID. + return this.#timer; + } + + static { + clearTimeoutImpl = (obj: Timeout): void => { + obj.#clearTimeout(); + }; + } +} + +class Immediate { + // @ts-expect-error TS2724 Node.js and global difference. + #timer: globalThis.Immediate; + #hasRef: boolean = false; + + public constructor( + callback: (...args: unknown[]) => void, + args: unknown[] = [] + ) { + this.#timer = globalThis.setImmediate(callback, ...args); + } + + public ref(): this { + // TODO: Implement this + this.#hasRef = true; + return this; + } + + public unref(): this { + // TODO: Implement this + this.#hasRef = false; + return this; + } + + public hasRef(): boolean { + return this.#hasRef; + } + + public [Symbol.dispose](): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + globalThis.clearImmediate(this.#timer); + } + + static { + clearImmediateImpl = (obj: Immediate): void => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + globalThis.clearImmediate(obj.#timer); + }; + } +} + +export function setTimeout( + callback: (...args: unknown[]) => unknown, + after: number, + arg1?: unknown, + arg2?: unknown, + arg3?: unknown +): Timeout { + validateFunction(callback, 'callback'); + + let i, args; + switch (arguments.length) { + // fast cases + case 1: + case 2: + break; + case 3: + args = [arg1]; + break; + case 4: + args = [arg1, arg2]; + break; + default: + args = [arg1, arg2, arg3]; + for (i = 5; i < arguments.length; i++) { + // eslint-disable-next-line prefer-rest-params + args.push(arguments[i]); + } + break; + } + + return new Timeout( + callback, + after, + args, + /* isRepeat */ false, + /* isRefed */ true + ); +} + +export function clearTimeout(timer: unknown): void { + if (timer instanceof Timeout) { + clearTimeoutImpl(timer); + return; + } else if (typeof timer === 'number') { + globalThis.clearTimeout(timer); + } +} + +export function setImmediate( + callback: (...args: unknown[]) => void, + arg1?: unknown, + arg2?: unknown, + arg3?: unknown +): Immediate { + validateFunction(callback, 'callback'); + + let i, args; + switch (arguments.length) { + // fast cases + case 1: + break; + case 2: + args = [arg1]; + break; + case 3: + args = [arg1, arg2]; + break; + default: + args = [arg1, arg2, arg3]; + for (i = 4; i < arguments.length; i++) { + // eslint-disable-next-line prefer-rest-params + args.push(arguments[i]); + } + break; + } + + return new Immediate(callback, args); +} + +export function clearImmediate(immediate?: Immediate): void { + if (immediate != null) { + clearImmediateImpl(immediate); + } +} + +export function setInterval( + callback: (...args: unknown[]) => void, + repeat: number, + arg1?: unknown, + arg2?: unknown, + arg3?: unknown +): Timeout { + validateFunction(callback, 'callback'); + + let i, args; + switch (arguments.length) { + // fast cases + case 1: + case 2: + break; + case 3: + args = [arg1]; + break; + case 4: + args = [arg1, arg2]; + break; + default: + args = [arg1, arg2, arg3]; + for (i = 5; i < arguments.length; i++) { + // eslint-disable-next-line prefer-rest-params + args.push(arguments[i]); + } + break; + } + + return new Timeout( + callback, + repeat, + args, + /* isRepeat */ true, + /* isRefed */ true + ); +} + +export function clearInterval(timer: unknown): void { + if (timer instanceof Timeout) { + clearTimeoutImpl(timer); + } else if (typeof timer === 'number') { + globalThis.clearInterval(timer); + } +} + +/** + * @deprecated Please use timeout.refresh() instead. + */ +export function active(timer: unknown): void { + if (timer instanceof Timeout) { + timer.refresh(); + } +} + +/** + * @deprecated Please use clearTimeout instead. + */ +export function unenroll(timer: unknown): void { + if (timer instanceof Timeout) { + clearTimeoutImpl(timer); + } +} + +/** + * @deprecated Please use setTimeout instead. + */ +export function enroll(_item: unknown, msecs: number): void { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + msecs = getTimerDuration(msecs, 'msecs'); + // TODO(soon): Implement this. + throw new Error('Not implemented'); +} diff --git a/src/node/internal/internal_timers_promises.ts b/src/node/internal/internal_timers_promises.ts new file mode 100644 index 00000000000..f16c7d8977c --- /dev/null +++ b/src/node/internal/internal_timers_promises.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/require-await,@typescript-eslint/no-deprecated,prefer-spread */ + +import * as timers from 'node-internal:internal_timers'; + +export async function setTimeout( + ...args: Parameters +): Promise> { + return timers.setTimeout.apply(timers, args); +} + +export async function clearTimeout( + ...args: Parameters +): Promise { + timers.clearTimeout.apply(timers, args); +} + +export async function setImmediate( + ...args: Parameters +): Promise> { + return timers.setImmediate.apply(timers, args); +} + +export async function clearImmediate( + ...args: Parameters +): Promise { + timers.clearImmediate.apply(timers, args); +} + +export async function setInterval( + ...args: Parameters +): Promise> { + return timers.setInterval.apply(timers, args); +} + +export async function clearInterval( + ...args: Parameters +): Promise { + timers.clearInterval.apply(timers, args); +} + +export async function active( + ...args: Parameters +): Promise> { + timers.active.apply(timers, args); +} + +export async function unenroll( + ...args: Parameters +): Promise { + timers.unenroll.apply(timers, args); +} + +export async function enroll( + ...args: Parameters +): Promise { + timers.enroll.apply(timers, args); +} diff --git a/src/node/timers.ts b/src/node/timers.ts new file mode 100644 index 00000000000..525715c2209 --- /dev/null +++ b/src/node/timers.ts @@ -0,0 +1,28 @@ +import * as _promises from 'node-internal:internal_timers_promises'; +import { + setTimeout, + clearTimeout, + setImmediate, + clearImmediate, + setInterval, + clearInterval, + active, + unenroll, + enroll, +} from 'node-internal:internal_timers'; + +export * from 'node-internal:internal_timers'; +export const promises = _promises; + +export default { + promises: _promises, + setTimeout, + clearTimeout, + setImmediate, + clearImmediate, + setInterval, + clearInterval, + active, + unenroll, + enroll, +}; diff --git a/src/node/timers/promises.ts b/src/node/timers/promises.ts new file mode 100644 index 00000000000..146d5ed6846 --- /dev/null +++ b/src/node/timers/promises.ts @@ -0,0 +1,3 @@ +import * as _default from 'node-internal:internal_timers_promises'; +export * from 'node-internal:internal_timers_promises'; +export default _default; diff --git a/src/node/tsconfig.json b/src/node/tsconfig.json index 0784c82b020..8d9f3a4a197 100644 --- a/src/node/tsconfig.json +++ b/src/node/tsconfig.json @@ -28,6 +28,7 @@ "node:util/*": ["./*"], "node:path/*": ["./*"], "node:stream/*": ["./*"], + "node:timers/*": ["./*"], "node-internal:*": ["./internal/*"], "cloudflare-internal:sockets": ["./internal/sockets.d.ts"], "cloudflare-internal:workers": ["./internal/workers.d.ts"], diff --git a/src/workerd/api/node/BUILD.bazel b/src/workerd/api/node/BUILD.bazel index 1e348768f40..8b838f0d3f9 100644 --- a/src/workerd/api/node/BUILD.bazel +++ b/src/workerd/api/node/BUILD.bazel @@ -272,3 +272,9 @@ wd_test( "//conditions:default": [], }), ) + +wd_test( + src = "tests/timers-nodejs-test.wd-test", + args = ["--experimental"], + data = ["tests/timers-nodejs-test.js"], +) diff --git a/src/workerd/api/node/tests/timers-nodejs-test.js b/src/workerd/api/node/tests/timers-nodejs-test.js new file mode 100644 index 00000000000..6e57c23dd12 --- /dev/null +++ b/src/workerd/api/node/tests/timers-nodejs-test.js @@ -0,0 +1,24 @@ +import timers from 'node:timers'; +import { throws } from 'node:assert'; + +export const enrollInvalidMsecs = { + async test() { + [{}, [], 'foo', () => {}, Symbol('foo')].forEach((val) => { + throws(() => timers.enroll({}, val), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + }); + + [-1, Infinity, NaN].forEach((val) => { + throws(() => timers.enroll({}, val), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: + 'The value of "msecs" is out of range. ' + + 'It must be a non-negative finite number. ' + + `Received ${val}`, + }); + }); + }, +}; diff --git a/src/workerd/api/node/tests/timers-nodejs-test.wd-test b/src/workerd/api/node/tests/timers-nodejs-test.wd-test new file mode 100644 index 00000000000..f69236a5326 --- /dev/null +++ b/src/workerd/api/node/tests/timers-nodejs-test.wd-test @@ -0,0 +1,15 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "nodejs-timers-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "timers-nodejs-test.js") + ], + compatibilityDate = "2025-01-09", + compatibilityFlags = ["nodejs_compat"], + ) + ), + ], +);