Index: lib/messaging.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/lib/messaging.js |
@@ -0,0 +1,309 @@ |
+/* |
+ * This file is part of Adblock Plus <https://adblockplus.org/>, |
+ * Copyright (C) 2006-2016 Eyeo GmbH |
+ * |
+ * Adblock Plus is free software: you can redistribute it and/or modify |
+ * it under the terms of the GNU General Public License version 3 as |
+ * published by the Free Software Foundation. |
+ * |
+ * Adblock Plus is distributed in the hope that it will be useful, |
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of |
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+ * GNU General Public License for more details. |
+ * |
+ * You should have received a copy of the GNU General Public License |
+ * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
+ */ |
+ |
+"use strict"; |
+ |
+const MESSAGE_NAME = "AdblockPlus:Message"; |
+const RESPONSE_NAME = "AdblockPlus:Response"; |
+ |
+function sendMessage(messageManager, messageName, payload, callbackID) |
+{ |
+ let request = {messageName, payload, callbackID}; |
+ if (messageManager instanceof Ci.nsIMessageSender) |
+ { |
+ messageManager.sendAsyncMessage(MESSAGE_NAME, request); |
+ return 1; |
+ } |
+ else if (messageManager instanceof Ci.nsIMessageBroadcaster) |
+ { |
+ messageManager.broadcastAsyncMessage(MESSAGE_NAME, request); |
+ return messageManager.childCount; |
+ } |
+ else |
+ { |
+ Cu.reportError("Unexpected message manager, impossible to send message"); |
+ return 0; |
+ } |
+} |
+ |
+function sendSyncMessage(messageManager, messageName, payload) |
+{ |
+ let request = {messageName, payload}; |
+ let responses = messageManager.sendRpcMessage(MESSAGE_NAME, request); |
+ for (let response of responses) |
+ if (typeof response != "undefined") |
+ return response; |
Thomas Greiner
2016/03/17 12:09:26
Detail: What's the reason for not using `flattenRe
Wladimir Palant
2016/03/21 15:31:31
No real reason, flattenResponses() was simply intr
|
+ return undefined; |
+} |
+ |
+function flattenResponses(responses, messageName) |
+{ |
+ let result = undefined; |
+ for (let response of responses) |
+ { |
+ if (typeof response == "undefined") |
Erik
2016/03/17 05:02:04
nit `==` -> `===`
Wladimir Palant
2016/03/21 15:31:32
We generally don't use === unless it makes a diffe
|
+ continue; |
+ |
+ if (typeof result == "undefined") |
Erik
2016/03/17 05:02:04
nit `==` -> `===`
Wladimir Palant
2016/03/21 15:31:33
Same as above.
|
+ result = response; |
+ else |
+ Cu.reportError("Got multiple responses to message '" + messageName + "', only first response was accepted."); |
+ } |
+ return result; |
+} |
+ |
+function getSender(origin) |
+{ |
+ if (origin instanceof Ci.nsIDOMXULElement) |
+ origin = origin.messageManager; |
+ |
+ if (origin instanceof Ci.nsIMessageSender) |
+ return new LightWeightPort(origin); |
+ else |
+ return null; |
+} |
+ |
+/** |
+ * Lightweight communication port allowing only sending messages. |
+ * @param {nsIMessageManager} messageManager |
+ * @constructor |
+ */ |
+function LightWeightPort(messageManager) |
+{ |
+ this._messageManager = messageManager; |
+} |
+LightWeightPort.prototype = |
+{ |
+ /** |
+ * @see Port#emit |
+ */ |
+ emit: function(messageName, payload) |
+ { |
+ sendMessage(this._messageManager, messageName, payload); |
+ }, |
+ |
+ /** |
+ * @see Port#emitSync |
+ */ |
+ emitSync: function(messageName, payload) |
+ { |
+ return sendSyncMessage(this._messageManager, messageName, payload); |
+ } |
+}; |
+ |
+/** |
+ * Communication port wrapping the message manager API to send and receive |
+ * messages. |
+ * @param {nsIMessageManager} messageManager |
+ * @constructor |
+ */ |
+function Port(messageManager) |
+{ |
+ this._messageManager = messageManager; |
+ |
+ this._callbacks = new Map(); |
+ this._responseCallbacks = new Map(); |
+ this._responseCallbackCounter = 0; |
+ |
+ this._handleRequest = this._handleRequest.bind(this); |
+ this._handleResponse = this._handleResponse.bind(this); |
+ this._messageManager.addMessageListener(MESSAGE_NAME, this._handleRequest); |
+ this._messageManager.addMessageListener(RESPONSE_NAME, this._handleResponse); |
+} |
+Port.prototype = { |
+ /** |
+ * Disables the port and makes it stop listening to incoming messages. |
+ */ |
+ disconnect: function() |
+ { |
+ this._messageManager.removeMessageListener(MESSAGE_NAME, this._handleRequest); |
+ this._messageManager.removeMessageListener(RESPONSE_NAME, this._handleResponse); |
+ }, |
+ |
+ _sendResponse: function(sender, callbackID, payload) |
+ { |
+ if (!sender || typeof callbackID == "undefined") |
+ return; |
+ |
+ var response = {callbackID, payload}; |
Thomas Greiner
2016/03/17 12:09:26
Detail: Unless there's a particular reason not to,
Wladimir Palant
2016/03/21 15:31:33
Done.
|
+ sender._messageManager.sendAsyncMessage(RESPONSE_NAME, response); |
+ }, |
+ |
+ _handleRequest: function(message) |
+ { |
+ let sender = getSender(message.target); |
+ let {callbackID, messageName, payload} = message.data; |
+ |
+ let result = this._dispatch(messageName, payload, sender); |
+ if (result && typeof result.then == "function") |
Erik
2016/03/17 05:02:04
It'd be nice to have a `isPromise` function to reu
Thomas Greiner
2016/03/17 12:09:26
You mean `result instanceof Promise`?
Wladimir Palant
2016/03/21 15:31:33
No, `result instanceof Promise` would be wrong - t
|
+ { |
+ // This is a promise - asynchronous response |
+ if (message.sync) |
+ { |
+ Cu.reportError("Asynchronous response to the synchronous message '" + messageName + "' is not possible"); |
+ return undefined; |
+ } |
+ |
+ result.then(result => { |
+ this._sendResponse(sender, callbackID, result) |
+ }, e => { |
+ Cu.reportError(e); |
+ this._sendResponse(sender, callbackID, undefined); |
+ }); |
+ } |
+ else |
+ this._sendResponse(sender, callbackID, result); |
+ |
+ return result; |
+ }, |
+ |
+ _handleResponse: function(message) |
+ { |
+ let {callbackID, payload} = message.data; |
+ if (!this._responseCallbacks.has(callbackID)) |
+ return; |
+ |
+ let [callback, messageName, responses, expectedResponses] = |
+ this._responseCallbacks.get(callbackID); |
+ responses.push(payload); |
Thomas Greiner
2016/03/17 12:09:26
Why do you wait for and store all the responses if
Wladimir Palant
2016/03/21 15:31:31
Because otherwise this code gets rather complicate
Thomas Greiner
2016/03/21 18:43:22
So decrementing `expectedResponses` is not an opti
Wladimir Palant
2016/03/21 20:42:34
Without storing all the values in an array we cann
|
+ if (responses.length == expectedResponses) |
Erik
2016/03/17 05:02:04
nit `==` -> `===`
Wladimir Palant
2016/03/21 15:31:32
Same as above.
|
+ { |
+ this._responseCallbacks.delete(callbackID); |
+ callback(flattenResponses(responses, messageName)); |
+ } |
+ }, |
+ |
+ _dispatch: function(messageName, payload, sender) |
+ { |
+ let callbacks = this._callbacks.get(messageName); |
+ if (!callbacks) |
+ return undefined; |
+ |
+ callbacks = callbacks.slice(); |
Thomas Greiner
2016/03/17 12:09:26
Why do you copy the array? I don't see that a call
Wladimir Palant
2016/03/21 15:31:32
It sure could. Classic example is a callback that
Thomas Greiner
2016/03/21 18:43:22
Of course, in theory it could happen because it's
Wladimir Palant
2016/03/21 20:42:34
No, it would merely have to call port.off(argument
Thomas Greiner
2016/03/22 11:15:07
Ah, right. Nevermind then.
|
+ let responses = []; |
+ for (let callback of callbacks) |
+ { |
+ try |
+ { |
+ responses.push(callback(payload, sender)); |
+ } |
+ catch (e) |
+ { |
+ Cu.reportError(e); |
+ } |
+ } |
+ return flattenResponses(responses, messageName); |
+ }, |
+ |
+ /** |
+ * Function to be called when a particular message is received |
+ * @callback Port~messageHandler |
+ * @param payload data attached to the message if any |
+ * @param {LightWeightPort} sender object that can be used to communicate with |
Erik
2016/03/17 05:02:05
this param appears to just accept a function, and
Wladimir Palant
2016/03/21 15:31:32
Explanation below.
|
+ * the sender of the message, could be null |
+ * @return the handler can return undefined (no response), a value (response |
+ * to be sent to sender immediately) or a promise (asynchronous |
+ * response). |
Erik
2016/03/17 05:02:05
`on` doesn't appear to return a promise, or anythi
Wladimir Palant
2016/03/21 15:31:32
Explanation below.
|
+ */ |
+ |
+ /** |
+ * Adds a handler for the specified message. |
+ * @param {string} messageName message that would trigger the callback |
+ * @param {Port~messageHandler} callback |
Erik
2016/03/17 05:02:04
same comment as above, this appears to just accept
Wladimir Palant
2016/03/21 15:31:32
Please see http://usejsdoc.org/tags-callback.html
|
+ */ |
+ on: function(messageName, callback) |
+ { |
+ if (!this._callbacks.has(messageName)) |
+ this._callbacks.set(messageName, []); |
Thomas Greiner
2016/03/17 12:09:26
Detail: Since you're using `Map` I wonder why you'
Wladimir Palant
2016/03/21 15:31:31
As Sebastian pointed out, Node.js (unlike Add-on S
Thomas Greiner
2016/03/21 18:43:23
Interesting, I wonder why they thought that'd be a
Wladimir Palant
2016/03/21 20:42:34
Probably because it's simpler/more efficient to im
|
+ |
+ let callbacks = this._callbacks.get(messageName); |
+ if (callbacks.indexOf(callback) < 0) |
+ callbacks.push(callback); |
+ }, |
+ |
+ /** |
+ * Removes a handler for the specified message. |
+ * @param {string} messageName message that would trigger the callback |
+ * @param {Port~messageHandler} callback |
Erik
2016/03/17 05:02:04
if I am not mistaken about the above comments for
Wladimir Palant
2016/03/21 15:31:32
Same as above.
|
+ */ |
+ off: function(messageName, callback) |
+ { |
+ let callbacks = this._callbacks.get(messageName); |
+ if (!callbacks) |
+ return; |
+ |
+ let index = callbacks.indexOf(callback); |
+ if (index >= 0) |
+ callbacks.splice(index, 1); |
+ }, |
+ |
+ /** |
+ * Sends a message. |
+ * @param {string} messageName message identifier |
+ * @param [payload] data to attach to the message |
Thomas Greiner
2016/03/17 12:09:26
Detail: Is this valid JSDoc syntax even with the t
Wladimir Palant
2016/03/21 15:31:32
Yes, it is.
|
+ */ |
+ emit: function(messageName, payload) |
+ { |
+ sendMessage(this._messageManager, messageName, payload, undefined); |
+ }, |
+ |
+ /** |
+ * Sends a message and expects a response. |
+ * @param {string} messageName message identifier |
+ * @param [payload] data to attach to the message |
+ * @return {Promise} promise that will be resolved with the response |
+ */ |
+ emitWithResponse: function(messageName, payload) |
+ { |
+ let callbackID = ++this._responseCallbackCounter; |
+ let expectedResponses = sendMessage( |
+ this._messageManager, messageName, payload, callbackID); |
+ return new Promise((resolve, reject) => { |
Erik
2016/03/17 05:02:04
nit add a newline here?
Wladimir Palant
2016/03/21 15:31:32
Done.
|
+ this._responseCallbacks.set(callbackID, |
+ [resolve, messageName, [], expectedResponses]); |
+ }); |
+ }, |
+ |
+ /** |
+ * Sends a synchonous message (DO NOT USE unless absolutely unavoidable). |
+ * @param {string} messageName message identifier |
+ * @param [payload] data to attach to the message |
+ * @return response returned by the handler |
+ */ |
+ emitSync: function(messageName, payload) |
+ { |
+ return sendSyncMessage(this._messageManager, messageName, payload); |
+ } |
+}; |
+exports.Port = Port; |
+ |
+let messageManager; |
+try |
+{ |
+ // Child |
+ messageManager = require("messageManager"); |
+} |
+catch (e) |
+{ |
+ // Parent |
+ messageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"] |
+ .getService(Ci.nsIMessageListenerManager); |
+} |
+ |
+let port = new Port(messageManager); |
+onShutdown.add(() => port.disconnect()); |
+exports.port = port; |