RocketChat/Rocket.Chat

View on GitHub
apps/meteor/packages/autoupdate/autoupdate_server.js

Summary

Maintainability
A
1 hr
Test Coverage
// Publish the current client versions for each client architecture
// (web.browser, web.browser.legacy, web.cordova). When a client observes
// a change in the versions associated with its client architecture,
// it will refresh itself, either by swapping out CSS assets or by
// reloading the page. Changes to the replaceable version are ignored
// and handled by the hot-module-replacement package.
//
// There are four versions for any given client architecture: `version`,
// `versionRefreshable`, `versionNonRefreshable`, and
// `versionReplaceable`. The refreshable version is a hash of just the
// client resources that are refreshable, such as CSS. The replaceable
// version is a hash of files that can be updated with HMR. The
// non-refreshable version is a hash of the rest of the client assets,
// excluding the refreshable ones: HTML, JS that is not replaceable, and
// static files in the `public` directory. The `version` version is a
// combined hash of everything.
//
// If the environment variable `AUTOUPDATE_VERSION` is set, it will be
// used in place of all client versions. You can use this variable to
// control when the client reloads. For example, if you want to force a
// reload only after major changes, use a custom AUTOUPDATE_VERSION and
// change it only when something worth pushing to clients happens.
//
// The server publishes a `meteor_autoupdate_clientVersions` collection.
// The ID of each document is the client architecture, and the fields of
// the document are the versions described above.

import { ClientVersions } from './client_versions.js';
var Future = Npm.require('fibers/future');

export const Autoupdate = (__meteor_runtime_config__.autoupdate = {
    // Map from client architectures (web.browser, web.browser.legacy,
    // web.cordova) to version fields { version, versionRefreshable,
    // versionNonRefreshable, refreshable } that will be stored in
    // ClientVersions documents (whose IDs are client architectures). This
    // data gets serialized into the boilerplate because it's stored in
    // __meteor_runtime_config__.autoupdate.versions.
    versions: {},
});

// Stores acceptable client versions.
const clientVersions = new ClientVersions();

// The client hash includes __meteor_runtime_config__, so wait until
// all packages have loaded and have had a chance to populate the
// runtime config before using the client hash as our default auto
// update version id.

// Note: Tests allow people to override Autoupdate.autoupdateVersion before
// startup.
Autoupdate.autoupdateVersion = null;
Autoupdate.autoupdateVersionRefreshable = null;
Autoupdate.autoupdateVersionCordova = null;
Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID;

var syncQueue = new Meteor._SynchronousQueue();

function updateVersions(shouldReloadClientProgram) {
    // Step 1: load the current client program on the server
    if (shouldReloadClientProgram) {
        WebAppInternals.reloadClientPrograms();
    }

    const {
        // If the AUTOUPDATE_VERSION environment variable is defined, it takes
        // precedence, but Autoupdate.autoupdateVersion is still supported as
        // a fallback. In most cases neither of these values will be defined.
        AUTOUPDATE_VERSION = Autoupdate.autoupdateVersion,
    } = process.env;

    // Step 2: update __meteor_runtime_config__.autoupdate.versions.
    const clientArchs = Object.keys(WebApp.clientPrograms);
    clientArchs.forEach((arch) => {
        Autoupdate.versions[arch] = {
            version: AUTOUPDATE_VERSION || WebApp.calculateClientHash(arch),
            versionRefreshable: AUTOUPDATE_VERSION || WebApp.calculateClientHashRefreshable(arch),
            versionNonRefreshable: AUTOUPDATE_VERSION || WebApp.calculateClientHashNonRefreshable(arch),
            versionReplaceable: AUTOUPDATE_VERSION || WebApp.calculateClientHashReplaceable(arch),
            versionHmr: WebApp.clientPrograms[arch].hmrVersion,
        };
    });

    // Step 3: form the new client boilerplate which contains the updated
    // assets and __meteor_runtime_config__.
    if (shouldReloadClientProgram) {
        WebAppInternals.generateBoilerplate();
    }

    // Step 4: update the ClientVersions collection.
    // We use `onListening` here because we need to use
    // `WebApp.getRefreshableAssets`, which is only set after
    // `WebApp.generateBoilerplate` is called by `main` in webapp.
    WebApp.onListening(() => {
        clientArchs.forEach((arch) => {
            const payload = {
                ...Autoupdate.versions[arch],
                assets: WebApp.getRefreshableAssets(arch),
            };

            clientVersions.set(arch, payload);
        });
    });
}

Meteor.publish(
    'meteor_autoupdate_clientVersions',
    function (appId) {
        // `null` happens when a client doesn't have an appId and passes
        // `undefined` to `Meteor.subscribe`. `undefined` is translated to
        // `null` as JSON doesn't have `undefined.
        check(appId, Match.OneOf(String, undefined, null));

        // Don't notify clients using wrong appId such as mobile apps built with a
        // different server but pointing at the same local url
        if (Autoupdate.appId && appId && Autoupdate.appId !== appId) return [];

        // Random value to delay the updates for 2-10 minutes
        const randomInterval = Meteor.isProduction ? (Math.floor(Math.random() * 8) + 2) * 1000 * 60 : 0;

        const stop = clientVersions.watch((version, isNew) => {
            setTimeout(() => {
                (isNew ? this.added : this.changed).call(this, 'meteor_autoupdate_clientVersions', version._id, version);
            }, randomInterval);
        });

        this.onStop(() => stop());
        this.ready();
    },
    { is_auto: true },
);

Meteor.startup(function () {
    updateVersions(false);

    // Force any connected clients that are still looking for these older
    // document IDs to reload.
    ['version', 'version-refreshable', 'version-cordova'].forEach((_id) => {
        clientVersions.set(_id, {
            version: 'outdated',
        });
    });
});

var fut = new Future();

// We only want 'refresh' to trigger 'updateVersions' AFTER onListen,
// so we add a queued task that waits for onListen before 'refresh' can queue
// tasks. Note that the `onListening` callbacks do not fire until after
// Meteor.startup, so there is no concern that the 'updateVersions' calls from
// 'refresh' will overlap with the `updateVersions` call from Meteor.startup.

syncQueue.queueTask(function () {
    fut.wait();
});

WebApp.onListening(function () {
    fut.return();
});

function enqueueVersionsRefresh() {
    syncQueue.queueTask(function () {
        updateVersions(true);
    });
}

// Listen for messages pertaining to the client-refresh topic.
import { onMessage } from 'meteor/inter-process-messaging';
onMessage('client-refresh', enqueueVersionsRefresh);

// Another way to tell the process to refresh: send SIGHUP signal
process.on(
    'SIGHUP',
    Meteor.bindEnvironment(function () {
        enqueueVersionsRefresh();
    }, 'handling SIGHUP signal for refresh'),
);