Skip to content

Commit

Permalink
Flush the React Native UI task queue whenever we call from C++ to JS …
Browse files Browse the repository at this point in the history
…by calling jsCallInvoker->invokeAsync.

React Native has its own JS "microtask" queue, implemented in https://github.com/facebook/react-native/blob/main/Libraries/Core/Timers/JSTimers.js. Any asynchronous work, e.g. `setTimeout` or `setImmediate`, is added to this queue, and the queue is flushed whenever a message is sent from React Native's native core to JS: https://github.com/facebook/react-native/blob/main/Libraries/BatchedBridge/MessageQueue.js#L108.

However, our native to JS messages are not being passed via this abstraction - instead, we hook directly into the JS engine. This means that React Native is not aware that when Realm has done some async work, we might need to update the UI (and therefore flush the task queue), and this can result in Realm-related UI updates not showing until some action is taken which causes React Native to send a message from the core to JS (e.g. touching the screen), which flushes this task queue, resolving pending promises and updating the UI.

This commit calls the React Native jsCallInvoker->invokeAsync method, which internally flushes the task queue. As this method is async, we wait for any current pending invocation to complete before triggering another one (using a flag, so the call is "debounced")
  • Loading branch information
Tom Duncalf committed Apr 19, 2022
1 parent a7c6de8 commit 34fcb41
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 62 deletions.
204 changes: 150 additions & 54 deletions integration-tests/environments/react-native/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 21 additions & 3 deletions react-native/ios/RealmReact/RealmReact.mm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#import <React/RCTBridge+Private.h>
#import <React/RCTJavaScriptExecutor.h>
#include <ReactCommon/CallInvoker.h>

#import <objc/runtime.h>
#import <arpa/inet.h>
Expand Down Expand Up @@ -51,6 +52,7 @@ - (JSContext *)context;
@interface RCTBridge (Realm_RCTCxxBridge)
- (JSGlobalContextRef)jsContextRef;
- (void *)runtime;
- (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvoker;
@end

extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create) {
Expand Down Expand Up @@ -83,6 +85,7 @@ @interface RealmReact () <RCTBridgeModule>

@implementation RealmReact {
NSMutableDictionary *_eventHandlers;
bool waitingForFlush;

#if DEBUG
GCDWebServer *_webServer;
Expand All @@ -108,6 +111,7 @@ - (instancetype)init {
self = [super init];
if (self) {
_eventHandlers = [[NSMutableDictionary alloc] init];
waitingForFlush = false;
}
return self;
}
Expand Down Expand Up @@ -274,15 +278,15 @@ - (void)dealloc {

typedef JSGlobalContextRef (^JSContextRefExtractor)();

void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor) {
void _initializeOnJSThread(JSContextRefExtractor jsContextExtractor, std::function<void()> flushUiQueue) {
// Make sure the previous JS thread is completely finished before continuing.
static __weak NSThread *s_currentJSThread;
while (s_currentJSThread && !s_currentJSThread.finished) {
[NSThread sleepForTimeInterval:0.1];
}
s_currentJSThread = [NSThread currentThread];

RJSInitializeInContext(jsContextExtractor());
RJSInitializeInContext(jsContextExtractor(), flushUiQueue);
}

- (void)setBridge:(RCTBridge *)bridge {
Expand Down Expand Up @@ -310,7 +314,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)]) {
Expand All @@ -325,6 +329,18 @@ - (void)setBridge:(RCTBridge *)bridge {
JSGlobalContextRef ctx_;
};
return static_cast<RealmJSCRuntime*>(bridge.runtime)->ctx_;
}, ^{
// This calls into JSIExecutor::flush() inside React Native to flush any pending UI
// updates after we have called into JS from C++
//
// TODO add a bit more info
if (!waitingForFlush) {
waitingForFlush = true;
[bridge jsCallInvoker]->invokeAsync([&](){
printf("flush\n");
waitingForFlush = false;
});
}
});
} queue:RCTJSThread];
} else { // React Native 0.44 and older
Expand All @@ -341,6 +357,8 @@ - (void)setBridge:(RCTBridge *)bridge {

_initializeOnJSThread(^ {
return RealmReactGetJSGlobalContextForExecutor(executor, true);
}, [&]() {
// jsCallInvoker does not exist on older RN
});
}];
}
Expand Down
Loading

0 comments on commit 34fcb41

Please sign in to comment.