├── .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 | | System time | |
31 | | Synchronized time | |
32 | | Offset | |
33 |
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 | | System time | |
31 | | Synchronized time | |
32 | | Offset | |
33 |
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 | | System time | |
31 | | Synchronized time | |
32 | | Offset | |
33 |
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 |
45 |
46 | Status
47 |
48 | | System time | |
49 | | Synchronized time | |
50 | | Offset | |
51 |
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 |
--------------------------------------------------------------------------------