mirror of
https://github.com/denoland/deno.git
synced 2024-11-23 15:16:54 -05:00
225 lines
7.3 KiB
TypeScript
225 lines
7.3 KiB
TypeScript
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
|
import { assert } from "./util";
|
|
import * as fbs from "gen/msg_generated";
|
|
import { flatbuffers } from "flatbuffers";
|
|
import { sendSync, setFireTimersCallback } from "./dispatch";
|
|
|
|
// Tell the dispatcher which function it should call to fire timers that are
|
|
// due. This is done using a callback because circular imports are disallowed.
|
|
setFireTimersCallback(fireTimers);
|
|
|
|
interface Timer {
|
|
id: number;
|
|
callback: () => void;
|
|
delay: number;
|
|
due: number;
|
|
repeat: boolean;
|
|
scheduled: boolean;
|
|
}
|
|
|
|
// We'll subtract EPOCH every time we retrieve the time with Date.now(). This
|
|
// ensures that absolute time values stay below UINT32_MAX - 2, which is the
|
|
// maximum object key that EcmaScript considers "numerical". After running for
|
|
// about a month, this is no longer true, and Deno explodes.
|
|
// TODO(piscisaureus): fix that ^.
|
|
const EPOCH = Date.now();
|
|
const APOCALYPS = 2 ** 32 - 2;
|
|
|
|
let globalTimeoutDue: number | null = null;
|
|
|
|
let nextTimerId = 1;
|
|
const idMap = new Map<number, Timer>();
|
|
const dueMap: { [due: number]: Timer[] } = Object.create(null);
|
|
|
|
function getTime() {
|
|
// TODO: use a monotonic clock.
|
|
const now = Date.now() - EPOCH;
|
|
assert(now >= 0 && now < APOCALYPS);
|
|
return now;
|
|
}
|
|
|
|
function setGlobalTimeout(due: number | null, now: number) {
|
|
// 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.
|
|
// Note that a negative time-out value stops the global timer.
|
|
let timeout;
|
|
if (due === null) {
|
|
timeout = -1;
|
|
} else {
|
|
timeout = due - now;
|
|
assert(timeout >= 0);
|
|
}
|
|
// Send message to the backend.
|
|
const builder = new flatbuffers.Builder();
|
|
fbs.SetTimeout.startSetTimeout(builder);
|
|
fbs.SetTimeout.addTimeout(builder, timeout);
|
|
const msg = fbs.SetTimeout.endSetTimeout(builder);
|
|
const res = sendSync(builder, fbs.Any.SetTimeout, msg);
|
|
assert(res == null);
|
|
// Remember when when the global timer will fire.
|
|
globalTimeoutDue = due;
|
|
}
|
|
|
|
function schedule(timer: Timer, now: number) {
|
|
assert(!timer.scheduled);
|
|
assert(now <= timer.due);
|
|
// Find or create the list of timers that will fire at point-in-time `due`.
|
|
let list = dueMap[timer.due];
|
|
if (list === undefined) {
|
|
list = dueMap[timer.due] = [];
|
|
}
|
|
// Append the newly scheduled timer to the list and mark it as scheduled.
|
|
list.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) {
|
|
setGlobalTimeout(timer.due, now);
|
|
}
|
|
}
|
|
|
|
function unschedule(timer: Timer) {
|
|
if (!timer.scheduled) {
|
|
return;
|
|
}
|
|
// Find the list of timers that will fire at point-in-time `due`.
|
|
const list = dueMap[timer.due];
|
|
if (list.length === 1) {
|
|
// Time timer is the only one in the list. Remove the entire list.
|
|
assert(list[0] === timer);
|
|
delete dueMap[timer.due];
|
|
// 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) {
|
|
let nextTimerDue: number | null = null;
|
|
for (const key in dueMap) {
|
|
nextTimerDue = Number(key);
|
|
break;
|
|
}
|
|
setGlobalTimeout(nextTimerDue, getTime());
|
|
}
|
|
} 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 > 0);
|
|
list.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
function fire(timer: 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 = getTime();
|
|
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;
|
|
callback();
|
|
}
|
|
|
|
function fireTimers() {
|
|
const now = getTime();
|
|
// Bail out if we're not expecting the global timer to fire (yet).
|
|
if (globalTimeoutDue === null || now < globalTimeoutDue) {
|
|
return;
|
|
}
|
|
// After firing the timers that are due now, this will hold the due time of
|
|
// the first timer that hasn't fired yet.
|
|
let nextTimerDue: number | null = null;
|
|
// Walk over the keys of the 'due' map. Since dueMap is actually a regular
|
|
// object and its keys are numerical and smaller than UINT32_MAX - 2,
|
|
// keys are iterated in ascending order.
|
|
for (const key in dueMap) {
|
|
// Convert the object key (a string) to a number.
|
|
const due = Number(key);
|
|
// Break out of the loop if the next timer isn't due to fire yet.
|
|
if (Number(due) > now) {
|
|
nextTimerDue = due;
|
|
break;
|
|
}
|
|
// Get the list of timers that have this due time, then drop it.
|
|
const list = dueMap[key];
|
|
delete dueMap[key];
|
|
// Fire all the timers in the list.
|
|
for (const timer of list) {
|
|
// With the list dropped, the timer is no longer scheduled.
|
|
timer.scheduled = false;
|
|
// Place the callback on the microtask queue.
|
|
Promise.resolve(timer).then(fire);
|
|
}
|
|
}
|
|
// Update the global alarm to go off when the first-up timer that hasn't fired
|
|
// yet is due.
|
|
setGlobalTimeout(nextTimerDue, now);
|
|
}
|
|
|
|
function setTimer<Args extends Array<unknown>>(
|
|
cb: (...args: Args) => void,
|
|
delay: number,
|
|
args: Args,
|
|
repeat: boolean
|
|
): number {
|
|
// If any `args` were provided (which is uncommon), bind them to the callback.
|
|
const callback: () => void = args.length === 0 ? cb : cb.bind(null, ...args);
|
|
// In the browser, the delay value must be coercable to an integer between 0
|
|
// and INT32_MAX. Any other value will cause the timer to fire immediately.
|
|
// We emulate this behavior.
|
|
const now = getTime();
|
|
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;
|
|
}
|
|
|
|
export function setTimeout<Args extends Array<unknown>>(
|
|
cb: (...args: Args) => void,
|
|
delay: number,
|
|
...args: Args
|
|
): number {
|
|
return setTimer(cb, delay, args, false);
|
|
}
|
|
|
|
export function setInterval<Args extends Array<unknown>>(
|
|
cb: (...args: Args) => void,
|
|
delay: number,
|
|
...args: Args
|
|
): number {
|
|
return setTimer(cb, delay, args, true);
|
|
}
|
|
|
|
export function clearTimer(id: number): void {
|
|
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);
|
|
}
|