Bug 1350411 - Add Message Channel for Activity Stream system add-on r=mconley

MozReview-Commit-ID: DCcGDjKdIHh

--HG--
extra : rebase_source : e4cb58d733c159aa9299348f089e062aa2c2bdd2
This commit is contained in:
k88hudson 2017-04-07 14:13:14 -04:00
Родитель bce402d182
Коммит b607e644b6
6 изменённых файлов: 371 добавлений и 7 удалений

Просмотреть файл

@ -3,9 +3,15 @@
* 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",
@ -14,6 +20,111 @@ this.actionTypes = [
// 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"
];

Просмотреть файл

@ -7,6 +7,25 @@
<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>

Просмотреть файл

@ -6,6 +6,9 @@
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 {
/**
@ -23,7 +26,9 @@ this.ActivityStream = class ActivityStream {
}
init() {
this.initialized = true;
this.store.init();
this.store.init([
new NewTabInit()
]);
}
uninit() {
this.store.uninit();

Просмотреть файл

@ -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"];

Просмотреть файл

@ -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"];

Просмотреть файл

@ -8,16 +8,19 @@ 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() {
@ -30,9 +33,10 @@ this.Store = class Store {
}.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)
);
}
@ -49,7 +53,7 @@ this.Store = class Store {
}
/**
* 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
@ -61,17 +65,20 @@ this.Store = class Store {
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();
}
};