// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. "use strict"; ((window) => { const assert = window.__bootstrap.util.assert; const core = window.Deno.core; const { sendSync } = window.__bootstrap.dispatchMinimal; function opStopGlobalTimer() { core.jsonOpSync("op_global_timer_stop"); } function opStartGlobalTimer(timeout) { return core.jsonOpSync("op_global_timer_start", { timeout }); } async function opWaitGlobalTimer() { await core.jsonOpAsync("op_global_timer"); } const nowBytes = new Uint8Array(8); function opNow() { sendSync("op_now", 0, nowBytes); return new DataView(nowBytes.buffer).getFloat64(); } function sleepSync(millis = 0) { return core.jsonOpSync("op_sleep_sync", { millis }); } // Derived from https://github.com/vadimg/js_bintrees. MIT Licensed. class RBNode { constructor(data) { this.data = data; this.left = null; this.right = null; this.red = true; } getChild(dir) { return dir ? this.right : this.left; } setChild(dir, val) { if (dir) { this.right = val; } else { this.left = val; } } } class RBTree { #comparator = null; #root = null; constructor(comparator) { this.#comparator = comparator; this.#root = null; } /** Returns `null` if tree is empty. */ min() { let res = this.#root; if (res === null) { return null; } while (res.left !== null) { res = res.left; } return res.data; } /** Returns node `data` if found, `null` otherwise. */ find(data) { let res = this.#root; while (res !== null) { const c = this.#comparator(data, res.data); if (c === 0) { return res.data; } else { res = res.getChild(c > 0); } } return null; } /** returns `true` if inserted, `false` if duplicate. */ insert(data) { let ret = false; if (this.#root === null) { // empty tree this.#root = new RBNode(data); ret = true; } else { const head = new RBNode(null); // fake tree root let dir = 0; let last = 0; // setup let gp = null; // grandparent let ggp = head; // grand-grand-parent let p = null; // parent let node = this.#root; ggp.right = this.#root; // search down while (true) { if (node === null) { // insert new node at the bottom node = new RBNode(data); p.setChild(dir, node); ret = true; } else if (isRed(node.left) && isRed(node.right)) { // color flip node.red = true; node.left.red = false; node.right.red = false; } // fix red violation if (isRed(node) && isRed(p)) { const dir2 = ggp.right === gp; assert(gp); if (node === p.getChild(last)) { ggp.setChild(dir2, singleRotate(gp, !last)); } else { ggp.setChild(dir2, doubleRotate(gp, !last)); } } const cmp = this.#comparator(node.data, data); // stop if found if (cmp === 0) { break; } last = dir; dir = Number(cmp < 0); // Fix type // update helpers if (gp !== null) { ggp = gp; } gp = p; p = node; node = node.getChild(dir); } // update root this.#root = head.right; } // make root black this.#root.red = false; return ret; } /** Returns `true` if removed, `false` if not found. */ remove(data) { if (this.#root === null) { return false; } const head = new RBNode(null); // fake tree root let node = head; node.right = this.#root; let p = null; // parent let gp = null; // grand parent let found = null; // found item let dir = 1; while (node.getChild(dir) !== null) { const last = dir; // update helpers gp = p; p = node; node = node.getChild(dir); const cmp = this.#comparator(data, node.data); dir = cmp > 0; // save found node if (cmp === 0) { found = node; } // push the red node down if (!isRed(node) && !isRed(node.getChild(dir))) { if (isRed(node.getChild(!dir))) { const sr = singleRotate(node, dir); p.setChild(last, sr); p = sr; } else if (!isRed(node.getChild(!dir))) { const sibling = p.getChild(!last); if (sibling !== null) { if ( !isRed(sibling.getChild(!last)) && !isRed(sibling.getChild(last)) ) { // color flip p.red = false; sibling.red = true; node.red = true; } else { assert(gp); const dir2 = gp.right === p; if (isRed(sibling.getChild(last))) { gp.setChild(dir2, doubleRotate(p, last)); } else if (isRed(sibling.getChild(!last))) { gp.setChild(dir2, singleRotate(p, last)); } // ensure correct coloring const gpc = gp.getChild(dir2); assert(gpc); gpc.red = true; node.red = true; assert(gpc.left); gpc.left.red = false; assert(gpc.right); gpc.right.red = false; } } } } } // replace and remove if found if (found !== null) { found.data = node.data; assert(p); p.setChild(p.right === node, node.getChild(node.left === null)); } // update root and make it black this.#root = head.right; if (this.#root !== null) { this.#root.red = false; } return found !== null; } } function isRed(node) { return node !== null && node.red; } function singleRotate(root, dir) { const save = root.getChild(!dir); assert(save); root.setChild(!dir, save.getChild(dir)); save.setChild(dir, root); root.red = true; save.red = false; return save; } function doubleRotate(root, dir) { root.setChild(!dir, singleRotate(root.getChild(!dir), !dir)); return singleRotate(root, dir); } const { console } = globalThis; const OriginalDateNow = Date.now; // Timeout values > TIMEOUT_MAX are set to 1. const TIMEOUT_MAX = 2 ** 31 - 1; let globalTimeoutDue = null; let nextTimerId = 1; const idMap = new Map(); const dueTree = new RBTree((a, b) => a.due - b.due); function clearGlobalTimeout() { globalTimeoutDue = null; opStopGlobalTimer(); } let pendingEvents = 0; const pendingFireTimers = []; /** Process and run a single ready timer macrotask. * This function should be registered through Deno.core.setMacrotaskCallback. * Returns true when all ready macrotasks have been processed, false if more * ready ones are available. The Isolate future would rely on the return value * to repeatedly invoke this function until depletion. Multiple invocations * of this function one at a time ensures newly ready microtasks are processed * before next macrotask timer callback is invoked. */ function handleTimerMacrotask() { if (pendingFireTimers.length > 0) { fire(pendingFireTimers.shift()); return pendingFireTimers.length === 0; } return true; } async function setGlobalTimeout(due, now) { // Since JS and Rust don't use the same clock, pass the time to rust as a // relative time value. On the Rust side we'll turn that into an absolute // value again. const timeout = due - now; assert(timeout >= 0); // Send message to the backend. globalTimeoutDue = due; pendingEvents++; // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` // is synchronous. That means that timer is cancelled, but this promise is still pending // until next turn of event loop. This leads to "leaking of async ops" in tests; // because `clearTimeout/clearInterval` might be the last statement in test function // `opSanitizer` will immediately complain that there is pending op going on, unless // some timeout/defer is put in place to allow promise resolution. // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but // I'm not if that's possible. opStartGlobalTimer(timeout); await opWaitGlobalTimer(); pendingEvents--; // eslint-disable-next-line @typescript-eslint/no-use-before-define prepareReadyTimers(); } function prepareReadyTimers() { const now = OriginalDateNow(); // Bail out if we're not expecting the global timer to fire. if (globalTimeoutDue === null || pendingEvents > 0) { return; } // After firing the timers that are due now, this will hold the first timer // list that hasn't fired yet. let nextDueNode; while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { dueTree.remove(nextDueNode); // Fire all the timers in the list. for (const timer of nextDueNode.timers) { // With the list dropped, the timer is no longer scheduled. timer.scheduled = false; // Place the callback to pending timers to fire. pendingFireTimers.push(timer); } } setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); } function setOrClearGlobalTimeout(due, now) { if (due == null) { clearGlobalTimeout(); } else { setGlobalTimeout(due, now); } } function schedule(timer, now) { assert(!timer.scheduled); assert(now <= timer.due); // Find or create the list of timers that will fire at point-in-time `due`. const maybeNewDueNode = { due: timer.due, timers: [] }; let dueNode = dueTree.find(maybeNewDueNode); if (dueNode === null) { dueTree.insert(maybeNewDueNode); dueNode = maybeNewDueNode; } // Append the newly scheduled timer to the list and mark it as scheduled. dueNode.timers.push(timer); timer.scheduled = true; // If the new timer is scheduled to fire before any timer that existed before, // update the global timeout to reflect this. if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { setOrClearGlobalTimeout(timer.due, now); } } function unschedule(timer) { // Check if our timer is pending scheduling or pending firing. // If either is true, they are not in tree, and their idMap entry // will be deleted soon. Remove it from queue. let index = -1; if ((index = pendingFireTimers.indexOf(timer)) >= 0) { pendingFireTimers.splice(index); return; } // If timer is not in the 2 pending queues and is unscheduled, // it is not in the tree. if (!timer.scheduled) { return; } const searchKey = { due: timer.due, timers: [] }; // Find the list of timers that will fire at point-in-time `due`. const list = dueTree.find(searchKey).timers; if (list.length === 1) { // Time timer is the only one in the list. Remove the entire list. assert(list[0] === timer); dueTree.remove(searchKey); // If the unscheduled timer was 'next up', find when the next timer that // still exists is due, and update the global alarm accordingly. if (timer.due === globalTimeoutDue) { const nextDueNode = dueTree.min(); setOrClearGlobalTimeout( nextDueNode && nextDueNode.due, OriginalDateNow(), ); } } else { // Multiple timers that are due at the same point in time. // Remove this timer from the list. const index = list.indexOf(timer); assert(index > -1); list.splice(index, 1); } } function fire(timer) { // If the timer isn't found in the ID map, that means it has been cancelled // between the timer firing and the promise callback (this function). if (!idMap.has(timer.id)) { return; } // Reschedule the timer if it is a repeating one, otherwise drop it. if (!timer.repeat) { // One-shot timer: remove the timer from this id-to-timer map. idMap.delete(timer.id); } else { // Interval timer: compute when timer was supposed to fire next. // However make sure to never schedule the next interval in the past. const now = OriginalDateNow(); timer.due = Math.max(now, timer.due + timer.delay); schedule(timer, now); } // Call the user callback. Intermediate assignment is to avoid leaking `this` // to it, while also keeping the stack trace neat when it shows up in there. const callback = timer.callback; if ("function" === typeof callback) { callback(); } else { eval(callback); } } function checkThis(thisArg) { if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { throw new TypeError("Illegal invocation"); } } function setTimer( cb, delay, args, repeat, ) { // If the callack is a function, bind `args` to the callback and bind `this` to globalThis(global). // otherwise call `String` on it, and `eval` it on calls; do not pass variardic args to the string let callback; if ("function" === typeof cb) { callback = Function.prototype.bind.call(cb, globalThis, ...args); } else { callback = String(cb); args = []; // args are ignored } // In the browser, the delay value must be coercible to an integer between 0 // and INT32_MAX. Any other value will cause the timer to fire immediately. // We emulate this behavior. const now = OriginalDateNow(); if (delay > TIMEOUT_MAX) { console.warn( `${delay} does not fit into` + " a 32-bit signed integer." + "\nTimeout duration was set to 1.", ); delay = 1; } delay = Math.max(0, delay | 0); // Create a new, unscheduled timer object. const timer = { id: nextTimerId++, callback, args, delay, due: now + delay, repeat, scheduled: false, }; // Register the timer's existence in the id-to-timer map. idMap.set(timer.id, timer); // Schedule the timer in the due table. schedule(timer, now); return timer.id; } function setTimeout( cb, delay = 0, ...args ) { delay >>>= 0; checkThis(this); return setTimer(cb, delay, args, false); } function setInterval( cb, delay = 0, ...args ) { delay >>>= 0; checkThis(this); return setTimer(cb, delay, args, true); } function clearTimer(id) { id >>>= 0; const timer = idMap.get(id); if (timer === undefined) { // Timer doesn't exist any more or never existed. This is not an error. return; } // Unschedule the timer if it is currently scheduled, and forget about it. unschedule(timer); idMap.delete(timer.id); } function clearTimeout(id = 0) { id >>>= 0; if (id === 0) { return; } clearTimer(id); } function clearInterval(id = 0) { id >>>= 0; if (id === 0) { return; } clearTimer(id); } window.__bootstrap.timers = { clearInterval, setInterval, clearTimeout, setTimeout, handleTimerMacrotask, opStopGlobalTimer, opStartGlobalTimer, opNow, sleepSync, }; })(this);