Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 const Cu = Components.utils;
20 const Cc = Components.classes;
21 const Ci = Components.interfaces;
22 const Cr = Components.results;
24 this.EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"];
26 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
27 Cu.import("resource://gre/modules/Services.jsm");
29 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
30 "@mozilla.org/childprocessmessagemanager;1",
31 "nsIMessageListenerManager");
33 this.DOMRequestIpcHelper = function DOMRequestIpcHelper() {
34 // _listeners keeps a list of messages for which we added a listener and the
35 // kind of listener that we added (strong or weak). It's an object of this
36 // form:
37 // {
38 // "message1": true,
39 // "messagen": false
40 // }
41 //
42 // where each property is the name of the message and its value is a boolean
43 // that indicates if the listener is weak or not.
44 this._listeners = null;
45 this._requests = null;
46 this._window = null;
47 }
49 DOMRequestIpcHelper.prototype = {
50 /**
51 * An object which "inherits" from DOMRequestIpcHelper and declares its own
52 * queryInterface method MUST implement Ci.nsISupportsWeakReference.
53 */
54 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
55 Ci.nsIObserver]),
57 /**
58 * 'aMessages' is expected to be an array of either:
59 * - objects of this form:
60 * {
61 * name: "messageName",
62 * weakRef: false
63 * }
64 * where 'name' is the message identifier and 'weakRef' a boolean
65 * indicating if the listener should be a weak referred one or not.
66 *
67 * - or only strings containing the message name, in which case the listener
68 * will be added as a strong reference by default.
69 */
70 addMessageListeners: function(aMessages) {
71 if (!aMessages) {
72 return;
73 }
75 if (!this._listeners) {
76 this._listeners = {};
77 }
79 if (!Array.isArray(aMessages)) {
80 aMessages = [aMessages];
81 }
83 aMessages.forEach((aMsg) => {
84 let name = aMsg.name || aMsg;
85 // If the listener is already set and it is of the same type we just
86 // bail out. If it is not of the same type, we throw an exception.
87 if (this._listeners[name] != undefined) {
88 if (!!aMsg.weakRef == this._listeners[name]) {
89 return;
90 } else {
91 throw Cr.NS_ERROR_FAILURE;
92 }
93 }
95 aMsg.weakRef ? cpmm.addWeakMessageListener(name, this)
96 : cpmm.addMessageListener(name, this);
97 this._listeners[name] = !!aMsg.weakRef;
98 });
99 },
101 /**
102 * 'aMessages' is expected to be a string or an array of strings containing
103 * the message names of the listeners to be removed.
104 */
105 removeMessageListeners: function(aMessages) {
106 if (!this._listeners || !aMessages) {
107 return;
108 }
110 if (!Array.isArray(aMessages)) {
111 aMessages = [aMessages];
112 }
114 aMessages.forEach((aName) => {
115 if (this._listeners[aName] == undefined) {
116 return;
117 }
119 this._listeners[aName] ? cpmm.removeWeakMessageListener(aName, this)
120 : cpmm.removeMessageListener(aName, this);
121 delete this._listeners[aName];
122 });
123 },
125 /**
126 * Initialize the helper adding the corresponding listeners to the messages
127 * provided as the second parameter.
128 *
129 * 'aMessages' is expected to be an array of either:
130 *
131 * - objects of this form:
132 * {
133 * name: 'messageName',
134 * weakRef: false
135 * }
136 * where 'name' is the message identifier and 'weakRef' a boolean
137 * indicating if the listener should be a weak referred one or not.
138 *
139 * - or only strings containing the message name, in which case the listener
140 * will be added as a strong referred one by default.
141 */
142 initDOMRequestHelper: function(aWindow, aMessages) {
143 // Query our required interfaces to force a fast fail if they are not
144 // provided. These calls will throw if the interface is not available.
145 this.QueryInterface(Ci.nsISupportsWeakReference);
146 this.QueryInterface(Ci.nsIObserver);
148 if (aMessages) {
149 this.addMessageListeners(aMessages);
150 }
152 this._id = this._getRandomId();
154 this._window = aWindow;
155 if (this._window) {
156 // We don't use this.innerWindowID, but other classes rely on it.
157 let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
158 .getInterface(Ci.nsIDOMWindowUtils);
159 this.innerWindowID = util.currentInnerWindowID;
160 }
162 this._destroyed = false;
164 Services.obs.addObserver(this, "inner-window-destroyed",
165 /* weak-ref */ true);
166 },
168 destroyDOMRequestHelper: function() {
169 if (this._destroyed) {
170 return;
171 }
173 this._destroyed = true;
175 Services.obs.removeObserver(this, "inner-window-destroyed");
177 if (this._listeners) {
178 Object.keys(this._listeners).forEach((aName) => {
179 this._listeners[aName] ? cpmm.removeWeakMessageListener(aName, this)
180 : cpmm.removeMessageListener(aName, this);
181 delete this._listeners[aName];
182 });
183 }
185 this._listeners = null;
186 this._requests = null;
188 // Objects inheriting from DOMRequestIPCHelper may have an uninit function.
189 if (this.uninit) {
190 this.uninit();
191 }
193 this._window = null;
194 },
196 observe: function(aSubject, aTopic, aData) {
197 if (aTopic !== "inner-window-destroyed") {
198 return;
199 }
201 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
202 if (wId != this.innerWindowID) {
203 return;
204 }
206 this.destroyDOMRequestHelper();
207 },
209 getRequestId: function(aRequest) {
210 if (!this._requests) {
211 this._requests = {};
212 }
214 let id = "id" + this._getRandomId();
215 this._requests[id] = aRequest;
216 return id;
217 },
219 getPromiseResolverId: function(aPromiseResolver) {
220 // Delegates to getRequest() since the lookup table is agnostic about
221 // storage.
222 return this.getRequestId(aPromiseResolver);
223 },
225 getRequest: function(aId) {
226 if (this._requests && this._requests[aId]) {
227 return this._requests[aId];
228 }
229 },
231 getPromiseResolver: function(aId) {
232 // Delegates to getRequest() since the lookup table is agnostic about
233 // storage.
234 return this.getRequest(aId);
235 },
237 removeRequest: function(aId) {
238 if (this._requests && this._requests[aId]) {
239 delete this._requests[aId];
240 }
241 },
243 removePromiseResolver: function(aId) {
244 // Delegates to getRequest() since the lookup table is agnostic about
245 // storage.
246 this.removeRequest(aId);
247 },
249 takeRequest: function(aId) {
250 if (!this._requests || !this._requests[aId]) {
251 return null;
252 }
253 let request = this._requests[aId];
254 delete this._requests[aId];
255 return request;
256 },
258 takePromiseResolver: function(aId) {
259 // Delegates to getRequest() since the lookup table is agnostic about
260 // storage.
261 return this.takeRequest(aId);
262 },
264 _getRandomId: function() {
265 return Cc["@mozilla.org/uuid-generator;1"]
266 .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
267 },
269 createRequest: function() {
270 return Services.DOMRequest.createRequest(this._window);
271 },
273 /**
274 * createPromise() creates a new Promise, with `aPromiseInit` as the
275 * PromiseInit callback. The promise constructor is obtained from the
276 * reference to window owned by this DOMRequestIPCHelper.
277 */
278 createPromise: function(aPromiseInit) {
279 return new this._window.Promise(aPromiseInit);
280 },
282 forEachRequest: function(aCallback) {
283 if (!this._requests) {
284 return;
285 }
287 Object.keys(this._requests).forEach((aKey) => {
288 if (this.getRequest(aKey) instanceof this._window.DOMRequest) {
289 aCallback(aKey);
290 }
291 });
292 },
294 forEachPromiseResolver: function(aCallback) {
295 if (!this._requests) {
296 return;
297 }
299 Object.keys(this._requests).forEach((aKey) => {
300 if ("resolve" in this.getPromiseResolver(aKey) &&
301 "reject" in this.getPromiseResolver(aKey)) {
302 aCallback(aKey);
303 }
304 });
305 },
306 }