Code Monkey home page Code Monkey logo

timesync's Introduction

timesync

Time synchronization between peers.

Usage scenarios:

  • master/slave: Clients synchronize their time to that of a single server, via either HTTP requests or WebSockets.
  • peer-to-peer: Clients are connected in a (dynamic) peer-to-peer network using WebRTC or WebSockets and must converge to a single, common time in the network.

Install

Install via npm:

npm install timesync

Usage

A timesync client can basically connect to one server or multiple peers, and will synchronize it's time. The synchronized time can be retrieved via the method now(), and the client can subscribe to events like 'change' and 'sync'.

// create a timesync instance
var ts = timesync({
  server: '...',  // either a single server,
  peers: [...]    // or multiple peers
});

// get notified on changes in the offset
ts.on('change', function (offset) {
  console.log('offset from system time:', offset, 'ms');
}

// get the synchronized time
console.log('now:', new Date(ts.now()));

Example

Here a full usage example with express.js, showing both server and client side. timesync has build-in support for requests over http and can be used with express, a default http server, or other solutions. timesync can also be used over other transports than http, for example using websockets or webrtc. This is demonstrated in the advanced examples.

More examples are available in the /examples folder. Some of the examples use libraries like express or socket.io. Before you can run these examples you will have to install these dependencies.

server.js

var express = require('express');
var timesyncServer = require('timesync/server');

// create an express app
var port = 8081;
var app = express();
app.listen(port);
console.log('Server listening at http://localhost:' + port);

// serve static index.html
app.get('/', express.static(__dirname));

// handle timesync requests
app.use('/timesync', timesyncServer.requestHandler);

index.html

<!DOCTYPE html>
<html>
<head>
  <!-- note: for support on older browsers, you will need to load es5-shim and es6-shim -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.23.0/es6-shim.min.js"></script>

  <script src="/timesync/timesync.js"></script>
</head>
<script>
  // create a timesync instance
  var ts = timesync.create({
    server: '/timesync',
    interval: 10000
  });

  // get notified on changes in the offset
  ts.on('change', function (offset) {
    document.write('changed offset: ' + offset + ' ms<br>');
  });

  // get synchronized time
  setInterval(function () {
    var now = new Date(ts.now());
    document.write('now: ' + now.toISOString() + ' ms<br>');
  }, 1000);
</script>
</html>

API

Client

Construction

An instance of timesync is created as:

var ts = timesync(options);

Options

The following options are available:

Name Type Default Description
delay number 1000 Delay in milliseconds between every request sent.
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()).
now function Date.now Function returning the local system time.
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.
repeat number 5 Number of times to do a request to every peer.
server string none Url of a single server in case of a master/slave configuration. Cannot be used in conjunction with option peers.
timeout number 10000 Timeout in milliseconds for requests to fail.

Methods

Name Return type Description
destroy() none Destroy the timesync instance. Stops automatic synchronization. If timesync is currently executing a synchronization, this synchronization will be finished first.
now() number Get the synchronized time. Returns a timestamp. To create a Date, call new Date(time.now()).
on(event, callback) Object Register a callback handler for an event. Returns the timesync instance. See section Events for more information.
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 for more information.
sync() none Do a synchronization with all peers now.

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.

Name Return type Description
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.
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.

timesync sends messages using the JSON-RPC protocol, as described in the section Protocol.

Events

timesync emits events when starting and finishing a synchronization, and when the time offset changes. To listen for events:

ts.on('change', function (offset) {
  console.log('offset changed:', offset);
});

Available events:

Name Description
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.
error Emitted when an error occurred. Callbacks are called with the error as argument.
sync Emitted when a synchronization is started or finished. Callback are called with a value 'start' or 'end' as argument.

Properties

Name Type Description
offset number The offset from system time in milliseconds.
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.

Server

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.

The protocol used by the server is described in the section Protocol.

Load

The server can be loaded in node.js as:

var timesyncServer = require('timesync/server');

Methods

Name Return type Description
createServer() http.Server Create a new, dedicated http Server. This is just a shortcut for doing http.createServer( timesyncServer.requestHandler ).
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.

Properties

