Bug 1322274: Use this._async() wrapper in PeerConnection.js for cleaner code draft
authorJan-Ivar Bruaroey <jib@mozilla.com>
Sun, 27 Nov 2016 10:34:46 -0500
changeset 448584 0e1f9d3bb1ba0edf2bf8d60896a5319e3373d64a
parent 448583 cbbf37d7163549728bb23fcfda02facbd3471a4c
child 448585 c78a67ae74b17eee4ef0038c033d0ab376122add
push id38366
push userjbruaroey@mozilla.com
push dateSun, 11 Dec 2016 01:26:04 +0000
bugs1322274
milestone53.0a1
Bug 1322274: Use this._async() wrapper in PeerConnection.js for cleaner code MozReview-Commit-ID: C5wwHiitrEz
dom/media/PeerConnection.js
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -524,51 +524,66 @@ RTCPeerConnection.prototype = {
       }
       return await func();
     })();
     // don't propagate errors in the operations chain (this is a fork of p).
     this._operationsChain = p.catch(() => {});
     return await p;
   },
 
-  // This wrapper helps implement legacy callbacks in a manner that produces
+  // These wrappers help implement legacy callbacks in a manner that produces
   // correct line-numbers in errors, provided that methods validate their inputs
   // before putting themselves on the pc's operations chain.
   //
   // It also serves as guard against settling promises past close().
 
