diff --git a/packages/react-native/React/Base/RCTConstants.h b/packages/react-native/React/Base/RCTConstants.h index 185f57f4881f..d0358f4b73a5 100644 --- a/packages/react-native/React/Base/RCTConstants.h +++ b/packages/react-native/React/Base/RCTConstants.h @@ -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; diff --git a/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm b/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm index 6702584d1af3..a2e2f332a70d 100644 --- a/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm +++ b/packages/react-native/ReactCommon/react/runtime/iostests/RCTInstanceTests.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import #import #import @@ -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 *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 *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(@[]); diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index fc18847f27af..7679c1aa0242 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -60,6 +60,16 @@ using namespace facebook; using namespace facebook::react; +static NSDictionary *RCTJavaScriptLoadUserInfo( + RCTBridgeProxy *_Nullable bridgeProxy, + NSURL *_Nullable url) +{ + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + userInfo[@"bridge"] = bridgeProxy; + userInfo[@"url"] = url; + return userInfo; +} + @interface RCTInstance () @end @@ -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([segmentId unsignedIntValue]), path.UTF8String); } } @@ -606,13 +620,22 @@ - (void)_loadScriptFromSource:(RCTSource *)source auto script = std::make_unique(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) { @@ -622,7 +645,7 @@ - (void)_loadScriptFromSource:(RCTSource *)source RCTExecuteOnMainQueue(^{ [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification object:strongSelf - userInfo:bridgeProxy ? @{@"bridge" : bridgeProxy} : @{}]; + userInfo:RCTJavaScriptLoadUserInfo(bridgeProxy, sourceURL)]; }); } };