diff --git a/React/Base/RCTDefines.h b/React/Base/RCTDefines.h index b3b017dff..a79f76b30 100644 --- a/React/Base/RCTDefines.h +++ b/React/Base/RCTDefines.h @@ -56,6 +56,14 @@ #endif #endif +#ifndef ENABLE_PACKAGER_CONNECTION +#if RCT_DEV && __has_include() +#define ENABLE_PACKAGER_CONNECTION 1 +#else +#define ENABLE_PACKAGER_CONNECTION 0 +#endif +#endif + #if RCT_DEV #define RCT_IF_DEV(...) __VA_ARGS__ #else diff --git a/React/DevSupport/RCTPackagerClient.h b/React/DevSupport/RCTPackagerClient.h index 41cb7b645..4b070aa9b 100644 --- a/React/DevSupport/RCTPackagerClient.h +++ b/React/DevSupport/RCTPackagerClient.h @@ -16,11 +16,16 @@ extern const int RCT_PACKAGER_CLIENT_PROTOCOL_VERSION; -@protocol RCTPackagerClientMethod +@protocol RCTPackagerClientMethod - (void)handleRequest:(id)params withResponder:(RCTPackagerClientResponder *)responder; - (void)handleNotification:(id)params; +@optional + +/** By default object will receive its methods on the main queue, unless this method is overriden. */ +- (dispatch_queue_t)methodQueue; + @end @interface RCTPackagerClientResponder : NSObject diff --git a/React/DevSupport/RCTPackagerConnection.h b/React/DevSupport/RCTPackagerConnection.h index c31a64cfd..ed8c951bb 100644 --- a/React/DevSupport/RCTPackagerConnection.h +++ b/React/DevSupport/RCTPackagerConnection.h @@ -13,19 +13,31 @@ #if RCT_DEV +NS_ASSUME_NONNULL_BEGIN + @class RCTBridge; @protocol RCTPackagerClientMethod; @protocol RCTPackagerConnectionConfig; /** - * Encapsulates connection to React Native packager + * Encapsulates connection to React Native packager. + * Dispatches messages from websocket to message handlers that must implement + * protocol. + * Message dispatch is performed on the main queue, unless message handler + * provides its own queue by overriding "methodQueue" method. */ @interface RCTPackagerConnection : NSObject ++ (void)checkDefaultConnectionWithCallback:(void (^)(BOOL isRunning))callback + queue:(dispatch_queue_t)queue; + + (instancetype)connectionForBridge:(RCTBridge *)bridge; - (instancetype)initWithConfig:(id)config; - (void)addHandler:(id)handler forMethod:(NSString *)name; +- (void)stop; @end +NS_ASSUME_NONNULL_END + #endif diff --git a/React/DevSupport/RCTPackagerConnection.m b/React/DevSupport/RCTPackagerConnection.m index 8cb79b6c6..5b16bb9a2 100644 --- a/React/DevSupport/RCTPackagerConnection.m +++ b/React/DevSupport/RCTPackagerConnection.m @@ -13,6 +13,7 @@ #import #import +#import #import #import #import @@ -26,6 +27,16 @@ #if RCT_DEV +static dispatch_queue_t RCTPackagerConnectionQueue() +{ + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.facebook.RCTPackagerConnectionQueue", DISPATCH_QUEUE_SERIAL); + }); + return queue; +}; + @interface RCTPackagerConnection () @end @@ -35,6 +46,29 @@ NSMutableDictionary> *_handlers; } ++ (void)checkDefaultConnectionWithCallback:(void (^)(BOOL isRunning))callback + queue:(dispatch_queue_t)queue +{ + RCTBundleURLProvider *const settings = [RCTBundleURLProvider sharedSettings]; + NSURLComponents *components = [NSURLComponents new]; + components.scheme = @"http"; + components.host = settings.jsLocation ?: @"localhost"; + components.port = @(kRCTBundleURLProviderDefaultPort); + components.path = @"/status"; + [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:components.URL] + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { + NSString *const status = data != nil + ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] + : nil; + BOOL isRunning = [status isEqualToString:@"packager-status:running"]; + + dispatch_async(queue, ^{ + callback(isRunning); + }); + }]; +} + + (instancetype)connectionForBridge:(RCTBridge *)bridge { RCTPackagerConnectionBridgeConfig *config = [[RCTPackagerConnectionBridgeConfig alloc] initWithBridge:bridge]; @@ -46,7 +80,6 @@ if (self = [super init]) { _packagerURL = [config packagerURL]; _handlers = [[config defaultPackagerMethods] mutableCopy]; - [self connect]; } return self; @@ -70,19 +103,35 @@ } NSString *key = [url absoluteString]; - RCTReconnectingWebSocket *webSocket = socketConnections[key]; - if (!webSocket) { - webSocket = [[RCTReconnectingWebSocket alloc] initWithURL:url]; - [webSocket start]; - socketConnections[key] = webSocket; + _socket = socketConnections[key]; + if (!_socket) { + _socket = [[RCTReconnectingWebSocket alloc] initWithURL:url]; + _socket.delegateDispatchQueue = RCTPackagerConnectionQueue(); + [_socket start]; + socketConnections[key] = _socket; } - webSocket.delegate = self; + _socket.delegate = self; } +- (void)stop +{ + [_socket stop]; +} + + - (void)addHandler:(id)handler forMethod:(NSString *)name { - _handlers[name] = handler; + @synchronized(self) { + _handlers[name] = handler; + } +} + +- (id)handlerForMethod:(NSString *)name +{ + @synchronized(self) { + return _handlers[name]; + } } static BOOL isSupportedVersion(NSNumber *version) @@ -95,10 +144,6 @@ static BOOL isSupportedVersion(NSNumber *version) - (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message { - if (!_handlers) { - return; - } - NSError *error = nil; NSDictionary *msg = RCTJSONParse(message, &error); @@ -112,7 +157,7 @@ static BOOL isSupportedVersion(NSNumber *version) return; } - id methodHandler = _handlers[msg[@"method"]]; + id methodHandler = [self handlerForMethod:msg[@"method"]]; if (!methodHandler) { if (msg[@"id"]) { NSString *errorMsg = [NSString stringWithFormat:@"%@ no handler found for method %@", [self class], msg[@"method"]]; @@ -123,13 +168,19 @@ static BOOL isSupportedVersion(NSNumber *version) return; // If it was a broadcast then we ignore it gracefully } - if (msg[@"id"]) { - [methodHandler handleRequest:msg[@"params"] - withResponder:[[RCTPackagerClientResponder alloc] initWithId:msg[@"id"] - socket:webSocket]]; - } else { - [methodHandler handleNotification:msg[@"params"]]; - } + dispatch_queue_t methodQueue = [methodHandler respondsToSelector:@selector(methodQueue)] + ? [methodHandler methodQueue] + : dispatch_get_main_queue(); + + dispatch_async(methodQueue, ^{ + if (msg[@"id"]) { + [methodHandler handleRequest:msg[@"params"] + withResponder:[[RCTPackagerClientResponder alloc] initWithId:msg[@"id"] + socket:webSocket]]; + } else { + [methodHandler handleNotification:msg[@"params"]]; + } + }); } - (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket diff --git a/React/DevSupport/RCTPackagerConnectionBridgeConfig.h b/React/DevSupport/RCTPackagerConnectionBridgeConfig.h index 0e8d3728e..afd02d51c 100644 --- a/React/DevSupport/RCTPackagerConnectionBridgeConfig.h +++ b/React/DevSupport/RCTPackagerConnectionBridgeConfig.h @@ -11,6 +11,8 @@ #if RCT_DEV // Only supported in dev mode +NS_ASSUME_NONNULL_BEGIN + @class RCTBridge; @interface RCTPackagerConnectionBridgeConfig : NSObject @@ -19,4 +21,6 @@ @end +NS_ASSUME_NONNULL_END + #endif diff --git a/React/DevSupport/RCTPackagerConnectionBridgeConfig.m b/React/DevSupport/RCTPackagerConnectionBridgeConfig.m index 4c1978187..97d78f00b 100644 --- a/React/DevSupport/RCTPackagerConnectionBridgeConfig.m +++ b/React/DevSupport/RCTPackagerConnectionBridgeConfig.m @@ -9,7 +9,10 @@ #import "RCTPackagerConnectionBridgeConfig.h" +#import + #import +#import #import "RCTJSEnvironment.h" #import "RCTReloadPackagerMethod.h" @@ -18,38 +21,44 @@ #if RCT_DEV // Only supported in dev mode @implementation RCTPackagerConnectionBridgeConfig { - RCTBridge *_bridge; + id _jsEnvironment; + RCTReloadPackagerMethodBlock _reloadCommand; + NSURL *_sourceURL; } - (instancetype)initWithBridge:(RCTBridge *)bridge { if (self = [super init]) { - _bridge = bridge; + _jsEnvironment = bridge; + _sourceURL = [bridge.bundleURL copy]; + __weak RCTBridge *weakBridge = bridge; + _reloadCommand = ^(id params) { + if (params != (id)kCFNull && [params[@"debug"] boolValue]) { + weakBridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor"); + } + [weakBridge reload]; + }; } return self; } - (NSURL *)packagerURL { - NSString *host = [_bridge.bundleURL host]; - NSString *scheme = [_bridge.bundleURL scheme]; - if (!host) { - host = @"localhost"; - scheme = @"http"; - } - - NSNumber *port = [_bridge.bundleURL port]; - if (!port) { - port = @8081; // Packager default port - } - return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=ios-rn-rctdevmenu", scheme, host, port]]; + NSURLComponents *components = [NSURLComponents new]; + NSString *host = [_sourceURL host]; + components.host = host ?: @"localhost"; + components.scheme = host ? [_sourceURL scheme] : @"http"; + components.port = [_sourceURL port] ?: @(kRCTBundleURLProviderDefaultPort); + components.path = @"/message"; + components.queryItems = @[[NSURLQueryItem queryItemWithName:@"role" value:@"ios-rn-rctdevmenu"]]; + return components.URL; } - (NSDictionary> *)defaultPackagerMethods { return @{ - @"reload": [[RCTReloadPackagerMethod alloc] initWithBridge:_bridge], - @"pokeSamplingProfiler": [[RCTSamplingProfilerPackagerMethod alloc] initWithJSEnvironment:_bridge] + @"reload": [[RCTReloadPackagerMethod alloc] initWithReloadCommand:_reloadCommand callbackQueue:dispatch_get_main_queue()], + @"pokeSamplingProfiler": [[RCTSamplingProfilerPackagerMethod alloc] initWithJSEnvironment:_jsEnvironment] }; } diff --git a/React/DevSupport/RCTPackagerConnectionConfig.h b/React/DevSupport/RCTPackagerConnectionConfig.h index da9461dd0..090bede5f 100644 --- a/React/DevSupport/RCTPackagerConnectionConfig.h +++ b/React/DevSupport/RCTPackagerConnectionConfig.h @@ -11,6 +11,8 @@ #if RCT_DEV // Only supported in dev mode +NS_ASSUME_NONNULL_BEGIN + @protocol RCTPackagerClientMethod; @protocol RCTPackagerConnectionConfig @@ -20,4 +22,6 @@ @end +NS_ASSUME_NONNULL_END + #endif diff --git a/React/DevSupport/RCTReloadPackagerMethod.h b/React/DevSupport/RCTReloadPackagerMethod.h index cd157edc6..d19fa6553 100644 --- a/React/DevSupport/RCTReloadPackagerMethod.h +++ b/React/DevSupport/RCTReloadPackagerMethod.h @@ -13,10 +13,16 @@ #if RCT_DEV // Only supported in dev mode +NS_ASSUME_NONNULL_BEGIN + +typedef void (^RCTReloadPackagerMethodBlock)(id); + @interface RCTReloadPackagerMethod : NSObject -- (instancetype)initWithBridge:(RCTBridge *)bridge; +- (instancetype)initWithReloadCommand:(RCTReloadPackagerMethodBlock)block callbackQueue:(dispatch_queue_t)callbackQueue; @end +NS_ASSUME_NONNULL_END + #endif diff --git a/React/DevSupport/RCTReloadPackagerMethod.m b/React/DevSupport/RCTReloadPackagerMethod.m index 1135d0c79..317daec42 100644 --- a/React/DevSupport/RCTReloadPackagerMethod.m +++ b/React/DevSupport/RCTReloadPackagerMethod.m @@ -9,20 +9,20 @@ #import "RCTReloadPackagerMethod.h" -#import - #import "RCTBridge.h" #if RCT_DEV // Only supported in dev mode @implementation RCTReloadPackagerMethod { - __weak RCTBridge *_bridge; + RCTReloadPackagerMethodBlock _block; + dispatch_queue_t _callbackQueue; } -- (instancetype)initWithBridge:(RCTBridge *)bridge +- (instancetype)initWithReloadCommand:(RCTReloadPackagerMethodBlock)block callbackQueue:(dispatch_queue_t)callbackQueue { if (self = [super init]) { - _bridge = bridge; + _block = [block copy]; + _callbackQueue = callbackQueue; } return self; } @@ -34,10 +34,12 @@ - (void)handleNotification:(id)params { - if (![params isEqual:[NSNull null]] && [params[@"debug"] boolValue]) { - _bridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor"); - } - [_bridge reload]; + _block(params); +} + +- (dispatch_queue_t)methodQueue +{ + return _callbackQueue; } @end diff --git a/React/Modules/RCTDevSettings.mm b/React/Modules/RCTDevSettings.mm index 8e8386ff0..964b3dca9 100644 --- a/React/Modules/RCTDevSettings.mm +++ b/React/Modules/RCTDevSettings.mm @@ -36,8 +36,6 @@ static NSString *const kRCTDevSettingStartSamplingProfilerOnLaunch = @"startSamp static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu"; -#define ENABLE_PACKAGER_CONNECTION RCT_DEV && __has_include("RCTPackagerConnection.h") - #if ENABLE_PACKAGER_CONNECTION #import "RCTPackagerConnection.h" #endif