Skip to content

Commit

Permalink
fix: Reject Promise with timeout if it gets destroyed (#444)
Browse files Browse the repository at this point in the history
* fix: Reject Promise with timeout if it gets destroyed

If a Promise gets destroyed without ever being resolved or rejected, it goes stale.

This PR fixes this and rejects a Promise with a timeout error if it gets destructed and is still pending

* Add promise typename

* Reject swift promise as well

* Update Promise.hpp

* fix: Build

* Update Dispatcher.cpp

* fix: Also reject `JPromise` if it gets destroyed
  • Loading branch information
mrousavy authored Dec 22, 2024
1 parent 67ee05d commit 2ebff1b
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class HybridTestObjectSwift : HybridTestObjectSwiftKotlinSpec {
func callSumUpNTimes(callback: @escaping (() -> Promise<Double>), n: Double) throws -> Promise<Double> {
var result = 0.0
return Promise.async {
for i in 1...Int(n) {
for _ in 1...Int(n) {
let current = try await callback().await()
result += current
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ class JPromise final : public jni::HybridClass<JPromise> {
return newObjectCxxArgs();
}

public:
~JPromise() override {
if (_result == nullptr && _error == nullptr) [[unlikely]] {
jni::ThreadScope::WithClassLoader([&]() {
std::runtime_error error("Timeouted: JPromise was destroyed!");
this->reject(jni::getJavaExceptionForCppException(std::make_exception_ptr(error)));
});
}
}

public:
void resolve(jni::alias_ref<jni::JObject> result) {
std::unique_lock lock(_mutex);
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native-nitro-modules/cpp/core/Promise.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class Promise final {
private:
Promise() {}

public:
~Promise() {
if (isPending()) [[unlikely]] {
auto message = std::string("Timeouted: Promise<") + TypeInfo::getFriendlyTypename<TResult>() + "> was destroyed!";
reject(std::make_exception_ptr(std::runtime_error(message)));
}
}

public:
/**
* Creates a new pending Promise that has to be resolved
Expand Down Expand Up @@ -258,6 +266,14 @@ class Promise<void> final {
private:
Promise() {}

public:
~Promise() {
if (isPending()) [[unlikely]] {
std::runtime_error error("Timeouted: Promise<void> was destroyed!");
reject(std::make_exception_ptr(std::move(error)));
}
}

public:
static std::shared_ptr<Promise> create() {
return std::shared_ptr<Promise>(new Promise());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
//
// Dispatcher.cpp
// react-native-nitro
//
// Created by Marc Rousavy on 22.07.24.
//

#include "Dispatcher.hpp"
#include "JSIHelpers.hpp"
#include "NitroDefines.hpp"
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-nitro-modules/ios/core/Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public final class Promise<T> {

deinit {
if state == nil {
print("⚠️ Promise<\(String(describing: T.self))> got destroyed, but was never resolved or rejected! It is probably left hanging in JS now.")
let message = "Timeouted: Promise<\(String(describing: T.self))> was destroyed!"
reject(withError: RuntimeError.error(withMessage: message))
}
}

Expand Down

0 comments on commit 2ebff1b

Please sign in to comment.