Bug 1350411 - Add Message Channel for Activity Stream system add-on r=mconley
authork88hudson <khudson@mozilla.com>
Fri, 07 Apr 2017 14:13:14 -0400
changeset 353694 ce2f726a2b86
parent 353693 5748607f3014
child 353695 0077229d7500
push id41015
push userryanvm@gmail.com
push dateWed, 19 Apr 2017 00:23:35 +0000
treeherderautoland@ce2f726a2b86 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1350411
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1350411 - Add Message Channel for Activity Stream system add-on r=mconley MozReview-Commit-ID: DCcGDjKdIHh
browser/extensions/activity-stream/common/Actions.jsm
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
browser/extensions/activity-stream/lib/NewTabInit.jsm
browser/extensions/activity-stream/lib/Store.jsm
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -1,19 +1,130 @@
 /* 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/. */
 "use strict";
 
+this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+
 this.actionTypes = [
   "INIT",
   "UNINIT",
+  "NEW_TAB_INITIAL_STATE",
+  "NEW_TAB_LOAD",
+  "NEW_TAB_UNLOAD"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 // It prevents accidentally adding a different key/value name.
 ].reduce((obj, type) => { obj[type] = type; return obj; }, {});
 
+// Helper function for creating routed actions between content and main
+// Not intended to be used by consumers
+function _RouteMessage(action, options) {
+  const meta = action.meta ? Object.assign({}, action.meta) : {};
+  if (!options || !options.from || !options.to) {
+    throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
+  }
+  // For each of these fields, if they are passed as an option,
+  // add them to the action. If they are not defined, remove them.
+  ["from", "to", "toTarget", "fromTarget", "skipOrigin"].forEach(o => {
+    if (typeof options[o] !== "undefined") {
+      meta[o] = options[o];
+    } else if (meta[o]) {
+      delete meta[o];
+    }
+  });
+  return Object.assign({}, action, {meta});
+}
+
+/**
+ * SendToMain - Creates a message that will be sent to the Main process.
+ *
+ * @param  {object} action Any redux action (required)
+ * @param  {object} options
+ * @param  {string} options.fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function SendToMain(action, options = {}) {
+  return _RouteMessage(action, {
+    from: CONTENT_MESSAGE_TYPE,
+    to: MAIN_MESSAGE_TYPE,
+    fromTarget: options.fromTarget
+  });
+}
+
+/**
+ * BroadcastToContent - Creates a message that will be sent to ALL content processes.
+ *
+ * @param  {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function BroadcastToContent(action) {
+  return _RouteMessage(action, {
+    from: MAIN_MESSAGE_TYPE,
+    to: CONTENT_MESSAGE_TYPE
+  });
+}
+
+/**
+ * SendToContent - Creates a message that will be sent to a particular Content process.
+ *
+ * @param  {object} action Any redux action (required)
+ * @param  {string} target The id of a content port
+ * @return {object} An action with added .meta properties
+ */
+function SendToContent(action, target) {
+  if (!target) {
+    throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent");
+  }
+  return _RouteMessage(action, {
+    from: MAIN_MESSAGE_TYPE,
+    to: CONTENT_MESSAGE_TYPE,
+    toTarget: target
+  });
+}
+
+this.actionCreators = {
+  SendToMain,
+  SendToContent,
+  BroadcastToContent
+};
+
+// These are helpers to test for certain kinds of actions
+this.actionUtils = {
+  isSendToMain(action) {
+    if (!action.meta) {
+      return false;
+    }
+    return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
+  },
+  isBroadcastToContent(action) {
+    if (!action.meta) {
+      return false;
+    }
+    if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
+      return true;
+    }
+    return false;
+  },
+  isSendToContent(action) {
+    if (!action.meta) {
+      return false;
+    }
+    if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
+      return true;
+    }
+    return false;
+  },
+  _RouteMessage
+};
+
 this.EXPORTED_SYMBOLS = [
-  "actionTypes"
+  "actionTypes",
+  "actionCreators",
+  "actionUtils",
+  "MAIN_MESSAGE_TYPE",
+  "CONTENT_MESSAGE_TYPE"
 ];
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -2,11 +2,30 @@
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title>New Tab</title>
   </head>
   <body>
     <div id="root">
       <h1>New Tab</h1>
