Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.6.0 refactor #56

Merged
merged 14 commits into from
Aug 19, 2024
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test suite
on:
push:
branches:
- master
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
meteorRelease:
- '--release 1.12.1'
- '--release 2.3'
- '--release 2.8.1'
- '--release 2.16'
# Latest version
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '16.x'

- name: Install Dependencies
run: |
curl https://install.meteor.com | /bin/sh
npm i -g @zodern/mtest
- name: Run Tests
run: |
mtest --package ./ --once ${{ matrix.meteorRelease }}
6 changes: 0 additions & 6 deletions .travis.yml

This file was deleted.

18 changes: 16 additions & 2 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
## vNEXT

## v0.6.0

- Code Format Refactor
- Changed Deps to Tracker (#49)
- Only show log output if running in development
- Added _timeSync Meteor Method for doing timesync over DDP instead of HTTP
- Auto switch to DDP after initial HTTP timesync to improve subsequent round trip times
- Added option TimeSync.forceDDP to always use DDP, even for first sync (which may be slow!)
- Shortened resync interval from 1 minute to 30 seconds when using DDP.
- Added tests for DDP and HTTP sync
- Added option to set the timesync URL using `TimeSync.setSyncUrl`
- Removed IE8 compat function

## v0.5.5

- Added compatibility for Meteor 3.0-beta.7
Expand Down Expand Up @@ -32,11 +46,11 @@

## v0.3.4

- Explicitly pull in client-side `check` for Meteor 1.2 apps.
- 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)
- Be more robust with sync url when outside of Cordova. (#30)

## v0.3.2

Expand Down
6 changes: 6 additions & 0 deletions client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TimeSync, SyncInternals } from './timesync-client';

export {
TimeSync,
SyncInternals,
};
194 changes: 194 additions & 0 deletions client/timesync-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { HTTP } from 'meteor/http';

TimeSync = {
loggingEnabled: Meteor.isDevelopment,
forceDDP: false
};

function log( /* arguments */ ) {
if (TimeSync.loggingEnabled) {
Meteor._debug.apply(this, arguments);
}
}

const defaultInterval = 1000;

// Internal values, exported for testing
SyncInternals = {
offset: undefined,
roundTripTime: undefined,
offsetTracker: new Tracker.Dependency(),
syncTracker: new Tracker.Dependency(),
isSynced: false,
usingDDP: false,
timeTick: {},
getDiscrepancy: function (lastTime, currentTime, interval) {
return currentTime - (lastTime + interval)
}
};

SyncInternals.timeTick[defaultInterval] = new Tracker.Dependency();

const maxAttempts = 5;
let 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.
*/

let syncUrl;

TimeSync.setSyncUrl = function (url) {
if (url) {
syncUrl = url;
} else if (Meteor.isCordova || Meteor.isDesktop) {
// Only use Meteor.absoluteUrl for Cordova and Desktop; see
// https://github.com/meteor/meteor/issues/4696
// https://github.com/mizzao/meteor-timesync/issues/30
// Cordova should never be running out of a subdirectory...
syncUrl = Meteor.absoluteUrl('_timesync');
} else {
// Support Meteor running in relative paths, based on computed root url prefix
// https://github.com/mizzao/meteor-timesync/pull/40
const basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
syncUrl = basePath + '/_timesync';
}
};
TimeSync.getSyncUrl = function () {
return syncUrl;
}
TimeSync.setSyncUrl();

const updateOffset = function () {
const t0 = Date.now();
if (TimeSync.forceDDP || SyncInternals.useDDP) {
Meteor.call('_timeSync', function (err, res) {
handleResponse(t0, err, res);
});
} else {
HTTP.get(syncUrl, function (err, res) {
handleResponse(t0, err, res);
});
}
};

const handleResponse = function (t0, err, res) {
const 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
const response = res.content || res;
const ts = parseInt(response, 10);
SyncInternals.isSynced = true;
SyncInternals.offset = Math.round(((ts - t0) + (ts - t3)) / 2);
SyncInternals.roundTripTime = t3 - t0; // - (ts - ts) which is 0
SyncInternals.offsetTracker.changed();
}

