diff options
Diffstat (limited to 'browser/components/newtab/lib/cache-worker.js')
-rw-r--r-- | browser/components/newtab/lib/cache-worker.js | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/cache-worker.js b/browser/components/newtab/lib/cache-worker.js new file mode 100644 index 0000000000..0996ecde45 --- /dev/null +++ b/browser/components/newtab/lib/cache-worker.js @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/chrome-worker */ + +/* global ReactDOMServer, NewtabRenderUtils */ + +const PAGE_TEMPLATE_RESOURCE_PATH = + "resource://activity-stream/data/content/abouthomecache/page.html.template"; +const SCRIPT_TEMPLATE_RESOURCE_PATH = + "resource://activity-stream/data/content/abouthomecache/script.js.template"; + +// If we don't stub these functions out, React throws warnings in the console +// upon being loaded. +let window = self; +window.requestAnimationFrame = () => {}; +window.cancelAnimationFrame = () => {}; +window.ASRouterMessage = () => { + return Promise.resolve(); +}; +window.ASRouterAddParentListener = () => {}; +window.ASRouterRemoveParentListener = () => {}; + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); + +{ + let oldChromeUtils = ChromeUtils; + + // ChromeUtils is defined inside of a Worker, but we don't want the + // activity-stream.bundle.js to detect it when loading, since that results + // in it attempting to import JSMs on load, which is not allowed in + // a Worker. So we temporarily clear ChromeUtils so that activity-stream.bundle.js + // thinks its being loaded in content scope. + // + // eslint-disable-next-line no-global-assign + ChromeUtils = undefined; + + /* import-globals-from ../vendor/react.js */ + /* import-globals-from ../vendor/react-dom.js */ + /* import-globals-from ../vendor/react-dom-server.js */ + /* import-globals-from ../vendor/redux.js */ + /* import-globals-from ../vendor/react-transition-group.js */ + /* import-globals-from ../vendor/prop-types.js */ + /* import-globals-from ../vendor/react-redux.js */ + /* import-globals-from ../data/content/activity-stream.bundle.js */ + importScripts( + "resource://activity-stream/vendor/react.js", + "resource://activity-stream/vendor/react-dom.js", + "resource://activity-stream/vendor/react-dom-server.js", + "resource://activity-stream/vendor/redux.js", + "resource://activity-stream/vendor/react-transition-group.js", + "resource://activity-stream/vendor/prop-types.js", + "resource://activity-stream/vendor/react-redux.js", + "resource://activity-stream/data/content/activity-stream.bundle.js" + ); + + // eslint-disable-next-line no-global-assign + ChromeUtils = oldChromeUtils; +} + +let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); + +let Agent = { + _templates: null, + + /** + * Synchronously loads the template files off of the file + * system, and returns them as an object. If the Worker has loaded + * these templates before, a cached copy of the templates is returned + * instead. + * + * @return Object + * An object with the following properties: + * + * pageTemplate (String): + * The template for the document markup. + * + * scriptTempate (String): + * The template for the script. + */ + getOrCreateTemplates() { + if (this._templates) { + return this._templates; + } + + const templateResources = new Map([ + ["pageTemplate", PAGE_TEMPLATE_RESOURCE_PATH], + ["scriptTemplate", SCRIPT_TEMPLATE_RESOURCE_PATH], + ]); + + this._templates = {}; + + for (let [name, path] of templateResources) { + const xhr = new XMLHttpRequest(); + // Using a synchronous XHR in a worker is fine. + xhr.open("GET", path, false); + xhr.responseType = "text"; + xhr.send(null); + this._templates[name] = xhr.responseText; + } + + return this._templates; + }, + + /** + * Constructs the cached about:home document using ReactDOMServer. This will + * be called when "construct" messages are sent to this PromiseWorker. + * + * @param state (Object) + * The most recent Activity Stream Redux state. + * @return Object + * An object with the following properties: + * + * page (String): + * The generated markup for the document. + * + * script (String): + * The generated script for the document. + */ + construct(state) { + // If anything in this function throws an exception, PromiseWorker + // runs the risk of leaving the Promise associated with this method + // forever unresolved. This is particularly bad when this method is + // called via AsyncShutdown, since the forever unresolved Promise can + // result in a AsyncShutdown timeout crash. + // + // To help ensure that no matter what, the Promise resolves with something, + // we wrap the whole operation in a try/catch. + try { + return this._construct(state); + } catch (e) { + console.error("about:home startup cache construction failed:", e); + return { page: null, script: null }; + } + }, + + /** + * Internal method that actually does the work of constructing the cached + * about:home document using ReactDOMServer. This should be called from + * `construct` only. + * + * @param state (Object) + * The most recent Activity Stream Redux state. + * @return Object + * An object with the following properties: + * + * page (String): + * The generated markup for the document. + * + * script (String): + * The generated script for the document. + */ + _construct(state) { + state.App.isForStartupCache = true; + + // ReactDOMServer.renderToString expects a Redux store to pull + // the state from, so we mock out a minimal store implementation. + let fakeStore = { + getState() { + return state; + }, + dispatch() {}, + }; + + let markup = ReactDOMServer.renderToString( + NewtabRenderUtils.NewTab({ + store: fakeStore, + isFirstrun: false, + }) + ); + + let { pageTemplate, scriptTemplate } = this.getOrCreateTemplates(); + let cacheTime = new Date().toUTCString(); + let page = pageTemplate + .replace("{{ MARKUP }}", markup) + .replace("{{ CACHE_TIME }}", cacheTime); + let script = scriptTemplate.replace( + "{{ STATE }}", + JSON.stringify(state, null, "\t") + ); + + return { page, script }; + }, +}; + +// This boilerplate connects the PromiseWorker to the Agent so +// that messages from the main thread map to methods on the +// Agent. +let worker = new PromiseWorker.AbstractWorker(); +worker.dispatch = function(method, args = []) { + return Agent[method](...args); +}; +worker.postMessage = function(result, ...transfers) { + self.postMessage(result, ...transfers); +}; +worker.close = function() { + self.close(); +}; + +self.addEventListener("message", msg => worker.handleMessage(msg)); +self.addEventListener("unhandledrejection", function(error) { + throw error.reason; +}); |