Bug 879245 - Implement thenables for Promises. r=bz
authorNikhil Marathe <nsm.nikhil@gmail.com>
Thu, 23 Jan 2014 10:47:29 -0800
changeset 180956 7bcc6805f93285526c6fde1f0edaf1d9ffa5a974
parent 180955 6aefaebc145dbd5a311c88be9002a70a297de228
child 180957 9871d49f5046859882ef2aa09961f03ed65977e5
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs879245
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 879245 - Implement thenables for Promises. r=bz
dom/bindings/BindingUtils.h
dom/promise/Promise.cpp
dom/promise/Promise.h
dom/promise/tests/test_promise.html
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -1311,16 +1311,35 @@ WrapCallThisObject(JSContext* cx, JS::Ha
   // But all that won't necessarily put things in the compartment of cx.
   if (!JS_WrapObject(cx, &obj)) {
     return nullptr;
   }
 
   return obj;
 }
 
+/*
+ * This specialized function simply wraps a JS::Rooted<> since
+ * WrapNativeParent() is not applicable for JS objects.
+ */
+template<>
+inline JSObject*
+WrapCallThisObject<JS::Rooted<JSObject*>>(JSContext* cx,
+                                          JS::Handle<JSObject*> scope,
+                                          const JS::Rooted<JSObject*>& p)
+{
+  JS::Rooted<JSObject*> obj(cx, p);
+
+  if (!JS_WrapObject(cx, &obj)) {
+    return nullptr;
+  }
+
+  return obj;
+}
+
 // Helper for calling WrapNewBindingObject with smart pointers
 // (nsAutoPtr/nsRefPtr/nsCOMPtr) or references.
 template <class T, bool isSmartPtr=HasgetMember<T>::Value>
 struct WrapNewBindingObjectHelper
 {
   static inline bool Wrap(JSContext* cx, JS::Handle<JSObject*> scope,
                           const T& value, JS::MutableHandle<JS::Value> rval)
   {
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -288,46 +288,143 @@ EnterCompartment(Maybe<JSAutoCompartment
   if (aValue.isObject()) {
     JS::Rooted<JSObject*> rooted(aCx, &aValue.toObject());
     aAc.construct(aCx, rooted);
   }
 }
 
 enum {
   SLOT_PROMISE = 0,
-  SLOT_TASK
+  SLOT_DATA
 };
 
 /* static */ bool
-Promise::JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp)
+Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
 {
   JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   JS::Rooted<JS::Value> v(aCx,
                           js::GetFunctionNativeReserved(&args.callee(),
                                                         SLOT_PROMISE));
   MOZ_ASSERT(v.isObject());
 
   Promise* promise;
   if (NS_FAILED(UNWRAP_OBJECT(Promise, &v.toObject(), promise))) {
     return Throw(aCx, NS_ERROR_UNEXPECTED);
   }
 
-  v = js::GetFunctionNativeReserved(&args.callee(), SLOT_TASK);
+  v = js::GetFunctionNativeReserved(&args.callee(), SLOT_DATA);
   PromiseCallback::Task task = static_cast<PromiseCallback::Task>(v.toInt32());
 
   if (task == PromiseCallback::Resolve) {
     promise->MaybeResolveInternal(aCx, args.get(0));
   } else {
     promise->MaybeRejectInternal(aCx, args.get(0));
   }
 
   return true;
 }
 
+/*
+ * Utilities for thenable callbacks.
+ *
+ * A thenable is a { then: function(resolve, reject) { } }.
+ * `then` is called with a resolve and reject callback pair.
+ * Since only one of these should be called at most once (first call wins), the
+ * two keep a reference to each other in SLOT_DATA. When either of them is
+ * called, the references are cleared. Further calls are ignored.
+ */
+namespace {
+void
+LinkThenableCallables(JSContext* aCx, JS::Handle<JSObject*> aResolveFunc,
+                      JS::Handle<JSObject*> aRejectFunc)
+{
+  js::SetFunctionNativeReserved(aResolveFunc, SLOT_DATA,
+                                JS::ObjectValue(*aRejectFunc));
+  js::SetFunctionNativeReserved(aRejectFunc, SLOT_DATA,
+                                JS::ObjectValue(*aResolveFunc));
+}
+
+/*
+ * Returns false if callback was already called before, otherwise breaks the
+ * links and returns true.
+ */
+bool
+MarkAsCalledIfNotCalledBefore(JSContext* aCx, JS::Handle<JSObject*> aFunc)
+{
+  JS::Value otherFuncVal = js::GetFunctionNativeReserved(aFunc, SLOT_DATA);
+
+  if (!otherFuncVal.isObject()) {
+    return false;
+  }
+
+  JSObject* otherFuncObj = &otherFuncVal.toObject();
+  MOZ_ASSERT(js::GetFunctionNativeReserved(otherFuncObj, SLOT_DATA).isObject());
+
+  // Break both references.
+  js::SetFunctionNativeReserved(aFunc, SLOT_DATA, JS::UndefinedValue());
+  js::SetFunctionNativeReserved(otherFuncObj, SLOT_DATA, JS::UndefinedValue());
+
+  return true;
+}
+
+Promise*
+GetPromise(JSContext* aCx, JS::Handle<JSObject*> aFunc)
+{
+  JS::Value promiseVal = js::GetFunctionNativeReserved(aFunc, SLOT_PROMISE);
+
+  MOZ_ASSERT(promiseVal.isObject());
+
+  Promise* promise;
+  UNWRAP_OBJECT(Promise, &promiseVal.toObject(), promise);
+  return promise;
+}
+};
+
+/*
+ * Common bits of (JSCallbackThenableResolver/JSCallbackThenableRejecter).
+ * Resolves/rejects the Promise if it is ok to do so, based on whether either of
+ * the callbacks have been called before or not.
+ */
+/* static */ bool
+Promise::ThenableResolverCommon(JSContext* aCx, uint32_t aTask,
+                                unsigned aArgc, JS::Value* aVp)
+{
+  JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
+  JS::Rooted<JSObject*> thisFunc(aCx, &args.callee());
+  if (!MarkAsCalledIfNotCalledBefore(aCx, thisFunc)) {
+    // A function from this pair has been called before.
+    return true;
+  }
+
+  Promise* promise = GetPromise(aCx, thisFunc);
+  MOZ_ASSERT(promise);
+
+  if (aTask == PromiseCallback::Resolve) {
+    promise->ResolveInternal(aCx, args.get(0), SyncTask);
+  } else {
+    promise->RejectInternal(aCx, args.get(0), SyncTask);
+  }
+  return true;
+}
+
+/* static */ bool
+Promise::JSCallbackThenableResolver(JSContext* aCx,
+                                    unsigned aArgc, JS::Value* aVp)
+{
+  return ThenableResolverCommon(aCx, PromiseCallback::Resolve, aArgc, aVp);
+}
+
+/* static */ bool
+Promise::JSCallbackThenableRejecter(JSContext* aCx,
+                                    unsigned aArgc, JS::Value* aVp)
+{
+  return ThenableResolverCommon(aCx, PromiseCallback::Reject, aArgc, aVp);
+}
+
 /* static */ JSObject*
 Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
                         int32_t aTask)
 {
   JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback,
                                                  1 /* nargs */, 0 /* flags */,
                                                  aParent, nullptr);
   if (!func) {
@@ -337,17 +434,43 @@ Promise::CreateFunction(JSContext* aCx, 
   JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func));
 
   JS::Rooted<JS::Value> promiseObj(aCx);
   if (!dom::WrapNewBindingObject(aCx, obj, aPromise, &promiseObj)) {
     return nullptr;
   }
 
   js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj);