Name Type Description
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 ).

Protocol

timesync sends messages using the JSON-RPC protocol. A peer sends a message:

{"jsonrpc": "2.0", "id": "12345", "method": "timesync"}

The receiving peer replies with the same id and its current time:

{"jsonrpc": "2.0", "id": "12345", "result": 1423151204595}

The sending peer matches the returned message by id and uses the result to adjust it's offset.

Algorithm

timesync uses a simple synchronization protocol aimed at the gaming industry, and extends this for peer-to-peer networks. The algorithm is described here:

A simple algorithm with these properties is as follows:

  1. Client stamps current local time on a "time request" packet and sends to server
  2. Upon receipt by server, server stamps server-time and returns
  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)
  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!)
  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
  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.
  7. All samples above approximately 1 standard-deviation from the median are discarded and the remaining samples are averaged using an arithmetic mean.

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.

Tutorials

Resources

Build

To build the library:

npm install
npm run build

This will generate the files timesync.js and timesync.min.js in the folder /dist.

To automatically build on changes, run:

npm run watch

timesync's People

Contributors

calebtomlinson avatar cracker0dks avatar dependabot[bot] avatar gioid avatar gokayokyay avatar josdejong avatar lauzierj avatar nicoherbig avatar pmuller avatar poky85 avatar reklawnos avatar supremetechnopriest avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

timesync's Issues

deprecated dependency "[email protected]"

When installing timesync via npm, the following warning message is displayed:

npm WARN deprecated [email protected]: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)

Run over arbitrary duplex stream

It'd be nice if there was a way to just shove a duplex stream from somewhere into this and have it work from that rather than specifying URLs and making assumptions about the transport medium.

Any way to use timesync with a custom API?

Hello

Your project is great. It's precisely what I'm looking for... Except for the dependency toward a JSON-RPC API.

My app is based on a GraphQL API, but my understanding is that JSON-RPC is hard-coded.

Maybe I missed something: is there an easy way to override this?

Thanks!

Suggestion: Add "sampling strategies" that control how many samples are taken

Hello there, first off thanks for writing this awesome library, it saved me a lot of time getting my project off the ground!

The only "issue" I've had is with the fixed sample count. I'd like the option of using a more "adaptive" sampling strategy that will e.g. take as many samples as necessary to guarantee with some confidence level (e.g. 98%) that the true offset has been found to within some tolerance (e.g. 10ms). This would be really nice for applications like mine where you need some guarantees on the level of synchronization.

As currently implemented, we can only control the total number of samples taken. But for clients with very low variance in offset measurements, the sample count could be overkill, and for clients with very high variance in offset measurements, the sample count could be too low.

I ultimately ended up implementing this sampling strategy myself for my project; here is the code:

class SamplePolicy {
    shouldSample(samples) {
        throw new Error('Not implemented');
    }
}


/** Take maxSamples (current timesync behavior). */
class MaxSamplesPolicy extends SamplePolicy {
    constructor(maxSamples) {
        super();
        this.maxSamples = maxSamples;
    }

    shouldSample(samples) {
        return samples.length < this.maxSamples;
    }
}


/**
 * Keep sampling until the offset is known to within the margin of error (MoE)
 * with the given confidence level.
 */
class AutoSamplePolicy extends SamplePolicy {
    constructor(config) {
        super();

        this.minSamples = config.minSamples;
        this.maxSamples = config.maxSamples;
        this._z = CI_TO_Z[config.confidenceLevel];
        this.moe = config.moe;
    }

    shouldSample(samples) {
        if (samples.length < this.minSamples) {
            return true;
        } else if (samples.length >= this.maxSamples) {
            return false;
        }

        const stddev_ = stddev(samples);
        const n = Math.ceil(Math.pow(this._z * stddev_ / this.moe, 2));

        return samples.length < n;
    }
}


/** Confidence interval to Z-score. */
const CI_TO_Z = {
    0.80 : 1.281551565545,
    0.85 : 1.439755747982,
    0.90 : 1.644853626951,
    0.95 : 1.959963984540,
    0.98 : 2.326347874041,
    0.99 : 2.575829303549,
    0.995: 2.807033485986,
    0.998: 3.090232306168,
    0.999: 3.290526731492,
};

