// Guest-facing timers in Node's shape: Timeout and Immediate objects wrap the loop's // handles, so ref()/unref() decide liveness and clearTimeout accepts the object or // its numeric id. node:timers and node:timers/promises are built from the same // functions as the globals. import type { Immediate as LoopImmediate, Loop, Timer } from "./loop"; const TIMEOUT_MAX = 2 ** 31 - 1; type Callback = (...args: any[]) => void; type Warn = (message: string, type: string) => void; export interface Timers { setTimeout(fn: Callback, ms?: unknown, ...args: unknown[]): Timeout; clearTimeout(t: unknown): void; setInterval(fn: Callback, ms?: unknown, ...args: unknown[]): Timeout; clearInterval(t: unknown): void; setImmediate(fn: Callback, ...args: unknown[]): Immediate; clearImmediate(im: unknown): void; } export interface Timeout { ref(): this; unref(): this; hasRef(): boolean; refresh(): this; close(): this; [Symbol.toPrimitive](): number; } export interface Immediate { ref(): this; unref(): this; hasRef(): boolean; } function checkCallback(fn: unknown): asserts fn is Callback { if (typeof fn !== "function") { throw Object.assign(new TypeError(`The "callback" argument must be of type function. Received ${describe(fn)}`), { code: "ERR_INVALID_ARG_TYPE", }); } } function describe(v: unknown): string { if (v === null || v === undefined) return String(v); return typeof v === "string" ? `type string ('${v}')` : `type ${typeof v} (${String(v)})`; } /** Node's delay coercion: anything that is not a number in [1, 2^31-1] becomes 1. */ function coerceDelay(ms: unknown, warn: Warn): number { const after = Number(ms); if (after >= 1 && after <= TIMEOUT_MAX) return after; if (after > TIMEOUT_MAX) { warn(`${after} does not fit into a 32-bit signed integer.\nTimeout duration was set to 1.`, "TimeoutOverflowWarning"); } else if (after < 0) { warn(`${after} is a negative number.\nTimeout duration was set to 1.`, "TimeoutNegativeWarning"); } else if (Number.isNaN(after) && ms !== undefined) { warn(`${String(ms)} is not a number.\nTimeout duration was set to 1.`, "TimeoutNaNWarning"); } return 1; } export function createTimers(loop: Loop, warn: Warn): Timers { const immediateHandles = new WeakMap(); class Timeout { #timer: Timer; #destroyed = false; constructor(fn: Callback, ms: unknown, args: unknown[], repeat: boolean) { this.#timer = loop.addTimer(() => fn.apply(this, args), coerceDelay(ms, warn), repeat); } ref(): this { loop.setTimerRef(this.#timer, true); return this; } unref(): this { loop.setTimerRef(this.#timer, false); return this; } hasRef(): boolean { return this.#timer.ref; } refresh(): this { if (!this.#destroyed) loop.refreshTimer(this.#timer); return this; } close(): this { this.#destroyed = true; loop.clearTimer(this.#timer); return this; } [Symbol.toPrimitive](): number { return this.#timer.id; } } class Immediate { #immediate: LoopImmediate; constructor(fn: Callback, args: unknown[]) { this.#immediate = loop.addImmediate(() => fn.apply(this, args)); immediateHandles.set(this, this.#immediate); } ref(): this { loop.setImmediateRef(this.#immediate, true); return this; } unref(): this { loop.setImmediateRef(this.#immediate, false); return this; } hasRef(): boolean { return this.#immediate.ref; } } function clear(t: unknown): void { if (t instanceof Timeout) { t.close(); return; } // Node also clears by the primitive id (from +timeout or a string of it). if (typeof t === "number" || typeof t === "string") { const timer = loop.timerById(Number(t)); if (timer) loop.clearTimer(timer); } } return { setTimeout(fn, ms, ...args) { checkCallback(fn); return new Timeout(fn, ms, args, false); }, clearTimeout: clear, setInterval(fn, ms, ...args) { checkCallback(fn); return new Timeout(fn, ms, args, true); }, clearInterval: clear, setImmediate(fn, ...args) { checkCallback(fn); return new Immediate(fn, args); }, clearImmediate(im) { const handle = typeof im === "object" && im !== null ? immediateHandles.get(im) : undefined; if (handle) loop.clearImmediate(handle); }, }; } type TimerOptions = { ref?: boolean; signal?: AbortSignal }; /** node:timers/promises: the promise forms frameworks import. */ export function timersPromises(timers: Timers) { function timeout(ms?: number, value?: T, options?: TimerOptions): Promise { return settle(options, (done) => timers.setTimeout(() => done(value), ms), (t) => timers.clearTimeout(t)); } function immediate(value?: T, options?: TimerOptions): Promise { return settle(options, (done) => timers.setImmediate(() => done(value)), (im) => timers.clearImmediate(im)); } /** setInterval(after, value): an async iterator that yields `value` once per tick, * catching up (one yield per missed tick) if the consumer falls behind, as Node's * own timers/promises.setInterval does. */ async function* interval(after?: number, value?: T, options?: TimerOptions): AsyncGenerator { const signal = options?.signal; if (signal?.aborted) throw abortError(signal); let notYielded = 0; let wake: (() => void) | undefined; const timer = timers.setInterval(() => { notYielded++; wake?.(); wake = undefined; }, after); if (options?.ref === false) timer.unref(); let onAbort: (() => void) | undefined; if (signal) { onAbort = (): void => { timers.clearInterval(timer); wake?.(); wake = undefined; }; signal.addEventListener("abort", onAbort, { once: true }); } try { while (!signal?.aborted) { if (notYielded === 0) await new Promise((resolve) => (wake = resolve)); for (; notYielded > 0; notYielded--) yield value; } throw abortError(signal!); } finally { timers.clearInterval(timer); if (signal && onAbort) signal.removeEventListener("abort", onAbort); } } return { setTimeout: timeout, setImmediate: immediate, setInterval: interval, scheduler: makeScheduler(timeout, immediate) }; } /** timers/promises.scheduler: the awaitable forms of the WICG Scheduling proposal. * `yield()` defers to the browser's native `scheduler.yield()` when present (it * cooperates with the page's own task scheduling better than a bare setImmediate); * `wait()` has no native counterpart (`scheduler.postTask` is not the same shape), * so it is always our own setTimeout. */ function makeScheduler( timeout: (ms?: number, value?: T, options?: TimerOptions) => Promise, immediate: (value?: T, options?: TimerOptions) => Promise, ): { wait(delay?: number, options?: TimerOptions): Promise; yield(): Promise } { const native = (globalThis as { scheduler?: { yield?: () => Promise } }).scheduler; class Scheduler { yield(): Promise { return native?.yield ? native.yield() : (immediate() as Promise); } wait(delay?: number, options?: TimerOptions): Promise { return timeout(delay, undefined, options) as Promise; } } return new Scheduler(); } function settle( options: { ref?: boolean; signal?: AbortSignal } | undefined, start: (done: (v: T) => void) => H, cancel: (h: H) => void, ): Promise { const signal = options?.signal; if (signal?.aborted) return Promise.reject(abortError(signal)); return new Promise((resolve, reject) => { const onAbort = (): void => { cancel(handle); reject(abortError(signal!)); }; const handle = start((v) => { signal?.removeEventListener("abort", onAbort); resolve(v); }); if (options?.ref === false) handle.unref(); signal?.addEventListener("abort", onAbort, { once: true }); }); } function abortError(signal: AbortSignal): Error { return Object.assign(new Error("The operation was aborted"), { name: "AbortError", code: "ABORT_ERR", cause: signal.reason }); }