-  js::SetFunctionNativeReserved(obj, SLOT_TASK, JS::Int32Value(aTask));
+  js::SetFunctionNativeReserved(obj, SLOT_DATA, JS::Int32Value(aTask));
+
+  return obj;
+}
+
+/* static */ JSObject*
+Promise::CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask)
+{
+  JSNative whichFunc =
+    aTask == PromiseCallback::Resolve ? JSCallbackThenableResolver :
+                                        JSCallbackThenableRejecter ;
+
+  JSFunction* func = js::NewFunctionWithReserved(aCx, whichFunc,
+                                                 1 /* nargs */, 0 /* flags */,
+                                                 nullptr, nullptr);
+  if (!func) {
+    return nullptr;
+  }
+
+  JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func));
+
+  JS::Rooted<JS::Value> promiseObj(aCx);
+  if (!dom::WrapNewBindingObject(aCx, obj, aPromise, &promiseObj)) {
+    return nullptr;
+  }
+
+  js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj);
 
   return obj;
 }
 
 /* static */ already_AddRefed<Promise>
 Promise::Constructor(const GlobalObject& aGlobal,
                      PromiseInit& aInit, ErrorResult& aRv)
 {
@@ -611,32 +734,87 @@ Promise::MaybeRejectInternal(JSContext* 
   if (mResolvePending) {
     return;
   }
 
   RejectInternal(aCx, aValue, aAsynchronous);
 }
 
 void
+Promise::HandleException(JSContext* aCx)
+{
+  JS::Rooted<JS::Value> exn(aCx);
+  if (JS_GetPendingException(aCx, &exn)) {
+    JS_ClearPendingException(aCx);
+    RejectInternal(aCx, exn, SyncTask);
+  }
+}
+
+void
 Promise::ResolveInternal(JSContext* aCx,
                          JS::Handle<JS::Value> aValue,
                          PromiseTaskSync aAsynchronous)
 {
   mResolvePending = true;
 
-  // TODO: Bug 879245 - Then-able objects
   if (aValue.isObject()) {
     JS::Rooted<JSObject*> valueObj(aCx, &aValue.toObject());
-    Promise* nextPromise;
-    nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise);
+
+    // Thenables.
+    JS::Rooted<JS::Value> then(aCx);
+    if (!JS_GetProperty(aCx, valueObj, "then", &then)) {
+      HandleException(aCx);
+      return;
+    }
+
+    if (then.isObject() && JS_ObjectIsCallable(aCx, &then.toObject())) {
+      JS::Rooted<JSObject*> resolveFunc(aCx,
+        CreateThenableFunction(aCx, this, PromiseCallback::Resolve));
+
+      if (!resolveFunc) {
+        HandleException(aCx);
+        return;
+      }
+
+      JS::Rooted<JSObject*> rejectFunc(aCx,
+        CreateThenableFunction(aCx, this, PromiseCallback::Reject));
+      if (!rejectFunc) {
+        HandleException(aCx);
+        return;
+      }
+
+      LinkThenableCallables(aCx, resolveFunc, rejectFunc);
 
-    if (NS_SUCCEEDED(rv)) {
-      nsRefPtr<PromiseCallback> resolveCb = new ResolvePromiseCallback(this);
-      nsRefPtr<PromiseCallback> rejectCb = new RejectPromiseCallback(this);
-      nextPromise->AppendCallbacks(resolveCb, rejectCb);
+      JS::Rooted<JSObject*> thenObj(aCx, &then.toObject());
+      nsRefPtr<PromiseInit> thenCallback =
+        new PromiseInit(thenObj, mozilla::dom::GetIncumbentGlobal());
+
+      ErrorResult rv;
+      thenCallback->Call(valueObj, resolveFunc, rejectFunc,
+                         rv, CallbackObject::eRethrowExceptions);
+      rv.WouldReportJSException();
+
+      if (rv.IsJSException()) {
+        JS::Rooted<JS::Value> exn(aCx);
+        rv.StealJSException(aCx, &exn);
+
+        bool couldMarkAsCalled = MarkAsCalledIfNotCalledBefore(aCx, resolveFunc);
+
+        // If we could mark as called, neither of the callbacks had been called
+        // when the exception was thrown. So we can reject the Promise.
+        if (couldMarkAsCalled) {
+          Maybe<JSAutoCompartment> ac;
+          EnterCompartment(ac, aCx, exn);
+          RejectInternal(aCx, exn, Promise::SyncTask);
+        }
+        // At least one of resolveFunc or rejectFunc have been called, so ignore
+        // the exception. FIXME(nsm): This should be reported to the error
+        // console though, for debugging.
+      }
+
       return;
     }
   }
 
   // If the synchronous flag is set, process our resolve callbacks with
   // value. Otherwise, the synchronous flag is unset, queue a task to process
   // own resolve callbacks with value. Otherwise, the synchronous flag is
   // unset, queue a task to process our resolve callbacks with value.
--- a/dom/promise/Promise.h
+++ b/dom/promise/Promise.h
@@ -146,20 +146,34 @@ private:
 
   void RejectInternal(JSContext* aCx,
                       JS::Handle<JS::Value> aValue,
                       PromiseTaskSync aSync = AsyncTask);
 
   // Static methods for the PromiseInit functions.
   static bool
   JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
+
+  static bool
+  ThenableResolverCommon(JSContext* aCx, uint32_t /* PromiseCallback::Task */ aTask,
+                         unsigned aArgc, JS::Value* aVp);
+  static bool
+  JSCallbackThenableResolver(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
+  static bool
+  JSCallbackThenableRejecter(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
+
   static JSObject*
   CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
                 int32_t aTask);
 
+  static JSObject*
+  CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask);
+
+  void HandleException(JSContext* aCx);
+
   nsRefPtr<nsPIDOMWindow> mWindow;
 
   nsTArray<nsRefPtr<PromiseCallback> > mResolveCallbacks;
   nsTArray<nsRefPtr<PromiseCallback> > mRejectCallbacks;
 
   JS::Heap<JS::Value> mResult;
   PromiseState mState;
   bool mTaskPending;
--- a/dom/promise/tests/test_promise.html
+++ b/dom/promise/tests/test_promise.html
@@ -461,16 +461,114 @@ function promiseResolveNestedPromise() {
   })).then(function(what) {
     is(what, 42, "Value == 42");
     runTest();
   }, function() {
     ok(false, "This should not be called");
   });
 }
 
