Skip to content

Commit be2bf28

Browse files
Merge pull request #56 from Meteor-Community-Packages/0.6.0-refactor
0.6.0 refactor
2 parents ff79088 + 623804e commit be2bf28

File tree

10 files changed

+349
-222
lines changed

10 files changed

+349
-222
lines changed

.github/workflows/tests.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Test suite
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
meteorRelease:
14+
- '--release 1.12.1'
15+
- '--release 2.3'
16+
- '--release 2.8.1'
17+
- '--release 2.16'
18+
# Latest version
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Install Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '16.x'
27+
28+
- name: Install Dependencies
29+
run: |
30+
curl https://install.meteor.com | /bin/sh
31+
npm i -g @zodern/mtest
32+
- name: Run Tests
33+
run: |
34+
mtest --package ./ --once ${{ matrix.meteorRelease }}

.travis.yml

Lines changed: 0 additions & 6 deletions
This file was deleted.

History.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
## vNEXT
2+
3+
## v0.6.0
4+
5+
- Code Format Refactor
6+
- Changed Deps to Tracker (#49)
7+
- Only show log output if running in development
8+
- Added _timeSync Meteor Method for doing timesync over DDP instead of HTTP
9+
- Auto switch to DDP after initial HTTP timesync to improve subsequent round trip times
10+
- Added option TimeSync.forceDDP to always use DDP, even for first sync (which may be slow!)
11+
- Shortened resync interval from 1 minute to 30 seconds when using DDP.
12+
- Added tests for DDP and HTTP sync
13+
- Added option to set the timesync URL using `TimeSync.setSyncUrl`
14+
- Removed IE8 compat function
15+
216
## v0.5.5
317

418
- Added compatibility for Meteor 3.0-beta.7
@@ -32,11 +46,11 @@
3246

3347
## v0.3.4
3448

35-
- Explicitly pull in client-side `check` for Meteor 1.2 apps.
49+
- Explicitly pull in client-side `check` for Meteor 1.2 apps.
3650

3751
## v0.3.3
3852

39-
- Be more robust with sync url when outside of Cordova. (#30)
53+
- Be more robust with sync url when outside of Cordova. (#30)
4054

4155
## v0.3.2
4256

client/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { TimeSync, SyncInternals } from './timesync-client';
2+
3+
export {
4+
TimeSync,
5+
SyncInternals,
6+
};

client/timesync-client.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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);

package.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
11
Package.describe({
2-
name: "mizzao:timesync",
3-
summary: "NTP-style time synchronization between server and client",
4-
version: "0.5.5",
2+
name: 'mizzao:timesync',
3+
summary: 'NTP-style time synchronization between server and client',
4+
version: '0.6.0',
55
git: "https://github.com/Meteor-Community-Packages/meteor-timesync"
66
});
77

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

1111
api.use([
1212
'check',
1313
'tracker',
1414
'http'
1515
], 'client');
1616

17-
api.use('webapp', 'server');
17+
api.use(['webapp'], 'server');
1818

19-
api.use('ecmascript');
19+
api.use(['ecmascript']);
2020

2121
// Our files
22-
api.addFiles('timesync-server.js', 'server');
23-
api.addFiles('timesync-client.js', 'client');
22+
api.addFiles('server/index.js', 'server');
23+
api.addFiles('client/index.js', 'client');
2424

2525
api.export('TimeSync', 'client');
26-
api.export('SyncInternals', 'client', {testOnly: true} );
26+
api.export('SyncInternals', 'client', {
27+
testOnly: true
28+
});
2729
});
2830

2931
Package.onTest(function (api) {
3032
api.use([
33+
'ecmascript',
3134
'tinytest',
3235
'test-helpers'
3336
]);
3437

35-
api.use(["tracker", "underscore"], 'client');
38+
api.use(['tracker', 'underscore'], 'client');
3639

37-
api.use("mizzao:timesync");
40+
api.use('mizzao:timesync');
3841

3942
api.addFiles('tests/client.js', 'client');
4043
});

server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './timesync-server';

timesync-server.js renamed to server/timesync-server.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
import { Meteor } from "meteor/meteor";
2+
13
// Use rawConnectHandlers so we get a response as quickly as possible
24
// https://github.com/meteor/meteor/blob/devel/packages/webapp/webapp_server.js
35

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

68
WebApp.rawConnectHandlers.use(url.pathname,
7-
function(req, res, next) {
9+
function (req, res, next) {
810
// Never ever cache this, otherwise weird times are shown on reload
911
// http://stackoverflow.com/q/18811286/586086
10-
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
11-
res.setHeader("Pragma", "no-cache");
12-
res.setHeader("Expires", 0);
12+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
13+
res.setHeader('Pragma', 'no-cache');
14+
res.setHeader('Expires', 0);
1315

1416
// Avoid MIME type warnings in browsers
15-
res.setHeader("Content-Type", "text/plain");
17+
res.setHeader('Content-Type', 'text/plain');
1618

1719
// Cordova lives in a local webserver, so it does CORS
1820
// we need to bless it's requests in order for it to accept our results
@@ -30,3 +32,10 @@ WebApp.rawConnectHandlers.use(url.pathname,
3032
res.end(Date.now().toString());
3133
}
3234
);
35+
36+
Meteor.methods({
37+
_timeSync: function () {
38+
this.unblock();
39+
return Date.now();
40+
}
41+
});

0 commit comments

Comments
 (0)