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;