├── .gitignore ├── Gruntfile.js ├── README.md ├── bower.json ├── dist ├── timekeeper.js └── timekeeper.min.js ├── index.js ├── package.json ├── public ├── dist │ ├── timekeeper.js │ └── timekeeper.min.js ├── index.html └── js │ └── action.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | function getPluginBanner () { 3 | var pluginBanner = ''; 4 | pluginBanner += '/**\n'; 5 | pluginBanner += ' * Plugin: TimeKeeper\n'; 6 | pluginBanner += ' * Author: Sundarasan Natarajan\n'; 7 | pluginBanner += ' * GIT: https://github.com/Sujsun/timekeeper.git\n'; 8 | pluginBanner += ' * Version: 0.0.2\n'; 9 | pluginBanner += ' */\n'; 10 | return pluginBanner; 11 | } 12 | 13 | var pluginBanner = getPluginBanner(); 14 | 15 | module.exports = function(grunt) { 16 | grunt.initConfig({ 17 | 18 | copy: { 19 | 20 | dist: { 21 | files: { 'dist/timekeeper.js': './index.js' } 22 | }, 23 | 24 | public_dist: { 25 | files: [ 26 | { expand: true, src: ['dist/*'], dest: 'public/', filter: 'isFile' }, 27 | ], 28 | }, 29 | 30 | }, 31 | 32 | uglify: { 33 | dist: { 34 | options: { 35 | banner: pluginBanner 36 | }, 37 | files: { 38 | 'dist/timekeeper.min.js': ['dist/timekeeper.js'] 39 | } 40 | } 41 | }, 42 | 43 | watch: { 44 | dist: { 45 | files: ['dist/timekeeper.js', './index.js'], 46 | tasks: ['build'] 47 | }, 48 | }, 49 | 50 | }); 51 | 52 | grunt.loadNpmTasks('grunt-contrib-copy'); 53 | grunt.loadNpmTasks('grunt-contrib-uglify'); 54 | grunt.loadNpmTasks('grunt-contrib-watch'); 55 | 56 | grunt.registerTask('start', 'My start task description', function() { 57 | grunt.util.spawn({ 58 | cmd: 'npm', 59 | args: ['start'] 60 | }); 61 | console.log('Server running at http://127.0.0.1:8989 (http://localhost:8989)'); 62 | grunt.task.run('watch'); 63 | }); 64 | 65 | grunt.registerTask('build', [ 66 | 'copy:dist', 67 | 'uglify', 68 | 'copy:public_dist', 69 | ]); 70 | 71 | grunt.registerTask('default', [ 72 | 'build', 73 | 'start', 74 | ]); 75 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeKeeper 2 | Plain javascript plugin to sync client time with server time 3 | 4 | ### Example: 5 | **Basic usage:** 6 | ```javascript 7 | var timekeeper = TimeKeeper({ 8 | ajaxType: 'get', 9 | ajaxMilliUrl: '/utcMillis', 10 | responseParser: function (response) { 11 | return parseInt(response); 12 | }, 13 | }); 14 | 15 | timekeeper.sync(function () { 16 | console.log('Correct date:', timekeeper.Date()); 17 | }); 18 | ``` 19 | **NOTE:** `responseParser` must return date in milliseconds 20 | 21 | **Sync at regular interval:** 22 | ```javascript 23 | var timekeeper = TimeKeeper(); 24 | 25 | timekeeper.on('synced', function () { 26 | console.log('Synced successfully'); 27 | }); 28 | 29 | timekeeper.startSync(5 * 60 * 1000); // Will sync regularly at 5 min interval 30 | ``` 31 | 32 | **Or if you already know the current correct time:** 33 | ```javascript 34 | var timekeeper = TimeKeeper({ correctTimeInMillis: 1467451864591 }); 35 | console.log('Correct time:', timekeeper.Date()); 36 | 37 | /** 38 | * Or you can use "setCorrectTimeInMillis" method 39 | */ 40 | var timekeeper = TimeKeeper(); 41 | timekeeper.setCorrectTimeInMillis(1467451864591); 42 | console.log('Correct time:', timekeeper.Date()); 43 | ``` 44 | 45 | **You can also override native `Date` with correct `Date`:** 46 | ```javascript 47 | var timekeeper = TimeKeeper({ overrideDate: true, correctTimeInMillis: 1467451864591 }); 48 | console.log('Correct time:', new Date()); 49 | ``` 50 | 51 | ## Options 52 | - `correctTimeInMillis` - Correct time (server time) 53 | - `ajaxType` - HTTP Method type [`get`/`post`/`put`] 54 | - `ajaxMilliUrl` - URL to hit to fetch time in UTC milliseconds (Default value: "/utcMillis") 55 | - `syncInterval` - Interval at which sycn should happen 56 | - `responseParser` - Parser method for response 57 | - `differenceInMillis` - Incase you know difference of machine time and server time in milliseconds you can pass 58 | 59 | ### Methods 60 | - `sync` - Fetches server time 61 | - `Date` - Gets Date object with server time (correct time) 62 | - `startSync()` - Starts to run sync operation at given regualar interval 63 | - `stopSync` - Stops sync operation loop 64 | - `setCorrectTimeInMillis(())` - Sets correct time (server time) 65 | - `overrideDate` - Overrides default Date object with server time Date object 66 | - `releaseDate` - Undos `overrideDate` operation 67 | - `setDifferenceInMillis()` - Sets the server and client time difference 68 | - `getDifferenceInMillis` - Gets the server and client time difference 69 | - `on(, )` - Attaches events 70 | - `off` - Removes events 71 | 72 | ### Events 73 | - Supported events 74 | - `sync` - Will be triggered before syncing (prehook for sync) 75 | - `synced` - Will be triggered when time sync is successful 76 | - `sync_error` - Will be triggered when time sync fails 77 | - `first_sync` - Will be triggered before syncing **for the first time** 78 | - `first_synced` - Will be triggered when time sync **for first time** is successful 79 | - `first_sync_error` - Will be triggered when time sync **for first time** fails 80 | 81 | ### Motivation 82 | Have you seen chat application showing _"1 day ago"_ for a message which you received **just now**.
83 | This is probably because the **client machine time is set wrongly**.
84 | To avoid this chaos the **client time should be in sync with server time**.
85 | 86 | ### To see demo 87 | - Clone the project 88 | ``` 89 | git clone https://github.com/Sujsun/timekeeper.git 90 | ``` 91 | 92 | - Install npm dependencies 93 | ``` 94 | npm install 95 | ``` 96 | 97 | - Run the server 98 | `npm start` or `grunt` 99 | 100 | - Visit 101 | `http://localhost:8989` or ` http://127.0.0.1:8989` 102 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timekeeper", 3 | "description": "Sync client time with server time. Get correct time even if client has set wrong time", 4 | "main": "index.js", 5 | "authors": [ 6 | "Sundarasan Natarajan" 7 | ], 8 | "license": "ISC", 9 | "keywords": [ 10 | "sync", 11 | "server", 12 | "time" 13 | ], 14 | "homepage": "https://github.com/Sujsun/timekeeper", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /dist/timekeeper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin: TimeKeeper 3 | * Author: Sundarasan Natarajan 4 | * GIT: https://github.com/Sujsun/timekeeper.git 5 | * Version: 0.0.2 6 | */ 7 | (function (root, factory) { 8 | 9 | if (typeof exports === 'object') { 10 | module.exports = factory(); 11 | } else if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define([], factory); 14 | } else { 15 | // Browser globals 16 | root.TimeKeeper = factory(); 17 | } 18 | 19 | })(window, function () { 20 | 21 | /** 22 | * ------------ 23 | * AJAX Helpers 24 | * ------------ 25 | */ 26 | var AJAX = { 27 | 28 | /** 29 | * AJAX Helper 30 | * @param {Object} params { type: 'get', dataType: 'json', data: { .. }, headers: { ... } success: Function, error: Function } 31 | * @return {XHRObject} 32 | */ 33 | call: function(params) { 34 | var result, 35 | headerName, 36 | headerValue; 37 | 38 | typeof(params) === 'object' || (params = {}); 39 | typeof(params.type) === 'string' || (params.type = 'get'); 40 | typeof(params.type) === 'string' && (params.type = params.type.toUpperCase()); 41 | typeof(params.success) === 'function' || (params.success = EMPTY_FUNCTION); 42 | typeof(params.error) === 'function' || (params.error = EMPTY_FUNCTION); 43 | 44 | var xhttp = new XMLHttpRequest(); 45 | 46 | for (headerName in params.headers) { 47 | headerValue = params.headers[headerName]; 48 | xhttp.setRequestHeader(headerName, headerValue); 49 | } 50 | 51 | xhttp.onreadystatechange = function() { 52 | 53 | if (xhttp.readyState == 4) { 54 | if ((xhttp.status >= 200 && xhttp.status < 300) || xhttp.status == 304) { 55 | result = xhttp.responseText; 56 | if (params.dataType === 'json') { 57 | try { 58 | result = JSON.parse(xhttp.responseText); 59 | } catch (error) { 60 | params.error.call(params.context, error, xhttp, params); 61 | } 62 | } 63 | params.success.call(params.context, result, xhttp, params); 64 | } else { 65 | params.error.call(params.context, new Error('Ajax failed'), xhttp, params); 66 | } 67 | } 68 | 69 | }; 70 | 71 | xhttp.open(params.type, params.url, true); 72 | xhttp.send(params.data); 73 | return xhttp; 74 | }, 75 | 76 | }; 77 | 78 | /** 79 | * ------------ 80 | * Object Utils 81 | * ------------ 82 | */ 83 | var ObjectUtils = { 84 | 85 | clone: function (obj) { 86 | var newObj = {}, key; 87 | for (key in obj) { 88 | newObj[key] = obj[key]; 89 | } 90 | return newObj; 91 | }, 92 | 93 | defaults: function (object, defaults) { 94 | var key; 95 | for (key in defaults) { 96 | typeof(object[key]) === 'undefined' && (object[key] = defaults[key]); 97 | } 98 | return object; 99 | }, 100 | 101 | unbind: Function.bind.bind(Function.bind), 102 | 103 | instantiate: function (constructor, args) { 104 | return new (this.unbind(constructor, null).apply(null, args)); 105 | }, 106 | 107 | }; 108 | 109 | /** 110 | * --------- 111 | * Minivents 112 | * --------- 113 | */ 114 | function Events(target) { 115 | var events = {}, 116 | empty = []; 117 | target = target || this 118 | /** 119 | * On: listen to events 120 | */ 121 | target.on = function(type, func, ctx) { 122 | (events[type] = events[type] || []).push([func, ctx]) 123 | } 124 | /** 125 | * Off: stop listening to event / specific callback 126 | */ 127 | target.off = function(type, func) { 128 | type || (events = {}) 129 | var list = events[type] || empty, 130 | i = list.length = func ? list.length : 0; 131 | while (i--) func == list[i][0] && list.splice(i, 1) 132 | } 133 | /** 134 | * Emit: send event, callbacks will be triggered 135 | */ 136 | target.emit = function(type) { 137 | var e = events[type] || empty, 138 | list = e.length > 0 ? e.slice(0, e.length) : e, 139 | i = 0, 140 | j; 141 | while (j = list[i++]) j[0].apply(j[1], empty.slice.call(arguments, 1)) 142 | }; 143 | }; 144 | 145 | /** 146 | * ---------- 147 | * TimeKeeper 148 | * ---------- 149 | */ 150 | var OPTIONS_DEFAULTS = { 151 | ajaxType: 'get', 152 | ajaxMilliUrl: '/utcMillis', 153 | syncInterval: 1 * 60 * 1000, // 1 minute 154 | differenceInMillis: 0, 155 | overrideDate: false, 156 | responseParser: function (response) { 157 | return parseInt(response); 158 | }, 159 | }; 160 | 161 | var EVENTS = { 162 | SYNC: 'sync', 163 | SYNCED: 'synced', 164 | SYNC_ERROR: 'sync_error', 165 | 166 | FIRST_SYNC: 'first_sync', 167 | FIRST_SYNCED: 'first_synced', 168 | FIRST_SYNC_ERROR: 'first_sync_error', 169 | }; 170 | 171 | /** 172 | * TimeKeeper Constructor 173 | */ 174 | function TimeKeeper (options) { 175 | this._options = ObjectUtils.clone(options || {}); 176 | this._options = ObjectUtils.defaults(this._options, OPTIONS_DEFAULTS); 177 | this._differenceInMillis = this._options.differenceInMillis; 178 | this._isSyncedOnce = false; 179 | this._event = new Events(); 180 | this.setCorrectTimeInMillis(this._options.correctTimeInMillis); 181 | this._options.overrideDate && (this.overrideDate()); 182 | return this; 183 | } 184 | 185 | /** 186 | * TimeKeeper Public Methods 187 | */ 188 | TimeKeeper.prototype.sync = function () { 189 | return this._sync.apply(this, arguments); 190 | }; 191 | 192 | TimeKeeper.prototype.Date = function () { 193 | return this._getCorrectDate(); 194 | }; 195 | 196 | TimeKeeper.prototype.overrideDate = function () { 197 | var self = this; 198 | window.Date = function (OriginalDate) { 199 | function CorrectedDate() { 200 | var date = ObjectUtils.instantiate(OriginalDate, arguments), 201 | argumentsLength = arguments.length; 202 | if (argumentsLength === 0) { 203 | date.setTime(self._getCorrectDateMillis()); 204 | } 205 | return date; 206 | } 207 | CorrectedDate.prototype = OriginalDate.prototype; 208 | return CorrectedDate; 209 | }(this._Date); 210 | if (!window.Date.now) { 211 | window.Date.now = function now () { 212 | return new window.Date().getTime(); 213 | }; 214 | } 215 | if (!window.Date.parse) { 216 | window.Date.parse = this._Date.parse; 217 | } 218 | if (!window.Date.UTC) { 219 | window.Date.UTC = this._Date.UTC; 220 | } 221 | }; 222 | 223 | TimeKeeper.prototype.releaseDate = function () { 224 | return (window.Date = this._Date); 225 | }; 226 | 227 | TimeKeeper.prototype.setCorrectTimeInMillis = function (correctTimeInMillis) { 228 | return typeof(correctTimeInMillis) === 'number' && this._findDifferenceInMillis(correctTimeInMillis); 229 | }; 230 | 231 | TimeKeeper.prototype.setDifferenceInMillis = function (differenceInMillis) { 232 | return (this._differenceInMillis = differenceInMillis); 233 | }; 234 | 235 | TimeKeeper.prototype.getDifferenceInMillis = function () { 236 | return this._differenceInMillis; 237 | }; 238 | 239 | TimeKeeper.prototype.startSync = function (syncInterval) { 240 | return this._startSync.apply(this, arguments); 241 | }; 242 | 243 | TimeKeeper.prototype.stopSync = function () { 244 | return this._stopSync.apply(this, arguments); 245 | }; 246 | 247 | TimeKeeper.prototype.on = function () { 248 | this._event.on.apply(this._event, arguments); 249 | }; 250 | 251 | TimeKeeper.prototype.off = function () { 252 | this._event.off.apply(this._event, arguments); 253 | }; 254 | 255 | /** 256 | * TimeKeeper Private Members 257 | */ 258 | 259 | /** 260 | * Taking a backup of original Date constructor 261 | */ 262 | TimeKeeper.prototype._Date = window.Date; 263 | 264 | TimeKeeper.prototype._startSync = function (syncInterval) { 265 | var self = this; 266 | typeof(syncInterval) === 'number' && (this._options.syncInterval = syncInterval); 267 | this.stopSync(); 268 | this.sync(); 269 | return this._syncIntervalIndex = window.setInterval(function () { 270 | self.sync(); 271 | }, this._options.syncInterval); 272 | }; 273 | 274 | TimeKeeper.prototype._stopSync = function () { 275 | window.clearInterval(this._syncIntervalIndex); 276 | delete this._syncIntervalIndex; 277 | }; 278 | 279 | TimeKeeper.prototype._sync = function (callback) { 280 | callback || (callback = function() {}); 281 | var self = this, 282 | correctDate = self.Date(); 283 | this._emitPreSyncEvent(); 284 | this._getServerDateMillis(function (err, serverTimeInMillis) { 285 | if (err) { 286 | console.error('Failed to fetch server time. Error:', err); 287 | callback(err); 288 | self._emitSyncEvent(err); 289 | } else { 290 | self._findDifferenceInMillis(serverTimeInMillis); 291 | correctDate = self.Date(); 292 | callback(null, correctDate); 293 | self._emitSyncedEvent(null, correctDate); 294 | } 295 | }); 296 | }; 297 | 298 | TimeKeeper.prototype._emitPreFirstSyncEvent = function () { 299 | if (this._isSyncedOnce === false) { 300 | this._event.emit(EVENTS.FIRST_SYNC); 301 | } 302 | }; 303 | 304 | TimeKeeper.prototype._emitPreSyncEvent = function () { 305 | this._emitPreFirstSyncEvent(); 306 | this._event.emit(EVENTS.SYNC); 307 | }; 308 | 309 | TimeKeeper.prototype._emitFirstSyncedEvent = function (err, data) { 310 | if (this._isSyncedOnce === false) { 311 | this._isSyncedOnce = true; 312 | this._event.emit(err ? EVENTS.FIRST_SYNC_ERROR : EVENTS.FIRST_SYNCED, err || data); 313 | } 314 | }; 315 | 316 | TimeKeeper.prototype._emitSyncedEvent = function (err, data) { 317 | this._emitFirstSyncedEvent(err, data); 318 | this._event.emit(err ? EVENTS.SYNC_ERROR : EVENTS.SYNCED, err || data); 319 | }; 320 | 321 | TimeKeeper.prototype._getCorrectDate = function () { 322 | return new this._Date(this._getCorrectDateMillis()); 323 | }; 324 | 325 | TimeKeeper.prototype._getCorrectDateMillis = function () { 326 | return this._getMachineDateMillis() + this._differenceInMillis; 327 | }; 328 | 329 | TimeKeeper.prototype._getMachineDateMillis = function () { 330 | return new this._Date().getTime(); 331 | }; 332 | 333 | TimeKeeper.prototype._getServerDateMillis = function (callback) { 334 | var self = this, 335 | startTime = new this._Date().getTime(); 336 | AJAX.call({ 337 | type: this._options.ajaxType, 338 | url: this._options.ajaxMilliUrl, 339 | success: function (data) { 340 | var timeForResponse = new self._Date().getTime() - startTime, 341 | serverTime = self._options.responseParser(data) - (timeForResponse / 2); // Adjusting the server time, since request takes some time 342 | callback(null, serverTime); 343 | }, 344 | error: function (err) { 345 | callback(err); 346 | }, 347 | }); 348 | }; 349 | 350 | TimeKeeper.prototype._findDifferenceInMillis = function (serverDateInMillis) { 351 | this._differenceInMillis = serverDateInMillis - this._getMachineDateMillis(); 352 | }; 353 | 354 | function createInstance (options) { 355 | return new TimeKeeper(options); 356 | } 357 | 358 | return createInstance; 359 | 360 | }); -------------------------------------------------------------------------------- /dist/timekeeper.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin: TimeKeeper 3 | * Author: Sundarasan Natarajan 4 | * GIT: https://github.com/Sujsun/timekeeper.git 5 | * Version: 0.0.2 6 | */ 7 | !function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define([],b):a.TimeKeeper=b()}(window,function(){function a(a){var b={},c=[];a=a||this,a.on=function(a,c,d){(b[a]=b[a]||[]).push([c,d])},a.off=function(a,d){a||(b={});for(var e=b[a]||c,f=e.length=d?e.length:0;f--;)d==e[f][0]&&e.splice(f,1)},a.emit=function(a){for(var d,e=b[a]||c,f=e.length>0?e.slice(0,e.length):e,g=0;d=f[g++];)d[0].apply(d[1],c.slice.call(arguments,1))}}function b(b){return this._options=e.clone(b||{}),this._options=e.defaults(this._options,f),this._differenceInMillis=this._options.differenceInMillis,this._isSyncedOnce=!1,this._event=new a,this.setCorrectTimeInMillis(this._options.correctTimeInMillis),this._options.overrideDate&&this.overrideDate(),this}function c(a){return new b(a)}var d={call:function(a){var b,c,d;"object"==typeof a||(a={}),"string"==typeof a.type||(a.type="get"),"string"==typeof a.type&&(a.type=a.type.toUpperCase()),"function"==typeof a.success||(a.success=EMPTY_FUNCTION),"function"==typeof a.error||(a.error=EMPTY_FUNCTION);var e=new XMLHttpRequest;for(c in a.headers)d=a.headers[c],e.setRequestHeader(c,d);return e.onreadystatechange=function(){if(4==e.readyState)if(e.status>=200&&e.status<300||304==e.status){if(b=e.responseText,"json"===a.dataType)try{b=JSON.parse(e.responseText)}catch(c){a.error.call(a.context,c,e,a)}a.success.call(a.context,b,e,a)}else a.error.call(a.context,new Error("Ajax failed"),e,a)},e.open(a.type,a.url,!0),e.send(a.data),e}},e={clone:function(a){var b,c={};for(b in a)c[b]=a[b];return c},defaults:function(a,b){var c;for(c in b)"undefined"==typeof a[c]&&(a[c]=b[c]);return a},unbind:Function.bind.bind(Function.bind),instantiate:function(a,b){return new(this.unbind(a,null).apply(null,b))}},f={ajaxType:"get",ajaxMilliUrl:"/utcMillis",syncInterval:6e4,differenceInMillis:0,overrideDate:!1,responseParser:function(a){return parseInt(a)}},g={SYNC:"sync",SYNCED:"synced",SYNC_ERROR:"sync_error",FIRST_SYNC:"first_sync",FIRST_SYNCED:"first_synced",FIRST_SYNC_ERROR:"first_sync_error"};return b.prototype.sync=function(){return this._sync.apply(this,arguments)},b.prototype.Date=function(){return this._getCorrectDate()},b.prototype.overrideDate=function(){var a=this;window.Date=function(b){function c(){var c=e.instantiate(b,arguments),d=arguments.length;return 0===d&&c.setTime(a._getCorrectDateMillis()),c}return c.prototype=b.prototype,c}(this._Date),window.Date.now||(window.Date.now=function(){return(new window.Date).getTime()}),window.Date.parse||(window.Date.parse=this._Date.parse),window.Date.UTC||(window.Date.UTC=this._Date.UTC)},b.prototype.releaseDate=function(){return window.Date=this._Date},b.prototype.setCorrectTimeInMillis=function(a){return"number"==typeof a&&this._findDifferenceInMillis(a)},b.prototype.setDifferenceInMillis=function(a){return this._differenceInMillis=a},b.prototype.getDifferenceInMillis=function(){return this._differenceInMillis},b.prototype.startSync=function(a){return this._startSync.apply(this,arguments)},b.prototype.stopSync=function(){return this._stopSync.apply(this,arguments)},b.prototype.on=function(){this._event.on.apply(this._event,arguments)},b.prototype.off=function(){this._event.off.apply(this._event,arguments)},b.prototype._Date=window.Date,b.prototype._startSync=function(a){var b=this;return"number"==typeof a&&(this._options.syncInterval=a),this.stopSync(),this.sync(),this._syncIntervalIndex=window.setInterval(function(){b.sync()},this._options.syncInterval)},b.prototype._stopSync=function(){window.clearInterval(this._syncIntervalIndex),delete this._syncIntervalIndex},b.prototype._sync=function(a){a||(a=function(){});var b=this,c=b.Date();this._emitPreSyncEvent(),this._getServerDateMillis(function(d,e){d?(console.error("Failed to fetch server time. Error:",d),a(d),b._emitSyncEvent(d)):(b._findDifferenceInMillis(e),c=b.Date(),a(null,c),b._emitSyncedEvent(null,c))})},b.prototype._emitPreFirstSyncEvent=function(){this._isSyncedOnce===!1&&this._event.emit(g.FIRST_SYNC)},b.prototype._emitPreSyncEvent=function(){this._emitPreFirstSyncEvent(),this._event.emit(g.SYNC)},b.prototype._emitFirstSyncedEvent=function(a,b){this._isSyncedOnce===!1&&(this._isSyncedOnce=!0,this._event.emit(a?g.FIRST_SYNC_ERROR:g.FIRST_SYNCED,a||b))},b.prototype._emitSyncedEvent=function(a,b){this._emitFirstSyncedEvent(a,b),this._event.emit(a?g.SYNC_ERROR:g.SYNCED,a||b)},b.prototype._getCorrectDate=function(){return new this._Date(this._getCorrectDateMillis())},b.prototype._getCorrectDateMillis=function(){return this._getMachineDateMillis()+this._differenceInMillis},b.prototype._getMachineDateMillis=function(){return(new this._Date).getTime()},b.prototype._getServerDateMillis=function(a){var b=this,c=(new this._Date).getTime();d.call({type:this._options.ajaxType,url:this._options.ajaxMilliUrl,success:function(d){var e=(new b._Date).getTime()-c,f=b._options.responseParser(d)-e/2;a(null,f)},error:function(b){a(b)}})},b.prototype._findDifferenceInMillis=function(a){this._differenceInMillis=a-this._getMachineDateMillis()},c}); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin: TimeKeeper 3 | * Author: Sundarasan Natarajan 4 | * GIT: https://github.com/Sujsun/timekeeper.git 5 | * Version: 0.0.2 6 | */ 7 | (function (root, factory) { 8 | 9 | if (typeof exports === 'object') { 10 | module.exports = factory(); 11 | } else if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define([], factory); 14 | } else { 15 | // Browser globals 16 | root.TimeKeeper = factory(); 17 | } 18 | 19 | })(window, function () { 20 | 21 | /** 22 | * ------------ 23 | * AJAX Helpers 24 | * ------------ 25 | */ 26 | var AJAX = { 27 | 28 | /** 29 | * AJAX Helper 30 | * @param {Object} params { type: 'get', dataType: 'json', data: { .. }, headers: { ... } success: Function, error: Function } 31 | * @return {XHRObject} 32 | */ 33 | call: function(params) { 34 | var result, 35 | headerName, 36 | headerValue; 37 | 38 | typeof(params) === 'object' || (params = {}); 39 | typeof(params.type) === 'string' || (params.type = 'get'); 40 | typeof(params.type) === 'string' && (params.type = params.type.toUpperCase()); 41 | typeof(params.success) === 'function' || (params.success = EMPTY_FUNCTION); 42 | typeof(params.error) === 'function' || (params.error = EMPTY_FUNCTION); 43 | 44 | var xhttp = new XMLHttpRequest(); 45 | 46 | for (headerName in params.headers) { 47 | headerValue = params.headers[headerName]; 48 | xhttp.setRequestHeader(headerName, headerValue); 49 | } 50 | 51 | xhttp.onreadystatechange = function() { 52 | 53 | if (xhttp.readyState == 4) { 54 | if ((xhttp.status >= 200 && xhttp.status < 300) || xhttp.status == 304) { 55 | result = xhttp.responseText; 56 | if (params.dataType === 'json') { 57 | try { 58 | result = JSON.parse(xhttp.responseText); 59 | } catch (error) { 60 | params.error.call(params.context, error, xhttp, params); 61 | } 62 | } 63 | params.success.call(params.context, result, xhttp, params); 64 | } else { 65 | params.error.call(params.context, new Error('Ajax failed'), xhttp, params); 66 | } 67 | } 68 | 69 | }; 70 | 71 | xhttp.open(params.type, params.url, true); 72 | xhttp.send(params.data); 73 | return xhttp; 74 | }, 75 | 76 | }; 77 | 78 | /** 79 | * ------------ 80 | * Object Utils 81 | * ------------ 82 | */ 83 | var ObjectUtils = { 84 | 85 | clone: function (obj) { 86 | var newObj = {}, key; 87 | for (key in obj) { 88 | newObj[key] = obj[key]; 89 | } 90 | return newObj; 91 | }, 92 | 93 | defaults: function (object, defaults) { 94 | var key; 95 | for (key in defaults) { 96 | typeof(object[key]) === 'undefined' && (object[key] = defaults[key]); 97 | } 98 | return object; 99 | }, 100 | 101 | unbind: Function.bind.bind(Function.bind), 102 | 103 | instantiate: function (constructor, args) { 104 | return new (this.unbind(constructor, null).apply(null, args)); 105 | }, 106 | 107 | }; 108 | 109 | /** 110 | * --------- 111 | * Minivents 112 | * --------- 113 | */ 114 | function Events(target) { 115 | var events = {}, 116 | empty = []; 117 | target = target || this 118 | /** 119 | * On: listen to events 120 | */ 121 | target.on = function(type, func, ctx) { 122 | (events[type] = events[type] || []).push([func, ctx]) 123 | } 124 | /** 125 | * Off: stop listening to event / specific callback 126 | */ 127 | target.off = function(type, func) { 128 | type || (events = {}) 129 | var list = events[type] || empty, 130 | i = list.length = func ? list.length : 0; 131 | while (i--) func == list[i][0] && list.splice(i, 1) 132 | } 133 | /** 134 | * Emit: send event, callbacks will be triggered 135 | */ 136 | target.emit = function(type) { 137 | var e = events[type] || empty, 138 | list = e.length > 0 ? e.slice(0, e.length) : e, 139 | i = 0, 140 | j; 141 | while (j = list[i++]) j[0].apply(j[1], empty.slice.call(arguments, 1)) 142 | }; 143 | }; 144 | 145 | /** 146 | * ---------- 147 | * TimeKeeper 148 | * ---------- 149 | */ 150 | var OPTIONS_DEFAULTS = { 151 | ajaxType: 'get', 152 | ajaxMilliUrl: '/utcMillis', 153 | syncInterval: 1 * 60 * 1000, // 1 minute 154 | differenceInMillis: 0, 155 | overrideDate: false, 156 | responseParser: function (response) { 157 | return parseInt(response); 158 | }, 159 | }; 160 | 161 | var EVENTS = { 162 | SYNC: 'sync', 163 | SYNCED: 'synced', 164 | SYNC_ERROR: 'sync_error', 165 | 166 | FIRST_SYNC: 'first_sync', 167 | FIRST_SYNCED: 'first_synced', 168 | FIRST_SYNC_ERROR: 'first_sync_error', 169 | }; 170 | 171 | /** 172 | * TimeKeeper Constructor 173 | */ 174 | function TimeKeeper (options) { 175 | this._options = ObjectUtils.clone(options || {}); 176 | this._options = ObjectUtils.defaults(this._options, OPTIONS_DEFAULTS); 177 | this._differenceInMillis = this._options.differenceInMillis; 178 | this._isSyncedOnce = false; 179 | this._event = new Events(); 180 | this.setCorrectTimeInMillis(this._options.correctTimeInMillis); 181 | this._options.overrideDate && (this.overrideDate()); 182 | return this; 183 | } 184 | 185 | /** 186 | * TimeKeeper Public Methods 187 | */ 188 | TimeKeeper.prototype.sync = function () { 189 | return this._sync.apply(this, arguments); 190 | }; 191 | 192 | TimeKeeper.prototype.Date = function () { 193 | return this._getCorrectDate(); 194 | }; 195 | 196 | TimeKeeper.prototype.overrideDate = function () { 197 | var self = this; 198 | window.Date = function (OriginalDate) { 199 | function CorrectedDate() { 200 | var date = ObjectUtils.instantiate(OriginalDate, arguments), 201 | argumentsLength = arguments.length; 202 | if (argumentsLength === 0) { 203 | date.setTime(self._getCorrectDateMillis()); 204 | } 205 | return date; 206 | } 207 | CorrectedDate.prototype = OriginalDate.prototype; 208 | return CorrectedDate; 209 | }(this._Date); 210 | if (!window.Date.now) { 211 | window.Date.now = function now () { 212 | return new window.Date().getTime(); 213 | }; 214 | } 215 | if (!window.Date.parse) { 216 | window.Date.parse = this._Date.parse; 217 | } 218 | if (!window.Date.UTC) { 219 | window.Date.UTC = this._Date.UTC; 220 | } 221 | }; 222 | 223 | TimeKeeper.prototype.releaseDate = function () { 224 | return (window.Date = this._Date); 225 | }; 226 | 227 | TimeKeeper.prototype.setCorrectTimeInMillis = function (correctTimeInMillis) { 228 | return typeof(correctTimeInMillis) === 'number' && this._findDifferenceInMillis(correctTimeInMillis); 229 | }; 230 | 231 | TimeKeeper.prototype.setDifferenceInMillis = function (differenceInMillis) { 232 | return (this._differenceInMillis = differenceInMillis); 233 | }; 234 | 235 | TimeKeeper.prototype.getDifferenceInMillis = function () { 236 | return this._differenceInMillis; 237 | }; 238 | 239 | TimeKeeper.prototype.startSync = function (syncInterval) { 240 | return this._startSync.apply(this, arguments); 241 | }; 242 | 243 | TimeKeeper.prototype.stopSync = function () { 244 | return this._stopSync.apply(this, arguments); 245 | }; 246 | 247 | TimeKeeper.prototype.on = function () { 248 | this._event.on.apply(this._event, arguments); 249 | }; 250 | 251 | TimeKeeper.prototype.off = function () { 252 | this._event.off.apply(this._event, arguments); 253 | }; 254 | 255 | /** 256 | * TimeKeeper Private Members 257 | */ 258 | 259 | /** 260 | * Taking a backup of original Date constructor 261 | */ 262 | TimeKeeper.prototype._Date = window.Date; 263 | 264 | TimeKeeper.prototype._startSync = function (syncInterval) { 265 | var self = this; 266 | typeof(syncInterval) === 'number' && (this._options.syncInterval = syncInterval); 267 | this.stopSync(); 268 | this.sync(); 269 | return this._syncIntervalIndex = window.setInterval(function () { 270 | self.sync(); 271 | }, this._options.syncInterval); 272 | }; 273 | 274 | TimeKeeper.prototype._stopSync = function () { 275 | window.clearInterval(this._syncIntervalIndex); 276 | delete this._syncIntervalIndex; 277 | }; 278 | 279 | TimeKeeper.prototype._sync = function (callback) { 280 | callback || (callback = function() {}); 281 | var self = this, 282 | correctDate = self.Date(); 283 | this._emitPreSyncEvent(); 284 | this._getServerDateMillis(function (err, serverTimeInMillis) { 285 | if (err) { 286 | console.error('Failed to fetch server time. Error:', err); 287 | callback(err); 288 | self._emitSyncEvent(err); 289 | } else { 290 | self._findDifferenceInMillis(serverTimeInMillis); 291 | correctDate = self.Date(); 292 | callback(null, correctDate); 293 | self._emitSyncedEvent(null, correctDate); 294 | } 295 | }); 296 | }; 297 | 298 | TimeKeeper.prototype._emitPreFirstSyncEvent = function () { 299 | if (this._isSyncedOnce === false) { 300 | this._event.emit(EVENTS.FIRST_SYNC); 301 | } 302 | }; 303 | 304 | TimeKeeper.prototype._emitPreSyncEvent = function () { 305 | this._emitPreFirstSyncEvent(); 306 | this._event.emit(EVENTS.SYNC); 307 | }; 308 | 309 | TimeKeeper.prototype._emitFirstSyncedEvent = function (err, data) { 310 | if (this._isSyncedOnce === false) { 311 | this._isSyncedOnce = true; 312 | this._event.emit(err ? EVENTS.FIRST_SYNC_ERROR : EVENTS.FIRST_SYNCED, err || data); 313 | } 314 | }; 315 | 316 | TimeKeeper.prototype._emitSyncedEvent = function (err, data) { 317 | this._emitFirstSyncedEvent(err, data); 318 | this._event.emit(err ? EVENTS.SYNC_ERROR : EVENTS.SYNCED, err || data); 319 | }; 320 | 321 | TimeKeeper.prototype._getCorrectDate = function () { 322 | return new this._Date(this._getCorrectDateMillis()); 323 | }; 324 | 325 | TimeKeeper.prototype._getCorrectDateMillis = function () { 326 | return this._getMachineDateMillis() + this._differenceInMillis; 327 | }; 328 | 329 | TimeKeeper.prototype._getMachineDateMillis = function () { 330 | return new this._Date().getTime(); 331 | }; 332 | 333 | TimeKeeper.prototype._getServerDateMillis = function (callback) { 334 | var self = this, 335 | startTime = new this._Date().getTime(); 336 | AJAX.call({ 337 | type: this._options.ajaxType, 338 | url: this._options.ajaxMilliUrl, 339 | success: function (data) { 340 | var timeForResponse = new self._Date().getTime() - startTime, 341 | serverTime = self._options.responseParser(data) - (timeForResponse / 2); // Adjusting the server time, since request takes some time 342 | callback(null, serverTime); 343 | }, 344 | error: function (err) { 345 | callback(err); 346 | }, 347 | }); 348 | }; 349 | 350 | TimeKeeper.prototype._findDifferenceInMillis = function (serverDateInMillis) { 351 | this._differenceInMillis = serverDateInMillis - this._getMachineDateMillis(); 352 | }; 353 | 354 | function createInstance (options) { 355 | return new TimeKeeper(options); 356 | } 357 | 358 | return createInstance; 359 | 360 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timekeeper", 3 | "version": "0.0.2", 4 | "description": "Sync client time with server time. Get correct time even if client has set wrong time", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server", 8 | "test": "mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Sujsun/timekeeper" 13 | }, 14 | "keywords": [ 15 | "sync", 16 | "server", 17 | "time" 18 | ], 19 | "author": "Sundarasan Natarajan", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/Sujsun/timekeeper/issues" 23 | }, 24 | "homepage": "https://github.com/Sujsun/timekeeper", 25 | "dependencies": { 26 | "express": "^4.14.0" 27 | }, 28 | "devDependencies": { 29 | "grunt": "^1.0.1", 30 | "grunt-contrib-copy": "^1.0.0", 31 | "grunt-contrib-uglify": "^1.0.1", 32 | "grunt-contrib-watch": "^1.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/dist/timekeeper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin: TimeKeeper 3 | * Author: Sundarasan Natarajan 4 | * GIT: https://github.com/Sujsun/timekeeper.git 5 | * Version: 0.0.2 6 | */ 7 | (function (root, factory) { 8 | 9 | if (typeof exports === 'object') { 10 | module.exports = factory(); 11 | } else if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define([], factory); 14 | } else { 15 | // Browser globals 16 | root.TimeKeeper = factory(); 17 | } 18 | 19 | })(window, function () { 20 | 21 | /** 22 | * ------------ 23 | * AJAX Helpers 24 | * ------------ 25 | */ 26 | var AJAX = { 27 | 28 | /** 29 | * AJAX Helper 30 | * @param {Object} params { type: 'get', dataType: 'json', data: { .. }, headers: { ... } success: Function, error: Function } 31 | * @return {XHRObject} 32 | */ 33 | call: function(params) { 34 | var result, 35 | headerName, 36 | headerValue; 37 | 38 | typeof(params) === 'object' || (params = {}); 39 | typeof(params.type) === 'string' || (params.type = 'get'); 40 | typeof(params.type) === 'string' && (params.type = params.type.toUpperCase()); 41 | typeof(params.success) === 'function' || (params.success = EMPTY_FUNCTION); 42 | typeof(params.error) === 'function' || (params.error = EMPTY_FUNCTION); 43 | 44 | var xhttp = new XMLHttpRequest(); 45 | 46 | for (headerName in params.headers) { 47 | headerValue = params.headers[headerName]; 48 | xhttp.setRequestHeader(headerName, headerValue); 49 | } 50 | 51 | xhttp.onreadystatechange = function() { 52 | 53 | if (xhttp.readyState == 4) { 54 | if ((xhttp.status >= 200 && xhttp.status < 300) || xhttp.status == 304) { 55 | result = xhttp.responseText; 56 | if (params.dataType === 'json') { 57 | try { 58 | result = JSON.parse(xhttp.responseText); 59 | } catch (error) { 60 | params.error.call(params.context, error, xhttp, params); 61 | } 62 | } 63 | params.success.call(params.context, result, xhttp, params); 64 | } else { 65 | params.error.call(params.context, new Error('Ajax failed'), xhttp, params); 66 | } 67 | } 68 | 69 | }; 70 | 71 | xhttp.open(params.type, params.url, true); 72 | xhttp.send(params.data); 73 | return xhttp; 74 | }, 75 | 76 | }; 77 | 78 | /** 79 | * ------------ 80 | * Object Utils 81 | * ------------ 82 | */ 83 | var ObjectUtils = { 84 | 85 | clone: function (obj) { 86 | var newObj = {}, key; 87 | for (key in obj) { 88 | newObj[key] = obj[key]; 89 | } 90 | return newObj; 91 | }, 92 | 93 | defaults: function (object, defaults) { 94 | var key; 95 | for (key in defaults) { 96 | typeof(object[key]) === 'undefined' && (object[key] = defaults[key]); 97 | } 98 | return object; 99 | }, 100 | 101 | unbind: Function.bind.bind(Function.bind), 102 | 103 | instantiate: function (constructor, args) { 104 | return new (this.unbind(constructor, null).apply(null, args)); 105 | }, 106 | 107 | }; 108 | 109 | /** 110 | * --------- 111 | * Minivents 112 | * --------- 113 | */ 114 | function Events(target) { 115 | var events = {}, 116 | empty = []; 117 | target = target || this 118 | /** 119 | * On: listen to events 120 | */ 121 | target.on = function(type, func, ctx) { 122 | (events[type] = events[type] || []).push([func, ctx]) 123 | } 124 | /** 125 | * Off: stop listening to event / specific callback 126 | */ 127 | target.off = function(type, func) { 128 | type || (events = {}) 129 | var list = events[type] || empty, 130 | i = list.length = func ? list.length : 0; 131 | while (i--) func == list[i][0] && list.splice(i, 1) 132 | } 133 | /** 134 | * Emit: send event, callbacks will be triggered 135 | */ 136 | target.emit = function(type) { 137 | var e = events[type] || empty, 138 | list = e.length > 0 ? e.slice(0, e.length) : e, 139 | i = 0, 140 | j; 141 | while (j = list[i++]) j[0].apply(j[1], empty.slice.call(arguments, 1)) 142 | }; 143 | }; 144 | 145 | /** 146 | * ---------- 147 | * TimeKeeper 148 | * ---------- 149 | */ 150 | var OPTIONS_DEFAULTS = { 151 | ajaxType: 'get', 152 | ajaxMilliUrl: '/utcMillis', 153 | syncInterval: 1 * 60 * 1000, // 1 minute 154 | differenceInMillis: 0, 155 | overrideDate: false, 156 | responseParser: function (response) { 157 | return parseInt(response); 158 | }, 159 | }; 160 | 161 | var EVENTS = { 162 | SYNC: 'sync', 163 | SYNCED: 'synced', 164 | SYNC_ERROR: 'sync_error', 165 | 166 | FIRST_SYNC: 'first_sync', 167 | FIRST_SYNCED: 'first_synced', 168 | FIRST_SYNC_ERROR: 'first_sync_error', 169 | }; 170 | 171 | /** 172 | * TimeKeeper Constructor 173 | */ 174 | function TimeKeeper (options) { 175 | this._options = ObjectUtils.clone(options || {}); 176 | this._options = ObjectUtils.defaults(this._options, OPTIONS_DEFAULTS); 177 | this._differenceInMillis = this._options.differenceInMillis; 178 | this._isSyncedOnce = false; 179 | this._event = new Events(); 180 | this.setCorrectTimeInMillis(this._options.correctTimeInMillis); 181 | this._options.overrideDate && (this.overrideDate()); 182 | return this; 183 | } 184 | 185 | /** 186 | * TimeKeeper Public Methods 187 | */ 188 | TimeKeeper.prototype.sync = function () { 189 | return this._sync.apply(this, arguments); 190 | }; 191 | 192 | TimeKeeper.prototype.Date = function () { 193 | return this._getCorrectDate(); 194 | }; 195 | 196 | TimeKeeper.prototype.overrideDate = function () { 197 | var self = this; 198 | window.Date = function (OriginalDate) { 199 | function CorrectedDate() { 200 | var date = ObjectUtils.instantiate(OriginalDate, arguments), 201 | argumentsLength = arguments.length; 202 | if (argumentsLength === 0) { 203 | date.setTime(self._getCorrectDateMillis()); 204 | } 205 | return date; 206 | } 207 | CorrectedDate.prototype = OriginalDate.prototype; 208 | return CorrectedDate; 209 | }(this._Date); 210 | if (!window.Date.now) { 211 | window.Date.now = function now () { 212 | return new window.Date().getTime(); 213 | }; 214 | } 215 | if (!window.Date.parse) { 216 | window.Date.parse = this._Date.parse; 217 | } 218 | if (!window.Date.UTC) { 219 | window.Date.UTC = this._Date.UTC; 220 | } 221 | }; 222 | 223 | TimeKeeper.prototype.releaseDate = function () { 224 | return (window.Date = this._Date); 225 | }; 226 | 227 | TimeKeeper.prototype.setCorrectTimeInMillis = function (correctTimeInMillis) { 228 | return typeof(correctTimeInMillis) === 'number' && this._findDifferenceInMillis(correctTimeInMillis); 229 | }; 230 | 231 | TimeKeeper.prototype.setDifferenceInMillis = function (differenceInMillis) { 232 | return (this._differenceInMillis = differenceInMillis); 233 | }; 234 | 235 | TimeKeeper.prototype.getDifferenceInMillis = function () { 236 | return this._differenceInMillis; 237 | }; 238 | 239 | TimeKeeper.prototype.startSync = function (syncInterval) { 240 | return this._startSync.apply(this, arguments); 241 | }; 242 | 243 | TimeKeeper.prototype.stopSync = function () { 244 | return this._stopSync.apply(this, arguments); 245 | }; 246 | 247 | TimeKeeper.prototype.on = function () { 248 | this._event.on.apply(this._event, arguments); 249 | }; 250 | 251 | TimeKeeper.prototype.off = function () { 252 | this._event.off.apply(this._event, arguments); 253 | }; 254 | 255 | /** 256 | * TimeKeeper Private Members 257 | */ 258 | 259 | /** 260 | * Taking a backup of original Date constructor 261 | */ 262 | TimeKeeper.prototype._Date = window.Date; 263 | 264 | TimeKeeper.prototype._startSync = function (syncInterval) { 265 | var self = this; 266 | typeof(syncInterval) === 'number' && (this._options.syncInterval = syncInterval); 267 | this.stopSync(); 268 | this.sync(); 269 | return this._syncIntervalIndex = window.setInterval(function () { 270 | self.sync(); 271 | }, this._options.syncInterval); 272 | }; 273 | 274 | TimeKeeper.prototype._stopSync = function () { 275 | window.clearInterval(this._syncIntervalIndex); 276 | delete this._syncIntervalIndex; 277 | }; 278 | 279 | TimeKeeper.prototype._sync = function (callback) { 280 | callback || (callback = function() {}); 281 | var self = this, 282 | correctDate = self.Date(); 283 | this._emitPreSyncEvent(); 284 | this._getServerDateMillis(function (err, serverTimeInMillis) { 285 | if (err) { 286 | console.error('Failed to fetch server time. Error:', err); 287 | callback(err); 288 | self._emitSyncEvent(err); 289 | } else { 290 | self._findDifferenceInMillis(serverTimeInMillis); 291 | correctDate = self.Date(); 292 | callback(null, correctDate); 293 | self._emitSyncedEvent(null, correctDate); 294 | } 295 | }); 296 | }; 297 | 298 | TimeKeeper.prototype._emitPreFirstSyncEvent = function () { 299 | if (this._isSyncedOnce === false) { 300 | this._event.emit(EVENTS.FIRST_SYNC); 301 | } 302 | }; 303 | 304 | TimeKeeper.prototype._emitPreSyncEvent = function () { 305 | this._emitPreFirstSyncEvent(); 306 | this._event.emit(EVENTS.SYNC); 307 | }; 308 | 309 | TimeKeeper.prototype._emitFirstSyncedEvent = function (err, data) { 310 | if (this._isSyncedOnce === false) { 311 | this._isSyncedOnce = true; 312 | this._event.emit(err ? EVENTS.FIRST_SYNC_ERROR : EVENTS.FIRST_SYNCED, err || data); 313 | } 314 | }; 315 | 316 | TimeKeeper.prototype._emitSyncedEvent = function (err, data) { 317 | this._emitFirstSyncedEvent(err, data); 318 | this._event.emit(err ? EVENTS.SYNC_ERROR : EVENTS.SYNCED, err || data); 319 | }; 320 | 321 | TimeKeeper.prototype._getCorrectDate = function () { 322 | return new this._Date(this._getCorrectDateMillis()); 323 | }; 324 | 325 | TimeKeeper.prototype._getCorrectDateMillis = function () { 326 | return this._getMachineDateMillis() + this._differenceInMillis; 327 | }; 328 | 329 | TimeKeeper.prototype._getMachineDateMillis = function () { 330 | return new this._Date().getTime(); 331 | }; 332 | 333 | TimeKeeper.prototype._getServerDateMillis = function (callback) { 334 | var self = this, 335 | startTime = new this._Date().getTime(); 336 | AJAX.call({ 337 | type: this._options.ajaxType, 338 | url: this._options.ajaxMilliUrl, 339 | success: function (data) { 340 | var timeForResponse = new self._Date().getTime() - startTime, 341 | serverTime = self._options.responseParser(data) - (timeForResponse / 2); // Adjusting the server time, since request takes some time 342 | callback(null, serverTime); 343 | }, 344 | error: function (err) { 345 | callback(err); 346 | }, 347 | }); 348 | }; 349 | 350 | TimeKeeper.prototype._findDifferenceInMillis = function (serverDateInMillis) { 351 | this._differenceInMillis = serverDateInMillis - this._getMachineDateMillis(); 352 | }; 353 | 354 | function createInstance (options) { 355 | return new TimeKeeper(options); 356 | } 357 | 358 | return createInstance; 359 | 360 | }); -------------------------------------------------------------------------------- /public/dist/timekeeper.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin: TimeKeeper 3 | * Author: Sundarasan Natarajan 4 | * GIT: https://github.com/Sujsun/timekeeper.git 5 | * Version: 0.0.2 6 | */ 7 | !function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define([],b):a.TimeKeeper=b()}(window,function(){function a(a){var b={},c=[];a=a||this,a.on=function(a,c,d){(b[a]=b[a]||[]).push([c,d])},a.off=function(a,d){a||(b={});for(var e=b[a]||c,f=e.length=d?e.length:0;f--;)d==e[f][0]&&e.splice(f,1)},a.emit=function(a){for(var d,e=b[a]||c,f=e.length>0?e.slice(0,e.length):e,g=0;d=f[g++];)d[0].apply(d[1],c.slice.call(arguments,1))}}function b(b){return this._options=e.clone(b||{}),this._options=e.defaults(this._options,f),this._differenceInMillis=this._options.differenceInMillis,this._isSyncedOnce=!1,this._event=new a,this.setCorrectTimeInMillis(this._options.correctTimeInMillis),this._options.overrideDate&&this.overrideDate(),this}function c(a){return new b(a)}var d={call:function(a){var b,c,d;"object"==typeof a||(a={}),"string"==typeof a.type||(a.type="get"),"string"==typeof a.type&&(a.type=a.type.toUpperCase()),"function"==typeof a.success||(a.success=EMPTY_FUNCTION),"function"==typeof a.error||(a.error=EMPTY_FUNCTION);var e=new XMLHttpRequest;for(c in a.headers)d=a.headers[c],e.setRequestHeader(c,d);return e.onreadystatechange=function(){if(4==e.readyState)if(e.status>=200&&e.status<300||304==e.status){if(b=e.responseText,"json"===a.dataType)try{b=JSON.parse(e.responseText)}catch(c){a.error.call(a.context,c,e,a)}a.success.call(a.context,b,e,a)}else a.error.call(a.context,new Error("Ajax failed"),e,a)},e.open(a.type,a.url,!0),e.send(a.data),e}},e={clone:function(a){var b,c={};for(b in a)c[b]=a[b];return c},defaults:function(a,b){var c;for(c in b)"undefined"==typeof a[c]&&(a[c]=b[c]);return a},unbind:Function.bind.bind(Function.bind),instantiate:function(a,b){return new(this.unbind(a,null).apply(null,b))}},f={ajaxType:"get",ajaxMilliUrl:"/utcMillis",syncInterval:6e4,differenceInMillis:0,overrideDate:!1,responseParser:function(a){return parseInt(a)}},g={SYNC:"sync",SYNCED:"synced",SYNC_ERROR:"sync_error",FIRST_SYNC:"first_sync",FIRST_SYNCED:"first_synced",FIRST_SYNC_ERROR:"first_sync_error"};return b.prototype.sync=function(){return this._sync.apply(this,arguments)},b.prototype.Date=function(){return this._getCorrectDate()},b.prototype.overrideDate=function(){var a=this;window.Date=function(b){function c(){var c=e.instantiate(b,arguments),d=arguments.length;return 0===d&&c.setTime(a._getCorrectDateMillis()),c}return c.prototype=b.prototype,c}(this._Date),window.Date.now||(window.Date.now=function(){return(new window.Date).getTime()}),window.Date.parse||(window.Date.parse=this._Date.parse),window.Date.UTC||(window.Date.UTC=this._Date.UTC)},b.prototype.releaseDate=function(){return window.Date=this._Date},b.prototype.setCorrectTimeInMillis=function(a){return"number"==typeof a&&this._findDifferenceInMillis(a)},b.prototype.setDifferenceInMillis=function(a){return this._differenceInMillis=a},b.prototype.getDifferenceInMillis=function(){return this._differenceInMillis},b.prototype.startSync=function(a){return this._startSync.apply(this,arguments)},b.prototype.stopSync=function(){return this._stopSync.apply(this,arguments)},b.prototype.on=function(){this._event.on.apply(this._event,arguments)},b.prototype.off=function(){this._event.off.apply(this._event,arguments)},b.prototype._Date=window.Date,b.prototype._startSync=function(a){var b=this;return"number"==typeof a&&(this._options.syncInterval=a),this.stopSync(),this.sync(),this._syncIntervalIndex=window.setInterval(function(){b.sync()},this._options.syncInterval)},b.prototype._stopSync=function(){window.clearInterval(this._syncIntervalIndex),delete this._syncIntervalIndex},b.prototype._sync=function(a){a||(a=function(){});var b=this,c=b.Date();this._emitPreSyncEvent(),this._getServerDateMillis(function(d,e){d?(console.error("Failed to fetch server time. Error:",d),a(d),b._emitSyncEvent(d)):(b._findDifferenceInMillis(e),c=b.Date(),a(null,c),b._emitSyncedEvent(null,c))})},b.prototype._emitPreFirstSyncEvent=function(){this._isSyncedOnce===!1&&this._event.emit(g.FIRST_SYNC)},b.prototype._emitPreSyncEvent=function(){this._emitPreFirstSyncEvent(),this._event.emit(g.SYNC)},b.prototype._emitFirstSyncedEvent=function(a,b){this._isSyncedOnce===!1&&(this._isSyncedOnce=!0,this._event.emit(a?g.FIRST_SYNC_ERROR:g.FIRST_SYNCED,a||b))},b.prototype._emitSyncedEvent=function(a,b){this._emitFirstSyncedEvent(a,b),this._event.emit(a?g.SYNC_ERROR:g.SYNCED,a||b)},b.prototype._getCorrectDate=function(){return new this._Date(this._getCorrectDateMillis())},b.prototype._getCorrectDateMillis=function(){return this._getMachineDateMillis()+this._differenceInMillis},b.prototype._getMachineDateMillis=function(){return(new this._Date).getTime()},b.prototype._getServerDateMillis=function(a){var b=this,c=(new this._Date).getTime();d.call({type:this._options.ajaxType,url:this._options.ajaxMilliUrl,success:function(d){var e=(new b._Date).getTime()-c,f=b._options.responseParser(d)-e/2;a(null,f)},error:function(b){a(b)}})},b.prototype._findDifferenceInMillis=function(a){this._differenceInMillis=a-this._getMachineDateMillis()},c}); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo | TimeKeeper 5 | 6 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 |

Demo | TimeKeeper

25 | 26 |
27 |
28 | 29 |
30 | 31 | 12:00 PM 32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 | 12:00 PM 40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 | 0 minutes 48 |
49 | 50 |
51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 |

Syncing...

61 |
62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/js/action.js: -------------------------------------------------------------------------------- 1 | var elem = {}; 2 | 3 | elem.machineTimeSpan = document.getElementById('machine-time'); 4 | elem.correctTimeSpan = document.getElementById('correct-time'); 5 | elem.timeDifferenceSpan = document.getElementById('time-difference'); 6 | elem.syncingStatus = document.getElementById('syncing-status'); 7 | elem.syncButton = document.getElementById('sync-button'); 8 | 9 | var timekeeper = TimeKeeper({ 10 | ajaxType: 'get', 11 | ajaxMilliUrl: '/date', 12 | syncInterval: 3000, 13 | responseParser: function (response) { 14 | return JSON.parse(response).date; 15 | }, 16 | }); 17 | 18 | timekeeper.on('sync', function () { 19 | elem.syncingStatus.style.display = 'block'; 20 | }); 21 | 22 | timekeeper.on('first_synced', function () { 23 | updateSyncInView(); 24 | }); 25 | 26 | timekeeper.on('first_sync_error', function () { 27 | updateSyncInView('fail'); 28 | }); 29 | 30 | timekeeper.on('synced', function () { 31 | updateSyncInView(); 32 | }); 33 | 34 | timekeeper.on('sync_error', function () { 35 | updateSyncInView('fail'); 36 | }); 37 | 38 | function updateTimeInView () { 39 | elem.machineTimeSpan.innerHTML = new Date(); 40 | elem.correctTimeSpan.innerHTML = timekeeper.Date(); 41 | } 42 | 43 | function updateSyncInView (err) { 44 | if (err) { 45 | elem.syncingStatus.innerHTML = 'Failed to sync!'; 46 | return; 47 | } 48 | elem.syncingStatus.style.display = 'none'; 49 | elem.timeDifferenceSpan.innerHTML = Math.round(timekeeper.getDifferenceInMillis() / (60 * 1000)) + ' minutes'; 50 | updateTimeInView(); 51 | } 52 | 53 | function onSyncClick (event) { 54 | timekeeper.sync(); 55 | } 56 | 57 | function attachViewEvents () { 58 | elem.syncButton.addEventListener('click', onSyncClick); 59 | } 60 | 61 | timekeeper.startSync(10 * 1000); 62 | updateTimeInView(); 63 | attachViewEvents(); 64 | 65 | setInterval(function () { 66 | updateTimeInView(); 67 | }, 1 * 1000); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static('public')); 5 | 6 | app.get('/', function (req, res) { 7 | res.sendFile('public/index.html'); 8 | }); 9 | 10 | app.get('/utcMillis', function (req, res) { 11 | setTimeout(function () { 12 | res.send((new Date().getTime() - (3 * 60 * 1000)) + ''); 13 | }, 1 * 1000); 14 | }); 15 | 16 | app.get('/date', function (req, res) { 17 | setTimeout(function () { 18 | res.json({ 19 | date: new Date().getTime() - (3 * 60 * 1000), 20 | }); 21 | }, 1 * 1000); 22 | }); 23 | 24 | app.listen(8989, function () { 25 | console.log('Example app listening on port 8989!'); 26 | }); --------------------------------------------------------------------------------