diff --git a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp index 235028a2b355a4..730da01a697a21 100644 --- a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp +++ b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp @@ -13,8 +13,8 @@ #include #include #include +#include #include -#include #include #include @@ -51,6 +51,12 @@ class JSCRuntime : public jsi::Runtime { const std::shared_ptr& buffer, const std::string& sourceURL) override; + // If we use this interface to implement microtasks in the host we need to + // polyfill `Promise` to use these methods, because JSC doesn't currently + // support providing a custom queue for its built-in implementation. + // Not doing this would result in a non-compliant behavior, as microtasks + // wouldn't execute in the order in which they were queued. + void queueMicrotask(const jsi::Function& callback) override; bool drainMicrotasks(int maxMicrotasksHint = -1) override; jsi::Object global() override; @@ -265,6 +271,7 @@ class JSCRuntime : public jsi::Runtime { std::atomic ctxInvalid_; std::string desc_; JSValueRef nativeStateSymbol_ = nullptr; + std::deque microtaskQueue_; #ifndef NDEBUG mutable std::atomic objectCounter_; mutable std::atomic symbolCounter_; @@ -380,6 +387,10 @@ JSCRuntime::JSCRuntime(JSGlobalContextRef ctx) } JSCRuntime::~JSCRuntime() { + // We need to clear the microtask queue to remove all references to the + // callbacks, so objectCounter_ would be 0 below. + microtaskQueue_.clear(); + // On shutting down and cleaning up: when JSC is actually torn down, // it calls JSC::Heap::lastChanceToFinalize internally which // finalizes anything left over. But at this point, @@ -436,7 +447,24 @@ jsi::Value JSCRuntime::evaluateJavaScript( return createValue(res); } -bool JSCRuntime::drainMicrotasks(int maxMicrotasksHint) { +void JSCRuntime::queueMicrotask(const jsi::Function& callback) { + microtaskQueue_.emplace_back( + jsi::Value(*this, callback).asObject(*this).asFunction(*this)); +} + +bool JSCRuntime::drainMicrotasks(int /*maxMicrotasksHint*/) { + // Note that new jobs can be enqueued during the draining. + while (!microtaskQueue_.empty()) { + jsi::Function callback = std::move(microtaskQueue_.front()); + + // We need to pop before calling the callback because that might throw. + // When that happens, the host will call `drainMicrotasks` again to execute + // the remaining microtasks, and this one shouldn't run again. + microtaskQueue_.pop_front(); + + callback.call(*this); + } + return true; } diff --git a/packages/react-native/ReactCommon/jsi/jsi/decorator.h b/packages/react-native/ReactCommon/jsi/jsi/decorator.h index 7bddd1fad80a52..6f4351a420da69 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/decorator.h +++ b/packages/react-native/ReactCommon/jsi/jsi/decorator.h @@ -126,6 +126,9 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { const std::shared_ptr& js) override { return plain().evaluatePreparedJavaScript(js); } + void queueMicrotask(const jsi::Function& callback) override { + return plain().queueMicrotask(callback); + } bool drainMicrotasks(int maxMicrotasksHint) override { return plain().drainMicrotasks(maxMicrotasksHint); } @@ -544,6 +547,10 @@ class WithRuntimeDecorator : public RuntimeDecorator { Around around{with_}; return RD::evaluatePreparedJavaScript(js); } + void queueMicrotask(const Function& callback) override { + Around around{with_}; + RD::queueMicrotask(callback); + } bool drainMicrotasks(int maxMicrotasksHint) override { Around around{with_}; return RD::drainMicrotasks(maxMicrotasksHint); diff --git a/packages/react-native/ReactCommon/jsi/jsi/jsi.h b/packages/react-native/ReactCommon/jsi/jsi/jsi.h index 962dae93609423..d77255f61a86c7 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/jsi.h +++ b/packages/react-native/ReactCommon/jsi/jsi/jsi.h @@ -209,6 +209,13 @@ class JSI_EXPORT Runtime { virtual Value evaluatePreparedJavaScript( const std::shared_ptr& js) = 0; + /// Queues a microtask in the JavaScript VM internal Microtask (a.k.a. Job in + /// ECMA262) queue, to be executed when the host drains microtasks in + /// its event loop implementation. + /// + /// \param callback a function to be executed as a microtask. + virtual void queueMicrotask(const jsi::Function& callback) = 0; + /// Drain the JavaScript VM internal Microtask (a.k.a. Job in ECMA262) queue. /// /// \param maxMicrotasksHint a hint to tell an implementation that it should diff --git a/packages/react-native/sdks/.hermesversion b/packages/react-native/sdks/.hermesversion index 038c63a3fd7afc..ffcbbccc462b7c 100644 --- a/packages/react-native/sdks/.hermesversion +++ b/packages/react-native/sdks/.hermesversion @@ -1 +1 @@ -hermes-2024-06-03-RNv0.74.2-bb1e74fe1e95c2b5a2f4f9311152da052badc2bc +hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85