зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1210583: Part 1 - [webext] Add support for cross-process messaging with async responses. r=billm
--HG-- extra : commitid : 5deKex3Nhp extra : rebase_source : 9c31f41230bb46127dead36ccad4449cd35ce1b0 extra : histedit_source : 2646ebbe46d96f8e50db75247a7672e81d57a74c
This commit is contained in:
Родитель
e55a022204
Коммит
3d50a9eb7e
|
@ -8,6 +8,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
@ -448,14 +450,10 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
|||
let options = {
|
||||
js: [],
|
||||
css: [],
|
||||
};
|
||||
|
||||
// We need to send the inner window ID to make sure we only
|
||||
// execute the script if the window is currently navigated to
|
||||
// the document that we expect.
|
||||
//
|
||||
// TODO: When we add support for callbacks, non-matching
|
||||
// window IDs and insufficient permissions need to result in a
|
||||
// callback with |lastError| set.
|
||||
let recipient = {
|
||||
extensionId: extension.id,
|
||||
innerWindowID: tab.linkedBrowser.innerWindowID,
|
||||
};
|
||||
|
||||
|
@ -487,8 +485,8 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
|||
if (details.runAt !== null) {
|
||||
options.run_at = details.runAt;
|
||||
}
|
||||
mm.sendAsyncMessage("Extension:Execute",
|
||||
{extensionId: extension.id, options});
|
||||
|
||||
MessageChannel.sendMessage(mm, "Extension:Execute", { options }, recipient);
|
||||
|
||||
// TODO: Call the callback with the result (which is what???).
|
||||
},
|
||||
|
|
|
@ -50,6 +50,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
||||
|
||||
|
@ -1076,6 +1078,8 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
|||
|
||||
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
|
||||
|
||||
MessageChannel.abortResponses({ extensionId: this.id });
|
||||
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
|
||||
// Clean up a generated file.
|
||||
|
|
|
@ -29,6 +29,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
|||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
|
@ -137,16 +139,6 @@ Script.prototype = {
|
|||
return false;
|
||||
}
|
||||
|
||||
if ("innerWindowID" in this.options) {
|
||||
let innerWindowID = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.currentInnerWindowID;
|
||||
|
||||
if (innerWindowID !== this.options.innerWindowID) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: match_about_blank.
|
||||
|
||||
return true;
|
||||
|
@ -404,6 +396,8 @@ var DocumentManager = {
|
|||
} else if (topic == "inner-window-destroyed") {
|
||||
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
|
||||
MessageChannel.abortResponses({ innerWindowID: windowId });
|
||||
|
||||
// Close any existent content-script context for the destroyed window.
|
||||
if (this.contentScriptWindows.has(windowId)) {
|
||||
let extensions = this.contentScriptWindows.get(windowId);
|
||||
|
@ -446,7 +440,7 @@ var DocumentManager = {
|
|||
let window = global.content;
|
||||
let context = this.getContentScriptContext(extensionId, window);
|
||||
if (!context) {
|
||||
return;
|
||||
throw new Error("Unexpected add-on ID");
|
||||
}
|
||||
|
||||
// TODO: Somehow make sure we have the right permissions for this origin!
|
||||
|
@ -535,6 +529,8 @@ var DocumentManager = {
|
|||
}
|
||||
}
|
||||
|
||||
MessageChannel.abortResponses({ extensionId });
|
||||
|
||||
this.extensionCount--;
|
||||
if (this.extensionCount == 0) {
|
||||
this.uninit();
|
||||
|
@ -645,44 +641,60 @@ ExtensionManager = {
|
|||
},
|
||||
};
|
||||
|
||||
class ExtensionGlobal {
|
||||
constructor(global) {
|
||||
this.global = global;
|
||||
|
||||
MessageChannel.addListener(global, "Extension:Execute", this);
|
||||
|
||||
this.broker = new MessageBroker([global]);
|
||||
|
||||
this.windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
|
||||
global.sendAsyncMessage("Extension:TopWindowID", { windowId: this.windowId });
|
||||
}
|
||||
|
||||
uninit() {
|
||||
this.global.sendAsyncMessage("Extension:RemoveTopWindowID", { windowId: this.windowId });
|
||||
}
|
||||
|
||||
get messageFilter() {
|
||||
return {
|
||||
innerWindowID: this.global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.currentInnerWindowID,
|
||||
};
|
||||
}
|
||||
|
||||
receiveMessage({ target, messageName, recipient, data }) {
|
||||
switch (messageName) {
|
||||
case "Extension:Execute":
|
||||
let script = new Script(data.options);
|
||||
let { extensionId } = recipient;
|
||||
DocumentManager.executeScript(target, extensionId, script);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ExtensionContent = {
|
||||
globals: new Map(),
|
||||
|
||||
init(global) {
|
||||
let broker = new MessageBroker([global]);
|
||||
this.globals.set(global, broker);
|
||||
|
||||
global.addMessageListener("Extension:Execute", this);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:TopWindowID", {windowId});
|
||||
this.globals.set(global, new ExtensionGlobal(global));
|
||||
},
|
||||
|
||||
uninit(global) {
|
||||
this.globals.get(global).uninit();
|
||||
this.globals.delete(global);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId});
|
||||
},
|
||||
|
||||
getBroker(messageManager) {
|
||||
return this.globals.get(messageManager);
|
||||
},
|
||||
|
||||
receiveMessage({target, name, data}) {
|
||||
switch (name) {
|
||||
case "Extension:Execute":
|
||||
let script = new Script(data.options);
|
||||
let {extensionId} = data;
|
||||
DocumentManager.executeScript(target, extensionId, script);
|
||||
break;
|
||||
}
|
||||
return this.globals.get(messageManager).broker;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,610 @@
|
|||
/* 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 module provides wrappers around standard message managers to
|
||||
* simplify bidirectional communication. It currently allows a caller to
|
||||
* send a message to a single listener, and receive a reply. If there
|
||||
* are no matching listeners, or the message manager disconnects before
|
||||
* a reply is received, the caller is returned an error.
|
||||
*
|
||||
* Since each message must have only one recipient, the listener end may
|
||||
* specify filters for the messages it wishes to receive, and the sender
|
||||
* end likewise may specify recipient tags to match the filters.
|
||||
*
|
||||
* The message handler on the listener side may return its response
|
||||
* value directly, or may return a promise, the resolution or rejection
|
||||
* of which will be returned instead. The sender end likewise receives a
|
||||
* promise which resolves or rejects to the listener's response.
|
||||
*
|
||||
*
|
||||
* A basic setup works something like this:
|
||||
*
|
||||
* A content script adds a message listener to its global
|
||||
* nsIContentFrameMessageManager, with an appropriate set of filters:
|
||||
*
|
||||
* {
|
||||
* init(messageManager, window, extensionID) {
|
||||
* this.window = window;
|
||||
*
|
||||
* MessageChannel.addListener(
|
||||
* messageManager, "ContentScript:TouchContent",
|
||||
* this);
|
||||
*
|
||||
* this.messageFilter = {
|
||||
* innerWindowID: getInnerWindowID(window),
|
||||
* extensionID: extensionID,
|
||||
* };
|
||||
* },
|
||||
*
|
||||
* receiveMessage({ target, messageName, sender, recipient, data }) {
|
||||
* if (messageName == "ContentScript:TouchContent") {
|
||||
* return new Promise(resolve => {
|
||||
* this.touchWindow(data.touchWith, result => {
|
||||
* resolve({ touchResult: result });
|
||||
* });
|
||||
* });
|
||||
* }
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* A script in the parent process sends a message to the content process
|
||||
* via a tab message manager, including recipient tags to match its
|
||||
* filter, and an optional sender tag to identify itself:
|
||||
*
|
||||
* let data = { touchWith: "pencil" };
|
||||
* let sender = { extensionID, contextID };
|
||||
* let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
|
||||
*
|
||||
* MessageChannel.sendMessage(
|
||||
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
|
||||
* data, recipient, sender
|
||||
* ).then(result => {
|
||||
* alert(result.touchResult);
|
||||
* });
|
||||
*
|
||||
* Since the lifetimes of message senders and receivers may not always
|
||||
* match, either side of the message channel may cancel pending
|
||||
* responses which match its sender or recipient tags.
|
||||
*
|
||||
* For the above client, this might be done from an
|
||||
* inner-window-destroyed observer, when its target scope is destroyed:
|
||||
*
|
||||
* observe(subject, topic, data) {
|
||||
* if (topic == "inner-window-destroyed") {
|
||||
* let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
*
|
||||
* MessageChannel.abortResponses({ innerWindowID });
|
||||
* }
|
||||
* },
|
||||
*
|
||||
* From the parent, it may be done when its context is being destroyed:
|
||||
*
|
||||
* onDestroy() {
|
||||
* MessageChannel.abortResponses({
|
||||
* extensionID: this.extensionID,
|
||||
* contextID: this.contextID,
|
||||
* });
|
||||
* },
|
||||
*
|
||||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MessageChannel"];
|
||||
|
||||
/* globals MessageChannel */
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
|
||||
/**
|
||||
* Handles the mapping and dispatching of messages to their registered
|
||||
* handlers. There is one broker per message manager and class of
|
||||
* messages. Each class of messages is mapped to one native message
|
||||
* name, e.g., "MessageChannel:Message", and is dispatched to handlers
|
||||
* based on an internal message name, e.g., "Extension:ExecuteScript".
|
||||
*/
|
||||
class FilteringMessageManager {
|
||||
/**
|
||||
* @param {string} messageName
|
||||
* The name of the native message this broker listens for.
|
||||
* @param {function} callback
|
||||
* A function which is called for each message after it has been
|
||||
* mapped to its handler. The function receives two arguments:
|
||||
*
|
||||
* result:
|
||||
* An object containing either a `handler` or an `error` property.
|
||||
* If no error occurs, `handler` will be a matching handler that
|
||||
* was registered by `addHandler`. Otherwise, the `error` property
|
||||
* will contain an object describing the error.
|
||||
*
|
||||
* data:
|
||||
* An object describing the message, as defined in
|
||||
* `MessageChannel.addListener`.
|
||||
*/
|
||||
constructor(messageName, callback, messageManager) {
|
||||
this.messageName = messageName;
|
||||
this.callback = callback;
|
||||
this.messageManager = messageManager;
|
||||
|
||||
this.messageManager.addMessageListener(this.messageName, this);
|
||||
|
||||
this.handlers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a message from our message manager, maps it to a handler, and
|
||||
* passes the result to our message callback.
|
||||
*/
|
||||
receiveMessage({ data, target }) {
|
||||
let handlers = Array.from(this.getHandlers(data.messageName, data.recipient));
|
||||
|
||||
let result = {};
|
||||
if (handlers.length == 0) {
|
||||
result.error = { result: MessageChannel.RESULT_NO_HANDLER,
|
||||
message: "No matching message handler" };
|
||||
} else if (handlers.length > 1) {
|
||||
result.error = { result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
|
||||
message: `Multiple matching handlers for ${data.messageName}` };
|
||||
} else {
|
||||
result.handler = handlers[0];
|
||||
}
|
||||
|
||||
data.target = target;
|
||||
this.callback(result, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all handlers for the given message name. If `recipient`
|
||||
* is provided, only iterates over handlers whose filters match it.
|
||||
*
|
||||
* @param {string|number} messageName
|
||||
* The message for which to return handlers.
|
||||
* @param {object} recipient
|
||||
* The recipient data on which to filter handlers.
|
||||
*/
|
||||
* getHandlers(messageName, recipient) {
|
||||
let handlers = this.handlers.get(messageName) || new Set();
|
||||
for (let handler of handlers) {
|
||||
if (MessageChannel.matchesFilter(handler.messageFilter, recipient)) {
|
||||
yield handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler for the given message.
|
||||
*
|
||||
* @param {string} messageName
|
||||
* The internal message name for which to register the handler.
|
||||
* @param {object} handler
|
||||
* An opaque handler object. The object must have a `messageFilter`
|
||||
* property on which to filter messages. Final dispatching is handled
|
||||
* by the message callback passed to the constructor.
|
||||
*/
|
||||
addHandler(messageName, handler) {
|
||||
if (!this.handlers.has(messageName)) {
|
||||
this.handlers.set(messageName, new Set());
|
||||
}
|
||||
|
||||
this.handlers.get(messageName).add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a handler for the given message.
|
||||
*
|
||||
* @param {string} messageName
|
||||
* The internal message name for which to unregister the handler.
|
||||
* @param {object} handler
|
||||
* The handler object to unregister.
|
||||
*/
|
||||
removeHandler(messageName, handler) {
|
||||
this.handlers.get(messageName).delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages mappings of message managers to their corresponding message
|
||||
* brokers. Brokers are lazily created for each message manager the
|
||||
* first time they are accessed. In the case of content frame message
|
||||
* managers, they are also automatically destroyed when the frame
|
||||
* unload event fires.
|
||||
*/
|
||||
class FilteringMessageManagerMap extends Map {
|
||||
// Unfortunately, we can't use a WeakMap for this, because message
|
||||
// managers do not support preserved wrappers.
|
||||
|
||||
/**
|
||||
* @param {string} messageName
|
||||
* The native message name passed to `FilteringMessageManager` constructors.
|
||||
* @param {function} callback
|
||||
* The message callback function passed to
|
||||
* `FilteringMessageManager` constructors.
|
||||
*/
|
||||
constructor(messageName, callback) {
|
||||
super();
|
||||
|
||||
this.messageName = messageName;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, and possibly creates, a message broker for the given
|
||||
* message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager for which to return a broker.
|
||||
*
|
||||
* @returns {FilteringMessageManager}
|
||||
*/
|
||||
get(target) {
|
||||
if (this.has(target)) {
|
||||
return super.get(target);
|
||||
}
|
||||
|
||||
let broker = new FilteringMessageManager(this.messageName, this.callback, target);
|
||||
this.set(target, broker);
|
||||
|
||||
if (target instanceof Ci.nsIDOMEventTarget) {
|
||||
let onUnload = event => {
|
||||
target.removeEventListener("unload", onUnload);
|
||||
this.delete(target);
|
||||
};
|
||||
target.addEventListener("unload", onUnload);
|
||||
}
|
||||
|
||||
return broker;
|
||||
}
|
||||
}
|
||||
|
||||
const MESSAGE_MESSAGE = "MessageChannel:Message";
|
||||
const MESSAGE_RESPONSE = "MessageChannel:Response";
|
||||
|
||||
let gChannelId = 0;
|
||||
|
||||
this.MessageChannel = {
|
||||
init() {
|
||||
Services.obs.addObserver(this, "message-manager-close", false);
|
||||
Services.obs.addObserver(this, "message-manager-disconnect", false);
|
||||
|
||||
this.messageManagers = new FilteringMessageManagerMap(
|
||||
MESSAGE_MESSAGE, this._handleMessage.bind(this));
|
||||
|
||||
this.responseManagers = new FilteringMessageManagerMap(
|
||||
MESSAGE_RESPONSE, this._handleResponse.bind(this));
|
||||
|
||||
/**
|
||||
* Contains a list of pending responses, either waiting to be
|
||||
* received or waiting to be sent. @see _addPendingResponse
|
||||
*/
|
||||
this.pendingResponses = new Set();
|
||||
},
|
||||
|
||||
RESULT_SUCCESS: 0,
|
||||
RESULT_DISCONNECTED: 1,
|
||||
RESULT_NO_HANDLER: 2,
|
||||
RESULT_MULTIPLE_HANDLERS: 3,
|
||||
RESULT_ERROR: 4,
|
||||
|
||||
REASON_DISCONNECTED: {
|
||||
result: this.RESULT_DISCONNECTED,
|
||||
message: "Message manager disconnected",
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the given `data` object matches the given `filter`
|
||||
* object. The objects match if every property of `filter` is present
|
||||
* in `data`, and the values in both objects are strictly equal.
|
||||
*
|
||||
* @param {object} filter
|
||||
* The filter object to match against.
|
||||
* @param {object} data
|
||||
* The data object being matched.
|
||||
* @returns {bool} True if the objects match.
|
||||
*/
|
||||
matchesFilter(filter, data) {
|
||||
return Object.keys(filter).every(key => {
|
||||
return key in data && data[key] === filter[key];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a message listener to the given message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to listen.
|
||||
* @param {string|number} messageName
|
||||
* The name of the message to listen for.
|
||||
* @param {MessageReceiver} handler
|
||||
* The handler to dispatch to. Must be an object with the following
|
||||
* properties:
|
||||
*
|
||||
* receiveMessage:
|
||||
* A method which is called for each message received by the
|
||||
* listener. The method takes one argument, an object, with the
|
||||
* following properties:
|
||||
*
|
||||
* messageName:
|
||||
* The internal message name, as passed to `sendMessage`.
|
||||
*
|
||||
* target:
|
||||
* The message manager which received this message.
|
||||
*
|
||||
* channelId:
|
||||
* The internal ID of the transaction, used to map responses to
|
||||
* the original sender.
|
||||
*
|
||||
* sender:
|
||||
* An object describing the sender, as passed to `sendMessage`.
|
||||
*
|
||||
* recipient:
|
||||
* An object describing the recipient, as passed to
|
||||
* `sendMessage`.
|
||||
*
|
||||
* data:
|
||||
* The contents of the message, as passed to `sendMessage`.
|
||||
*
|
||||
* The method may return any structured-clone-compatible
|
||||
* object, which will be returned as a response to the message
|
||||
* sender. It may also instead return a `Promise`, the
|
||||
* resolution or rejection value of which will likewise be
|
||||
* returned to the message sender.
|
||||
*
|
||||
* messageFilter:
|
||||
* An object containing arbitrary properties on which to filter
|
||||
* received messages. Messages will only be dispatched to this
|
||||
* object if the `recipient` object passed to `sendMessage`
|
||||
* matches this filter, as determined by `matchesFilter`.
|
||||
*/
|
||||
addListener(target, messageName, handler) {
|
||||
this.messageManagers.get(target).addHandler(messageName, handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a message listener from the given message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to stop listening.
|
||||
* @param {string|number} messageName
|
||||
* The name of the message to stop listening for.
|
||||
* @param {MessageReceiver} handler
|
||||
* The handler to stop dispatching to.
|
||||
*/
|
||||
removeListener(target, messageName, handler) {
|
||||
this.messageManagers.get(target).removeListener(messageName, handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message via the given message manager. Returns a promise which
|
||||
* resolves or rejects with the return value of the message receiver.
|
||||
*
|
||||
* The promise also rejects if there is no matching listener, or the other
|
||||
* side of the message manager disconnects before the response is received.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to send the message.
|
||||
* @param {string} messageName
|
||||
* The name of the message to send, as passed to `addListener`.
|
||||
* @param {object} data
|
||||
* A structured-clone-compatible object to send to the message
|
||||
* recipient.
|
||||
* @param {object} [recipient]
|
||||
* A structured-clone-compatible object to identify the message
|
||||
* recipient. The object must match the `messageFilter` defined by
|
||||
* recipients in order for the message to be received.
|
||||
* @param {object} [sender]
|
||||
* A structured-clone-compatible object to identify the message
|
||||
* sender. This object may also be used as a filter to prematurely
|
||||
* abort responses when the sender is being destroyed.
|
||||
* @see `abortResponses`.
|
||||
* @returns Promise
|
||||
*/
|
||||
sendMessage(target, messageName, data, recipient = {}, sender = {}) {
|
||||
let channelId = gChannelId++;
|
||||
let message = { messageName, channelId, sender, recipient, data };
|
||||
|
||||
let deferred = PromiseUtils.defer();
|
||||
deferred.messageFilter = {};
|
||||
deferred.sender = recipient;
|
||||
deferred.messageManager = target;
|
||||
|
||||
this._addPendingResponse(deferred);
|
||||
|
||||
// The channel ID is used as the message name when routing responses.
|
||||
// Add a message listener to the response broker, and remove it once
|
||||
// we've gotten (or canceled) a response.
|
||||
let broker = this.responseManagers.get(target);
|
||||
broker.addHandler(channelId, deferred);
|
||||
|
||||
let cleanup = () => {
|
||||
broker.removeHandler(channelId, deferred);
|
||||
};
|
||||
deferred.promise.then(cleanup, cleanup);
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles dispatching message callbacks from the message brokers to their
|
||||
* appropriate `MessageReceivers`, and routing the responses back to the
|
||||
* original senders.
|
||||
*
|
||||
* Each handler object is a `MessageReceiver` object as passed to
|
||||
* `addListener`.
|
||||
*/
|
||||
_handleMessage({ handler, error }, data) {
|
||||
// The target passed to `receiveMessage` is sometimes a message manager
|
||||
// owner instead of a message manager, so make sure to convert it to a
|
||||
// message manager first if necessary.
|
||||
let { target } = data;
|
||||
if (!(target instanceof Ci.nsIMessageSender)) {
|
||||
target = target.messageManager;
|
||||
}
|
||||
|
||||
let deferred = {
|
||||
sender: data.sender,
|
||||
messageManager: target,
|
||||
};
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.reject = reject;
|
||||
|
||||
if (handler) {
|
||||
let result = handler.receiveMessage(data);
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
}).then(
|
||||
value => {
|
||||
let response = {
|
||||
result: this.RESULT_SUCCESS,
|
||||
messageName: data.channelId,
|
||||
recipient: {},
|
||||
value,
|
||||
};
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
|
||||
},
|
||||
error => {
|
||||
let response = {
|
||||
result: this.RESULT_ERROR,
|
||||
messageName: data.channelId,
|
||||
recipient: {},
|
||||
error,
|
||||
};
|
||||
|
||||
if (error && typeof(error) == "object") {
|
||||
if (error.result) {
|
||||
response.result = error.result;
|
||||
}
|
||||
}
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
|
||||
});
|
||||
|
||||
this._addPendingResponse(deferred);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles message callbacks from the response brokers.
|
||||
*
|
||||
* Each handler object is a deferred object created by `sendMessage`, and
|
||||
* should be resolved or rejected based on the contents of the response.
|
||||
*/
|
||||
_handleResponse({ handler, error }, data) {
|
||||
if (error) {
|
||||
// If we have an error at this point, we have handler to report it to,
|
||||
// so just log it.
|
||||
Cu.reportError(error.message);
|
||||
} else if (data.result === this.RESULT_SUCCESS) {
|
||||
handler.resolve(data.value);
|
||||
} else {
|
||||
handler.reject(data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a pending response to the the `pendingResponses` list.
|
||||
*
|
||||
* The response object must be a deferred promise with the following
|
||||
* properties:
|
||||
*
|
||||
* promise:
|
||||
* The promise object which resolves or rejects when the response
|
||||
* is no longer pending.
|
||||
*
|
||||
* reject:
|
||||
* A function which, when called, causes the `promise` object to be
|
||||
* rejected.
|
||||
*
|
||||
* sender:
|
||||
* A sender object, as passed to `sendMessage.
|
||||
*
|
||||
* messageManager:
|
||||
* The message manager the response will be sent or received on.
|
||||
*
|
||||
* When the promise resolves or rejects, it will be removed from the
|
||||
* list.
|
||||
*
|
||||
* These values are used to clear pending responses when execution
|
||||
* contexts are destroyed.
|
||||
*/
|
||||
_addPendingResponse(deferred) {
|
||||
let cleanup = () => {
|
||||
this.pendingResponses.delete(deferred);
|
||||
};
|
||||
this.pendingResponses.add(deferred);
|
||||
deferred.promise.then(cleanup, cleanup);
|
||||
},
|
||||
|
||||
/**
|
||||
* Aborts any pending message responses to senders matching the given
|
||||
* filter.
|
||||
*
|
||||
* @param {object} sender
|
||||
* The object on which to filter senders, as determined by
|
||||
* `matchesFilter`.
|
||||
* @param {object} [reason]
|
||||
* An optional object describing the reason the response was aborted.
|
||||
* Will be passed to the promise rejection handler of all aborted
|
||||
* responses.
|
||||
*/
|
||||
abortResponses(sender, reason = this.REASON_DISCONNECTED) {
|
||||
for (let response of this.pendingResponses) {
|
||||
if (this.matchesFilter(sender, response.sender)) {
|
||||
response.reject(reason);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Aborts any pending message responses to the broker for the given
|
||||
* message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager for which to abort brokers.
|
||||
* @param {object} reason
|
||||
* An object describing the reason the responses were aborted.
|
||||
* Will be passed to the promise rejection handler of all aborted
|
||||
* responses.
|
||||
*/
|
||||
abortMessageManager(target, reason) {
|
||||
for (let response of this.pendingResponses) {
|
||||
if (response.messageManager === target) {
|
||||
response.reject(reason);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "message-manager-close":
|
||||
case "message-manager-disconnect":
|
||||
try {
|
||||
if (this.responseManagers.has(subject)) {
|
||||
this.abortMessageManager(subject, this.REASON_DISCONNECTED);
|
||||
}
|
||||
} finally {
|
||||
this.responseManagers.delete(subject);
|
||||
this.messageManagers.delete(subject);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
MessageChannel.init();
|
|
@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [
|
|||
'ExtensionManagement.jsm',
|
||||
'ExtensionStorage.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
'MessageChannel.jsm',
|
||||
'Schemas.jsm',
|
||||
]
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче