/* 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; });