-  _legacyCatchAndCloseGuard: function(onSuccess, onError, func) {
-    let operation = () => {
-      this._checkClosed();
-      return func();
-    };
+  _async: function(func) {
+    return this._win.Promise.resolve(this._closeWrapper(func));
+  },
+
+  _legacy: function(...args) {
+    return this._win.Promise.resolve(this._legacyCloseWrapper(...args));
+  },
+
+  _auto: function(onSucc, onErr, func) {
+    return (typeof onSucc == "function") ? this._legacy(onSucc, onErr, func)
+                                         : this._async(func);
+  },
 
-    if (!onSuccess) {
-      return this._win.Promise.resolve(operation())
-        .then(v => (this._closed ? new Promise(() => {}) : v),
-              e => (this._closed ? new Promise(() => {}) : Promise.reject(e)));
-    }
+  _closeWrapper: async function(func) {
+    let closed = this._closed;
     try {
-      return this._win.Promise.resolve(operation())
-        .then(this._wrapLegacyCallback(onSuccess),
-              this._wrapLegacyCallback(onError));
+      let result = await func();
+      if (!closed && this._closed) {
+        await new Promise(() => {});
+      }
+      return result;
     } catch (e) {
-      this._wrapLegacyCallback(onError)(e);
-      return this._win.Promise.resolve(); // avoid webidl TypeError
+      if (!closed && this._closed) {
+        await new Promise(() => {});
+      }
+      throw e;
     }
   },
 
-  _wrapLegacyCallback: function(func) {
-    return result => {
+  _legacyCloseWrapper: async function(onSucc, onErr, func) {
+
+    let wrapCallback = cb => result => {
       try {
-        func && func(result);
+        cb && cb(result);
       } catch (e) {
         this.logErrorAndCallOnError(e);
       }
     };
+
+    try {
+      wrapCallback(onSucc)(await func());
+    } catch (e) {
+      wrapCallback(onErr)(e);
+    }
   },
 
   /**
    * An RTCConfiguration may look like this:
    *
    * { "iceServers": [ { urls: "stun:stun.example.org", },
    *                   { url: "stun:stun.example.org", }, // deprecated version
    *                   { urls: ["turn:turn1.x.org", "turn:turn2.x.org"],
@@ -720,86 +735,86 @@ RTCPeerConnection.prototype = {
                             get:function()  { return this.getEH(name); },
                             set:function(h) {
                               this.logWarning(name + " is deprecated! " + msg);
                               return this.setEH(name, h);
                             }
                           });
   },
 
-  createOffer: function(optionsOrOnSuccess, onError, options) {
+  createOffer: function(optionsOrOnSucc, onErr, options) {
     // This entry-point handles both new and legacy call sig. Decipher which one
-    let onSuccess;
-    if (typeof optionsOrOnSuccess == "function") {
-      onSuccess = optionsOrOnSuccess;
-    } else {
-      options = optionsOrOnSuccess;
+    if (typeof optionsOrOnSucc == "function") {
+      return this._legacy(optionsOrOnSucc, onErr, () => this._createOffer(options));
     }
-    return this._legacyCatchAndCloseGuard(onSuccess, onError, async () => {
-      let origin = Cu.getWebIDLCallerPrincipal().origin;
-      return await this._chain(async () => {
-        let haveAssertion;
-        if (this._localIdp.enabled) {
-          haveAssertion = this._getIdentityAssertion(origin);
-        }
-        await this._getPermission();
-        await this._certificateReady;
-        let sdp = await new Promise((resolve, reject) => {
-          this._onCreateOfferSuccess = resolve;
-          this._onCreateOfferFailure = reject;
-          this._impl.createOffer(options);
-        });
-        if (haveAssertion) {
-          await haveAssertion;
-          sdp = this._localIdp.addIdentityAttribute(sdp);
-        }
-        return Cu.cloneInto({ type: "offer", sdp }, this._win);
+    return this._async(() => this._createOffer(optionsOrOnSucc));
+  },
+
+  _createOffer: async function(options) {
+    this._checkClosed();
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+    return await this._chain(async () => {
+      let haveAssertion;
+      if (this._localIdp.enabled) {
+        haveAssertion = this._getIdentityAssertion(origin);
+      }
+      await this._getPermission();
+      await this._certificateReady;
+      let sdp = await new Promise((resolve, reject) => {
+        this._onCreateOfferSuccess = resolve;
+        this._onCreateOfferFailure = reject;
+        this._impl.createOffer(options);
       });
+      if (haveAssertion) {
+        await haveAssertion;
+        sdp = this._localIdp.addIdentityAttribute(sdp);
+      }
+      return Cu.cloneInto({ type: "offer", sdp }, this._win);
     });
   },
 
-  createAnswer: function(optionsOrOnSuccess, onError) {
+  createAnswer: function(optionsOrOnSucc, onErr) {
     // This entry-point handles both new and legacy call sig. Decipher which one
-    let onSuccess, options;
-    if (typeof optionsOrOnSuccess == "function") {
-      onSuccess = optionsOrOnSuccess;
-    } else {
-      options = optionsOrOnSuccess;
+    if (typeof optionsOrOnSucc == "function") {
+      return this._legacy(optionsOrOnSucc, onErr, () => this._createAnswer({}));
     }
-    return this._legacyCatchAndCloseGuard(onSuccess, onError, async () => {
-      let origin = Cu.getWebIDLCallerPrincipal().origin;
-      return await this._chain(async () => {
-        // We give up line-numbers in errors by doing this here, but do all
-        // state-checks inside the chain, to support the legacy feature that
-        // callers don't have to wait for setRemoteDescription to finish.
-        if (!this.remoteDescription) {
-          throw new this._win.DOMException("setRemoteDescription not called",
-                                           "InvalidStateError");
-        }
-        if (this.remoteDescription.type != "offer") {
-          throw new this._win.DOMException("No outstanding offer",
-                                           "InvalidStateError");
-        }
-        let haveAssertion;
-        if (this._localIdp.enabled) {
-          haveAssertion = this._getIdentityAssertion(origin);
-        }
-        await this._getPermission();
-        await this._certificateReady;
-        let sdp = await new Promise((resolve, reject) => {
-          this._onCreateAnswerSuccess = resolve;
-          this._onCreateAnswerFailure = reject;
-          this._impl.createAnswer();
-        });
-        if (haveAssertion) {
-          await haveAssertion;
-          sdp = this._localIdp.addIdentityAttribute(sdp);
-        }
-        return Cu.cloneInto({ type: "answer", sdp }, this._win);
+    return this._async(() => this._createAnswer(optionsOrOnSucc));
+  },
+
+  _createAnswer: async function(options) {
+    this._checkClosed();
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+    return await this._chain(async () => {
+      // We give up line-numbers in errors by doing this here, but do all
+      // state-checks inside the chain, to support the legacy feature that
+      // callers don't have to wait for setRemoteDescription to finish.
+      if (!this.remoteDescription) {
+        throw new this._win.DOMException("setRemoteDescription not called",
+                                         "InvalidStateError");
+      }
+      if (this.remoteDescription.type != "offer") {
+        throw new this._win.DOMException("No outstanding offer",
+                                         "InvalidStateError");
+      }
+      let haveAssertion;
+      if (this._localIdp.enabled) {
+        haveAssertion = this._getIdentityAssertion(origin);
+      }
+      await this._getPermission();
+      await this._certificateReady;
+      let sdp = await new Promise((resolve, reject) => {
+        this._onCreateAnswerSuccess = resolve;
+        this._onCreateAnswerFailure = reject;
+        this._impl.createAnswer();
       });
+      if (haveAssertion) {
+        await haveAssertion;
+        sdp = this._localIdp.addIdentityAttribute(sdp);
+      }
+      return Cu.cloneInto({ type: "answer", sdp }, this._win);
     });
   },
 
   _getPermission: async function() {
     if (!this._havePermission) {
       let privileged = this._isChrome || AppConstants.MOZ_B2G ||
           Services.prefs.getBoolPref("media.navigator.permission.disabled");
 
@@ -824,44 +839,48 @@ RTCPeerConnection.prototype = {
   _actions: {
     offer: Ci.IPeerConnection.kActionOffer,
     answer: Ci.IPeerConnection.kActionAnswer,
     pranswer: Ci.IPeerConnection.kActionPRAnswer,
     rollback: Ci.IPeerConnection.kActionRollback,
     answer: Ci.IPeerConnection.kActionAnswer,
   },
 
-  setLocalDescription: function({ type, sdp }, onSuccess, onError) {
-    return this._legacyCatchAndCloseGuard(onSuccess, onError, async () => {
-      this._localType = type;
+  setLocalDescription: function(desc, onSucc, onErr) {
+    return this._auto(onSucc, onErr, () => this._setLocalDescription(desc));
+  },
+
+  _setLocalDescription: async function({ type, sdp }) {
+    this._checkClosed();
+
+    this._localType = type;
 
-      let action = this._actions[type];
-      if (action === undefined) {
-        throw new this._win.DOMException(
-            "Invalid type " + type + " provided to setLocalDescription",
-            "InvalidParameterError");
-      }
-      if (action == Ci.IPeerConnection.kActionPRAnswer) {
-        throw new this._win.DOMException("pranswer not yet implemented",
-                                         "NotSupportedError");
-      }
+    let action = this._actions[type];
+    if (action === undefined) {
+      throw new this._win.DOMException(
+          "Invalid type " + type + " provided to setLocalDescription",
+          "InvalidParameterError");
+    }
+    if (action == Ci.IPeerConnection.kActionPRAnswer) {
+      throw new this._win.DOMException("pranswer not yet implemented",
+                                       "NotSupportedError");
+    }
 
-      if (!sdp && action != Ci.IPeerConnection.kActionRollback) {
-        throw new this._win.DOMException(
-            "Empty or null SDP provided to setLocalDescription",
-            "InvalidParameterError");
-      }
+    if (!sdp && action != Ci.IPeerConnection.kActionRollback) {
+      throw new this._win.DOMException(
+          "Empty or null SDP provided to setLocalDescription",
+          "InvalidParameterError");
+    }
 
-      return await this._chain(async () => {
-        await this._getPermission();
-        await new Promise((resolve, reject) => {
-          this._onSetLocalDescriptionSuccess = resolve;
-          this._onSetLocalDescriptionFailure = reject;
-          this._impl.setLocalDescription(action, sdp);
-        });
+    return await this._chain(async () => {
+      await this._getPermission();
+      await new Promise((resolve, reject) => {
+        this._onSetLocalDescriptionSuccess = resolve;
+        this._onSetLocalDescriptionFailure = reject;
+        this._impl.setLocalDescription(action, sdp);
       });
     });
   },
 
   _validateIdentity: async function(sdp, origin) {
     let expectedIdentity;
 
     // Only run a single identity verification at a time.  We have to do this to
@@ -901,57 +920,60 @@ RTCPeerConnection.prototype = {
     this._lastIdentityValidation = p.catch(() => {});
 
     // Only wait for IdP validation if we need identity matching
     if (expectedIdentity) {
       await p;
     }
   },
 
-  setRemoteDescription: function({ type, sdp }, onSuccess, onError) {
-    return this._legacyCatchAndCloseGuard(onSuccess, onError, async () => {
-      this._remoteType = type;
+  setRemoteDescription: function(desc, onSucc, onErr) {
+    return this._auto(onSucc, onErr, () => this._setRemoteDescription(desc));
+  },
+
+  _setRemoteDescription: async function({ type, sdp }) {
+    this._checkClosed();
+    this._remoteType = type;
 
-      let action = this._actions[type];
-      if (action === undefined) {
-        throw new this._win.DOMException(
-            "Invalid type " + type + " provided to setRemoteDescription",
-            "InvalidParameterError");
-      }
-      if (action == Ci.IPeerConnection.kActionPRAnswer) {
-        throw new this._win.DOMException("pranswer not yet implemented",
-                                         "NotSupportedError");
-      }
-
-      if (!sdp && action != Ci.IPeerConnection.kActionRollback) {
-        throw new this._win.DOMException(
-            "Empty or null SDP provided to setRemoteDescription",
-            "InvalidParameterError");
-      }
+    let action = this._actions[type];
+    if (action === undefined) {
+      throw new this._win.DOMException(
+          "Invalid type " + type + " provided to setRemoteDescription",
+          "InvalidParameterError");
+    }
+    if (action == Ci.IPeerConnection.kActionPRAnswer) {
+      throw new this._win.DOMException("pranswer not yet implemented",
+                                       "NotSupportedError");
+    }
 
-      // Get caller's origin before hitting the promise chain
-      let origin = Cu.getWebIDLCallerPrincipal().origin;
+    if (!sdp && type != "rollback") {
+      throw new this._win.DOMException(
+          "Empty or null SDP provided to setRemoteDescription",
+          "InvalidParameterError");
+    }
+
+    // Get caller's origin before hitting the promise chain
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
 
-      return await this._chain(async () => {
-        let haveSetRemote = (async () => {
-          await this._getPermission();
-          await new Promise((resolve, reject) => {
-            this._onSetRemoteDescriptionSuccess = resolve;
-            this._onSetRemoteDescriptionFailure = reject;
-            this._impl.setRemoteDescription(action, sdp);
-          });
-          this._updateCanTrickle();
-        })();
+    return await this._chain(async () => {
+      let haveSetRemote = (async () => {
+        await this._getPermission();
+        await new Promise((resolve, reject) => {
+          this._onSetRemoteDescriptionSuccess = resolve;
+          this._onSetRemoteDescriptionFailure = reject;
+          this._impl.setRemoteDescription(action, sdp);
+        });
+        this._updateCanTrickle();
+      })();
 
-        if (action != Ci.IPeerConnection.kActionRollback) {
-          // Do setRemoteDescription and identity validation in parallel
-          await this._validateIdentity(sdp, origin);
-        }
-        await haveSetRemote;
-      });
+      if (action != Ci.IPeerConnection.kActionRollback) {
+        // Do setRemoteDescription and identity validation in parallel
+        await this._validateIdentity(sdp, origin);
+      }
+      await haveSetRemote;
     });
   },
 
   setIdentityProvider: function(provider, protocol, username) {
     this._checkClosed();
     this._localIdp.setIdentityProvider(provider, protocol, username);
   },
 
@@ -996,31 +1018,32 @@ RTCPeerConnection.prototype = {
 
     let sections = desc.sdp.split(/(?:\r\n?|\n)m=/);
     let topSection = sections.shift();
     this._canTrickle =
       containsTrickle(topSection) || sections.every(containsTrickle);
   },
 
   // TODO: Implement processing for end-of-candidates (bug 1318167)
-  addIceCandidate: function(candidate, onSuccess, onError) {
-    let add = async ({ candidate, sdpMid, sdpMLineIndex }) => {
-      if (sdpMid === null && sdpMLineIndex === null) {
-        throw new this._win.DOMException(
-            "Invalid candidate (both sdpMid and sdpMLineIndex are null).",
-            "TypeError");
-      }
-      return await this._chain(() => new Promise((resolve, reject) => {
-        this._onAddIceCandidateSuccess = resolve;
-        this._onAddIceCandidateError = reject;
-        this._impl.addIceCandidate(candidate, sdpMid || "", sdpMLineIndex);
-      }));
-    };
-    return this._legacyCatchAndCloseGuard(onSuccess, onError,
-        () => candidate ? add(candidate) : Promise.resolve());
+  addIceCandidate: function(cand, onSucc, onErr) {
+    return this._auto(onSucc, onErr, () => cand && this._addIceCandidate(cand));
+  },
+
+  _addIceCandidate: async function({ candidate, sdpMid, sdpMLineIndex }) {
+    this._checkClosed();
+    if (sdpMid === null && sdpMLineIndex === null) {
+      throw new this._win.DOMException(
+          "Invalid candidate (both sdpMid and sdpMLineIndex are null).",
+          "TypeError");
+    }
+    return await this._chain(() => new Promise((resolve, reject) => {
+      this._onAddIceCandidateSuccess = resolve;
+      this._onAddIceCandidateError = reject;
+      this._impl.addIceCandidate(candidate, sdpMid || "", sdpMLineIndex);
+    }));
   },
 
   addStream: function(stream) {
     stream.getTracks().forEach(track => this.addTrack(track, stream));
   },
 
   addTrack: function(track, stream) {
     if (stream.currentTime === undefined) {
@@ -1053,24 +1076,25 @@ RTCPeerConnection.prototype = {
   _insertDTMF: function(sender, tones, duration, interToneGap) {
     return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
   },
 
   _getDTMFToneBuffer: function(sender) {
     return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
   },
 
-  _replaceTrack: function(sender, withTrack) {
-    return new this._win.Promise((resolve, reject) => {
+  _replaceTrack: async function(sender, withTrack) {
+    this._checkClosed();
+    return await this._chain(() => new Promise((resolve, reject) => {
       this._onReplaceTrackSender = sender;
       this._onReplaceTrackWithTrack = withTrack;
       this._onReplaceTrackSuccess = resolve;
       this._onReplaceTrackFailure = reject;
       this._impl.replaceTrack(sender.track, withTrack);
-    });
+    }));
   },
 
   _setParameters: function({ track }, parameters) {
     if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
       return;
     }
     // validate parameters input
     var encodings = parameters.encodings || [];
@@ -1183,24 +1207,27 @@ RTCPeerConnection.prototype = {
   },
 
   changeIceConnectionState: function(state) {
     this._iceConnectionState = state;
     _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange");
     this.dispatchEvent(new this._win.Event("iceconnectionstatechange"));
   },
 
-  getStats: function(selector, onSuccess, onError) {
-    return this._legacyCatchAndCloseGuard(onSuccess, onError, () => {
-      return this._chain(() => new Promise((resolve, reject) => {
-        this._onGetStatsSuccess = resolve;
-        this._onGetStatsFailure = reject;
-        this._impl.getStats(selector);
-      }));
-    });
+  getStats: function(selector, onSucc, onErr) {
+    return this._auto(onSucc, onErr, () => this._getStats(selector));
+  },
+
+  _getStats: async function(selector) {
+    // getStats is allowed even in closed state.
+    return await this._chain(() => new Promise((resolve, reject) => {
+      this._onGetStatsSuccess = resolve;
+      this._onGetStatsFailure = reject;
+      this._impl.getStats(selector);
+    }));
   },
 
   createDataChannel: function(label, {
                                 maxRetransmits, ordered, negotiated,
                                 id = 0xFFFF,
                                 maxRetransmitTime,
                                 maxPacketLifeTime = maxRetransmitTime,
                                 protocol,
@@ -1600,18 +1627,17 @@ function RTCRtpSender(pc, track, stream)
 }
 RTCRtpSender.prototype = {
   classDescription: "RTCRtpSender",
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 
   replaceTrack: function(withTrack) {
-    this._pc._checkClosed();
-    return this._pc._chain(() => this._pc._replaceTrack(this, withTrack));
+    return this._pc._async(() => this._pc._replaceTrack(this, withTrack));
   },
 
   setParameters: function(parameters) {
     return this._pc._win.Promise.resolve()
       .then(() => this._pc._setParameters(this, parameters));
   },
 
   getParameters: function() {