socket is null in `send` function

I have used this module in a realtime web game. The websocket module I use is uws. But sometimes I get the error saying it couldn't perform send function on null and located to this line of my code. But this doesn't happen all the time. It only happens occasionally so it is hard to reproduce. What could possibly be the reason?

      ts.send = function(socket, data) {
      	socket.send(JSON.stringify(data));
  };

The above code is similar to socket.io example in your code but I am actually using a native browser websocket implementation.

I wanted to give a uws/ws implementation example to your code but I don't have much free time at the moment.

Can't make 'error' event to work

Hello,

I was making some tests with this great time sync, but when I put the:

ts.on('error', function (err) {
console.log('error on sync')
}

I turn off the server but the event is not called.

I can see in the console the following message:
timesync.js:962 POST http://localhost:3000/timesync net::ERR_CONNECTION_REFUSED

But no call to the event

From my reading in the source code, it looks like the function to emit the error:

  function emitError(err) {
    if (timesync.list('error').length > 0) {
      timesync.emit('error', err);
    }
    else {
      console.log('Error', err);
    }
  }

is reading from this timesync.list that is not being populated by the event listen subscription, so it is always 0.

If I use the ts.on('change', function()...), it works and shows the change events.

Can anyone check this?

Thanks

Cannot use timesync.js in the browser

When I tried to add timesync.js as a JS script to my .html file, the following warnings appear in the browser console and it doesn't work.

Uncaught SyntaxError: Cannot use import statement outside a module at timesync.js: 9
Uncaught ReferenceError: timesync is not defined at client.html:21

client.html is as follows. Note that unnecessary code is omitted for brevity.

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js"></script> 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.23.0/es6-shim.min.js"></script> 
    <script src="./timesync.js"></script>
  </head>
  <body>
    <script>
      // create a timesync instance
      var ts = timesync.create({
        server: "http://localhost:8080/timesync",
        interval: 1000
      }); 
      var socket = io("http://localhost:8080");
    </script>
  </body>
</html>

Error: Cannot find module '_process' when require timesync client in nodejs

Hi,

I'm trying to use timesync client in nodejs, but I get an error back when requiring the js file:

var timesync = require('./node_modules/timesync/dist/timesync.js');

this is the error
Error: Cannot find module '_process' at Function.Module._resolveFilename (module.js:325:15) at Function.Module._load (module.js:276:25) at Module.require (module.js:353:17) at require (internal/module.js:12:17) at s (/Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:1:502) at /Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:1:647 at Object.14._process (/Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:1061:14) at s (/Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:1:596) at /Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:1:647 at Object.10.asap (/Users/martijnmellema/Documents/Development/Projecten/groenewijkstroom/wijk-client/node_modules/timesync/dist/timesync.js:651:12)

Tried adding the shim files, but still no result:


require('es6-shim');
require('es5-shim');

using nodejs 4.4.5

Quarter of a second out of sync

Hi,

I'm trying to play algoritmic music in sync on a network of computers, but with the examples of express and socket provided. The system is constantly quarter of a second out of time when I have this code:

socket = io.connect("http://192.168.0.100:3000"); // This connects the sockets to my server

But when I leave it on localhost, then it is in time with the express example, but I have no socket connections then. With the socket example its still about a quarter of a second out of time.

socket = io.connect("http://localhost:3000");

Am I doing something wrong?
I use a combination of Express and Socket for this project.

I hope that anyone can help me with this.
If I need to provide some more info then let me know (this is my first issue).

Geert

Rapid-fire sends regardless of interval.

I used the socket.io example to implement timesync using basic websockets (not a fan of socket.io's overhead and lack of binary buffers). Here is the gist of the implementation:

Server

socket.on('timesync', data  => socket.send('timesync', {
    id: data && data.id || null,
    result: Date.now()
}));

Client

const ts = require('timesync').create({server: socket});
ts.send = (socket, data) => socket.send('timesync', data); // Sends {jsonrpc: "2.0", id: null, result: <timestamp>}
socket.on('timesync', data => ts.receive(null, data)); // Receives {id: null, result: <timestamp>}

The issue is that hundreds of timesync requests are being sent per second, even though each is receiving a correct response. I have tried create({server: socket, interval: 5000}) as in the socket.io example, but am getting the same result. I'm not sure if I'm missing something, or if there's a bug. Any help would be appreciated.

Does not work in react-native

When using this library the frontend will throw an error when using react native. This is because of the way that the request file checks that the typeof window is not undefined

https://github.com/enmasseio/timesync/blob/master/src/request/request.js

The package at "node_modules/timesync/lib/request/request.node.js" attempted to import the Node standard library module "http". 
It failed because the native React runtime does not include the Node standard library. 
Failed building JavaScript bundle.

There is a fork of this project for react native but its outdated and not on npm, so I though that I would bring this up here as it seems like a simple fix.

Here is one stack overflow answer with a fix - This should work as it was added by react native to differentiate between itself and node - discussed here

if (typeof document != 'undefined') {
  // I'm on the web!
}
else if (typeof navigator != 'undefined' || navigator.product == 'ReactNative') {
  // I'm in react-native
}
else {
  // I'm in node js
}

So we could make it something like:

if (typeof document != 'undefined' && navigator.product == 'ReactNative') {
  // I'm in browser or react-native
 isBrowser=true
}
else {
  // I'm in node js
 isBrowser=false
}

Or another way to do it would be to check whether it has the node only default modules like http that could also be another good way to differentiate

timesync + bodyParser.json makes node.js stuck

Using timesync with bodyParser.json makes node.js freeze after few timesync requests.
I init timesync & bodyParser like this:

var bodyParser = require('body-parser')
app.use(bodyParser.json({type: 'application/json'}));
app.use('/timesync', timesync.requestHandler);

Less polyfills

Using timesync brings in Promise.js and emitter.js, which are files which a browser will not need. It would be great to be able to use timesync without bringing in these unnecessary files.

stat.median not using sorted array

Like your implementation however I noticed that median is currently just returning the median element of the unsorted array not the sorted array and is thus giving incorrect results:

The current implementation:

export function median (arr) {
  if (arr.length < 2) return arr[0];

  var sorted = arr.slice().sort(compare);
  if (sorted.length % 2 === 0) {
    // even
    return (arr[arr.length / 2 - 1] + arr[arr.length / 2]) / 2;
  }
  else {
    // odd
    return arr[(arr.length - 1) / 2];
  }

Proposed fix:

export function median (arr) {
  if (arr.length < 2) return arr[0];

  var sorted = arr.slice().sort(compare);
  if (sorted.length % 2 === 0) {
    // even
    return (sorted [sorted .length / 2 - 1] + sorted [sorted .length / 2]) / 2;
  }
  else {
    // odd
    return sorted [(sorted .length - 1) / 2];
  }

Unable to resolve module 'http'

I'm getting "Unable to resolve module 'http'" in my React Native app.
It is not present in node_modules indeed.
Where does it come from ?

Thanks.

filename is not defined before used

code snippet from /server/index.js

exports.requestHandler = function (req, res) {
  debug('request ' + req.method + ' ' + req.url + ' ' + req.method);

  if (req.method == 'POST') {
    if (!filename) {
      // a time request
      return sendTimestamp(req, res);
    }
  }

  if (req.method == 'GET') {
    var match = req.url.match(/\/(timesync(.min)?.js)$/);
    var filename = match && match[1];
    if (filename === 'timesync.js' || filename === 'timesync.min.js') {
      // static file handler
      return sendFile(res, __dirname + '/../dist/' + filename);
    }
  }

  res.writeHead(404);
  res.end('Not found');
};

Add high accuracy sync ?

Hi,
is it possible to add an option to have a synchronization with a high precision, kind real time ?

Somme time I have less than 100ms between my differents clients, and on the next sync I have more than 600ms.

I need to play different part of video on different glued screen. Same thing when I need to play a set of images

Thank in advance

Add CORS support to server

index.js needs to return some extra headers for this to work, here.

Let me know if you're willing to merge a PR for this and I'll write one up.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.