diff options
Diffstat (limited to 'dom/base/DOMRequestHelper.sys.mjs')
-rw-r--r-- | dom/base/DOMRequestHelper.sys.mjs | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/dom/base/DOMRequestHelper.sys.mjs b/dom/base/DOMRequestHelper.sys.mjs new file mode 100644 index 0000000000..832c06c4de --- /dev/null +++ b/dom/base/DOMRequestHelper.sys.mjs @@ -0,0 +1,335 @@ +/* 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. + */ +export 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: ChromeUtils.generateQI([ + "nsISupportsWeakReference", + "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(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; + } + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + aMsg.weakRef + ? Services.cpmm.addWeakMessageListener(name, this) + : Services.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(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 + ? Services.cpmm.removeWeakMessageListener(aName, this) + : Services.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(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. + this.innerWindowID = this._window.windowGlobalChild.innerWindowId; + } + + this._destroyed = false; + + Services.obs.addObserver( + this, + "inner-window-destroyed", + /* weak-ref */ true + ); + }, + + destroyDOMRequestHelper() { + 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 + ? Services.cpmm.removeWeakMessageListener(aName, this) + : Services.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(aSubject, aTopic, aData) { + if (aTopic !== "inner-window-destroyed") { + return; + } + + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId != this.innerWindowID) { + return; + } + + this.destroyDOMRequestHelper(); + }, + + getRequestId(aRequest) { + if (!this._requests) { + this._requests = {}; + } + + let id = "id" + this._getRandomId(); + this._requests[id] = aRequest; + return id; + }, + + getPromiseResolverId(aPromiseResolver) { + // Delegates to getRequest() since the lookup table is agnostic about + // storage. + return this.getRequestId(aPromiseResolver); + }, + + getRequest(aId) { + if (this._requests && this._requests[aId]) { + return this._requests[aId]; + } + return undefined; + }, + + getPromiseResolver(aId) { + // Delegates to getRequest() since the lookup table is agnostic about + // storage. + return this.getRequest(aId); + }, + + removeRequest(aId) { + if (this._requests && this._requests[aId]) { + delete this._requests[aId]; + } + }, + + removePromiseResolver(aId) { + // Delegates to getRequest() since the lookup table is agnostic about + // storage. + this.removeRequest(aId); + }, + + takeRequest(aId) { + if (!this._requests || !this._requests[aId]) { + return null; + } + let request = this._requests[aId]; + delete this._requests[aId]; + return request; + }, + + takePromiseResolver(aId) { + // Delegates to getRequest() since the lookup table is agnostic about + // storage. + return this.takeRequest(aId); + }, + + _getRandomId() { + return Services.uuid.generateUUID().toString(); + }, + + createRequest() { + // If we don't have a valid window object, throw. + if (!this._window) { + console.error( + "DOMRequestHelper trying to create a DOMRequest without a valid window, failing." + ); + throw Components.Exception("", 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(aPromiseInit) { + // If we don't have a valid window object, throw. + if (!this._window) { + console.error( + "DOMRequestHelper trying to create a Promise without a valid window, failing." + ); + throw Components.Exception("", 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(aCallback) { + return this.createPromise((aResolve, aReject) => { + let resolverId = this.getPromiseResolverId({ + resolve: aResolve, + reject: aReject, + }); + aCallback(resolverId); + }); + }, + + forEachRequest(aCallback) { + if (!this._requests) { + return; + } + + Object.keys(this._requests).forEach(aKey => { + if (this._window.DOMRequest.isInstance(this.getRequest(aKey))) { + aCallback(aKey); + } + }); + }, + + forEachPromiseResolver(aCallback) { + if (!this._requests) { + return; + } + + Object.keys(this._requests).forEach(aKey => { + if ( + "resolve" in this.getPromiseResolver(aKey) && + "reject" in this.getPromiseResolver(aKey) + ) { + aCallback(aKey); + } + }); + }, +}; |