'use strict';
const _isNil = require('lodash/isNil');
const debug = require('debug')('k4-base-bridge:timingUtility');
/**
* @class
* @description A general-purpose set of utility and helper functions
* for timer-based operations.
*/
class TimingUtility {
/**
* @property {Object} constants
* @static
*/
static get constants() {
return {
ALLOWED_PERCENT_TOLERANCE: 20,
MIN_ACCEPTABLE_FRAMES_PER_INTERVAL: 20,
MAX_ACCEPTABLE_FRAMES_PER_INTERVAL: 50
};
}
/**
* @description Wraps Node.js setTimeout() in a thenable. It has some
* additional computation to guarantee that at least the specified
* delay duration has elapsed.
* @param {Number} delay The minimum wait duration (msec)
* @returns {Promise}
* @static
*/
static setTimeoutPromise(delay) {
return new Promise((resolve, reject) => {
const specifiedDelayNanos = delay * 1e6;
const startTime = process.hrtime();
const startTimeNanos = this.convertHrTimeToNanos(startTime);
setTimeout(() => {
const currentTime = process.hrtime();
const currentTimeNanos = this.convertHrTimeToNanos(currentTime);
const actualDelayNanos = currentTimeNanos - startTimeNanos;
if (actualDelayNanos >= specifiedDelayNanos) {
resolve();
} else {
const remainderDelayMsec = Math.ceil((specifiedDelayNanos - actualDelayNanos) / 1e6);
setTimeout(() => {
resolve();
}, remainderDelayMsec);
}
}, delay);
});
}
/**
* @description Wraps Node.js setImmediate() in a thenable/Promise-like
* @returns {Promise}
* @static
*/
static setImmediatePromise() {
return new Promise((resolve, reject) => {
setImmediate(resolve);
});
}
/**
* @description Runs a recurring interval timer that checks for its deadline
* at multiple smaller frames which are smaller than the
* specified interval. Depends on the base method:
* {@link TimingUtility#selfCorrectingInterval}
* @param callback
* @param {Number} intervalDuration Must be at least 100 ms
* @param {Number} [numFramesPerInterval] The number of frames to divide the
* interval into. Default is 20 frames per interval. The acceptable divisions
* are currently between 20 and 40 frames per interval.
* Currently, only the 20 frames per interval option is supported with a
* known tolerance of +/- 20%.
* @returns {Object} The frameIntervalTimer field yields a: reference to an
* object whose 'timer' field refers to a NodeJS.Timer
* @static
*/
static setIntervalByFrames(callback, intervalDuration, numFramesPerInterval) {
const numFrames = !_isNil(numFramesPerInterval) ? numFramesPerInterval : 20;
const frameDuration = intervalDuration / numFrames;
let startingTime = Date.now();
if (numFrames > TimingUtility.constants.MAX_ACCEPTABLE_FRAMES_PER_INTERVAL || numFrames < TimingUtility.constants.MIN_ACCEPTABLE_FRAMES_PER_INTERVAL) {
throw new Error(`The specified number of frames ${numFrames}, is beyond the acceptable range of [${TimingUtility.constants.MIN_ACCEPTABLE_FRAMES_PER_INTERVAL}, ${TimingUtility.constants.MAX_ACCEPTABLE_FRAMES_PER_INTERVAL}] (inclusive)`);
}
const timerObj = TimingUtility.selfCorrectingInterval(() => {
const currentTime = Date.now();
const timeElapsed = currentTime - startingTime;
if ((currentTime - (timeElapsed - intervalDuration)) <= 0) {
debug(`[setIntervalByFrames] currentTime, timeElapsed, intervalDuration, nextStartingTime: `, currentTime, timeElapsed, intervalDuration, Date.now() - (timeElapsed - intervalDuration));
}
if (timeElapsed >= intervalDuration) {
startingTime = Date.now() - (timeElapsed - intervalDuration);
callback();
}
}, frameDuration);
timerObj.toleranceMsec = ((TimingUtility.constants.ALLOWED_PERCENT_TOLERANCE) / 100) * intervalDuration;
return timerObj;
}
/**
* @description A self-correcting recurring interval timer. Intended maximum
* correction resolution is the interval duration up to +/- the allowed
* percent tolerance by TimingUtility (currently 20%).
* @param {Function} callback Function to call once each interval
* @param {Number} intervalDuration Delay duration (msec)
* @returns {Object} Reference to an object whose 'timer' field refers to
* a NodeJS.Timer
* @static
*/
static selfCorrectingInterval(callback, intervalDuration) {
let previousTime = Date.now();
let desiredDuration = intervalDuration;
const retObj = {};
function tick() {
const now = Date.now();
const actualDuration = now - previousTime;
previousTime = now;
desiredDuration = intervalDuration - (actualDuration - desiredDuration);
callback();
retObj.timer = setTimeout(tick, desiredDuration);
}
retObj.timer = setTimeout(tick, intervalDuration);
retObj.delayInterval = intervalDuration;
retObj.toleranceMsec = ((TimingUtility.constants.ALLOWED_PERCENT_TOLERANCE) / 100) * intervalDuration;
return retObj;
}
/**
* @description Converts the process.hrtime() tuple to a single numerical
* value.
* @param {Number[]} hrTimeTuple The original result of process.hrtime()
* @returns {Number} The time value in nanoseconds
* @static
*/
static convertHrTimeToNanos(hrTimeTuple) {
return (hrTimeTuple[0] * 1e9) + hrTimeTuple[1];
}
};
module.exports = TimingUtility;