mirror of
https://github.com/denoland/deno.git
synced 2024-11-26 16:09:27 -05:00
394 lines
11 KiB
JavaScript
394 lines
11 KiB
JavaScript
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
|
"use strict";
|
|
|
|
((window) => {
|
|
const core = window.Deno.core;
|
|
const {
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeShift,
|
|
Error,
|
|
FunctionPrototypeCall,
|
|
Map,
|
|
MapPrototypeDelete,
|
|
MapPrototypeGet,
|
|
MapPrototypeHas,
|
|
MapPrototypeSet,
|
|
// deno-lint-ignore camelcase
|
|
NumberPOSITIVE_INFINITY,
|
|
PromisePrototypeThen,
|
|
ObjectPrototypeIsPrototypeOf,
|
|
SafeArrayIterator,
|
|
SymbolFor,
|
|
TypeError,
|
|
} = window.__bootstrap.primordials;
|
|
const { webidl } = window.__bootstrap;
|
|
|
|
// Shamelessly cribbed from extensions/fetch/11_streams.js
|
|
class AssertionError extends Error {
|
|
constructor(msg) {
|
|
super(msg);
|
|
this.name = "AssertionError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} cond
|
|
* @param {string=} msg
|
|
* @returns {asserts cond}
|
|
*/
|
|
function assert(cond, msg = "Assertion failed.") {
|
|
if (!cond) {
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
|
|
function opNow() {
|
|
return core.opSync("op_now");
|
|
}
|
|
|
|
function sleepSync(millis = 0) {
|
|
return core.opSync("op_sleep_sync", millis);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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 = core.opSync("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;
|
|
}
|
|
|
|
// 2.
|
|
// 3.
|
|
// TODO(@andreubotella): Error handling.
|
|
if (typeof callback === "function") {
|
|
FunctionPrototypeCall(
|
|
callback,
|
|
globalThis,
|
|
...new SafeArrayIterator(args),
|
|
);
|
|
} else {
|
|
// TODO(@andreubotella): eval doesn't seem to have a primordial, but
|
|
// it can be redefined in the global scope.
|
|
(0, eval)(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,
|
|
};
|
|
|
|
// 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,
|
|
};
|
|
|
|
// 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,
|
|
() => {
|
|
// 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;
|
|
}
|
|
},
|
|
(err) => {
|
|
if (ObjectPrototypeIsPrototypeOf(core.InterruptedPrototype, err)) {
|
|
// The timer was cancelled.
|
|
removeFromScheduledTimers(timerObject);
|
|
} else {
|
|
throw err;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/** @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 checkThis(thisArg) {
|
|
if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) {
|
|
throw new TypeError("Illegal invocation");
|
|
}
|
|
}
|
|
|
|
function setTimeout(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, false);
|
|
}
|
|
|
|
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 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 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);
|
|
}
|
|
|
|
window.__bootstrap.timers = {
|
|
setTimeout,
|
|
setInterval,
|
|
clearTimeout,
|
|
clearInterval,
|
|
handleTimerMacrotask,
|
|
opNow,
|
|
sleepSync,
|
|
refTimer,
|
|
unrefTimer,
|
|
};
|
|
})(this);
|