├── .babelrc ├── src ├── Promise.js ├── request │ ├── request.js │ ├── request.node.js │ └── request.browser.js ├── stat.js ├── emitter.js ├── util.js └── timesync.js ├── examples ├── basic │ ├── express │ │ ├── README.md │ │ ├── server.js │ │ └── index.html │ └── http │ │ ├── README.md │ │ ├── node_client.js │ │ ├── server.js │ │ └── index.html └── advanced │ ├── express │ ├── server.js │ └── index.html │ ├── peerjs │ ├── peer1.html │ ├── peer2.html │ ├── peer3.html │ ├── custom.html │ └── client.js │ ├── socket.io │ ├── index.html │ └── server.js │ └── http │ ├── index.html │ └── server.js ├── .gitignore ├── LICENSE ├── package.json ├── HISTORY.md ├── docs └── android-tutorial.md ├── server └── index.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /src/Promise.js: -------------------------------------------------------------------------------- 1 | module.exports = (typeof window === 'undefined' || typeof window.Promise === 'undefined') ? 2 | require('promise') : 3 | window.Promise; 4 | -------------------------------------------------------------------------------- /examples/basic/express/README.md: -------------------------------------------------------------------------------- 1 | To run this example, first install dependencies: 2 | 3 | npm install express 4 | 5 | Then start the server: 6 | 7 | node server.js 8 | 9 | Or with debugging: 10 | 11 | DEBUG=* node server.js 12 | 13 | Then open the following url in your browser: 14 | 15 | http://localhost:8081 16 | 17 | -------------------------------------------------------------------------------- /examples/basic/http/README.md: -------------------------------------------------------------------------------- 1 | To run this example, first install dependencies: 2 | 3 | npm install promise 4 | 5 | Then start the server: 6 | 7 | node server.js 8 | 9 | Or with debugging: 10 | 11 | DEBUG=* node server.js 12 | 13 | Then open the following url in your browser: 14 | 15 | http://localhost:8081 16 | 17 | -------------------------------------------------------------------------------- /src/request/request.js: -------------------------------------------------------------------------------- 1 | var isBrowser = (typeof document !== 'undefined'); 2 | var isReactNative = (typeof navigator !== 'undefined' && navigator.product === 'ReactNative'); 3 | 4 | // FIXME: how to do conditional loading this with ES6 modules? 5 | module.exports = (isBrowser || isReactNative) ? 6 | require('./request.browser') : 7 | require('./request.node'); 8 | -------------------------------------------------------------------------------- /examples/basic/express/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var timesyncServer = require('../../../server'); 3 | 4 | var PORT = 8081; 5 | 6 | // create an express app 7 | var app = express(); 8 | app.listen(PORT); 9 | console.log('Server listening at http://localhost:' + PORT); 10 | 11 | // serve static index.html 12 | app.get('/', express.static(__dirname)); 13 | app.get('/index.html', express.static(__dirname)); 14 | 15 | // handle timesync requests 16 | app.use('/timesync', timesyncServer.requestHandler); 17 | -------------------------------------------------------------------------------- /examples/basic/http/node_client.js: -------------------------------------------------------------------------------- 1 | // node.js client 2 | 3 | if (typeof global.Promise === 'undefined') { 4 | global.Promise = require('promise'); 5 | } 6 | var timesync = require('../../../dist/timesync'); 7 | 8 | // create a timesync client 9 | var ts = timesync.create({ 10 | peers: 'http://localhost:8081/timesync', 11 | interval: 10000 12 | }); 13 | 14 | // get notified on changes in the offset 15 | ts.on('change', function (offset) { 16 | console.log('changed offset: ' + offset + ' ms'); 17 | }); 18 | 19 | // get synchronized time 20 | setInterval(function () { 21 | var now = new Date(ts.now()); 22 | console.log('now: ' + now.toISOString() + ' ms'); 23 | }, 1000); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Build directories 28 | dist 29 | lib 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | # WebStorm settings 35 | .idea 36 | -------------------------------------------------------------------------------- /examples/advanced/express/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Before you can run this example you will have to install some dependencies: 3 | * 4 | * npm install express 5 | * npm install body-parser 6 | */ 7 | var express = require('express'); 8 | var bodyParser = require('body-parser'); 9 | var path = require('path'); 10 | 11 | var PORT = 8081; 12 | 13 | var app = express(); 14 | app.use(bodyParser.json()); // for parsing application/json 15 | app.use('/', express.static(__dirname)); 16 | app.use('/timesync/', express.static(path.join(__dirname, '/../../../dist'))); 17 | 18 | app.post('/timesync', function (req, res) { 19 | var data = { 20 | id: (req.body && 'id' in req.body) ? req.body.id : null, 21 | result: Date.now() 22 | }; 23 | res.json(data); 24 | }); 25 | 26 | app.listen(PORT); 27 | console.log('Server listening at http://localhost:' + PORT); 28 | -------------------------------------------------------------------------------- /examples/basic/http/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var http = require('http'); 3 | var timesyncServer = require('../../../server'); 4 | 5 | var PORT = 8081; 6 | 7 | // Create an http server 8 | var server = http.createServer(handler); 9 | server.listen(PORT); 10 | console.log('Server listening at http://localhost:' + PORT); 11 | 12 | // Attach a timesync request handler to the server. Optionally, a custom path 13 | // can be provided as second argument (defaults to '/timesync') 14 | timesyncServer.attachServer(server); 15 | 16 | // just server a static index.html file 17 | function handler (req, res) { 18 | sendFile(res, __dirname + '/index.html'); 19 | } 20 | 21 | function sendFile(res, filename) { 22 | fs.readFile(filename, function (err, data) { 23 | if (err) { 24 | res.writeHead(500); 25 | res.end('Error loading ' + filename.split('/').pop()); 26 | } 27 | else { 28 | res.writeHead(200); 29 | res.end(data); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/stat.js: -------------------------------------------------------------------------------- 1 | // basic statistical functions 2 | 3 | export function compare (a, b) { 4 | return a > b ? 1 : a < b ? -1 : 0; 5 | } 6 | 7 | export function add (a, b) { 8 | return a + b; 9 | } 10 | 11 | export function sum (arr) { 12 | return arr.reduce(add); 13 | } 14 | 15 | export function mean (arr) { 16 | return sum(arr) / arr.length; 17 | } 18 | 19 | export function std (arr) { 20 | return Math.sqrt(variance(arr)); 21 | } 22 | 23 | export function variance (arr) { 24 | if (arr.length < 2) return 0; 25 | 26 | var _mean = mean(arr); 27 | return arr 28 | .map(x => Math.pow(x - _mean, 2)) 29 | .reduce(add) / (arr.length - 1); 30 | } 31 | 32 | export function median (arr) { 33 | if (arr.length < 2) return arr[0]; 34 | 35 | var sorted = arr.slice().sort(compare); 36 | if (sorted.length % 2 === 0) { 37 | // even 38 | return (sorted[arr.length / 2 - 1] + sorted[arr.length / 2]) / 2; 39 | } 40 | else { 41 | // odd 42 | return sorted[(arr.length - 1) / 2]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/basic/http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 29 | 30 | 31 | (see outputs in the developer console) 32 | 33 | -------------------------------------------------------------------------------- /examples/basic/express/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | 32 | (see outputs in the developer console) 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2022 enmasse.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/emitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn an object into an event emitter. Attaches methods `on`, `off`, 3 | * `emit`, and `list` 4 | * @param {Object} obj 5 | * @return {Object} Returns the original object, extended with emitter functions 6 | */ 7 | export default function emitter(obj) { 8 | var _callbacks = {}; 9 | 10 | obj.emit = function (event, data) { 11 | var callbacks = _callbacks[event]; 12 | callbacks && callbacks.forEach(callback => callback(data)); 13 | }; 14 | 15 | obj.on = function (event, callback) { 16 | var callbacks = _callbacks[event] || (_callbacks[event] = []); 17 | callbacks.push(callback); 18 | return obj; 19 | }; 20 | 21 | obj.off = function (event, callback) { 22 | if (callback) { 23 | var callbacks = _callbacks[event]; 24 | var index = callbacks.indexOf(callback); 25 | if (index !== -1) { 26 | callbacks.splice(index, 1); 27 | } 28 | if (callbacks.length === 0) { 29 | delete _callbacks[event]; 30 | } 31 | } 32 | else { 33 | delete _callbacks[event]; 34 | } 35 | return obj; 36 | }; 37 | 38 | obj.list = function (event) { 39 | return _callbacks[event] || []; 40 | }; 41 | 42 | return obj; 43 | } 44 | -------------------------------------------------------------------------------- /examples/advanced/peerjs/peer1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | peer1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 |

