cache.worker.js (6829B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* global ReactDOMServer, NewtabRenderUtils */ 6 7 const PAGE_TEMPLATE_RESOURCE_PATH = 8 "resource://newtab/data/content/abouthomecache/page.html.template"; 9 const SCRIPT_TEMPLATE_RESOURCE_PATH = 10 "resource://newtab/data/content/abouthomecache/script.js.template"; 11 12 // If we don't stub these functions out, React throws warnings in the console 13 // upon being loaded. 14 let window = self; 15 window.requestAnimationFrame = () => {}; 16 window.cancelAnimationFrame = () => {}; 17 18 /* import-globals-from /toolkit/components/workerloader/require.js */ 19 importScripts("resource://gre/modules/workers/require.js"); 20 21 { 22 let oldChromeUtils = ChromeUtils; 23 24 // ChromeUtils is defined inside of a Worker, but we don't want the 25 // activity-stream.bundle.js to detect it when loading, since that results 26 // in it attempting to import JSMs on load, which is not allowed in 27 // a Worker. So we temporarily clear ChromeUtils so that activity-stream.bundle.js 28 // thinks its being loaded in content scope. 29 // 30 // eslint-disable-next-line no-implicit-globals, no-global-assign 31 ChromeUtils = undefined; 32 33 /* import-globals-from ../../../../toolkit/content/vendor/react/react.js */ 34 /* import-globals-from ../../../../toolkit/content/vendor/react/react-dom.js */ 35 /* import-globals-from ../../../../toolkit/content/vendor/react/react-dom-server.js */ 36 /* import-globals-from ../../../../toolkit/content/vendor/react/redux.js */ 37 /* import-globals-from ../../../../toolkit/content/vendor/react/react-transition-group.js */ 38 /* import-globals-from ../../../../toolkit/content/vendor/react/prop-types.js */ 39 /* import-globals-from ../../../../toolkit/content/vendor/react/react-redux.js */ 40 /* import-globals-from ../data/content/activity-stream.bundle.js */ 41 importScripts( 42 "chrome://global/content/vendor/react.js", 43 "chrome://global/content/vendor/react-dom.js", 44 "chrome://global/content/vendor/react-dom-server.js", 45 "chrome://global/content/vendor/redux.js", 46 "chrome://global/content/vendor/react-transition-group.js", 47 "chrome://global/content/vendor/prop-types.js", 48 "chrome://global/content/vendor/react-redux.js", 49 "resource://newtab/data/content/activity-stream.bundle.js" 50 ); 51 52 // eslint-disable-next-line no-global-assign, no-implicit-globals 53 ChromeUtils = oldChromeUtils; 54 } 55 56 let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); 57 58 let Agent = { 59 _templates: null, 60 61 /** 62 * Synchronously loads the template files off of the file 63 * system, and returns them as an object. If the Worker has loaded 64 * these templates before, a cached copy of the templates is returned 65 * instead. 66 * 67 * @return Object 68 * An object with the following properties: 69 * 70 * pageTemplate (String): 71 * The template for the document markup. 72 * 73 * scriptTempate (String): 74 * The template for the script. 75 */ 76 getOrCreateTemplates() { 77 if (this._templates) { 78 return this._templates; 79 } 80 81 const templateResources = new Map([ 82 ["pageTemplate", PAGE_TEMPLATE_RESOURCE_PATH], 83 ["scriptTemplate", SCRIPT_TEMPLATE_RESOURCE_PATH], 84 ]); 85 86 this._templates = {}; 87 88 for (let [templateName, path] of templateResources) { 89 const xhr = new XMLHttpRequest(); 90 // Using a synchronous XHR in a worker is fine. 91 xhr.open("GET", path, false); 92 xhr.responseType = "text"; 93 xhr.send(null); 94 this._templates[templateName] = xhr.responseText; 95 } 96 97 return this._templates; 98 }, 99 100 /** 101 * Constructs the cached about:home document using ReactDOMServer. This will 102 * be called when "construct" messages are sent to this PromiseWorker. 103 * 104 * @param state (Object) 105 * The most recent Activity Stream Redux state. 106 * @return Object 107 * An object with the following properties: 108 * 109 * page (String): 110 * The generated markup for the document. 111 * 112 * script (String): 113 * The generated script for the document. 114 */ 115 construct(state) { 116 // If anything in this function throws an exception, PromiseWorker 117 // runs the risk of leaving the Promise associated with this method 118 // forever unresolved. This is particularly bad when this method is 119 // called via AsyncShutdown, since the forever unresolved Promise can 120 // result in a AsyncShutdown timeout crash. 121 // 122 // To help ensure that no matter what, the Promise resolves with something, 123 // we wrap the whole operation in a try/catch. 124 try { 125 return this._construct(state); 126 } catch (e) { 127 console.error("about:home startup cache construction failed:", e); 128 return { page: null, script: null }; 129 } 130 }, 131 132 /** 133 * Internal method that actually does the work of constructing the cached 134 * about:home document using ReactDOMServer. This should be called from 135 * `construct` only. 136 * 137 * @param state (Object) 138 * The most recent Activity Stream Redux state. 139 * @return Object 140 * An object with the following properties: 141 * 142 * page (String): 143 * The generated markup for the document. 144 * 145 * script (String): 146 * The generated script for the document. 147 */ 148 _construct(state) { 149 for (const key of Object.keys(state.App.isForStartupCache)) { 150 state.App.isForStartupCache[key] = true; 151 } 152 153 // ReactDOMServer.renderToString expects a Redux store to pull 154 // the state from, so we mock out a minimal store implementation. 155 let fakeStore = { 156 getState() { 157 return state; 158 }, 159 dispatch() {}, 160 }; 161 162 let markup = ReactDOMServer.renderToString( 163 NewtabRenderUtils.NewTab({ 164 store: fakeStore, 165 isFirstrun: false, 166 }) 167 ); 168 169 let { pageTemplate, scriptTemplate } = this.getOrCreateTemplates(); 170 let cacheTime = new Date().toUTCString(); 171 let page = pageTemplate 172 .replace("{{ MARKUP }}", markup) 173 .replace("{{ CACHE_TIME }}", cacheTime); 174 let script = scriptTemplate.replace( 175 "{{ STATE }}", 176 JSON.stringify(state, null, "\t") 177 ); 178 179 return { page, script }; 180 }, 181 }; 182 183 // This boilerplate connects the PromiseWorker to the Agent so 184 // that messages from the main thread map to methods on the 185 // Agent. 186 let worker = new PromiseWorker.AbstractWorker(); 187 worker.dispatch = function (method, args = []) { 188 return Agent[method](...args); 189 }; 190 worker.postMessage = function (result, ...transfers) { 191 self.postMessage(result, ...transfers); 192 }; 193 worker.close = function () { 194 self.close(); 195 }; 196 197 self.addEventListener("message", msg => worker.handleMessage(msg)); 198 self.addEventListener("unhandledrejection", function (error) { 199 throw error.reason; 200 });