-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic NSURLSessionWebSocketTask functionality #1541
base: main
Are you sure you want to change the base?
Changes from all commits
5e915b4
0319753
30d0fc1
95606a5
2d685ef
0a13cd6
4793ebd
565a550
b7b9be9
b05377d
297a0b9
78efcd9
7374511
7aef468
2752fab
703482c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
#import <Foundation/Foundation.h> | ||
#import "ARTWebSocket.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) | ||
@interface ARTURLSessionWebSocket : NSObject<ARTWebSocket> | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
#import "ARTURLSessionWebSocket.h" | ||
#import "ARTRealtimeTransport.h" | ||
#import "ARTLog.h" | ||
|
||
@interface ARTURLSessionWebSocket () <NSURLSessionWebSocketDelegate> | ||
|
||
@property (atomic, assign, readwrite) ARTWebSocketState readyState; | ||
|
||
@end | ||
|
||
@implementation ARTURLSessionWebSocket { | ||
NSURLSession *_urlSession; | ||
NSURLSessionWebSocketTask *_webSocketTask; | ||
ARTLog *_logger; | ||
} | ||
|
||
#pragma mark - ARTWebSocket | ||
|
||
@synthesize delegate = _delegate; | ||
@synthesize readyState = _readyState; | ||
@synthesize delegateDispatchQueue = _delegateDispatchQueue; | ||
|
||
- (instancetype)initWithURLRequest:(NSURLRequest *)request logger:(ARTLog *)logger { | ||
if (self = [super init]) { | ||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; | ||
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; | ||
_webSocketTask = [_urlSession webSocketTaskWithRequest:request]; | ||
_readyState = ARTWebSocketStateClosed; | ||
_logger = logger; | ||
} | ||
return self; | ||
} | ||
|
||
- (dispatch_queue_t)getDelegateDispatchQueue { | ||
return _delegateDispatchQueue ?: dispatch_get_main_queue(); | ||
} | ||
|
||
- (void)listenForMessage { | ||
__weak ARTURLSessionWebSocket *weakSelf = self; | ||
[_webSocketTask receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) { | ||
ARTURLSessionWebSocket *strongSelf = weakSelf; | ||
if (strongSelf) { | ||
dispatch_async([strongSelf getDelegateDispatchQueue], ^{ | ||
/* | ||
* We will ignore `error` object here, relying only on `message` object presence, since: | ||
* 1) there is additional handler for connectivity issues (`URLSession:task:didCompleteWithError:`) and | ||
* 2) the `ARTWebSocketTransport` is not very welcoming for error emerging from here, causing some tests to fail. | ||
*/ | ||
if (error != nil) { | ||
[strongSelf->_logger debug:__FILE__ line:__LINE__ message:@"Receive message error: %@, task state = %@", error, @(strongSelf->_webSocketTask.state)]; | ||
} | ||
if (message != nil) { | ||
switch (message.type) { | ||
case NSURLSessionWebSocketMessageTypeData: | ||
[strongSelf->_delegate webSocket:strongSelf didReceiveMessage:[message data]]; | ||
break; | ||
case NSURLSessionWebSocketMessageTypeString: | ||
[strongSelf->_delegate webSocket:strongSelf didReceiveMessage:[message string]]; | ||
break; | ||
} | ||
} | ||
if (strongSelf.readyState == ARTWebSocketStateOpened) { | ||
[strongSelf listenForMessage]; | ||
} | ||
}); | ||
} | ||
}]; | ||
} | ||
|
||
- (void)open { | ||
_readyState = ARTWebSocketStateConnecting; | ||
[_webSocketTask resume]; | ||
} | ||
|
||
- (void)send:(id)data { | ||
NSURLSessionWebSocketMessage *wsMessage = [data isKindOfClass:[NSString class]] ? | ||
[[NSURLSessionWebSocketMessage alloc] initWithString:data] : | ||
[[NSURLSessionWebSocketMessage alloc] initWithData:data]; | ||
__weak ARTURLSessionWebSocket *weakSelf = self; | ||
[_webSocketTask sendMessage:wsMessage completionHandler:^(NSError *error) { | ||
ARTURLSessionWebSocket *strongSelf = weakSelf; | ||
if (strongSelf) { | ||
dispatch_async([strongSelf getDelegateDispatchQueue], ^{ | ||
if (error != nil) { | ||
[strongSelf->_delegate webSocket:strongSelf didFailWithError:error]; | ||
} | ||
}); | ||
} | ||
}]; | ||
} | ||
|
||
- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason { | ||
if (self.readyState != ARTWebSocketStateClosed) { | ||
self.readyState = ARTWebSocketStateClosing; | ||
} | ||
[_webSocketTask cancelWithCloseCode:code reason:[reason dataUsingEncoding:NSUTF8StringEncoding]]; | ||
} | ||
|
||
- (ARTRealtimeTransportError *)classifyError:(NSError *)error { | ||
ARTRealtimeTransportErrorType type = ARTRealtimeTransportErrorTypeOther; | ||
|
||
if ([error.domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork]) { | ||
type = ARTRealtimeTransportErrorTypeHostUnreachable; | ||
} | ||
else if ([error.domain isEqualToString:@"NSPOSIXErrorDomain"] && (error.code == 57 || error.code == 50)) { | ||
type = ARTRealtimeTransportErrorTypeNoInternet; | ||
} | ||
else if ([error.domain isEqualToString:@"NSURLErrorDomain"] && (error.code == NSURLErrorNotConnectedToInternet)) { | ||
type = ARTRealtimeTransportErrorTypeNoInternet; | ||
} | ||
else if ([error.domain isEqualToString:@"NSURLErrorDomain"] && (error.code == NSURLErrorTimedOut)) { | ||
type = ARTRealtimeTransportErrorTypeTimeout; | ||
} | ||
else if ([error.domain isEqualToString:@"NSURLErrorDomain"] && (error.code == NSURLErrorBadServerResponse)) { | ||
type = ARTRealtimeTransportErrorTypeBadResponse; | ||
} | ||
return [[ARTRealtimeTransportError alloc] initWithError:error type:type url:_webSocketTask.originalRequest.URL]; | ||
} | ||
|
||
#pragma mark - NSURLSessionWebSocketDelegate | ||
|
||
- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didOpenWithProtocol:(NSString *)protocol { | ||
self.readyState = ARTWebSocketStateOpened; | ||
[self listenForMessage]; | ||
dispatch_async([self getDelegateDispatchQueue], ^{ | ||
[self->_delegate webSocketDidOpen:self]; | ||
}); | ||
} | ||
|
||
- (void)URLSession:(NSURLSession *)session | ||
webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask | ||
didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode reason:(NSData *)reasonData { | ||
if (self.readyState == ARTWebSocketStateClosing || self.readyState == ARTWebSocketStateOpened) { | ||
self.readyState = ARTWebSocketStateClosed; | ||
} | ||
dispatch_async([self getDelegateDispatchQueue], ^{ | ||
NSString *reason = [[NSString alloc] initWithData:reasonData encoding:NSUTF8StringEncoding]; | ||
[self->_delegate webSocket:self didCloseWithCode:closeCode reason:reason wasClean:YES]; | ||
}); | ||
} | ||
|
||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { | ||
self.readyState = ARTWebSocketStateClosed; | ||
if (error != nil) { | ||
[_logger debug:__FILE__ line:__LINE__ message:@"Session completion error: %@, task state = %@", error, @(_webSocketTask.state)]; | ||
dispatch_async([self getDelegateDispatchQueue], ^{ | ||
[self->_delegate webSocket:self didFailWithError:error]; | ||
}); | ||
} | ||
else { | ||
[_logger debug:__FILE__ line:__LINE__ message:@"Session completion task state = %@", @(_webSocketTask.state)]; | ||
} | ||
} | ||
|
||
@end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,32 +5,116 @@ | |
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@class ARTLog; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
@class ARTRealtimeTransportError; | ||
@protocol ARTWebSocketDelegate; | ||
|
||
typedef NS_ENUM(NSInteger, ARTWebSocketState) { | ||
ARTWebSocketStateConnecting = 0, | ||
ARTWebSocketStateOpened = 1, | ||
ARTWebSocketStateClosing = 2, | ||
ARTWebSocketStateClosed = 3, | ||
}; | ||
|
||
/** | ||
This protocol has the subset of ARTSRWebSocket we actually use. | ||
* Describes an object that lets you connect, send and receive data to a remote web socket. | ||
*/ | ||
@protocol ARTWebSocket <NSObject> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of my task in reviewing (Same goes for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
@property (nonatomic, weak) id <ARTWebSocketDelegate> _Nullable delegate; | ||
/** | ||
* The delegate of the web socket. | ||
* | ||
* The web socket delegate is notified on all state changes that happen to the web socket. | ||
*/ | ||
@property (nullable, nonatomic, weak) id<ARTWebSocketDelegate> delegate; | ||
|
||
/** | ||
* A dispatch queue for scheduling the delegate calls. | ||
* | ||
* If `nil`, the socket uses main queue for performing all delegate method calls. | ||
*/ | ||
@property (nullable, nonatomic, strong) dispatch_queue_t delegateDispatchQueue; | ||
@property (atomic, assign, readonly) ARTSRReadyState readyState; | ||
|
||
- (instancetype)initWithURLRequest:(NSURLRequest *)request logger:(nullable ARTLog *)logger; | ||
/** | ||
* Current ready state of the socket. | ||
* | ||
* This property is thread-safe. | ||
*/ | ||
@property (atomic, assign, readonly) ARTWebSocketState readyState; | ||
|
||
- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; | ||
/** | ||
* Initializes a web socket with a given `NSURLRequest`. | ||
* | ||
* @param request A request to initialize with. | ||
* @param logger An `ARTLog` object to initialize with. | ||
*/ | ||
- (instancetype)initWithURLRequest:(NSURLRequest *)request logger:(nullable ARTLog *)logger; | ||
|
||
/** | ||
* Opens web socket, which will trigger connection, authentication and start receiving/sending events. | ||
*/ | ||
- (void)open; | ||
|
||
/** | ||
* Closes a web socket using a given code and reason. | ||
* | ||
* @param code Code to close the socket with. | ||
* @param reason Reason to send to the server or `nil`. | ||
*/ | ||
- (void)closeWithCode:(NSInteger)code reason:(nullable NSString *)reason; | ||
- (void)send:(nullable id)message; | ||
|
||
/** | ||
* Sends a UTF-8 string or a binary data to the server. | ||
* | ||
* @param message UTF-8 `NSString` or `NSData` to send. | ||
*/ | ||
- (void)send:(id)message; | ||
|
||
/** | ||
* Classifies error for reconnection attemts. | ||
* | ||
* @param error An error object to classify for reconnection. | ||
*/ | ||
- (ARTRealtimeTransportError *)classifyError:(NSError *)error; | ||
|
||
@end | ||
|
||
/** | ||
* Describes methods that `ARTWebSocket` objects call on their delegates to handle status and messsage events. | ||
*/ | ||
@protocol ARTWebSocketDelegate <NSObject> | ||
|
||
- (void)webSocketDidOpen:(id<ARTWebSocket>)websocket; | ||
- (void)webSocket:(id<ARTWebSocket>)webSocket didCloseWithCode:(NSInteger)code reason:(NSString * _Nullable)reason wasClean:(BOOL)wasClean; | ||
/** | ||
* Called when a given web socket was open and authenticated. | ||
* | ||
* @param webSocket An instance of an `ARTWebSocket` conforming object that was open. | ||
*/ | ||
- (void)webSocketDidOpen:(id<ARTWebSocket>)webSocket; | ||
|
||
/** | ||
* Called when a given web socket was closed. | ||
* | ||
* @param webSocket An instance of an `ARTWebSocket` conforming object that was closed. | ||
* @param code Code reported by the server. | ||
* @param reason Reason in a form of a string that was reported by the server or `nil`. | ||
* @param wasClean Boolean value indicating whether a socket was closed in a clean state. | ||
*/ | ||
- (void)webSocket:(id<ARTWebSocket>)webSocket didCloseWithCode:(NSInteger)code reason:(nullable NSString *)reason wasClean:(BOOL)wasClean; | ||
|
||
/** | ||
* Called when a given web socket encountered an error. | ||
* | ||
* @param webSocket An instance of an `ARTWebSocket` conforming object that failed with an error. | ||
* @param error An instance of `NSError`. | ||
*/ | ||
- (void)webSocket:(id<ARTWebSocket>)webSocket didFailWithError:(NSError *)error; | ||
|
||
/** | ||
* Called when any message was received from a web socket. | ||
* | ||
* @param webSocket An instance of an `ARTWebSocket` conforming object that received a message. | ||
* @param message Received message. Either a `NSString` or `NSData`. | ||
*/ | ||
- (void)webSocket:(id<ARTWebSocket>)webSocket didReceiveMessage:(id)message; | ||
|
||
@end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given how fundamental this class is to the SDK, I think it's important that we unit test it, to confirm that it conforms to the
ARTWebSocket
protocol and that it respects the contract of theARTWebSocketDelegate
protocol.