Source code

Revision control

Copy as Markdown

Other Tools

/* 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.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() {
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 {<browser>} A <browser> 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);
}
}