ActivityStreamMessageChannel.sys.mjs (10660B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs", 9 AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs", 10 }); 11 12 import { 13 actionCreators as ac, 14 actionTypes as at, 15 actionUtils as au, 16 } from "resource://newtab/common/Actions.mjs"; 17 18 const ABOUT_NEW_TAB_URL = "about:newtab"; 19 20 export const DEFAULT_OPTIONS = { 21 dispatch(action) { 22 throw new Error( 23 `\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n` 24 ); 25 }, 26 pageURL: ABOUT_NEW_TAB_URL, 27 outgoingMessageName: "ActivityStream:MainToContent", 28 incomingMessageName: "ActivityStream:ContentToMain", 29 }; 30 31 export class ActivityStreamMessageChannel { 32 /** 33 * ActivityStreamMessageChannel - This module connects a Redux store to the new tab page actor. 34 * You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators 35 * in common/Actions.sys.mjs to help you create actions that will be automatically routed 36 * to the correct location. 37 * 38 * @param {object} options 39 * @param {function} options.dispatch The dispatch method from a Redux store 40 * @param {string} options.pageURL The URL to which the channel is attached, such as about:newtab. 41 * @param {string} options.outgoingMessageName The name of the message sent to child processes 42 * @param {string} options.incomingMessageName The name of the message received from child processes 43 * @return {ActivityStreamMessageChannel} 44 */ 45 constructor(options = {}) { 46 Object.assign(this, DEFAULT_OPTIONS, options); 47 48 this.middleware = this.middleware.bind(this); 49 this.onMessage = this.onMessage.bind(this); 50 this.onNewTabLoad = this.onNewTabLoad.bind(this); 51 this.onNewTabUnload = this.onNewTabUnload.bind(this); 52 this.onNewTabInit = this.onNewTabInit.bind(this); 53 } 54 55 /** 56 * Get an iterator over the loaded tab objects. 57 */ 58 get loadedTabs() { 59 // In the test, AboutNewTabParent is not defined. 60 return lazy.AboutNewTabParent?.loadedTabs || new Map(); 61 } 62 63 /** 64 * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type 65 * actions, and sends them out. 66 * 67 * @param {object} store A redux store 68 * @return {function} Redux middleware 69 */ 70 middleware() { 71 return next => action => { 72 const skipMain = action.meta && action.meta.skipMain; 73 if (au.isSendToOneContent(action)) { 74 this.send(action); 75 } else if (au.isBroadcastToContent(action)) { 76 this.broadcast(action); 77 } else if (au.isSendToPreloaded(action)) { 78 this.sendToPreloaded(action); 79 } 80 81 if (!skipMain) { 82 next(action); 83 } 84 }; 85 } 86 87 /** 88 * onActionFromContent - Handler for actions from a content processes 89 * 90 * @param {object} action A Redux action 91 * @param {string} targetId The portID of the port that sent the message 92 */ 93 onActionFromContent(action, targetId) { 94 this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId))); 95 } 96 97 /** 98 * broadcast - Sends an action to all ports 99 * 100 * @param {object} action A Redux action 101 */ 102 broadcast(action) { 103 // We're trying to update all tabs, so signal the AboutHomeStartupCache 104 // that its likely time to refresh the cache. 105 lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); 106 107 for (let { actor } of this.loadedTabs.values()) { 108 try { 109 actor.sendAsyncMessage(this.outgoingMessageName, action); 110 } catch (e) { 111 // The target page is closed/closing by the user or test, so just ignore. 112 } 113 } 114 } 115 116 /** 117 * send - Sends an action to a specific port 118 * 119 * @param {obj} action A redux action; it should contain a portID in the meta.toTarget property 120 */ 121 send(action) { 122 const targetId = action.meta && action.meta.toTarget; 123 const target = this.getTargetById(targetId); 124 try { 125 target.sendAsyncMessage(this.outgoingMessageName, action); 126 } catch (e) { 127 // The target page is closed/closing by the user or test, so just ignore. 128 } 129 } 130 131 /** 132 * A valid portID is a combination of process id and a port number. 133 * It is generated in AboutNewTabChild.sys.mjs. 134 */ 135 validatePortID(id) { 136 if (typeof id !== "string" || !id.includes(":")) { 137 console.error("Invalid portID"); 138 } 139 140 return id; 141 } 142 143 /** 144 * getTargetById - Retrieve the message target by portID, if it exists 145 * 146 * @param {string} id A portID 147 * @return {obj|null} The message target, if it exists. 148 */ 149 getTargetById(id) { 150 this.validatePortID(id); 151 152 for (let { portID, actor } of this.loadedTabs.values()) { 153 if (portID === id) { 154 return actor; 155 } 156 } 157 return null; 158 } 159 160 /** 161 * sendToPreloaded - Sends an action to each preloaded browser, if any 162 * 163 * @param {obj} action A redux action 164 */ 165 sendToPreloaded(action) { 166 // We're trying to update the preloaded about:newtab, so signal 167 // the AboutHomeStartupCache that its likely time to refresh 168 // the cache. 169 lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); 170 171 const preloadedActors = this.getPreloadedActors(); 172 if (preloadedActors && action.data) { 173 for (let preloadedActor of preloadedActors) { 174 try { 175 preloadedActor.sendAsyncMessage(this.outgoingMessageName, action); 176 } catch (e) { 177 // The preloaded page is no longer available, so just ignore. 178 } 179 } 180 } 181 } 182 183 /** 184 * getPreloadedActors - Retrieve the preloaded actors 185 * 186 * @return {Array|null} An array of actors belonging to the preloaded browsers, or null 187 * if there aren't any preloaded browsers 188 */ 189 getPreloadedActors() { 190 let preloadedActors = []; 191 for (let { actor, browser } of this.loadedTabs.values()) { 192 if (this.isPreloadedBrowser(browser)) { 193 preloadedActors.push(actor); 194 } 195 } 196 return preloadedActors.length ? preloadedActors : null; 197 } 198 199 /** 200 * isPreloadedBrowser - Returns true if the passed browser has been preloaded 201 * for faster rendering of new tabs. 202 * 203 * @param {<browser>} A <browser> to check. 204 * @return {bool} True if the browser is preloaded. 205 * if there aren't any preloaded browsers 206 */ 207 isPreloadedBrowser(browser) { 208 return browser.getAttribute("preloadedState") === "preloaded"; 209 } 210 211 simulateMessagesForExistingTabs() { 212 // Some pages might have already loaded, so we won't get the usual message 213 for (const loadedTab of this.loadedTabs.values()) { 214 let simulatedDetails = { 215 actor: loadedTab.actor, 216 browser: loadedTab.browser, 217 browsingContext: loadedTab.browsingContext, 218 portID: loadedTab.portID, 219 url: loadedTab.url, 220 simulated: true, 221 }; 222 223 this.onActionFromContent( 224 { 225 type: at.NEW_TAB_INIT, 226 data: simulatedDetails, 227 }, 228 loadedTab.portID 229 ); 230 231 if (loadedTab.loaded) { 232 this.tabLoaded(simulatedDetails); 233 } 234 } 235 236 // It's possible that those existing tabs had sent some messages up 237 // to us before the feeds / ActivityStreamMessageChannel was ready. 238 // 239 // AboutNewTabParent takes care of queueing those for us, so 240 // now that we're ready, we can flush these queued messages. 241 lazy.AboutNewTabParent.flushQueuedMessagesFromContent(); 242 } 243 244 /** 245 * onNewTabInit - Handler for special RemotePage:Init message fired 246 * on initialization. 247 * 248 * @param {obj} msg The messsage from a page that was just initialized 249 * @param {obj} tabDetails details about a loaded tab 250 * 251 * tabDetails contains: 252 * actor, browser, browsingContext, portID, url 253 */ 254 onNewTabInit(msg, tabDetails) { 255 this.onActionFromContent( 256 { 257 type: at.NEW_TAB_INIT, 258 data: tabDetails, 259 }, 260 msg.data.portID 261 ); 262 } 263 264 /** 265 * onNewTabLoad - Handler for special RemotePage:Load message fired on page load. 266 * 267 * @param {obj} msg The messsage from a page that was just loaded 268 * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit 269 */ 270 onNewTabLoad(msg, tabDetails) { 271 this.tabLoaded(tabDetails); 272 } 273 274 tabLoaded(tabDetails) { 275 tabDetails.loaded = true; 276 277 let { browser } = tabDetails; 278 if ( 279 this.isPreloadedBrowser(browser) && 280 browser.ownerGlobal.windowState !== browser.ownerGlobal.STATE_MINIMIZED && 281 !browser.ownerGlobal.isFullyOccluded 282 ) { 283 // As a perceived performance optimization, if this loaded Activity Stream 284 // happens to be a preloaded browser in a window that is not minimized or 285 // occluded, have it render its layers to the compositor now to increase 286 // the odds that by the time we switch to the tab, the layers are already 287 // ready to present to the user. 288 browser.renderLayers = true; 289 } 290 291 this.onActionFromContent({ type: at.NEW_TAB_LOAD }, tabDetails.portID); 292 } 293 294 /** 295 * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired 296 * on page unload. 297 * 298 * @param {obj} msg The messsage from a page that was just unloaded 299 * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit 300 */ 301 onNewTabUnload(msg, tabDetails) { 302 this.onActionFromContent({ type: at.NEW_TAB_UNLOAD }, tabDetails.portID); 303 } 304 305 /** 306 * onMessage - Handles custom messages from content. It expects all messages to 307 * be formatted as Redux actions, and dispatches them to this.store 308 * 309 * @param {obj} msg A custom message from content 310 * @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"}) 311 * @param {obj} msg.target A message target 312 * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit 313 */ 314 onMessage(msg, tabDetails) { 315 if (!msg.data || !msg.data.type) { 316 console.error( 317 new Error( 318 `Received an improperly formatted message from ${tabDetails.portID}` 319 ) 320 ); 321 return; 322 } 323 let action = {}; 324 Object.assign(action, msg.data); 325 // target is used to access a browser reference that came from the content 326 // and should only be used in feeds (not reducers) 327 action._target = { 328 browser: tabDetails.browser, 329 }; 330 331 this.onActionFromContent(action, tabDetails.portID); 332 } 333 }