peer1

26 |

27 | Synchronizes with peer2 and peer3. 28 |

29 | 30 | 31 | 32 | 33 |
System time
Synchronized time
Offset
34 |

35 | 36 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/advanced/peerjs/peer2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | peer2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 |

peer2

26 |

27 | Synchronizes with peer1 and peer3. 28 |

29 | 30 | 31 | 32 | 33 |
System time
Synchronized time
Offset
34 |

35 | 36 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/advanced/peerjs/peer3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | peer3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 |

peer3

26 |

27 | Synchronizes with peer1 and peer2. 28 |

29 | 30 | 31 | 32 | 33 |
System time
Synchronized time
Offset
34 |

35 | 36 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/advanced/socket.io/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 45 | 46 | 47 | (see outputs in the developer console) 48 | 49 | -------------------------------------------------------------------------------- /examples/advanced/socket.io/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Before you can run this example you will have to install a dependency: 3 | * 4 | * npm install socket.io 5 | */ 6 | var app = require('http').createServer(handler); 7 | var io = require('socket.io')(app); 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | 11 | var PORT = 8081; 12 | 13 | app.listen(PORT); 14 | console.log('Server listening at http://localhost:' + PORT); 15 | 16 | function handler (req, res) { 17 | console.log('request', req.url); 18 | 19 | if (req.url === '/timesync/timesync.js') { 20 | res.setHeader('Content-Type', 'application/javascript'); 21 | return sendFile(path.join(__dirname, '../../../dist/timesync.js'), res); 22 | } 23 | 24 | if (req.url === '/' || req.url === 'index.html') { 25 | return sendFile(__dirname + '/index.html', res); 26 | } 27 | 28 | res.writeHead(404); 29 | res.end('Not found'); 30 | } 31 | 32 | io.on('connection', function (socket) { 33 | socket.on('timesync', function (data) { 34 | console.log('message', data); 35 | socket.emit('timesync', { 36 | id: data && 'id' in data ? data.id : null, 37 | result: Date.now() 38 | }); 39 | }); 40 | }); 41 | 42 | function sendFile(filename, res) { 43 | fs.readFile(filename, 44 | function (err, data) { 45 | if (err) { 46 | res.writeHead(500); 47 | return res.end('Error loading ' + filename.split('/').pop()); 48 | } 49 | 50 | res.writeHead(200); 51 | res.end(data); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/request/request.node.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import url from 'url'; 4 | var parseUrl = url.parse; 5 | 6 | export function post (url, body, timeout) { 7 | return new Promise(function (resolve, reject) { 8 | var data = (body === 'string') ? body : JSON.stringify(body); 9 | var urlObj = parseUrl(url); 10 | 11 | // An object of options to indicate where to post to 12 | var options = { 13 | host: urlObj.hostname, 14 | port: urlObj.port, 15 | path: urlObj.path, 16 | method: 'POST', 17 | headers: {'Content-Length': data.length} 18 | }; 19 | 20 | if (body !== 'string') { 21 | options.headers['Content-Type'] = 'application/json'; 22 | } 23 | 24 | var proto = urlObj.protocol === 'https:' ? https : http; 25 | 26 | var req = proto.request(options, function(res) { 27 | res.setEncoding('utf8'); 28 | var result = ''; 29 | res.on('data', function (data) { 30 | result += data; 31 | }); 32 | 33 | res.on('end', function () { 34 | var contentType = res.headers['content-type']; 35 | var isJSON = contentType && contentType.indexOf('json') !== -1; 36 | 37 | try { 38 | var body = isJSON ? JSON.parse(result) : result; 39 | 40 | resolve([body, res.statusCode]); 41 | } catch (err) { 42 | reject(err) 43 | } 44 | }); 45 | }); 46 | 47 | req.on('error', reject); 48 | 49 | req.on('socket', function(socket) { 50 | socket.setTimeout(timeout, function() { 51 | req.abort(); 52 | }); 53 | }); 54 | 55 | req.write(data); 56 | req.end(); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/advanced/express/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 48 | 49 | 50 | (see outputs in the developer console) 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/advanced/http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 49 | 50 | 51 | (see outputs in the developer console) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var Promise = require('./Promise'); 2 | 3 | /** 4 | * Resolve a promise after a delay 5 | * @param {number} delay A delay in milliseconds 6 | * @returns {Promise} Resolves after given delay 7 | */ 8 | export function wait(delay) { 9 | return new Promise(function (resolve) { 10 | setTimeout(resolve, delay); 11 | }); 12 | } 13 | 14 | /** 15 | * Repeat a given asynchronous function a number of times 16 | * @param {function} fn A function returning a promise 17 | * @param {number} times 18 | * @return {Promise} 19 | */ 20 | export function repeat(fn, times) { 21 | return new Promise(function (resolve, reject) { 22 | var count = 0; 23 | var results = []; 24 | 25 | function recurse() { 26 | if (count < times) { 27 | count++; 28 | fn().then(function (result) { 29 | results.push(result); 30 | recurse(); 31 | }) 32 | } 33 | else { 34 | resolve(results); 35 | } 36 | } 37 | 38 | recurse(); 39 | }); 40 | } 41 | 42 | /** 43 | * Repeat an asynchronous callback function whilst 44 | * @param {function} condition A function returning true or false 45 | * @param {function} callback A callback returning a Promise 46 | * @returns {Promise} 47 | */ 48 | export function whilst(condition, callback) { 49 | return new Promise(function (resolve, reject) { 50 | function recurse() { 51 | if (condition()) { 52 | callback().then(() => recurse()); 53 | } 54 | else { 55 | resolve(); 56 | } 57 | } 58 | 59 | recurse(); 60 | }); 61 | } 62 | 63 | /** 64 | * Simple id generator 65 | * @returns {number} Returns a new id 66 | */ 67 | export function nextId() { 68 | return _id++; 69 | } 70 | var _id = 0; 71 | -------------------------------------------------------------------------------- /examples/advanced/http/server.js: -------------------------------------------------------------------------------- 1 | var app = require('http').createServer(handler); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var PORT = 8081; 6 | 7 | app.listen(PORT); 8 | console.log('Server listening at http://localhost:' + PORT); 9 | 10 | function handler (req, res) { 11 | console.log('request', req.url); 12 | 13 | if (req.url === '/timesync/timesync.js') { 14 | res.setHeader('Content-Type', 'application/javascript'); 15 | return sendFile(path.join(__dirname, '../../../dist/timesync.js'), res); 16 | } 17 | 18 | if (req.url === '/timesync') { 19 | if (req.method == 'POST') { 20 | var body = ''; 21 | req.on('data', function (data) { 22 | body += data; 23 | 24 | // Too much POST data, kill the connection! 25 | if (body.length > 1e6) { 26 | req.connection.destroy(); 27 | } 28 | }); 29 | req.on('end', function () { 30 | var input = JSON.parse(body); 31 | 32 | var data = { 33 | id: 'id' in input ? input.id : null, 34 | result: Date.now() 35 | }; 36 | res.writeHead(200); 37 | res.end(JSON.stringify(data)); 38 | }); 39 | } 40 | 41 | return; 42 | } 43 | 44 | if (req.url === '/' || req.url === 'index.html') { 45 | res.setHeader('Content-Type', 'text/html'); 46 | return sendFile(__dirname + '/index.html', res); 47 | } 48 | 49 | res.writeHead(404); 50 | res.end('Not found'); 51 | } 52 | 53 | function sendFile(filename, res) { 54 | fs.readFile(filename, 55 | function (err, data) { 56 | if (err) { 57 | res.writeHead(500); 58 | return res.end('Error loading ' + filename.split('/').pop()); 59 | } 60 | 61 | res.writeHead(200); 62 | res.end(data); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timesync", 3 | "version": "1.0.11", 4 | "description": "Time synchronization between peers", 5 | "author": "Jos de Jong (https://github.com/josdejong)", 6 | "main": "./lib/timesync.js", 7 | "browser": "./dist/timesync.min.js", 8 | "react-native": "./dist/timesync.min.js", 9 | "license": "MIT", 10 | "keywords": [ 11 | "time", 12 | "synchronization", 13 | "ntp", 14 | "client", 15 | "server", 16 | "isomorphic" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/enmasseio/timesync.git" 21 | }, 22 | "dependencies": { 23 | "debug": "^4.3.4" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "7.18.10", 27 | "@babel/core": "7.18.13", 28 | "@babel/preset-env": "7.18.10", 29 | "babel-cli": "6.26.0", 30 | "babelify": "10.0.0", 31 | "body-parser": "1.20.0", 32 | "browserify": "17.0.0", 33 | "express": "4.18.1", 34 | "mkdirp": "1.0.4", 35 | "promise": "8.2.0", 36 | "socket.io": "4.5.2", 37 | "uglify-js": "3.17.0", 38 | "watch": "1.0.2" 39 | }, 40 | "scripts": { 41 | "bundle": "mkdirp dist && browserify src/timesync.js -t babelify -s timesync -o dist/timesync.js --bare", 42 | "minify": "uglifyjs dist/timesync.js -o dist/timesync.min.js", 43 | "compile": "babel src --out-dir lib", 44 | "build": "npm run bundle && npm run minify && npm run compile", 45 | "watch": "watch \"npm run build\" src", 46 | "prepublishOnly": "npm run build" 47 | }, 48 | "browserify": { 49 | "transform": [ 50 | "babelify" 51 | ] 52 | }, 53 | "files": [ 54 | "dist", 55 | "docs", 56 | "examples", 57 | "lib", 58 | "server", 59 | "src", 60 | "HISTORY.md", 61 | "LICENSE", 62 | "README.md" 63 | ] 64 | } -------------------------------------------------------------------------------- /src/request/request.browser.js: -------------------------------------------------------------------------------- 1 | export function fetch (method, url, body, headers, callback, timeout) { 2 | try { 3 | var xhr = new XMLHttpRequest(); 4 | xhr.onreadystatechange = function() { 5 | if (xhr.readyState == 4) { 6 | var contentType = xhr.getResponseHeader('Content-Type'); 7 | if (contentType && contentType.indexOf('json') !== -1) { 8 | try { 9 | // return JSON object 10 | var response = JSON.parse(xhr.responseText); 11 | callback(null, response, xhr.status); 12 | } catch (err) { 13 | callback(err, null, xhr.status); 14 | } 15 | } 16 | else { 17 | // return text 18 | callback(null, xhr.responseText, xhr.status); 19 | } 20 | } 21 | }; 22 | if (headers) { 23 | for (var name in headers) { 24 | if (headers.hasOwnProperty(name)) { 25 | xhr.setRequestHeader(name, headers[name]); 26 | } 27 | } 28 | } 29 | 30 | xhr.ontimeout = function (err) { 31 | callback(err, null, 0); 32 | }; 33 | 34 | xhr.open(method, url, true); 35 | xhr.timeout = timeout; 36 | 37 | if (typeof body === 'string') { 38 | xhr.send(body); 39 | } 40 | else if (body) { // body is an object 41 | xhr.setRequestHeader('Content-Type', 'application/json'); 42 | xhr.send(JSON.stringify(body)); 43 | } 44 | else { 45 | xhr.send(); 46 | } 47 | } 48 | catch (err) { 49 | callback(err, null, 0); 50 | } 51 | } 52 | 53 | export function post (url, body, timeout) { 54 | return new Promise((resolve, reject) => { 55 | var callback = (err, res, status) => { 56 | if (err) { 57 | return reject(err); 58 | } 59 | 60 | resolve([res, status]); 61 | }; 62 | 63 | fetch('POST', url, body, null, callback, timeout) 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /examples/advanced/peerjs/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | custom peer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 |

Setup

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
your own id
comma separated list with id's of peers
45 | 46 |

Status

47 | 48 | 49 | 50 | 51 |
System time
Synchronized time
Offset
52 |

53 | 54 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | 4 | ## 2022-09-05, version 1.0.11 5 | 6 | - Fix #51: upgrade `debug` dependency to the latest version. 7 | 8 | 9 | ## 2021-10-24, version 1.0.10 10 | 11 | - Fix #22: Emit error when the HTTP StatusCode is not 200 (see #44). 12 | Thanks @gioid. 13 | 14 | 15 | ## 2021-10-24, version 1.0.9 16 | 17 | - (oopsie, no changes merged, see 1.0.10) 18 | 19 | 20 | ## 2020-10-03, version 1.0.8 21 | 22 | - Fix #31: add fields `"browser"` and `"react-native"` to package.json, 23 | referring to the browser bundle `./dist/timesync.min.js`. 24 | 25 | 26 | ## 2020-06-29, version 1.0.7 27 | 28 | - Fixed a try/catch using optional catch not supported by 29 | all JavaScript engines. Thanks @cracker0dks. 30 | 31 | 32 | ## 2020-06-10, version 1.0.6 33 | 34 | - Fix #31: library not working in React Native. 35 | - Make the server side robust against requests with an invalid, 36 | unparsable body: return ah HTTP 400 error. Thanks @calebTomlinson. 37 | - Upgraded the dependency `debug` and all devDependencies. 38 | 39 | 40 | ## 2020-02-01, version 1.0.5 41 | 42 | - Fix handing invalid JSON responses, see #27. Thanks @Poky85. 43 | 44 | 45 | ## 2019-10-12, version 1.0.4 46 | 47 | - Move the `dependencies` only used in examples to `devDependencies`: 48 | `express`, `body-parser`, `socket.io`, promise`. To run the examples, 49 | you will have to install these dependencies yourself. 50 | 51 | 52 | ## 2018-05-21, version 1.0.3 53 | 54 | - Fixed the `send` method passing a wrong parameter to `request.post`. 55 | 56 | 57 | ## 2018-03-07, version 1.0.2 58 | 59 | - Fixed a broken example. Thanks @gokayokyay. 60 | 61 | 62 | ## 2018-02-12, version 1.0.1 63 | 64 | - Upgraded some dependencies containing security vulnerabilities. 65 | 66 | 67 | ## 2018-02-12, version 1.0.0 68 | 69 | - Function `send` now needs to return a Promise, and has to handle 70 | timeouts itself. This fixes timeouts occurring before xhr success. 71 | Thanks @DerekKeeler. 72 | - Upgraded dependencies. 73 | 74 | 75 | ## 2017-10-23, version 0.4.2 76 | 77 | - Fixed syncing to peers not using custom configuration `now`. 78 | Thanks @reklawnos. 79 | 80 | 81 | ## 2017-08-19, version 0.4.1 82 | 83 | - Made examples robust against a varying working directory. 84 | - Fixed #11: added timeout to http requests to fix infinite 85 | call / response loops. Thanks @dfeblowitz. 86 | 87 | 88 | ## 2017-06-13, version 0.4.0 89 | 90 | - Added support for `https`. Thanks @jaquadro. 91 | - Added a tutorial on how to use `timesync` in Android. Thanks @NicoHerbig. 92 | 93 | 94 | ## 2017-04-08, version 0.3.1 95 | 96 | - Fixed a bug where JSON could be truncated on the server. 97 | Thanks Matt Kincaid. 98 | 99 | 100 | ## 2017-01-25, version 0.3.0 101 | 102 | - Deliver ES5 version of the library in lib, bundled files in dist. 103 | Thanks @electrified. 104 | 105 | 106 | ## 2016-12-15, version 0.2.3 107 | 108 | - Fixed some of the browser examples not working on Firefox. 109 | 110 | 111 | ## 2016-12-10, version 0.2.2 112 | 113 | - Fixed library not working as part of a browserify setup. Thanks @SupremeTechnopriest. 114 | - Fixed `sendTimestamp` not working in an express application. Thanks @SupremeTechnopriest. 115 | 116 | 117 | ## 2016-06-01, version 0.2.1 118 | 119 | - Fixed #6: error when loading `timesync` in a node.js client. 120 | 121 | 122 | ## 2015-12-04, version 0.2.0 123 | 124 | - Implemented an `'error'` event. 125 | - Fixed unnecessary wait of 1 sec before the new offset is applied. 126 | - Fixed #3: stat.median not using sorted array. 127 | 128 | 129 | ## 2015-02-17, version 0.1.0 130 | 131 | - The first usable version. 132 | 133 | 134 | ## 2015-01-26, version 0.0.1 135 | 136 | - Library name reserved at npm and bower. 137 | -------------------------------------------------------------------------------- /examples/advanced/peerjs/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a peer with id, and connect to the given peers 3 | * @param {string} id 4 | * @param {string[]} peers 5 | * @return {{peer: Window.Peer, ts: Object}} Returns an object with the 6 | * created peer and the timesync 7 | */ 8 | function connect(id, peers) { 9 | var domSystemTime = document.getElementById('systemTime'); 10 | var domSyncTime = document.getElementById('syncTime'); 11 | var domOffset = document.getElementById('offset'); 12 | var domSyncing = document.getElementById('syncing'); 13 | 14 | var ts = timesync.create({ 15 | peers: [], // start empty, will be updated at the start of every synchronization 16 | interval: 5000, 17 | delay: 200, 18 | timeout: 1000 19 | }); 20 | 21 | ts.on('sync', function (state) { 22 | console.log('sync ' + state); 23 | if (state == 'start') { 24 | ts.options.peers = openConnections(); 25 | console.log('syncing with peers [' + ts.options.peers.join(', ') + ']'); 26 | if (ts.options.peers.length) { 27 | domSyncing.innerHTML = 'syncing with ' + ts.options.peers.join(', ') + '...'; 28 | } 29 | } 30 | if (state == 'end') { 31 | domSyncing.innerHTML = ''; 32 | } 33 | }); 34 | 35 | ts.on('change', function (offset) { 36 | console.log('changed offset: ' + offset); 37 | domOffset.innerHTML = offset.toFixed(1) + ' ms'; 38 | }); 39 | 40 | ts.send = function (id, data, timeout) { 41 | //console.log('send', id, data); 42 | var all = peer.connections[id]; 43 | var conn = all && all.filter(function (conn) { 44 | return conn.open; 45 | })[0]; 46 | 47 | if (conn) { 48 | conn.send(data); 49 | } 50 | else { 51 | console.log(new Error('Cannot send message: not connected to ' + id).toString()); 52 | } 53 | 54 | // Ignoring timeouts 55 | return Promise.resolve(); 56 | }; 57 | 58 | // show the system time and synced time once a second on screen 59 | setInterval(function () { 60 | domSystemTime.innerHTML = new Date().toISOString().replace(/[A-Z]/g, ' '); 61 | domSyncTime.innerHTML = new Date(ts.now()).toISOString().replace(/[A-Z]/g, ' '); 62 | }, 1000); 63 | 64 | // Create a new Peer with the demo API key 65 | var peer = new Peer(id, {key: 'lwjd5qra8257b9', debug: 1}); 66 | peer.on('open', connectToPeers); 67 | peer.on('connection', setupConnection); 68 | 69 | function openConnections() { 70 | return Object.keys(peer.connections).filter(function (id) { 71 | return peer.connections[id].some(function (conn) { 72 | return conn.open; 73 | }); 74 | }); 75 | } 76 | 77 | function connectToPeers() { 78 | peers 79 | .filter(function (id) { 80 | return peer.connections[id] === undefined; 81 | }) 82 | .forEach(function (id) { 83 | console.log('connecting with ' + id + '...'); 84 | var conn = peer.connect(id); 85 | setupConnection(conn); 86 | }); 87 | } 88 | 89 | function setupConnection(conn) { 90 | conn 91 | .on('open', function () { 92 | console.log('connected with ' + conn.peer); 93 | }) 94 | .on('data', function(data) { 95 | //console.log('receive', conn.peer, data); 96 | ts.receive(conn.peer, data); 97 | }) 98 | .on('close', function () { 99 | console.log('disconnected from ' + conn.peer); 100 | }) 101 | .on('error', function (err) { 102 | console.log('Error', err); 103 | }); 104 | } 105 | 106 | // check whether there are connections missing every 10 sec 107 | setInterval(connectToPeers, 10000); 108 | 109 | return { 110 | peer: peer, 111 | ts: ts 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /docs/android-tutorial.md: -------------------------------------------------------------------------------- 1 | # Using the timesync library in Android applications 2 | 3 | This tutorial illustrates how the timesync library can be used to synchronize time between a node.js server and an android application. 4 | 5 | ## Step 1: 6 | Place the following index.html on your webserver which uses the timesync library and will be called from Java: 7 | ```HTML 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 39 | 40 | 41 | ``` 42 | 43 | ## Step 2: 44 | Add an invisible webview to your Android applications layout.xml: 45 | 46 | ```XML 47 | 52 | ``` 53 | 54 | ## Step 3: 55 | In your Acitivity's `onCreate` method retieve this webview and initialize time synchronization: 56 | 57 | ```Java 58 | mWebView = (WebView) findViewById(R.id.webview); 59 | WebSettings settings = mWebView.getSettings(); 60 | settings.setJavaScriptEnabled(true); 61 | settings.setAllowFileAccessFromFileURLs(true); 62 | settings.setAllowUniversalAccessFromFileURLs(true); 63 | mWebView.loadUrl("http://" + serverIPPort); 64 | mWebView.addJavascriptInterface(new CustomJavaScriptInterface(mWebView.getContext()), "Android"); 65 | ``` 66 | 67 | ## Step 4: 68 | Define the `CustomJavaScriptInterface`: 69 | 70 | ```Java 71 | public class CustomJavaScriptInterface { 72 | Context mContext; 73 | 74 | /** 75 | * Instantiate the interface and set the context 76 | */ 77 | CustomJavaScriptInterface(Context c) { 78 | mContext = c; 79 | } 80 | 81 | /** 82 | * retrieve the server time 83 | */ 84 | @JavascriptInterface 85 | public void getServerTime(final long time) { 86 | Log.d(TAG, "Time is " + time); 87 | MainActivity.serverTime = time; 88 | } 89 | 90 | @JavascriptInterface 91 | public void getTimeOffset(final long offset) { 92 | Log.d(TAG, "Time offset is " + offset); 93 | MainActivity.timeOffset = offset; 94 | } 95 | } 96 | ``` 97 | 98 | ## Step 5: 99 | Add the following Java methods to your Activity to retrieve the server time and the time offset through your JavascriptInterface. As you can see, these methods use the `ts` variable of the HTML file on the server to set the server time within the Android Activity: 100 | 101 | ```Java 102 | private long getServerTime() { 103 | mWebView.loadUrl("javascript:Android.getServerTime(ts.now());"); 104 | return serverTime; 105 | } 106 | 107 | private long getTimeOffset() { 108 | mWebView.loadUrl("javascript:Android.getTimeOffset(lastOffset);"); 109 | return timeOffset; 110 | } 111 | ``` 112 | 113 | ## Step 6: 114 | Call the Java methods `getServerTime()` and `getTimeOffset` anywhere in your code to receive the synchronized time or the time offset. 115 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var http = require('http'); 3 | var debug = require('debug')('timesync'); 4 | 5 | /** 6 | * Create a http server for time synchronization 7 | * @return {http.Server} Returns the server instance. 8 | */ 9 | exports.createServer = function () { 10 | // create a new server 11 | debug('creating a new server'); 12 | return http.createServer(exports.requestHandler); 13 | }; 14 | 15 | /** 16 | * Attaches a timesync request hander to an existing server 17 | * 18 | * Implementation based on code from socket.io 19 | * https://github.com/Automattic/socket.io/blob/master/lib/index.js 20 | * 21 | * @param {http.Server} server http server 22 | * @param {string} [path] optional path, `/timesync` by default 23 | */ 24 | exports.attachServer = function (server, path) { 25 | if (server instanceof http.Server) { 26 | debug('attach to existing server'); 27 | 28 | var listeners = server.listeners('request').slice(0); 29 | server.removeAllListeners('request'); 30 | 31 | var route = createRoute(path); 32 | 33 | server.on('request', function(req, res) { 34 | var handled = route(req, res); 35 | if (!handled) { 36 | listeners.forEach(function (listener) { 37 | listener.call(server, req, res) 38 | }); 39 | } 40 | }); 41 | } 42 | else { 43 | throw new TypeError('Instance of http.Server expected'); 44 | } 45 | }; 46 | 47 | /** 48 | * Create a route matching whether to apply the timesync's request handler 49 | * @param {string} [path] optional path, `/timesync` by default 50 | */ 51 | function createRoute (path) { 52 | path = path || '/timesync'; 53 | var pattern = new RegExp(path + '($|/(.*))'); 54 | 55 | debug('creating request handler listening on path ' + path); 56 | 57 | return function (req, res) { 58 | if (pattern.exec(req.url)) { 59 | exports.requestHandler(req, res); 60 | return true; // handled 61 | } 62 | else { 63 | return false; // not handled 64 | } 65 | }; 66 | } 67 | 68 | /** 69 | * A request handler for time requests. 70 | * - In case of POST, an object containing the current timestamp will be 71 | * returned. 72 | * - In case of GET .../timesync.js or GET .../timesync.min.js, the static 73 | * files will be returned. 74 | * @param req 75 | * @param res 76 | * @returns {*} 77 | */ 78 | exports.requestHandler = function (req, res) { 79 | debug('request ' + req.method + ' ' + req.url + ' ' + req.method); 80 | 81 | if (req.method == 'POST') { 82 | if (!filename) { 83 | // a time request 84 | return sendTimestamp(req, res); 85 | } 86 | } 87 | 88 | if (req.method == 'GET') { 89 | var match = req.url.match(/\/(timesync(.min)?.js)$/); 90 | var filename = match && match[1]; 91 | if (filename === 'timesync.js' || filename === 'timesync.min.js') { 92 | // static file handler 93 | return sendFile(res, __dirname + '/../dist/' + filename); 94 | } 95 | } 96 | 97 | res.writeHead(404); 98 | res.end('Not found'); 99 | }; 100 | 101 | 102 | function sendTimestamp(req, res) { 103 | if (req.body) { 104 | // express.js or similar 105 | sendTimeStampResponse(null, res, req.body); 106 | } 107 | else { 108 | // node.js 109 | readRequestBody(req, function (err, body) { 110 | sendTimeStampResponse(err, res, body); 111 | }); 112 | } 113 | } 114 | 115 | function readRequestBody (req, callback) { 116 | var body = ''; 117 | req.on('data', function (data) { 118 | body += data; 119 | 120 | // Too much POST data, kill the connection! 121 | if (body.length > 1e6) { 122 | callback(new Error('Too much post data. Killing connection...'), null); 123 | req.connection.destroy(); 124 | } 125 | }); 126 | req.on('end', function () { 127 | try { 128 | callback(null, JSON.parse(body)); 129 | } catch(e) { 130 | callback(new Error('Invalid json in post body')); 131 | } 132 | }); 133 | } 134 | 135 | function sendTimeStampResponse (err, res, body) { 136 | if (err) { 137 | res.writeHead(400); 138 | res.end(err.message); 139 | } else { 140 | 141 | var data = { 142 | id: 'id' in body ? body.id : null, 143 | result: Date.now() 144 | }; 145 | 146 | // Set content-type header 147 | res.setHeader('Content-Type', 'application/json'); 148 | 149 | if(res.status && res.send) { 150 | // express.js or similar 151 | res.status(200).send(JSON.stringify(data)); 152 | } else { 153 | // node.js 154 | res.writeHead(200); 155 | res.end(JSON.stringify(data)); 156 | } 157 | } 158 | 159 | } 160 | 161 | 162 | function sendFile(res, filename) { 163 | debug('serve static file ' + filename); 164 | fs.readFile(filename, function (err, data) { 165 | if (err) { 166 | res.writeHead(500); 167 | res.end('Error loading ' + filename.split('/').pop()); 168 | } 169 | else { 170 | // note: content type depends on type of file, 171 | // but in this case we only serve javascript files 172 | // so we just hard code the Content-Type to application/javascript 173 | res.setHeader('Content-Type', 'application/javascript'); 174 | res.writeHead(200); 175 | res.end(data); 176 | } 177 | }); 178 | } 179 | -------------------------------------------------------------------------------- /src/timesync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * timesync 3 | * 4 | * Time synchronization between peers 5 | * 6 | * https://github.com/enmasseio/timesync 7 | */ 8 | 9 | import * as util from './util.js'; 10 | import * as stat from './stat.js'; 11 | import * as request from './request/request'; 12 | import emitter from './emitter.js'; 13 | var Promise = require('./Promise'); 14 | 15 | /** 16 | * Factory function to create a timesync instance 17 | * @param {Object} [options] TODO: describe options 18 | * @return {Object} Returns a new timesync instance 19 | */ 20 | export function create(options) { 21 | var timesync = { 22 | // configurable options 23 | options: { 24 | interval: 60 * 60 * 1000, // interval for doing synchronizations in ms. Set to null to disable auto sync 25 | timeout: 10000, // timeout for requests to fail in ms 26 | delay: 1000, // delay between requests in ms 27 | repeat: 5, // number of times to do a request to one peer 28 | peers: [], // uri's or id's of the peers 29 | server: null, // uri of a single server (master/slave configuration) 30 | now: Date.now // function returning the system time 31 | }, 32 | 33 | /** @type {number} The current offset from system time */ 34 | offset: 0, // ms 35 | 36 | /** @type {number} Contains the timeout for the next synchronization */ 37 | _timeout: null, 38 | 39 | /** @type {Object.} Contains a map with requests in progress */ 40 | _inProgress: {}, 41 | 42 | /** 43 | * @type {boolean} 44 | * This property used to immediately apply the first ever received offset. 45 | * After that, it's set to false and not used anymore. 46 | */ 47 | _isFirst: true, 48 | 49 | /** 50 | * Send a message to a peer 51 | * This method must be overridden when using timesync 52 | * @param {string} to 53 | * @param {*} data 54 | */ 55 | send: function (to, data, timeout) { 56 | return request.post(to, data, timeout) 57 | .then(function (val) { 58 | var res = val[0]; 59 | 60 | if(val[1] !== 200) { 61 | throw new Error('Send failure: bad HTTP StatusCode (' + val[1] + ')'); 62 | } 63 | 64 | timesync.receive(to, res); 65 | }) 66 | .catch(function (err) { 67 | emitError(err); 68 | }); 69 | }, 70 | 71 | /** 72 | * Receive method to be called when a reply comes in 73 | * @param {string | undefined} [from] 74 | * @param {*} data 75 | */ 76 | receive: function (from, data) { 77 | if (data === undefined) { 78 | data = from; 79 | from = undefined; 80 | } 81 | 82 | if (data && data.id in timesync._inProgress) { 83 | // this is a reply 84 | timesync._inProgress[data.id](data.result); 85 | } 86 | else if (data && data.id !== undefined) { 87 | // this is a request from an other peer 88 | // reply with our current time 89 | timesync.send(from, { 90 | jsonrpc: '2.0', 91 | id: data.id, 92 | result: timesync.now() 93 | }) 94 | } 95 | }, 96 | 97 | _handleRPCSendError: function (id, reject, err) { 98 | delete timesync._inProgress[id]; 99 | reject(new Error('Send failure')); 100 | }, 101 | 102 | /** 103 | * Send a JSON-RPC message and retrieve a response 104 | * @param {string} to 105 | * @param {string} method 106 | * @param {*} [params] 107 | * @returns {Promise} 108 | */ 109 | rpc: function (to, method, params) { 110 | var id = util.nextId(); 111 | var resolve, reject; 112 | var deferred = new Promise((res, rej) => { 113 | resolve = res; 114 | reject = rej; 115 | }); 116 | 117 | timesync._inProgress[id] = function (data) { 118 | delete timesync._inProgress[id]; 119 | 120 | resolve(data); 121 | }; 122 | 123 | let sendResult; 124 | 125 | try { 126 | sendResult = timesync.send(to, { 127 | jsonrpc: '2.0', 128 | id: id, 129 | method: method, 130 | params: params 131 | }, timesync.options.timeout); 132 | } catch(err) { 133 | timesync._handleRPCSendError(id, reject, err); 134 | } 135 | 136 | if (sendResult && (sendResult instanceof Promise || (sendResult.then && sendResult.catch))) { 137 | sendResult.catch(timesync._handleRPCSendError.bind(this, id, reject)); 138 | } else { 139 | console.warn('Send should return a promise'); 140 | } 141 | 142 | return deferred; 143 | }, 144 | 145 | /** 146 | * Synchronize now with all configured peers 147 | * Docs: http://www.mine-control.com/zack/timesync/timesync.html 148 | */ 149 | sync: function () { 150 | timesync.emit('sync', 'start'); 151 | 152 | var peers = timesync.options.server ? 153 | [timesync.options.server] : 154 | timesync.options.peers; 155 | return Promise 156 | .all(peers.map(peer => timesync._syncWithPeer(peer))) 157 | .then(function (all) { 158 | var offsets = all.filter(offset => timesync._validOffset(offset)); 159 | if (offsets.length > 0) { 160 | // take the average of all peers (excluding self) as new offset 161 | timesync.offset = stat.mean(offsets); 162 | timesync.emit('change', timesync.offset); 163 | } 164 | timesync.emit('sync', 'end'); 165 | }); 166 | }, 167 | 168 | /** 169 | * Test whether given offset is a valid number (not NaN, Infinite, or null) 170 | * @param {number} offset 171 | * @returns {boolean} 172 | * @private 173 | */ 174 | _validOffset: function (offset) { 175 | return offset !== null && !isNaN(offset) && isFinite(offset); 176 | }, 177 | 178 | /** 179 | * Sync one peer 180 | * @param {string} peer 181 | * @return {Promise.} Resolves with the offset to this peer, 182 | * or null if failed to sync with this peer. 183 | * @private 184 | */ 185 | _syncWithPeer: function (peer) { 186 | // retrieve the offset of a peer, then wait 1 sec 187 | var all = []; 188 | 189 | function sync () { 190 | return timesync._getOffset(peer).then(result => all.push(result)); 191 | } 192 | 193 | function waitAndSync() { 194 | return util.wait(timesync.options.delay).then(sync); 195 | } 196 | 197 | function notDone() { 198 | return all.length < timesync.options.repeat; 199 | } 200 | 201 | return sync() 202 | .then(function () { 203 | return util.whilst(notDone, waitAndSync) 204 | }) 205 | .then(function () { 206 | // filter out null results 207 | var results = all.filter(result => result !== null); 208 | 209 | // calculate the limit for outliers 210 | var roundtrips = results.map(result => result.roundtrip); 211 | var limit = stat.median(roundtrips) + stat.std(roundtrips); 212 | 213 | // filter all results which have a roundtrip smaller than the mean+std 214 | var filtered = results.filter(result => result.roundtrip < limit); 215 | var offsets = filtered.map(result => result.offset); 216 | 217 | // return the new offset 218 | return (offsets.length > 0) ? stat.mean(offsets) : null; 219 | }); 220 | }, 221 | 222 | /** 223 | * Retrieve the offset from one peer by doing a single call to the peer 224 | * @param {string} peer 225 | * @returns {Promise.<{roundtrip: number, offset: number} | null>} 226 | * @private 227 | */ 228 | _getOffset: function (peer) { 229 | var start = timesync.options.now(); // local system time 230 | 231 | return timesync.rpc(peer, 'timesync') 232 | .then(function (timestamp) { 233 | var end = timesync.options.now(); // local system time 234 | var roundtrip = end - start; 235 | var offset = timestamp - end + roundtrip / 2; // offset from local system time 236 | 237 | // apply the first ever retrieved offset immediately. 238 | if (timesync._isFirst) { 239 | timesync._isFirst = false; 240 | timesync.offset = offset; 241 | timesync.emit('change', offset); 242 | } 243 | 244 | return { 245 | roundtrip: roundtrip, 246 | offset: offset 247 | }; 248 | }) 249 | .catch(function (err) { 250 | // just ignore failed requests, return null 251 | return null; 252 | }); 253 | }, 254 | 255 | /** 256 | * Get the current time 257 | * @returns {number} Returns a timestamp 258 | */ 259 | now: function () { 260 | return timesync.options.now() + timesync.offset; 261 | }, 262 | 263 | /** 264 | * Destroy the timesync instance. Stops automatic synchronization. 265 | * If timesync is currently executing a synchronization, this 266 | * synchronization will be finished first. 267 | */ 268 | destroy: function () { 269 | clearTimeout(timesync._timeout); 270 | } 271 | }; 272 | 273 | // apply provided options 274 | if (options) { 275 | if (options.server && options.peers) { 276 | throw new Error('Configure either option "peers" or "server", not both.'); 277 | } 278 | 279 | for (var prop in options) { 280 | if (options.hasOwnProperty(prop)) { 281 | if (prop === 'peers' && typeof options.peers === 'string') { 282 | // split a comma separated string with peers into an array 283 | timesync.options.peers = options.peers 284 | .split(',') 285 | .map(peer => peer.trim()) 286 | .filter(peer => peer !== ''); 287 | } 288 | else { 289 | timesync.options[prop] = options[prop]; 290 | } 291 | } 292 | } 293 | } 294 | 295 | // turn into an event emitter 296 | emitter(timesync); 297 | 298 | /** 299 | * Emit an error message. If there are no listeners, the error is outputted 300 | * to the console. 301 | * @param {Error} err 302 | */ 303 | function emitError(err) { 304 | if (timesync.list('error').length > 0) { 305 | timesync.emit('error', err); 306 | } 307 | else { 308 | console.log('Error', err); 309 | } 310 | } 311 | 312 | if (timesync.options.interval !== null) { 313 | // start an interval to automatically run a synchronization once per interval 314 | timesync._timeout = setInterval(timesync.sync, timesync.options.interval); 315 | 316 | // synchronize immediately on the next tick (allows to attach event 317 | // handlers before the timesync starts). 318 | setTimeout(function () { 319 | timesync.sync().catch(err => emitError(err)); 320 | }, 0); 321 | } 322 | 323 | return timesync; 324 | } 325 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timesync 2 | 3 | Time synchronization between peers. 4 | 5 | Usage scenarios: 6 | 7 | - **master/slave**: Clients synchronize their time to that of a single server, 8 | via either HTTP requests or WebSockets. 9 | - **peer-to-peer**: Clients are connected in a (dynamic) peer-to-peer network 10 | using WebRTC or WebSockets and must converge to a single, common time in the 11 | network. 12 | 13 | 14 | # Install 15 | 16 | Install via npm: 17 | 18 | ``` 19 | npm install timesync 20 | ``` 21 | 22 | 23 | # Usage 24 | 25 | A timesync client can basically connect to one server or multiple peers, 26 | and will synchronize it's time. The synchronized time can be retrieved via 27 | the method `now()`, and the client can subscribe to events like `'change'` 28 | and `'sync'`. 29 | 30 | ```js 31 | // create a timesync instance 32 | var ts = timesync({ 33 | server: '...', // either a single server, 34 | peers: [...] // or multiple peers 35 | }); 36 | 37 | // get notified on changes in the offset 38 | ts.on('change', function (offset) { 39 | console.log('offset from system time:', offset, 'ms'); 40 | } 41 | 42 | // get the synchronized time 43 | console.log('now:', new Date(ts.now())); 44 | ``` 45 | 46 | 47 | # Example 48 | 49 | Here a full usage example with express.js, showing both server and client side. 50 | `timesync` has build-in support for requests over http and can be used with 51 | express, a default http server, or other solutions. `timesync` can also be 52 | used over other transports than http, for example using websockets or webrtc. 53 | This is demonstrated in the [advanced examples](/examples/advanced). 54 | 55 | More examples are available in the [/examples](/examples) folder. 56 | Some of the examples use libraries like `express` or `socket.io`. 57 | Before you can run these examples you will have to install these dependencies. 58 | 59 | **server.js** 60 | 61 | ```js 62 | var express = require('express'); 63 | var timesyncServer = require('timesync/server'); 64 | 65 | // create an express app 66 | var port = 8081; 67 | var app = express(); 68 | app.listen(port); 69 | console.log('Server listening at http://localhost:' + port); 70 | 71 | // serve static index.html 72 | app.get('/', express.static(__dirname)); 73 | 74 | // handle timesync requests 75 | app.use('/timesync', timesyncServer.requestHandler); 76 | ``` 77 | 78 | **index.html** 79 | 80 | ```html 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 108 | 109 | ``` 110 | 111 | 112 | # API 113 | 114 | ## Client 115 | 116 | ### Construction 117 | 118 | An instance of timesync is created as: 119 | 120 | ```js 121 | var ts = timesync(options); 122 | ``` 123 | 124 | #### Options 125 | 126 | The following options are available: 127 | 128 | Name | Type | Default | Description 129 | ---------- | ---------------------- | ---------- | ---------------------------------------- 130 | `delay` | `number` | `1000` | Delay in milliseconds between every request sent. 131 | `interval` | `number` or `null` | `3600000` | Interval in milliseconds for running a synchronization. Defaults to 1 hour. Set to `null` to disable automatically running synchronizations (synchronize by calling `sync()`). 132 | `now` | `function` | `Date.now` | Function returning the local system time. 133 | `peers` | `string[]` or `string` | `[]` | Array or comma separated string with uri's or id's of the peers to synchronize with. Cannot be used in conjunction with option `server`. 134 | `repeat` | `number` | `5` | Number of times to do a request to every peer. 135 | `server` | `string` | none | Url of a single server in case of a master/slave configuration. Cannot be used in conjunction with option `peers`. 136 | `timeout` | `number` | `10000` | Timeout in milliseconds for requests to fail. 137 | 138 | ### Methods 139 | 140 | Name | Return type | Description 141 | --------------------- | ----------- | ---------------------------------- 142 | `destroy()` | none | Destroy the timesync instance. Stops automatic synchronization. If timesync is currently executing a synchronization, this synchronization will be finished first. 143 | `now()` | `number` | Get the synchronized time. Returns a timestamp. To create a `Date`, call `new Date(time.now())`. 144 | `on(event, callback)` | `Object` | Register a callback handler for an event. Returns the timesync instance. See section [Events](#events) for more information. 145 | `off(event [, callback])` | `Object` | Unregister a callback handler for an event. If no callback is provided, all callbacks of this event will be removed. Returns the timesync instance. See section [Events](#events) for more information. 146 | `sync()` | none | Do a synchronization with all peers now. 147 | 148 | To be able to send and receive messages from peers, `timesync` needs a transport. To hook up a transport like a websocket or http requests, one has to override the `send(id, data)` method of the `timesync` instance, and has to call `ts.receive(id, data)` on incoming messages. 149 | 150 | Name | Return type | Description 151 | ----------------------------------- | ----------- | ---------------------------------- 152 | `send(to, data, timeout) : Promise` | none | Send a message to a peer. `to` is the id of the peer, and `data` a JSON object containing the message. Must return a Promise which resolves when the message has been sent, or rejects when sending failed or a timeout occurred. 153 | `receive(from, data)` | none | Receive a message from a peer. `from` is the id of the sender, and `data` a JSON object containing the message. 154 | 155 | `timesync` sends messages using the JSON-RPC protocol, as described in the section [Protocol](#protocol). 156 | 157 | 158 | ### Events 159 | 160 | `timesync` emits events when starting and finishing a synchronization, and when the time offset changes. To listen for events: 161 | 162 | ```js 163 | ts.on('change', function (offset) { 164 | console.log('offset changed:', offset); 165 | }); 166 | ``` 167 | 168 | Available events: 169 | 170 | Name | Description 171 | ---------| ---------- 172 | `change` | Emitted when the offset is changed. This can only happen during a synchronization. Callbacks are called with the new offset (a number) as argument. 173 | `error` | Emitted when an error occurred. Callbacks are called with the error as argument. 174 | `sync` | Emitted when a synchronization is started or finished. Callback are called with a value `'start'` or `'end'` as argument. 175 | 176 | 177 | ### Properties 178 | 179 | Name | Type | Description 180 | --------- | -------- | -------------------------------------------- 181 | `offset` | `number` | The offset from system time in milliseconds. 182 | `options` | `Object` | An object holding all options of the timesync instance. One can safely adjust options like `peers` at any time. Not all options can be changed after construction, for example a changed `interval` value will not be applied. 183 | 184 | 185 | ## Server 186 | 187 | `timesync` comes with a build in server to serve as a master for time synchronization. Clients can adjust their time to that of the server. The server basically just implements a POST request responding with its current time, and serves the static files `timesync.js` and `timesync.min.js` from the `/dist` folder. It's quite easy to implement this request handler yourself, as is demonstrated in the [advanced examples](/examples/advanced). 188 | 189 | The protocol used by the server is described in the section [Protocol](#protocol). 190 | 191 | ### Load 192 | 193 | The server can be loaded in node.js as: 194 | 195 | ```js 196 | var timesyncServer = require('timesync/server'); 197 | ``` 198 | 199 | ### Methods 200 | 201 | Name | Return type | Description 202 | ----------------------------- | ------------ | ---------------------------------- 203 | `createServer()` | `http.Server`| Create a new, dedicated http Server. This is just a shortcut for doing `http.createServer( timesyncServer.requestHandler )`. 204 | `attachServer(server, [path])`| `http.Server`| Attach a request handler for time synchronization requests to an existing http Server. Argument `server` must be an instance of `http.Server`. Argument `path` is optional, and is `/timesync` by default. 205 | 206 | 207 | ### Properties 208 | 209 | Name | Type | Description 210 | ----------------- | ---------- | -------------------------------------------- 211 | `requestHandler` | `function` | A default request handler, handling requests for the timesync server. Signature is `requestHandler(request, response)`. This handler can be used to attach to an expressjs server, or to create a plain http server by doing `http.createServer( timesyncServer.requestHandler )`. 212 | 213 | 214 | # Protocol 215 | 216 | `timesync` sends messages using the JSON-RPC protocol. A peer sends a message: 217 | 218 | ```json 219 | {"jsonrpc": "2.0", "id": "12345", "method": "timesync"} 220 | ``` 221 | 222 | The receiving peer replies with the same id and its current time: 223 | 224 | ```json 225 | {"jsonrpc": "2.0", "id": "12345", "result": 1423151204595} 226 | ``` 227 | 228 | The sending peer matches the returned message by id and uses the result to adjust it's offset. 229 | 230 | 231 | # Algorithm 232 | 233 | `timesync` uses a simple synchronization protocol aimed at the gaming industry, and extends this for peer-to-peer networks. The algorithm is described [here](https://web.archive.org/web/20160310125700/http://mine-control.com/zack/timesync/timesync.html): 234 | 235 | > A simple algorithm with these properties is as follows: 236 | > 237 | > 1. Client stamps current local time on a "time request" packet and sends to server 238 | > 2. Upon receipt by server, server stamps server-time and returns 239 | > 3. Upon receipt by client, client subtracts current time from sent time and divides by two to compute latency. It subtracts current time from server time to determine client-server time delta and adds in the half-latency to get the correct clock delta. (So far this algorithm is very similar to SNTP) 240 | > 4. The first result should immediately be used to update the clock since it will get the local clock into at least the right ballpark (at least the right timezone!) 241 | > 5. The client repeats steps 1 through 3 five or more times, pausing a few seconds each time. Other traffic may be allowed in the interim, but should be minimized for best results 242 | > 6. The results of the packet receipts are accumulated and sorted in lowest-latency to highest-latency order. The median latency is determined by picking the mid-point sample from this ordered list. 243 | > 7. All samples above approximately 1 standard-deviation from the median are discarded and the remaining samples are averaged using an arithmetic mean. 244 | 245 | This algorithm assumes multiple clients synchronizing with a single server. In case of multiple peers, `timesync` will take the average offset of all peers (excluding itself) as offset. 246 | 247 | 248 | # Tutorials 249 | 250 | - [Using the timesync library in Android applications](https://github.com/enmasseio/timesync/blob/master/docs/android-tutorial.md) 251 | 252 | 253 | # Resources 254 | 255 | - [A Stream-based Time Synchronization Technique For Networked Computer Games](https://web.archive.org/web/20160310125700/http://mine-control.com/zack/timesync/timesync.html) 256 | - [Network Time Protocol](http://www.wikiwand.com/en/Network_Time_Protocol) 257 | 258 | 259 | # Build 260 | 261 | To build the library: 262 | 263 | npm install 264 | npm run build 265 | 266 | This will generate the files `timesync.js` and `timesync.min.js` in the folder `/dist`. 267 | 268 | To automatically build on changes, run: 269 | 270 | npm run watch 271 | --------------------------------------------------------------------------------