|
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/. */ |
|
4 |
|
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; |
|
23 |
|
24 this.EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"]; |
|
25 |
|
26 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
27 Cu.import("resource://gre/modules/Services.jsm"); |
|
28 |
|
29 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
30 "@mozilla.org/childprocessmessagemanager;1", |
|
31 "nsIMessageListenerManager"); |
|
32 |
|
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 } |
|
48 |
|
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]), |
|
56 |
|
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 } |
|
74 |
|
75 if (!this._listeners) { |
|
76 this._listeners = {}; |
|
77 } |
|
78 |
|
79 if (!Array.isArray(aMessages)) { |
|
80 aMessages = [aMessages]; |
|
81 } |
|
82 |
|
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 } |
|
94 |
|
95 aMsg.weakRef ? cpmm.addWeakMessageListener(name, this) |
|
96 : cpmm.addMessageListener(name, this); |
|
97 this._listeners[name] = !!aMsg.weakRef; |
|
98 }); |
|
99 }, |
|
100 |
|
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 } |
|
109 |
|
110 if (!Array.isArray(aMessages)) { |
|
111 aMessages = [aMessages]; |
|
112 } |
|
113 |
|
114 aMessages.forEach((aName) => { |
|
115 if (this._listeners[aName] == undefined) { |
|
116 return; |
|
117 } |
|
118 |
|
119 this._listeners[aName] ? cpmm.removeWeakMessageListener(aName, this) |
|
120 : cpmm.removeMessageListener(aName, this); |
|
121 delete this._listeners[aName]; |
|
122 }); |
|
123 }, |
|
124 |
|
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); |
|
147 |
|
148 if (aMessages) { |
|
149 this.addMessageListeners(aMessages); |
|
150 } |
|
151 |
|
152 this._id = this._getRandomId(); |
|
153 |
|
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 } |
|
161 |
|
162 this._destroyed = false; |
|
163 |
|
164 Services.obs.addObserver(this, "inner-window-destroyed", |
|
165 /* weak-ref */ true); |
|
166 }, |
|
167 |
|
168 destroyDOMRequestHelper: function() { |
|
169 if (this._destroyed) { |
|
170 return; |
|
171 } |
|
172 |
|
173 this._destroyed = true; |
|
174 |
|
175 Services.obs.removeObserver(this, "inner-window-destroyed"); |
|
176 |
|
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 } |
|
184 |
|
185 this._listeners = null; |
|
186 this._requests = null; |
|
187 |
|
188 // Objects inheriting from DOMRequestIPCHelper may have an uninit function. |
|
189 if (this.uninit) { |
|
190 this.uninit(); |
|
191 } |
|
192 |
|
193 this._window = null; |
|
194 }, |
|
195 |
|
196 observe: function(aSubject, aTopic, aData) { |
|
197 if (aTopic !== "inner-window-destroyed") { |
|
198 return; |
|
199 } |
|
200 |
|
201 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; |
|
202 if (wId != this.innerWindowID) { |
|
203 return; |
|
204 } |
|
205 |
|
206 this.destroyDOMRequestHelper(); |
|
207 }, |
|
208 |
|
209 getRequestId: function(aRequest) { |
|
210 if (!this._requests) { |
|
211 this._requests = {}; |
|
212 } |
|
213 |
|
214 let id = "id" + this._getRandomId(); |
|
215 this._requests[id] = aRequest; |
|
216 return id; |
|
217 }, |
|
218 |
|
219 getPromiseResolverId: function(aPromiseResolver) { |
|
220 // Delegates to getRequest() since the lookup table is agnostic about |
|
221 // storage. |
|
222 return this.getRequestId(aPromiseResolver); |
|
223 }, |
|
224 |
|
225 getRequest: function(aId) { |
|
226 if (this._requests && this._requests[aId]) { |
|
227 return this._requests[aId]; |
|
228 } |
|
229 }, |
|
230 |
|
231 getPromiseResolver: function(aId) { |
|
232 // Delegates to getRequest() since the lookup table is agnostic about |
|
233 // storage. |
|
234 return this.getRequest(aId); |
|
235 }, |
|
236 |
|
237 removeRequest: function(aId) { |
|
238 if (this._requests && this._requests[aId]) { |
|
239 delete this._requests[aId]; |
|
240 } |
|
241 }, |
|
242 |
|
243 removePromiseResolver: function(aId) { |
|
244 // Delegates to getRequest() since the lookup table is agnostic about |
|
245 // storage. |
|
246 this.removeRequest(aId); |
|
247 }, |
|
248 |
|
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 }, |
|
257 |
|
258 takePromiseResolver: function(aId) { |
|
259 // Delegates to getRequest() since the lookup table is agnostic about |
|
260 // storage. |
|
261 return this.takeRequest(aId); |
|
262 }, |
|
263 |
|
264 _getRandomId: function() { |
|
265 return Cc["@mozilla.org/uuid-generator;1"] |
|
266 .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); |
|
267 }, |
|
268 |
|
269 createRequest: function() { |
|
270 return Services.DOMRequest.createRequest(this._window); |
|
271 }, |
|
272 |
|
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 }, |
|
281 |
|
282 forEachRequest: function(aCallback) { |
|
283 if (!this._requests) { |
|
284 return; |
|
285 } |
|
286 |
|
287 Object.keys(this._requests).forEach((aKey) => { |
|
288 if (this.getRequest(aKey) instanceof this._window.DOMRequest) { |
|
289 aCallback(aKey); |
|
290 } |
|
291 }); |
|
292 }, |
|
293 |
|
294 forEachPromiseResolver: function(aCallback) { |
|
295 if (!this._requests) { |
|
296 return; |
|
297 } |
|
298 |
|
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 } |