| 
 | 1 | +import { Meteor } from 'meteor/meteor';  | 
 | 2 | +import { Tracker } from 'meteor/tracker';  | 
 | 3 | +import { HTTP } from 'meteor/http';  | 
 | 4 | + | 
 | 5 | +TimeSync = {  | 
 | 6 | +  loggingEnabled: Meteor.isDevelopment,  | 
 | 7 | +  forceDDP: false  | 
 | 8 | +};  | 
 | 9 | + | 
 | 10 | +function log( /* arguments */ ) {  | 
 | 11 | +  if (TimeSync.loggingEnabled) {  | 
 | 12 | +    Meteor._debug.apply(this, arguments);  | 
 | 13 | +  }  | 
 | 14 | +}  | 
 | 15 | + | 
 | 16 | +const defaultInterval = 1000;  | 
 | 17 | + | 
 | 18 | +// Internal values, exported for testing  | 
 | 19 | +SyncInternals = {  | 
 | 20 | +  offset: undefined,  | 
 | 21 | +  roundTripTime: undefined,  | 
 | 22 | +  offsetTracker: new Tracker.Dependency(),  | 
 | 23 | +  syncTracker: new Tracker.Dependency(),  | 
 | 24 | +  isSynced: false,  | 
 | 25 | +  usingDDP: false,  | 
 | 26 | +  timeTick: {},  | 
 | 27 | +  getDiscrepancy: function (lastTime, currentTime, interval) {  | 
 | 28 | +    return currentTime - (lastTime + interval)  | 
 | 29 | +  }  | 
 | 30 | +};  | 
 | 31 | + | 
 | 32 | +SyncInternals.timeTick[defaultInterval] = new Tracker.Dependency();  | 
 | 33 | + | 
 | 34 | +const maxAttempts = 5;  | 
 | 35 | +let attempts = 0;  | 
 | 36 | + | 
 | 37 | +/*  | 
 | 38 | +  This is an approximation of  | 
 | 39 | +  http://en.wikipedia.org/wiki/Network_Time_Protocol  | 
 | 40 | +
  | 
 | 41 | +  If this turns out to be more accurate under the connect handlers,  | 
 | 42 | +  we should try taking multiple measurements.  | 
 | 43 | + */  | 
 | 44 | + | 
 | 45 | +let syncUrl;  | 
 | 46 | + | 
 | 47 | +TimeSync.setSyncUrl = function (url) {  | 
 | 48 | +  if (url) {  | 
 | 49 | +    syncUrl = url;  | 
 | 50 | +  } else if (Meteor.isCordova || Meteor.isDesktop) {  | 
 | 51 | +    // Only use Meteor.absoluteUrl for Cordova and Desktop; see  | 
 | 52 | +    // https://github.com/meteor/meteor/issues/4696  | 
 | 53 | +    // https://github.com/mizzao/meteor-timesync/issues/30  | 
 | 54 | +    // Cordova should never be running out of a subdirectory...  | 
 | 55 | +    syncUrl = Meteor.absoluteUrl('_timesync');  | 
 | 56 | +  } else {  | 
 | 57 | +    // Support Meteor running in relative paths, based on computed root url prefix  | 
 | 58 | +    // https://github.com/mizzao/meteor-timesync/pull/40  | 
 | 59 | +    const basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';  | 
 | 60 | +    syncUrl = basePath + '/_timesync';  | 
 | 61 | +  }  | 
 | 62 | +};  | 
 | 63 | +TimeSync.getSyncUrl = function () {  | 
 | 64 | +  return syncUrl;  | 
 | 65 | +}  | 
 | 66 | +TimeSync.setSyncUrl();  | 
 | 67 | + | 
 | 68 | +const updateOffset = function () {  | 
 | 69 | +  const t0 = Date.now();  | 
 | 70 | +  if (TimeSync.forceDDP || SyncInternals.useDDP) {  | 
 | 71 | +    Meteor.call('_timeSync', function (err, res) {  | 
 | 72 | +      handleResponse(t0, err, res);  | 
 | 73 | +    });  | 
 | 74 | +  } else {  | 
 | 75 | +    HTTP.get(syncUrl, function (err, res) {  | 
 | 76 | +      handleResponse(t0, err, res);  | 
 | 77 | +    });  | 
 | 78 | +  }  | 
 | 79 | +};  | 
 | 80 | + | 
 | 81 | +const handleResponse = function (t0, err, res) {  | 
 | 82 | +  const t3 = Date.now(); // Grab this now  | 
 | 83 | +  if (err) {  | 
 | 84 | +    // We'll still use our last computed offset if is defined  | 
 | 85 | +    log('Error syncing to server time: ', err);  | 
 | 86 | +    if (++attempts <= maxAttempts) {  | 
 | 87 | +      Meteor.setTimeout(TimeSync.resync, 1000);  | 
 | 88 | +    } else {  | 
 | 89 | +      log('Max number of time sync attempts reached. Giving up.');  | 
 | 90 | +    }  | 
 | 91 | +    return;  | 
 | 92 | +  }  | 
 | 93 | + | 
 | 94 | +  attempts = 0; // It worked  | 
 | 95 | +  const response = res.content || res;  | 
 | 96 | +  const ts = parseInt(response, 10);  | 
 | 97 | +  SyncInternals.isSynced = true;  | 
 | 98 | +  SyncInternals.offset = Math.round(((ts - t0) + (ts - t3)) / 2);  | 
 | 99 | +  SyncInternals.roundTripTime = t3 - t0; // - (ts - ts) which is 0  | 
 | 100 | +  SyncInternals.offsetTracker.changed();  | 
 | 101 | +}  | 
 | 102 | + | 
 | 103 | +// Reactive variable for server time that updates every second.  | 
 | 104 | +TimeSync.serverTime = function (clientTime, interval) {  | 
 | 105 | +  check(interval, Match.Optional(Match.Integer));  | 
 | 106 | +  // If a client time is provided, we don't need to depend on the tick.  | 
 | 107 | +  if (!clientTime) getTickDependency(interval || defaultInterval).depend();  | 
 | 108 | + | 
 | 109 | +  SyncInternals.offsetTracker.depend(); // depend on offset to enable reactivity  | 
 | 110 | +  // Convert Date argument to epoch as necessary  | 
 | 111 | +  return (+clientTime || Date.now()) + SyncInternals.offset;  | 
 | 112 | +};  | 
 | 113 | + | 
 | 114 | +// Reactive variable for the difference between server and client time.  | 
 | 115 | +TimeSync.serverOffset = function () {  | 
 | 116 | +  SyncInternals.offsetTracker.depend();  | 
 | 117 | +  return SyncInternals.offset;  | 
 | 118 | +};  | 
 | 119 | + | 
 | 120 | +TimeSync.roundTripTime = function () {  | 
 | 121 | +  SyncInternals.offsetTracker.depend();  | 
 | 122 | +  return SyncInternals.roundTripTime;  | 
 | 123 | +};  | 
 | 124 | + | 
 | 125 | +TimeSync.isSynced = function () {  | 
 | 126 | +  SyncInternals.offsetTracker.depend();  | 
 | 127 | +  return SyncInternals.isSynced;  | 
 | 128 | +};  | 
 | 129 | + | 
 | 130 | +let resyncIntervalId = null;  | 
 | 131 | + | 
 | 132 | +TimeSync.resync = function () {  | 
 | 133 | +  if (resyncIntervalId !== null) Meteor.clearInterval(resyncIntervalId);  | 
 | 134 | + | 
 | 135 | +  updateOffset();  | 
 | 136 | +  resyncIntervalId = Meteor.setInterval(updateOffset, (SyncInternals.useDDP) ? 300000 : 600000);  | 
 | 137 | +};  | 
 | 138 | + | 
 | 139 | +// Run this as soon as we load, even before Meteor.startup()  | 
 | 140 | +// Run again whenever we reconnect after losing connection  | 
 | 141 | +let wasConnected = false;  | 
 | 142 | + | 
 | 143 | +Tracker.autorun(function () {  | 
 | 144 | +  const connected = Meteor.status().connected;  | 
 | 145 | +  if (connected && !wasConnected) TimeSync.resync();  | 
 | 146 | +  wasConnected = connected;  | 
 | 147 | +  SyncInternals.useDDP = connected;  | 
 | 148 | +});  | 
 | 149 | + | 
 | 150 | +// Resync if unexpected change by more than a few seconds. This needs to be  | 
 | 151 | +// somewhat lenient, or a CPU-intensive operation can trigger a re-sync even  | 
 | 152 | +// when the offset is still accurate. In any case, we're not going to be able to  | 
 | 153 | +// catch very small system-initiated NTP adjustments with this, anyway.  | 
 | 154 | +const tickCheckTolerance = 5000;  | 
 | 155 | + | 
 | 156 | +let lastClientTime = Date.now();  | 
 | 157 | + | 
 | 158 | +// Set up a new interval for any amount of reactivity.  | 
 | 159 | +function getTickDependency(interval) {  | 
 | 160 | + | 
 | 161 | +  if (!SyncInternals.timeTick[interval]) {  | 
 | 162 | +    const dep = new Tracker.Dependency();  | 
 | 163 | + | 
 | 164 | +    Meteor.setInterval(function () {  | 
 | 165 | +      dep.changed();  | 
 | 166 | +    }, interval);  | 
 | 167 | + | 
 | 168 | +    SyncInternals.timeTick[interval] = dep;  | 
 | 169 | +  }  | 
 | 170 | + | 
 | 171 | +  return SyncInternals.timeTick[interval];  | 
 | 172 | +}  | 
 | 173 | + | 
 | 174 | +// Set up special interval for the default tick, which also watches for re-sync  | 
 | 175 | +Meteor.setInterval(function () {  | 
 | 176 | +  const currentClientTime = Date.now();  | 
 | 177 | +  const discrepancy = SyncInternals.getDiscrepancy(lastClientTime, currentClientTime, defaultInterval);  | 
 | 178 | + | 
 | 179 | +  if (Math.abs(discrepancy) < tickCheckTolerance) {  | 
 | 180 | +    // No problem here, just keep ticking along  | 
 | 181 | +    SyncInternals.timeTick[defaultInterval].changed();  | 
 | 182 | +  } else {  | 
 | 183 | +    // resync on major client clock changes  | 
 | 184 | +    // based on http://stackoverflow.com/a/3367542/1656818  | 
 | 185 | +    log('Clock discrepancy detected. Attempting re-sync.');  | 
 | 186 | +    // Refuse to compute server time and try to guess new server offset. Guessing only works if the server time hasn't changed.  | 
 | 187 | +    SyncInternals.offset = SyncInternals.offset - discrepancy;  | 
 | 188 | +    SyncInternals.isSynced = false;  | 
 | 189 | +    SyncInternals.offsetTracker.changed();  | 
 | 190 | +    TimeSync.resync();  | 
 | 191 | +  }  | 
 | 192 | + | 
 | 193 | +  lastClientTime = currentClientTime;  | 
 | 194 | +}, defaultInterval);  | 
0 commit comments