// Reactive variable for server time that updates every second.
TimeSync.serverTime = function (clientTime, interval) {
check(interval, Match.Optional(Match.Integer));
// If a client time is provided, we don't need to depend on the tick.
if (!clientTime) getTickDependency(interval || defaultInterval).depend();

SyncInternals.offsetTracker.depend(); // depend on offset to enable reactivity
// 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.offsetTracker.depend();
return SyncInternals.offset;
};

TimeSync.roundTripTime = function () {
SyncInternals.offsetTracker.depend();
return SyncInternals.roundTripTime;
};

TimeSync.isSynced = function () {
SyncInternals.offsetTracker.depend();
return SyncInternals.isSynced;
};

let resyncIntervalId = null;

TimeSync.resync = function () {
if (resyncIntervalId !== null) Meteor.clearInterval(resyncIntervalId);

updateOffset();
resyncIntervalId = Meteor.setInterval(updateOffset, (SyncInternals.useDDP) ? 300000 : 600000);
};

// Run this as soon as we load, even before Meteor.startup()
// Run again whenever we reconnect after losing connection
let wasConnected = false;

Tracker.autorun(function () {
const connected = Meteor.status().connected;
if (connected && !wasConnected) TimeSync.resync();
wasConnected = connected;
SyncInternals.useDDP = 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.
const tickCheckTolerance = 5000;

let lastClientTime = Date.now();

// Set up a new interval for any amount of reactivity.
function getTickDependency(interval) {

if (!SyncInternals.timeTick[interval]) {
const dep = new Tracker.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 () {
const currentClientTime = Date.now();
const discrepancy = SyncInternals.getDiscrepancy(lastClientTime, currentClientTime, defaultInterval);

if (Math.abs(discrepancy) < 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 and try to guess new server offset. Guessing only works if the server time hasn't changed.
SyncInternals.offset = SyncInternals.offset - discrepancy;
SyncInternals.isSynced = false;
SyncInternals.offsetTracker.changed();
TimeSync.resync();
}

lastClientTime = currentClientTime;
}, defaultInterval);
25 changes: 14 additions & 11 deletions package.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
Package.describe({
name: "mizzao:timesync",
summary: "NTP-style time synchronization between server and client",
version: "0.5.5",
name: 'mizzao:timesync',
summary: 'NTP-style time synchronization between server and client',
version: '0.6.0',
git: "https://github.com/Meteor-Community-Packages/meteor-timesync"
});

Package.onUse(function (api) {
api.versionsFrom(["1.12", "2.3", '3.0-beta.7']);
api.versionsFrom(["1.12", "2.3"]);

api.use([
'check',
'tracker',
'http'
], 'client');

api.use('webapp', 'server');
api.use(['webapp'], 'server');

api.use('ecmascript');
api.use(['ecmascript']);

// Our files
api.addFiles('timesync-server.js', 'server');
api.addFiles('timesync-client.js', 'client');
api.addFiles('server/index.js', 'server');
api.addFiles('client/index.js', 'client');

api.export('TimeSync', 'client');
api.export('SyncInternals', 'client', {testOnly: true} );
api.export('SyncInternals', 'client', {
testOnly: true
});
});

Package.onTest(function (api) {
api.use([
'ecmascript',
'tinytest',
'test-helpers'
]);

api.use(["tracker", "underscore"], 'client');
api.use(['tracker', 'underscore'], 'client');

api.use("mizzao:timesync");
api.use('mizzao:timesync');

api.addFiles('tests/client.js', 'client');
});
1 change: 1 addition & 0 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './timesync-server';
19 changes: 14 additions & 5 deletions timesync-server.js → server/timesync-server.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Meteor } from "meteor/meteor";

// Use rawConnectHandlers so we get a response as quickly as possible
// https://github.com/meteor/meteor/blob/devel/packages/webapp/webapp_server.js

const url = new URL(Meteor.absoluteUrl("/_timesync"));

WebApp.rawConnectHandlers.use(url.pathname,
function(req, res, next) {
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);
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");
res.setHeader('Content-Type', 'text/plain');

// Cordova lives in a local webserver, so it does CORS
// we need to bless it's requests in order for it to accept our results
Expand All @@ -30,3 +32,10 @@ WebApp.rawConnectHandlers.use(url.pathname,
res.end(Date.now().toString());
}
);

Meteor.methods({
_timeSync: function () {
this.unblock();
return Date.now();
}
});
Loading