Introduction
What if we want the accurate time when the client and server time are not accurate? The only workaround that I could find was json-time API. But json-time
had a big Achilles Heel. That was json-time
is hosted on Google and most of my clients couldn't call the API.
I couldn't find any alternative API and I lost hope, until I saw this comment in James Padolseys' blog:
Why not do a HTTP Head request to google.com to get the current time? I have used it in apps that need to check the correct time and never found different from ntp servers.
So I could find what I need in google.com header:
I liked the idea and here is an effort to access the accurate time in JavaScript from Google server.
Writing the Code Step by Step
Retrieving data by XMLHttpRequest
and access to the response HEADER
seems very easy so I write the first version:
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.google.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
alert(xmlhttp.getResponseHeader("Date"));
}
}
xmlhttp.send(null);
But as a result, I get NULL
instead of date
! I found the problem after viewing browser console log:
MLHttpRequest cannot load http://www.google.com/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://fiddle.jshell.net' is therefore not allowed access.
So I need URL with CORS support. I searched really hard but I couldn't find any good URL after days of searching. I changed my strategy and searched for another solution to capture the date header. Finally, I found the solution. When I want to retrieve data from URL that does not exist, I recieve HTTP/1.1 404 Not Found error but this time I can read date header correctly. So I changed URL from http://www.google.com to http://www.googleapis.com:
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
alert(xmlhttp.getResponseHeader("Date"));
}
}
xmlhttp.send(null);
OK now I have what I want but it's not a good way to send a request to another server any time I need time. We can calculate local time and server time difference and store it in local variable:
var TimeDiff;
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) - (new Date());
alert(TimeDiff);
}
}
xmlhttp.send(null);
Or store it in localstorage
:
var TimeDiffKey = 'Local-Server-TimeDiff';
var TimeDiff;
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) - (new Date());
window.localStorage.setItem(TimeDiffKey, TimeDiff);
}
}
xmlhttp.send(null);
Storing TimeDiff
in localstorage
raises another problem. We need a mechanism to resync the difference in a timely manner:
var SyncTimeframe = 1000 * 60 * 60 * 3;
var LastSyncKey = 'LastSyncWithTimeServer';
var TimeDiffKey = 'Local-Server-TimeDiff';
if (window.localStorage.getItem(LastSyncKey) == null) {
window.localStorage.setItem(LastSyncKey, '' + (new Date(0)));
}
LastSync = new Date(window.localStorage.getItem(LastSyncKey));
if ( Math.abs((new Date()) - LastSync) > SyncTimeframe) {
SyncTime();
}
else {
ShowTime();
}
function SyncTime() {
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) - (new Date());
window.localStorage.setItem(LastSyncKey, '' + (new Date()));
window.localStorage.setItem(TimeDiffKey, TimeDiff);
ShowTime();
}
}
xmlhttp.send(null);
}
function ShowTime(){
alert(window.localStorage.getItem(TimeDiffKey));
}
Network delay is the other concern, and to take care of it, I add AcceptedDelay
and RetryCount
to code:
var SyncTimeframe = 1000 * 60 * 60 * 3;
var LastSyncKey = 'LastSyncWithTimeServer';
var TimeDiffKey = 'Local-Server-TimeDiff';
var RetryMax = 3;
var RetryCount = 0;
var AcceptedDelay = 500;
if (window.localStorage.getItem(LastSyncKey) == null) {
window.localStorage.setItem(LastSyncKey, '' + (new Date(0)));
}
LastSync = new Date(window.localStorage.getItem(LastSyncKey));
if ( Math.abs((new Date()) - LastSync) > SyncTimeframe) {
SyncTime();
}
else {
ShowTime();
}
function SyncTime() {
var StartTime = new Date();
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) - (new Date());
if (++RetryCount < 3 && (new Date()) - StartTime > AcceptedDelay) {
SyncTime();
}
else {
window.localStorage.setItem(LastSyncKey, '' + (new Date()));
window.localStorage.setItem(TimeDiffKey, TimeDiff);
ShowTime();
}
}
}
xmlhttp.send(null);
}
function ShowTime(){
alert(window.localStorage.getItem(TimeDiffKey));
}
Also, we can add half of network delay to time diff to handle network delay better:
var SyncTimeframe = 1000 * 60 * 60 * 3;
var LastSyncKey = 'LastSyncWithTimeServer';
var TimeDiffKey = 'Local-Server-TimeDiff';
var RetryMax = 3;
var RetryCount = 0;
var AcceptedDelay = 500;
if (window.localStorage.getItem(LastSyncKey) == null) {
window.localStorage.setItem(LastSyncKey, '' + (new Date(0)));
}
LastSync = new Date(window.localStorage.getItem(LastSyncKey));
if ( Math.abs((new Date()) - LastSync) > SyncTimeframe) {
SyncTime();
}
else {
ShowTime();
}
function SyncTime() {
var StartTime = new Date();
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) -
(new Date()) + ((new Date()) - StartTime) / 2;
if (++RetryCount < 3 && (new Date()) - StartTime > AcceptedDelay) {
SyncTime();
}
else {
window.localStorage.setItem(LastSyncKey, '' + (new Date()));
window.localStorage.setItem(TimeDiffKey, TimeDiff);
ShowTime();
}
}
}
xmlhttp.send(null);
}
function ShowTime(){
alert(window.localStorage.getItem(TimeDiffKey));
}
Using the Code (Show the Date and Time)
I want to show the Time
in any DOM element with "RealServerTime
" class, so I use document.getElementsByClassName
to find all nodes:
var SyncTimeframe = 1000 * 60 * 60 * 3;
var LastSyncKey = 'LastSyncWithTimeServer';
var TimeDiffKey = 'Local-Server-TimeDiff';
var RetryMax = 3;
var RetryCount = 0;
var AcceptedDelay = 500;
if (window.localStorage.getItem(LastSyncKey) == null) {
window.localStorage.setItem(LastSyncKey, '' + (new Date(0)));
}
LastSync = new Date(window.localStorage.getItem(LastSyncKey));
if ( Math.abs((new Date()) - LastSync) > SyncTimeframe) {
SyncTime();
}
else {
ShowTime();
}
function SyncTime() {
var StartTime = new Date();
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) -
(new Date()) + ((new Date()) - StartTime) / 2;
if (++RetryCount < 3 && (new Date()) - StartTime > AcceptedDelay) {
SyncTime();
}
else {
window.localStorage.setItem(LastSyncKey, '' + (new Date()));
window.localStorage.setItem(TimeDiffKey, TimeDiff);
ShowTime();
}
}
}
xmlhttp.send(null);
}
function ShowTime(){
var AllNodes=document.getElementsByClassName("RealServerTime");
var diff = parseInt(window.localStorage.getItem(TimeDiffKey), 10);
var TimeToString=(new Date(Date.now() + diff)).toTimeString().split(' ')[0];
for(var ipos=0;ipos<AllNodes.length;ipos++){
AllNodes[ipos].innerHTML=TimeToString;
}
window.setTimeout(ShowTime, 1000);
}
The last step is to wrap all code in anonymous function to hide all local variable and functiuons:
(function () {
var SyncTimeframe = 1000 * 60 * 60 * 3;
var LastSyncKey = 'LastSyncWithTimeServer';
var TimeDiffKey = 'Local-Server-TimeDiff';
var RetryMax = 3;
var RetryCount = 0;
var AcceptedDelay = 500;
if (window.localStorage.getItem(LastSyncKey) == null) {
window.localStorage.setItem(LastSyncKey, '' + (new Date(0)));
}
LastSync = new Date(window.localStorage.getItem(LastSyncKey));
if ( Math.abs((new Date()) - LastSync) > SyncTimeframe) {
SyncTime();
}
else {
ShowTime();
}
function SyncTime() {
var StartTime = new Date();
xmlhttp = new XMLHttpRequest();
xmlhttp.open("HEAD", "http://www.googleapis.com",true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
TimeDiff=new Date(xmlhttp.getResponseHeader("Date")) -
(new Date()) + ((new Date()) - StartTime) / 2;
if (++RetryCount < 3 && (new Date()) - StartTime > AcceptedDelay) {
SyncTime();
}
else {
window.localStorage.setItem(LastSyncKey, '' + (new Date()));
window.localStorage.setItem(TimeDiffKey, TimeDiff);
ShowTime();
}
}
}
xmlhttp.send(null);
}
function ShowTime(){
var AllNodes=document.getElementsByClassName("RealServerTime");
var diff = parseInt(window.localStorage.getItem(TimeDiffKey), 10);
var TimeToString=(new Date(Date.now() + diff)).toTimeString().split(' ')[0];
for(var ipos=0;ipos<AllNodes.length;ipos++){
AllNodes[ipos].innerHTML=TimeToString;
}
window.setTimeout(ShowTime, 1000);
}
})();
Points of Interest
This is the first time I enjoyed seeing HTTP/1.1 404 Not Found error.
If anyone knows a better solution to achieve a more accurate time, I will be happy to hear about it.