Bug 1126350 - Settle all toolbox destruction before final cleanup. r=pbrosset
authorJ. Ryan Stinnett <jryans@gmail.com>
Tue, 10 Feb 2015 12:37:21 -0600
changeset 242085 c70ba1f08a7fd1487e95d59f1d616f0f7dc558c8
parent 242084 58eed354d2a9258cc4c94008cef3179abb953247
child 242086 38a668c3efaa990ab0585749f21b4ccc76fe71cc
push id634
push usermozilla@noorenberghe.ca
push dateTue, 10 Feb 2015 22:34:30 +0000
reviewerspbrosset
bugs1126350
milestone38.0a1
Bug 1126350 - Settle all toolbox destruction before final cleanup. r=pbrosset
browser/devtools/framework/toolbox.js
toolkit/devtools/DevToolsUtils.js
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -42,16 +42,17 @@ loader.lazyGetter(this, "toolboxStrings"
       Services.console.logStringMessage("Error reading '" + name + "'");
       return null;
     }
   };
 });
 
 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
+loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils");
 
 // White-list buttons that can be toggled to prevent adding prefs for
 // addons that have manually inserted toolbarbuttons into DOM.
 // (By default, supported target is only local tab)
 const ToolboxButtons = [
   { id: "command-button-pick",
     isTargetSupported: target =>
       target.getTrait("highlightable")
@@ -1654,18 +1655,21 @@ Toolbox.prototype = {
       this._requisition.destroy();
     }
     this._telemetry.toolClosed("toolbox");
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
-    this._destroyer = promise.all(outstanding)
-      .then(() => this.destroyHost()).then(null, console.error).then(() => {
+    this._destroyer = DevToolsUtils.settleAll(outstanding)
+                                   .catch(console.error)
+                                   .then(() => this.destroyHost())
+                                   .catch(console.error)
+                                   .then(() => {
       // Targets need to be notified that the toolbox is being torn down.
       // This is done after other destruction tasks since it may tear down
       // fronts and the debugger transport which earlier destroy methods may
       // require to complete.
       if (!this._target) {
         return null;
       }
       let target = this._target;
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -563,8 +563,81 @@ function convertToUnicode(aString, aChar
     .createInstance(Ci.nsIScriptableUnicodeConverter);
   try {
     converter.charset = aCharset || "UTF-8";
     return converter.ConvertToUnicode(aString);
   } catch(e) {
     return aString;
   }
 }
+
+/**
+ * Returns a promise that is resolved or rejected when all promises have settled
+ * (resolved or rejected).
+ *
+ * This differs from Promise.all, which will reject immediately after the first
+ * rejection, instead of waiting for the remaining promises to settle.
+ *
+ * @param values
+ *        Iterable of promises that may be pending, resolved, or rejected. When
+ *        when all promises have settled (resolved or rejected), the returned
+ *        promise will be resolved or rejected as well.
+ *
+ * @return A new promise that is fulfilled when all values have settled
+ *         (resolved or rejected). Its resolution value will be an array of all
+ *         resolved values in the given order, or undefined if values is an
+ *         empty array. The reject reason will be forwarded from the first
+ *         promise in the list of given promises to be rejected.
+ */
+exports.settleAll = values => {
+  if (values === null || typeof(values[Symbol.iterator]) != "function") {
+    throw new Error("settleAll() expects an iterable.");
+  }
+
+  let deferred = promise.defer();
+
+  values = Array.isArray(values) ? values : [...values];
+  let countdown = values.length;
+  let resolutionValues = new Array(countdown);
+  let rejectionValue;
+  let rejectionOccurred = false;
+
+  if (!countdown) {
+    deferred.resolve(resolutionValues);
+    return deferred.promise;
+  }
+
+  function checkForCompletion() {
+    if (--countdown > 0) {
+      return;
+    }
+    if (!rejectionOccurred) {
+      deferred.resolve(resolutionValues);
+    } else {
+      deferred.reject(rejectionValue);
+    }
+  }
+
+  for (let i = 0; i < values.length; i++) {
+    let index = i;
+    let value = values[i];
+    let resolver = result => {
+      resolutionValues[index] = result;
+      checkForCompletion();
+    };
+    let rejecter = error => {
+      if (!rejectionOccurred) {
+        rejectionValue = error;
+        rejectionOccurred = true;
+      }
+      checkForCompletion();
+    };
+
+    if (value && typeof(value.then) == "function") {
+      value.then(resolver, rejecter);
+    } else {
+      // Given value is not a promise, forward it as a resolution value.
+      resolver(value);
+    }
+  }
+
+  return deferred.promise;
+};