Files
react-native/React/CoreModules/RCTWebSocketExecutor.mm
T
Adam Gleitman 9ee0e1c78e Use SocketRocket for web socket library (#36471)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/36471

The previous native web socket API, `RCTSRWebSocket`, appears to be an older version of the one provided as part of [SocketRocket](https://github.com/facebookincubator/SocketRocket). The latter has several improvements, such as the ability to respect proxy settings, which has been requested by a user of a React Native app.

Everything translates over pretty easily, and considering that SocketRocket is already a dependency of Flipper, there doesn't seem to be much additional cost to swapping out the libraries. If we wanted to make things even slimmer, it may even be possible to make the WebSocket library be optional for release builds.

## Changelog

[IOS] [CHANGED] - Use SocketRocket for web socket library

Pull Request resolved: https://github.com/facebook/react-native/pull/36347

Test Plan:
Validated the following:
* The WebSocket test page in RNTester
* Live reloading

Reviewed By: cortinico

Differential Revision: D43768835

Pulled By: javache

fbshipit-source-id: 11e1ac2700bc92991897c594622e6687339bfcbf
2023-03-15 13:32:59 -07:00

299 lines
9.2 KiB
Plaintext

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTWebSocketExecutor.h>
#import <React/RCTAssert.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#import <SocketRocket/SRWebSocket.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV // Debug executors are only supported in dev mode
typedef void (^RCTWSMessageCallback)(NSError *error, NSDictionary<NSString *, id> *reply);
@interface RCTWebSocketExecutor () <SRWebSocketDelegate, RCTTurboModule>
@end
@implementation RCTWebSocketExecutor {
SRWebSocket *_socket;
dispatch_queue_t _jsQueue;
NSMutableDictionary<NSNumber *, RCTWSMessageCallback> *_callbacks;
dispatch_semaphore_t _socketOpenSemaphore;
NSMutableDictionary<NSString *, NSString *> *_injectedObjects;
NSURL *_url;
NSError *_setupError;
}
RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
- (instancetype)initWithURL:(NSURL *)URL
{
RCTAssertParam(URL);
if ((self = [self init])) {
_url = URL;
}
return self;
}
- (void)setUp
{
if (!_url) {
NSInteger port = [[[_bridge bundleURL] port] integerValue] ?: RCT_METRO_PORT;
NSString *host = [[_bridge bundleURL] host] ?: @"localhost";
NSString *URLString =
[NSString stringWithFormat:@"http://%@:%lld/debugger-proxy?role=client", host, (long long)port];
_url = [RCTConvert NSURL:URLString];
}
_jsQueue = dispatch_queue_create("com.facebook.react.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
_socket = [[SRWebSocket alloc] initWithURL:_url];
_socket.delegate = self;
_callbacks = [NSMutableDictionary new];
_injectedObjects = [NSMutableDictionary new];
[_socket setDelegateDispatchQueue:_jsQueue];
NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-js-devtools" relativeToURL:_url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask =
[session dataTaskWithRequest:[NSURLRequest requestWithURL:startDevToolsURL]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
}];
[dataTask resume];
if (![self connectToProxy]) {
[self invalidate];
NSString *error = [NSString stringWithFormat:
@"Connection to %@ timed out. Are you "
"running node proxy? If you are running on the device, check if "
"you have the right IP address in `RCTWebSocketExecutor.m`.",
_url];
_setupError = RCTErrorWithMessage(error);
RCTFatal(_setupError);
return;
}
NSInteger retries = 3;
BOOL runtimeIsReady = [self prepareJSRuntime];
while (!runtimeIsReady && retries > 0) {
runtimeIsReady = [self prepareJSRuntime];
retries--;
}
if (!runtimeIsReady) {
[self invalidate];
NSString *error =
@"Runtime is not ready for debugging.\n "
"- Make sure Metro is running.\n"
"- Make sure the JavaScript Debugger is running and not paused on a "
"breakpoint or exception and try reloading again.";
_setupError = RCTErrorWithMessage(error);
RCTFatal(_setupError);
return;
}
}
- (BOOL)connectToProxy
{
_socketOpenSemaphore = dispatch_semaphore_create(0);
[_socket open];
long connected = dispatch_semaphore_wait(_socketOpenSemaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 15));
return connected == 0 && _socket.readyState == SR_OPEN;
}
- (BOOL)prepareJSRuntime
{
__block NSError *initError;
dispatch_semaphore_t s = dispatch_semaphore_create(0);
[self sendMessage:@{@"method" : @"prepareJSRuntime"}
onReply:^(NSError *error, NSDictionary<NSString *, id> *reply) {
initError = error;
dispatch_semaphore_signal(s);
}];
long runtimeIsReady = dispatch_semaphore_wait(s, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 10));
if (initError) {
RCTLogInfo(@"Websocket runtime setup failed: %@", initError);
}
return runtimeIsReady == 0 && initError == nil;
}
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSError *error = nil;
NSDictionary<NSString *, id> *reply = RCTJSONParse(message, &error);
NSNumber *messageID = reply[@"replyID"];
RCTWSMessageCallback callback = _callbacks[messageID];
if (callback) {
callback(error, reply);
[_callbacks removeObjectForKey:messageID];
}
}
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
dispatch_semaphore_signal(_socketOpenSemaphore);
}
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
dispatch_semaphore_signal(_socketOpenSemaphore);
RCTLogInfo(@"WebSocket connection failed with error %@", error);
}
- (void)sendMessage:(NSDictionary<NSString *, id> *)message onReply:(RCTWSMessageCallback)callback
{
static NSUInteger lastID = 10000;
if (_setupError) {
callback(_setupError, nil);
return;
}
dispatch_async(_jsQueue, ^{
if (!self.valid) {
callback(RCTErrorWithMessage(@"Runtime is not ready for debugging. Make sure Metro is running."), nil);
return;
}
NSNumber *expectedID = @(lastID++);
self->_callbacks[expectedID] = [callback copy];
NSMutableDictionary<NSString *, id> *messageWithID = [message mutableCopy];
messageWithID[@"id"] = expectedID;
[self->_socket sendString:RCTJSONStringify(messageWithID, NULL) error:nil];
});
}
- (void)executeApplicationScript:(NSData *)script
sourceURL:(NSURL *)URL
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
// Hack: the bridge transitions out of loading state as soon as this method returns, which prevents us
// from completely invalidating the bridge and preventing an endless barage of RCTLog.logIfNoNativeHook
// calls if the JS execution environment is broken. We therefore block this thread until this message has returned.
dispatch_semaphore_t scriptSem = dispatch_semaphore_create(0);
NSDictionary<NSString *, id> *message = @{
@"method" : @"executeApplicationScript",
@"url" : RCTNullIfNil(URL.absoluteString),
@"inject" : _injectedObjects,
};
[self sendMessage:message
onReply:^(NSError *socketError, NSDictionary<NSString *, id> *reply) {
if (socketError) {
onComplete(socketError);
} else {
NSString *error = reply[@"error"];
onComplete(error ? RCTErrorWithMessage(error) : nil);
}
dispatch_semaphore_signal(scriptSem);
}];
dispatch_semaphore_wait(scriptSem, DISPATCH_TIME_FOREVER);
}
- (void)flushedQueue:(RCTJavaScriptCallback)onComplete
{
[self _executeJSCall:@"flushedQueue" arguments:@[] callback:onComplete];
}
- (void)callFunctionOnModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args
callback:(RCTJavaScriptCallback)onComplete
{
[self _executeJSCall:@"callFunctionReturnFlushedQueue" arguments:@[ module, method, args ] callback:onComplete];
}
- (void)invokeCallbackID:(NSNumber *)cbID arguments:(NSArray *)args callback:(RCTJavaScriptCallback)onComplete
{
[self _executeJSCall:@"invokeCallbackAndReturnFlushedQueue" arguments:@[ cbID, args ] callback:onComplete];
}
- (void)_executeJSCall:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete
{
RCTAssert(onComplete != nil, @"callback was missing for exec JS call");
NSDictionary<NSString *, id> *message = @{@"method" : method, @"arguments" : arguments};
[self sendMessage:message
onReply:^(NSError *socketError, NSDictionary<NSString *, id> *reply) {
if (socketError) {
onComplete(nil, socketError);
return;
}
NSError *jsonError;
id result = RCTJSONParse(reply[@"result"], &jsonError);
NSString *error = reply[@"error"];
onComplete(result, error ? RCTErrorWithMessage(error) : jsonError);
}];
}
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete
{
dispatch_async(_jsQueue, ^{
self->_injectedObjects[objectName] = script;
onComplete(nil);
});
}
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
RCTExecuteOnMainQueue(block);
}
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block
{
dispatch_async(dispatch_get_main_queue(), block);
}
- (void)invalidate
{
_socket.delegate = nil;
[_socket closeWithCode:1000 reason:@"Invalidated"];
_socket = nil;
}
- (BOOL)isValid
{
return _socket != nil && _socket.readyState == SR_OPEN;
}
- (void)dealloc
{
RCTAssert(!self.valid, @"-invalidate must be called before -dealloc");
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
@end
#endif
Class RCTWebSocketExecutorCls(void)
{
#if RCT_DEV
return RCTWebSocketExecutor.class;
#else
return nil;
#endif
}