UNPKG

timesync

Version:

Time synchronization between peers

350 lines (295 loc) 11.5 kB
"use strict"; function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } Object.defineProperty(exports, "__esModule", { value: true }); exports.create = create; var util = _interopRequireWildcard(require("./util.js")); var stat = _interopRequireWildcard(require("./stat.js")); var request = _interopRequireWildcard(require("./request/request")); var _emitter = _interopRequireDefault(require("./emitter.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /** * timesync * * Time synchronization between peers * * https://github.com/enmasseio/timesync */ var Promise = require('./Promise'); /** * Factory function to create a timesync instance * @param {Object} [options] TODO: describe options * @return {Object} Returns a new timesync instance */ 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 send(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 receive(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 _handleRPCSendError(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 rpc(to, method, params) { var id = util.nextId(); var resolve, reject; var deferred = new Promise(function (res, rej) { resolve = res; reject = rej; }); timesync._inProgress[id] = function (data) { delete timesync._inProgress[id]; resolve(data); }; var 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 sync() { timesync.emit('sync', 'start'); var peers = timesync.options.server ? [timesync.options.server] : timesync.options.peers; return Promise.all(peers.map(function (peer) { return timesync._syncWithPeer(peer); })).then(function (all) { var offsets = all.filter(function (offset) { return 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 _validOffset(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 _syncWithPeer(peer) { // retrieve the offset of a peer, then wait 1 sec var all = []; function sync() { return timesync._getOffset(peer).then(function (result) { return 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(function (result) { return result !== null; }); // calculate the limit for outliers var roundtrips = results.map(function (result) { return 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(function (result) { return result.roundtrip < limit; }); var offsets = filtered.map(function (result) { return 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 _getOffset(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 now() { 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 destroy() { 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(function (peer) { return peer.trim(); }).filter(function (peer) { return peer !== ''; }); } else { timesync.options[prop] = options[prop]; } } } } // turn into an event emitter (0, _emitter["default"])(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"](function (err) { return emitError(err); }); }, 0); } return timesync; }