summaryrefslogtreecommitdiff
path: root/ext/web/02_timers.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/web/02_timers.js')
-rw-r--r--ext/web/02_timers.js691
1 files changed, 344 insertions, 347 deletions
diff --git a/ext/web/02_timers.js b/ext/web/02_timers.js
index a582cf428..302b6f62c 100644
--- a/ext/web/02_timers.js
+++ b/ext/web/02_timers.js
@@ -1,375 +1,372 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-"use strict";
-
-((window) => {
- const core = window.Deno.core;
- const ops = core.ops;
- const {
- ArrayPrototypePush,
- ArrayPrototypeShift,
- FunctionPrototypeCall,
- Map,
- MapPrototypeDelete,
- MapPrototypeGet,
- MapPrototypeHas,
- MapPrototypeSet,
- Uint8Array,
- Uint32Array,
- // deno-lint-ignore camelcase
- NumberPOSITIVE_INFINITY,
- PromisePrototypeThen,
- SafeArrayIterator,
- SymbolFor,
- TypeError,
- indirectEval,
- } = window.__bootstrap.primordials;
- const { webidl } = window.__bootstrap;
- const { reportException } = window.__bootstrap.event;
- const { assert } = window.__bootstrap.infra;
-
- const hrU8 = new Uint8Array(8);
- const hr = new Uint32Array(hrU8.buffer);
- function opNow() {
- ops.op_now(hrU8);
- return (hr[0] * 1000 + hr[1] / 1e6);
- }
- // ---------------------------------------------------------------------------
-
- /**
- * The task queue corresponding to the timer task source.
- *
- * @type { {action: () => void, nestingLevel: number}[] }
- */
- const timerTasks = [];
-
- /**
- * The current task's timer nesting level, or zero if we're not currently
- * running a timer task (since the minimum nesting level is 1).
- *
- * @type {number}
- */
- let timerNestingLevel = 0;
-
- function handleTimerMacrotask() {
- if (timerTasks.length === 0) {
- return true;
- }
-
- const task = ArrayPrototypeShift(timerTasks);
-
- timerNestingLevel = task.nestingLevel;
-
- try {
- task.action();
- } finally {
- timerNestingLevel = 0;
- }
- return timerTasks.length === 0;
+const core = globalThis.Deno.core;
+const ops = core.ops;
+const primordials = globalThis.__bootstrap.primordials;
+const {
+ ArrayPrototypePush,
+ ArrayPrototypeShift,
+ FunctionPrototypeCall,
+ Map,
+ MapPrototypeDelete,
+ MapPrototypeGet,
+ MapPrototypeHas,
+ MapPrototypeSet,
+ Uint8Array,
+ Uint32Array,
+ // deno-lint-ignore camelcase
+ NumberPOSITIVE_INFINITY,
+ PromisePrototypeThen,
+ SafeArrayIterator,
+ SymbolFor,
+ TypeError,
+ indirectEval,
+} = primordials;
+import * as webidl from "internal:ext/webidl/00_webidl.js";
+import { reportException } from "internal:ext/web/02_event.js";
+import { assert } from "internal:ext/web/00_infra.js";
+
+const hrU8 = new Uint8Array(8);
+const hr = new Uint32Array(hrU8.buffer);
+function opNow() {
+ ops.op_now(hrU8);
+ return (hr[0] * 1000 + hr[1] / 1e6);
+}
+
+// ---------------------------------------------------------------------------
+
+/**
+ * The task queue corresponding to the timer task source.
+ *
+ * @type { {action: () => void, nestingLevel: number}[] }
+ */
+const timerTasks = [];
+
+/**
+ * The current task's timer nesting level, or zero if we're not currently
+ * running a timer task (since the minimum nesting level is 1).
+ *
+ * @type {number}
+ */
+let timerNestingLevel = 0;
+
+function handleTimerMacrotask() {
+ if (timerTasks.length === 0) {
+ return true;
}
- // ---------------------------------------------------------------------------
-
- /**
- * The keys in this map correspond to the key ID's in the spec's map of active
- * timers. The values are the timeout's cancel rid.
- *
- * @type {Map<number, { cancelRid: number, isRef: boolean, promiseId: number }>}
- */
- const activeTimers = new Map();
-
- let nextId = 1;
-
- /**
- * @param {Function | string} callback
- * @param {number} timeout
- * @param {Array<any>} args
- * @param {boolean} repeat
- * @param {number | undefined} prevId
- * @returns {number} The timer ID
- */
- function initializeTimer(
- callback,
- timeout,
- args,
- repeat,
- prevId,
- ) {
- // 2. If previousId was given, let id be previousId; otherwise, let
- // previousId be an implementation-defined integer than is greater than zero
- // and does not already exist in global's map of active timers.
- let id;
- let timerInfo;
- if (prevId !== undefined) {
- // `prevId` is only passed for follow-up calls on intervals
- assert(repeat);
- id = prevId;
- timerInfo = MapPrototypeGet(activeTimers, id);
- } else {
- // TODO(@andreubotella): Deal with overflow.
- // https://github.com/whatwg/html/issues/7358
- id = nextId++;
- const cancelRid = ops.op_timer_handle();
- timerInfo = { cancelRid, isRef: true, promiseId: -1 };
-
- // Step 4 in "run steps after a timeout".
- MapPrototypeSet(activeTimers, id, timerInfo);
- }
-
- // 3. If the surrounding agent's event loop's currently running task is a
- // task that was created by this algorithm, then let nesting level be the
- // task's timer nesting level. Otherwise, let nesting level be zero.
- // 4. If timeout is less than 0, then set timeout to 0.
- // 5. If nesting level is greater than 5, and timeout is less than 4, then
- // set timeout to 4.
- //
- // The nesting level of 5 and minimum of 4 ms are spec-mandated magic
- // constants.
- if (timeout < 0) timeout = 0;
- if (timerNestingLevel > 5 && timeout < 4) timeout = 4;
-
- // 9. Let task be a task that runs the following steps:
- const task = {
- action: () => {
- // 1. If id does not exist in global's map of active timers, then abort
- // these steps.
- //
- // This is relevant if the timer has been canceled after the sleep op
- // resolves but before this task runs.
- if (!MapPrototypeHas(activeTimers, id)) {
- return;
- }
+ const task = ArrayPrototypeShift(timerTasks);
- // 2.
- // 3.
- if (typeof callback === "function") {
- try {
- FunctionPrototypeCall(
- callback,
- globalThis,
- ...new SafeArrayIterator(args),
- );
- } catch (error) {
- reportException(error);
- }
- } else {
- indirectEval(callback);
- }
+ timerNestingLevel = task.nestingLevel;
- if (repeat) {
- if (MapPrototypeHas(activeTimers, id)) {
- // 4. If id does not exist in global's map of active timers, then
- // abort these steps.
- // NOTE: If might have been removed via the author code in handler
- // calling clearTimeout() or clearInterval().
- // 5. If repeat is true, then perform the timer initialization steps
- // again, given global, handler, timeout, arguments, true, and id.
- initializeTimer(callback, timeout, args, true, id);
- }
- } else {
- // 6. Otherwise, remove global's map of active timers[id].
- core.tryClose(timerInfo.cancelRid);
- MapPrototypeDelete(activeTimers, id);
- }
- },
-
- // 10. Increment nesting level by one.
- // 11. Set task's timer nesting level to nesting level.
- nestingLevel: timerNestingLevel + 1,
- };
-
- // 12. Let completionStep be an algorithm step which queues a global task on
- // the timer task source given global to run task.
- // 13. Run steps after a timeout given global, "setTimeout/setInterval",
- // timeout, completionStep, and id.
- runAfterTimeout(
- () => ArrayPrototypePush(timerTasks, task),
- timeout,
- timerInfo,
- );
-
- return id;
+ try {
+ task.action();
+ } finally {
+ timerNestingLevel = 0;
+ }
+ return timerTasks.length === 0;
+}
+
+// ---------------------------------------------------------------------------
+
+/**
+ * The keys in this map correspond to the key ID's in the spec's map of active
+ * timers. The values are the timeout's cancel rid.
+ *
+ * @type {Map<number, { cancelRid: number, isRef: boolean, promiseId: number }>}
+ */
+const activeTimers = new Map();
+
+let nextId = 1;
+
+/**
+ * @param {Function | string} callback
+ * @param {number} timeout
+ * @param {Array<any>} args
+ * @param {boolean} repeat
+ * @param {number | undefined} prevId
+ * @returns {number} The timer ID
+ */
+function initializeTimer(
+ callback,
+ timeout,
+ args,
+ repeat,
+ prevId,
+) {
+ // 2. If previousId was given, let id be previousId; otherwise, let
+ // previousId be an implementation-defined integer than is greater than zero
+ // and does not already exist in global's map of active timers.
+ let id;
+ let timerInfo;
+ if (prevId !== undefined) {
+ // `prevId` is only passed for follow-up calls on intervals
+ assert(repeat);
+ id = prevId;
+ timerInfo = MapPrototypeGet(activeTimers, id);
+ } else {
+ // TODO(@andreubotella): Deal with overflow.
+ // https://github.com/whatwg/html/issues/7358
+ id = nextId++;
+ const cancelRid = ops.op_timer_handle();
+ timerInfo = { cancelRid, isRef: true, promiseId: -1 };
+
+ // Step 4 in "run steps after a timeout".
+ MapPrototypeSet(activeTimers, id, timerInfo);
}
- // ---------------------------------------------------------------------------
-
- /**
- * @typedef ScheduledTimer
- * @property {number} millis
- * @property {() => void} cb
- * @property {boolean} resolved
- * @property {ScheduledTimer | null} prev
- * @property {ScheduledTimer | null} next
- */
-
- /**
- * A doubly linked list of timers.
- * @type { { head: ScheduledTimer | null, tail: ScheduledTimer | null } }
- */
- const scheduledTimers = { head: null, tail: null };
-
- /**
- * @param {() => void} cb Will be run after the timeout, if it hasn't been
- * cancelled.
- * @param {number} millis
- * @param {{ cancelRid: number, isRef: boolean, promiseId: number }} timerInfo
- */
- function runAfterTimeout(cb, millis, timerInfo) {
- const cancelRid = timerInfo.cancelRid;
- const sleepPromise = core.opAsync("op_sleep", millis, cancelRid);
- timerInfo.promiseId =
- sleepPromise[SymbolFor("Deno.core.internalPromiseId")];
- if (!timerInfo.isRef) {
- core.unrefOp(timerInfo.promiseId);
- }
-
- /** @type {ScheduledTimer} */
- const timerObject = {
- millis,
- cb,
- resolved: false,
- prev: scheduledTimers.tail,
- next: null,
- };
-
- // Add timerObject to the end of the list.
- if (scheduledTimers.tail === null) {
- assert(scheduledTimers.head === null);
- scheduledTimers.head = scheduledTimers.tail = timerObject;
- } else {
- scheduledTimers.tail.next = timerObject;
- scheduledTimers.tail = timerObject;
- }
-
- // 1.
- PromisePrototypeThen(
- sleepPromise,
- (cancelled) => {
- if (!cancelled) {
- // The timer was cancelled.
- removeFromScheduledTimers(timerObject);
- return;
+ // 3. If the surrounding agent's event loop's currently running task is a
+ // task that was created by this algorithm, then let nesting level be the
+ // task's timer nesting level. Otherwise, let nesting level be zero.
+ // 4. If timeout is less than 0, then set timeout to 0.
+ // 5. If nesting level is greater than 5, and timeout is less than 4, then
+ // set timeout to 4.
+ //
+ // The nesting level of 5 and minimum of 4 ms are spec-mandated magic
+ // constants.
+ if (timeout < 0) timeout = 0;
+ if (timerNestingLevel > 5 && timeout < 4) timeout = 4;
+
+ // 9. Let task be a task that runs the following steps:
+ const task = {
+ action: () => {
+ // 1. If id does not exist in global's map of active timers, then abort
+ // these steps.
+ //
+ // This is relevant if the timer has been canceled after the sleep op
+ // resolves but before this task runs.
+ if (!MapPrototypeHas(activeTimers, id)) {
+ return;
+ }
+
+ // 2.
+ // 3.
+ if (typeof callback === "function") {
+ try {
+ FunctionPrototypeCall(
+ callback,
+ globalThis,
+ ...new SafeArrayIterator(args),
+ );
+ } catch (error) {
+ reportException(error);
}
- // 2. Wait until any invocations of this algorithm that had the same
- // global and orderingIdentifier, that started before this one, and
- // whose milliseconds is equal to or less than this one's, have
- // completed.
- // 4. Perform completionSteps.
-
- // IMPORTANT: Since the sleep ops aren't guaranteed to resolve in the
- // right order, whenever one resolves, we run through the scheduled
- // timers list (which is in the order in which they were scheduled), and
- // we call the callback for every timer which both:
- // a) has resolved, and
- // b) its timeout is lower than the lowest unresolved timeout found so
- // far in the list.
-
- timerObject.resolved = true;
-
- let lowestUnresolvedTimeout = NumberPOSITIVE_INFINITY;
-
- let currentEntry = scheduledTimers.head;
- while (currentEntry !== null) {
- if (currentEntry.millis < lowestUnresolvedTimeout) {
- if (currentEntry.resolved) {
- currentEntry.cb();
- removeFromScheduledTimers(currentEntry);
- } else {
- lowestUnresolvedTimeout = currentEntry.millis;
- }
- }
-
- currentEntry = currentEntry.next;
+ } else {
+ indirectEval(callback);
+ }
+
+ if (repeat) {
+ if (MapPrototypeHas(activeTimers, id)) {
+ // 4. If id does not exist in global's map of active timers, then
+ // abort these steps.
+ // NOTE: If might have been removed via the author code in handler
+ // calling clearTimeout() or clearInterval().
+ // 5. If repeat is true, then perform the timer initialization steps
+ // again, given global, handler, timeout, arguments, true, and id.
+ initializeTimer(callback, timeout, args, true, id);
}
- },
- );
- }
+ } else {
+ // 6. Otherwise, remove global's map of active timers[id].
+ core.tryClose(timerInfo.cancelRid);
+ MapPrototypeDelete(activeTimers, id);
+ }
+ },
+
+ // 10. Increment nesting level by one.
+ // 11. Set task's timer nesting level to nesting level.
+ nestingLevel: timerNestingLevel + 1,
+ };
- /** @param {ScheduledTimer} timerObj */
- function removeFromScheduledTimers(timerObj) {
- if (timerObj.prev !== null) {
- timerObj.prev.next = timerObj.next;
- } else {
- assert(scheduledTimers.head === timerObj);
- scheduledTimers.head = timerObj.next;
- }
- if (timerObj.next !== null) {
- timerObj.next.prev = timerObj.prev;
- } else {
- assert(scheduledTimers.tail === timerObj);
- scheduledTimers.tail = timerObj.prev;
- }
+ // 12. Let completionStep be an algorithm step which queues a global task on
+ // the timer task source given global to run task.
+ // 13. Run steps after a timeout given global, "setTimeout/setInterval",
+ // timeout, completionStep, and id.
+ runAfterTimeout(
+ () => ArrayPrototypePush(timerTasks, task),
+ timeout,
+ timerInfo,
+ );
+
+ return id;
+}
+
+// ---------------------------------------------------------------------------
+
+/**
+ * @typedef ScheduledTimer
+ * @property {number} millis
+ * @property {() => void} cb
+ * @property {boolean} resolved
+ * @property {ScheduledTimer | null} prev
+ * @property {ScheduledTimer | null} next
+ */
+
+/**
+ * A doubly linked list of timers.
+ * @type { { head: ScheduledTimer | null, tail: ScheduledTimer | null } }
+ */
+const scheduledTimers = { head: null, tail: null };
+
+/**
+ * @param {() => void} cb Will be run after the timeout, if it hasn't been
+ * cancelled.
+ * @param {number} millis
+ * @param {{ cancelRid: number, isRef: boolean, promiseId: number }} timerInfo
+ */
+function runAfterTimeout(cb, millis, timerInfo) {
+ const cancelRid = timerInfo.cancelRid;
+ const sleepPromise = core.opAsync("op_sleep", millis, cancelRid);
+ timerInfo.promiseId = sleepPromise[SymbolFor("Deno.core.internalPromiseId")];
+ if (!timerInfo.isRef) {
+ core.unrefOp(timerInfo.promiseId);
}
- // ---------------------------------------------------------------------------
+ /** @type {ScheduledTimer} */
+ const timerObject = {
+ millis,
+ cb,
+ resolved: false,
+ prev: scheduledTimers.tail,
+ next: null,
+ };
- function checkThis(thisArg) {
- if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) {
- throw new TypeError("Illegal invocation");
- }
+ // Add timerObject to the end of the list.
+ if (scheduledTimers.tail === null) {
+ assert(scheduledTimers.head === null);
+ scheduledTimers.head = scheduledTimers.tail = timerObject;
+ } else {
+ scheduledTimers.tail.next = timerObject;
+ scheduledTimers.tail = timerObject;
}
- function setTimeout(callback, timeout = 0, ...args) {
- checkThis(this);
- if (typeof callback !== "function") {
- callback = webidl.converters.DOMString(callback);
- }
- timeout = webidl.converters.long(timeout);
+ // 1.
+ PromisePrototypeThen(
+ sleepPromise,
+ (cancelled) => {
+ if (!cancelled) {
+ // The timer was cancelled.
+ removeFromScheduledTimers(timerObject);
+ return;
+ }
+ // 2. Wait until any invocations of this algorithm that had the same
+ // global and orderingIdentifier, that started before this one, and
+ // whose milliseconds is equal to or less than this one's, have
+ // completed.
+ // 4. Perform completionSteps.
+
+ // IMPORTANT: Since the sleep ops aren't guaranteed to resolve in the
+ // right order, whenever one resolves, we run through the scheduled
+ // timers list (which is in the order in which they were scheduled), and
+ // we call the callback for every timer which both:
+ // a) has resolved, and
+ // b) its timeout is lower than the lowest unresolved timeout found so
+ // far in the list.
+
+ timerObject.resolved = true;
+
+ let lowestUnresolvedTimeout = NumberPOSITIVE_INFINITY;
+
+ let currentEntry = scheduledTimers.head;
+ while (currentEntry !== null) {
+ if (currentEntry.millis < lowestUnresolvedTimeout) {
+ if (currentEntry.resolved) {
+ currentEntry.cb();
+ removeFromScheduledTimers(currentEntry);
+ } else {
+ lowestUnresolvedTimeout = currentEntry.millis;
+ }
+ }
- return initializeTimer(callback, timeout, args, false);
+ currentEntry = currentEntry.next;
+ }
+ },
+ );
+}
+
+/** @param {ScheduledTimer} timerObj */
+function removeFromScheduledTimers(timerObj) {
+ if (timerObj.prev !== null) {
+ timerObj.prev.next = timerObj.next;
+ } else {
+ assert(scheduledTimers.head === timerObj);
+ scheduledTimers.head = timerObj.next;
+ }
+ if (timerObj.next !== null) {
+ timerObj.next.prev = timerObj.prev;
+ } else {
+ assert(scheduledTimers.tail === timerObj);
+ scheduledTimers.tail = timerObj.prev;
}
+}
- function setInterval(callback, timeout = 0, ...args) {
- checkThis(this);
- if (typeof callback !== "function") {
- callback = webidl.converters.DOMString(callback);
- }
- timeout = webidl.converters.long(timeout);
+// ---------------------------------------------------------------------------
- return initializeTimer(callback, timeout, args, true);
+function checkThis(thisArg) {
+ if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) {
+ throw new TypeError("Illegal invocation");
}
+}
- function clearTimeout(id = 0) {
- checkThis(this);
- id = webidl.converters.long(id);
- const timerInfo = MapPrototypeGet(activeTimers, id);
- if (timerInfo !== undefined) {
- core.tryClose(timerInfo.cancelRid);
- MapPrototypeDelete(activeTimers, id);
- }
+function setTimeout(callback, timeout = 0, ...args) {
+ checkThis(this);
+ if (typeof callback !== "function") {
+ callback = webidl.converters.DOMString(callback);
}
+ timeout = webidl.converters.long(timeout);
- function clearInterval(id = 0) {
- checkThis(this);
- clearTimeout(id);
- }
+ return initializeTimer(callback, timeout, args, false);
+}
- function refTimer(id) {
- const timerInfo = MapPrototypeGet(activeTimers, id);
- if (timerInfo === undefined || timerInfo.isRef) {
- return;
- }
- timerInfo.isRef = true;
- core.refOp(timerInfo.promiseId);
+function setInterval(callback, timeout = 0, ...args) {
+ checkThis(this);
+ if (typeof callback !== "function") {
+ callback = webidl.converters.DOMString(callback);
}
-
- function unrefTimer(id) {
- const timerInfo = MapPrototypeGet(activeTimers, id);
- if (timerInfo === undefined || !timerInfo.isRef) {
- return;
- }
- timerInfo.isRef = false;
- core.unrefOp(timerInfo.promiseId);
+ timeout = webidl.converters.long(timeout);
+
+ return initializeTimer(callback, timeout, args, true);
+}
+
+function clearTimeout(id = 0) {
+ checkThis(this);
+ id = webidl.converters.long(id);
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo !== undefined) {
+ core.tryClose(timerInfo.cancelRid);
+ MapPrototypeDelete(activeTimers, id);
}
+}
- window.__bootstrap.timers = {
- setTimeout,
- setInterval,
- clearTimeout,
- clearInterval,
- handleTimerMacrotask,
- opNow,
- refTimer,
- unrefTimer,
- };
-})(this);
+function clearInterval(id = 0) {
+ checkThis(this);
+ clearTimeout(id);
+}
+
+function refTimer(id) {
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo === undefined || timerInfo.isRef) {
+ return;
+ }
+ timerInfo.isRef = true;
+ core.refOp(timerInfo.promiseId);
+}
+
+function unrefTimer(id) {
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo === undefined || !timerInfo.isRef) {
+ return;
+ }
+ timerInfo.isRef = false;
+ core.unrefOp(timerInfo.promiseId);
+}
+
+export {
+ clearInterval,
+ clearTimeout,
+ handleTimerMacrotask,
+ opNow,
+ refTimer,
+ setInterval,
+ setTimeout,
+ unrefTimer,
+};