+      <ul id="top-sites"></ul>
     </div>
+    <script>
+      const topSitesEl = document.getElementById("top-sites");
+      window.addMessageListener("ActivityStream:MainToContent", msg => {
+        if (msg.data.type === "NEW_TAB_INITIAL_STATE") {
+          const fragment = document.createDocumentFragment()
+          for (const row of msg.data.data.TopSites.rows) {
+            const li = document.createElement("li");
+            const a = document.createElement("a");
+            a.href = row.url;
+            a.textContent = row.title;
+            li.appendChild(a);
+            fragment.appendChild(li);
+          }
+          topSitesEl.appendChild(fragment);
+        }
+      });
+
+    </script>
   </body>
 </html>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,16 +1,19 @@
 /* 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/. */
 "use strict";
 
 const {utils: Cu} = Components;
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 
+// Feeds
+const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
+
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
    * @param  {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
    * @param  {string} options.version Version of the add-on. e.g. "0.1.0"
@@ -18,17 +21,19 @@ this.ActivityStream = class ActivityStre
    */
   constructor(options) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
   }
   init() {
     this.initialized = true;
-    this.store.init();
+    this.store.init([
+      new NewTabInit()
+    ]);
   }
   uninit() {
     this.store.uninit();
     this.initialized = false;
   }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -0,0 +1,197 @@
