Bug 1491403 - Part 2: Expose SpiderMonkey APIs for indicating whether a promise must propagate user input event handling state r=arai
authorEhsan Akhgari <ehsan@mozilla.com>
Tue, 09 Oct 2018 21:42:20 +0000
changeset 496071 b62dd5af680e03d48caaa31f6fa337b12dc0eeb7
parent 496070 2ba3a8f9424d711a83765981eb8dedb50944e507
child 496072 18d88d24495acae45a381f8bf95c3ab86fe800ec
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1491403
milestone64.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 1491403 - Part 2: Expose SpiderMonkey APIs for indicating whether a promise must propagate user input event handling state r=arai Depends on D7003 Differential Revision: https://phabricator.services.mozilla.com/D7004
js/src/builtin/Promise.cpp
js/src/builtin/Promise.h
js/src/jsapi.cpp
js/src/jsapi.h
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -659,16 +659,23 @@ const Class PromiseReactionRecord::class
 
 static void
 AddPromiseFlags(PromiseObject& promise, int32_t flag)
 {
     int32_t flags = promise.flags();
     promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags | flag));
 }
 
+static void
+RemovePromiseFlags(PromiseObject& promise, int32_t flag)
+{
+    int32_t flags = promise.flags();
+    promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags & ~flag));
+}
+
 static bool
 PromiseHasAnyFlag(PromiseObject& promise, int32_t flag)
 {
     return promise.flags() & flag;
 }
 
 static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp);
 static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp);
@@ -3287,16 +3294,31 @@ PromiseThenNewPromiseCapability(JSContex
 
         if (createDependent == CreateDependentPromise::Always ||
             !IsNativeFunction(C, PromiseConstructor))
         {
             // Step 4.
             if (!NewPromiseCapability(cx, C, resultCapability, true)) {
                 return false;
             }
+
+            RootedObject unwrappedPromise(cx, promiseObj);
+            if (IsWrapper(promiseObj)) {
+              unwrappedPromise = UncheckedUnwrap(promiseObj);
+            }
+            RootedObject unwrappedNewPromise(cx, resultCapability.promise());
+            if (IsWrapper(resultCapability.promise())) {
+              unwrappedNewPromise = UncheckedUnwrap(resultCapability.promise());
+            }
+            if (unwrappedPromise->is<PromiseObject>() &&
+                unwrappedNewPromise->is<PromiseObject>())
+            {
+              unwrappedNewPromise->as<PromiseObject>()
+                .copyUserInteractionFlagsFrom(*unwrappedPromise.as<PromiseObject>());
+            }
         }
     }
 
     return true;
 }
 
 // ES2016, 25.4.5.3., steps 3-5.
 MOZ_MUST_USE bool
