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