22#include < unordered_map>
33#include " Logger.hpp"
44
5- // This file provides a React Native-friendly implementation of Node-API's
6- // thread-safe function primitive. In RN we don't own/libuv, so we:
7- // - Use CallInvoker to hop onto the JS thread instead of uv_async.
8- // - Track a registry mapping unique IDs to shared_ptrs for lookup/lifetime.
9- // - Emulate ref/unref semantics without affecting any event loop.
10-
5+ // Global registry to map unique IDs to ThreadSafeFunction instances.
6+ // We use IDs instead of raw pointers to avoid any use-after-free issues.
117static std::unordered_map<std::uintptr_t ,
128 std::shared_ptr<callstack::nodeapihost::ThreadSafeFunction>>
139 registry;
1410static std::mutex registryMutex;
1511static std::atomic<std::uintptr_t > nextId{1 };
1612
17- // Constants for better readability
1813static constexpr size_t INITIAL_REF_COUNT = 1 ;
1914
2015namespace callstack ::nodeapihost {
@@ -44,13 +39,10 @@ ThreadSafeFunction::ThreadSafeFunction(
4439 context_{context},
4540 callJsCb_{callJsCb} {
4641 if (jsFunc) {
47- // Keep JS function alive across async hops; fatal here mirrors Node-API's
48- // behavior when environment is irrecoverable.
42+ // Keep JS function alive across async hops
4943 const auto status =
5044 napi_create_reference (env, jsFunc, INITIAL_REF_COUNT, &jsFuncRef_);
5145 if (status != napi_ok) {
52- // Consider throwing an exception instead of fatal error in future
53- // versions
5446 napi_fatal_error (nullptr ,
5547 0 ,
5648 " Failed to create JS function reference" ,
@@ -101,6 +93,7 @@ std::shared_ptr<ThreadSafeFunction> ThreadSafeFunction::create(
10193std::shared_ptr<ThreadSafeFunction> ThreadSafeFunction::get (
10294 napi_threadsafe_function func) {
10395 std::lock_guard lock{registryMutex};
96+ // Cast the handle back to ID for registry lookup
10497 const auto id = reinterpret_cast <std::uintptr_t >(func);
10598 const auto it = registry.find (id);
10699 return it != registry.end () ? it->second : nullptr ;
@@ -120,7 +113,7 @@ napi_status ThreadSafeFunction::getContext(void** result) noexcept {
120113}
121114
122115napi_status ThreadSafeFunction::call (
123- void * data, napi_threadsafe_function_call_mode isBlocking) {
116+ void * data, napi_threadsafe_function_call_mode isBlocking) noexcept {
124117 if (isClosingOrAborted ()) {
125118 return napi_closing;
126119 }
@@ -152,7 +145,7 @@ napi_status ThreadSafeFunction::call(
152145 return napi_ok;
153146}
154147
155- napi_status ThreadSafeFunction::acquire () {
148+ napi_status ThreadSafeFunction::acquire () noexcept {
156149 if (closing_.load (std::memory_order_acquire)) {
157150 return napi_closing;
158151 }
@@ -161,7 +154,7 @@ napi_status ThreadSafeFunction::acquire() {
161154}
162155
163156napi_status ThreadSafeFunction::release (
164- napi_threadsafe_function_release_mode mode) {
157+ napi_threadsafe_function_release_mode mode) noexcept {
165158 // Node-API semantics: abort prevents further JS calls and wakes any waiters.
166159 if (mode == napi_tsfn_abort) {
167160 aborted_.store (true , std::memory_order_relaxed);
@@ -185,8 +178,8 @@ napi_status ThreadSafeFunction::release(
185178}
186179
187180napi_status ThreadSafeFunction::ref () noexcept {
188- // In libuv, this would keep the loop alive. In RN we don't own or expose a
189- // libuv loop. We just track the state for API parity .
181+ // In libuv, this allows the loop to exit if nothing else is keeping it
182+ // alive. In RN this is a no-op beyond state tracking .
190183 referenced_.store (true , std::memory_order_relaxed);
191184 return napi_ok;
192185}
@@ -199,6 +192,7 @@ napi_status ThreadSafeFunction::unref() noexcept {
199192}
200193
201194void ThreadSafeFunction::finalize () {
195+ // Ensure finalization happens exactly once
202196 bool expected = false ;
203197 if (!finalizeScheduled_.compare_exchange_strong (
204198 expected, true , std::memory_order_acq_rel)) {
@@ -208,7 +202,6 @@ void ThreadSafeFunction::finalize() {
208202 closing_.store (true , std::memory_order_release);
209203
210204 const auto onFinalize = [self = shared_from_this ()] {
211- // Invoke user finalizer and unregister the handle from the global map.
212205 if (self->threadFinalizeCb_ ) {
213206 self->threadFinalizeCb_ (
214207 self->env_ , self->threadFinalizeData_ , self->context_ );
@@ -249,12 +242,14 @@ void ThreadSafeFunction::processQueue() {
249242 // Execute JS callback if we have data and aren't aborted
250243 if (queuedData && !aborted_.load (std::memory_order_relaxed)) {
251244 if (callJsCb_) {
245+ // Prefer the user-provided callJsCb_ (Node-API compatible)
252246 napi_value fn{nullptr };
253247 if (jsFuncRef_) {
254248 napi_get_reference_value (env_, jsFuncRef_, &fn);
255249 }
256250 callJsCb_ (env_, fn, context_, queuedData);
257251 } else if (jsFuncRef_) {
252+ // Fallback: call JS function directly with no args
258253 napi_value fn{nullptr };
259254 if (napi_get_reference_value (env_, jsFuncRef_, &fn) == napi_ok) {
260255 napi_value recv{nullptr };
@@ -271,14 +266,13 @@ void ThreadSafeFunction::processQueue() {
271266 }
272267}
273268
274- [[nodiscard]] bool ThreadSafeFunction::isClosingOrAborted () const noexcept {
269+ bool ThreadSafeFunction::isClosingOrAborted () const noexcept {
275270 return aborted_.load (std::memory_order_relaxed) ||
276271 closing_.load (std::memory_order_relaxed);
277272}
278273
279- [[nodiscard]] bool ThreadSafeFunction::shouldFinalize () const noexcept {
274+ bool ThreadSafeFunction::shouldFinalize () const noexcept {
280275 return threadCount_.load (std::memory_order_acquire) == 0 &&
281276 !closing_.load (std::memory_order_acquire);
282277}
283-
284- } // namespace callstack::nodeapihost
278+ } // namespace callstack::nodeapihost
0 commit comments