зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
bce402d182
Коммит
b607e644b6
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче