Create a new timesimp instance.
load()
must be an async function that returns the current offset in microseconds, or
null
if no offset is currently stored.
store()
must be an async function that stores a given offset in microseconds. Once
store()
has been called once, load()
should no longer return null
if it did.
query()
must be an async function that sends the request
buffer to a timesimp server,
and returns the bytes that the server sends back. If a transport error occurs, the function
should throw. For example, this can be an HTTP POST using fetch()
.
Due to internal API limitations, all three of these have a first err
argument; this
must be immediately thrown if truthy:
new Timesimp(
async (err) => { // load
if (err) throw err;
return db.query("offset");
},
async (err, offset) => { // store
if (err) throw err;
await db.update("offset", offset);
},
async (err, request) => {
if (err) throw err;
const res = await fetch("https://timesimp.server", {
method: "POST",
body: request,
});
return res.blob();
}
);
The implementation of the server endpoint.
Use this in your server endpoint implementation. The endpoint should do as little else as possible to avoid adding unpredictable latency.
You should obtain some bytes from the request’s payload (in this version, 8 bytes), and this method will return some other bytes (in this version, 16 bytes), which you should send back to the client.
The main client state driver. Call this in a loop.
You’re expected to sleep for a while after calling this, or to run it on a schedule. Take
care to compute your schedule on your raw system clock or equivalent, so it does not get
influenced by the offset, which could make it jump around or even spin. setInterval
or
setTimeout
are appropriate.
If load()
returns null
, this method will attempt to store()
the first delta it gets
from the server. This lets you get an “accurate enough” timestamp pretty quickly, instead
of waiting for a full round of samples. Errors from that store are ignored silently.
If this returns null
, not enough samples were obtained to have enough confidence in the
result, likely because the query()
function encountered an error for most tries. Errors
from query()
are not returned; you may want to catch them for logging before passing them
on.
On success, returns the calculated offset in microseconds.
The current time in microseconds since the epoch, adjusted with the offset.
This is a convenience function that internally calls your load()
. You may want to
implement your own function, especially if you want to get a Date
or Temporal
, or if
you’ve implemented some caching.
Simple sans-io timesync client and server.
Timesimp is based on the averaging method described in Simpson (2002), A Stream-based Time Synchronization Technique For Networked Computer Games, but with a corrected delta calculation. Compared to NTP, it's a simpler and less accurate time synchronisation algorithm that is usable over network streams, rather than datagrams. Simpson asserts they were able to achieve accuracies of 100ms or better, which is sufficient in many cases; my testing gets accuracies well below 5ms. The main limitation of the algorithm is that round-trip-time is assumed to be symmetric: if the forward trip time is different from the return trip time, then an error is induced equal to the value of the difference in trip times.
This library provides a sans-io implementation: you bring in your transport and your storage; timesimp gives you time offsets. Internally, timesimp is implemented in Rust.
If the local clock goes backward during a synchronisation, the invalid delta is discarded; this may cause the sync attempt to fail, especially if the
samples
count is lowered to its minimum of 3. This is a deliberate design decision: you should handle failure and retry, and the sync will proceed correctly when the clock is stable.