Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,33 @@ jobs:
strategy:
matrix:
meteorRelease:
- '--release 1.12.1'
- '--release 2.3'
- '--release 2.8.1'
- '--release 2.16'
- '3.0.1'
- '3.4'
# Latest version
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

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

- name: Setup meteor ${{ matrix.meteorRelease }}
uses: meteorengineer/setup-meteor@v1
with:
meteor-release: ${{ matrix.meteorRelease }}

- name: cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-22-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-22-

- 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 }}
cd test-proxy
npm ci
npm run test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.build*
.versions

.idea/
.vscode
.DS_Store
node_modules/
61 changes: 61 additions & 0 deletions .versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
allow-deny@2.1.0
babel-compiler@7.13.0
babel-runtime@1.5.2
base64@1.0.13
binary-heap@1.0.12
blaze@3.0.2
boilerplate-generator@2.1.0
callback-hook@1.6.1
check@1.5.0
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.1.1
ddp-common@1.4.4
ddp-server@3.1.2
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.17.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.12.3
ecmascript-runtime-server@0.11.1
ejson@1.1.5
facts-base@1.0.2
fetch@0.1.6
geojson-utils@1.0.12
htmljs@2.0.1
id-map@1.2.0
inter-process-messaging@0.1.2
jquery@1.11.11
local-test:mizzao:timesync@1.0.0-beta.2
logging@1.3.6
meteor@2.2.0
meteortesting:browser-tests@0.1.2
meteortesting:mocha@0.4.4
minimongo@2.0.5
mizzao:timesync@1.0.0-beta.2
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.2.0
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@6.16.1
observe-sequence@2.0.0
ordered-dict@1.2.0
practicalmeteor:mocha-core@1.0.1
promise@1.0.0
random@1.2.2
react-fast-refresh@0.3.0
reactive-var@1.0.13
reload@1.3.2
retry@1.1.1
routepolicy@1.1.2
socket-stream-client@0.6.1
test-helpers@2.0.3
tinytest@1.3.2
tracker@1.3.4
typescript@5.9.3
url@1.3.5
webapp@2.1.0
webapp-hashing@1.1.2
10 changes: 10 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
## vNEXT

## v1.0.0

** BREAKING **

- Upgraded to Meteor 3 only, with no backwards compatibility for Meteor 2.
- Switched package usage to Meteor 3 module imports, for example `import { TimeSync } from 'meteor/mizzao:timesync'`.
- Kept regular browser clients on HTTP timesync by default to avoid DDP method queueing delays.
- Forced DDP timesync for Cordova and Capacitor clients where HTTP sync can fail in mobile webviews.
- Migrated package tests to Mocha/Puppeteer and GitHub Actions for Meteor 3.

## v0.7.0

