Skip to content

Commit

Permalink
Merge pull request #56 from Meteor-Community-Packages/0.6.0-refactor
Browse files Browse the repository at this point in the history
0.6.0 refactor
  • Loading branch information
StorytellerCZ authored Aug 19, 2024
2 parents ff79088 + 623804e commit be2bf28
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 222 deletions.
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

0 comments on commit be2bf28

Please sign in to comment.