diff --git a/packages/react-native/Libraries/Types/CodegenTypes.js b/packages/react-native/Libraries/Types/CodegenTypes.js index 737e2f290b1049..444b1869a76120 100644 --- a/packages/react-native/Libraries/Types/CodegenTypes.js +++ b/packages/react-native/Libraries/Types/CodegenTypes.js @@ -10,6 +10,7 @@ 'use strict'; +import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type {SyntheticEvent} from './CoreEventTypes'; // Event types @@ -40,3 +41,7 @@ type DefaultTypes = number | boolean | string | $ReadOnlyArray; // // eslint-disable-next-line no-unused-vars export type WithDefault = ?Type; + +export type EventEmitter = { + addListener(handler: (T) => mixed): EventSubscription, +}; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index e083609f06e083..379620662cbd05 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -8133,6 +8133,9 @@ export type UnsafeObject = $FlowFixMe; export type UnsafeMixed = mixed; type DefaultTypes = number | boolean | string | $ReadOnlyArray; export type WithDefault = ?Type; +export type EventEmitter = { + addListener(handler: (T) => mixed): EventSubscription, +}; " `; diff --git a/packages/react-native/ReactCommon/react/bridging/Bridging.h b/packages/react-native/ReactCommon/react/bridging/Bridging.h index b8a02290d1aee1..2ea53d588ae767 100644 --- a/packages/react-native/ReactCommon/react/bridging/Bridging.h +++ b/packages/react-native/ReactCommon/react/bridging/Bridging.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include diff --git a/packages/react-native/ReactCommon/react/bridging/EventEmitter.h b/packages/react-native/ReactCommon/react/bridging/EventEmitter.h new file mode 100644 index 00000000000000..33bec2cedbbe5e --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridging/EventEmitter.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#define FRIEND_TEST(test_case_name, test_name) \ + friend class test_case_name##_##test_name##_Test + +namespace facebook::react { + +class EventSubscription { + public: + explicit EventSubscription(std::function remove) + : remove_(std::move(remove)) {} + ~EventSubscription() = default; + EventSubscription(EventSubscription&&) noexcept = default; + EventSubscription& operator=(EventSubscription&&) noexcept = default; + EventSubscription(const EventSubscription&) = delete; + EventSubscription& operator=(const EventSubscription&) = delete; + + private: + friend Bridging; + + std::function remove_; +}; + +template <> +struct Bridging { + static jsi::Object toJs( + jsi::Runtime& rt, + const EventSubscription& eventSubscription, + const std::shared_ptr& jsInvoker) { + auto result = jsi::Object(rt); + result.setProperty( + rt, "remove", bridging::toJs(rt, eventSubscription.remove_, jsInvoker)); + return result; + } +}; + +class IAsyncEventEmitter { + public: + IAsyncEventEmitter() noexcept = default; + virtual ~IAsyncEventEmitter() noexcept = default; + IAsyncEventEmitter(IAsyncEventEmitter&&) noexcept = default; + IAsyncEventEmitter& operator=(IAsyncEventEmitter&&) noexcept = default; + IAsyncEventEmitter(const IAsyncEventEmitter&) = delete; + IAsyncEventEmitter& operator=(const IAsyncEventEmitter&) = delete; + + virtual jsi::Object get( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker) const = 0; +}; + +template +class AsyncEventEmitter : public IAsyncEventEmitter { + static_assert( + sizeof...(Args) <= 1, + "AsyncEventEmitter must have at most one argument"); + + public: + AsyncEventEmitter() : state_(std::make_shared()) { + listen_ = [state = state_](AsyncCallback listener) { + std::lock_guard lock(state->mutex); + auto listenerId = state->listenerId++; + state->listeners.emplace(listenerId, std::move(listener)); + return EventSubscription([state, listenerId]() { + std::lock_guard innerLock(state->mutex); + state->listeners.erase(listenerId); + }); + }; + } + ~AsyncEventEmitter() override = default; + AsyncEventEmitter(AsyncEventEmitter&&) noexcept = default; + AsyncEventEmitter& operator=(AsyncEventEmitter&&) noexcept = default; + AsyncEventEmitter(const AsyncEventEmitter&) = delete; + AsyncEventEmitter& operator=(const AsyncEventEmitter&) = delete; + + void emit(Args... value) { + std::lock_guard lock(state_->mutex); + for (const auto& [_, listener] : state_->listeners) { + listener.call(static_cast(value)...); + } + } + + jsi::Object get( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker) const override { + auto result = jsi::Object(rt); + result.setProperty( + rt, "addListener", bridging::toJs(rt, listen_, jsInvoker)); + return result; + } + + private: + friend Bridging; + FRIEND_TEST(BridgingTest, eventEmitterTest); + + struct SharedState { + std::mutex mutex; + std::unordered_map> listeners; + size_t listenerId{}; + }; + + std::function)> listen_; + std::shared_ptr state_; +}; + +template +struct Bridging> { + static jsi::Object toJs( + jsi::Runtime& rt, + const AsyncEventEmitter& eventEmitter, + const std::shared_ptr& jsInvoker) { + return eventEmitter.get(rt, jsInvoker); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp b/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp index 45beaeff7b4e8a..3359083bde07c7 100644 --- a/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp +++ b/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp @@ -123,7 +123,7 @@ TEST_F(BridgingTest, hostObjectTest) { struct TestHostObject : public jsi::HostObject { jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override { if (name.utf8(rt) == "test") { - return jsi::Value(1); + return {1}; } return jsi::Value::undefined(); } @@ -426,6 +426,110 @@ TEST_F(BridgingTest, promiseTest) { EXPECT_NO_THROW(promise.reject("ignored")); } +using EventType = std::vector; +using EventSubscriptionsWithLastEvent = + std::vector>>; + +namespace { + +template +void addEventSubscription( + jsi::Runtime& rt, + const AsyncEventEmitter& eventEmitter, + EventSubscriptionsWithLastEvent& eventSubscriptionsWithListener, + const std::shared_ptr& invoker) { + auto eventEmitterJs = bridging::toJs(rt, eventEmitter, invoker); + auto lastEvent = std::make_shared(); + auto listenJs = bridging::toJs( + rt, + [lastEvent = lastEvent](const EventType& event) { *lastEvent = event; }, + invoker); + eventSubscriptionsWithListener.emplace_back(std::make_pair( + jsi::Object(eventEmitterJs.getPropertyAsFunction(rt, "addListener") + .callWithThis(rt, eventEmitterJs, listenJs) + .asObject(rt)), + std::move(lastEvent))); +} + +} // namespace + +TEST_F(BridgingTest, eventEmitterTest) { + EventSubscriptionsWithLastEvent eventSubscriptionsWithListener; + + AsyncEventEmitter eventEmitter; + EXPECT_NO_THROW(eventEmitter.emit({"one", "two", "three"})); + EXPECT_EQ(0, eventSubscriptionsWithListener.size()); + + // register 3 JavaScript listeners to the event emitter + for (int i = 0; i < 3; ++i) { + addEventSubscription( + rt, eventEmitter, eventSubscriptionsWithListener, invoker); + } + + EXPECT_TRUE(eventEmitter.state_->listeners.contains(0)); + EXPECT_TRUE(eventEmitter.state_->listeners.contains(1)); + EXPECT_TRUE(eventEmitter.state_->listeners.contains(2)); + + EXPECT_NO_THROW(eventEmitter.emit({"four", "five", "six"})); + flushQueue(); + + // verify all listeners received the event + for (const auto& [_, lastEvent] : eventSubscriptionsWithListener) { + EXPECT_EQ(3, lastEvent->size()); + EXPECT_EQ("four", lastEvent->at(0)); + EXPECT_EQ("five", lastEvent->at(1)); + EXPECT_EQ("six", lastEvent->at(2)); + } + + // Remove 2nd eventSubscriptions + eventSubscriptionsWithListener[1] + .first.getPropertyAsFunction(rt, "remove") + .callWithThis(rt, eventSubscriptionsWithListener[1].first); + eventSubscriptionsWithListener.erase( + eventSubscriptionsWithListener.begin() + 1); + + // Add 4th and 5th eventSubscriptions + addEventSubscription( + rt, eventEmitter, eventSubscriptionsWithListener, invoker); + addEventSubscription( + rt, eventEmitter, eventSubscriptionsWithListener, invoker); + + EXPECT_TRUE(eventEmitter.state_->listeners.contains(0)); + EXPECT_FALSE(eventEmitter.state_->listeners.contains(1)); + EXPECT_TRUE(eventEmitter.state_->listeners.contains(2)); + EXPECT_TRUE(eventEmitter.state_->listeners.contains(3)); + EXPECT_TRUE(eventEmitter.state_->listeners.contains(4)); + + // Emit more events + EXPECT_NO_THROW(eventEmitter.emit({"seven", "eight", "nine"})); + flushQueue(); + + for (const auto& [_, lastEvent] : eventSubscriptionsWithListener) { + EXPECT_EQ(3, lastEvent->size()); + EXPECT_EQ("seven", lastEvent->at(0)); + EXPECT_EQ("eight", lastEvent->at(1)); + EXPECT_EQ("nine", lastEvent->at(2)); + } + + // clean-up the event subscriptions + for (const auto& [eventSubscription, _] : eventSubscriptionsWithListener) { + eventSubscription.getPropertyAsFunction(rt, "remove") + .callWithThis(rt, eventSubscription); + } + flushQueue(); + + EXPECT_NO_THROW(eventEmitter.emit({"ten", "eleven", "twelve"})); + flushQueue(); + + // no new data as listeners had been removed + for (const auto& [_, lastEvent] : eventSubscriptionsWithListener) { + EXPECT_EQ(3, lastEvent->size()); + EXPECT_EQ("seven", lastEvent->at(0)); + EXPECT_EQ("eight", lastEvent->at(1)); + EXPECT_EQ("nine", lastEvent->at(2)); + } +} + TEST_F(BridgingTest, optionalTest) { EXPECT_EQ( 1, bridging::fromJs>(rt, jsi::Value(1), invoker));