Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/react-native/React/Base/RCTConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ RCT_EXTERN NSString *const RCTJavaScriptWillStartLoadingNotification;

/**
* This notification fires when the bridge starts executing the JS bundle.
* On iOS, `RCTInstance` posts it synchronously right before a bundle runs, on the JS thread, and right before a
* segment is registered, on the registering thread. `userInfo` holds the `bridge` and the bundle or segment file
* `url`. Observers must not use UIKit or wait on the main thread.
*/
RCT_EXTERN NSString *const RCTJavaScriptWillStartExecutingNotification;

/**
* This notification fires when the bridge has finished loading the JS bundle.
* On iOS, `RCTInstance` posts it on the main thread after the bundle has run, with the `bridge` and the bundle's
* `url` in `userInfo`.
*/
RCT_EXTERN NSString *const RCTJavaScriptDidLoadNotification;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import <OCMock/OCMock.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTBundleManager.h>
#import <React/RCTConstants.h>
#import <React/RCTJavaScriptLoader.h>
#import <React/RCTSurfacePresenter.h>
#import <ReactCommon/RCTHermesInstance.h>
Expand Down Expand Up @@ -242,6 +243,111 @@ - (void)testBundleLoadAwaitsMainQueueModuleSetup
@"FakeEagerModule -init must have fully returned before the JS bundle finishes loading");
}

- (void)testPostsTheBundleURLBeforeTheBundleRunsAndAfterItLoads
{
OCMStub([_mockDelegate unstableModulesRequiringMainQueueSetup]).andReturn(@[]);
NSURL *bundleURL = [NSURL URLWithString:@"file:///empty.bundle"];
RCTSource *emptySource = OCMClassMock([RCTSource class]);
OCMStub([emptySource url]).andReturn(bundleURL);
OCMStub([emptySource data]).andReturn([NSData data]);
OCMStub([_mockDelegate loadBundleAtURL:[OCMArg any] onProgress:[OCMArg any] onComplete:[OCMArg any]])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained RCTSourceLoadBlock onComplete;
[invocation getArgument:&onComplete atIndex:4];
onComplete(nil, emptySource);
});

NSMutableArray<NSString *> *order = [NSMutableArray new];
__block NSDictionary *willExecuteUserInfo = nil;
__block NSDictionary *didLoadUserInfo = nil;
XCTestExpectation *willExecute = [self expectationWithDescription:RCTJavaScriptWillStartExecutingNotification];
XCTestExpectation *didLoad = [self expectationWithDescription:RCTJavaScriptDidLoadNotification];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
id willExecuteObserver = [center addObserverForName:RCTJavaScriptWillStartExecutingNotification
object:nil
queue:nil
usingBlock:^(NSNotification *notification) {
@synchronized(order) {
[order addObject:@"willExecute"];
}
willExecuteUserInfo = notification.userInfo;
[willExecute fulfill];
}];
_bundleLoadObserver = [center addObserverForName:@"RCTInstanceDidLoadBundle"
object:nil
queue:nil
usingBlock:^(NSNotification *_) {
@synchronized(order) {
[order addObject:@"didLoadBundle"];
}
}];
id didLoadObserver = [center addObserverForName:RCTJavaScriptDidLoadNotification
object:nil
queue:nil
usingBlock:^(NSNotification *notification) {
didLoadUserInfo = notification.userInfo;
[didLoad fulfill];
}];

RCTInstance *instance = [self makeInstance];
[self waitForExpectations:@[ willExecute, didLoad ] timeout:10.0];
[instance invalidate];
[center removeObserver:willExecuteObserver];
[center removeObserver:didLoadObserver];

XCTAssertEqualObjects(willExecuteUserInfo[@"url"], bundleURL);
XCTAssertNotNil(willExecuteUserInfo[@"bridge"]);
XCTAssertEqualObjects(didLoadUserInfo[@"url"], bundleURL);
XCTAssertNotNil(didLoadUserInfo[@"bridge"]);
@synchronized(order) {
XCTAssertEqualObjects(order, (@[ @"willExecute", @"didLoadBundle" ]));
}
}

