import { Map, MapPrototypeDelete as remove, MapPrototypeGet as get, MapPrototypeHas as has, MapPrototypeSet as set, MathMin as min, ReflectApply as apply, TypeError, } from "ext:core/primordials"; import { op_timer_cancel as cancel, op_timer_now as now, op_timer_wait as wait, op_timer_wait_unreferenced as waitUnreferenced, } from "ext:core/ops"; import { promiseThen as then } from "ext:core/promises"; import { enqueueMicrotask } from "ext:core/microtasks"; import { enqueueTask } from "ext:core/tasks"; import { array, missing, required, string } from "ext:web/idl.js"; import { reportErrorValue } from "ext:web/events.js"; interface Timer { id: number; handler: Function | null; args: unknown[] | null; repeat: boolean; delay: number; privateTimer: boolean; nesting: number; deadline: number; sequence: number; } const receiver = globalThis; const timers = new Map(), queue = array(); let nextId = 1, pending: { deadline: number; referenced: boolean } | null = null, firing = false, nesting = 0; // Heap order is (deadline, sequence); the sequence restores FIFO among equals. let sequence = 0; // Cleared entries stay in the heap until they surface; stale counts them so // schedule() can rebuild when garbage dominates. let stale = 0; // Non-private timers in the map, including the one currently firing. let referencedCount = 0; function report(error: unknown): void { reportErrorValue(error); } function earlier(a: Timer, b: Timer): boolean { return a.deadline < b.deadline || (a.deadline === b.deadline && a.sequence < b.sequence); } function siftUp(index: number): void { while (index > 0) { const parent = (index - 1) >> 1; if (!earlier(queue[index], queue[parent])) break; const entry = queue[index]; queue[index] = queue[parent]; queue[parent] = entry; index = parent; } } function siftDown(index: number): void { const count = queue.length; for (;;) { const left = index * 2 + 1; const right = left + 1; let least = index; if (left < count && earlier(queue[left], queue[least])) least = left; if (right < count && earlier(queue[right], queue[least])) least = right; if (least === index) break; const entry = queue[index]; queue[index] = queue[least]; queue[least] = entry; index = least; } } function insert(entry: Timer): void { entry.sequence = sequence++; const index = queue.length; queue[index] = entry; siftUp(index); } function popHead(): Timer { const head = queue[0]; const last = queue[queue.length - 1]; queue.length = queue.length - 1; if (queue.length > 0) { queue[0] = last; siftDown(0); } return head; } // Drop cleared heads so the live head is on top. Interior garbage stays // until it surfaces; schedule() rebuilds if it dominates. function peekHead(): Timer | undefined { while (queue.length > 0) { const head = queue[0]; if (get(timers, head.id) === head) return head; popHead(); if (stale > 0) stale--; } return undefined; } function compactIfNeeded(): void { if (stale <= 0 || queue.length < 128 || stale * 2 <= queue.length) return; let live = 0; for (let index = 0; index < queue.length; index++) { const entry = queue[index]; if (get(timers, entry.id) === entry) queue[live++] = entry; } queue.length = live; stale = 0; for (let index = (live >> 1) - 1; index >= 0; index--) siftDown(index); } function schedule(): void { if (firing) return; compactIfNeeded(); const head = peekHead(); if (!head) { pending = null; cancel(null); return; } const referenced = referencedCount > 0; if ( pending && pending.deadline <= head.deadline && pending.referenced === referenced ) return; const record = { deadline: head.deadline, referenced }; pending = record; const promise = (referenced ? wait : waitUnreferenced)(record.deadline); // Promise reactions must not consult guest-controlled constructor/species hooks. then(promise, () => { if (pending !== record) return; pending = null; const entry = peekHead(); if (!entry || entry.deadline > now(null)) { schedule(); return; } popHead(); firing = true; enqueueTask(() => { if (get(timers, entry.id) !== entry) return; nesting = entry.privateTimer ? 0 : entry.nesting; try { // Map membership above guarantees this entry has not been cleared. apply(entry.handler!, receiver, entry.args!); } catch (error) { report(error); } finally { nesting = 0; } }, () => { if (get(timers, entry.id) === entry) { if (entry.repeat) { const level = entry.nesting++; entry.deadline = now(null) + (level > 5 && entry.delay < 4 ? 4 : entry.delay); insert(entry); } else { remove(timers, entry.id); if (!entry.privateTimer) referencedCount--; } } firing = false; schedule(); }); }, (error) => { if (pending !== record) return; pending = null; report(error); }); } function start(handler: unknown, rawDelay: unknown, args: unknown[], repeat: boolean, privateTimer = false): number { if (typeof handler !== "function") handler = string(handler); // Keep unary +: Number() would accept BigInt. let delay = +(rawDelay as number) >> 0; if (typeof handler !== "function") missing("string timer callbacks"); if (delay < 0) delay = 0; const id = nextId; do { nextId = nextId === 2147483647 ? 1 : nextId + 1; } while (has(timers, nextId)); const entry = { id, handler, args, repeat, delay, privateTimer, nesting: nesting + 1, deadline: now(null) + (!privateTimer && nesting > 5 && delay < 4 ? 4 : delay), sequence: 0, }; set(timers, id, entry); if (!privateTimer) referencedCount++; insert(entry); schedule(); return id; } function scheduleTimeout(handler: () => void, milliseconds: number): void { let remaining = milliseconds, previous = now(null); function fire(): void { const current = now(null); remaining -= current - previous; previous = current; if (remaining <= 0) handler(); else start(fire, min(remaining, 2147483647), array(), false, true); } start(fire, min(remaining, 2147483647), array(), false, true); } function clear(rawId: unknown): void { const id = +(rawId as number) >> 0; const entry = get(timers, id); if (!entry || entry.privateTimer) return; remove(timers, id); referencedCount--; entry.handler = null; entry.args = null; stale++; schedule(); } const api = { setTimeout(handler: unknown, timeout: unknown = 0, ...args: unknown[]): number { required(arguments.length, 1); return start(handler, timeout, args, false); }, setInterval(handler: unknown, timeout: unknown = 0, ...args: unknown[]): number { required(arguments.length, 1); return start(handler, timeout, args, true); }, clearTimeout(id: unknown = 0): void { clear(id); }, clearInterval(id: unknown = 0): void { clear(id); }, queueMicrotask(callback: unknown): void { required(arguments.length, 1); if (typeof callback !== "function") { throw new TypeError("Expected a microtask callback"); } enqueueMicrotask(callback); }, }; const { setTimeout, setInterval, clearTimeout, clearInterval, queueMicrotask } = api; export { clearInterval, clearTimeout, queueMicrotask, scheduleTimeout, setInterval, setTimeout, };