/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AboutHomeStartupCache: "resource:///modules/BrowserGlue.sys.mjs", AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs", }); import { actionCreators as ac, actionTypes as at, actionUtils as au, } from "resource://activity-stream/common/Actions.sys.mjs"; const ABOUT_NEW_TAB_URL = "about:newtab"; export const DEFAULT_OPTIONS = { dispatch(action) { throw new Error( `\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n` ); }, pageURL: ABOUT_NEW_TAB_URL, outgoingMessageName: "ActivityStream:MainToContent", incomingMessageName: "ActivityStream:ContentToMain", }; export class ActivityStreamMessageChannel { /** * ActivityStreamMessageChannel - This module connects a Redux store to the new tab page actor. * You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators * in common/Actions.sys.mjs to help you create actions that will be automatically routed * to the correct location. * * @param {object} options * @param {function} options.dispatch The dispatch method from a Redux store * @param {string} options.pageURL The URL to which the channel is attached, such as about:newtab. * @param {string} options.outgoingMessageName The name of the message sent to child processes * @param {string} options.incomingMessageName The name of the message received from child processes * @return {ActivityStreamMessageChannel} */ constructor(options = {}) { Object.assign(this, DEFAULT_OPTIONS, options); this.middleware = this.middleware.bind(this); this.onMessage = this.onMessage.bind(this); this.onNewTabLoad = this.onNewTabLoad.bind(this); this.onNewTabUnload = this.onNewTabUnload.bind(this); this.onNewTabInit = this.onNewTabInit.bind(this); } /** * Get an iterator over the loaded tab objects. */ get loadedTabs() { // In the test, AboutNewTabParent is not defined. return lazy.AboutNewTabParent?.loadedTabs || new Map(); } /** * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type * actions, and sends them out. * * @param {object} store A redux store * @return {function} Redux middleware */ middleware(store) { return next => action => { const skipMain = action.meta && action.meta.skipMain; if (au.isSendToOneContent(action)) { this.send(action); } else if (au.isBroadcastToContent(action)) { this.broadcast(action); } else if (au.isSendToPreloaded(action)) { this.sendToPreloaded(action); } if (!skipMain) { next(action); } }; } /** * onActionFromContent - Handler for actions from a content processes * * @param {object} action A Redux action * @param {string} targetId The portID of the port that sent the message */ onActionFromContent(action, targetId) { this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId))); } /** * broadcast - Sends an action to all ports * * @param {object} action A Redux action */ broadcast(action) { // We're trying to update all tabs, so signal the AboutHomeStartupCache // that its likely time to refresh the cache. lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); for (let { actor } of this.loadedTabs.values()) { try { actor.sendAsyncMessage(this.outgoingMessageName, action); } catch (e) { // The target page is closed/closing by the user or test, so just ignore. } } } /** * send - Sends an action to a specific port * * @param {obj} action A redux action; it should contain a portID in the meta.toTarget property */ send(action) { const targetId = action.meta && action.meta.toTarget; const target = this.getTargetById(targetId); try { target.sendAsyncMessage(this.outgoingMessageName, action); } catch (e) { // The target page is closed/closing by the user or test, so just ignore. } } /** * A valid portID is a combination of process id and a port number. * It is generated in AboutNewTabChild.sys.mjs. */ validatePortID(id) { if (typeof id !== "string" || !id.includes(":")) { console.error("Invalid portID"); } return id; } /** * getTargetById - Retrieve the message target by portID, if it exists * * @param {string} id A portID * @return {obj|null} The message target, if it exists. */ getTargetById(id) { this.validatePortID(id); for (let { portID, actor } of this.loadedTabs.values()) { if (portID === id) { return actor; } } return null; } /** * sendToPreloaded - Sends an action to each preloaded browser, if any * * @param {obj} action A redux action */ sendToPreloaded(action) { // We're trying to update the preloaded about:newtab, so signal // the AboutHomeStartupCache that its likely time to refresh // the cache. lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); const preloadedActors = this.getPreloadedActors(); if (preloadedActors && action.data) { for (let preloadedActor of preloadedActors) { try { preloadedActor.sendAsyncMessage(this.outgoingMessageName, action); } catch (e) { // The preloaded page is no longer available, so just ignore. } } } } /** * getPreloadedActors - Retrieve the preloaded actors * * @return {Array|null} An array of actors belonging to the preloaded browsers, or null * if there aren't any preloaded browsers */ getPreloadedActors() { let preloadedActors = []; for (let { actor, browser } of this.loadedTabs.values()) { if (this.isPreloadedBrowser(browser)) { preloadedActors.push(actor); } } return preloadedActors.length ? preloadedActors : null; } /** * isPreloadedBrowser - Returns true if the passed browser has been preloaded * for faster rendering of new tabs. * * @param {} A to check. * @return {bool} True if the browser is preloaded. * if there aren't any preloaded browsers */ isPreloadedBrowser(browser) { return browser.getAttribute("preloadedState") === "preloaded"; } simulateMessagesForExistingTabs() { // Some pages might have already loaded, so we won't get the usual message for (const loadedTab of this.loadedTabs.values()) { let simulatedDetails = { actor: loadedTab.actor, browser: loadedTab.browser, browsingContext: loadedTab.browsingContext, portID: loadedTab.portID, url: loadedTab.url, simulated: true, }; this.onActionFromContent( { type: at.NEW_TAB_INIT, data: simulatedDetails, }, loadedTab.portID ); if (loadedTab.loaded) { this.tabLoaded(simulatedDetails); } } // It's possible that those existing tabs had sent some messages up // to us before the feeds / ActivityStreamMessageChannel was ready. // // AboutNewTabParent takes care of queueing those for us, so // now that we're ready, we can flush these queued messages. lazy.AboutNewTabParent.flushQueuedMessagesFromContent(); } /** * onNewTabInit - Handler for special RemotePage:Init message fired * on initialization. * * @param {obj} msg The messsage from a page that was just initialized * @param {obj} tabDetails details about a loaded tab * * tabDetails contains: * actor, browser, browsingContext, portID, url */ onNewTabInit(msg, tabDetails) { this.onActionFromContent( { type: at.NEW_TAB_INIT, data: tabDetails, }, msg.data.portID ); } /** * onNewTabLoad - Handler for special RemotePage:Load message fired on page load. * * @param {obj} msg The messsage from a page that was just loaded * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit */ onNewTabLoad(msg, tabDetails) { this.tabLoaded(tabDetails); } tabLoaded(tabDetails) { tabDetails.loaded = true; let { browser } = tabDetails; if ( this.isPreloadedBrowser(browser) && browser.ownerGlobal.windowState !== browser.ownerGlobal.STATE_MINIMIZED && !browser.ownerGlobal.isFullyOccluded ) { // As a perceived performance optimization, if this loaded Activity Stream // happens to be a preloaded browser in a window that is not minimized or // occluded, have it render its layers to the compositor now to increase // the odds that by the time we switch to the tab, the layers are already // ready to present to the user. browser.renderLayers = true; } this.onActionFromContent({ type: at.NEW_TAB_LOAD }, tabDetails.portID); } /** * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired * on page unload. * * @param {obj} msg The messsage from a page that was just unloaded * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit */ onNewTabUnload(msg, tabDetails) { this.onActionFromContent({ type: at.NEW_TAB_UNLOAD }, tabDetails.portID); } /** * onMessage - Handles custom messages from content. It expects all messages to * be formatted as Redux actions, and dispatches them to this.store * * @param {obj} msg A custom message from content * @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"}) * @param {obj} msg.target A message target * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit */ onMessage(msg, tabDetails) { if (!msg.data || !msg.data.type) { console.error( new Error( `Received an improperly formatted message from ${tabDetails.portID}` ) ); return; } let action = {}; Object.assign(action, msg.data); // target is used to access a browser reference that came from the content // and should only be used in feeds (not reducers) action._target = { browser: tabDetails.browser, }; this.onActionFromContent(action, tabDetails.portID); } }