Skip to content
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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Ably.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
8412FDFE2661AC7B001FE9E6 /* Nimble.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8412FDF42661AC7B001FE9E6 /* Nimble.xcframework */; };
8412FDFF2661AC7B001FE9E6 /* Nimble.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8412FDF42661AC7B001FE9E6 /* Nimble.xcframework */; };
8412FE002661AC7B001FE9E6 /* Nimble.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8412FDF42661AC7B001FE9E6 /* Nimble.xcframework */; };
84767F682933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 84767F662933FF7700899C1A /* ARTURLSessionWebSocket.h */; settings = {ATTRIBUTES = (Private, ); }; };
84767F692933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 84767F662933FF7700899C1A /* ARTURLSessionWebSocket.h */; settings = {ATTRIBUTES = (Private, ); }; };
84767F6A2933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 84767F662933FF7700899C1A /* ARTURLSessionWebSocket.h */; settings = {ATTRIBUTES = (Private, ); }; };
84767F6B2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 84767F672933FF7700899C1A /* ARTURLSessionWebSocket.m */; };
84767F6C2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 84767F672933FF7700899C1A /* ARTURLSessionWebSocket.m */; };
84767F6D2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 84767F672933FF7700899C1A /* ARTURLSessionWebSocket.m */; };
848ED97326E50D0F0087E800 /* ObjcppTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 848ED97226E50D0F0087E800 /* ObjcppTest.mm */; settings = {COMPILER_FLAGS = "-fmodules"; }; };
848ED97426E50D0F0087E800 /* ObjcppTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 848ED97226E50D0F0087E800 /* ObjcppTest.mm */; settings = {COMPILER_FLAGS = "-fmodules"; }; };
848ED97526E50D0F0087E800 /* ObjcppTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 848ED97226E50D0F0087E800 /* ObjcppTest.mm */; settings = {COMPILER_FLAGS = "-fmodules"; }; };
Expand Down Expand Up @@ -1023,6 +1029,8 @@
8412FDF12661AC7A001FE9E6 /* Aspects.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Aspects.xcframework; path = Carthage/Build/Aspects.xcframework; sourceTree = "<group>"; };
8412FDF22661AC7B001FE9E6 /* SwiftyJSON.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SwiftyJSON.xcframework; path = Carthage/Build/SwiftyJSON.xcframework; sourceTree = "<group>"; };
8412FDF42661AC7B001FE9E6 /* Nimble.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Nimble.xcframework; path = Carthage/Build/Nimble.xcframework; sourceTree = "<group>"; };
84767F662933FF7700899C1A /* ARTURLSessionWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTURLSessionWebSocket.h; sourceTree = "<group>"; };
84767F672933FF7700899C1A /* ARTURLSessionWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTURLSessionWebSocket.m; sourceTree = "<group>"; };
848ED97226E50D0F0087E800 /* ObjcppTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ObjcppTest.mm; sourceTree = "<group>"; };
850BFB4A1B79323C009D0ADD /* ARTPaginatedResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPaginatedResult.h; sourceTree = "<group>"; };
850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTPaginatedResult.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1695,6 +1703,8 @@
96E408461A3895E800087F77 /* ARTWebSocketTransport.m */,
EB20F8D61C653F1E00EF3978 /* ARTPresence+Private.h */,
EBB721C42376A948001C3550 /* ARTWebSocket.h */,
84767F662933FF7700899C1A /* ARTURLSessionWebSocket.h */,
84767F672933FF7700899C1A /* ARTURLSessionWebSocket.m */,
);
name = Transport;
sourceTree = "<group>";
Expand Down Expand Up @@ -2021,6 +2031,7 @@
D5BB210B26AA98A200AA5F3E /* ARTStringifiable+Private.h in Headers */,
EB4B1A0C1F2190BB00467F07 /* ARTRestChannels+Private.h in Headers */,
D785C4291E549E33008FEC05 /* ARTPushChannelSubscription.h in Headers */,
84767F682933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */,
D7F1D3731BF4DE07001A4B5E /* ARTRestPresence.h in Headers */,
D7B17EE31C07208B00A6958E /* ARTConnectionDetails.h in Headers */,
D7F2B8B21E42410D00B65151 /* ARTPresenceMessage+Private.h in Headers */,
Expand Down Expand Up @@ -2173,6 +2184,7 @@
D710D50421949C18008F54AD /* ARTRealtime+Private.h in Headers */,
D710D58D21949D29008F54AD /* ARTPresenceMap.h in Headers */,
D5BB210E26AA98A800AA5F3E /* ARTStringifiable.h in Headers */,
84767F692933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */,
D710D56B21949CB9008F54AD /* ARTPushDeviceRegistrations.h in Headers */,
D710D62021949DEC008F54AD /* ARTNSMutableURLRequest+ARTPaginated.h in Headers */,
D710D4B021949AF8008F54AD /* ARTAuth.h in Headers */,
Expand Down Expand Up @@ -2317,6 +2329,7 @@
D710D51021949C19008F54AD /* ARTRealtime+Private.h in Headers */,
D710D5B321949D2A008F54AD /* ARTPresenceMap.h in Headers */,
D710D57121949CBA008F54AD /* ARTPushDeviceRegistrations.h in Headers */,
84767F6A2933FF7700899C1A /* ARTURLSessionWebSocket.h in Headers */,
D710D62C21949DED008F54AD /* ARTNSMutableURLRequest+ARTPaginated.h in Headers */,
D710D4B221949AF9008F54AD /* ARTAuth.h in Headers */,
D710D5B121949D2A008F54AD /* ARTPresence.h in Headers */,
Expand Down Expand Up @@ -2728,6 +2741,7 @@
1C55427D1B148306003068DB /* ARTStatus.m in Sources */,
D785C42A1E549E33008FEC05 /* ARTPushChannelSubscription.m in Sources */,
D7B17EE41C07208B00A6958E /* ARTConnectionDetails.m in Sources */,
84767F6B2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */,
96BF61591A35B52C004CF2B3 /* ARTHttp.m in Sources */,
217D1833254222F600DFF07E /* ARTSRPinningSecurityPolicy.m in Sources */,
D3AD0EBE215E2FB000312105 /* ARTNSString+ARTUtil.m in Sources */,
Expand Down Expand Up @@ -2907,6 +2921,7 @@
D710D5D921949D78008F54AD /* ARTProtocolMessage.m in Sources */,
D710D53721949C54008F54AD /* ARTLocalDevice.m in Sources */,
D710D53621949C54008F54AD /* ARTDeviceIdentityTokenDetails.m in Sources */,
84767F6C2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */,
D710D4C821949BAA008F54AD /* ARTRealtimeTransport.m in Sources */,
217D184A254222F700DFF07E /* ARTSRPinningSecurityPolicy.m in Sources */,
D710D49821949ACA008F54AD /* ARTRestChannel.m in Sources */,
Expand Down Expand Up @@ -3018,6 +3033,7 @@
D54C55AC26957FDE00729EC4 /* ARTNSURL+ARTUtils.m in Sources */,
D710D54921949C55008F54AD /* ARTLocalDevice.m in Sources */,
D710D54821949C55008F54AD /* ARTDeviceIdentityTokenDetails.m in Sources */,
84767F6D2933FF7700899C1A /* ARTURLSessionWebSocket.m in Sources */,
D710D4CC21949BAB008F54AD /* ARTRealtimeTransport.m in Sources */,
217D1861254222FA00DFF07E /* ARTSRPinningSecurityPolicy.m in Sources */,
D710D4A221949ACB008F54AD /* ARTRestChannel.m in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions Source/ARTURLSessionWebSocket.h
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
155 changes: 155 additions & 0 deletions Source/ARTURLSessionWebSocket.m
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 {
Copy link
Collaborator

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 the ARTWebSocketDelegate protocol.

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
100 changes: 92 additions & 8 deletions Source/ARTWebSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,116 @@

NS_ASSUME_NONNULL_BEGIN

@class ARTLog;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of my task in reviewing ARTURLSessionWebSocket is to confirm whether it seems to correctly conform to the ARTWebSocket protocol, which I can't currently do since this protocol has no documentation. I think that now would be a good moment to add documentation to this protocol. What do you think?

(Same goes for ARTWebSocketDelegate; to review ARTURLSessionWebSocket I need to verify whether it respects the contract of the delegate protocol, which I can't do since it's not documented.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Expand Down
Loading