Bug 1728955: part 3) Add logging to `nsBaseClipboard`. r=masayuki
[gecko.git] / dom / base / DOMRequestHelper.jsm
blob2b2aca05b464eb453ce88b524e5841f1b0ebb641
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * Helper object for APIs that deal with DOMRequests and Promises.
7  * It allows objects inheriting from it to create and keep track of DOMRequests
8  * and Promises objects in the common scenario where requests are created in
9  * the child, handed out to content and delivered to the parent within an async
10  * message (containing the identifiers of these requests). The parent may send
11  * messages back as answers to different requests and the child will use this
12  * helper to get the right request object. This helper also takes care of
13  * releasing the requests objects when the window goes out of scope.
14  *
15  * DOMRequestIPCHelper also deals with message listeners, allowing to add them
16  * to the child side of frame and process message manager and removing them
17  * when needed.
18  */
19 var EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"];
21 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
23 function DOMRequestIpcHelper() {
24   // _listeners keeps a list of messages for which we added a listener and the
25   // kind of listener that we added (strong or weak). It's an object of this
26   // form:
27   //  {
28   //    "message1": true,
29   //    "messagen": false
30   //  }
31   //
32   // where each property is the name of the message and its value is a boolean
33   // that indicates if the listener is weak or not.
34   this._listeners = null;
35   this._requests = null;
36   this._window = null;
39 DOMRequestIpcHelper.prototype = {
40   /**
41    * An object which "inherits" from DOMRequestIpcHelper and declares its own
42    * queryInterface method MUST implement Ci.nsISupportsWeakReference.
43    */
44   QueryInterface: ChromeUtils.generateQI([
45     "nsISupportsWeakReference",
46     "nsIObserver",
47   ]),
49   /**
50    *  'aMessages' is expected to be an array of either:
51    *  - objects of this form:
52    *    {
53    *      name: "messageName",
54    *      weakRef: false
55    *    }
56    *    where 'name' is the message identifier and 'weakRef' a boolean
57    *    indicating if the listener should be a weak referred one or not.
58    *
59    *  - or only strings containing the message name, in which case the listener
60    *    will be added as a strong reference by default.
61    */
62   addMessageListeners(aMessages) {
63     if (!aMessages) {
64       return;
65     }
67     if (!this._listeners) {
68       this._listeners = {};
69     }
71     if (!Array.isArray(aMessages)) {
72       aMessages = [aMessages];
73     }
75     aMessages.forEach(aMsg => {
76       let name = aMsg.name || aMsg;
77       // If the listener is already set and it is of the same type we just
78       // increase the count and bail out. If it is not of the same type,
79       // we throw an exception.
80       if (this._listeners[name] != undefined) {
81         if (!!aMsg.weakRef == this._listeners[name].weakRef) {
82           this._listeners[name].count++;
83           return;
84         } else {
85           throw Components.Exception("", Cr.NS_ERROR_FAILURE);
86         }
87       }
89       aMsg.weakRef
90         ? Services.cpmm.addWeakMessageListener(name, this)
91         : Services.cpmm.addMessageListener(name, this);
92       this._listeners[name] = {
93         weakRef: !!aMsg.weakRef,
94         count: 1,
95       };
96     });
97   },
99   /**
100    * 'aMessages' is expected to be a string or an array of strings containing
101    * the message names of the listeners to be removed.
102    */
103   removeMessageListeners(aMessages) {
104     if (!this._listeners || !aMessages) {
105       return;
106     }
108     if (!Array.isArray(aMessages)) {
109       aMessages = [aMessages];
110     }
112     aMessages.forEach(aName => {
113       if (this._listeners[aName] == undefined) {
114         return;
115       }
117       // Only remove the listener really when we don't have anybody that could
118       // be waiting on a message.
119       if (!--this._listeners[aName].count) {
120         this._listeners[aName].weakRef
121           ? Services.cpmm.removeWeakMessageListener(aName, this)
122           : Services.cpmm.removeMessageListener(aName, this);
123         delete this._listeners[aName];
124       }
125     });
126   },
128   /**
129    * Initialize the helper adding the corresponding listeners to the messages
130    * provided as the second parameter.
131    *
132    * 'aMessages' is expected to be an array of either:
133    *
134    *  - objects of this form:
135    *    {
136    *      name: 'messageName',
137    *      weakRef: false
138    *    }
139    *    where 'name' is the message identifier and 'weakRef' a boolean
140    *    indicating if the listener should be a weak referred one or not.
141    *
142    *  - or only strings containing the message name, in which case the listener
143    *    will be added as a strong referred one by default.
144    */
145   initDOMRequestHelper(aWindow, aMessages) {
146     // Query our required interfaces to force a fast fail if they are not
147     // provided. These calls will throw if the interface is not available.
148     this.QueryInterface(Ci.nsISupportsWeakReference);
149     this.QueryInterface(Ci.nsIObserver);
151     if (aMessages) {
152       this.addMessageListeners(aMessages);
153     }
155     this._id = this._getRandomId();
157     this._window = aWindow;
158     if (this._window) {
159       // We don't use this.innerWindowID, but other classes rely on it.
160       this.innerWindowID = this._window.windowGlobalChild.innerWindowId;
161     }
163     this._destroyed = false;
165     Services.obs.addObserver(
166       this,
167       "inner-window-destroyed",
168       /* weak-ref */ true
169     );
170   },
172   destroyDOMRequestHelper() {
173     if (this._destroyed) {
174       return;
175     }
177     this._destroyed = true;
179     Services.obs.removeObserver(this, "inner-window-destroyed");
181     if (this._listeners) {
182       Object.keys(this._listeners).forEach(aName => {
183         this._listeners[aName].weakRef
184           ? Services.cpmm.removeWeakMessageListener(aName, this)
185           : Services.cpmm.removeMessageListener(aName, this);
186       });
187     }
189     this._listeners = null;
190     this._requests = null;
192     // Objects inheriting from DOMRequestIPCHelper may have an uninit function.
193     if (this.uninit) {
194       this.uninit();
195     }
197     this._window = null;
198   },
200   observe(aSubject, aTopic, aData) {
201     if (aTopic !== "inner-window-destroyed") {
202       return;
203     }
205     let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
206     if (wId != this.innerWindowID) {
207       return;
208     }
210     this.destroyDOMRequestHelper();
211   },
213   getRequestId(aRequest) {
214     if (!this._requests) {
215       this._requests = {};
216     }
218     let id = "id" + this._getRandomId();
219     this._requests[id] = aRequest;
220     return id;
221   },
223   getPromiseResolverId(aPromiseResolver) {
224     // Delegates to getRequest() since the lookup table is agnostic about
225     // storage.
226     return this.getRequestId(aPromiseResolver);
227   },
229   getRequest(aId) {
230     if (this._requests && this._requests[aId]) {
231       return this._requests[aId];
232     }
233   },
235   getPromiseResolver(aId) {
236     // Delegates to getRequest() since the lookup table is agnostic about
237     // storage.
238     return this.getRequest(aId);
239   },
241   removeRequest(aId) {
242     if (this._requests && this._requests[aId]) {
243       delete this._requests[aId];
244     }
245   },
247   removePromiseResolver(aId) {
248     // Delegates to getRequest() since the lookup table is agnostic about
249     // storage.
250     this.removeRequest(aId);
251   },
253   takeRequest(aId) {
254     if (!this._requests || !this._requests[aId]) {
255       return null;
256     }
257     let request = this._requests[aId];
258     delete this._requests[aId];
259     return request;
260   },
262   takePromiseResolver(aId) {
263     // Delegates to getRequest() since the lookup table is agnostic about
264     // storage.
265     return this.takeRequest(aId);
266   },
268   _getRandomId() {
269     return Cc["@mozilla.org/uuid-generator;1"]
270       .getService(Ci.nsIUUIDGenerator)
271       .generateUUID()
272       .toString();
273   },
275   createRequest() {
276     // If we don't have a valid window object, throw.
277     if (!this._window) {
278       Cu.reportError(
279         "DOMRequestHelper trying to create a DOMRequest without a valid window, failing."
280       );
281       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
282     }
283     return Services.DOMRequest.createRequest(this._window);
284   },
286   /**
287    * createPromise() creates a new Promise, with `aPromiseInit` as the
288    * PromiseInit callback. The promise constructor is obtained from the
289    * reference to window owned by this DOMRequestIPCHelper.
290    */
291   createPromise(aPromiseInit) {
292     // If we don't have a valid window object, throw.
293     if (!this._window) {
294       Cu.reportError(
295         "DOMRequestHelper trying to create a Promise without a valid window, failing."
296       );
297       throw Components.Exception("", Cr.NS_ERROR_FAILURE);
298     }
299     return new this._window.Promise(aPromiseInit);
300   },
302   /**
303    * createPromiseWithId() creates a new Promise, accepting a callback
304    * which is immediately called with the generated resolverId.
305    */
306   createPromiseWithId(aCallback) {
307     return this.createPromise((aResolve, aReject) => {
308       let resolverId = this.getPromiseResolverId({
309         resolve: aResolve,
310         reject: aReject,
311       });
312       aCallback(resolverId);
313     });
314   },
316   forEachRequest(aCallback) {
317     if (!this._requests) {
318       return;
319     }
321     Object.keys(this._requests).forEach(aKey => {
322       if (this.getRequest(aKey) instanceof this._window.DOMRequest) {
323         aCallback(aKey);
324       }
325     });
326   },
328   forEachPromiseResolver(aCallback) {
329     if (!this._requests) {
330       return;
331     }
333     Object.keys(this._requests).forEach(aKey => {
334       if (
335         "resolve" in this.getPromiseResolver(aKey) &&
336         "reject" in this.getPromiseResolver(aKey)
337       ) {
338         aCallback(aKey);
339       }
340     });
341   },