- Replace `HTTP` package and use with `fetch`
Expand Down
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
meteor-timesync [![Build Status](https://travis-ci.org/mizzao/meteor-timesync.svg?branch=master)](https://travis-ci.org/mizzao/meteor-timesync)
===============
# Meteor Timesync
[![Meteor Community Package](https://img.shields.io/badge/Meteor-Package-green?logo=meteor&logoColor=white)](https://meteor.com)
[![Test suite](https://github.com/Meteor-Community-Packages/meteor-timesync/actions/workflows/tests.yml/badge.svg)](https://github.com/Meteor-Community-Packages/meteor-timesync/actions/workflows/tests.yml)
[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
![GitHub License](https://img.shields.io/github/license/Meteor-Community-Packages/meteor-timesync)

NTP-style time synchronization between server and client, and facilities to
use server time reactively in Meteor applications.
Expand All @@ -8,24 +11,27 @@ use server time reactively in Meteor applications.

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

```
```shell
meteor add mizzao:timesync
```

## Usage

```js
import { TimeSync } from 'meteor/mizzao:timesync';
```


- `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.
- `TimeSync.loggingEnabled`: defaults to `true` in development, 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).
To use the above functions in a non-reactive context, use [`Tracker.nonreactive`](https://docs.meteor.com/api/tracker.html#Tracker-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.

Note that `TimeSync.serverTime` returns a timestamp, not a `Date`, but you can easily construct a date with `new Date(TimeSync.serverTime(...))`.

Expand All @@ -36,3 +42,17 @@ You can also use something like `TimeSync.serverTime(null, 5000)` to get a react
- 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.

## Tests

To run the test suite, clone this repository and run:

```shell
cd test-proxy
npm ci
npm test
```

## License

MIT, see [LICENSE](./LICENSE) file.
40 changes: 27 additions & 13 deletions client/timesync-client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { fetch } from 'meteor/fetch';
import { check, Match } from 'meteor/check';

TimeSync = {
export const TimeSync = {
loggingEnabled: Meteor.isDevelopment,
forceDDP: false
};
Expand All @@ -16,7 +17,7 @@ function log( /* arguments */ ) {
const defaultInterval = 1000;

// Internal values, exported for testing
SyncInternals = {
export const SyncInternals = {
offset: undefined,
roundTripTime: undefined,
offsetTracker: new Tracker.Dependency(),
Expand All @@ -33,6 +34,9 @@ SyncInternals.timeTick[defaultInterval] = new Tracker.Dependency();

const maxAttempts = 5;
let attempts = 0;
let syncInFlight = false;
const ddpResyncInterval = 300000;
const httpResyncInterval = 600000;

/*
This is an approximation of
Expand All @@ -57,7 +61,7 @@ TimeSync.setSyncUrl = function (url) {
// 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';
syncUrl = `${basePath}/_timesync`;
}
};
TimeSync.getSyncUrl = function () {
Expand All @@ -66,19 +70,28 @@ TimeSync.getSyncUrl = function () {
TimeSync.setSyncUrl();

const updateOffset = function () {
if (syncInFlight) return;

syncInFlight = true;
const t0 = Date.now();
if (TimeSync.forceDDP || SyncInternals.useDDP) {
Meteor.call('_timeSync', function (err, res) {
handleResponse(t0, err, res);
});
} else {
fetch(syncUrl, { method: 'GET', cache: 'no-cache' })
.then(res => res.json())
const request = useDDPTransport()
? Meteor.callAsync('_timeSync')
.then((res) => handleResponse(t0, null, res))
.catch((err) => handleResponse(t0, err, null));
}
: fetch(syncUrl, { method: 'GET', cache: 'no-cache' })
.then(res => res.text())
.then((res) => handleResponse(t0, null, res));

request
.catch((err) => handleResponse(t0, err, null))
.finally(() => {
syncInFlight = false;
});
};

function useDDPTransport() {
return Meteor.isCordova || TimeSync.forceDDP;
}

const handleResponse = function (t0, err, res) {
const t3 = Date.now(); // Grab this now
if (err) {
Expand Down Expand Up @@ -132,8 +145,9 @@ let resyncIntervalId = null;
TimeSync.resync = function () {
if (resyncIntervalId !== null) Meteor.clearInterval(resyncIntervalId);

const interval = useDDPTransport() ? ddpResyncInterval : httpResyncInterval;
updateOffset();
resyncIntervalId = Meteor.setInterval(updateOffset, (SyncInternals.useDDP) ? 300000 : 600000);
resyncIntervalId = Meteor.setInterval(updateOffset, interval);
};

// Run this as soon as we load, even before Meteor.startup()
Expand Down
22 changes: 9 additions & 13 deletions package.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
Package.describe({
name: 'mizzao:timesync',
summary: 'NTP-style time synchronization between server and client',
version: '0.7.0',
version: '1.0.0',
git: "https://github.com/Meteor-Community-Packages/meteor-timesync"
});

Package.onUse(function (api) {
api.versionsFrom(["1.12", "2.3"]);
api.versionsFrom(["3.0.1", '3.4']);

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

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

api.use(['ecmascript']);

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

api.export('TimeSync', 'client');
api.export('SyncInternals', 'client', {
testOnly: true
});
api.mainModule('server/index.js', 'server');
api.mainModule('client/index.js', 'client');
});

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

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

api.use('mizzao:timesync');


api.addFiles('tests/client.js', 'client');
});
13 changes: 7 additions & 6 deletions server/timesync-server.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Meteor } from "meteor/meteor";
import { URL } from 'meteor/url';

// 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,
WebApp.handlers.use(url.pathname,
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.append('Cache-Control', 'no-cache, no-store, must-revalidate');
res.append('Pragma', 'no-cache');
res.append('Expires', 0);

// Avoid MIME type warnings in browsers
res.setHeader('Content-Type', 'text/plain');
res.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 @@ -26,7 +27,7 @@ WebApp.rawConnectHandlers.use(url.pathname,
origin === 'capacitor://meteor.local' ||
origin === 'meteor://desktop' ||
/^http:\/\/localhost:1[23]\d\d\d$/.test(origin) ) ) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.append('Access-Control-Allow-Origin', origin);
}

res.end(Date.now().toString());
Expand Down
1 change: 1 addition & 0 deletions test-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
19 changes: 19 additions & 0 deletions test-proxy/.meteor/.finished-upgraders
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This file contains information which helps Meteor properly upgrade your
# app when you run 'meteor update'. You should check it into version control
# with your project.

notices-for-0.9.0
notices-for-0.9.1
0.9.4-platform-file
notices-for-facebook-graph-api-2
1.2.0-standard-minifiers-package
1.2.0-meteor-platform-split
1.2.0-cordova-changes
1.2.0-breaking-changes
1.3.0-split-minifiers-package
1.4.0-remove-old-dev-bundle-link
1.4.1-add-shell-server-package
1.4.3-split-account-service-packages
1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base
1.8.3-split-jquery-from-blaze
1 change: 1 addition & 0 deletions test-proxy/.meteor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
local
Loading
Loading