mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
308 lines
9.2 KiB
JavaScript
308 lines
9.2 KiB
JavaScript
/**
|
|
* This code does _NOT_ support hot (session-restoring) reloads on
|
|
* IE6,7. It only works on browsers with sessionStorage support.
|
|
*
|
|
* There are a couple approaches to add IE6,7 support:
|
|
*
|
|
* - use IE's "userData" mechanism in combination with window.name.
|
|
* This mostly works, however the problem is that it can not get to the
|
|
* data until after DOMReady. This is a problem for us since this API
|
|
* relies on the data being ready before API users run. We could
|
|
* refactor using Meteor.startup in all API users, but that might slow
|
|
* page loads as we couldn't start the stream until after DOMReady.
|
|
* Here are some resources on this approach:
|
|
* https://github.com/hugeinc/USTORE.js
|
|
* http://thudjs.tumblr.com/post/419577524/localstorage-userdata
|
|
* http://www.javascriptkit.com/javatutors/domstorage2.shtml
|
|
*
|
|
* - POST the data to the server, and have the server send it back on
|
|
* page load. This is nice because it sidesteps all the local storage
|
|
* compatibility issues, however it is kinda tricky. We can use a unique
|
|
* token in the URL, then get rid of it with HTML5 pushstate, but that
|
|
* only works on pushstate browsers.
|
|
*
|
|
* This will all need to be reworked entirely when we add server-side
|
|
* HTML rendering. In that case, the server will need to have access to
|
|
* the client's session to render properly.
|
|
*/
|
|
|
|
// XXX when making this API public, also expose a flag for the app
|
|
// developer to know whether a hot code push is happening. This is
|
|
// useful for apps using `window.onbeforeunload`. See
|
|
// https://github.com/meteor/meteor/pull/657
|
|
|
|
export const Reload = {};
|
|
|
|
const reloadSettings =
|
|
(Meteor.settings &&
|
|
Meteor.settings.public &&
|
|
Meteor.settings.public.packages &&
|
|
Meteor.settings.public.packages.reload) ||
|
|
{};
|
|
|
|
function debug(message, context) {
|
|
if (!reloadSettings.debug) {
|
|
return;
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[reload] ${message}`, JSON.stringify(context));
|
|
}
|
|
|
|
const KEY_NAME = 'Meteor_Reload';
|
|
|
|
let old_data = {};
|
|
// read in old data at startup.
|
|
let old_json;
|
|
|
|
// This logic for sessionStorage detection is based on browserstate/history.js
|
|
let safeSessionStorage = null;
|
|
try {
|
|
// This throws a SecurityError on Chrome if cookies & localStorage are
|
|
// explicitly disabled
|
|
//
|
|
// On Firefox with dom.storage.enabled set to false, sessionStorage is null
|
|
//
|
|
// We can't even do (typeof sessionStorage) on Chrome, it throws. So we rely
|
|
// on the throw if sessionStorage == null; the alternative is browser
|
|
// detection, but this seems better.
|
|
safeSessionStorage = window.sessionStorage;
|
|
|
|
// Check we can actually use it
|
|
if (safeSessionStorage) {
|
|
safeSessionStorage.setItem('__dummy__', '1');
|
|
safeSessionStorage.removeItem('__dummy__');
|
|
} else {
|
|
// Be consistently null, for safety
|
|
safeSessionStorage = null;
|
|
}
|
|
} catch (e) {
|
|
// Expected on chrome with strict security, or if sessionStorage not supported
|
|
safeSessionStorage = null;
|
|
}
|
|
|
|
// Exported for test.
|
|
Reload._getData = function () {
|
|
return safeSessionStorage && safeSessionStorage.getItem(KEY_NAME);
|
|
};
|
|
|
|
if (safeSessionStorage) {
|
|
old_json = Reload._getData();
|
|
safeSessionStorage.removeItem(KEY_NAME);
|
|
} else {
|
|
// Unsupported browser (IE 6,7) or locked down security settings.
|
|
// No session resumption.
|
|
// Meteor._debug("XXX UNSUPPORTED BROWSER/SETTINGS");
|
|
}
|
|
|
|
if (!old_json) old_json = '{}';
|
|
let old_parsed = {};
|
|
try {
|
|
old_parsed = JSON.parse(old_json);
|
|
if (typeof old_parsed !== 'object') {
|
|
Meteor._debug('Got bad data on reload. Ignoring.');
|
|
old_parsed = {};
|
|
}
|
|
} catch (err) {
|
|
Meteor._debug('Got invalid JSON on reload. Ignoring.');
|
|
}
|
|
|
|
if (old_parsed.reload && typeof old_parsed.data === 'object') {
|
|
// Meteor._debug("Restoring reload data.");
|
|
old_data = old_parsed.data;
|
|
}
|
|
|
|
let providers = [];
|
|
|
|
////////// External API //////////
|
|
|
|
// Packages that support migration should register themselves by calling
|
|
// this function. When it's time to migrate, callback will be called
|
|
// with one argument, the "retry function," and an optional 'option'
|
|
// argument (containing a key 'immediateMigration'). If the package
|
|
// is ready to migrate, it should return [true, data], where data is
|
|
// its migration data, an arbitrary JSON value (or [true] if it has
|
|
// no migration data this time). If the package needs more time
|
|
// before it is ready to migrate, it should return false. Then, once
|
|
// it is ready to migrating again, it should call the retry
|
|
// function. The retry function will return immediately, but will
|
|
// schedule the migration to be retried, meaning that every package
|
|
// will be polled once again for its migration data. If they are all
|
|
// ready this time, then the migration will happen. name must be set if there
|
|
// is migration data. If 'immediateMigration' is set in the options
|
|
// argument, then it doesn't matter whether the package is ready to
|
|
// migrate or not; the reload will happen immediately without waiting
|
|
// (used for OAuth redirect login).
|
|
//
|
|
Reload._onMigrate = function (name, callback) {
|
|
debug('_onMigrate', {name});
|
|
if (!callback) {
|
|
// name not provided, so first arg is callback.
|
|
callback = name;
|
|
name = undefined;
|
|
debug('_onMigrate no callback');
|
|
}
|
|
|
|
providers.push({name: name, callback: callback});
|
|
};
|
|
|
|
// Called by packages when they start up.
|
|
// Returns the object that was saved, or undefined if none saved.
|
|
//
|
|
Reload._migrationData = function (name) {
|
|
debug('_migrationData', {name});
|
|
return old_data[name];
|
|
};
|
|
|
|
// Options are the same as for `Reload._migrate`.
|
|
const pollProviders = function (tryReload, options) {
|
|
debug('pollProviders', {options});
|
|
tryReload = tryReload || function () {
|
|
};
|
|
options = options || {};
|
|
|
|
const {immediateMigration} = options;
|
|
debug(
|
|
`pollProviders is ${immediateMigration ? '' : 'NOT '}immediateMigration`,
|
|
{options}
|
|
);
|
|
const migrationData = {};
|
|
let allReady = true;
|
|
providers.forEach(p => {
|
|
const {callback, name} = p || {};
|
|
const [ready, data] = callback(tryReload, options) || [];
|
|
|
|
debug(
|
|
`pollProviders provider ${name || 'unknown'} is ${
|
|
ready ? 'ready' : 'NOT ready'
|
|
}`,
|
|
{options}
|
|
);
|
|
if (!ready) {
|
|
allReady = false;
|
|
}
|
|
|
|
if (data !== undefined && name) {
|
|
migrationData[name] = data;
|
|
}
|
|
});
|
|
|
|
if (allReady) {
|
|
debug('pollProviders allReady', {options, migrationData});
|
|
return migrationData;
|
|
}
|
|
|
|
if (immediateMigration) {
|
|
debug('pollProviders immediateMigration', {options, migrationData});
|
|
return migrationData;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// Options are:
|
|
// - immediateMigration: true if the page will be reloaded immediately
|
|
// regardless of whether packages report that they are ready or not.
|
|
Reload._migrate = function (tryReload, options) {
|
|
debug('_migrate', {options});
|
|
// Make sure each package is ready to go, and collect their
|
|
// migration data
|
|
const migrationData = pollProviders(tryReload, options);
|
|
if (migrationData === null) {
|
|
return false; // not ready yet..
|
|
}
|
|
|
|
let json;
|
|
try {
|
|
// Persist the migration data
|
|
json = JSON.stringify({
|
|
data: migrationData,
|
|
reload: true,
|
|
});
|
|
} catch (err) {
|
|
Meteor._debug("Couldn't serialize data for migration", migrationData);
|
|
throw err;
|
|
}
|
|
|
|
if (safeSessionStorage) {
|
|
try {
|
|
safeSessionStorage.setItem(KEY_NAME, json);
|
|
} catch (err) {
|
|
// We should have already checked this, but just log - don't throw
|
|
Meteor._debug("Couldn't save data for migration to sessionStorage", err);
|
|
}
|
|
} else {
|
|
Meteor._debug(
|
|
'Browser does not support sessionStorage. Not saving migration state.'
|
|
);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Allows tests to isolate the list of providers.
|
|
Reload._withFreshProvidersForTest = function (f) {
|
|
const originalProviders = providers.slice(0);
|
|
providers = [];
|
|
try {
|
|
f();
|
|
} finally {
|
|
providers = originalProviders;
|
|
}
|
|
};
|
|
|
|
// Migrating reload: reload this page (presumably to pick up a new
|
|
// version of the code or assets), but save the program state and
|
|
// migrate it over. This function returns immediately. The reload
|
|
// will happen at some point in the future once all of the packages
|
|
// are ready to migrate.
|
|
//
|
|
let reloading = false;
|
|
Reload._reload = function (options) {
|
|
debug('_reload', {options});
|
|
options = options || {};
|
|
|
|
if (reloading) {
|
|
debug('reloading in progress already', {options});
|
|
return;
|
|
}
|
|
reloading = true;
|
|
|
|
function tryReload() {
|
|
debug('tryReload');
|
|
setTimeout(reload, 1);
|
|
}
|
|
|
|
function forceBrowserReload() {
|
|
debug('forceBrowserReload');
|
|
// We'd like to make the browser reload the page using location.replace()
|
|
// instead of location.reload(), because this avoids validating assets
|
|
// with the server if we still have a valid cached copy. This doesn't work
|
|
// when the location contains a hash however, because that wouldn't reload
|
|
// the page and just scroll to the hash location instead.
|
|
if (window.location.hash || window.location.href.endsWith('#')) {
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
|
|
window.location.replace(window.location.href);
|
|
}
|
|
|
|
function reload() {
|
|
debug('reload');
|
|
if (!Reload._migrate(tryReload, options)) {
|
|
return;
|
|
}
|
|
|
|
if (Meteor.isCordova) {
|
|
WebAppLocalServer.switchToPendingVersion(() => {
|
|
forceBrowserReload();
|
|
});
|
|
return;
|
|
}
|
|
|
|
forceBrowserReload();
|
|
}
|
|
|
|
tryReload();
|
|
};
|