+/* 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/. */
+/* globals AboutNewTab, RemotePages, XPCOMUtils */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {
+  actionUtils: au,
+  actionCreators: ac,
+  actionTypes: at
+} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
+  "resource:///modules/AboutNewTab.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
+  "resource://gre/modules/RemotePageManager.jsm");
+
+const ABOUT_NEW_TAB_URL = "about:newtab";
+
+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"
+};
+
+this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
+  /**
+   * ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
+   *                  Call .createChannel to start the connection, and .destroyChannel to destroy it.
+   *                  You should use the BroadcastToContent, SendToContent, and SendToMain action creators
+   *                  in common/Actions.jsm 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 a RemotePageManager should be attached.
+   *                                  Note that if it is about:newtab, the existing RemotePageManager
+   *                                  for about:newtab will also be disabled
+   * @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.channel = null;
+
+    this.middleware = this.middleware.bind(this);
+    this.onMessage = this.onMessage.bind(this);
+    this.onNewTabLoad = this.onNewTabLoad.bind(this);
+    this.onNewTabUnload = this.onNewTabUnload.bind(this);
+  }
+
+  /**
+   * middleware - Redux middleware that looks for SendToContent and BroadcastToContent type
+   *              actions, and sends them out.
+   *
+   * @param  {object} store A redux store
+   * @return {function} Redux middleware
+   */
+  middleware(store) {
+    return next => action => {
+      if (!this.channel) {
+        next(action);
+        return;
+      }
+      if (au.isSendToContent(action)) {
+        this.send(action);
+      } else if (au.isBroadcastToContent(action)) {
+        this.broadcast(action);
+      }
+      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.SendToMain(action, {fromTarget: targetId}));
+  }
+
+  /**
+   * broadcast - Sends an action to all ports
+   *
+   * @param  {object} action A Redux action
+   */
+  broadcast(action) {
+    this.channel.sendAsyncMessage(this.outgoingMessageName, action);
+  }
+
+  /**
+   * 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);
+    if (!target) {
+      // The target is no longer around - maybe the user closed the page
+      return;
+    }
+    target.sendAsyncMessage(this.outgoingMessageName, action);
+  }
+
+  /**
+   * getIdByTarget - Retrieve the id of a message target, if it exists in this.targets
+   *
+   * @param  {obj} targetObj A message target
+   * @return {string|null} The unique id of the target, if it exists.
+   */
+  getTargetById(id) {
+    for (let port of this.channel.messagePorts) {
+      if (port.portID === id) {
+        return port;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * createChannel - Create RemotePages channel to establishing message passing
+   *                 between the main process and child pages
+   */
+  createChannel() {
+    //  RemotePageManager must be disabled for about:newtab, since only one can exist at once
+    if (this.pageURL === ABOUT_NEW_TAB_URL) {
+      AboutNewTab.override();
+    }
+    this.channel = new RemotePages(this.pageURL);
+    this.channel.addMessageListener("RemotePage:Load", this.onNewTabLoad);
+    this.channel.addMessageListener("RemotePage:Unload", this.onNewTabUnload);
+    this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
+  }
+
+  /**
+   * destroyChannel - Destroys the RemotePages channel
+   */
+  destroyChannel() {
+    this.channel.destroy();
+    this.channel = null;
+    if (this.pageURL === ABOUT_NEW_TAB_URL) {
+      AboutNewTab.reset();
+    }
+  }
+
+  /**
+   * onNewTabLoad - Handler for special RemotePage:Load message fired by RemotePages
+   *
+   * @param  {obj} msg The messsage from a page that was just loaded
+   */
+  onNewTabLoad(msg) {
+    this.onActionFromContent({type: at.NEW_TAB_LOAD}, msg.target.portID);
+  }
+
+  /**
+   * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired by RemotePages
+   *
+   * @param  {obj} msg The messsage from a page that was just unloaded
+   */
+  onNewTabUnload(msg) {
+    this.onActionFromContent({type: at.NEW_TAB_UNLOAD}, msg.target.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
+   */
+  onMessage(msg) {
+    const action = msg.data;
+    const {portID} = msg.target;
+    if (!action || !action.type) {
+      Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
+      return;
+    }
+    this.onActionFromContent(action, msg.target.portID);
+  }
+}
+
+this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
+this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm
@@ -0,0 +1,25 @@
+/* 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/. */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+/**
+ * NewTabInit - A placeholder for now. This will send a copy of the state to all
+ *              newly opened tabs.
+ */
+this.NewTabInit = class NewTabInit {
+  onAction(action) {
+    let newAction;
+    switch (action.type) {
+      case at.NEW_TAB_LOAD:
+        newAction = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
+        this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+        break;
+    }
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["NewTabInit"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -3,76 +3,83 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
+const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
 
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
- *         functionality. It accepts an array of "Feeds" on inititalization, which
+ *         functionality to allow for routing of actions between the Main processes
+ *         and child processes via a ActivityStreamMessageChannel.
+ *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
 this.Store = class Store {
 
   /**
-   * constructor - The redux store is created here,
+   * constructor - The redux store and message manager are created here,
    *               but no listeners are added until "init" is called.
    */
   constructor() {
     this._middleware = this._middleware.bind(this);
     // Bind each redux method so we can call it directly from the Store. E.g.,
     // store.dispatch() will call store._store.dispatch();
     ["dispatch", "getState", "subscribe"].forEach(method => {
       this[method] = function(...args) {
         return this._store[method](...args);
       }.bind(this);
     });
     this.feeds = new Set();
+    this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
-      redux.applyMiddleware(this._middleware)
+      redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
   /**
    * _middleware - This is redux middleware consumed by redux.createStore.
    *               it calls each feed's .onAction method, if one
    *               is defined.
    */
   _middleware(store) {
     return next => action => {
       next(action);
       this.feeds.forEach(s => s.onAction && s.onAction(action));
     };
   }
 
   /**
-   * init - Initializes the MessageManager channel, and adds feeds.
+   * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
    *        After initialization has finished, an INIT action is dispatched.
    *
    * @param  {array} feeds An array of objects with an optional .onAction method
    */
   init(feeds) {
     if (feeds) {
       feeds.forEach(subscriber => {
         subscriber.store = this;
         this.feeds.add(subscriber);
       });
     }
+    this._messageChannel.createChannel();
     this.dispatch({type: at.INIT});
   }
 
   /**
-   * uninit - Clears all feeds, dispatches an UNINIT action
+   * uninit - Clears all feeds, dispatches an UNINIT action, and
+   *          destroys the message manager channel.
    *
    * @return {type}  description
    */
   uninit() {
     this.feeds.clear();
     this.dispatch({type: at.UNINIT});
+    this._messageChannel.destroyChannel();
   }
 };
 
 this.EXPORTED_SYMBOLS = ["Store"];