Fix _timesync for subfolders

pull/3007/head
Rodrigo Nascimento 10 years ago
parent a7f9771de1
commit 96f392c475
No known key found for this signature in database
GPG Key ID: 2C85B3AFE75D23F9
  1. 2
      packages/meteor-timesync/.gitignore
  2. 5
      packages/meteor-timesync/.travis.yml
  3. 72
      packages/meteor-timesync/History.md
  4. 21
      packages/meteor-timesync/LICENSE
  5. 38
      packages/meteor-timesync/README.md
  6. 38
      packages/meteor-timesync/package.js
  7. 121
      packages/meteor-timesync/tests/client.js
  8. 175
      packages/meteor-timesync/timesync-client.js
  9. 28
      packages/meteor-timesync/timesync-server.js

@ -0,0 +1,2 @@
.build*
.versions

@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/ejPSng | /bin/sh"

@ -0,0 +1,72 @@
## vNEXT
## v0.3.4
- Explicitly pull in client-side `check` for Meteor 1.2 apps.
## v0.3.3
- Be more robust with sync url when outside of Cordova. (#30)
## v0.3.2
- Fix issue when used in Cordova. (#22, #26, #27)
## v0.3.1
- Fix an issue where `TimeSync.serverTime` returned an erroneous value when passed a `Date` (instead of an epoch). (#23)
## v0.3.0
- `TimeSync.serverTime` now supports an optional second `updateInterval` argument, causing the reactive value to update less frequently. (#10)
- `TimeSync.loggingEnabled` can be now set to false to suppress client log output. (#21)
- Explicitly set MIME type on timesync endpoint. (#17, #18)
## v0.2.2
- **Updated for Meteor 0.9.**
- Further adjust clock watching tolerance to be less sensitive to CPU.
## v0.2.1
- Re-sync automatically after a reconnection.
- Adjust clock watching tolerance so as to be less sensitive to heavy client CPU usage.
## v0.2.0
- Clock change watching is now on by default (it's very lightweight and only involves grabbing and checking a `Date`).
- Invalidate offset value and dependent time computations when we detect a clock change.
- Added a `Date.now` shim for earlier versions of IE.
- Reorganized code for testing and added some basic tests.
## v0.1.6
- Added the optional `TimeSync.watchClockChanges` which can resync if a client's clock is detected to have significantly changed.
- Added retry attempts to syncing, making it more robust over a hot code reload among other situations.
## v0.1.5
- Use `WebApp.rawConnectHandlers` as a less janky way of getting our date request handled first.
- Fixed an issue where a cached reload could result in a wacky time offset due to the server time being cached.
## v0.1.4
- Switch to JS at the request of @raix and @arunoda ;-)
- Use a middleware handler, spliced into the top of the connect stack, instead of a Meteor method to avoid arbitrary method blocking delay. This improves accuracy significantly.
- Compute a RTT value in `TimeSync.roundTripTime` as well as a time offset.
## v0.1.3
- Ensure that the computed offset is always an integer number of milliseconds.
## v0.1.2
- Added the `TimeSync.resync` function that triggers a resync with the server.
## v0.1.1
- Added the reactive function `TimeSync.isSynced` to determine if an initial sync has taken place.
## v0.1.0
- First release.

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Andrew Mao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,38 @@
meteor-timesync [![Build Status](https://travis-ci.org/mizzao/meteor-timesync.svg)](https://travis-ci.org/mizzao/meteor-timesync)
===============
NTP-style time synchronization between server and client, and facilities to
use server time reactively in Meteor applications.
## What's this do?
Meteor clients don't necessarily have accurate timestamps relative to your server. This package computes and maintains an offset between server and client, allowing server timestamps to be used on the client (especially for displaying time differences). It also provides facilities to use time reactively in your application.
There is a demo as part of the user-status app at http://user-status.meteor.com.
## Installation
```
meteor add mizzao:timesync
```
## Usage
- `TimeSync.serverTime(clientTime, updateInterval)`: returns the server time for a given client time, as a UTC/Unix timestamp. A reactive variable which changes with the computed offset, and updates continually. Pass in `clientTime` optionally to specify a particular time on the client, instead of reactively depending on the current time. Pass in `updateInterval` to change the rate (in milliseconds) at which the reactive variable updates; the default value is 1000 (1 second).
- `TimeSync.serverOffset()`: returns the current time difference between the server and the client. Reactively updates as the offset is recomputed.
- `TimeSync.roundTripTime()`: The round trip ping to the server. Also reactive.
- `TimeSync.isSynced()`: Reactive variable that determines if an initial sync has taken place.
- `TimeSync.resync()`: Re-triggers a sync with the server. Can be useful because the initial sync often takes place during a lot of traffic with the server and could be less accurate.
- `TimeSync.loggingEnabled`: defaults to `true`, set this to `false` to suppress diagnostic syncing messages on the client.
To use the above functions in a non-reactive context, use [`Deps.nonreactive`](http://docs.meteor.com/#deps_nonreactive). This is useful if you are displaying a lot of timestamps or differences on a page and you don't want them to be constantly recomputed on the client. However, displaying time reactively should be pretty efficient with Meteor 0.8.0+ (Blaze).
Note that `TimeSync.serverTime` returns a timestamp, not a `Date`, but you can easily construct a date with `new Date(TimeSync.serverTime(...))`.
You can also use something like `TimeSync.serverTime(null, 5000)` to get a reactive time value that only updates at 5 second intervals. All reactive time variables with the same value of `updateInterval` are guaranteed to be invalidated at the same time.
## Notes
- This library is a crude approximation of NTP, at the moment. It's empirically shown to be accurate to under 100 ms on the meteor.com servers.
- We could definitely do something smarter and more accurate, with multiple measurements and exponentially weighted updating.
- Check out the moment library [packaged for meteor](https://github.com/acreeger/meteor-moment) for formatting and displaying the differences computed by this package.

@ -0,0 +1,38 @@
Package.describe({
name: "mizzao:timesync",
summary: "NTP-style time synchronization between server and client",
version: "0.3.4",
git: "https://github.com/mizzao/meteor-timesync.git"
});
Package.onUse(function (api) {
api.versionsFrom("1.2.0.1");
api.use([
'check',
'tracker',
'http'
], 'client');
api.use('webapp', 'server');
// Our files
api.addFiles('timesync-server.js', 'server');
api.addFiles('timesync-client.js', 'client');
api.export('TimeSync', 'client');
api.export('SyncInternals', 'client', {testOnly: true} );
});
Package.onTest(function (api) {
api.use([
'tinytest',
'test-helpers'
]);
api.use(["tracker", "underscore"], 'client');
api.use("mizzao:timesync");
api.addFiles('tests/client.js', 'client');
});

@ -0,0 +1,121 @@
Tinytest.add("timesync - tick check - normal tick", function(test) {
var lastTime = 5000;
var currentTime = 6000;
var interval = 1000;
var tolerance = 1000;
test.equal(SyncInternals.timeCheck(lastTime, currentTime, interval, tolerance), true);
});
Tinytest.add("timesync - tick check - slightly off", function(test) {
var lastTime = 5000;
var currentTime = 6500;
var interval = 1000;
var tolerance = 1000;
test.equal(SyncInternals.timeCheck(lastTime, currentTime, interval, tolerance), true);
currentTime = 5500;
test.equal(SyncInternals.timeCheck(lastTime, currentTime, interval, tolerance), true);
});
Tinytest.add("timesync - tick check - big jump", function(test) {
var lastTime = 5000;
var currentTime = 0;
var interval = 1000;
var tolerance = 1000;
test.equal(SyncInternals.timeCheck(lastTime, currentTime, interval, tolerance), false);
currentTime = 10000;
test.equal(SyncInternals.timeCheck(lastTime, currentTime, interval, tolerance), false);
});
/*
TODO: add tests for proper dependencies in reactive functions
*/
Tinytest.addAsync("timesync - basic - initial sync", function(test, next) {
function success() {
var syncedTime = TimeSync.serverTime();
// Make sure the time exists
test.isTrue(syncedTime);
// Make sure it's close to the current time on the client. This should
// always be true in PhantomJS tests where client/server are the same
// machine, although it might fail in development environments, for example
// when the server and client are different VMs.
test.isTrue( Math.abs(syncedTime - Date.now()) < 1000 );
next();
}
function fail() {
test.fail();
next();
}
simplePoll(TimeSync.isSynced, success, fail, 5000, 100);
});
Tinytest.addAsync("timesync - basic - serverTime format", function(test, next) {
test.isTrue(_.isNumber( TimeSync.serverTime() ));
test.isTrue(_.isNumber( TimeSync.serverTime(null) ));
// Accept Date as client time
test.isTrue(_.isNumber( TimeSync.serverTime(new Date()) ));
// Accept epoch as client time
test.isTrue(_.isNumber( TimeSync.serverTime(Date.now()) ));
next();
});
Tinytest.addAsync("timesync - basic - different sync intervals", function(test, next) {
var aCount = 0, bCount = 0, cCount = 0;
var a = Tracker.autorun(function () {
TimeSync.serverTime(null, 500);
aCount++;
});
var b = Tracker.autorun(function () {
TimeSync.serverTime();
bCount++;
});
var c = Tracker.autorun(function () {
TimeSync.serverTime(null, 2000);
cCount++;
});
var testInterval = 5000;
Meteor.setTimeout(function() {
test.equal(aCount, 10); // 0, 500, 1000, 1500 ...
// not going to be 5 since the first tick won't generate this dep
test.equal(bCount, 6);
test.equal(cCount, 3); // 0, 2000, 4000
test.isTrue(SyncInternals.timeTick[500]);
test.isTrue(SyncInternals.timeTick[1000]);
test.isTrue(SyncInternals.timeTick[2000]);
test.equal(Object.keys(SyncInternals.timeTick).length, 3);
a.stop();
b.stop();
c.stop();
next()
}, testInterval);
});

@ -0,0 +1,175 @@
/* eslint-disable */
//IE8 doesn't have Date.now()
Date.now = Date.now || function() { return +new Date; };
TimeSync = {
loggingEnabled: true
};
function log(/* arguments */) {
if (TimeSync.loggingEnabled) {
Meteor._debug.apply(this, arguments);
}
}
var defaultInterval = 1000;
// Internal values, exported for testing
SyncInternals = {
offset: undefined,
roundTripTime: undefined,
offsetDep: new Deps.Dependency(),
timeTick: {},
timeCheck: function (lastTime, currentTime, interval, tolerance) {
if (Math.abs(currentTime - lastTime - interval) < tolerance) {
// Everything is A-OK
return true;
}
// We're no longer in sync.
return false;
}
};
SyncInternals.timeTick[defaultInterval] = new Deps.Dependency();
var maxAttempts = 5;
var attempts = 0;
/*
This is an approximation of
http://en.wikipedia.org/wiki/Network_Time_Protocol
If this turns out to be more accurate under the connect handlers,
we should try taking multiple measurements.
*/
// Only use Meteor.absoluteUrl for Cordova; see
// https://github.com/meteor/meteor/issues/4696
// https://github.com/mizzao/meteor-timesync/issues/30
var syncUrl = "/_timesync";
if (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX) {
syncUrl = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + syncUrl;
}
if (Meteor.isCordova) {
syncUrl = Meteor.absoluteUrl("_timesync");
}
var updateOffset = function() {
var t0 = Date.now();
HTTP.get(syncUrl, function(err, response) {
var t3 = Date.now(); // Grab this now
if (err) {
// We'll still use our last computed offset if is defined
log("Error syncing to server time: ", err);
if (++attempts <= maxAttempts)
Meteor.setTimeout(TimeSync.resync, 1000);
else
log("Max number of time sync attempts reached. Giving up.");
return;
}
attempts = 0; // It worked
var ts = parseInt(response.content);
SyncInternals.offset = Math.round(((ts - t0) + (ts - t3)) / 2);
SyncInternals.roundTripTime = t3 - t0; // - (ts - ts) which is 0
SyncInternals.offsetDep.changed();
});
};
// Reactive variable for server time that updates every second.
TimeSync.serverTime = function(clientTime, interval) {
check(interval, Match.Optional(Match.Integer));
// If we don't know the offset, we can't provide the server time.
if ( !TimeSync.isSynced() ) return undefined;
// If a client time is provided, we don't need to depend on the tick.
if ( !clientTime ) getTickDependency(interval || defaultInterval).depend();
// SyncInternals.offsetDep.depend(); implicit as we call isSynced()
// Convert Date argument to epoch as necessary
return (+clientTime || Date.now()) + SyncInternals.offset;
};
// Reactive variable for the difference between server and client time.
TimeSync.serverOffset = function() {
SyncInternals.offsetDep.depend();
return SyncInternals.offset;
};
TimeSync.roundTripTime = function() {
SyncInternals.offsetDep.depend();
return SyncInternals.roundTripTime;
};
TimeSync.isSynced = function() {
SyncInternals.offsetDep.depend();
return SyncInternals.offset !== undefined;
};
var resyncIntervalId = null;
TimeSync.resync = function() {
if (resyncIntervalId !== null) Meteor.clearInterval(resyncIntervalId);
updateOffset();
resyncIntervalId = Meteor.setInterval(updateOffset, 600000);
};
// Run this as soon as we load, even before Meteor.startup()
// Run again whenever we reconnect after losing connection
var wasConnected = false;
Deps.autorun(function() {
var connected = Meteor.status().connected;
if ( connected && !wasConnected ) TimeSync.resync();
wasConnected = connected;
});
// Resync if unexpected change by more than a few seconds. This needs to be
// somewhat lenient, or a CPU-intensive operation can trigger a re-sync even
// when the offset is still accurate. In any case, we're not going to be able to
// catch very small system-initiated NTP adjustments with this, anyway.
var tickCheckTolerance = 5000;
var lastClientTime = Date.now();
// Set up a new interval for any amount of reactivity.
function getTickDependency(interval) {
if ( !SyncInternals.timeTick[interval] ) {
var dep = new Deps.Dependency();
Meteor.setInterval(function() {
dep.changed();
}, interval);
SyncInternals.timeTick[interval] = dep;
}
return SyncInternals.timeTick[interval];
}
// Set up special interval for the default tick, which also watches for re-sync
Meteor.setInterval(function() {
var currentClientTime = Date.now();
if ( SyncInternals.timeCheck(
lastClientTime, currentClientTime, defaultInterval, tickCheckTolerance) ) {
// No problem here, just keep ticking along
SyncInternals.timeTick[defaultInterval].changed();
}
else {
// resync on major client clock changes
// based on http://stackoverflow.com/a/3367542/1656818
log("Clock discrepancy detected. Attempting re-sync.");
// Refuse to compute server time.
SyncInternals.offset = undefined;
SyncInternals.offsetDep.changed();
TimeSync.resync();
}
lastClientTime = currentClientTime;
}, defaultInterval);

@ -0,0 +1,28 @@
/* eslint-disable */
// Use rawConnectHandlers so we get a response as quickly as possible
// https://github.com/meteor/meteor/blob/devel/packages/webapp/webapp_server.js
var syncUrl = "/_timesync";
if (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX) {
syncUrl = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + syncUrl;
}
WebApp.rawConnectHandlers.use(syncUrl,
function(req, res, next) {
// Never ever cache this, otherwise weird times are shown on reload
// http://stackoverflow.com/q/18811286/586086
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", 0);
// Avoid MIME type warnings in browsers
res.setHeader("Content-Type", "text/plain");
// Cordova lives in meteor.local, so it does CORS
if (req.headers && req.headers.origin === 'http://meteor.local') {
res.setHeader('Access-Control-Allow-Origin', 'http://meteor.local');
}
res.end(Date.now().toString());
}
);
Loading…
Cancel
Save