diff --git a/CHANGELOG.md b/CHANGELOG.md index 5372da7145..914b4c3f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ x.x.x Release notes (yyyy-MM-dd) * None. ### Fixed -* ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?) +* Fixed issue where React Native apps would sometimes show stale Realm data until the user interacted with the app UI ([#4389](https://github.com/realm/realm-js/issues/4389), since v10.0.0) * None. ### Compatibility diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b4a4e0f891..3e1a948ded 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -342,7 +342,7 @@ PODS: - React-jsi (= 0.66.4) - React-logger (= 0.66.4) - React-perflogger (= 0.66.4) - - RealmJS (10.14.0): + - RealmJS (10.16.0): - GCDWebServer - React - Yoga (1.14.0) @@ -535,7 +535,7 @@ SPEC CHECKSUMS: React-RCTVibration: e3ffca672dd3772536cb844274094b0e2c31b187 React-runtimeexecutor: dec32ee6f2e2a26e13e58152271535fadff5455a ReactCommon: 57b69f6383eafcbd7da625bfa6003810332313c4 - RealmJS: 5a939376f3bdd94708abe2326d33d8f935b9fb37 + RealmJS: 8a3478957315c29cdc0b3f958f2e370d22330b2d Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm index 4bcc1dee97..0285be4c7e 100644 --- a/react-native/ios/RealmReact/RealmReact.mm +++ b/react-native/ios/RealmReact/RealmReact.mm @@ -22,6 +22,7 @@ #import #import +#include #import #import @@ -51,6 +52,8 @@ - (JSContext *)context; @interface RCTBridge (Realm_RCTCxxBridge) - (JSGlobalContextRef)jsContextRef; - (void *)runtime; +// Expose the CallInvoker so that we can call `invokeAsync` +- (std::shared_ptr)jsCallInvoker; @end extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create) { @@ -83,6 +86,8 @@ @interface RealmReact () @implementation RealmReact { NSMutableDictionary *_eventHandlers; + // Keep track of whether we are already waiting for the React Native UI queue to be flushed asynchronously + bool waitingForUiFlush; #if DEBUG GCDWebServer *_webServer; @@ -108,6 +113,7 @@ - (instancetype)init { self = [super init]; if (self) { _eventHandlers = [[NSMutableDictionary alloc] init]; + waitingForUiFlush = false; } return self; } @@ -274,7 +280,7 @@ - (void)dealloc { typedef JSGlobalContextRef (^JSContextRefExtractor)(); -void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor) { +void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::function flushUiQueueFn) { // Make sure the previous JS thread is completely finished before continuing. static __weak NSThread *s_currentJSThread; while (s_currentJSThread && !s_currentJSThread.finished) { @@ -282,7 +288,7 @@ void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor) { } s_currentJSThread = [NSThread currentThread]; - RJSInitializeInContext(jsContextExtractor()); + RJSInitializeInContext(jsContextExtractor(), flushUiQueueFn); } - (void)setBridge:(RCTBridge *)bridge { @@ -310,7 +316,7 @@ - (void)setBridge:(RCTBridge *)bridge { if (!self || !bridge) { return; } - + _initializeOnJSThread(^{ // RN < 0.58 has a private method that returns the js context if ([bridge respondsToSelector:@selector(jsContextRef)]) { @@ -325,6 +331,28 @@ - (void)setBridge:(RCTBridge *)bridge { JSGlobalContextRef ctx_; }; return static_cast(bridge.runtime)->ctx_; + }, ^{ + // Calling jsCallInvokver->invokeAsync tells React Native to execute the lambda passed + // in on the JS thread, and then flush the internal "microtask queue", which has the + // effect of flushing any pending UI updates. + // + // We call this after we have called into JS from C++, in order to ensure that the RN + // UI updates in response to any changes from Realm. We need to do this as we bypass + // the usual RN bridge mechanism for communicating between C++ and JS, so without doing + // this RN has no way to know that a change has occurred which might require an update + // (see #4389, facebook/react-native#33006). + // + // Calls are debounced using the waitingForUiFlush flag, so if an async flush is already + // pending when another JS to C++ call happens, we don't call invokeAsync again. This works + // because the work is performed before the microtask queue is flushed - see sequence + // diagram at https://bit.ly/3kexhHm. It might be possible to further optimize this, + // e.g. only flush the queue a maximum of once per frame, but this seems reasonable. + if (!waitingForUiFlush) { + waitingForUiFlush = true; + [bridge jsCallInvoker]->invokeAsync([&](){ + waitingForUiFlush = false; + }); + } }); } queue:RCTJSThread]; } else { // React Native 0.44 and older @@ -341,6 +369,8 @@ - (void)setBridge:(RCTBridge *)bridge { _initializeOnJSThread(^ { return RealmReactGetJSGlobalContextForExecutor(executor, true); + }, [&]() { + // jsCallInvoker does not exist on older RN }); }]; } diff --git a/src/android/jsc_override.cpp b/src/android/jsc_override.cpp index 88a9f6610a..580a42e2bd 100644 --- a/src/android/jsc_override.cpp +++ b/src/android/jsc_override.cpp @@ -144,7 +144,10 @@ static JSGlobalContextRef create_context(JSContextGroupRef group, JSClassRef glo // Clear cache from previous instances. RJSInvalidateCaches(); - RJSInitializeInContext(ctx); + // We pass a no-op lambda for the `flushUiQueue` function, as there's no easy way to + // access the React Native context from here and the UI flush bug (#4389) has not been + // seen on Android. We should be able to do this properly on Hermes. + RJSInitializeInContext(ctx, []() {}); realmContextInjected = true; return ctx; } diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp index 984bf55454..8d9d633794 100644 --- a/src/jsc/jsc_class.hpp +++ b/src/jsc/jsc_class.hpp @@ -19,6 +19,7 @@ #pragma once #include "jsc_types.hpp" +#include "jsc_function.hpp" #include "js_class.hpp" #include "js_util.hpp" @@ -41,7 +42,7 @@ extern js::Protected FunctionPrototype; extern js::Protected RealmObjectClassConstructor; extern js::Protected RealmObjectClassConstructorPrototype; -static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject) +static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject, std::function flushUiQueue) { // handle ReactNative app refresh by reseting the cached constructor values if (RealmObjectClassConstructor) { @@ -63,6 +64,8 @@ static inline void jsc_class_init(JSContextRef ctx, JSObjectRef globalObject) JSObjectRef globalFunction = jsc::Value::to_object(ctx, value); value = jsc::Object::get_property(ctx, globalFunction, "prototype"); FunctionPrototype = js::Protected(ctx, Value::to_object(ctx, value)); + + js::flush_ui_queue = flushUiQueue; } template diff --git a/src/jsc/jsc_function.hpp b/src/jsc/jsc_function.hpp index d2f99d5c1e..4ce04ef477 100644 --- a/src/jsc/jsc_function.hpp +++ b/src/jsc/jsc_function.hpp @@ -23,12 +23,18 @@ namespace realm { namespace js { +// Function passed in from the React Native initialisation code to flush the UI microtask queue +extern std::function flush_ui_queue; + template <> inline JSValueRef jsc::Function::call(JSContextRef ctx, const JSObjectRef& function, const JSObjectRef& this_object, size_t argc, const JSValueRef arguments[]) { JSValueRef exception = nullptr; JSValueRef result = JSObjectCallAsFunction(ctx, function, this_object, argc, arguments, &exception); + + flush_ui_queue(); + if (exception) { throw jsc::Exception(ctx, exception); } @@ -48,6 +54,9 @@ inline JSObjectRef jsc::Function::construct(JSContextRef ctx, const JSObjectRef& { JSValueRef exception = nullptr; JSObjectRef result = JSObjectCallAsConstructor(ctx, function, argc, arguments, &exception); + + flush_ui_queue(); + if (exception) { throw jsc::Exception(ctx, exception); } diff --git a/src/jsc/jsc_init.cpp b/src/jsc/jsc_init.cpp index ba290616b7..63d3070f98 100644 --- a/src/jsc/jsc_init.cpp +++ b/src/jsc/jsc_init.cpp @@ -31,6 +31,10 @@ js::Protected FunctionPrototype; js::Protected RealmObjectClassConstructor; js::Protected RealmObjectClassConstructorPrototype; } // namespace jsc + +namespace js { +std::function flush_ui_queue; +} // namespace js } // namespace realm extern "C" { @@ -43,13 +47,13 @@ JSObjectRef RJSConstructorCreate(JSContextRef ctx) return js::RealmClass::create_constructor(ctx); } -void RJSInitializeInContext(JSContextRef ctx) +void RJSInitializeInContext(JSContextRef ctx, std::function flush_ui_queue) { static const jsc::String realm_string = "Realm"; JSObjectRef global_object = JSContextGetGlobalObject(ctx); - jsc_class_init(ctx, global_object); + jsc_class_init(ctx, global_object, flush_ui_queue); JSObjectRef realm_constructor = RJSConstructorCreate(ctx); diff --git a/src/jsc/jsc_init.h b/src/jsc/jsc_init.h index 8521abef9d..96f7ddb168 100644 --- a/src/jsc/jsc_init.h +++ b/src/jsc/jsc_init.h @@ -19,13 +19,14 @@ #pragma once #include +#include #ifdef __cplusplus extern "C" { #endif JSObjectRef RJSConstructorCreate(JSContextRef ctx); -void RJSInitializeInContext(JSContextRef ctx); +void RJSInitializeInContext(JSContextRef ctx, std::function flush_ui_queue); void RJSInvalidateCaches(); #ifdef __cplusplus diff --git a/src/jsc/rpc.cpp b/src/jsc/rpc.cpp index 9940ea3011..1b963a5d89 100644 --- a/src/jsc/rpc.cpp +++ b/src/jsc/rpc.cpp @@ -387,7 +387,7 @@ RPCServerImpl::RPCServerImpl() } m_requests["/create_session"] = [this](const json dict) { - RJSInitializeInContext(m_context); + RJSInitializeInContext(m_context, []() {}); jsc::String realm_string = "Realm"; JSObjectRef realm_constructor =