ActivityStreamMessageChannel.jsm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. "use strict";
  5. const {AboutNewTab} = ChromeUtils.import("resource:///modules/AboutNewTab.jsm");
  6. /* globals RemotePages */ // Remove when updating eslint-plugin-mozilla 0.14.0+
  7. const {RemotePages} = ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
  8. const {actionCreators: ac, actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
  9. const ABOUT_NEW_TAB_URL = "about:newtab";
  10. const ABOUT_HOME_URL = "about:home";
  11. const DEFAULT_OPTIONS = {
  12. dispatch(action) {
  13. throw new Error(`\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`);
  14. },
  15. pageURL: ABOUT_NEW_TAB_URL,
  16. outgoingMessageName: "ActivityStream:MainToContent",
  17. incomingMessageName: "ActivityStream:ContentToMain",
  18. };
  19. this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
  20. /**
  21. * ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
  22. * Call .createChannel to start the connection, and .destroyChannel to destroy it.
  23. * You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators
  24. * in common/Actions.jsm to help you create actions that will be automatically routed
  25. * to the correct location.
  26. *
  27. * @param {object} options
  28. * @param {function} options.dispatch The dispatch method from a Redux store
  29. * @param {string} options.pageURL The URL to which a RemotePageManager should be attached.
  30. * Note that if it is about:newtab, the existing RemotePageManager
  31. * for about:newtab will also be disabled
  32. * @param {string} options.outgoingMessageName The name of the message sent to child processes
  33. * @param {string} options.incomingMessageName The name of the message received from child processes
  34. * @return {ActivityStreamMessageChannel}
  35. */
  36. constructor(options = {}) {
  37. Object.assign(this, DEFAULT_OPTIONS, options);
  38. this.channel = null;
  39. this.middleware = this.middleware.bind(this);
  40. this.onMessage = this.onMessage.bind(this);
  41. this.onNewTabLoad = this.onNewTabLoad.bind(this);
  42. this.onNewTabUnload = this.onNewTabUnload.bind(this);
  43. this.onNewTabInit = this.onNewTabInit.bind(this);
  44. }
  45. /**
  46. * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type
  47. * actions, and sends them out.
  48. *
  49. * @param {object} store A redux store
  50. * @return {function} Redux middleware
  51. */
  52. middleware(store) {
  53. return next => action => {
  54. const skipMain = action.meta && action.meta.skipMain;
  55. if (!this.channel && !skipMain) {
  56. next(action);
  57. return;
  58. }
  59. if (au.isSendToOneContent(action)) {
  60. this.send(action);
  61. } else if (au.isBroadcastToContent(action)) {
  62. this.broadcast(action);
  63. } else if (au.isSendToPreloaded(action)) {
  64. this.sendToPreloaded(action);
  65. }
  66. if (!skipMain) {
  67. next(action);
  68. }
  69. };
  70. }
  71. /**
  72. * onActionFromContent - Handler for actions from a content processes
  73. *
  74. * @param {object} action A Redux action
  75. * @param {string} targetId The portID of the port that sent the message
  76. */
  77. onActionFromContent(action, targetId) {
  78. this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId)));
  79. }
  80. /**
  81. * broadcast - Sends an action to all ports
  82. *
  83. * @param {object} action A Redux action
  84. */
  85. broadcast(action) {
  86. this.channel.sendAsyncMessage(this.outgoingMessageName, action);
  87. }
  88. /**
  89. * send - Sends an action to a specific port
  90. *
  91. * @param {obj} action A redux action; it should contain a portID in the meta.toTarget property
  92. */
  93. send(action) {
  94. const targetId = action.meta && action.meta.toTarget;
  95. const target = this.getTargetById(targetId);
  96. try {
  97. target.sendAsyncMessage(this.outgoingMessageName, action);
  98. } catch (e) {
  99. // The target page is closed/closing by the user or test, so just ignore.
  100. }
  101. }
  102. /**
  103. * A valid portID is a combination of process id and port
  104. * https://searchfox.org/mozilla-central/rev/196560b95f191b48ff7cba7c2ba9237bba6b5b6a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm#14
  105. */
  106. validatePortID(id) {
  107. if (typeof id !== "string" || !id.includes(":")) {
  108. Cu.reportError("Invalid portID");
  109. }
  110. return id;
  111. }
  112. /**
  113. * getIdByTarget - Retrieve the id of a message target, if it exists in this.targets
  114. *
  115. * @param {obj} targetObj A message target
  116. * @return {string|null} The unique id of the target, if it exists.
  117. */
  118. getTargetById(id) {
  119. this.validatePortID(id);
  120. for (let port of this.channel.messagePorts) {
  121. if (port.portID === id) {
  122. return port;
  123. }
  124. }
  125. return null;
  126. }
  127. /**
  128. * sendToPreloaded - Sends an action to each preloaded browser, if any
  129. *
  130. * @param {obj} action A redux action
  131. */
  132. sendToPreloaded(action) {
  133. const preloadedBrowsers = this.getPreloadedBrowser();
  134. if (preloadedBrowsers && action.data) {
  135. for (let preloadedBrowser of preloadedBrowsers) {
  136. try {
  137. preloadedBrowser.sendAsyncMessage(this.outgoingMessageName, action);
  138. } catch (e) {
  139. // The preloaded page is no longer available, so just ignore.
  140. }
  141. }
  142. }
  143. }
  144. /**
  145. * getPreloadedBrowser - Retrieve the port of any preloaded browsers
  146. *
  147. * @return {Array|null} An array of ports belonging to the preloaded browsers, or null
  148. * if there aren't any preloaded browsers
  149. */
  150. getPreloadedBrowser() {
  151. let preloadedPorts = [];
  152. for (let port of this.channel.messagePorts) {
  153. if (this.isPreloadedBrowser(port.browser)) {
  154. preloadedPorts.push(port);
  155. }
  156. }
  157. return preloadedPorts.length ? preloadedPorts : null;
  158. }
  159. /**
  160. * isPreloadedBrowser - Returns true if the passed browser has been preloaded
  161. * for faster rendering of new tabs.
  162. *
  163. * @param {<browser>} A <browser> to check.
  164. * @return {bool} True if the browser is preloaded.
  165. * if there aren't any preloaded browsers
  166. */
  167. isPreloadedBrowser(browser) {
  168. return browser.getAttribute("preloadedState") === "preloaded";
  169. }
  170. /**
  171. * createChannel - Create RemotePages channel to establishing message passing
  172. * between the main process and child pages
  173. */
  174. createChannel() {
  175. // Receive AboutNewTab's Remote Pages instance, if it exists, on override
  176. const channel = this.pageURL === ABOUT_NEW_TAB_URL && AboutNewTab.override(true);
  177. this.channel = channel || new RemotePages([ABOUT_HOME_URL, ABOUT_NEW_TAB_URL]);
  178. this.channel.addMessageListener("RemotePage:Init", this.onNewTabInit);
  179. this.channel.addMessageListener("RemotePage:Load", this.onNewTabLoad);
  180. this.channel.addMessageListener("RemotePage:Unload", this.onNewTabUnload);
  181. this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
  182. }
  183. simulateMessagesForExistingTabs() {
  184. // Some pages might have already loaded, so we won't get the usual message
  185. for (const target of this.channel.messagePorts) {
  186. const simulatedMsg = {target: Object.assign({simulated: true}, target)};
  187. this.onNewTabInit(simulatedMsg);
  188. if (target.loaded) {
  189. this.onNewTabLoad(simulatedMsg);
  190. }
  191. }
  192. }
  193. /**
  194. * destroyChannel - Destroys the RemotePages channel
  195. */
  196. destroyChannel() {
  197. this.channel.removeMessageListener("RemotePage:Init", this.onNewTabInit);
  198. this.channel.removeMessageListener("RemotePage:Load", this.onNewTabLoad);
  199. this.channel.removeMessageListener("RemotePage:Unload", this.onNewTabUnload);
  200. this.channel.removeMessageListener(this.incomingMessageName, this.onMessage);
  201. if (this.pageURL === ABOUT_NEW_TAB_URL) {
  202. AboutNewTab.reset(this.channel);
  203. } else {
  204. this.channel.destroy();
  205. }
  206. this.channel = null;
  207. }
  208. /**
  209. * onNewTabInit - Handler for special RemotePage:Init message fired
  210. * by RemotePages
  211. *
  212. * @param {obj} msg The messsage from a page that was just initialized
  213. */
  214. onNewTabInit(msg) {
  215. this.onActionFromContent({
  216. type: at.NEW_TAB_INIT,
  217. data: msg.target,
  218. }, msg.target.portID);
  219. }
  220. /**
  221. * onNewTabLoad - Handler for special RemotePage:Load message fired by RemotePages
  222. *
  223. * @param {obj} msg The messsage from a page that was just loaded
  224. */
  225. onNewTabLoad(msg) {
  226. let {browser} = msg.target;
  227. if (this.isPreloadedBrowser(browser)) {
  228. // As a perceived performance optimization, if this loaded Activity Stream
  229. // happens to be a preloaded browser, have it render its layers to the
  230. // compositor now to increase the odds that by the time we switch to
  231. // the tab, the layers are already ready to present to the user.
  232. browser.renderLayers = true;
  233. }
  234. this.onActionFromContent({type: at.NEW_TAB_LOAD}, msg.target.portID);
  235. }
  236. /**
  237. * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired by RemotePages
  238. *
  239. * @param {obj} msg The messsage from a page that was just unloaded
  240. */
  241. onNewTabUnload(msg) {
  242. this.onActionFromContent({type: at.NEW_TAB_UNLOAD}, msg.target.portID);
  243. }
  244. /**
  245. * onMessage - Handles custom messages from content. It expects all messages to
  246. * be formatted as Redux actions, and dispatches them to this.store
  247. *
  248. * @param {obj} msg A custom message from content
  249. * @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"})
  250. * @param {obj} msg.target A message target
  251. */
  252. onMessage(msg) {
  253. const {portID} = msg.target;
  254. if (!msg.data || !msg.data.type) {
  255. Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
  256. return;
  257. }
  258. let action = {};
  259. Object.assign(action, msg.data);
  260. // target is used to access a browser reference that came from the content
  261. // and should only be used in feeds (not reducers)
  262. action._target = msg.target;
  263. this.onActionFromContent(action, portID);
  264. }
  265. };
  266. this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
  267. const EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];