Home Manual Reference Source

src/setAlarm.js

import {TOLERANCE_LEFT} from './TOLERANCE.js';
import Alarm from './Alarm.js';

// See:
//  - https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#maximum_delay_value
//  - https://stackoverflow.com/questions/3468607/why-does-settimeout-break-for-large-millisecond-delay-values
const DELAY_MAX_FOR_SET_TIMEOUT = 2147483647;
const FACTOR_OVERTAKE = 0.1; // We try to be ahead of the event by that much.
const FACTOR_COMPLETION = 1 - FACTOR_OVERTAKE;
const THRESHOLD_DRIFT = 100; // We assume no drift that large will ever occur.
const THRESHOLD_FINAL_COUNTDOWN = THRESHOLD_DRIFT / FACTOR_OVERTAKE;

/**
 * Set an alarm to trigger at a given date/time specified by a Date object.
 * When triggered, the alarm will call the callback function with passed
 * arguments, if any.
 *
 * We guarantee that the alarm will not trigger before the given date/time.
 * We try to trigger the alarm as soom as the given date/time has passed.
 *
 * Note that we workaround two issues raised by a naive setTimeout
 * implementation:
 *   1. the delay parameter is a 32-bit signed integer
 *   2. taking the first point into account, the resolution time of setAlarm
 *   could be imprecise with large delays (because of setInterval/setTimeout
 *   drift).
 *
 * @param {Function} callback - The Function to call when the alarm triggers.
 * @param {Date} date - The date/time at which to trigger the alarm.
 * @param {...any} rest - The arguments to call the callback with.
 * @returns {Alarm} The alarm object.
 */
export default function setAlarm(callback, date, ...rest) {
	const delay = () => date - Date.now();
	const alarm = new Alarm();

	const iter = () => {
		const _delay = delay();
		if (_delay >= THRESHOLD_FINAL_COUNTDOWN) {
			const step = Math.min(
				DELAY_MAX_FOR_SET_TIMEOUT,
				_delay * FACTOR_COMPLETION,
			);
			alarm.record(setTimeout(iter, step));
		} else if (_delay > -TOLERANCE_LEFT) {
			// We make sure to queue the final callback only if we respect the
			// left tolerance
			alarm.record(setTimeout(iter, _delay));
		} else {
			// Here we recompute delay() to get the most accurate delay for the
			// last setTimeout call.
			alarm.record(setTimeout(callback, delay(), ...rest));
		}
	};

	alarm.record(setTimeout(iter, 0));
	return alarm;
}