+function promiseSimpleThenableResolve() {
+  var thenable = { then: function(resolve) { resolve(5); } };
+  var promise = new Promise(function(resolve, reject) {
+    resolve(thenable);
+  });
+
+  promise.then(function(v) {
+    ok(v === 5, "promiseSimpleThenableResolve");
+    runTest();
+  }, function(e) {
+    ok(false, "promiseSimpleThenableResolve: Should not reject");
+  });
+}
+
+function promiseSimpleThenableReject() {
+  var thenable = { then: function(resolve, reject) { reject(5); } };
+  var promise = new Promise(function(resolve, reject) {
+    resolve(thenable);
+  });
+
+  promise.then(function() {
+    ok(false, "promiseSimpleThenableReject: Should not resolve");
+    runTest();
+  }, function(e) {
+    ok(e === 5, "promiseSimpleThenableReject");
+    runTest();
+  });
+}
+
+function promiseThenableThrowsBeforeCallback() {
+  var thenable = { then: function(resolve) {
+    throw new TypeError("Hi there");
+    resolve(5);
+  }};
+
+  var promise = Promise.resolve(thenable);
+  promise.then(function(v) {
+    ok(false, "promiseThenableThrowsBeforeCallback: Should've rejected");
+    runTest();
+  }, function(e) {
+    ok(e instanceof TypeError, "promiseThenableThrowsBeforeCallback");
+    runTest();
+  });
+}
+
+function promiseThenableThrowsAfterCallback() {
+  var thenable = { then: function(resolve) {
+    resolve(5);
+    throw new TypeError("Hi there");
+  }};
+
+  var promise = Promise.resolve(thenable);
+  promise.then(function(v) {
+    ok(v === 5, "promiseThenableThrowsAfterCallback");
+    runTest();
+  }, function(e) {
+    ok(false, "promiseThenableThrowsAfterCallback: Should've resolved");
+    runTest();
+  });
+}
+
+function promiseThenableRejectThenResolve() {
+  var thenable = { then: function(resolve, reject) {
+    reject(new TypeError("Hi there"));
+    resolve(5);
+  }};
+
+  var promise = Promise.resolve(thenable);
+  promise.then(function(v) {
+    ok(false, "promiseThenableRejectThenResolve should have rejected");
+    runTest();
+  }, function(e) {
+    ok(e instanceof TypeError, "promiseThenableRejectThenResolve");
+    runTest();
+  });
+}
+
+function promiseWithThenReplaced() {
+  // Ensure that we call the 'then' on the promise and not the internal then.
+  var promise = new Promise(function(resolve, reject) {
+    resolve(5);
+  });
+
+  // Rogue `then` always rejects.
+  promise.then = function(onFulfill, onReject) {
+    onReject(new TypeError("Foo"));
+  }
+
+  var promise2 = Promise.resolve(promise);
+  promise2.then(function(v) {
+    ok(false, "promiseWithThenReplaced: Should've rejected");
+    runTest();
+  }, function(e) {
+    ok(e instanceof TypeError, "promiseWithThenReplaced");
+    runTest();
+  });
+}
+
 var tests = [ promiseResolve, promiseReject,
               promiseException, promiseGC, promiseAsync,
               promiseDoubleThen, promiseThenException,
               promiseThenCatchThen, promiseRejectThenCatchThen,
               promiseRejectThenCatchThen2,
               promiseRejectThenCatchExceptionThen,
               promiseThenCatchOrderingResolve,
               promiseThenCatchOrderingReject,
@@ -480,16 +578,22 @@ var tests = [ promiseResolve, promiseRej
               promiseResolveNestedPromise,
               promiseResolveNoArg,
               promiseRejectNoArg,
               promiseThenNoArg,
               promiseThenUndefinedResolveFunction,
               promiseThenNullResolveFunction,
               promiseCatchNoArg,
               promiseRejectNoHandler,
+              promiseSimpleThenableResolve,
+              promiseSimpleThenableReject,
+              promiseThenableThrowsBeforeCallback,
+              promiseThenableThrowsAfterCallback,
+              promiseThenableRejectThenResolve,
+              promiseWithThenReplaced,
             ];
 
 function runTest() {
   if (!tests.length) {
     SimpleTest.finish();
     return;
   }