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