michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Helper object for APIs that deal with DOMRequests and Promises. michael@0: * It allows objects inheriting from it to create and keep track of DOMRequests michael@0: * and Promises objects in the common scenario where requests are created in michael@0: * the child, handed out to content and delivered to the parent within an async michael@0: * message (containing the identifiers of these requests). The parent may send michael@0: * messages back as answers to different requests and the child will use this michael@0: * helper to get the right request object. This helper also takes care of michael@0: * releasing the requests objects when the window goes out of scope. michael@0: * michael@0: * DOMRequestIPCHelper also deals with message listeners, allowing to add them michael@0: * to the child side of frame and process message manager and removing them michael@0: * when needed. michael@0: */ michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"]; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageListenerManager"); michael@0: michael@0: this.DOMRequestIpcHelper = function DOMRequestIpcHelper() { michael@0: // _listeners keeps a list of messages for which we added a listener and the michael@0: // kind of listener that we added (strong or weak). It's an object of this michael@0: // form: michael@0: // { michael@0: // "message1": true, michael@0: // "messagen": false michael@0: // } michael@0: // michael@0: // where each property is the name of the message and its value is a boolean michael@0: // that indicates if the listener is weak or not. michael@0: this._listeners = null; michael@0: this._requests = null; michael@0: this._window = null; michael@0: } michael@0: michael@0: DOMRequestIpcHelper.prototype = { michael@0: /** michael@0: * An object which "inherits" from DOMRequestIpcHelper and declares its own michael@0: * queryInterface method MUST implement Ci.nsISupportsWeakReference. michael@0: */ michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, michael@0: Ci.nsIObserver]), michael@0: michael@0: /** michael@0: * 'aMessages' is expected to be an array of either: michael@0: * - objects of this form: michael@0: * { michael@0: * name: "messageName", michael@0: * weakRef: false michael@0: * } michael@0: * where 'name' is the message identifier and 'weakRef' a boolean michael@0: * indicating if the listener should be a weak referred one or not. michael@0: * michael@0: * - or only strings containing the message name, in which case the listener michael@0: * will be added as a strong reference by default. michael@0: */ michael@0: addMessageListeners: function(aMessages) { michael@0: if (!aMessages) { michael@0: return; michael@0: } michael@0: michael@0: if (!this._listeners) { michael@0: this._listeners = {}; michael@0: } michael@0: michael@0: if (!Array.isArray(aMessages)) { michael@0: aMessages = [aMessages]; michael@0: } michael@0: michael@0: aMessages.forEach((aMsg) => { michael@0: let name = aMsg.name || aMsg; michael@0: // If the listener is already set and it is of the same type we just michael@0: // bail out. If it is not of the same type, we throw an exception. michael@0: if (this._listeners[name] != undefined) { michael@0: if (!!aMsg.weakRef == this._listeners[name]) { michael@0: return; michael@0: } else { michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: michael@0: aMsg.weakRef ? cpmm.addWeakMessageListener(name, this) michael@0: : cpmm.addMessageListener(name, this); michael@0: this._listeners[name] = !!aMsg.weakRef; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * 'aMessages' is expected to be a string or an array of strings containing michael@0: * the message names of the listeners to be removed. michael@0: */ michael@0: removeMessageListeners: function(aMessages) { michael@0: if (!this._listeners || !aMessages) { michael@0: return; michael@0: } michael@0: michael@0: if (!Array.isArray(aMessages)) { michael@0: aMessages = [aMessages]; michael@0: } michael@0: michael@0: aMessages.forEach((aName) => { michael@0: if (this._listeners[aName] == undefined) { michael@0: return; michael@0: } michael@0: michael@0: this._listeners[aName] ? cpmm.removeWeakMessageListener(aName, this) michael@0: : cpmm.removeMessageListener(aName, this); michael@0: delete this._listeners[aName]; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the helper adding the corresponding listeners to the messages michael@0: * provided as the second parameter. michael@0: * michael@0: * 'aMessages' is expected to be an array of either: michael@0: * michael@0: * - objects of this form: michael@0: * { michael@0: * name: 'messageName', michael@0: * weakRef: false michael@0: * } michael@0: * where 'name' is the message identifier and 'weakRef' a boolean michael@0: * indicating if the listener should be a weak referred one or not. michael@0: * michael@0: * - or only strings containing the message name, in which case the listener michael@0: * will be added as a strong referred one by default. michael@0: */ michael@0: initDOMRequestHelper: function(aWindow, aMessages) { michael@0: // Query our required interfaces to force a fast fail if they are not michael@0: // provided. These calls will throw if the interface is not available. michael@0: this.QueryInterface(Ci.nsISupportsWeakReference); michael@0: this.QueryInterface(Ci.nsIObserver); michael@0: michael@0: if (aMessages) { michael@0: this.addMessageListeners(aMessages); michael@0: } michael@0: michael@0: this._id = this._getRandomId(); michael@0: michael@0: this._window = aWindow; michael@0: if (this._window) { michael@0: // We don't use this.innerWindowID, but other classes rely on it. michael@0: let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: this.innerWindowID = util.currentInnerWindowID; michael@0: } michael@0: michael@0: this._destroyed = false; michael@0: michael@0: Services.obs.addObserver(this, "inner-window-destroyed", michael@0: /* weak-ref */ true); michael@0: }, michael@0: michael@0: destroyDOMRequestHelper: function() { michael@0: if (this._destroyed) { michael@0: return; michael@0: } michael@0: michael@0: this._destroyed = true; michael@0: michael@0: Services.obs.removeObserver(this, "inner-window-destroyed"); michael@0: michael@0: if (this._listeners) { michael@0: Object.keys(this._listeners).forEach((aName) => { michael@0: this._listeners[aName] ? cpmm.removeWeakMessageListener(aName, this) michael@0: : cpmm.removeMessageListener(aName, this); michael@0: delete this._listeners[aName]; michael@0: }); michael@0: } michael@0: michael@0: this._listeners = null; michael@0: this._requests = null; michael@0: michael@0: // Objects inheriting from DOMRequestIPCHelper may have an uninit function. michael@0: if (this.uninit) { michael@0: this.uninit(); michael@0: } michael@0: michael@0: this._window = null; michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic !== "inner-window-destroyed") { michael@0: return; michael@0: } michael@0: michael@0: let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: if (wId != this.innerWindowID) { michael@0: return; michael@0: } michael@0: michael@0: this.destroyDOMRequestHelper(); michael@0: }, michael@0: michael@0: getRequestId: function(aRequest) { michael@0: if (!this._requests) { michael@0: this._requests = {}; michael@0: } michael@0: michael@0: let id = "id" + this._getRandomId(); michael@0: this._requests[id] = aRequest; michael@0: return id; michael@0: }, michael@0: michael@0: getPromiseResolverId: function(aPromiseResolver) { michael@0: // Delegates to getRequest() since the lookup table is agnostic about michael@0: // storage. michael@0: return this.getRequestId(aPromiseResolver); michael@0: }, michael@0: michael@0: getRequest: function(aId) { michael@0: if (this._requests && this._requests[aId]) { michael@0: return this._requests[aId]; michael@0: } michael@0: }, michael@0: michael@0: getPromiseResolver: function(aId) { michael@0: // Delegates to getRequest() since the lookup table is agnostic about michael@0: // storage. michael@0: return this.getRequest(aId); michael@0: }, michael@0: michael@0: removeRequest: function(aId) { michael@0: if (this._requests && this._requests[aId]) { michael@0: delete this._requests[aId]; michael@0: } michael@0: }, michael@0: michael@0: removePromiseResolver: function(aId) { michael@0: // Delegates to getRequest() since the lookup table is agnostic about michael@0: // storage. michael@0: this.removeRequest(aId); michael@0: }, michael@0: michael@0: takeRequest: function(aId) { michael@0: if (!this._requests || !this._requests[aId]) { michael@0: return null; michael@0: } michael@0: let request = this._requests[aId]; michael@0: delete this._requests[aId]; michael@0: return request; michael@0: }, michael@0: michael@0: takePromiseResolver: function(aId) { michael@0: // Delegates to getRequest() since the lookup table is agnostic about michael@0: // storage. michael@0: return this.takeRequest(aId); michael@0: }, michael@0: michael@0: _getRandomId: function() { michael@0: return Cc["@mozilla.org/uuid-generator;1"] michael@0: .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); michael@0: }, michael@0: michael@0: createRequest: function() { michael@0: return Services.DOMRequest.createRequest(this._window); michael@0: }, michael@0: michael@0: /** michael@0: * createPromise() creates a new Promise, with `aPromiseInit` as the michael@0: * PromiseInit callback. The promise constructor is obtained from the michael@0: * reference to window owned by this DOMRequestIPCHelper. michael@0: */ michael@0: createPromise: function(aPromiseInit) { michael@0: return new this._window.Promise(aPromiseInit); michael@0: }, michael@0: michael@0: forEachRequest: function(aCallback) { michael@0: if (!this._requests) { michael@0: return; michael@0: } michael@0: michael@0: Object.keys(this._requests).forEach((aKey) => { michael@0: if (this.getRequest(aKey) instanceof this._window.DOMRequest) { michael@0: aCallback(aKey); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: forEachPromiseResolver: function(aCallback) { michael@0: if (!this._requests) { michael@0: return; michael@0: } michael@0: michael@0: Object.keys(this._requests).forEach((aKey) => { michael@0: if ("resolve" in this.getPromiseResolver(aKey) && michael@0: "reject" in this.getPromiseResolver(aKey)) { michael@0: aCallback(aKey); michael@0: } michael@0: }); michael@0: }, michael@0: }