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 */ 19var EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"]; 20 21const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 22 23function 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; 37} 38 39DOMRequestIpcHelper.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 ]), 48 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 } 66 67 if (!this._listeners) { 68 this._listeners = {}; 69 } 70 71 if (!Array.isArray(aMessages)) { 72 aMessages = [aMessages]; 73 } 74 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 } 88 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 }, 98 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 } 107 108 if (!Array.isArray(aMessages)) { 109 aMessages = [aMessages]; 110 } 111 112 aMessages.forEach(aName => { 113 if (this._listeners[aName] == undefined) { 114 return; 115 } 116 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 }, 127 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); 150 151 if (aMessages) { 152 this.addMessageListeners(aMessages); 153 } 154 155 this._id = this._getRandomId(); 156 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 } 162 163 this._destroyed = false; 164 165 Services.obs.addObserver( 166 this, 167 "inner-window-destroyed", 168 /* weak-ref */ true 169 ); 170 }, 171 172 destroyDOMRequestHelper() { 173 if (this._destroyed) { 174 return; 175 } 176 177 this._destroyed = true; 178 179 Services.obs.removeObserver(this, "inner-window-destroyed"); 180 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 } 188 189 this._listeners = null; 190 this._requests = null; 191 192 // Objects inheriting from DOMRequestIPCHelper may have an uninit function. 193 if (this.uninit) { 194 this.uninit(); 195 } 196 197 this._window = null; 198 }, 199 200 observe(aSubject, aTopic, aData) { 201 if (aTopic !== "inner-window-destroyed") { 202 return; 203 } 204 205 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; 206 if (wId != this.innerWindowID) { 207 return; 208 } 209 210 this.destroyDOMRequestHelper(); 211 }, 212 213 getRequestId(aRequest) { 214 if (!this._requests) { 215 this._requests = {}; 216 } 217 218 let id = "id" + this._getRandomId(); 219 this._requests[id] = aRequest; 220 return id; 221 }, 222 223 getPromiseResolverId(aPromiseResolver) { 224 // Delegates to getRequest() since the lookup table is agnostic about 225 // storage. 226 return this.getRequestId(aPromiseResolver); 227 }, 228 229 getRequest(aId) { 230 if (this._requests && this._requests[aId]) { 231 return this._requests[aId]; 232 } 233 }, 234 235 getPromiseResolver(aId) { 236 // Delegates to getRequest() since the lookup table is agnostic about 237 // storage. 238 return this.getRequest(aId); 239 }, 240 241 removeRequest(aId) { 242 if (this._requests && this._requests[aId]) { 243 delete this._requests[aId]; 244 } 245 }, 246 247 removePromiseResolver(aId) { 248 // Delegates to getRequest() since the lookup table is agnostic about 249 // storage. 250 this.removeRequest(aId); 251 }, 252 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 }, 261 262 takePromiseResolver(aId) { 263 // Delegates to getRequest() since the lookup table is agnostic about 264 // storage. 265 return this.takeRequest(aId); 266 }, 267 268 _getRandomId() { 269 return Cc["@mozilla.org/uuid-generator;1"] 270 .getService(Ci.nsIUUIDGenerator) 271 .generateUUID() 272 .toString(); 273 }, 274 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 }, 285 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 }, 301 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 }, 315 316 forEachRequest(aCallback) { 317 if (!this._requests) { 318 return; 319 } 320 321 Object.keys(this._requests).forEach(aKey => { 322 if (this.getRequest(aKey) instanceof this._window.DOMRequest) { 323 aCallback(aKey); 324 } 325 }); 326 }, 327 328 forEachPromiseResolver(aCallback) { 329 if (!this._requests) { 330 return; 331 } 332 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 }, 342}; 343