Make RCTWebSocketModule TurboModule-compatible

Summary:
Changelog:
[iOS][Added] - Make RCTWebSocketModule TurboModule-compatible

Reviewed By: PeteTheHeat

Differential Revision: D18353766

fbshipit-source-id: fde0f6593dd203ab3dcb8f9cf40012ba4761d6ba
This commit is contained in:
Ramanpreet Nara
2019-11-08 14:14:46 -08:00
committed by Facebook Github Bot
parent 1b2992e8b3
commit d73ae1baa3
11 changed files with 68 additions and 60 deletions
@@ -2613,12 +2613,6 @@ namespace facebook {
} // namespace react
} // namespace facebook
@implementation RCTCxxConvert (NativeWebSocketModule_SpecConnectOptionsHeaders)
+ (RCTManagedPointer *)JS_NativeWebSocketModule_SpecConnectOptionsHeaders:(id)json
{
return facebook::react::managedPointer<JS::NativeWebSocketModule::SpecConnectOptionsHeaders>(json);
}
@end
@implementation RCTCxxConvert (NativeWebSocketModule_SpecConnectOptions)
+ (RCTManagedPointer *)JS_NativeWebSocketModule_SpecConnectOptions:(id)json
{
@@ -2634,11 +2628,11 @@ namespace facebook {
}
static facebook::jsi::Value __hostFunction_NativeWebSocketModuleSpecJSI_send(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "send", @selector(send:socketID:), args, count);
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "send", @selector(send:forSocketID:), args, count);
}
static facebook::jsi::Value __hostFunction_NativeWebSocketModuleSpecJSI_sendBinary(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "sendBinary", @selector(sendBinary:socketID:), args, count);
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "sendBinary", @selector(sendBinary:forSocketID:), args, count);
}
static facebook::jsi::Value __hostFunction_NativeWebSocketModuleSpecJSI_ping(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
@@ -2697,26 +2697,10 @@ namespace facebook {
} // namespace react
} // namespace facebook
namespace JS {
namespace NativeWebSocketModule {
struct SpecConnectOptionsHeaders {
NSString *origin() const;
SpecConnectOptionsHeaders(NSDictionary *const v) : _v(v) {}
private:
NSDictionary *_v;
};
}
}
@interface RCTCxxConvert (NativeWebSocketModule_SpecConnectOptionsHeaders)
+ (RCTManagedPointer *)JS_NativeWebSocketModule_SpecConnectOptionsHeaders:(id)json;
@end
namespace JS {
namespace NativeWebSocketModule {
struct SpecConnectOptions {
folly::Optional<JS::NativeWebSocketModule::SpecConnectOptionsHeaders> headers() const;
id<NSObject> _Nullable headers() const;
SpecConnectOptions(NSDictionary *const v) : _v(v) {}
private:
@@ -2735,9 +2719,9 @@ namespace JS {
options:(JS::NativeWebSocketModule::SpecConnectOptions &)options
socketID:(double)socketID;
- (void)send:(NSString *)message
socketID:(double)socketID;
forSocketID:(double)forSocketID;
- (void)sendBinary:(NSString *)base64String
socketID:(double)socketID;
forSocketID:(double)forSocketID;
- (void)ping:(double)socketID;
- (void)close:(double)code
reason:(NSString *)reason
@@ -3738,13 +3722,8 @@ inline JS::NativeUIManager::Constants::Builder::Builder(const Input i) : _factor
inline JS::NativeUIManager::Constants::Builder::Builder(Constants i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline NSString *JS::NativeWebSocketModule::SpecConnectOptionsHeaders::origin() const
{
id const p = _v[@"origin"];
return RCTBridgingToString(p);
}
inline folly::Optional<JS::NativeWebSocketModule::SpecConnectOptionsHeaders> JS::NativeWebSocketModule::SpecConnectOptions::headers() const
inline id<NSObject> _Nullable JS::NativeWebSocketModule::SpecConnectOptions::headers() const
{
id const p = _v[@"headers"];
return (p == nil ? folly::none : folly::make_optional(JS::NativeWebSocketModule::SpecConnectOptionsHeaders(p)));
return p;
}
+4 -4
View File
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @flow
* @format
*/
@@ -17,11 +17,11 @@ export interface Spec extends TurboModule {
+connect: (
url: string,
protocols: ?Array<string>,
options: ?{headers?: {origin?: string}},
options: {|headers?: Object|},
socketID: number,
) => void;
+send: (message: string, socketID: number) => void;
+sendBinary: (base64String: string, socketID: number) => void;
+send: (message: string, forSocketID: number) => void;
+sendBinary: (base64String: string, forSocketID: number) => void;
+ping: (socketID: number) => void;
+close: (code: number, reason: string, socketID: number) => void;
@@ -1,19 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its 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/RCTDefines.h>
#import <React/RCTJavaScriptExecutor.h>
#if RCT_DEV // Debug executors are only supported in dev mode
@interface RCTWebSocketExecutor : NSObject <RCTJavaScriptExecutor>
- (instancetype)initWithURL:(NSURL *)URL;
@end
#endif
-273
View File
@@ -1,273 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its 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 <React/RCTSRWebSocket.h>
#if RCT_DEV // Debug executors are only supported in dev mode
typedef void (^RCTWSMessageCallback)(NSError *error, NSDictionary<NSString *, id> *reply);
@interface RCTWebSocketExecutor () <RCTSRWebSocketDelegate>
@end
@implementation RCTWebSocketExecutor
{
RCTSRWebSocket *_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 = [[RCTSRWebSocket 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 Packager server 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 == RCTSR_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:(RCTSRWebSocket *)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:(RCTSRWebSocket *)webSocket
{
dispatch_semaphore_signal(_socketOpenSemaphore);
}
- (void)webSocket:(RCTSRWebSocket *)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 Packager server is running."), nil);
return;
}
NSNumber *expectedID = @(lastID++);
self->_callbacks[expectedID] = [callback copy];
NSMutableDictionary<NSString *, id> *messageWithID = [message mutableCopy];
messageWithID[@"id"] = expectedID;
[self->_socket send:RCTJSONStringify(messageWithID, NULL)];
});
}
- (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 == RCTSR_OPEN;
}
- (void)dealloc
{
RCTAssert(!self.valid, @"-invalidate must be called before -dealloc");
}
@end
#endif
-35
View File
@@ -1,35 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its 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/RCTEventEmitter.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTWebSocketContentHandler <NSObject>
- (id)processWebsocketMessage:(id __nullable)message
forSocketID:(NSNumber *)socketID
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
@end
@interface RCTWebSocketModule : RCTEventEmitter
// Register a custom handler for a specific websocket. The handler will be strongly held by the WebSocketModule.
- (void)setContentHandler:(id<RCTWebSocketContentHandler> __nullable)handler forSocketID:(NSNumber *)socketID;
- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID;
@end
@interface RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule;
@end
NS_ASSUME_NONNULL_END
-201
View File
@@ -1,201 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its 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/RCTWebSocketModule.h>
#import <objc/runtime.h>
#import <React/RCTConvert.h>
#import <React/RCTUtils.h>
#import <React/RCTSRWebSocket.h>
@implementation RCTSRWebSocket (React)
- (NSNumber *)reactTag
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactTag:(NSNumber *)reactTag
{
objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
@interface RCTWebSocketModule () <RCTSRWebSocketDelegate>
@end
@implementation RCTWebSocketModule
{
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
NSMutableDictionary<NSNumber *, id<RCTWebSocketContentHandler>> *_contentHandlers;
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (NSArray *)supportedEvents
{
return @[@"websocketMessage",
@"websocketOpen",
@"websocketFailed",
@"websocketClosed"];
}
- (void)invalidate
{
_contentHandlers = nil;
for (RCTSRWebSocket *socket in _sockets.allValues) {
socket.delegate = nil;
[socket close];
}
}
RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols options:(NSDictionary *)options socketID:(nonnull NSNumber *)socketID)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
// We load cookies from sharedHTTPCookieStorage (shared with XHR and
// fetch). To get secure cookies for wss URLs, replace wss with https
// in the URL.
NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:true];
if ([components.scheme.lowercaseString isEqualToString:@"wss"]) {
components.scheme = @"https";
}
// Load and set the cookie header.
NSArray<NSHTTPCookie *> *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL];
request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
// Load supplied headers
[options[@"headers"] enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
[request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key];
}];
RCTSRWebSocket *webSocket = [[RCTSRWebSocket alloc] initWithURLRequest:request protocols:protocols];
[webSocket setDelegateDispatchQueue:[self methodQueue]];
webSocket.delegate = self;
webSocket.reactTag = socketID;
if (!_sockets) {
_sockets = [NSMutableDictionary new];
}
_sockets[socketID] = webSocket;
[webSocket open];
}
RCT_EXPORT_METHOD(send:(NSString *)message forSocketID:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] send:message];
}
RCT_EXPORT_METHOD(sendBinary:(NSString *)base64String forSocketID:(nonnull NSNumber *)socketID)
{
[self sendData:[[NSData alloc] initWithBase64EncodedString:base64String options:0] forSocketID:socketID];
}
- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID
{
[_sockets[socketID] send:data];
}
RCT_EXPORT_METHOD(ping:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] sendPing:NULL];
}
RCT_EXPORT_METHOD(close:(NSInteger)code reason:(NSString *)reason socketID:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] closeWithCode:code reason:reason];
[_sockets removeObjectForKey:socketID];
}
- (void)setContentHandler:(id<RCTWebSocketContentHandler>)handler forSocketID:(NSString *)socketID
{
if (!_contentHandlers) {
_contentHandlers = [NSMutableDictionary new];
}
_contentHandlers[socketID] = handler;
}
#pragma mark - RCTSRWebSocketDelegate methods
- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSString *type;
NSNumber *socketID = [webSocket reactTag];
id contentHandler = _contentHandlers[socketID];
if (contentHandler) {
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
} else {
if ([message isKindOfClass:[NSData class]]) {
type = @"binary";
message = [message base64EncodedStringWithOptions:0];
} else {
type = @"text";
}
}
[self sendEventWithName:@"websocketMessage" body:@{
@"data": message,
@"type": type,
@"id": webSocket.reactTag
}];
}
- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket
{
[self sendEventWithName:@"websocketOpen" body:@{
@"id": webSocket.reactTag,
@"protocol": webSocket.protocol ? webSocket.protocol : @""
}];
}
- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
[self sendEventWithName:@"websocketFailed" body:@{
@"message": error.localizedDescription,
@"id": socketID
}];
}
- (void)webSocket:(RCTSRWebSocket *)webSocket
didCloseWithCode:(NSInteger)code
reason:(NSString *)reason
wasClean:(BOOL)wasClean
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
[self sendEventWithName:@"websocketClosed" body:@{
@"code": @(code),
@"reason": RCTNullIfNil(reason),
@"clean": @(wasClean),
@"id": socketID
}];
}
@end
@implementation RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule
{
return [self moduleForClass:[RCTWebSocketModule class]];
}
@end