UNPKG

timesync

Version:

Time synchronization between peers

325 lines (281 loc) 9.64 kB
/** * timesync * * Time synchronization between peers * * https://github.com/enmasseio/timesync */ import * as util from './util.js'; import * as stat from './stat.js'; import * as request from './request/request'; import emitter from './emitter.js'; var Promise = require('./Promise'); /** * Factory function to create a timesync instance * @param {Object} [options] TODO: describe options * @return {Object} Returns a new timesync instance */ export function create(options) { var timesync = { // configurable options options: { interval: 60 * 60 * 1000, // interval for doing synchronizations in ms. Set to null to disable auto sync timeout: 10000, // timeout for requests to fail in ms delay: 1000, // delay between requests in ms repeat: 5, // number of times to do a request to one peer peers: [], // uri's or id's of the peers server: null, // uri of a single server (master/slave configuration) now: Date.now // function returning the system time }, /** @type {number} The current offset from system time */ offset: 0, // ms /** @type {number} Contains the timeout for the next synchronization */ _timeout: null, /** @type {Object.<string, function>} Contains a map with requests in progress */ _inProgress: {}, /** * @type {boolean} * This property used to immediately apply the first ever received offset. * After that, it's set to false and not used anymore. */ _isFirst: true, /** * Send a message to a peer * This method must be overridden when using timesync * @param {string} to * @param {*} data */ send: function (to, data, timeout) { return request.post(to, data, timeout) .then(function (val) { var res = val[0]; if(val[1] !== 200) { throw new Error('Send failure: bad HTTP StatusCode (' + val[1] + ')'); } timesync.receive(to, res); }) .catch(function (err) { emitError(err); }); }, /** * Receive method to be called when a reply comes in * @param {string | undefined} [from] * @param {*} data */ receive: function (from, data) { if (data === undefined) { data = from; from = undefined; } if (data && data.id in timesync._inProgress) { // this is a reply timesync._inProgress[data.id](data.result); } else if (data && data.id !== undefined) { // this is a request from an other peer // reply with our current time timesync.send(from, { jsonrpc: '2.0', id: data.id, result: timesync.now() }) } }, _handleRPCSendError: function (id, reject, err) { delete timesync._inProgress[id]; reject(new Error('Send failure')); }, /** * Send a JSON-RPC message and retrieve a response * @param {string} to * @param {string} method * @param {*} [params] * @returns {Promise} */ rpc: function (to, method, params) { var id = util.nextId(); var resolve, reject; var deferred = new Promise((res, rej) => { resolve = res; reject = rej; }); timesync._inProgress[id] = function (data) { delete timesync._inProgress[id]; resolve(data); }; let sendResult; try { sendResult = timesync.send(to, { jsonrpc: '2.0', id: id, method: method, params: params }, timesync.options.timeout); } catch(err) { timesync._handleRPCSendError(id, reject, err); } if (sendResult && (sendResult instanceof Promise || (sendResult.then && sendResult.catch))) { sendResult.catch(timesync._handleRPCSendError.bind(this, id, reject)); } else { console.warn('Send should return a promise'); } return deferred; }, /** * Synchronize now with all configured peers * Docs: http://www.mine-control.com/zack/timesync/timesync.html */ sync: function () { timesync.emit('sync', 'start'); var peers = timesync.options.server ? [timesync.options.server] : timesync.options.peers; return Promise .all(peers.map(peer => timesync._syncWithPeer(peer))) .then(function (all) { var offsets = all.filter(offset => timesync._validOffset(offset)); if (offsets.length > 0) { // take the average of all peers (excluding self) as new offset timesync.offset = stat.mean(offsets); timesync.emit('change', timesync.offset); } timesync.emit('sync', 'end'); }); }, /** * Test whether given offset is a valid number (not NaN, Infinite, or null) * @param {number} offset * @returns {boolean} * @private */ _validOffset: function (offset) { return offset !== null && !isNaN(offset) && isFinite(offset); }, /** * Sync one peer * @param {string} peer * @return {Promise.<number | null>} Resolves with the offset to this peer, * or null if failed to sync with this peer. * @private */ _syncWithPeer: function (peer) { // retrieve the offset of a peer, then wait 1 sec var all = []; function sync () { return timesync._getOffset(peer).then(result => all.push(result)); } function waitAndSync() { return util.wait(timesync.options.delay).then(sync); } function notDone() { return all.length < timesync.options.repeat; } return sync() .then(function () { return util.whilst(notDone, waitAndSync) }) .then(function () { // filter out null results var results = all.filter(result => result !== null); // calculate the limit for outliers var roundtrips = results.map(result => result.roundtrip); var limit = stat.median(roundtrips) + stat.std(roundtrips); // filter all results which have a roundtrip smaller than the mean+std var filtered = results.filter(result => result.roundtrip < limit); var offsets = filtered.map(result => result.offset); // return the new offset return (offsets.length > 0) ? stat.mean(offsets) : null; }); }, /** * Retrieve the offset from one peer by doing a single call to the peer * @param {string} peer * @returns {Promise.<{roundtrip: number, offset: number} | null>} * @private */ _getOffset: function (peer) { var start = timesync.options.now(); // local system time return timesync.rpc(peer, 'timesync') .then(function (timestamp) { var end = timesync.options.now(); // local system time var roundtrip = end - start; var offset = timestamp - end + roundtrip / 2; // offset from local system time // apply the first ever retrieved offset immediately. if (timesync._isFirst) { timesync._isFirst = false; timesync.offset = offset; timesync.emit('change', offset); } return { roundtrip: roundtrip, offset: offset }; }) .catch(function (err) { // just ignore failed requests, return null return null; }); }, /** * Get the current time * @returns {number} Returns a timestamp */ now: function () { return timesync.options.now() + timesync.offset; }, /** * Destroy the timesync instance. Stops automatic synchronization. * If timesync is currently executing a synchronization, this * synchronization will be finished first. */ destroy: function () { clearTimeout(timesync._timeout); } }; // apply provided options if (options) { if (options.server && options.peers) { throw new Error('Configure either option "peers" or "server", not both.'); } for (var prop in options) { if (options.hasOwnProperty(prop)) { if (prop === 'peers' && typeof options.peers === 'string') { // split a comma separated string with peers into an array timesync.options.peers = options.peers .split(',') .map(peer => peer.trim()) .filter(peer => peer !== ''); } else { timesync.options[prop] = options[prop]; } } } } // turn into an event emitter emitter(timesync); /** * Emit an error message. If there are no listeners, the error is outputted * to the console. * @param {Error} err */ function emitError(err) { if (timesync.list('error').length > 0) { timesync.emit('error', err); } else { console.log('Error', err); } } if (timesync.options.interval !== null) { // start an interval to automatically run a synchronization once per interval timesync._timeout = setInterval(timesync.sync, timesync.options.interval); // synchronize immediately on the next tick (allows to attach event // handlers before the timesync starts). setTimeout(function () { timesync.sync().catch(err => emitError(err)); }, 0); } return timesync; }