- (void)testPostsTheSegmentURLBeforeTheSegmentIsRegistered
{
OCMStub([_mockDelegate unstableModulesRequiringMainQueueSetup]).andReturn(@[]);
RCTSource *emptySource = OCMClassMock([RCTSource class]);
OCMStub([emptySource url]).andReturn([NSURL URLWithString:@"file:///empty.bundle"]);
OCMStub([emptySource data]).andReturn([NSData data]);
OCMStub([_mockDelegate loadBundleAtURL:[OCMArg any] onProgress:[OCMArg any] onComplete:[OCMArg any]])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained RCTSourceLoadBlock onComplete;
[invocation getArgument:&onComplete atIndex:4];
onComplete(nil, emptySource);
});
XCTestExpectation *didLoad = [self expectationWithDescription:RCTJavaScriptDidLoadNotification];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
id didLoadObserver = [center addObserverForName:RCTJavaScriptDidLoadNotification
object:nil
queue:nil
usingBlock:^(NSNotification *_) {
[didLoad fulfill];
}];
RCTInstance *instance = [self makeInstance];
[self waitForExpectations:@[ didLoad ] timeout:10.0];
[center removeObserver:didLoadObserver];

NSMutableArray<NSDictionary *> *willExecuteUserInfos = [NSMutableArray new];
id willExecuteObserver = [center addObserverForName:RCTJavaScriptWillStartExecutingNotification
object:nil
queue:nil
usingBlock:^(NSNotification *notification) {
@synchronized(willExecuteUserInfos) {
[willExecuteUserInfos addObject:notification.userInfo ?: @{}];
}
}];
[instance registerSegmentWithId:@7 path:@"/segments/missing-7.js"];
[center removeObserver:willExecuteObserver];
[instance invalidate];

@synchronized(willExecuteUserInfos) {
XCTAssertEqual(willExecuteUserInfos.count, 1u);
XCTAssertEqualObjects(willExecuteUserInfos.firstObject[@"url"], [NSURL fileURLWithPath:@"/segments/missing-7.js"]);
XCTAssertNotNil(willExecuteUserInfos.firstObject[@"bridge"]);
}
}

- (void)testInvalidateDoesNotDeadlockWhenSuspendCallsIntoJS
{
OCMStub([_mockDelegate unstableModulesRequiringMainQueueSetup]).andReturn(@[]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@
using namespace facebook;
using namespace facebook::react;

static NSDictionary<NSString *, id> *RCTJavaScriptLoadUserInfo(
RCTBridgeProxy *_Nullable bridgeProxy,
NSURL *_Nullable url)
{
NSMutableDictionary<NSString *, id> *userInfo = [NSMutableDictionary new];
userInfo[@"bridge"] = bridgeProxy;
userInfo[@"url"] = url;
return userInfo;
}

@interface RCTInstance () <RCTTurboModuleManagerDelegate, RCTEventDispatcherObserver>
@end

Expand Down Expand Up @@ -227,6 +237,10 @@ - (void)invalidate
- (void)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path
{
if (_valid) {
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptWillStartExecutingNotification
object:self
userInfo:RCTJavaScriptLoadUserInfo(_bridgeProxy, path ? [NSURL fileURLWithPath:path] : nil)];
_reactInstance->registerSegment(static_cast<uint32_t>([segmentId unsignedIntValue]), path.UTF8String);
}
}
Expand Down Expand Up @@ -606,13 +620,22 @@ - (void)_loadScriptFromSource:(RCTSource *)source
auto script = std::make_unique<NSDataBigString>(source.data);
const auto *url = deriveSourceURL(source.url).UTF8String;

auto beforeLoad = [waitUntilModuleSetupComplete = self->_waitUntilModuleSetupComplete](jsi::Runtime &_) {
if (waitUntilModuleSetupComplete) {
waitUntilModuleSetupComplete();
}
};
NSURL *sourceURL = source.url;
__weak __typeof(self) weakSelf = self;
auto afterLoad = [weakSelf](jsi::Runtime &) {
auto beforeLoad =
[waitUntilModuleSetupComplete = self->_waitUntilModuleSetupComplete, weakSelf, sourceURL](jsi::Runtime &) {
if (waitUntilModuleSetupComplete) {
waitUntilModuleSetupComplete();
}
// Posted synchronously on the JS thread right before the bundle runs, so observers learn of every bundle that
// starts executing, including one that throws before afterLoad.
__strong __typeof(weakSelf) strongSelf = weakSelf;
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptWillStartExecutingNotification
object:strongSelf
userInfo:RCTJavaScriptLoadUserInfo(strongSelf ? strongSelf->_bridgeProxy : nil, sourceURL)];
};
auto afterLoad = [weakSelf, sourceURL](jsi::Runtime &) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTInstanceDidLoadBundle" object:nil];
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
Expand All @@ -622,7 +645,7 @@ - (void)_loadScriptFromSource:(RCTSource *)source
RCTExecuteOnMainQueue(^{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
object:strongSelf
userInfo:bridgeProxy ? @{@"bridge" : bridgeProxy} : @{}];
userInfo:RCTJavaScriptLoadUserInfo(bridgeProxy, sourceURL)];
});
}
};
Expand Down
Loading