UNPKG

dcp-client

Version:

Core libraries for accessing DCP network

237 lines (215 loc) 9.29 kB
/** * @file event-loop-virtualization.js * * File that creates an event loop using a reactor pattern * for handling timers on the event loop. This is made to * be similar to the event loops in nodejs and browsers with respect * to timers * * The node and web worker evaluators already have an event loop, along with * their own timer functions, so only the v8 evaluator needs to be modified. Before * this, it has only the following primitive functions: * - ontimer will be invoked by the reactor when there are timers that * should need servicing based on the information provided to nextTimer(). * - nextTimer() sets when the next timer will be fired (in ms) * * Once this file has run, the following methods will be * available on the global object for every evaluator: * - setTimeout() execute callback after minimum timeout time in ms * - clearTimeout() clear the timeout created by setTimeout * - setInterval() recurringly execute callback after minimum timeout time in ms * - clearInterval() clear the interval created by setInterval * - setImmediate() execute callback after 0ms, immediately when the event loop allows * - clearImmediate() clear the Immediate created by setImmediate * - queueMicrotask() add a microtask to the microtask queue, bypassing 4ms timeout clamping * * @author Parker Rowe, parker@kingsds.network * Ryan Saweczko, ryansaweczko@kingsds.network * @date August 2020 * * @note Unusual function scoping is done to eliminate spurious symbols * from being accessible from the global object, to mitigate * certain classes of security risks. The global object here is * the top of the scope chain (ie global object) for all code run * by hosts in this environment. */ /* globals self, ontimer, nextTimer, evalTimer */ self.wrapScriptLoading({ scriptName: 'native-event-loop' }, function nativeEventLoop$$fn(protectedStorage, ring0PostMessage) { (function privateScope(ontimer, nextTimer) { const timers = []; timers.serial = 0; /* If this isn't set, it becomes NaN */ function sortTimers() { timers.sort(function (a, b) { return a.when - b.when; }); } /* Fire any timers which are ready to run, being careful not to * get into a recurring timer death loop without reactor mediation. */ function fireTimerCallbacks() { sortTimers(); let timer = timers.shift(); let now = Date.now(); if (!timer) throw new Error('Logic error: trying to run timer when no timer exists') /* should be impossible */ if (timer.when > now) /* should be impossible, but at least we can handle this */ { timers.unshift(timer); nextTimer(timers[0].when); return; } timer.fn.apply(null, timer.args); if (timer.recur) { timer.when = Date.now() + timer.recur; timers.push(timer); } sortTimers(); if (timers.length) nextTimer(timers[0].when); } ontimer(fireTimerCallbacks); /** Execute callback after at least timeout ms. * * @param callback {function} Callback function to fire after a minimum callback time * @param timeout {int} integer containing the minimum time to fire callback in ms * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the timeoutId parameter of clearTimeout() */ self.setTimeout = function eventLoop$$Worker$setTimeout(callback, timeout, arg) { let timer, args; if (typeof callback === 'string') { let code = callback; callback = function eventLoop$$Worker$setTimeout$wrapper() { let indirectEval = eval; return indirectEval(code); } } args = Array.prototype.slice.call(arguments); // get a plain array from function arguments args = args.slice(2); // slice the first two elements (callback & timeout), leaving an array of user arguments let fn = callback; callback = () => fn.apply(fn, args); // apply the arguments to the callback function timers.serial = +timers.serial + 1; timer = { fn: callback, when: Date.now() + (+timeout || 0), serial: timers.serial, valueOf: function () { return this.serial; } } timers.push(timer); if (timer.when <= timers[0].when) { sortTimers(); nextTimer(timers[0].when); } return timer; } /** Remove a timeout from the list of pending timeouts, regardless of its current * status. * * @param timeoutId {object} The value, returned from setTimeout(), identifying the timer. */ self.clearTimeout = function eventLoop$$Worker$clearTimeout(timeoutId) { if (typeof timeoutId === "object") { let i = timers.indexOf(timeoutId); if (i != -1) { timers.splice(i, 1); /* if there is a timer at the top of the timers list, set that to be the nextTimer to fire * otherwise, tell the event loop that there are no more timers to fire, and end the loop accordingly. * this fixes a bug where you clear a timeout, but the program still waits that time before ending, * despite never calling the callback function * * for example: * const timeout = setTimeout(() => console.log("hi"), 10000); * clearTimeout(timeout); * * used to still wait 10 seconds before closing the program, despite never printing hi to the console */ if (timers.length) { nextTimer(timers[0].when); } else { nextTimer(0); } } } else if (typeof timeoutId === "number") { /* slow path - object has been reinterpreted in terms of valueOf() */ for (let i = 0; i < timers.length; i++) { if (timers[i].serial === timeoutId) { timers.splice(i, 1); if (timers.length) { nextTimer(timers[0].when); } else { nextTimer(0); } break; } } } } // Memoise the original setTimeout for use in interval/immediate const innerSetTimeout = setTimeout; /** Execute callback after at least interval ms, regularly, at least interval ms apart. * * @param callback {function} Callback function to fire after a minimum callback time * @param timeout {int} integer containing the minimum time to fire callback in ms * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the intervalId paramter of clearInterval() */ self.setInterval = function eventLoop$$Worker$setInterval(callback, interval, arg) { let timer = innerSetTimeout(callback, +interval || 0, arg); timer.recur = interval; return timer; } /** Execute callback after 0 ms, immediately when the event loop allows. * * @param callback {function} Callback function to fire after a minimum callback time * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the intervalId paramter of clearImmediate() */ self.setImmediate = function eventLoop$$Worker$setImmediate(callback, arg) { let timer = innerSetTimeout(callback, 0, arg); return timer; } /** Remove an interval timer from the list of pending interval timers, regardless of its current * status. (Same as clearTimeout) * * @param intervalId {object} The value, returned from setInterval(), identifying the timer. */ self.clearInterval = self.clearTimeout; self.clearImmediate = self.clearTimeout; /** queues a microtask to be executed at a safe time prior to control returning to the event loop * * @param callback {function} Callback function to fire */ self.queueMicrotask = function eventLoop$$Worker$queueMicrotask(callback) { Promise.resolve().then(callback); } function clearAllTimers() { timers.length = 0; nextTimer(0); } addEventListener('message', async (event) => { try { if (event.request === 'clearTimers') { clearAllTimers(); ring0PostMessage({ request: 'clearTimersDone', }); } } catch (error) { ring0PostMessage({ request: 'error', error: { name: error.name, message: error.message, stack: error.stack, }, }); } }); })(self.ontimer, self.nextTimer); ontimer = nextTimer = evalTimer = undefined; delete self.ontimer; delete self.nextTimer; delete self.evalTimer; });