const isFunction = require("lodash.isfunction"); /** * @constant * @default * @description Default timeout of all heartbeat objects. * @see <code>setBeatTimeout</code> * @see <code>getBeatTimeout</code> */ const DEFAULT_TIMEOUT = 5000; /** * @constant * @default * @description Default heartbeat interval of all heartbeat objects. * @see <code>setBeatInterval</code> * @see <code>getBeatInterval</code> */ const DEFAULT_INTERVAL = 3000; /** * @public * @author Pedro Miguel Pereira Serrano Martins * @version 1.0.3 * @module heartBeat * @desc Factory function that creates heartbeat objects with a default Timeout of <code>DEFAULT_TIMEOUT</code> seconds and a default BeatInterval of <code>DEFAULT_INTERVAL</code> seconds. * The heartbeat returned is stopped and needs to be started to execute. */ const heartBeatFactory = () => { let interval = DEFAULT_INTERVAL, timeout = DEFAULT_TIMEOUT, ping, pong, timer, lastHeartbeatTime, timeoutTimer, timeoutFn = () => { }; /** * @public * @func hasTimedOut * @returns {Boolean} <code>true</code> if the heartbeat has timedout, * <code>false</code> otherwise. * * @description Used to detected if a heartbeat has timedout. * A heartbeat times out when it sends a ping, and receives no pong after a given period of time. * The timeout period can be manipulated via <code>setBeatTimeout</code>. * @see <code>setBeatTimeout</code> */ const hasTimedOut = () => Date.now() - lastHeartbeatTime > timeout; /** * @public * @func getBeatInterval * @returns {Number} The current heartbeat interval. * * @description Returns the current hearbeat interval. * The heartbeat interval is the interval at which the heartbeat will run the <code>ping</code> function. */ const getBeatInterval = () => interval; /** * @public * @func setBeatInterval * @param {Number} newInterval The new heartbeat interval. * @throws {TypeError} If <code>newInterval</code> is not a Number. * * @description Sets the current heartbeat interval to the given one. * Note that setting the heartbeat interval will <b>not</b> affetct current heartbeat running. You must <code>stop</code> them and then <code>start</code> them for the new interval to be applied. * @see <code>stop</code> * @see <code>start</code> */ const setBeatInterval = newInterval => { if(isNaN(newInterval)) throw new TypeError(`${newInterval} must be a Number.`); interval = newInterval; }; /** * @public * @func getBeatTimeout * @returns {Number} The current timeout. * * @description Returns the current hearbeat timeout. * The heartbeat timeout is the amount of time that must pass for the <code>hasTimedOut</code> to return <code>true</code>. * @see <code>hasTimedOut</code> */ const getBeatTimeout = () => timeout; /** * @public * @func setBeatTimeout * @param {Number} newTimeout The new newTimeout. * @throws {TypeError} If <code>newTimeout</code> is not a Number. * * @description Sets the current timeout to the given one. * Setting the timeout this way will immediatly affect the <code>hasTimedOut</code> method without the need to restart the heartbeat object. * Invoking this method <b>does</b> restart the timer controlling the <code>onTimeout</code> event. * @see <code>hasTimedOut</code> * @see <code>onTimeout</code> */ const setBeatTimeout = newTimeout => { if(isNaN(newTimeout)) throw new TypeError(`${newTimeout} must be a Number.`); timeout = newTimeout; clearTimeout(timeoutTimer); timeoutTimer = setTimeout( timeoutFn, timeout ); }; /** * @public * @func getPing * @returns {Object} The current object being used as a ping. * * @description Returns the ping object being used. */ const getPing = () => ping; /** * @public * @func setPing * @param {Object} newPing The new ping object. * * @description Sets the current ping object. * A ping object can be anything that the receiver accepts, from a Buffer of bytes to plain Object to a primitive. */ const setPing = newPing => { ping = newPing; }; /** * @public * @func getPong * @returns {Object} The current object being used as a pong. * * @description Returns the pong object being used. */ const getPong = () => pong; /** * @public * @func setPong * @param {Object} newPong The new pong object. * * @description Sets the pong object we expect to receive from the target of the heartbeats. * This is only needed if there is a need to distinguish between normal messages from the target of heartbeat and pong messages that need to be processed differently. */ const setPong = newPong => { pong = newPong; }; /** * @public * @func receivedPong * * @description Notifies the hearbeat that it has received a pong from the target. */ const receivedPong = () => { lastHeartbeatTime = Date.now(); clearTimeout(timeoutTimer); timeoutTimer = setTimeout( timeoutFn, timeout ); }; /** * @public * @func stop * * @description Stops the heartbeat object and clears all internal states. */ const stop = () => { lastHeartbeatTime = undefined; clearInterval(timer); timer = undefined; clearTimeout(timeoutTimer); timeoutTimer = undefined; }; /** * @public * @func start * @param {Function} fn The function that will be executed periodically * by the heartbeat object. * @throws {TypeError} If <code>fn</code> is not a function. * * @description Starts the heartbeat object, executing the given function <code>fn</code> every interval. * If you want to send a ping to an object every interval, this is where you defined that. */ const start = fn => { if (!isFunction(fn)) throw new TypeError(`${fn} must be a function.`); lastHeartbeatTime = Date.now(); timer = setInterval( fn, interval ); timeoutTimer = setTimeout( timeoutFn, timeout ); }; /** * @public * @func onTimeout * @param {Function} fn The function to be executed when a timeout * occurs. * @throws {TypeError} If <code>fn</code> is not a function. * * @description Runs the given function when the heartbeat detects a timeout. * A timeout is deteceted if <code>receivedPong</code> is not called within the defined 'timeout' period. */ const onTimeout = fn => { if (!isFunction(fn)) throw new TypeError(`${fn} must be a function.`); timeoutFn = fn; }; /** * @public * @func isBeating * @returns {Boolean} <code>true</code> if the heartbeat is active, * <code>false</code> otherwise. * * @description Returns <code>true</code> if the heartbeat is active, <code>false</code> otherwise. * A heartbeat is considered active if it was started and has not beend stopped yet. */ const isBeating = () => timer !== undefined; /** * @public * @func reset * * @description Stops the heartbeat if it is beating, and resets all properties to the original default values. */ const reset = () => { stop(); interval = DEFAULT_INTERVAL; timeout = DEFAULT_TIMEOUT; ping = undefined; pong = undefined; timeoutFn = () => { }; }; return Object.freeze({ getBeatInterval, setBeatInterval, getBeatTimeout, setBeatTimeout, hasTimedOut, getPing, setPing, getPong, receivedPong, setPong, stop, start, reset, isBeating, onTimeout }); }; module.exports = heartBeatFactory;