I've been trying to debug why torn timers disagree across tabs and within Torn War Stuff Enhanced and I've uncovered some improvements that could be made.
Overview
Time synchronization between two points is an impossible problem in physics, in computer science we have approximations we can use but it will never be perfect.
However, the Torn frontend's time synchronization code has a number of common errors in it that can cause timers to be out of sync by a second or more as well as counting faster or slower than a second.
TL;DR
Don't count ticks, calculate the server offset by sampling over time and apply it to `new Date().getTime()` in a function called `this.timeNow()`. Call that function from `window.getCurrentTimestamp()`.
Current implementation
```javascript
function ServerTimeService() {
var self = this;
var serverSyncFrequency = 120000;
this.timeNow = Date.now();
window.addEventListener("focus", function () {
self.syncTimeWithServer();
});
this.startClock = function () {
if (window.Worker) {
var worker = new Worker("/js/script/lib/custom/timers_web_worker.js");
worker.addEventListener("message", function (e) {
if (e.data === "tick") {
self.timeNow += 1000;
}
});
worker.postMessage("run the timer");
} else {
setInterval(function () {
self.timeNow += 1000;
}, 1000);
}
};
this.syncTimeWithServer = function () {
if (typeof getAction === "function" && getCookie("isLoggedIn") === "1") {
var start = Date.now();
var action = "/sidebarAjaxAction.php?action=servertime&t=" + start;
getAction({
type: "get",
action,
success: function (resp) {
try {
var data = JSON.parse(resp);
var latency = (Date.now() - start) / 2;
self.timeNow = data && data.time + latency;
} catch (e) {
console.error("Some error happen during JSON parsing: ", resp);
}
},
});
}
};
self.syncTimeWithServer();
setInterval(function () {
if (document.hidden === false) {
self.syncTimeWithServer();
}
}, serverSyncFrequency);
}
var serverTimeService = new ServerTimeService();
serverTimeService.startClock();
function getCurrentTimestamp() {
return serverTimeService.timeNow;
}```
Don't look inside `/js/script/lib/custom/timers_web_worker.js` unless you want to be saddened by another setInterval.
Problems
- The actual offset value is thrown away inside syncTimeWithServer. This is the MOST IMPORTANT part of time synchronization.
- Offset is only calculated with a single value instead of being sampled over time. This means that one delayed response can cause the clock to be out of sync.
- Calculating time by counting seconds will always cause drift. NEVER count time with an interval. Not only will it force you to have more synchronization points (like every focus, for example), it will also mean the clock doesn't support millisecond precision.
- Clocks aren't shared across tabs. This means that every tab is guaranteed to have a different time, varying by as much 2 seconds or more.
Solutions (sorted by easiest to hardest)
Calculate time based on browser time + offset
Instead of making your own javascript-based ticker, store the offset as an instance variable (`this.offset`). Create a function called `timeNow` that calculates current time plus offset.
```javascript
this.timeNow = function () {
return new Date().getTime() + this.offset;
};
```
Call `timeNow()` in `getCurrentTimestamp()`. Now your clock will be synchronized with millisecond precision and you save yourself a useless interval timer.
Use NTP-style offset calculations
There are actually two variables you're trying to account for when synchronizing a clock `offset` and `round trip delay`. To make it possible to calculate each of those independently, `/sidebarAjaxAction.php?action=servertime&t=...` needs to return both its packet acceptance time `t1` and its packet response time `t2`. Once you have those two values you can calculate `θ` (theta) and `δ` (delta). Even just storing theta would be an improvement over what you have now.
An additional improvement would be to store the last X theta and delta values, throw away outliers, and take the average theta.
Put ServerTimeService in a SharedWorker so all tabs can share the same offset
Really, the only thing your `ServerTimeService` exists to do is calculate how the offset changes over time. The rest is just math against `Date.now()`. Why not do the synchronization and calculation in a SharedWorker so that all tabs can benefit from the same calculation and get the same time? This would make hospital timers on mini-profiles and profile pages in different tabs agree.
Bonus points if you store the offset data for some amount of time so that browser restarts can benefit from the synchronization work you already did. Don't store it for too long because clocks drift though!
Only ever compare server time to getCurrentTimestamp
I just found another example in the same file `checkClientAndServerTime(selector)`. It reads server time from a data attribute called `stime` but then it compares that time to `$.now()` (browser time). This is a no-no and will always give an incorrect result. At the very least it should compare to `getCurrentTimestamp()`. Luckily I couldn't find any `.countdown` elements on the page, so maybe this is just dead code that can be deleted. I'm sure I'd find more examples in your code if I looked harder.