@@ -3365,16 +3387,17 @@ OriginalPromiseThenBuiltin(JSContext* cx
     // Steps 3-4.
     Rooted<PromiseCapability> resultCapability(cx);
     if (rvalUsed) {
         PromiseObject* resultPromise = CreatePromiseObjectWithoutResolutionFunctions(cx);
         if (!resultPromise) {
             return false;
         }
 
+        resultPromise->copyUserInteractionFlagsFrom(promiseVal.toObject().as<PromiseObject>());
         resultCapability.promise().set(resultPromise);
     }
 
     // Step 5.
     if (!PerformPromiseThen(cx, promise, onFulfilled, onRejected, resultCapability)) {
         return false;
     }
 
@@ -4432,16 +4455,45 @@ PromiseObject::onSettled(JSContext* cx, 
 
     if (promise->state() == JS::PromiseState::Rejected && promise->isUnhandled()) {
         cx->runtime()->addUnhandledRejectedPromise(cx, promise);
     }
 
     Debugger::onPromiseSettled(cx, promise);
 }
 
+void
+PromiseObject::setRequiresUserInteractionHandling(bool state)
+{
+    if (state) {
+        AddPromiseFlags(*this, PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING);
+    } else {
+        RemovePromiseFlags(*this, PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING);
+    }
+}
+
+void
+PromiseObject::setHadUserInteractionUponCreation(bool state)
+{
+    MOZ_ASSERT(this->state() == JS::PromiseState::Pending);
+    if (state) {
+        AddPromiseFlags(*this, PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION);
+    } else {
+        RemovePromiseFlags(*this, PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION);
+    }
+}
+
+void
+PromiseObject::copyUserInteractionFlagsFrom(PromiseObject& rhs)
+{
+    MOZ_ASSERT(state() == JS::PromiseState::Pending);
+    setRequiresUserInteractionHandling(rhs.requiresUserInteractionHandling());
+    setHadUserInteractionUponCreation(rhs.hadUserInteractionUponCreation());
+}
+
 JSFunction*
 js::PromiseLookup::getPromiseConstructor(JSContext* cx)
 {
     const Value& val = cx->global()->getConstructor(JSProto_Promise);
     return val.isObject() ? &val.toObject().as<JSFunction>() : nullptr;
 }
 
 NativeObject*
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -59,16 +59,34 @@ enum PromiseSlots {
 
 // This promise uses the default resolving functions.
 // The PromiseSlot_RejectFunction slot is not used.
 #define PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS 0x08
 
 // This promise is the return value of an async function invocation.
 #define PROMISE_FLAG_ASYNC    0x10
 
+// This promise knows how to propagate information required to keep track of
+// whether an activation behavior was in progress when the original promise in
+// the promise chain was created.  This is a concept defined in the HTML spec:
+// https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-user-activation
+// It is used by the embedder in order to request SpiderMonkey to keep track of
+// this information in a Promise, and also to propagate it to newly created
+// promises while processing Promise#then.
+#define PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING 0x20
+
+// This flag indicates whether an activation behavior was in progress when the
+// original promise in the promise chain was created.  Activation behavior is a
+// concept defined by the HTML spec:
+// https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-user-activation
+// This flag is only effective when the
+// PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING is set.  Also, it is only
+// possible to set this flag on pending promises.
+#define PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION 0x40
+
 class AutoSetNewObjectMetadata;
 
 class PromiseObject : public NativeObject
 {
   public:
     static const unsigned RESERVED_SLOTS = PromiseSlots;
     static const Class class_;
     static const Class protoClass_;
@@ -131,16 +149,30 @@ class PromiseObject : public NativeObjec
 
     // Return the process-unique ID of this promise. Only used by the debugger.
     uint64_t getID();
 
     bool isUnhandled() {
         MOZ_ASSERT(state() == JS::PromiseState::Rejected);
         return !(flags() & PROMISE_FLAG_HANDLED);
     }
+
+    bool requiresUserInteractionHandling() {
+        return (flags() & PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING);
+    }
+
+    void setRequiresUserInteractionHandling(bool state);
+
+    bool hadUserInteractionUponCreation() {
+        return (flags() & PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION);
+    }
+
+    void setHadUserInteractionUponCreation(bool state);
+
+    void copyUserInteractionFlagsFrom(PromiseObject& rhs);
 };
 
 /**
  * Unforgeable version of the JS builtin Promise.all.
  *
  * Takes an AutoObjectVector of Promise objects and returns a promise that's
  * resolved with an array of resolution values when all those promises have
  * been resolved, or rejected with the rejection value of the first rejected
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4547,16 +4547,67 @@ JS::AddPromiseReactions(JSContext* cx, J
 {
     RootedObject resultPromise(cx);
     bool result = CallOriginalPromiseThenImpl(cx, promiseObj, onResolvedObj, onRejectedObj,
                                               &resultPromise, CreateDependentPromise::Never);
     MOZ_ASSERT(!resultPromise);
     return result;
 }
 
+JS_PUBLIC_API(JS::PromiseUserInputEventHandlingState)
+JS::GetPromiseUserInputEventHandlingState(JS::HandleObject promiseObj_)
+{
+    JSObject* promiseObj = CheckedUnwrap(promiseObj_);
+    if (!promiseObj || !promiseObj->is<PromiseObject>()) {
+        return JS::PromiseUserInputEventHandlingState::DontCare;
+    }
+
+    auto& promise = promiseObj->as<PromiseObject>();
+    if (!promise.requiresUserInteractionHandling()) {
+      return JS::PromiseUserInputEventHandlingState::DontCare;
+    }
+    if (promise.hadUserInteractionUponCreation()) {
+      return JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation;
+    }
+    return JS::PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation;
+}
+
+JS_PUBLIC_API(bool)
+JS::SetPromiseUserInputEventHandlingState(JS::HandleObject promiseObj_,
+                                          JS::PromiseUserInputEventHandlingState state)
+{
+    JSObject* promiseObj = CheckedUnwrap(promiseObj_);
+    if (!promiseObj || !promiseObj->is<PromiseObject>()) {
+        return false;
+    }
+
+    auto& promise = promiseObj->as<PromiseObject>();
+    if (promise.state() != JS::PromiseState::Pending) {
+      return false;
+    }
+
+    switch (state) {
+      case JS::PromiseUserInputEventHandlingState::DontCare:
+        promise.setRequiresUserInteractionHandling(false);
+        break;
+      case JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation:
+        promise.setRequiresUserInteractionHandling(true);
+        promise.setHadUserInteractionUponCreation(true);
+        break;
+      case JS::PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation:
+        promise.setRequiresUserInteractionHandling(true);
+        promise.setHadUserInteractionUponCreation(false);
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Invalid PromiseUserInputEventHandlingState enum value");
+        return false;
+    }
+    return true;
+}
+
 /**
  * Unforgeable version of Promise.all for internal use.
  *
  * Takes a dense array of Promise objects and returns a promise that's
  * resolved with an array of resolution values when all those promises ahve
  * been resolved, or rejected with the rejection value of the first rejected
  * promise.
  *
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -3378,16 +3378,61 @@ CallOriginalPromiseThen(JSContext* cx, J
  * Asserts if the passed-in `promise` object isn't an unwrapped instance of
  * `Promise` or a subclass or `onResolve` and `onReject` aren't both callable
  * objects.
  */
 extern JS_PUBLIC_API(bool)
 AddPromiseReactions(JSContext* cx, JS::HandleObject promise,
                     JS::HandleObject onResolve, JS::HandleObject onReject);
 
+// This enum specifies whether a promise is expected to keep track of information
+// that is useful for embedders to implement user activation behavior handling as
+// specified in the HTML spec:
+// https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-user-activation
+// By default, promises created by SpiderMonkey do not make any attempt to keep
+// track of information about whether an activation behavior was being processed
+// when the original promise in a promise chain was created.  If the embedder sets
+// either of the HadUserInteractionAtCreation or DidntHaveUserInteractionAtCreation
+// flags on a promise after creating it, SpiderMonkey will propagate that flag to
+// newly created promises when processing Promise#then and will make it possible
+// to query this flag off of a promise further down the chain later using the
+// GetPromiseUserInputEventHandlingState() API.
+enum class PromiseUserInputEventHandlingState {
+  // Don't keep track of this state (default for all promises)
+  DontCare,
+  // Keep track of this state, the original promise in the chain was created
+  // while an activation behavior was being processed.
+  HadUserInteractionAtCreation,
+  // Keep track of this state, the original promise in the chain was created
+  // while an activation behavior was not being processed.
+  DidntHaveUserInteractionAtCreation
+};
+
+/**
+ * Returns the given Promise's activation behavior state flag per above as a
+ * JS::PromiseUserInputEventHandlingState value.  All promises are created with
+ * the DontCare state by default.
+ *
+ * Returns JS::PromiseUserInputEventHandlingState::DontCare if the given object
+ * is a wrapper that can't safely be unwrapped.
+ */
+extern JS_PUBLIC_API(PromiseUserInputEventHandlingState)
+GetPromiseUserInputEventHandlingState(JS::HandleObject promise);
+
+/**
+ * Sets the given Promise's activation behavior state flag per above as a
+ * JS::PromiseUserInputEventHandlingState value.
+ *
+ * Returns false if the given object is a wrapper that can't safely be unwrapped,
+ * or if the promise isn't pending.
+ */
+extern JS_PUBLIC_API(bool)
+SetPromiseUserInputEventHandlingState(JS::HandleObject promise,
+                                      JS::PromiseUserInputEventHandlingState state);
+
 /**
  * Unforgeable version of the JS builtin Promise.all.
  *
  * Takes an AutoObjectVector of Promise objects and returns a promise that's
  * resolved with an array of resolution values when all those promises have
  * been resolved, or rejected with the rejection value of the first rejected
  * promise.
  *