diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 0b6981ca9..a50a6fb64 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -132,15 +132,6 @@ 21276CC229F00BAA00107B5F /* ContinuousClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21276CC129F00BAA00107B5F /* ContinuousClockTests.swift */; }; 21276CC329F00BAA00107B5F /* ContinuousClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21276CC129F00BAA00107B5F /* ContinuousClockTests.swift */; }; 21276CC429F00BAA00107B5F /* ContinuousClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21276CC129F00BAA00107B5F /* ContinuousClockTests.swift */; }; - 2132C20E29D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C20D29D20EEC000C4355 /* ARTResumeRequestResponse.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 2132C20F29D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C20D29D20EEC000C4355 /* ARTResumeRequestResponse.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 2132C21029D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C20D29D20EEC000C4355 /* ARTResumeRequestResponse.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 2132C21229D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21129D20F05000C4355 /* ARTResumeRequestResponse.m */; }; - 2132C21329D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21129D20F05000C4355 /* ARTResumeRequestResponse.m */; }; - 2132C21429D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21129D20F05000C4355 /* ARTResumeRequestResponse.m */; }; - 2132C21629D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21529D20F69000C4355 /* ResumeRequestResponseTests.swift */; }; - 2132C21729D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21529D20F69000C4355 /* ResumeRequestResponseTests.swift */; }; - 2132C21829D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C21529D20F69000C4355 /* ResumeRequestResponseTests.swift */; }; 2132C21A29D230CE000C4355 /* ARTErrorChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C21929D230AE000C4355 /* ARTErrorChecker.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2132C21B29D230CF000C4355 /* ARTErrorChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C21929D230AE000C4355 /* ARTErrorChecker.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2132C21C29D230D0000C4355 /* ARTErrorChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 2132C21929D230AE000C4355 /* ARTErrorChecker.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -1169,9 +1160,6 @@ 21276CB929EF323100107B5F /* ARTContinuousClock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTContinuousClock.h; path = PrivateHeaders/Ably/ARTContinuousClock.h; sourceTree = ""; }; 21276CBD29EF34AE00107B5F /* ARTContinuousClock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTContinuousClock.m; sourceTree = ""; }; 21276CC129F00BAA00107B5F /* ContinuousClockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinuousClockTests.swift; sourceTree = ""; }; - 2132C20D29D20EEC000C4355 /* ARTResumeRequestResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTResumeRequestResponse.h; path = PrivateHeaders/Ably/ARTResumeRequestResponse.h; sourceTree = ""; }; - 2132C21129D20F05000C4355 /* ARTResumeRequestResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTResumeRequestResponse.m; sourceTree = ""; }; - 2132C21529D20F69000C4355 /* ResumeRequestResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeRequestResponseTests.swift; sourceTree = ""; }; 2132C21929D230AE000C4355 /* ARTErrorChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTErrorChecker.h; path = PrivateHeaders/Ably/ARTErrorChecker.h; sourceTree = ""; }; 2132C21D29D23196000C4355 /* ARTErrorChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTErrorChecker.m; sourceTree = ""; }; 2132C22129D233EB000C4355 /* DefaultErrorCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultErrorCheckerTests.swift; sourceTree = ""; }; @@ -1729,7 +1717,6 @@ D72768201C9C19040022F8B2 /* RestClientPresenceTests.swift */, 853ED7C31B7A1A3C006F1C6F /* RestClientStatsTests.swift */, D74CBC09212EC02C00D090E4 /* RestPaginatedTests.swift */, - 2132C21529D20F69000C4355 /* ResumeRequestResponseTests.swift */, 217FCF3129D62460006E5F2D /* RetrySequenceTests.swift */, 851674EE1B7BA5CD00D35169 /* StatsTests.swift */, D520C4DD2680A1E3000012B2 /* StringifiableTests.swift */, @@ -1959,8 +1946,6 @@ EB89D4081C61C5ED007FA5B7 /* ARTRealtimeChannels.h */, D7CEF12C1C8D821D004FB242 /* ARTRealtimeChannels+Private.h */, EB89D40A1C61C6EA007FA5B7 /* ARTRealtimeChannels.m */, - 2132C20D29D20EEC000C4355 /* ARTResumeRequestResponse.h */, - 2132C21129D20F05000C4355 /* ARTResumeRequestResponse.m */, 2104EFAB2A4CC33300CC1184 /* ARTAttachRetryState.h */, 2104EFA72A4CC30C00CC1184 /* ARTAttachRetryState.m */, 21088DC22A5354F10033C722 /* ARTConnectRetryState.h */, @@ -2246,7 +2231,6 @@ D7534C321D79E5C20054C182 /* Ably.h in Headers */, D777EEE0206285CF002EBA03 /* ARTDeviceIdentityTokenDetails.h in Headers */, 2124B79F29DB14D000AD8361 /* ARTLogAdapter.h in Headers */, - 2132C20E29D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */, 215F76032922C76C009E0E76 /* ARTClientInformation+Private.h in Headers */, 211A610329DA05C700D169C5 /* ARTAttachRequestMetadata.h in Headers */, 96BF615E1A35C1C8004CF2B3 /* ARTTypes.h in Headers */, @@ -2552,7 +2536,6 @@ D5BB211026AA993C00AA5F3E /* ARTNSURL+ARTUtils.h in Headers */, D710D60E21949DDB008F54AD /* ARTPaginatedResult.h in Headers */, D710D4BF21949B6C008F54AD /* ARTNSMutableRequest+ARTRest.h in Headers */, - 2132C20F29D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */, 2147F02E29E583AD0071CB94 /* ARTInternalLogCore.h in Headers */, D710D5BE21949D4F008F54AD /* ARTBaseMessage+Private.h in Headers */, D5BB20FE26A7F50000AA5F3E /* ARTSRPinningSecurityPolicy.h in Headers */, @@ -2721,7 +2704,6 @@ D710D61821949DDC008F54AD /* ARTPaginatedResult.h in Headers */, D5BB213B26AAA60500AA5F3E /* ARTNSError+ARTUtils.h in Headers */, D710D4C121949B6D008F54AD /* ARTNSMutableRequest+ARTRest.h in Headers */, - 2132C21029D20EEC000C4355 /* ARTResumeRequestResponse.h in Headers */, 2147F02F29E583AD0071CB94 /* ARTInternalLogCore.h in Headers */, D710D5CE21949D50008F54AD /* ARTBaseMessage+Private.h in Headers */, D5BB20FF26A7F50800AA5F3E /* ARTSRPinningSecurityPolicy.h in Headers */, @@ -3074,7 +3056,6 @@ D510E4AB29F1659F00F77F43 /* Aspects.m in Sources */, D74A17B81FA0D9A3006D27B5 /* PushAdminTests.swift in Sources */, 2110CC3A2A530D42007310D4 /* AttachRetryStateTests.swift in Sources */, - 2132C21629D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */, EB7913A81C6E54C3000ABF9B /* CryptoTests.swift in Sources */, 2132C22229D233EB000C4355 /* DefaultErrorCheckerTests.swift in Sources */, 21FD9F272A015BE400216482 /* Test.swift in Sources */, @@ -3170,7 +3151,6 @@ D7D29B421BE3DEB300374295 /* ARTConnection.m in Sources */, D74CBC08212EB5B900D090E4 /* ARTNSMutableURLRequest+ARTPaginated.m in Sources */, 96BF61711A35FB7C004CF2B3 /* ARTAuth.m in Sources */, - 2132C21229D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */, 96E408441A38939E00087F77 /* ARTProtocolMessage.m in Sources */, D71966E51E5DF360000974DD /* ARTPushActivationStateMachine.m in Sources */, EB9121401CA0AD8200BA0A40 /* ARTMsgPackEncoder.m in Sources */, @@ -3261,7 +3241,6 @@ D798554923EB96C000946BE2 /* DeltaCodecTests.swift in Sources */, 2124B78829DB127900AD8361 /* MockVersion2Log.swift in Sources */, D510E4AC29F1659F00F77F43 /* Aspects.m in Sources */, - 2132C21729D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */, 21113B4A29DB60F800652C86 /* MockRetryDelayCalculator.swift in Sources */, 217FCF4329D626E4006E5F2D /* MockJitterCoefficientGenerator.swift in Sources */, D7093C21219E466E00723F17 /* RestClientChannelsTests.swift in Sources */, @@ -3320,7 +3299,6 @@ D798554A23EB96C000946BE2 /* DeltaCodecTests.swift in Sources */, 2124B78929DB127900AD8361 /* MockVersion2Log.swift in Sources */, D510E4AD29F1659F00F77F43 /* Aspects.m in Sources */, - 2132C21829D20F69000C4355 /* ResumeRequestResponseTests.swift in Sources */, 21113B4B29DB60F800652C86 /* MockRetryDelayCalculator.swift in Sources */, 217FCF4429D626E4006E5F2D /* MockJitterCoefficientGenerator.swift in Sources */, D7093C74219EE26400723F17 /* AuthTests.swift in Sources */, @@ -3414,7 +3392,6 @@ D710D49921949ACA008F54AD /* ARTAuth.m in Sources */, D710D5D321949D78008F54AD /* ARTTokenParams.m in Sources */, D710D53221949C54008F54AD /* ARTPushChannel.m in Sources */, - 2132C21329D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */, D710D5D421949D78008F54AD /* ARTTokenDetails.m in Sources */, D710D49B21949ACA008F54AD /* ARTRestChannels.m in Sources */, D710D62E21949E03008F54AD /* ARTURLSessionServerTrust.m in Sources */, @@ -3540,7 +3517,6 @@ D710D5F921949D79008F54AD /* ARTTokenParams.m in Sources */, D710D54421949C55008F54AD /* ARTPushChannel.m in Sources */, D710D5FA21949D79008F54AD /* ARTTokenDetails.m in Sources */, - 2132C21429D20F05000C4355 /* ARTResumeRequestResponse.m in Sources */, D710D4A521949ACB008F54AD /* ARTRestChannels.m in Sources */, D710D63E21949E04008F54AD /* ARTURLSessionServerTrust.m in Sources */, D710D65121949E77008F54AD /* ARTJsonEncoder.m in Sources */, diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index cb0d1c521..2da822fd9 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -3,6 +3,10 @@ #import "ARTRealtime+Private.h" #import "ARTEventEmitter+Private.h" #import "ARTQueuedDealloc.h" +#import "ARTRealtimeChannels+Private.h" +#import "ARTRealtimeChannel+Private.h" + +#define IsInactiveConnectionState(state) (state == ARTRealtimeClosing || state == ARTRealtimeClosed || state == ARTRealtimeFailed || state == ARTRealtimeSuspended) @implementation ARTConnection { ARTQueuedDealloc *_dealloc; @@ -20,12 +24,16 @@ - (NSString *)key { return _internal.key; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-implementations" - (NSString *)recoveryKey { - return _internal.recoveryKey; + return [_internal createRecoveryKey]; } +#pragma GCC diagnostic pop -- (int64_t)serial { - return _internal.serial; +// RTN16g - recovery key as a JSON serialized version of [ARTConnectionRecoveryKey] +- (NSString *)createRecoveryKey { + return [_internal createRecoveryKey]; } - (NSInteger)maxMessageSize { @@ -96,7 +104,6 @@ @implementation ARTConnectionInternal { NSString *_id; NSString *_key; NSInteger _maxMessageSize; - int64_t _serial; ARTRealtimeConnectionState _state; ARTErrorInfo *_errorReason; } @@ -106,7 +113,6 @@ - (instancetype)initWithRealtime:(ARTRealtimeInternal *)realtime logger:(ARTInte _eventEmitter = [[ARTPublicEventEmitter alloc] initWithRest:realtime.rest logger:logger]; _realtime = realtime; _queue = _realtime.rest.queue; - _serial = -1; } return self; } @@ -139,14 +145,6 @@ - (NSString *)key { return ret; } -- (int64_t)serial { - __block int64_t ret; -dispatch_sync(_queue, ^{ - ret = [self serial_nosync]; -}); - return ret; -} - - (ARTRealtimeConnectionState)state { __block ARTRealtimeConnectionState ret; dispatch_sync(_queue, ^{ @@ -195,10 +193,6 @@ - (NSString *)key_nosync { return _key; } -- (int64_t)serial_nosync { - return _serial; -} - - (ARTRealtimeConnectionState)state_nosync { return _state; } @@ -215,44 +209,22 @@ - (void)setKey:(NSString *)key { _key = key; } -- (void)setSerial:(int64_t)serial { - _serial = serial; -} - - (void)setMaxMessageSize:(NSInteger)maxMessageSize { _maxMessageSize = maxMessageSize; } - (void)setState:(ARTRealtimeConnectionState)state { _state = state; + if (IsInactiveConnectionState(state)) { + _id = nil; // RTN8c + _key = nil; // RTN9c + } } - (void)setErrorReason:(ARTErrorInfo *_Nullable)errorReason { _errorReason = errorReason; } -- (NSString *)recoveryKey { - __block NSString *ret; -dispatch_sync(_queue, ^{ - ret = [self recoveryKey_nosync]; -}); - return ret; -} - -- (NSString *)recoveryKey_nosync { - switch(self.state_nosync) { - case ARTRealtimeConnecting: - case ARTRealtimeConnected: - case ARTRealtimeDisconnected: - case ARTRealtimeSuspended: { - return [self.key_nosync stringByAppendingString:[NSString stringWithFormat:@":%ld:%ld", (long)self.serial_nosync, (long)_realtime.msgSerial]]; - } - default: { - return nil; - } - } -} - - (ARTEventListener *)on:(ARTRealtimeConnectionEvent)event callback:(ARTConnectionStateCallback)cb { return [_eventEmitter on:[ARTEvent newWithConnectionEvent:event] callback:cb]; } @@ -284,6 +256,39 @@ - (void)off:(ARTEventListener *)listener { [_eventEmitter off:listener]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (NSString *)recoveryKey { + return [self createRecoveryKey]; +} +#pragma clang diagnostic pop + +- (NSString *)createRecoveryKey_nosync { + if (_key == nil || IsInactiveConnectionState(_state)) { // RTN16g2 + return nil; + } + + NSMutableDictionary *channelSerials = [NSMutableDictionary new]; + for (ARTRealtimeChannelInternal *const channel in _realtime.channels.nosyncIterable) { + if (channel.state_nosync == ARTRealtimeChannelAttached) { + channelSerials[channel.name] = channel.serial; + } + } + + ARTConnectionRecoveryKey *const recoveryKey = [[ARTConnectionRecoveryKey alloc] initWithConnectionKey:_key + msgSerial:_realtime.msgSerial + channelSerials:channelSerials]; + return [recoveryKey jsonString]; +} + +- (NSString *)createRecoveryKey { + __block NSString *ret; +dispatch_sync(_queue, ^{ + ret = [self createRecoveryKey_nosync]; +}); + return ret; +} + - (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)data { [_eventEmitter emit:[ARTEvent newWithConnectionEvent:event] with:data]; } @@ -303,3 +308,57 @@ + (instancetype)newWithConnectionEvent:(ARTRealtimeConnectionEvent)value { } @end + +@implementation ARTConnectionRecoveryKey + +- (instancetype)initWithConnectionKey:(NSString *)connectionKey + msgSerial:(int64_t)msgSerial + channelSerials:(NSDictionary *)channelSerials { + self = [super init]; + if (self) { + _connectionKey = connectionKey; + _msgSerial = msgSerial; + _channelSerials = channelSerials; + } + return self; +} + +- (NSString *)jsonString { + NSError *error; + NSDictionary *const object = @{ + @"msgSerial": @(_msgSerial), + @"connectionKey": _connectionKey, + @"channelSerials": _channelSerials + }; + + NSData *const jsonData = [NSJSONSerialization dataWithJSONObject:object + options:0 + error:&error]; + if (error) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"%@: This JSON serialization should pass without errors.", self.class] + userInfo:nil]; + } + + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + ++ (ARTConnectionRecoveryKey *)fromJsonString:(NSString *)json error:(NSError **)errorPtr { + NSData *const jsonData = [json dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error = nil; + NSDictionary *const object = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + if (errorPtr) { + *errorPtr = error; + } + return nil; + } + + return [[ARTConnectionRecoveryKey alloc] initWithConnectionKey:[object valueForKey:@"connectionKey"] + msgSerial:[[object valueForKey:@"msgSerial"] longLongValue] + channelSerials:[object valueForKey:@"channelSerials"]]; +} + +@end diff --git a/Source/ARTDefault.m b/Source/ARTDefault.m index c1b86ef5e..5daab3df1 100644 --- a/Source/ARTDefault.m +++ b/Source/ARTDefault.m @@ -3,7 +3,7 @@ #import "ARTNSArray+ARTFunctional.h" #import "ARTClientInformation+Private.h" -static NSString *const ARTDefault_apiVersion = @"1.2"; +static NSString *const ARTDefault_apiVersion = @"2"; // CSV2 NSString *const ARTDefaultProduction = @"production"; diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index efea77dde..a020dce26 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -719,10 +719,6 @@ - (ARTProtocolMessage *)protocolMessageFromDictionary:(NSDictionary *)input { message.channel = [input artString:@"channel"]; message.channelSerial = [input artString:@"channelSerial"]; message.connectionId = [input artString:@"connectionId"]; - NSNumber * serial = [input artNumber:@"connectionSerial"]; - if (serial != nil) { - message.connectionSerial = [serial longLongValue]; - } message.id = [input artString:@"id"]; message.msgSerial = [input artNumber:@"msgSerial"]; message.timestamp = [input artTimestamp:@"timestamp"]; diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 8cfb372d0..7649c9071 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -39,7 +39,7 @@ @interface ARTPresenceMap () { ARTPresenceSyncState _syncState; ARTEventEmitter *_syncEventEmitter; NSMutableDictionary *_members; - NSMutableSet *_localMembers; + NSMutableDictionary *_localMembers; // RTP17h } @end @@ -64,7 +64,7 @@ - (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTIntern return _members; } -- (NSMutableSet *)localMembers { +- (NSDictionary *)localMembers { return _localMembers; } @@ -102,7 +102,7 @@ - (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sess [_members setObject:message forKey:message.memberKey]; // Local member if ([message.connectionId isEqualToString:self.delegate.connectionId]) { - [_localMembers addObject:message]; + _localMembers[message.clientId] = message; ARTLogDebug(_logger, @"local member %@ with action %@ has been added", message.memberKey, ARTPresenceActionToStr(message.action).uppercaseString); } } @@ -113,7 +113,7 @@ - (void)internalRemove:(ARTPresenceMessage *)message { - (void)internalRemove:(ARTPresenceMessage *)message force:(BOOL)force { if ([message.connectionId isEqualToString:self.delegate.connectionId] && !message.isSynthesized) { - [_localMembers removeObject:message]; + [_localMembers removeObjectForKey:message.clientId]; } const BOOL syncInProgress = self.syncInProgress; @@ -144,26 +144,24 @@ - (void)leaveMembersNotPresentInSync { if (member.syncSessionId != _syncSessionId) { // Handle members that have not been added or updated in the PresenceMap during the sync process ARTPresenceMessage *leave = [member copy]; - [self internalRemove:member]; + [self internalRemove:member force:true]; [self.delegate map:self didRemovedMemberNoLongerPresent:leave]; } } } -- (void)reenterLocalMembersMissingFromSync { - ARTLogDebug(_logger, @"%p reentering local members missed from sync (syncSessionId=%lu)", self, (unsigned long)_syncSessionId); - NSSet *filteredLocalMembers = [_localMembers filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"syncSessionId != %lu", (unsigned long)_syncSessionId]]; - for (ARTPresenceMessage *localMember in filteredLocalMembers) { +- (void)reenterLocalMembers { + ARTLogDebug(_logger, @"%p reentering local members", self); + for (ARTPresenceMessage *localMember in [_localMembers allValues]) { ARTPresenceMessage *reenter = [localMember copy]; - [self internalRemove:localMember]; [self.delegate map:self shouldReenterLocalMember:reenter]; } [self cleanUpAbsentMembers]; } - (void)reset { - _members = [NSMutableDictionary dictionary]; - _localMembers = [NSMutableSet set]; + _members = [NSMutableDictionary new]; + _localMembers = [NSMutableDictionary new]; } - (void)startSync { @@ -178,7 +176,7 @@ - (void)endSync { [self cleanUpAbsentMembers]; [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; - [self reenterLocalMembersMissingFromSync]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; ARTLogDebug(_logger, @"%p PresenceMap sync ended", self); diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index 5f0bfac08..e36d0bb1c 100644 --- a/Source/ARTProtocolMessage.m +++ b/Source/ARTProtocolMessage.m @@ -17,8 +17,6 @@ - (id)init { _channelSerial = nil; _connectionId = nil; _connectionKey = nil; - _connectionSerial = 0; - _hasConnectionSerial = false; _msgSerial = nil; _timestamp = nil; _messages = nil; @@ -46,7 +44,6 @@ - (NSString *)description { [description appendFormat:@" channelSerial: %@,\n", self.channelSerial]; [description appendFormat:@" connectionId: %@,\n", self.connectionId]; [description appendFormat:@" connectionKey: %@,\n", self.connectionKey]; - [description appendFormat:@" connectionSerial: %lld,\n", self.connectionSerial]; [description appendFormat:@" msgSerial: %@,\n", self.msgSerial]; [description appendFormat:@" timestamp: %@,\n", self.timestamp]; [description appendFormat:@" flags: %lld,\n", self.flags]; @@ -54,6 +51,7 @@ - (NSString *)description { [description appendFormat:@" flags.hasBacklog: %@,\n", NSStringFromBOOL(self.hasBacklog)]; [description appendFormat:@" flags.resumed: %@,\n", NSStringFromBOOL(self.resumed)]; [description appendFormat:@" messages: %@\n", self.messages]; + [description appendFormat:@" presence: %@\n", self.presence]; [description appendFormat:@" params: %@\n", self.params]; [description appendFormat:@"}"]; return description; @@ -68,8 +66,6 @@ - (id)copyWithZone:(NSZone *)zone { pm.channelSerial = self.channelSerial; pm.connectionId = self.connectionId; pm.connectionKey = self.connectionKey; - pm.connectionSerial = self.connectionSerial; - pm.hasConnectionSerial = self.hasConnectionSerial; pm.msgSerial = self.msgSerial; pm.timestamp = self.timestamp; pm.messages = self.messages; @@ -163,11 +159,6 @@ - (BOOL)mergeWouldExceedMaxSize:(NSArray*)messages { return totalSize > maxSize; } -- (void)setConnectionSerial:(int64_t)connectionSerial { - _connectionSerial =connectionSerial; - _hasConnectionSerial = true; -} - - (BOOL)ackRequired { return self.action == ARTProtocolMessageMessage || self.action == ARTProtocolMessagePresence; } diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index d207b27c8..7bf008227 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -38,6 +38,11 @@ #import "ARTRealtimeChannels+Private.h" #import "ARTPush+Private.h" #import "ARTQueuedDealloc.h" +#import "ARTTypes.h" +#import "ARTChannels.h" +#import "ARTConstants.h" +#import "ARTCrypto.h" +#import "ARTDeviceIdentityTokenDetails.h" #import "ARTErrorChecker.h" #import "ARTConnectionStateChangeMetadata.h" #import "ARTChannelStateChangeMetadata.h" @@ -239,6 +244,21 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { self.rest.prioritizedHost = nil; + if (options.recover) { + NSError *error; + ARTConnectionRecoveryKey *const recoveryKey = [ARTConnectionRecoveryKey fromJsonString:options.recover error:&error]; + if (error) { + ARTLogError(self.logger, @"Couldn't construct a recovery key from the string provided: %@", options.recover); + } + else { + _msgSerial = recoveryKey.msgSerial; // RTN16f + for (NSString *const channelName in recoveryKey.channelSerials) { + ARTRealtimeChannelInternal *const channel = [_channels get:channelName]; + channel.serial = recoveryKey.channelSerials[channelName]; // RTN16j + } + } + } + if (options.autoConnect) { [self connect]; } @@ -280,7 +300,7 @@ - (void)auth:(ARTAuthInternal *)auth didAuthorize:(ARTTokenDetails *)tokenDetail // Halt the current connection and reconnect with the most recent token ARTLogDebug(self.logger, @"RS:%p halt current connection and reconnect with %@", self.rest, tokenDetails); [self abortAndReleaseTransport:[ARTStatus state:ARTStateOk]]; - [self setTransportWithResumeKey:self->_transport.resumeKey connectionSerial:self->_transport.connectionSerial]; + [self setTransportWithResumeKey:self->_transport.resumeKey]; [self->_transport connectWithToken:tokenDetails.token]; [self cancelAllPendingAuthorizations]; waitForResponse(); @@ -501,8 +521,13 @@ - (BOOL)stats:(ARTStatsQuery *)query callback:(ARTPaginatedStatsCallback)callbac - (void)transition:(ARTRealtimeConnectionState)state withMetadata:(ARTConnectionStateChangeMetadata *)metadata { ARTLogVerbose(self.logger, @"R:%p realtime state transitions to %tu - %@%@", self, state, ARTRealtimeConnectionStateToStr(state), metadata.retryAttempt ? [NSString stringWithFormat: @" (result of %@)", metadata.retryAttempt.id] : @""); - ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state_nosync event:(ARTRealtimeConnectionEvent)state reason:metadata.errorInfo retryIn:0 retryAttempt:metadata.retryAttempt]; - + ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state + previous:self.connection.state_nosync + event:(ARTRealtimeConnectionEvent)state + reason:metadata.errorInfo + retryIn:0 + retryAttempt:metadata.retryAttempt + resumed:metadata.resumed]; [self.connection setState:state]; [self.connection setErrorReason:metadata.errorInfo]; @@ -603,7 +628,6 @@ - (void)clearConnectionStateIfInactive { if (intervalSinceLast > (_maxIdleInterval + _connectionStateTtl)) { [self.connection setId:nil]; [self.connection setKey:nil]; - [self.connection setSerial:0]; } } @@ -712,13 +736,8 @@ - (ARTEventListener *)performTransitionWithStateChange:(ARTConnectionStateChange case ARTRealtimeConnected: { _fallbacks = nil; _connectionLostAt = nil; - if (stateChange.reason) { - ARTStatus *status = [ARTStatus state:ARTStateError info:[stateChange.reason copy]]; - [self failPendingMessages:status]; - } - else { - [self resendPendingMessages]; - } + self.options.recover = nil; // RTN16k + [self resendPendingMessagesWithResumed:stateChange.resumed]; // RTN19a1 [_connectedEventEmitter emit:nil with:nil]; break; } @@ -796,13 +815,11 @@ - (ARTEventListener *)performTransitionWithStateChange:(ARTConnectionStateChange - (void)createAndConnectTransportWithConnectionResume:(BOOL)resume { NSString *resumeKey = nil; - NSNumber *connectionSerial = nil; if (resume) { resumeKey = self.connection.key_nosync; - connectionSerial = [NSNumber numberWithLongLong:self.connection.serial_nosync]; _resuming = true; } - [self setTransportWithResumeKey:resumeKey connectionSerial:connectionSerial]; + [self setTransportWithResumeKey:resumeKey]; [self transportConnectForcingNewToken:_renewingToken newConnection:true]; } @@ -818,14 +835,14 @@ - (void)closeAndReleaseTransport { } } -- (void)resetTransportWithResumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { +- (void)resetTransportWithResumeKey:(NSString *const)resumeKey { [self closeAndReleaseTransport]; - [self setTransportWithResumeKey:resumeKey connectionSerial:connectionSerial]; + [self setTransportWithResumeKey:resumeKey]; } -- (void)setTransportWithResumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { +- (void)setTransportWithResumeKey:(NSString *)resumeKey { const id factory = self.options.testOptions.transportFactory; - _transport = [factory transportWithRest:self.rest options:self.options resumeKey:resumeKey connectionSerial:connectionSerial logger:self.logger]; + _transport = [factory transportWithRest:self.rest options:self.options resumeKey:resumeKey logger:self.logger]; _transport.delegate = self; } @@ -851,34 +868,29 @@ - (void)onHeartbeat { - (void)onConnected:(ARTProtocolMessage *)message { _renewingToken = false; - // Resuming - if (_resuming) { - if (![message.connectionId isEqualToString:self.connection.id_nosync]) { // RTN15c3 - ARTLogWarn(self.logger, @"RT:%p connection \"%@\" has reconnected, but resume failed. Reattaching any attached channels", self, message.connectionId); - // Reattach all channels - for (ARTRealtimeChannelInternal *channel in self.channels.nosyncIterable) { - ARTAttachRequestMetadata *const metadata = [[ARTAttachRequestMetadata alloc] initWithReason:message.error]; - [channel reattachWithMetadata:metadata]; - } - _resuming = false; - } - else if (message.error) { - ARTLogWarn(self.logger, @"RT:%p connection \"%@\" has resumed with non-fatal error \"%@\"", self, message.connectionId, message.error.message); - // The error will be emitted on `transition` - } - else { - ARTLogDebug(self.logger, @"RT:%p connection \"%@\" has reconnected and resumed successfully", self, message.connectionId); - } - } - switch (self.connection.state_nosync) { case ARTRealtimeConnecting: { + if (_resuming) { + if ([message.connectionId isEqualToString:self.connection.id_nosync]) { + ARTLogDebug(self.logger, @"RT:%p connection \"%@\" has reconnected and resumed successfully", self, message.connectionId); + } + else { + ARTLogWarn(self.logger, @"RT:%p connection \"%@\" has reconnected, but resume failed. Error: \"%@\"", self, message.connectionId, message.error.message); + } + // Reattach all channels regardless resume success - RTN15c6, RTN15c7 + for (ARTRealtimeChannelInternal *channel in self.channels.nosyncIterable) { + ARTAttachRequestMetadata *const metadata = [[ARTAttachRequestMetadata alloc] initWithReason:message.error]; + [channel reattachWithMetadata:metadata]; + } + _resuming = false; + } // If there's no previous connectionId, then don't reset the msgSerial //as it may have been set by recover data (unless the recover failed). NSString *prevConnId = self.connection.id_nosync; BOOL connIdChanged = prevConnId && ![message.connectionId isEqualToString:prevConnId]; - BOOL recoverFailure = !prevConnId && message.error; - if (connIdChanged || recoverFailure) { + BOOL recoverFailure = !prevConnId && message.error; // RTN16d + BOOL resumed = !(connIdChanged || recoverFailure); + if (!resumed) { ARTLogDebug(self.logger, @"RT:%p msgSerial of connection \"%@\" has been reset", self, self.connection.id_nosync); self.msgSerial = 0; self.pendingMessageStartSerial = 0; @@ -887,7 +899,6 @@ - (void)onConnected:(ARTProtocolMessage *)message { [self.connection setId:message.connectionId]; [self.connection setKey:message.connectionKey]; [self.connection setMaxMessageSize:message.connectionDetails.maxMessageSize]; - [self.connection setSerial:message.connectionSerial]; if (message.connectionDetails && message.connectionDetails.connectionStateTtl) { _connectionStateTtl = message.connectionDetails.connectionStateTtl; @@ -898,7 +909,9 @@ - (void)onConnected:(ARTProtocolMessage *)message { [self setIdleTimer]; } ARTConnectionStateChangeMetadata *const metadata = [[ARTConnectionStateChangeMetadata alloc] initWithErrorInfo:message.error]; + metadata.resumed = resumed; // RTN19a [self transition:ARTRealtimeConnected withMetadata:metadata]; + break; } case ARTRealtimeConnected: { @@ -1051,7 +1064,7 @@ - (BOOL)isTokenError:(nullable ARTErrorInfo *)error { } - (void)transportReconnectWithExistingParameters { - [self resetTransportWithResumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + [self resetTransportWithResumeKey:_transport.resumeKey]; NSString *host = [self getClientOptions].testOptions.reconnectionRealtimeHost; // for tests purposes only, always `nil` in production if (host != nil) { [self.transport setHost:host]; @@ -1060,14 +1073,14 @@ - (void)transportReconnectWithExistingParameters { } - (void)transportReconnectWithHost:(NSString *)host { - [self resetTransportWithResumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + [self resetTransportWithResumeKey:_transport.resumeKey]; [self.transport setHost:host]; [self transportConnectForcingNewToken:false newConnection:true]; } - (void)transportReconnectWithRenewedToken { _renewingToken = true; - [self resetTransportWithResumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + [self resetTransportWithResumeKey:_transport.resumeKey]; [_connectingTimeoutListener restartTimer]; [self transportConnectForcingNewToken:true newConnection:true]; } @@ -1124,7 +1137,7 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken newConnection:(BOOL) } if (forceNewToken && newConnection) { - [self resetTransportWithResumeKey:self->_transport.resumeKey connectionSerial:self->_transport.connectionSerial]; + [self resetTransportWithResumeKey:self->_transport.resumeKey]; } if (newConnection) { [self.transport connectWithToken:tokenDetails.token]; @@ -1242,9 +1255,11 @@ - (BOOL)isActive { return [self shouldQueueEvents] || [self shouldSendEvents]; } -- (void)sendImpl:(ARTProtocolMessage *)pm sentCallback:(ARTCallback)sentCallback ackCallback:(ARTStatusCallback)ackCallback { +- (void)sendImpl:(ARTProtocolMessage *)pm reuseMsgSerial:(BOOL)reuseMsgSerial sentCallback:(ARTCallback)sentCallback ackCallback:(ARTStatusCallback)ackCallback { if (pm.ackRequired) { - pm.msgSerial = [NSNumber numberWithLongLong:self.msgSerial]; + if (!reuseMsgSerial) { // RTN19a2 + pm.msgSerial = [NSNumber numberWithLongLong:self.msgSerial]; + } } for (ARTMessage *msg in pm.messages) { @@ -1268,7 +1283,9 @@ - (void)sendImpl:(ARTProtocolMessage *)pm sentCallback:(ARTCallback)sentCallback } if (pm.ackRequired) { - self.msgSerial++; + if (!reuseMsgSerial) { + self.msgSerial++; + } ARTPendingMessage *pendingMessage = [[ARTPendingMessage alloc] initWithProtocolMessage:pm ackCallback:ackCallback]; [self.pendingMessages addObject:pendingMessage]; } @@ -1280,9 +1297,9 @@ - (void)sendImpl:(ARTProtocolMessage *)pm sentCallback:(ARTCallback)sentCallback } } -- (void)send:(ARTProtocolMessage *)msg sentCallback:(ARTCallback)sentCallback ackCallback:(ARTStatusCallback)ackCallback { +- (void)send:(ARTProtocolMessage *)msg reuseMsgSerial:(BOOL)reuseMsgSerial sentCallback:(ARTCallback)sentCallback ackCallback:(ARTStatusCallback)ackCallback { if ([self shouldSendEvents]) { - [self sendImpl:msg sentCallback:sentCallback ackCallback:ackCallback]; + [self sendImpl:msg reuseMsgSerial:reuseMsgSerial sentCallback:sentCallback ackCallback:ackCallback]; } else if ([self shouldQueueEvents]) { ARTQueuedMessage *lastQueuedMessage = self.queuedMessages.lastObject; //RTL6d5 @@ -1303,14 +1320,19 @@ - (void)send:(ARTProtocolMessage *)msg sentCallback:(ARTCallback)sentCallback ac } } -- (void)resendPendingMessages { - NSArray *pms = self.pendingMessages; - if (pms.count > 0) { +- (void)send:(ARTProtocolMessage *)msg sentCallback:(ARTCallback)sentCallback ackCallback:(ARTStatusCallback)ackCallback { + [self send:msg reuseMsgSerial:NO sentCallback:sentCallback ackCallback:ackCallback]; +} + +- (void)resendPendingMessagesWithResumed:(BOOL)reuseMsgSerial { + NSArray *pendingMessages = self.pendingMessages; + if (pendingMessages.count > 0) { ARTLogDebug(self.logger, @"RT:%p resending messages waiting for acknowledgment", self); } self.pendingMessages = [NSMutableArray array]; - for (ARTPendingMessage *pendingMessage in pms) { - [self send:pendingMessage.msg sentCallback:nil ackCallback:^(ARTStatus *status) { + for (ARTPendingMessage *pendingMessage in pendingMessages) { + ARTProtocolMessage* pm = pendingMessage.msg; + [self send:pm reuseMsgSerial:reuseMsgSerial sentCallback:nil ackCallback:^(ARTStatus *status) { pendingMessage.ackCallback(status); }]; } @@ -1329,7 +1351,7 @@ - (void)sendQueuedMessages { self.queuedMessages = [NSMutableArray array]; for (ARTQueuedMessage *message in qms) { - [self sendImpl:message.msg sentCallback:message.sentCallback ackCallback:message.ackCallback]; + [self sendImpl:message.msg reuseMsgSerial:NO sentCallback:message.sentCallback ackCallback:message.ackCallback]; } } @@ -1528,10 +1550,7 @@ - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *) } NSAssert(transport == self.transport, @"Unexpected transport"); - if (message.hasConnectionSerial) { - [self.connection setSerial:message.connectionSerial]; - } - + switch (message.action) { case ARTProtocolMessageHeartbeat: [self onHeartbeat]; diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 75022c069..c62d26244 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -230,7 +230,6 @@ @interface ARTRealtimeChannelInternal () { ARTEventEmitter *_attachedEventEmitter; ARTEventEmitter *_detachedEventEmitter; NSString * _Nullable _lastPayloadMessageId; - NSString * _Nullable _lastPayloadProtocolMessageChannelSerial; BOOL _decodeFailureRecoveryInProgress; } @@ -620,6 +619,7 @@ - (void)transition:(ARTRealtimeChannelState)state withMetadata:(ARTChannelStateC self.attachResume = true; break; case ARTRealtimeChannelSuspended: { + self.serial = nil; // RTP5a1 ARTRetryAttempt *const retryAttempt = [self.attachRetryState addRetryAttempt]; [_attachedEventEmitter emit:nil with:metadata.errorInfo]; @@ -638,9 +638,11 @@ - (void)transition:(ARTRealtimeChannelState)state withMetadata:(ARTChannelStateC self.attachResume = false; break; case ARTRealtimeChannelDetached: + self.serial = nil; // RTP5a1 [self.presenceMap failsSync:metadata.errorInfo]; break; case ARTRealtimeChannelFailed: + self.serial = nil; // RTP5a1 self.attachResume = false; [_attachedEventEmitter emit:nil with:metadata.errorInfo]; [_detachedEventEmitter emit:nil with:metadata.errorInfo]; @@ -727,6 +729,10 @@ - (void)setAttached:(ARTProtocolMessage *)message { } self.attachSerial = message.channelSerial; + // RTL15b + if (message.channelSerial) { + self.serial = message.channelSerial; + } if (message.hasPresence) { [self.presenceMap startSync]; @@ -745,6 +751,7 @@ - (void)setAttached:(ARTProtocolMessage *)message { } ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state_nosync previous:self.state_nosync event:ARTChannelEventUpdate reason:message.error resumed:message.resumed]; [self emit:stateChange.event with:stateChange]; + [self.presenceMap reenterLocalMembers]; // RTP17i } return; } @@ -759,6 +766,7 @@ - (void)setAttached:(ARTProtocolMessage *)message { [_attachedEventEmitter emit:nil with:nil]; [self.presence sendPendingPresence]; + [self.presenceMap reenterLocalMembers]; // RTP17i } - (void)setDetached:(ARTProtocolMessage *)message { @@ -835,7 +843,7 @@ - (void)onMessage:(ARTProtocolMessage *)pm { for (int j = i + 1; j < pm.messages.count; j++) { ARTLogVerbose(self.logger, @"R:%p C:%p (%@) message skipped %@", _realtime, self, self.name, pm.messages[j]); } - [self startDecodeFailureRecoveryWithChannelSerial:_lastPayloadProtocolMessageChannelSerial error:incompatibleIdError]; + [self startDecodeFailureRecoveryWithErrorInfo:incompatibleIdError]; return; } } @@ -856,7 +864,7 @@ - (void)onMessage:(ARTProtocolMessage *)pm { [self emit:stateChange.event with:stateChange]; if (decodeError.code == ARTErrorUnableToDecodeMessage) { - [self startDecodeFailureRecoveryWithChannelSerial:_lastPayloadProtocolMessageChannelSerial error:errorInfo]; + [self startDecodeFailureRecoveryWithErrorInfo:errorInfo]; return; } } @@ -875,12 +883,20 @@ - (void)onMessage:(ARTProtocolMessage *)pm { ++i; } - - _lastPayloadProtocolMessageChannelSerial = pm.channelSerial; + + // RTL15b + if (pm.channelSerial) { + self.serial = pm.channelSerial; + } } - (void)onPresence:(ARTProtocolMessage *)message { ARTLogDebug(self.logger, @"RT:%p C:%p (%@) handle PRESENCE message", _realtime, self, self.name); + // RTL15b + if (message.channelSerial) { + self.serial = message.channelSerial; + } + int i = 0; ARTDataEncoder *dataEncoder = self.dataEncoder; for (ARTPresenceMessage *p in message.presence) { @@ -1019,15 +1035,14 @@ - (void)internalAttach:(ARTCallback)callback metadata:(ARTAttachRequestMetadata storeErrorInfo:NO retryAttempt:metadata.retryAttempt]; [self transition:ARTRealtimeChannelAttaching withMetadata:stateChangeMetadata]; - - [self attachAfterChecks:callback channelSerial:metadata.channelSerial]; + [self attachAfterChecks:callback]; } -- (void)attachAfterChecks:(ARTCallback)callback channelSerial:(NSString *)channelSerial { +- (void)attachAfterChecks:(ARTCallback)callback { ARTProtocolMessage *attachMessage = [[ARTProtocolMessage alloc] init]; attachMessage.action = ARTProtocolMessageAttach; attachMessage.channel = self.name; - attachMessage.channelSerial = channelSerial; + attachMessage.channelSerial = self.serial; // RTL4c1 attachMessage.params = self.options_nosync.params; attachMessage.flags = self.options_nosync.modes; @@ -1173,15 +1188,14 @@ - (BOOL)history:(ARTRealtimeHistoryQuery *)query callback:(ARTPaginatedMessagesC return [_restChannel history:query callback:callback error:errorPtr]; } -- (void)startDecodeFailureRecoveryWithChannelSerial:(NSString *)channelSerial error:(ARTErrorInfo *)error { +- (void)startDecodeFailureRecoveryWithErrorInfo:(ARTErrorInfo *)error { if (_decodeFailureRecoveryInProgress) { return; } ARTLogWarn(self.logger, @"R:%p C:%p (%@) starting delta decode failure recovery process", _realtime, self, self.name); _decodeFailureRecoveryInProgress = true; - ARTAttachRequestMetadata *const metadata = [[ARTAttachRequestMetadata alloc] initWithReason:error - channelSerial:channelSerial]; + ARTAttachRequestMetadata *const metadata = [[ARTAttachRequestMetadata alloc] initWithReason:error]; [self internalAttach:^(ARTErrorInfo *e) { self->_decodeFailureRecoveryInProgress = false; } metadata:metadata]; @@ -1202,7 +1216,7 @@ - (void)map:(ARTPresenceMap *)map didRemovedMemberNoLongerPresent:(ARTPresenceMe } - (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage *)presence { - [self.presence enterClient:presence.clientId data:presence.data callback:^(ARTErrorInfo *error) { + [self.presence enterWithPresenceMessageId:presence.id clientId:presence.clientId data:presence.data callback:^(ARTErrorInfo *error) { if (error != nil) { NSString *message = [NSString stringWithFormat:@"Re-entering member \"%@\" is failed with code %ld (%@)", presence.memberKey, (long)error.code, error.message]; ARTErrorInfo *reenterError = [ARTErrorInfo createWithCode:ARTErrorUnableToAutomaticallyReEnterPresenceChannel message:message]; diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index c5530a479..5d76cbd57 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -252,7 +252,7 @@ - (void)enter:(id)data callback:(ARTCallback)cb { } dispatch_async(_queue, ^{ - [self enterOrUpdateAfterChecks:ARTPresenceEnter clientId:nil data:data callback:cb]; + [self enterOrUpdateAfterChecks:ARTPresenceEnter messageId:nil clientId:nil data:data callback:cb]; }); } @@ -271,10 +271,24 @@ - (void)enterClient:(NSString *)clientId data:(id)data callback:(ARTCallback)cb } dispatch_async(_queue, ^{ - [self enterOrUpdateAfterChecks:ARTPresenceEnter clientId:clientId data:data callback:cb]; + [self enterOrUpdateAfterChecks:ARTPresenceEnter messageId:nil clientId:clientId data:data callback:cb]; }); } +- (void)enterWithPresenceMessageId:(NSString *)messageId clientId:(NSString *)clientId data:(id)data callback:(ARTCallback)cb { + if (cb) { + ARTCallback userCallback = cb; + cb = ^(ARTErrorInfo *_Nullable error) { + dispatch_async(self->_userQueue, ^{ + userCallback(error); + }); + }; + } + dispatch_async(_queue, ^{ + [self enterOrUpdateAfterChecks:ARTPresenceEnter messageId:messageId clientId:clientId data:data callback:cb]; + }); +} + - (void)update:(id)data { [self update:data callback:nil]; } @@ -290,7 +304,7 @@ - (void)update:(id)data callback:(ARTCallback)cb { } dispatch_async(_queue, ^{ - [self enterOrUpdateAfterChecks:ARTPresenceUpdate clientId:nil data:data callback:cb]; + [self enterOrUpdateAfterChecks:ARTPresenceUpdate messageId:nil clientId:nil data:data callback:cb]; }); } @@ -309,11 +323,11 @@ - (void)updateClient:(NSString *)clientId data:(id)data callback:(ARTCallback)cb } dispatch_async(_queue, ^{ - [self enterOrUpdateAfterChecks:ARTPresenceUpdate clientId:clientId data:data callback:cb]; + [self enterOrUpdateAfterChecks:ARTPresenceUpdate messageId:nil clientId:clientId data:data callback:cb]; }); } -- (void)enterOrUpdateAfterChecks:(ARTPresenceAction)action clientId:(NSString *_Nullable)clientId data:(id)data callback:(ARTCallback)cb { +- (void)enterOrUpdateAfterChecks:(ARTPresenceAction)action messageId:(NSString *_Nullable)messageId clientId:(NSString *_Nullable)clientId data:(id)data callback:(ARTCallback)cb { switch (_channel.state_nosync) { case ARTRealtimeChannelDetached: case ARTRealtimeChannelFailed: { @@ -329,6 +343,7 @@ - (void)enterOrUpdateAfterChecks:(ARTPresenceAction)action clientId:(NSString *_ ARTPresenceMessage *msg = [[ARTPresenceMessage alloc] init]; msg.action = action; + msg.id = messageId; msg.clientId = clientId; msg.data = data; msg.connectionId = _channel.realtime.connection.id_nosync; diff --git a/Source/ARTRealtimeTransportFactory.m b/Source/ARTRealtimeTransportFactory.m index c87e7168b..aeb2759fb 100644 --- a/Source/ARTRealtimeTransportFactory.m +++ b/Source/ARTRealtimeTransportFactory.m @@ -4,13 +4,11 @@ @implementation ARTDefaultRealtimeTransportFactory -- (id)transportWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial logger:(ARTInternalLog *)logger { +- (id)transportWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey logger:(ARTInternalLog *)logger { const id webSocketFactory = [[ARTDefaultWebSocketFactory alloc] init]; - return [[ARTWebSocketTransport alloc] initWithRest:rest options:options resumeKey:resumeKey - connectionSerial:connectionSerial logger:logger webSocketFactory:webSocketFactory]; } diff --git a/Source/ARTResumeRequestResponse.m b/Source/ARTResumeRequestResponse.m deleted file mode 100644 index bec30341d..000000000 --- a/Source/ARTResumeRequestResponse.m +++ /dev/null @@ -1,68 +0,0 @@ -#import "ARTResumeRequestResponse.h" -#import "ARTStatus.h" -#import "ARTProtocolMessage.h" -#import "ARTErrorChecker.h" - -static NSString *TypeDescription(const ARTResumeRequestResponseType type) { - switch (type) { - case ARTResumeRequestResponseTypeValid: - return @"Valid"; - case ARTResumeRequestResponseTypeInvalid: - return @"Invalid"; - case ARTResumeRequestResponseTypeFatalError: - return @"FatalError"; - case ARTResumeRequestResponseTypeTokenError: - return @"TokenError"; - case ARTResumeRequestResponseTypeUnknown: - return @"Unknown"; - } -} - -@implementation ARTResumeRequestResponse - -- (instancetype)initWithCurrentConnectionID:(NSString *const)currentConnectionID - protocolMessage:(ARTProtocolMessage *const)protocolMessage - errorChecker:(const id)errorChecker { - if (!(self = [super init])) { - return nil; - } - - // RTN15c6: "A CONNECTED ProtocolMessage with the same connectionId as the current client (and no error property)." - if (protocolMessage.action == ARTProtocolMessageConnected && [protocolMessage.connectionId isEqualToString:currentConnectionID] && protocolMessage.error == nil) { - _type = ARTResumeRequestResponseTypeValid; - return self; - } - - // RTN15c7: "CONNECTED ProtocolMessage with a new connectionId and an ErrorInfo in the error field." - if (protocolMessage.action == ARTProtocolMessageConnected && ![protocolMessage.connectionId isEqualToString:currentConnectionID] && protocolMessage.error != nil) { - _type = ARTResumeRequestResponseTypeInvalid; - _error = protocolMessage.error; - return self; - } - - // RTN15c5: "ERROR ProtocolMessage indicating a failure to authenticate as a result of a token error (see RTN15h)." - if (protocolMessage.action == ARTProtocolMessageError && protocolMessage.error != nil && [errorChecker isTokenError:protocolMessage.error]) { - _type = ARTResumeRequestResponseTypeTokenError; - _error = protocolMessage.error; - return self; - } - - // RTN15c4: "Any other ERROR ProtocolMessage indicating a fatal error in the connection." - // (I’m reading this as "Any other ERROR ProtocolMessage. This indicates a fatal error in the connection." — that is, I am not expected to apply any further criteria to determine if it is a "fatal" error.) - - // We can assume that an ERROR ProtocolMessage will have a non-nil error (see protocol spec), and if it does not then we will treat it as an "unknown" response type. - if (protocolMessage.action == ARTProtocolMessageError && protocolMessage.error != nil) { - _type = ARTResumeRequestResponseTypeFatalError; - _error = protocolMessage.error; - return self; - } - - _type = ARTResumeRequestResponseTypeUnknown; - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat: @"<%@ %p: type: %@, error: %@>", [self class], self, TypeDescription(self.type), [self.error localizedDescription]]; -} - -@end diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index 990d544a0..b9a35e08b 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -45,10 +45,10 @@ - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(AR } - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn { - return [self initWithCurrent:current previous:previous event:event reason:reason retryIn:retryIn retryAttempt:nil]; + return [self initWithCurrent:current previous:previous event:event reason:reason retryIn:retryIn retryAttempt:nil resumed:NO]; } -- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn retryAttempt:(ARTRetryAttempt *)retryAttempt { +- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn retryAttempt:(ARTRetryAttempt *)retryAttempt resumed:(BOOL)resumed { self = [self init]; if (self) { _current = current; @@ -57,6 +57,7 @@ - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(AR _reason = reason; _retryIn = retryIn; _retryAttempt = retryAttempt; + _resumed = resumed; } return self; } diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index f0394e87f..c14c83c6c 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -17,6 +17,7 @@ #import "ARTNSMutableDictionary+ARTDictionaryUtil.h" #import "ARTStringifiable.h" #import "ARTClientInformation.h" +#import "ARTConnection+Private.h" #import "ARTInternalLog.h" #import "ARTWebSocketFactory.h" @@ -59,7 +60,7 @@ @implementation ARTWebSocketTransport { @synthesize delegate = _delegate; @synthesize stateEmitter = _stateEmitter; -- (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory { +- (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory { self = [super init]; if (self) { _workQueue = rest.queue; @@ -69,7 +70,6 @@ - (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions * _logger = logger; _options = [options copy]; _resumeKey = resumeKey; - _connectionSerial = connectionSerial; _stateEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_workQueue]; _webSocketFactory = webSocketFactory; @@ -121,7 +121,7 @@ - (void)connectWithKey:(NSString *)key { _state = ARTRealtimeTransportStateOpening; ARTLogDebug(self.logger, @"R:%p WS:%p websocket connect with key", _delegate, self); NSURLQueryItem *keyParam = [NSURLQueryItem queryItemWithName:@"key" value:key]; - [self setupWebSocket:@{keyParam.name: keyParam} withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; + [self setupWebSocket:@{keyParam.name: keyParam} withOptions:self.options resumeKey:self.resumeKey]; // Connect [self.websocket open]; } @@ -130,12 +130,12 @@ - (void)connectWithToken:(NSString *)token { _state = ARTRealtimeTransportStateOpening; ARTLogDebug(self.logger, @"R:%p WS:%p websocket connect with token", _delegate, self); NSURLQueryItem *accessTokenParam = [NSURLQueryItem queryItemWithName:@"accessToken" value:token]; - [self setupWebSocket:@{accessTokenParam.name: accessTokenParam} withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; + [self setupWebSocket:@{accessTokenParam.name: accessTokenParam} withOptions:self.options resumeKey:self.resumeKey]; // Connect [self.websocket open]; } -- (NSURL *)setupWebSocket:(NSDictionary *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { +- (NSURL *)setupWebSocket:(NSDictionary *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey { __block NSMutableDictionary *queryItems = [params mutableCopy]; // ClientID @@ -149,28 +149,19 @@ - (NSURL *)setupWebSocket:(NSDictionary *)params w // Format: MsgPack, JSON [queryItems addValueAsURLQueryItem:[_encoder formatAsString] forKey:@"format"]; - if (options.recover) { - NSArray *recoverParts = [options.recover componentsSeparatedByString:@":"]; - if (recoverParts.count > 1 && recoverParts.count <= 3) { - NSString *key = [recoverParts objectAtIndex:0]; - NSString *serial = [recoverParts objectAtIndex:1]; - ARTLogInfo(self.logger, @"R:%p WS:%p ARTWebSocketTransport: attempting recovery of connection %@", _delegate, self, key); - - [queryItems addValueAsURLQueryItem:key forKey:@"recover"]; - [queryItems addValueAsURLQueryItem:serial forKey:@"connectionSerial"]; - - int64_t msgSerial = [[recoverParts lastObject] longLongValue]; - if (msgSerial) { - [_delegate realtimeTransportSetMsgSerial:self msgSerial:msgSerial]; - } + // RTN16k + if (options.recover != nil) { + NSError *error; + ARTConnectionRecoveryKey *const recoveryKey = [ARTConnectionRecoveryKey fromJsonString:options.recover error:&error]; + if (error) { + ARTLogError(_logger, @"Couldn't construct a recovery key from the string provided: %@", options.recover); } else { - ARTLogError(self.logger, @"R:%p WS:%p ARTWebSocketTransport: recovery string is malformed, ignoring: '%@'", _delegate, self, options.recover); + [queryItems addValueAsURLQueryItem:recoveryKey.connectionKey forKey:@"recover"]; } } - else if (resumeKey != nil && connectionSerial != nil) { - [queryItems addValueAsURLQueryItem:resumeKey forKey:@"resume"]; - [queryItems addValueAsURLQueryItem:[NSString stringWithFormat:@"%lld", (long long)[connectionSerial integerValue]] forKey:@"connectionSerial"]; + else if (resumeKey != nil) { + [queryItems addValueAsURLQueryItem:resumeKey forKey:@"resume"]; // RTN15b1 } [queryItems addValueAsURLQueryItem:[ARTDefault apiVersion] forKey:@"v"]; diff --git a/Source/Ably.modulemap b/Source/Ably.modulemap index 7b3c8f9f0..c76454eb1 100644 --- a/Source/Ably.modulemap +++ b/Source/Ably.modulemap @@ -81,7 +81,6 @@ framework module Ably { header "ARTNSURL+ARTUtils.h" header "ARTNSMutableURLRequest+ARTUtils.h" header "ARTErrorChecker.h" - header "ARTResumeRequestResponse.h" header "ARTJitterCoefficientGenerator.h" header "ARTRetryDelayCalculator.h" header "ARTBackoffRetryDelayCalculator.h" diff --git a/Source/PrivateHeaders/Ably/ARTConnection+Private.h b/Source/PrivateHeaders/Ably/ARTConnection+Private.h index 4ab920efa..72329db0b 100644 --- a/Source/PrivateHeaders/Ably/ARTConnection+Private.h +++ b/Source/PrivateHeaders/Ably/ARTConnection+Private.h @@ -8,12 +8,25 @@ NS_ASSUME_NONNULL_BEGIN @class ARTRealtimeInternal; @class ARTInternalLog; +@interface ARTConnectionRecoveryKey : NSObject + +@property (readonly, nonatomic) NSString *connectionKey; +@property (readonly, nonatomic) int64_t msgSerial; +@property (readonly, nonatomic) NSDictionary *channelSerials; + +- (instancetype)initWithConnectionKey:(NSString *)connectionKey + msgSerial:(int64_t)msgSerial + channelSerials:(NSDictionary *)channelSerials; + +- (NSString *)jsonString; ++ (nullable ARTConnectionRecoveryKey *)fromJsonString:(NSString *)json error:(NSError *_Nullable *_Nullable)errorPtr; + +@end + @interface ARTConnectionInternal : NSObject @property (nullable, readonly, nonatomic) NSString *id; @property (nullable, readonly, nonatomic) NSString *key; -@property (nullable, readonly) NSString *recoveryKey; -@property (readonly, nonatomic) int64_t serial; @property (readonly, nonatomic) NSInteger maxMessageSize; @property (readonly, nonatomic) ARTRealtimeConnectionState state; @property (nullable, readonly, nonatomic) ARTErrorInfo *errorReason; @@ -22,19 +35,17 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)id_nosync; - (nullable NSString *)key_nosync; -- (int64_t)serial_nosync; - (BOOL)isActive_nosync; - (ARTRealtimeConnectionState)state_nosync; - (nullable ARTErrorInfo *)errorReason_nosync; - (nullable ARTErrorInfo *)error_nosync; -- (nullable NSString *)recoveryKey_nosync; +- (nullable NSString *)createRecoveryKey_nosync; @property (readonly, nonatomic) ARTEventEmitter *eventEmitter; @property(weak, nonatomic) ARTRealtimeInternal* realtime; // weak because realtime owns self - (void)setId:(NSString *_Nullable)newId; - (void)setKey:(NSString *_Nullable)key; -- (void)setSerial:(int64_t)serial; - (void)setMaxMessageSize:(NSInteger)maxMessageSize; - (void)setState:(ARTRealtimeConnectionState)state; - (void)setErrorReason:(ARTErrorInfo *_Nullable)errorReason; diff --git a/Source/PrivateHeaders/Ably/ARTConnectionStateChangeMetadata.h b/Source/PrivateHeaders/Ably/ARTConnectionStateChangeMetadata.h index 9f255c89d..e4124cc79 100644 --- a/Source/PrivateHeaders/Ably/ARTConnectionStateChangeMetadata.h +++ b/Source/PrivateHeaders/Ably/ARTConnectionStateChangeMetadata.h @@ -20,6 +20,8 @@ NS_SWIFT_NAME(ConnectionStateChangeMetadata) @property (nullable, nonatomic, readonly) ARTRetryAttempt *retryAttempt; +@property (assign, nonatomic) BOOL resumed; + /** Creates an `ARTConnectionStateChangeMetadata` instance whose `errorInfo` is `nil`. */ diff --git a/Source/PrivateHeaders/Ably/ARTPresenceMap.h b/Source/PrivateHeaders/Ably/ARTPresenceMap.h index a2bb0afbe..99ea6c957 100644 --- a/Source/PrivateHeaders/Ably/ARTPresenceMap.h +++ b/Source/PrivateHeaders/Ably/ARTPresenceMap.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN /// List of internal members. /// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. -@property (readonly, atomic) NSMutableSet *localMembers; +@property (readonly, atomic) NSMutableDictionary *localMembers; @property (nullable, weak) id delegate; // weak because delegates outlive their counterpart @@ -49,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)internalAdd:(ARTPresenceMessage *)message; - (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId; +- (void)reenterLocalMembers; + @end NS_ASSUME_NONNULL_END diff --git a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h index 39c0eaa0f..0722be468 100644 --- a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h +++ b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h @@ -16,7 +16,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTProtocolMessage () -@property (readwrite, nonatomic) BOOL hasConnectionSerial; @property (readonly, nonatomic) BOOL ackRequired; @property (readonly, nonatomic) BOOL hasPresence; diff --git a/Source/PrivateHeaders/Ably/ARTRealtime+Private.h b/Source/PrivateHeaders/Ably/ARTRealtime+Private.h index caf3604a3..b5a4fe6b1 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtime+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRealtime+Private.h @@ -110,6 +110,8 @@ NS_ASSUME_NONNULL_BEGIN // Message sending - (void)send:(ARTProtocolMessage *)msg sentCallback:(nullable ARTCallback)sentCallback ackCallback:(nullable ARTStatusCallback)ackCallback; +- (void)send:(ARTProtocolMessage *)msg reuseMsgSerial:(BOOL)reuseMsgSerial sentCallback:(nullable ARTCallback)sentCallback ackCallback:(nullable ARTStatusCallback)ackCallback; + @end NS_ASSUME_NONNULL_END diff --git a/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h b/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h index bb368309b..17b52a031 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h @@ -37,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, weak, nonatomic) ARTRealtimeInternal *realtime; // weak because realtime owns self @property (readonly, nonatomic) ARTRestChannelInternal *restChannel; @property (readwrite, nonatomic, nullable) NSString *attachSerial; +@property (readwrite, nonatomic, nullable) NSString *serial; // CP2b @property (readonly, nullable, getter=getClientId) NSString *clientId; @property (readonly, nonatomic) ARTEventEmitter *internalEventEmitter; @property (readonly, nonatomic) ARTEventEmitter *statesEventEmitter; diff --git a/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h b/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h index c16b32a30..05a5d157f 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h @@ -12,6 +12,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)sendPendingPresence; - (void)failPendingPresence:(ARTStatus *)status; +/* + * Helper method for enforcing RTP17g, where in addition to clientId and data, message id is required. + */ +- (void)enterWithPresenceMessageId:(NSString *)messageId clientId:(NSString *)clientId data:(id)data callback:(ARTCallback)cb; + @property (nonatomic) dispatch_queue_t queue; @property (readwrite, nonatomic) ARTPresenceAction lastPresenceAction; @property (readonly, nonatomic) NSMutableArray *pendingPresence; diff --git a/Source/PrivateHeaders/Ably/ARTRealtimeTransport.h b/Source/PrivateHeaders/Ably/ARTRealtimeTransport.h index 96dc62310..470e4dd4d 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimeTransport.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimeTransport.h @@ -69,7 +69,6 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { // All methods must be called from rest's serial queue. @property (readonly, nonatomic) NSString *resumeKey; -@property (readonly, nonatomic) NSNumber *connectionSerial; @property (readonly, nonatomic) ARTRealtimeTransportState state; @property (nullable, readwrite, nonatomic) id delegate; @property (nonatomic, readonly) ARTEventEmitter *stateEmitter; diff --git a/Source/PrivateHeaders/Ably/ARTRealtimeTransportFactory.h b/Source/PrivateHeaders/Ably/ARTRealtimeTransportFactory.h index d4ad82b7b..5ee438fba 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimeTransportFactory.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimeTransportFactory.h @@ -16,7 +16,6 @@ NS_SWIFT_NAME(RealtimeTransportFactory) - (id)transportWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(nullable NSString *)resumeKey - connectionSerial:(nullable NSNumber *)connectionSerial logger:(ARTInternalLog *)logger; @end diff --git a/Source/PrivateHeaders/Ably/ARTResumeRequestResponse.h b/Source/PrivateHeaders/Ably/ARTResumeRequestResponse.h deleted file mode 100644 index 2b5aab2e6..000000000 --- a/Source/PrivateHeaders/Ably/ARTResumeRequestResponse.h +++ /dev/null @@ -1,72 +0,0 @@ -@import Foundation; - -@class ARTProtocolMessage; -@class ARTErrorInfo; -@protocol ARTErrorChecker; - -/** - The type of the Realtime system’s response to a resume request, as described by RTN15c. - */ -typedef NS_ENUM(NSUInteger, ARTResumeRequestResponseType) { - /** - RTN15c6: "This indicates that the resume attempt was valid. The client library should move all channels that were in the `ATTACHING`, `ATTACHED`, or `SUSPENDED` states to the `ATTACHING` state, and initiate an `RTL4c` attach sequence for each. The connection should also process any messages queued per `RTL6c2` (there is no need to wait for the attaches to finish before processing queued messages)." - */ - ARTResumeRequestResponseTypeValid, - - /** - RTN15c7: "In this case, the resume was invalid, and the error indicates the cause. The `error` should be set as the `reason` in the `CONNECTED` event, and as the `Connection#errorReason`. The internal `msgSerial` counter should be reset so that the first message published to Ably will contain a `msgSerial` of `0`. The rest of the process is the same as for `RTN16c6`: The client library should move all channels that were in the `ATTACHING`, `ATTACHED`, or `SUSPENDED` states to the `ATTACHING` state, and initiate an `RTL4c` attach sequence for each. The connection should also process any messages queued per `RTL6c2`." - */ - ARTResumeRequestResponseTypeInvalid, - - /** - RTN15c5: "The transport will be closed by the server. The spec described in RTN15h must be followed for a connection being resumed with a token error" - */ - ARTResumeRequestResponseTypeTokenError, - - /** - RTN15c4: "Any other `ERROR` `ProtocolMessage` indicating a fatal error in the connection. The server will close the transport immediately after. The client should transition to the `FAILED` state triggering all attached channels to transition to the `FAILED` state as well. Additionally the `Connection#errorReason` will be set should be set with the error received from Ably" - */ - ARTResumeRequestResponseTypeFatalError, - - /** - The response from the Realtime system was not one of the expected responses. - */ - ARTResumeRequestResponseTypeUnknown, -} NS_SWIFT_NAME(ResumeRequestResponse.ResponseType); - -NS_ASSUME_NONNULL_BEGIN - -/** - The Realtime system’s response to a resume request, as described by RTN15c. - */ -NS_SWIFT_NAME(ResumeRequestResponse) -@interface ARTResumeRequestResponse: NSObject - -- (instancetype)init NS_UNAVAILABLE; - -/** - Creates an `ARTResumeRequestResponse` describing the resume request response that the Realtime system has communicated through the use of a protocol message. - - @param currentConnectionID The ID of the connection that we are trying to resume. - @param protocolMessage The first protocol message received on a transport which is trying to resume a connection with ID `currentConnectionID`. - @param errorChecker An error checker which will be used to check whether an error is a token error. - */ -- (instancetype)initWithCurrentConnectionID:(NSString *)currentConnectionID - protocolMessage:(ARTProtocolMessage *)protocolMessage - errorChecker:(id)errorChecker NS_DESIGNATED_INITIALIZER; - -/** - The type of the response. This indicates how a client is meant to act upon receiving this response. - */ -@property (nonatomic, readonly) ARTResumeRequestResponseType type; - -/** - The error that the Realtime system included in its response. - - Non-nil if and only if `type` is `ARTResumeRequestResponseTypeInvalid`, `ARTResumeRequestResponseTypeTokenError`, or `ARTResumeRequestResponseTypeFatalError`. - */ -@property (nullable, nonatomic, readonly) ARTErrorInfo *error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/PrivateHeaders/Ably/ARTTypes+Private.h b/Source/PrivateHeaders/Ably/ARTTypes+Private.h index ba0cd2561..72c753b52 100644 --- a/Source/PrivateHeaders/Ably/ARTTypes+Private.h +++ b/Source/PrivateHeaders/Ably/ARTTypes+Private.h @@ -15,12 +15,18 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly, nullable) ARTRetryAttempt *retryAttempt; +/** + * Indicates whether the connection was resumed. + */ +@property (assign, nonatomic) BOOL resumed; + - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(nullable ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn - retryAttempt:(nullable ARTRetryAttempt *)retryAttempt; + retryAttempt:(nullable ARTRetryAttempt *)retryAttempt + resumed:(BOOL)resumed; @end diff --git a/Source/PrivateHeaders/Ably/ARTWebSocketTransport+Private.h b/Source/PrivateHeaders/Ably/ARTWebSocketTransport+Private.h index c23657ae8..5fc3d8739 100644 --- a/Source/PrivateHeaders/Ably/ARTWebSocketTransport+Private.h +++ b/Source/PrivateHeaders/Ably/ARTWebSocketTransport+Private.h @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readwrite, nonatomic, nullable) id websocket; @property (readwrite, nonatomic, nullable) NSURL *websocketURL; -- (NSURL *)setupWebSocket:(NSDictionary *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *_Nullable)resumeKey connectionSerial:(NSNumber *_Nullable)connectionSerial; +- (NSURL *)setupWebSocket:(NSDictionary *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *_Nullable)resumeKey; - (void)setState:(ARTRealtimeTransportState)state; diff --git a/Source/PrivateHeaders/Ably/ARTWebSocketTransport.h b/Source/PrivateHeaders/Ably/ARTWebSocketTransport.h index 62e1e9575..ab5108b68 100644 --- a/Source/PrivateHeaders/Ably/ARTWebSocketTransport.h +++ b/Source/PrivateHeaders/Ably/ARTWebSocketTransport.h @@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTWebSocketTransport : NSObject - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(nullable NSString *)resumeKey connectionSerial:(nullable NSNumber *)connectionSerial logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(nullable NSString *)resumeKey logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory NS_DESIGNATED_INITIALIZER; @property (readonly, nonatomic) NSString *resumeKey; -@property (readonly, nonatomic) NSNumber *connectionSerial; @end diff --git a/Source/include/Ably.modulemap b/Source/include/Ably.modulemap index a33967d55..c887ff09a 100644 --- a/Source/include/Ably.modulemap +++ b/Source/include/Ably.modulemap @@ -81,7 +81,6 @@ framework module Ably { header "Ably/ARTNSURL+ARTUtils.h" header "Ably/ARTNSMutableURLRequest+ARTUtils.h" header "Ably/ARTErrorChecker.h" - header "Ably/ARTResumeRequestResponse.h" header "Ably/ARTJitterCoefficientGenerator.h" header "Ably/ARTRetryDelayCalculator.h" header "Ably/ARTBackoffRetryDelayCalculator.h" diff --git a/Source/include/Ably/ARTConnection.h b/Source/include/Ably/ARTConnection.h index 7011495f9..aa25e2f56 100644 --- a/Source/include/Ably/ARTConnection.h +++ b/Source/include/Ably/ARTConnection.h @@ -4,6 +4,7 @@ @class ARTRealtime; @class ARTEventEmitter; +@class ARTConnectionRecoveryKey; NS_ASSUME_NONNULL_BEGIN @@ -22,16 +23,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, readonly) NSString *key; -/** - * The recovery key string can be used by another client to recover this connection's state in the recover client options property. See [connection state recover options](https://ably.com/docs/realtime/connection#connection-state-recover-options) for more information. - */ -@property (nullable, readonly) NSString *recoveryKey; - -/** - * The serial number of the last message to be received on this connection, used automatically by the library when recovering or resuming a connection. When recovering a connection explicitly, the `recoveryKey` is used in the recover client options as it contains both the key and the last message serial. - */ -@property (readonly) int64_t serial; - /** * The maximum message size is an attribute of an Ably account and enforced by Ably servers. `maxMessageSize` indicates the maximum message size allowed by the Ably account this connection is using. Overrides the default value of `+[ARTDefault maxMessageSize]`. */ @@ -47,6 +38,17 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, readonly) ARTErrorInfo *errorReason; +/** + * This property is deprecated and will be removed in future versions of the library. You should use `createRecoveryKey` method instead. + */ +@property (nullable, readonly) NSString *recoveryKey DEPRECATED_MSG_ATTRIBUTE("Use `createRecoveryKey` method instead."); + +/** + * The recovery key string can be used by another client to recover this connection's state in the recover client options property. See [connection state recover options](https://ably.com/docs/realtime/connection#connection-state-recover-options) for more information. + * This will return `nil` if connection is in `CLOSED`, `CLOSING`, `FAILED`, or `SUSPENDED` states, or when it does not have a connection `key` (for example, it has not yet become connected). + */ +- (nullable NSString *)createRecoveryKey; + /** * Explicitly calling `connect` is unnecessary unless the `ARTClientOptions.autoConnect` is `false`. Unless already connected or connecting, this method causes the connection to open, entering the `ARTRealtimeConnectionState.ARTRealtimeConnecting` state. */ diff --git a/Source/include/Ably/ARTProtocolMessage.h b/Source/include/Ably/ARTProtocolMessage.h index e7ecc36cf..99d9186ee 100644 --- a/Source/include/Ably/ARTProtocolMessage.h +++ b/Source/include/Ably/ARTProtocolMessage.h @@ -52,7 +52,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, readwrite, nonatomic) NSString *channelSerial; @property (nullable, readwrite, nonatomic) NSString *connectionId; @property (nullable, readwrite, nonatomic, getter=getConnectionKey) NSString *connectionKey; -@property (readwrite, nonatomic) int64_t connectionSerial; @property (nullable, readwrite, nonatomic) NSNumber *msgSerial; @property (nullable, readwrite, nonatomic) NSDate *timestamp; @property (nullable, readwrite, nonatomic) NSArray *messages; diff --git a/Test/Test Utilities/TestProxyTransportFactory.swift b/Test/Test Utilities/TestProxyTransportFactory.swift index d5299cfc5..d115ec6ef 100644 --- a/Test/Test Utilities/TestProxyTransportFactory.swift +++ b/Test/Test Utilities/TestProxyTransportFactory.swift @@ -7,7 +7,9 @@ class TestProxyTransportFactory: RealtimeTransportFactory { // This value will be used by all TestProxyTransportFactory instances created by this factory (including those created before this property is updated). var networkConnectEvent: ((ARTRealtimeTransport, URL) -> Void)? - func transport(withRest rest: ARTRestInternal, options: ARTClientOptions, resumeKey: String?, connectionSerial: NSNumber?, logger: InternalLog) -> ARTRealtimeTransport { + var transportCreatedEvent: ((ARTRealtimeTransport) -> Void)? + + func transport(withRest rest: ARTRestInternal, options: ARTClientOptions, resumeKey: String?, logger: InternalLog) -> ARTRealtimeTransport { let webSocketFactory = WebSocketFactory() let testProxyTransport = TestProxyTransport( @@ -15,13 +17,14 @@ class TestProxyTransportFactory: RealtimeTransportFactory { rest: rest, options: options, resumeKey: resumeKey, - connectionSerial: connectionSerial, logger: logger, webSocketFactory: webSocketFactory ) webSocketFactory.testProxyTransport = testProxyTransport + transportCreatedEvent?(testProxyTransport) + return testProxyTransport } diff --git a/Test/Test Utilities/TestUtilities.swift b/Test/Test Utilities/TestUtilities.swift index 0fcb4f643..d9664fb93 100644 --- a/Test/Test Utilities/TestUtilities.swift +++ b/Test/Test Utilities/TestUtilities.swift @@ -165,16 +165,14 @@ class AblyTests { return protocolMessage } - class func newPresenceProtocolMessage(_ channel: String, action: ARTPresenceAction, clientId: String) -> ARTProtocolMessage { + class func newPresenceProtocolMessage(id: String, channel: String, action: ARTPresenceAction, clientId: String, connectionId: String) -> ARTProtocolMessage { let protocolMessage = ARTProtocolMessage() protocolMessage.action = .presence protocolMessage.channel = channel protocolMessage.timestamp = Date() - let presenceMessage = ARTPresenceMessage() - presenceMessage.action = action - presenceMessage.clientId = clientId - presenceMessage.timestamp = Date() - protocolMessage.presence = [presenceMessage] + protocolMessage.presence = [ + ARTPresenceMessage(clientId: clientId, action: action, connectionId: connectionId, id: id, timestamp: Date()) + ] return protocolMessage } @@ -183,12 +181,13 @@ class AblyTests { let transportFactory: TestProxyTransportFactory } - class func newRealtime(_ options: ARTClientOptions) -> RealtimeTestEnvironment { + class func newRealtime(_ options: ARTClientOptions, onTransportCreated event: ((ARTRealtimeTransport) -> Void)? = nil) -> RealtimeTestEnvironment { let modifiedOptions = options.copy() as! ARTClientOptions let autoConnect = modifiedOptions.autoConnect modifiedOptions.autoConnect = false let transportFactory = TestProxyTransportFactory() + transportFactory.transportCreatedEvent = event modifiedOptions.testOptions.transportFactory = transportFactory let realtime = ARTRealtime(options: modifiedOptions) realtime.internal.setReachabilityClass(TestReachability.self) @@ -1113,9 +1112,9 @@ class TestProxyTransport: ARTWebSocketTransport { return _factory } - init(factory: TestProxyTransportFactory, rest: ARTRestInternal, options: ARTClientOptions, resumeKey: String?, connectionSerial: NSNumber?, logger: InternalLog, webSocketFactory: WebSocketFactory) { + init(factory: TestProxyTransportFactory, rest: ARTRestInternal, options: ARTClientOptions, resumeKey: String?, logger: InternalLog, webSocketFactory: WebSocketFactory) { self._factory = factory - super.init(rest: rest, options: options, resumeKey: resumeKey, connectionSerial: connectionSerial, logger: logger, webSocketFactory: webSocketFactory) + super.init(rest: rest, options: options, resumeKey: resumeKey, logger: logger, webSocketFactory: webSocketFactory) } fileprivate(set) var lastUrl: URL? @@ -1227,6 +1226,16 @@ class TestProxyTransport: ARTWebSocketTransport { self.replacingAcksWithNacks = nil } } + + func emulateTokenRevokationBeforeConnected() { + setBeforeIncomingMessageModifier { protocolMessage in + if protocolMessage.action == .connected { + protocolMessage.action = .disconnected + protocolMessage.error = .create(withCode: ARTErrorCode.tokenRevoked.intValue, status: 401, message: "Test token revokation") + } + return protocolMessage + } + } // MARK: ARTWebSocket @@ -1325,8 +1334,8 @@ class TestProxyTransport: ARTWebSocketTransport { } } - override func setupWebSocket(_ params: [String: URLQueryItem], with options: ARTClientOptions, resumeKey: String?, connectionSerial: NSNumber?) -> URL { - let url = super.setupWebSocket(params, with: options, resumeKey: resumeKey, connectionSerial: connectionSerial) + override func setupWebSocket(_ params: [String: URLQueryItem], with options: ARTClientOptions, resumeKey: String?) -> URL { + let url = super.setupWebSocket(params, with: options, resumeKey: resumeKey) lastUrl = url return url } @@ -1440,7 +1449,6 @@ class TestProxyTransport: ARTWebSocketTransport { msg.action = .connected msg.connectionId = "x-xxxxxxxx" msg.connectionKey = "xxxxxxx-xxxxxxxxxxxxxx-xxxxxxxx" - msg.connectionSerial = -1 msg.connectionDetails = ARTConnectionDetails(clientId: clientId, connectionKey: "a8c10!t-3D0O4ejwTdvLkl-b33a8c10", maxMessageSize: 16384, maxFrameSize: 262144, maxInboundRate: 250, connectionStateTtl: 60, serverId: "testServerId", maxIdleInterval: 15000) super.receive(msg) } @@ -1602,6 +1610,10 @@ extension ARTRealtime { self.internal.options.testOptions.transportFactory as? TestProxyTransportFactory } + func simulateLostConnection() { + self.internal.onDisconnected() + } + func simulateLostConnectionAndState() { //1. Abruptly disconnect //2. Change the `Connection#id` and `Connection#key` before the client @@ -1659,7 +1671,23 @@ extension ARTRealtime { } simulateRestoreInternetConnection(after: seconds, transportFactory: transportFactory) } - + + @discardableResult + func waitUntilConnected() -> Bool { + var connected = false + waitUntil(timeout: testTimeout) { done in + self.connection.once(.connected) { _ in + connected = true + done() + } + } + return connected + } + + func waitForPendingMessages() { + expect(self.internal.pendingMessages).toEventually(haveCount(0),timeout: testTimeout) + } + func overrideConnectionStateTTL(_ ttl: TimeInterval) -> HookToken { return self.internal.testSuite_injectIntoMethod(before: NSSelectorFromString("connectionStateTtl")) { self.internal.connectionStateTtl = ttl diff --git a/Test/Tests/ARTDefaultTests.swift b/Test/Tests/ARTDefaultTests.swift index b6ec36314..28297b9b8 100644 --- a/Test/Tests/ARTDefaultTests.swift +++ b/Test/Tests/ARTDefaultTests.swift @@ -5,7 +5,7 @@ import Ably.ARTDefault // System under Test class ARTDefaultTests: XCTestCase { func testVersions() { - XCTAssertEqual(ARTDefault.apiVersion(), "1.2") + XCTAssertEqual(ARTDefault.apiVersion(), "2") XCTAssertEqual(ARTDefault.libraryVersion(), "1.2.24") } } diff --git a/Test/Tests/RealtimeClientChannelTests.swift b/Test/Tests/RealtimeClientChannelTests.swift index d5bb95de7..1da4b3ef6 100644 --- a/Test/Tests/RealtimeClientChannelTests.swift +++ b/Test/Tests/RealtimeClientChannelTests.swift @@ -847,8 +847,7 @@ class RealtimeClientChannelTests: XCTestCase { } // RTL3d - https://github.com/ably/ably-cocoa/issues/881 - // RTL2f - func test__015__Channel__connection_state__should_attach_successfully_and_remain_attached_when_the_connection_state_without_a_successful_recovery_gets_CONNECTED() throws { + func test__015__Channel__connection_state__should_attach_successfully_and_remain_attached_after_the_connection_goes_from_SUSPENDED_to_CONNECTED() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.disconnectedRetryTimeout = 0.5 @@ -878,6 +877,8 @@ class RealtimeClientChannelTests: XCTestCase { } client.connect() } + + let oldConnectionId = client.connection.id waitUntil(timeout: testTimeout) { done in channel.once(.suspended) { stateChange in @@ -890,14 +891,9 @@ class RealtimeClientChannelTests: XCTestCase { client.simulateNoInternetConnection(transportFactory: transportFactory) } - AblyTests.queue.async { - // Do not resume - client.simulateLostConnectionAndState() - } - waitUntil(timeout: testTimeout) { done in client.connection.once(.connected) { stateChange in - XCTAssertEqual(stateChange.reason?.code, ARTErrorCode.unableToRecoverConnectionExpired.intValue) // didn't resumed + XCTAssertNotEqual(oldConnectionId, client.connection.id) // didn't resumed done() } client.simulateRestoreInternetConnection(after: 1.0, transportFactory: transportFactory) @@ -905,7 +901,6 @@ class RealtimeClientChannelTests: XCTestCase { waitUntil(timeout: testTimeout) { done in channel.once(.attached) { stateChange in - XCTAssertFalse(stateChange.resumed) // RTL2f (resumed is false when the channel is ATTACHED following a failed connection recovery) XCTAssertNil(stateChange.reason) channel.on(.suspended) { _ in fail("Should not reach SUSPENDED state") @@ -1203,6 +1198,60 @@ class RealtimeClientChannelTests: XCTestCase { expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .attached }.count, 1) } + + // RTL4c1 + func test__202__Channel__attach__protocol_message_channelSerial_must_be_set_to_channelSerial_of_the_most_recent_protocol_message_or_omitted_if_no_previous_protocol_message_received() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + options.autoConnect = false + let transportFactory = TestProxyTransportFactory() + options.testOptions.transportFactory = transportFactory + + let client = ARTRealtime(options: options) + client.internal.setReachabilityClass(TestReachability.self) + client.connect() + defer { client.dispose(); client.close() } + + let latestAttachProtocolMessage: () throws -> ARTProtocolMessage = { + let transport = try XCTUnwrap(client.internal.transport as? TestProxyTransport) + let protocolAttachMessagesSent = transport.protocolMessagesSent.filter { $0.action == .attach } + return try XCTUnwrap(protocolAttachMessagesSent.last) + } + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + + let channel = client.channels.get(test.uniqueChannelName()) + channel.attach() + + expect(channel.state).to(equal(ARTRealtimeChannelState.attaching)) + + let firstProtocolAttachMessage = try latestAttachProtocolMessage() + expect(firstProtocolAttachMessage.channelSerial).to(beNil()) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe { message in + expect(message.data as? String).to(equal("message")) + partialDone() + } + channel.publish(nil, data: "message") { error in + expect(error).to(beNil()) + partialDone() + } + } + expect(channel.internal.serial).toEventuallyNot(beNil()) + + client.simulateNoInternetConnection(transportFactory: transportFactory) + client.simulateRestoreInternetConnection(after: 0.1, transportFactory: transportFactory) + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.disconnected), timeout: testTimeout) + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + + let secondProtocolAttachMessage = try latestAttachProtocolMessage() + expect(secondProtocolAttachMessage.channelSerial).to(equal(channel.internal.serial)) + } // RTL4e func test__031__Channel__attach__should_transition_the_channel_state_to_FAILED_if_the_user_does_not_have_sufficient_permissions() throws { @@ -4162,6 +4211,88 @@ class RealtimeClientChannelTests: XCTestCase { XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) } + + // RTL15 + + // RTL15b + func test__200__channel_serial_is_updated_whenever_a_protocol_message_with_either_message_presence_or_attached_actions_is_received_in_a_channel() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + let channel = client.channels.get(test.uniqueChannelName()) + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + + let transport = try XCTUnwrap(client.internal.transport as? TestProxyTransport) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.attach { error in + expect(error).to(beNil()) + let attachMessage = transport.protocolMessagesReceived.filter { $0.action == .attached }[0] + if attachMessage.channelSerial != nil { + expect(attachMessage.channelSerial).to(equal(channel.internal.serial)) + } + partialDone() + + channel.subscribe { message in + let messageMessage = transport.protocolMessagesReceived.filter { $0.action == .message }[0] + if messageMessage.channelSerial != nil { + expect(messageMessage.channelSerial).to(equal(channel.internal.serial)) + } + channel.presence.enterClient("client1", data: "Hey") + partialDone() + } + channel.presence.subscribe { presenceMessage in + let presenceMessage = transport.protocolMessagesReceived.filter { $0.action == .presence }[0] + if presenceMessage.channelSerial != nil { + expect(presenceMessage.channelSerial).to(equal(channel.internal.serial)) + } + partialDone() + } + channel.publish([ARTMessage()]) + } + } + } + + // RTP5a1 + func test__201__channel_serial_is_cleared_whenever_a_channel_entered_into_detached_suspended_or_failed_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + let channel = client.channels.get(test.uniqueChannelName()) + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + + // Case for detached + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(channel.internal.serial).toNot(beNil()) + + channel.detach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.detached), timeout: testTimeout) + expect(channel.internal.serial).to(beNil()) + + // Case for suspended + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(channel.internal.serial).toNot(beNil()) + + channel.internal.setSuspended(.init(state: .ok)) + expect(channel.state).to(equal(ARTRealtimeChannelState.suspended)) + expect(channel.internal.serial).to(beNil()) + + // Case for failed + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(channel.internal.serial).toNot(beNil()) + + channel.internal.setFailed(.init(state: .ok)) + expect(channel.state).to(equal(ARTRealtimeChannelState.failed)) + expect(channel.internal.serial).to(beNil()) + } // RTL16 diff --git a/Test/Tests/RealtimeClientConnectionTests.swift b/Test/Tests/RealtimeClientConnectionTests.swift index 27c09c92e..129725578 100644 --- a/Test/Tests/RealtimeClientConnectionTests.swift +++ b/Test/Tests/RealtimeClientConnectionTests.swift @@ -351,7 +351,7 @@ class RealtimeClientConnectionTests: XCTestCase { // This test should not directly validate version against ARTDefault.version(), as // ultimately the version header has been derived from that value. - expect(webSocketTransport.websocketURL?.query).to(haveParam("v", withValue: "1.2")) + expect(webSocketTransport.websocketURL?.query).to(haveParam("v", withValue: "2")) done() } @@ -1262,57 +1262,6 @@ class RealtimeClientConnectionTests: XCTestCase { } } - func test__037__Connection__ACK_and_NACK__should_trigger_the_failure_callback_for_the_remaining_pending_messages_if__lost_connection_state() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - options.autoConnect = false - options.testOptions.transportFactory = TestProxyTransportFactory() - let client = ARTRealtime(options: options) - client.connect() - defer { - client.dispose() - client.close() - } - - let channel = client.channels.get(test.uniqueChannelName()) - - let transport = client.internal.transport as! TestProxyTransport - transport.actionsIgnored += [.ack, .nack] - - waitUntil(timeout: testTimeout) { done in - channel.attach { _ in - done() - } - } - - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(3, done: done) - - channel.publish(nil, data: "message") { error in - guard let error = error else { - fail("Error is nil"); return - } - XCTAssertEqual(error.code, ARTErrorCode.unableToRecoverConnectionExpired.intValue) - expect(error.message).to(contain("Unable to recover connection")) - partialDone() - } - - let oldConnectionId = client.connection.id! - - // Wait until the message is pushed to Ably first - delay(1.0) { - client.connection.once(.disconnected) { _ in - partialDone() - } - client.connection.once(.connected) { _ in - XCTAssertNotEqual(client.connection.id, oldConnectionId) - partialDone() - } - client.simulateLostConnectionAndState() - } - } - } - // RTN8 // RTN8a @@ -1390,6 +1339,91 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertEqual(ids.count, max) } + + // RTN8c, RTN9c (connection's `id` and `key` respectively) + + func test__139__Connection__connection_id_and_key__should_be_nil_when_sdk_is_in_CLOSING_and_CLOSED_states() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + client.connection.on { stateChange in + expect(stateChange.reason).to(beNil()) + if stateChange.current == .connected { + expect(client.connection.id).toNot(beNil()) + expect(client.connection.key).toNot(beNil()) + client.internal.close() + partialDone() + } + else if stateChange.current == .closing { + expect(client.connection.id).to(beNil()) + expect(client.connection.key).to(beNil()) + partialDone() + } + else if stateChange.current == .closed { + expect(client.connection.id).to(beNil()) + expect(client.connection.key).to(beNil()) + partialDone() + } + } + } + } + + func test__140__Connection__connection_id_and_key__should_be_nil_when_sdk_is_in_SUSPENDED_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.on { stateChange in + expect(stateChange.reason).to(beNil()) + if stateChange.current == .connected { + expect(client.connection.id).toNot(beNil()) + expect(client.connection.key).toNot(beNil()) + client.internal.onSuspended() + partialDone() + } + else if stateChange.current == .suspended { + expect(client.connection.id).to(beNil()) + expect(client.connection.key).to(beNil()) + partialDone() + } + } + } + } + + func test__141__Connection__connection_id_and_key__should_be_nil_when_sdk_is_in_FAILED_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.on { stateChange in + expect(stateChange.reason).to(beNil()) + if stateChange.current == .connected { + expect(client.connection.id).toNot(beNil()) + expect(client.connection.key).toNot(beNil()) + client.internal.onError(ARTProtocolMessage()) + partialDone() + } + else if stateChange.current == .failed { + expect(client.connection.id).to(beNil()) + expect(client.connection.key).to(beNil()) + partialDone() + } + } + } + } // RTN9 @@ -1466,114 +1500,6 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertEqual(keys.count, max) } - // RTN10 - - // RTN10a - func test__042__Connection__serial__should_be_minus_1_once_connected() throws { - let test = Test() - let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) - defer { - client.dispose() - client.close() - } - waitUntil(timeout: testTimeout) { done in - client.connection.on { stateChange in - let state = stateChange.current - let error = stateChange.reason - XCTAssertNil(error) - if state == .connected { - XCTAssertEqual(client.connection.serial, -1) - done() - } - } - } - } - - // RTN10b - func test__043__Connection__serial__should_not_update_when_a_message_is_sent_but_increments_by_one_when_ACK_is_received() throws { - let test = Test() - let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) - defer { - client.dispose() - client.close() - } - let channel = client.channels.get(test.uniqueChannelName()) - - XCTAssertEqual(client.connection.serial, -1) - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) - XCTAssertEqual(client.connection.serial, -1) - - for index in 0 ... 3 { - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.publish(nil, data: "message", callback: { errorInfo in - XCTAssertNil(errorInfo) - partialDone() - }) - channel.subscribe { _ in - // Updated - XCTAssertEqual(client.connection.serial, Int64(index)) - channel.unsubscribe() - partialDone() - } - // Not updated - XCTAssertEqual(client.connection.serial, Int64(index - 1)) - } - } - } - - func test__044__Connection__serial__should_have_last_known_connection_serial_from_restored_connection() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - let channelName = test.uniqueChannelName() - let channel = client.channels.get(channelName) - - // Attach first to avoid bundling publishes in the same ProtocolMessage. - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - - for _ in 1 ... 5 { - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.publish(nil, data: "message", callback: { errorInfo in - XCTAssertNil(errorInfo) - partialDone() - }) - channel.subscribe { _ in - channel.unsubscribe() - partialDone() - } - } - } - let lastSerial = client.connection.serial - XCTAssertEqual(lastSerial, 4) - - options.recover = client.connection.recoveryKey - client.internal.onError(AblyTests.newErrorProtocolMessage()) - - let recoveredClient = ARTRealtime(options: options) - defer { recoveredClient.close() } - expect(recoveredClient.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) - - waitUntil(timeout: testTimeout) { done in - XCTAssertEqual(recoveredClient.connection.serial, lastSerial) - let recoveredChannel = recoveredClient.channels.get(channelName) - recoveredChannel.publish(nil, data: "message", callback: { errorInfo in - XCTAssertNil(errorInfo) - }) - recoveredChannel.subscribe { _ in - XCTAssertEqual(recoveredClient.connection.serial, lastSerial + 1) - recoveredChannel.unsubscribe() - done() - } - } - } - // RTN11b func test__007__Connection__should_make_a_new_connection_with_a_new_transport_instance_if_the_state_is_CLOSING() throws { let test = Test() @@ -2578,61 +2504,6 @@ class RealtimeClientConnectionTests: XCTestCase { // RTN15 - // RTN15a - func test__063__Connection__connection_failures_once_CONNECTED__should_not_receive_published_messages_until_the_connection_reconnects_successfully() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - options.autoConnect = false - - let client1 = ARTRealtime(options: options) - defer { client1.close() } - - let channelName = test.uniqueChannelName() - let channel1 = client1.channels.get(channelName) - - var states = [ARTRealtimeConnectionState]() - client1.connection.on { stateChange in - states = states + [stateChange.current] - } - client1.connect() - - let client2 = ARTRealtime(options: options) - client2.connect() - defer { client2.close() } - let channel2 = client2.channels.get(channelName) - - channel1.subscribe { _ in - fail("Shouldn't receive the messsage") - } - - expect(channel1.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - - let firstConnection: (id: String, key: String) = (client1.connection.id!, client1.connection.key!) - - // Connection state cannot be resumed - client1.simulateLostConnectionAndState() - - channel2.publish(nil, data: "message") { errorInfo in - XCTAssertNil(errorInfo) - } - - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - client1.connection.once(.connecting) { _ in - XCTAssertTrue(client1.internal.resuming) - partialDone() - } - client1.connection.once(.connected) { _ in - XCTAssertFalse(client1.internal.resuming) - XCTAssertNotEqual(client1.connection.id, firstConnection.id) - XCTAssertNotEqual(client1.connection.key, firstConnection.key) - partialDone() - } - } - - expect(states).toEventually(equal([.connecting, .connected, .disconnected, .connecting, .connected]), timeout: testTimeout) - } - // RTN15a func test__064__Connection__connection_failures_once_CONNECTED__if_a_Connection_transport_is_disconnected_unexpectedly_or_if_a_token_expires__then_the_Connection_manager_will_immediately_attempt_to_reconnect() throws { let test = Test() @@ -2664,8 +2535,8 @@ class RealtimeClientConnectionTests: XCTestCase { // RTN15b - // RTN15b1, RTN15b2 - func test__067__Connection__connection_failures_once_CONNECTED__reconnects_to_the_websocket_endpoint_with_additional_querystring_params__resume_is_the_private_connection_key_and_connection_serial_is_the_most_recent_ProtocolMessage_connectionSerial_received() throws { + // RTN15b1 + func test__067__Connection__connection_failures_once_CONNECTED__reconnects_to_the_websocket_endpoint_with_additional_querystring_params__resume_is_the_private_connection_key_from_the_most_recent_CONNECTED_ProtocolMessage_received() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client @@ -2673,7 +2544,6 @@ class RealtimeClientConnectionTests: XCTestCase { expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) let expectedConnectionKey = client.connection.key! - let expectedConnectionSerial = client.connection.serial client.internal.onDisconnected() waitUntil(timeout: testTimeout) { done in @@ -2681,145 +2551,186 @@ class RealtimeClientConnectionTests: XCTestCase { let transport = client.internal.transport as! TestProxyTransport let query = transport.lastUrl!.query expect(query).to(haveParam("resume", withValue: expectedConnectionKey)) - expect(query).to(haveParam("connectionSerial", withValue: "\(expectedConnectionSerial)")) done() } } } // RTN15c - - // RTN15c1 - func test__068__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_same_connectionId_as_the_current_client__and_no_error() throws { + + // RTN15c6 (attaching) + func test__068a__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_same_connectionId_as_the_current_client__and_no_error() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } let channel = client.channels.get(test.uniqueChannelName()) - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + XCTAssertTrue(client.waitUntilConnected()) let expectedConnectionId = client.connection.id - client.internal.onDisconnected() - channel.attach() + let transport = client.internal.transport as! TestProxyTransport + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) + + client.internal.onDisconnected() + channel.publish(nil, data: "queued message") expect(client.internal.queuedMessages).toEventually(haveCount(1), timeout: testTimeout) - + + XCTAssertEqual(channel.state, ARTRealtimeChannelState.attaching, "Channel should be still attaching.") waitUntil(timeout: testTimeout) { done in client.connection.once(.connected) { stateChange in let transport = client.internal.transport as! TestProxyTransport let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] XCTAssertEqual(connectedPM.connectionId, expectedConnectionId) XCTAssertNil(stateChange.reason) + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) + XCTAssertEqual(channel.state, ARTRealtimeChannelState.attaching, "Channel should be attaching now.") done() } } - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - expect(client.internal.queuedMessages).toEventually(haveCount(0), timeout: testTimeout) + + expect(client.internal.queuedMessages).toEventually(haveCount(0), timeout: testTimeout) // RTL6c2 } - - // RTN15c2 - func test__069__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_same_connectionId_as_the_current_client_and_an_non_fatal_error() throws { + + // RTN15c6 (attached, detaching) + func test__068b__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_same_connectionId_as_the_current_client__and_no_error() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } - let channel = client.channels.get(test.uniqueChannelName()) - - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + let channel1 = client.channels.get(test.uniqueChannelName()) + let channel2 = client.channels.get(test.uniqueChannelName(prefix: "second_")) + XCTAssertTrue(client.waitUntilConnected()) + expect(channel1.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(channel2.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) let expectedConnectionId = client.connection.id - client.internalAsync { _internal in - _internal.onDisconnected() - } - channel.attach() - channel.publish(nil, data: "queued message") + let transport = client.internal.transport as! TestProxyTransport + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 2, "Should contain 2 attach messages.") // for channel 1 and 2 + + channel2.detach() + XCTAssertEqual(channel2.state, ARTRealtimeChannelState.detaching) + + client.internal.onDisconnected() + + channel1.publish(nil, data: "queued message") expect(client.internal.queuedMessages).toEventually(haveCount(1), timeout: testTimeout) - - client.connection.once(.connecting) { _ in - client.internalSync { _internal in - let transport = _internal.transport as! TestProxyTransport - transport.setBeforeIncomingMessageModifier { protocolMessage in - if protocolMessage.action == .connected { - protocolMessage.error = .create(withCode: 0, message: "Injected error") - } else if protocolMessage.action == .attached { - protocolMessage.error = .create(withCode: 0, message: "Channel injected error") - } - return protocolMessage - } - } - } - + + XCTAssertEqual(channel1.state, ARTRealtimeChannelState.attached, "Channel should be still attached.") waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.connected) { stateChange in - XCTAssertEqual(stateChange.reason?.message, "Injected error") - XCTAssertTrue(client.connection.errorReason === stateChange.reason) let transport = client.internal.transport as! TestProxyTransport let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] XCTAssertEqual(connectedPM.connectionId, expectedConnectionId) - XCTAssertEqual(client.connection.id, expectedConnectionId) - partialDone() - } - channel.once(.attached) { stateChange in - guard let error = stateChange.reason else { - fail("Reason error is nil"); done(); return - } - XCTAssertEqual(error.message, "Channel injected error") - XCTAssertTrue(channel.errorReason === error) - partialDone() + XCTAssertNil(stateChange.reason) + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) + XCTAssertEqual(channel1.state, ARTRealtimeChannelState.attaching, "Channel should be attaching now.") + XCTAssertNotEqual(channel2.state, ARTRealtimeChannelState.attaching, "Channel 2 should not be attaching.") + done() } } - - expect(client.internal.queuedMessages).toEventually(haveCount(0), timeout: testTimeout) + + expect(client.internal.queuedMessages).toEventually(haveCount(0), timeout: testTimeout) // RTL6c2 } - - // RTN15c3 - func test__070__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_a_new_connectionId_and_an_error() throws { + + // RTN15c6 (suspended) + func test__068c__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_same_connectionId_as_the_current_client__and_no_error() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } let channel = client.channels.get(test.uniqueChannelName()) + XCTAssertTrue(client.waitUntilConnected()) + let expectedConnectionId = client.connection.id + + let transport = client.internal.transport as! TestProxyTransport + transport.actionsIgnored += [.attached] + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) + + XCTAssertEqual(channel.state, ARTRealtimeChannelState.attaching) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.suspended), timeout: testTimeout) + + client.internal.onDisconnected() + + channel.publish(nil, data: "queued message") + XCTAssertEqual(client.internal.queuedMessages.count, 0) // should fail message immidiatly if channel is suspended + waitUntil(timeout: testTimeout) { done in - channel.attach { error in - XCTAssertNil(error) + client.connection.once(.connected) { stateChange in + let transport = client.internal.transport as! TestProxyTransport + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + XCTAssertEqual(connectedPM.connectionId, expectedConnectionId) + XCTAssertNil(stateChange.reason) + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) + XCTAssertEqual(channel.state, ARTRealtimeChannelState.attaching, "Channel should be attaching now.") done() } } + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + } + + // RTN15c7 + func test__069__Connection__connection_failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_different_connectionId_than_the_current_client_and_an_non_fatal_error() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + let channel = client.channels.get(test.uniqueChannelName()) + + XCTAssertTrue(client.waitUntilConnected()) + + let expectedConnectionId = client.connection.id + client.internalAsync { _internal in + _internal.onDisconnected() + } - let oldConnectionId = client.connection.id + channel.attach() + channel.publish(nil, data: "queued message") + expect(client.internal.queuedMessages).toEventually(haveCount(1), timeout: testTimeout) + client.connection.once(.connecting) { _ in + client.internalSync { _internal in + let transport = _internal.transport as! TestProxyTransport + transport.setBeforeIncomingMessageModifier { protocolMessage in + if protocolMessage.action == .connected { + protocolMessage.error = .create(withCode: 0, message: "Injected error") + } else if protocolMessage.action == .attached { + protocolMessage.error = .create(withCode: 0, message: "Channel injected error") + } + protocolMessage.connectionId = "nonsense" + return protocolMessage + } + } + } + + expect(client.internal.msgSerial).to(equal(0)) + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - - channel.once(.attaching) { _ in - XCTAssertNil(channel.errorReason) + client.connection.once(.connected) { stateChange in + XCTAssertEqual(stateChange.reason?.message, "Injected error") + XCTAssertTrue(client.connection.errorReason === stateChange.reason) + let transport = client.internal.transport as! TestProxyTransport + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + XCTAssertNotEqual(connectedPM.connectionId, expectedConnectionId) + XCTAssertNotEqual(client.connection.id, expectedConnectionId) + XCTAssertEqual((transport.protocolMessagesSent.filter { $0.action == .attach }).count, 1) partialDone() } - - client.connection.once(.connected) { stateChange in + channel.once(.attached) { stateChange in guard let error = stateChange.reason else { - fail("Connection resume failed and error should be propagated to the channel"); done(); return + fail("Reason error is nil"); done(); return } - XCTAssertEqual(error.code, ARTErrorCode.unableToRecoverConnectionExpired.intValue) - expect(error.message).to(contain("Unable to recover connection")) - XCTAssertTrue(client.connection.errorReason === stateChange.reason) + XCTAssertEqual(error.message, "Channel injected error") + XCTAssertTrue(channel.errorReason === error) partialDone() } - - client.simulateLostConnectionAndState() } - let transport = client.internal.transport as! TestProxyTransport - let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] - XCTAssertNotEqual(connectedPM.connectionId, oldConnectionId) - XCTAssertEqual(client.connection.id, connectedPM.connectionId) - XCTAssertEqual(client.internal.msgSerial, 0) - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(client.internal.queuedMessages).toEventually(haveCount(0), timeout: testTimeout) } // RTN15c4 @@ -3014,7 +2925,7 @@ class RealtimeClientConnectionTests: XCTestCase { } } - // RTN15f + // RTN19a func test__066__Connection__connection_failures_once_CONNECTED__ACK_and_NACK_responses_for_published_messages_can_only_ever_be_received_on_the_transport_connection_on_which_those_messages_were_sent() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3065,19 +2976,18 @@ class RealtimeClientConnectionTests: XCTestCase { client.connection.once(.connected) { _ in resumed = true } - client.internal.testSuite_injectIntoMethod(before: Selector(("resendPendingMessages"))) { + client.internal.testSuite_injectIntoMethod(before: Selector(("resendPendingMessagesWithResumed:"))) { XCTAssertEqual(client.internal.pendingMessages.count, 1) let pm: ARTProtocolMessage? = (client.internal.pendingMessages.firstObject as? ARTPendingMessage)?.msg sentPendingMessage = pm?.messages?[0] } - client.internal.testSuite_injectIntoMethod(after: Selector(("resendPendingMessages"))) { + client.internal.testSuite_injectIntoMethod(after: Selector(("resendPendingMessagesWithResumed:"))) { partialDone() } } } // RTN15g RTN15g1 - func skipped__test__074__Connection__connection_failures_once_CONNECTED__when_connection__ttl_plus_idle_interval__period_has_passed_since_last_activity__uses_a_new_connection() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3141,7 +3051,6 @@ class RealtimeClientConnectionTests: XCTestCase { ttlAndIdleIntervalPassedTestsClient.connection.once(.connected) { _ in XCTAssertNotEqual(ttlAndIdleIntervalPassedTestsClient.connection.id, ttlAndIdleIntervalPassedTestsConnectionId) channel.once(.attached) { stateChange in - XCTAssertFalse(stateChange.resumed) done() } } @@ -3153,7 +3062,6 @@ class RealtimeClientConnectionTests: XCTestCase { } // RTN15g2 - func test__076__Connection__connection_failures_once_CONNECTED__when_connection__ttl_plus_idle_interval__period_has_NOT_passed_since_last_activity__uses_the_same_connection() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3307,7 +3215,7 @@ class RealtimeClientConnectionTests: XCTestCase { // RTN16 - // RTN16a + // RTN16i func test__080__Connection__Connection_recovery__connection_state_should_recover_explicitly_with_a_recover_key() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3334,7 +3242,7 @@ class RealtimeClientConnectionTests: XCTestCase { }) } - options.recover = clientReceive.connection.recoveryKey + options.recover = clientReceive.connection.createRecoveryKey() clientReceive.internal.onError(AblyTests.newErrorProtocolMessage()) waitUntil(timeout: testTimeout) { done in @@ -3356,38 +3264,6 @@ class RealtimeClientConnectionTests: XCTestCase { } } - // RTN16b - func test__081__Connection__Connection_recovery__Connection_recoveryKey_should_be_composed_with_the_connection_key_and_latest_serial_received_and_msgSerial() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - let client = ARTRealtime(options: options) - defer { client.dispose(); client.close() } - let channel = client.channels.get(test.uniqueChannelName()) - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(3, done: done) - client.connection.once(.connected) { _ in - XCTAssertEqual(client.connection.serial, -1) - XCTAssertEqual(client.connection.recoveryKey, "\(client.connection.key!):\(client.connection.serial):\(client.internal.msgSerial)") - partialDone() - } - channel.subscribe(attachCallback: { error in - XCTAssertNil(error) - - channel.publish(nil, data: "message") { error in - XCTAssertNil(error) - partialDone() - } - }, callback: { message in - XCTAssertEqual(message.data as? String, "message") - XCTAssertEqual(client.connection.serial, 0) - channel.unsubscribe() - partialDone() - }) - } - XCTAssertEqual(client.internal.msgSerial, 1) - XCTAssertEqual(client.connection.recoveryKey, "\(client.connection.key!):\(client.connection.serial):\(client.internal.msgSerial)") - } - // RTN16d func test__082__Connection__Connection_recovery__when_a_connection_is_successfully_recovered__Connection_id_will_be_identical_to_the_id_of_the_connection_that_was_recovered_and_Connection_key_will_always_be_updated_to_the_ConnectionDetails_connectionKey_provided_in_the_first_CONNECTED_ProtocolMessage() throws { let test = Test() @@ -3399,7 +3275,7 @@ class RealtimeClientConnectionTests: XCTestCase { let expectedConnectionId = clientOriginal.connection.id - options.recover = clientOriginal.connection.recoveryKey + options.recover = clientOriginal.connection.createRecoveryKey() clientOriginal.internal.onError(AblyTests.newErrorProtocolMessage()) let clientRecover = AblyTests.newRealtime(options).client @@ -3426,7 +3302,7 @@ class RealtimeClientConnectionTests: XCTestCase { waitUntil(timeout: testTimeout) { done in client.connection.once(.connected) { _ in client.connection.once(.closed) { _ in - XCTAssertNil(client.connection.recoveryKey) + XCTAssertNil(client.connection.createRecoveryKey()) XCTAssertNil(client.connection.key) XCTAssertNil(client.connection.id) done() @@ -3435,74 +3311,512 @@ class RealtimeClientConnectionTests: XCTestCase { } } } + + // RTN16g + func test__110__Connection__Connection_recovery__connection_recovery_key_is_correctly_constructed_from_defined_parts() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + let firstChannelName = test.uniqueChannelName() + let secondChannelName = test.uniqueChannelName() + let thirdChannelName = test.uniqueChannelName() + var firstChannelSerial: String? + var secondChannelSerial: String? + var thirdChannelSerial: String? - // RTN16e - func test__084__Connection__Connection_recovery__should_connect_anyway_if_the_recoverKey_is_no_longer_valid() throws { + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(5, done: done) + client.connection.once(.connected) { stateChange in + let firstChannel = client.channels.get(firstChannelName) + firstChannel.on(.attached) {_ in + firstChannelSerial = firstChannel.internal.serial + partialDone() + } + let secondChannel = client.channels.get(secondChannelName) + secondChannel.on(.attached) {_ in + secondChannelSerial = secondChannel.internal.serial + secondChannel.publish(nil, data: "message") { error in + expect(error).to(beNil()) + partialDone() + } + partialDone() + } + let thirdChannel = client.channels.get(thirdChannelName) + thirdChannel.on(.attached) {_ in + thirdChannelSerial = thirdChannel.internal.serial + partialDone() + } + firstChannel.attach() + secondChannel.attach() + thirdChannel.attach() + partialDone() + } + } + + let thirdChannel = client.channels.get(thirdChannelName) + thirdChannel.detach() + + guard let recoveryKeyString = client.connection.createRecoveryKey() else { + fail("recoveryKeyString shouldn't be null") + return + } + + let recoveryKey = try! ARTConnectionRecoveryKey.fromJsonString(recoveryKeyString) + expect(recoveryKey).toNot(beNil()) + expect(recoveryKey.connectionKey).to(equal(client.connection.key)) + expect(recoveryKey.channelSerials.count).to(equal(2)) + + XCTAssertNil(recoveryKey.channelSerials.first(where: { $0.key == thirdChannelSerial })) + + recoveryKey.channelSerials.keys.forEach { key in + let serial = recoveryKey.channelSerials[key] + expect(serial).toNot(beNil()) + if key == firstChannelName { + expect(serial).to(equal(firstChannelSerial)) + } else if key == secondChannelName { + expect(serial).to(equal(secondChannelSerial)) + } + } + expect(recoveryKey.msgSerial).to(equal(client.internal.msgSerial)) + } + + // RTN16g1 + func test__111__Connection__Connection_recovery__connection_recovery_key_is_properly_serializing_any_unicode_channel_name() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - options.recover = "99999!xxxxxx-xxxxxxxxx-xxxxxxxxx:-1" - let client = ARTRealtime(options: options) + let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } + + let sanskritChannelName = "channel_खगौघाङचिच्छौजाझाञ्ज्ञोऽटौठीडडण्ढणः।" + let koreanChannelName = "channel_키스의고유조건은" + var firstChannelSerial: String? + var secondChannelSerial: String? + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) client.connection.once(.connected) { stateChange in - guard let reason = stateChange.reason else { - fail("Reason is empty"); done(); return + let firstChannel = client.channels.get(sanskritChannelName) + firstChannel.on(.attached) {_ in + firstChannelSerial = firstChannel.internal.serial + partialDone() + } + let secondChannel = client.channels.get(koreanChannelName) + secondChannel.on(.attached) {_ in + secondChannelSerial = secondChannel.internal.serial + + secondChannel.publish(nil, data: "message") { error in + expect(error).to(beNil()) + partialDone() + } + partialDone() } - expect(reason.message).to(contain("Unable to recover connection")) - XCTAssertTrue(client.connection.errorReason === reason) + firstChannel.attach() + secondChannel.attach() + partialDone() + } + } + + guard let recoveryKeyString = client.connection.createRecoveryKey() else { + fail("recoveryKeyString shouldn't be null") + return + } + + let recoveryKey = try! ARTConnectionRecoveryKey.fromJsonString(recoveryKeyString) + expect(recoveryKey).toNot(beNil()) + expect(recoveryKey.connectionKey).to(equal(client.connection.key)) + expect(recoveryKey.channelSerials.count).to(equal(2)) + recoveryKey.channelSerials.keys.forEach { key in + let serial = recoveryKey.channelSerials[key] + expect(serial).toNot(beNil()) + if key == sanskritChannelName{ + expect(serial).to(equal(firstChannelSerial)) + } else if key == koreanChannelName { + expect(serial).to(equal(secondChannelSerial)) + } + } + expect(recoveryKey.msgSerial).to(equal(client.internal.msgSerial)) + } + + // RTN16g2 - closing, closed + func test__112__Connection__Connection_recovery__connection_recovery_key_is_null_when_connection_is_in_invalid_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + client.connection.once(.connected) { stateChange in + expect(client.connection.createRecoveryKey()).notTo(beNil()) + client.internal.close() + } + client.connection.once(.closing) { stateChange in + expect(client.connection.createRecoveryKey()).to(beNil()) + partialDone() + } + client.connection.once(.closed) { stateChange in + expect(client.connection.createRecoveryKey()).to(beNil()) + partialDone() + } + } + } + + // RTN16g2 - suspended + func test__113__Connection__Connection_recovery__connection_recovery_key_is_null_when_connection_is_in_suspended_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.connected) { stateChange in + expect(client.connection.createRecoveryKey()).notTo(beNil()) + client.internal.onSuspended() + } + client.connection.once(.suspended) { stateChange in + expect(client.connection.createRecoveryKey()).to(beNil()) + done() + } + } + } + + // RTN16g2 - failed + func test__114__Connection__Connection_recovery__connection_recovery_key_is_null_when_connection_is_in_failed_state() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.connected) { stateChange in + expect(client.connection.createRecoveryKey()).notTo(beNil()) + client.internal.onError(ARTProtocolMessage()) + } + client.connection.once(.failed) { stateChange in + expect(client.connection.createRecoveryKey()).to(beNil()) done() } } } // RTN16f - func test__085__Connection__Connection_recovery__should_use_msgSerial_from_recoveryKey_to_set_the_client_internal_msgSerial_but_is_not_sent_to_Ably() throws { + func test__085__Connection__Connection_recovery__should_set_internal_message_serial_to_component_in_recovery_key() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - options.autoConnect = false - options.recover = "99999!xxxxxx-xxxxxxxxx-xxxxxxxxx:-1:7" + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } - let testEnvironment = AblyTests.newRealtime(options) - let client = testEnvironment.client + var recoveryKey: ARTConnectionRecoveryKey! + + waitUntil(timeout: testTimeout) { done in + publishFirstTestMessage(client, channelName: test.uniqueChannelName(), completion: { error in + XCTAssertNil(error) + let recoveryKeyString = client.connection.createRecoveryKey()! + recoveryKey = try! ARTConnectionRecoveryKey.fromJsonString(recoveryKeyString) + XCTAssertEqual(recoveryKey.msgSerial, client.internal.msgSerial) + options.recover = recoveryKeyString + done() + }) + } + let recoverClient = AblyTests.newRealtime(options).client + expect(recoverClient.internal.msgSerial).to(equal(recoveryKey.msgSerial)) + } + + // RTN16j + func test__086__Connection__Connection_recovery__library_should_create_channel_with_corresponding_serial_in_given_recovery_key() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + + let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } + + let channelName = test.uniqueChannelName() + var expectedChannelSerial: String? + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + client.connection.once(.connected) { stateChange in + let channel = client.channels.get(channelName) + channel.on(.attached) { _ in + expectedChannelSerial = channel.internal.serial + partialDone() + } + channel.attach() + partialDone() + } + } - var urlConnections = [URL]() - testEnvironment.transportFactory.networkConnectEvent = { transport, url in - if client.internal.transport !== transport { - return + let recoverOptions = options + recoverOptions.recover = client.connection.createRecoveryKey() + + let recoveredClient = AblyTests.newRealtime(recoverOptions).client + defer { recoveredClient.dispose(); recoveredClient.close() } + expect(recoveredClient.connection.state).toEventually(equal(.connected), timeout: testTimeout) + + XCTAssertTrue(recoveredClient.channels.exists(channelName)) + let recoveredChannel = recoveredClient.channels.get(channelName) + expect(recoveredChannel.internal.serial).to(equal(expectedChannelSerial)) + } + + // RTN16k + func test__090__Connection__Connection_recovery__library_provides_additional_querystring_when_recover_is_provided() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + options.autoConnect = false + + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + func recoverQueryPart(_ query: String) -> String? { + let parts = query.components(separatedBy: "&") + let recoverPart = parts.filter({ part in + part.contains("recover") + }).first + return recoverPart + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.connecting) {_ in + guard let webSocketTransport = client.internal.transport as? ARTWebSocketTransport else { + fail("Transport should be of type ARTWebSocketTransport"); done() + return + } + expect(webSocketTransport.websocketURL?.query).toNot(beNil()) + let recoverPart = recoverQueryPart(webSocketTransport.websocketURL!.query!) + expect(recoverPart).to(beNil()) } - urlConnections.append(url) - if urlConnections.count == 1 { - testEnvironment.transportFactory.networkConnectEvent = nil + client.connection.once(.connected) { stateChange in + done() } + client.connect() } + let recoverOptions = options + recoverOptions.recover = client.connection.createRecoveryKey() + recoverOptions.autoConnect = false + let recoverClient = AblyTests.newRealtime(recoverOptions).client + defer { recoverClient.dispose(); recoverClient.close() } + waitUntil(timeout: testTimeout) { done in - client.connection.once(.connected) { stateChange in - guard let reason = stateChange.reason else { - fail("Reason is empty"); done(); return + let partialDone = AblyTests.splitDone(3, done: done) + recoverClient.connection.once(.connecting) {_ in + guard let webSocketTransport = recoverClient.internal.transport as? ARTWebSocketTransport else { + fail("Transport should be of type ARTWebSocketTransport"); done() + return } - - XCTAssertEqual(urlConnections.count, 1) - guard let urlConnectionQuery = urlConnections.first?.query else { - fail("Missing URL Connection query"); done(); return + expect(webSocketTransport.websocketURL?.query).toNot(beNil()) + let recoverPart = recoverQueryPart(webSocketTransport.websocketURL!.query!) + expect(recoverPart).toNot(beNil()) + let recoverValue = recoverPart? + .components(separatedBy: "=")[1] + expect(recoverValue).to(equal(client.connection.key)) + partialDone() + } + recoverClient.connection.once(.connected) {_ in + recoverClient.connection.off() + recoverClient.internal.onDisconnected() + partialDone() + recoverClient.connection.once(.connecting) {_ in + guard let webSocketTransport = recoverClient.internal.transport as? ARTWebSocketTransport else { + fail("Transport should be of type ARTWebSocketTransport"); done() + return + } + expect(webSocketTransport.websocketURL?.query).toNot(beNil()) + let recoverPart = recoverQueryPart(webSocketTransport.websocketURL!.query!) + expect(recoverPart).to(beNil()) + partialDone() } + } + recoverClient.connect() + } + } - expect(urlConnectionQuery).to(haveParam("recover", withValue: "99999!xxxxxx-xxxxxxxxx-xxxxxxxxx")) - expect(urlConnectionQuery).to(haveParam("connectionSerial", withValue: "-1")) - expect(urlConnectionQuery).toNot(haveParam("msgSerial")) + // RTN16l for (RTN15c5 + RTN15h1) + func test__200a__Connection__Connection_recovery__failures_system_response_to_unrecoverable_token_error() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + expect(client.internal.connection.state).toEventually(equal(.connected), timeout: testTimeout) + + let recoverOptions = try AblyTests.commonAppSetup(for: test) + recoverOptions.recover = client.connection.createRecoveryKey() + + let key = recoverOptions.key + // set the key to nil so that the client can't sign further token requests + recoverOptions.key = nil + let tokenDetails = try getTestTokenDetails(for: test, key: key, ttl: 30.0) + recoverOptions.token = tokenDetails.token + + let recoverClient = AblyTests.newRealtime(recoverOptions).client + defer { recoverClient.dispose(); recoverClient.close() } - // recover fails, the counter should be reset to 0 - XCTAssertEqual(client.internal.msgSerial, 0) + let transport = recoverClient.internal.transport as! TestProxyTransport + transport.emulateTokenRevokationBeforeConnected() - expect(reason.message).to(contain("Unable to recover connection")) - XCTAssertTrue(client.connection.errorReason === reason) + waitUntil(timeout: testTimeout) { done in + recoverClient.connection.once(.failed) { stateChange in + XCTAssertEqual(stateChange.previous, ARTRealtimeConnectionState.connecting) + XCTAssertEqual(recoverClient.connection.errorReason?.code, ARTErrorCode.tokenRevoked.intValue) done() } - client.connect() - XCTAssertEqual(client.internal.msgSerial, 7) + recoverClient.connection.once(.connecting) { stateChange in + XCTFail("Should not attempt to connect") + } + } + } + + // RTN16l for (RTN15c5 + RTN15h2 success) + func test__200b__Connection__Connection_recovery__failures_system_response_to_unrecoverable_token_error() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + expect(client.internal.connection.state).toEventually(equal(.connected), timeout: testTimeout) + + let recoverOptions = try AblyTests.commonAppSetup(for: test) + recoverOptions.recover = client.connection.createRecoveryKey() + + let tokenDetails = try getTestTokenDetails(for: test, key: recoverOptions.key, ttl: 30.0) + recoverOptions.token = tokenDetails.token + + let recoverClient = AblyTests.newRealtime(recoverOptions).client + defer { recoverClient.dispose(); recoverClient.close() } + + let transport = recoverClient.internal.transport as! TestProxyTransport + transport.emulateTokenRevokationBeforeConnected() + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + recoverClient.connection.once(.disconnected) { stateChange in + XCTAssertEqual(stateChange.previous, ARTRealtimeConnectionState.connecting) + partialDone() + } + recoverClient.connection.once(.connected) { stateChange in + partialDone() + } + } + } + + // RTN16l for (RTN15c5 + RTN15h2 failure) + func test__200c__Connection__Connection_recovery__failures_system_response_to_unrecoverable_token_error() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + expect(client.internal.connection.state).toEventually(equal(.connected), timeout: testTimeout) + + let recoverOptions = try AblyTests.commonAppSetup(for: test) + recoverOptions.recover = client.connection.createRecoveryKey() + + let tokenDetails = try getTestTokenDetails(for: test, key: recoverOptions.key, ttl: 30.0) + recoverOptions.token = tokenDetails.token + + let recoverClient = AblyTests.newRealtime(recoverOptions, onTransportCreated: { transport in + (transport as! TestProxyTransport).emulateTokenRevokationBeforeConnected() + }).client + defer { recoverClient.dispose(); recoverClient.close() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + recoverClient.connection.once(.disconnected) { stateChange in + partialDone() + XCTAssertNil(recoverClient.connection.errorReason) + recoverClient.connection.once(.disconnected) { stateChange in + XCTAssertEqual(recoverClient.connection.errorReason?.code, ARTErrorCode.tokenRevoked.intValue) + partialDone() + } + } + recoverClient.connection.on(.connected) { _ in + XCTFail("Should not be connected") + } + } + } + + // RTN16l for (RTN15c7) + func test__201__Connection__Connection_recovery__failures_once_CONNECTED__System_s_response_to_a_resume_request__CONNECTED_ProtocolMessage_with_the_different_connectionId_than_the_current_client_and_an_non_fatal_error() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + defer { client.dispose(); client.close() } + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + + let expectedConnectionId = client.connection.id + + let recoverOptions = try AblyTests.commonAppSetup(for: test) + recoverOptions.recover = client.connection.createRecoveryKey() + recoverOptions.autoConnect = false + let recoverClient = AblyTests.newRealtime(recoverOptions).client + defer { recoverClient.dispose(); recoverClient.close() } + + waitUntil(timeout: testTimeout) { done in + + recoverClient.connection.once(.connecting) { _ in + let transport = recoverClient.internal.transport as! TestProxyTransport + transport.setBeforeIncomingMessageModifier { protocolMessage in + protocolMessage.connectionId = "nonsense" + protocolMessage.error = .create(withCode: 0, message: "Injected error") + return protocolMessage + } + } + + recoverClient.connection.once(.connected) { stateChange in + expect(stateChange.reason?.message).to(equal("Injected error")) + expect(recoverClient.connection.errorReason).to(beIdenticalTo(stateChange.reason)) + let transport = recoverClient.internal.transport as! TestProxyTransport + + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + expect(connectedPM.connectionId).toNot(equal(expectedConnectionId)) + expect(recoverClient.connection.id).toNot(equal(expectedConnectionId)) + done() + } + + recoverClient.connect() + } + } + + // RTN16l (for RTN15c4) + func test__202__Connection__Connection_recovery__failures_with_any_other_fatal_error_in_the_connection() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + expect(client.internal.connection.state).toEventually(equal(.connected), timeout: testTimeout) + + let recoverOptions = try AblyTests.commonAppSetup(for: test) + recoverOptions.recover = client.connection.createRecoveryKey() + recoverOptions.autoConnect = false + + let tokenDetails = try getTestTokenDetails(for: test, key: recoverOptions.key, ttl: 30.0) + recoverOptions.token = tokenDetails.token + + let recoverClient = AblyTests.newRealtime(recoverOptions).client + defer { recoverClient.dispose(); recoverClient.close() } + + waitUntil(timeout: testTimeout) { done in + recoverClient.connection.once(.connecting) { _ in + let transport = recoverClient.internal.transport as! TestProxyTransport + transport.setBeforeIncomingMessageModifier { protocolMessage in + if protocolMessage.action == .connected { + protocolMessage.action = .error + protocolMessage.error = .create(withCode: ARTErrorCode.rateLimitExceededFatal.intValue, status: 403, message: "Fatal error") + } + return protocolMessage + } + } + recoverClient.connection.on(.failed) { _ in + XCTAssertEqual(recoverClient.connection.errorReason?.code, ARTErrorCode.rateLimitExceededFatal.intValue) + done() + } + recoverClient.connection.on(.connected) { _ in + XCTFail("Should not be connected") + } + recoverClient.connection.on(.disconnected) { _ in + XCTFail("Should not be disconnected") + } + recoverClient.connect() } } + + // RTN17 // RTN17b @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.fallbackHostsUseDefault property. Remove this deprecation when removing the property.") @@ -4132,8 +4446,8 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertEqual(realtime.connection.key, connectedProtocolMessage.connectionDetails!.connectionKey) } - // RTN19a - func skipped__test__103__Connection__Transport_disconnected_side_effects__should_resend_any_ProtocolMessage_that_is_awaiting_a_ACK_NACK() throws { + // RTN19a1 + func test__103a__Connection__Transport_disconnected_side_effects__should_resend_any_ProtocolMessage_by_processing_connection_wide_pending_messages() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client @@ -4154,14 +4468,144 @@ class RealtimeClientConnectionTests: XCTestCase { expect(newTransport).toNot(beIdenticalTo(transport)) XCTAssertEqual(transport.protocolMessagesSent.filter { $0.action == .message }.count, 1) XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .connected }.count, 1) - XCTAssertEqual(newTransport.protocolMessagesReceived.filter { $0.action == .connected }.count, 1) - XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .connected }.count, 1) XCTAssertEqual(newTransport.protocolMessagesSent.filter { $0.action == .message }.count, 1) + XCTAssertEqual(newTransport.protocolMessagesReceived.filter { $0.action == .connected }.count, 1) done() } + client.connection.once(.disconnected) { _ in + expect(client.internal.pendingMessages).to(haveCount(1)) + } client.internal.onDisconnected() } } + + // RTN19a2 (for resume success) + func test__103b__Connection__Transport_disconnected_side_effects_message_serial_for_pending_message_must_remain_the_same_that_is_awaiting_a_ACK_NACK() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + + defer { client.dispose(); client.close() } + let channel = client.channels.get(test.uniqueChannelName()) + let transport = client.internal.transport as! TestProxyTransport + + var connectionId: String? + + waitUntil(timeout: testTimeout) { done in + channel.attach { _ in + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + connectionId = connectedPM.connectionId + done() + } + } + + waitUntil(timeout: testTimeout) { done in + // send messages to increase msgSerial above zero + let partialDone = AblyTests.splitDone(2, done: done) + channel.publish(nil, data: "First message") { error in + XCTAssertNil(error) + let msgSerial = transport.protocolMessagesSent.filter { $0.action == .message }[0].msgSerial + XCTAssertEqual(msgSerial, 0) + partialDone() + } + channel.publish(nil, data: "Second message") { error in + XCTAssertNil(error) + let msgSerial = transport.protocolMessagesSent.filter { $0.action == .message }[1].msgSerial + XCTAssertEqual(msgSerial, 1) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "Third message") { error in + expect(error).to(beNil()) + guard let newTransport = client.internal.transport as? TestProxyTransport else { + fail("Transport is nil"); done(); return + } + expect(newTransport).toNot(beIdenticalTo(transport)) + + let msgSerial1 = transport.protocolMessagesSent.filter { $0.action == .message }[2].msgSerial + let msgSerial2 = newTransport.protocolMessagesSent.filter { $0.action == .message }[0].msgSerial + XCTAssertEqual(msgSerial1, 2) + XCTAssertEqual(msgSerial2, 2) + + done() + } + + client.connection.once(.connected) { stateChange in + // Ensure same connection id and no error + let transport = client.internal.transport as! TestProxyTransport + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + XCTAssertNil(connectedPM.error) + XCTAssertEqual(connectedPM.connectionId, connectionId) + } + client.internal.onDisconnected() + } + } + + // RTN19a2 (for resume failure) + func test__103c__Connection__Transport_disconnected_side_effects_message_must_be_assigned_a_new_msgSerial_from_the_internal_msgSerial() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + + defer { client.dispose(); client.close() } + let channel = client.channels.get(test.uniqueChannelName()) + let transport = client.internal.transport as! TestProxyTransport + + var connectionId: String? + + waitUntil(timeout: testTimeout) { done in + channel.attach { _ in + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + connectionId = connectedPM.connectionId + done() + } + } + + waitUntil(timeout: testTimeout) { done in + // send messages to increase msgSerial above zero + let partialDone = AblyTests.splitDone(2, done: done) + channel.publish(nil, data: "First message") { error in + XCTAssertNil(error) + let msgSerial = transport.protocolMessagesSent.filter { $0.action == .message }[0].msgSerial + XCTAssertEqual(msgSerial, 0) + partialDone() + } + channel.publish(nil, data: "Second message") { error in + XCTAssertNil(error) + let msgSerial = transport.protocolMessagesSent.filter { $0.action == .message }[1].msgSerial + XCTAssertEqual(msgSerial, 1) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "Third message") { error in + expect(error).to(beNil()) + guard let newTransport = client.internal.transport as? TestProxyTransport else { + fail("Transport is nil"); done(); return + } + expect(newTransport).toNot(beIdenticalTo(transport)) + + let msgSerial1 = transport.protocolMessagesSent.filter { $0.action == .message }[2].msgSerial + let msgSerial2 = newTransport.protocolMessagesSent.filter { $0.action == .message }[0].msgSerial + XCTAssertEqual(msgSerial1, 0) + XCTAssertEqual(msgSerial2, 0) + + done() + } + + client.connection.once(.connected) { stateChange in + // Ensure different connection id and error + let transport = client.internal.transport as! TestProxyTransport + let connectedPM = transport.protocolMessagesReceived.filter { $0.action == .connected }[0] + XCTAssertNotNil(connectedPM.error) + XCTAssertNotEqual(connectedPM.connectionId, connectionId) + } + client.simulateLostConnectionAndState() + } + } // RTN19b func skipped__test__104__Connection__Transport_disconnected_side_effects__should_resend_the_ATTACH_message_if_there_are_any_pending_channels() throws { diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index e3d5f1836..5cb2407c7 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -207,16 +207,11 @@ class RealtimeClientPresenceTests: XCTestCase { } } - guard let lastConnectionSerial = transport.protocolMessagesReceived.last?.connectionSerial else { - fail("No protocol message has been received yet"); done(); return - } - // Inject a SYNC Presence message (first page) let sync1Message = ARTProtocolMessage() sync1Message.action = .sync sync1Message.channel = channel.name sync1Message.channelSerial = "sequenceid:cursor" - sync1Message.connectionSerial = lastConnectionSerial + 1 sync1Message.timestamp = Date() sync1Message.presence = [ ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), @@ -229,7 +224,6 @@ class RealtimeClientPresenceTests: XCTestCase { sync2Message.action = .sync sync2Message.channel = channel.name sync2Message.channelSerial = "sequenceid:" // indicates SYNC is complete - sync2Message.connectionSerial = lastConnectionSerial + 2 sync2Message.timestamp = Date() sync2Message.presence = [ ARTPresenceMessage(clientId: "a", action: .leave, connectionId: "another", id: "another:1:0"), @@ -285,15 +279,10 @@ class RealtimeClientPresenceTests: XCTestCase { done() } - guard let lastConnectionSerial = transport.protocolMessagesReceived.last?.connectionSerial else { - fail("No protocol message has been received yet"); done(); return - } - // Inject a SYNC Presence message (entirely contained) let syncMessage = ARTProtocolMessage() syncMessage.action = .sync syncMessage.channel = channel.name - syncMessage.connectionSerial = lastConnectionSerial + 1 syncMessage.timestamp = Date() syncMessage.presence = [ ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), @@ -785,6 +774,8 @@ class RealtimeClientPresenceTests: XCTestCase { let leavesChannel = leavesClient.channels.get(channelName) let mainChannel = mainClient.channels.get(channelName) + var oldConnectionId = "" + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(4, done: done) mainChannel.presence.subscribe { message in @@ -799,6 +790,7 @@ class RealtimeClientPresenceTests: XCTestCase { } mainChannel.presence.enter(nil) { error in XCTAssertNil(error) + oldConnectionId = mainChannel.internal.connectionId partialDone() } leavesChannel.presence.enter(nil) { error in @@ -826,6 +818,7 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(4, done: done) mainChannel.presence.subscribe { presence in + guard presence.clientId != mainClient.clientId, presence.action != .enter else { return } // ignore ENTER from "main" after re-attach, since it's not "between ATTACHED states" presenceEvents += [presence] delay(1) { partialDone() // Wait a bit to make sure we don't receive any other presence messages @@ -855,6 +848,14 @@ class RealtimeClientPresenceTests: XCTestCase { mainChannel.presence.unsubscribe() + guard let transport = mainClient.internal.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + // Same can be achieved with sleep for more than 15 seconds for the Realtime to send synthesised presence LEAVE + // for the mainClient’s original connection after not receiving a heartbeat. + transport.receive(AblyTests.newPresenceProtocolMessage(id: "\(mainChannel.internal.connectionId):0:0", channel: mainChannel.name, action: .leave, clientId: mainClient.clientId!, connectionId: oldConnectionId)) + waitUntil(timeout: testTimeout) { done in mainChannel.presence.get { members, error in XCTAssertNil(error) @@ -1572,7 +1573,6 @@ class RealtimeClientPresenceTests: XCTestCase { let presenceMessage = ARTProtocolMessage() presenceMessage.action = .presence presenceMessage.channel = protocolMessage.channel - presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 presenceMessage.timestamp = Date() presenceMessage.presence = presenceData @@ -1583,7 +1583,6 @@ class RealtimeClientPresenceTests: XCTestCase { endSyncMessage.action = .sync endSyncMessage.channel = protocolMessage.channel endSyncMessage.channelSerial = "validserialprefix:" // with no part after the `:` this indicates the end to the SYNC - endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 endSyncMessage.timestamp = Date() transport.setAfterIncomingMessageModifier(nil) @@ -1652,7 +1651,6 @@ class RealtimeClientPresenceTests: XCTestCase { let presenceMessage = ARTProtocolMessage() presenceMessage.action = .presence presenceMessage.channel = protocolMessage.channel - presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 presenceMessage.timestamp = Date() presenceMessage.presence = presenceData @@ -1663,7 +1661,6 @@ class RealtimeClientPresenceTests: XCTestCase { endSyncMessage.action = .sync endSyncMessage.channel = protocolMessage.channel endSyncMessage.channelSerial = "validserialprefix:" // with no part after the `:` this indicates the end to the SYNC - endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 endSyncMessage.timestamp = Date() transport.setAfterIncomingMessageModifier(nil) @@ -1980,7 +1977,6 @@ class RealtimeClientPresenceTests: XCTestCase { let leaveMessage = ARTProtocolMessage() leaveMessage.action = .presence leaveMessage.channel = channel.name - leaveMessage.connectionSerial = client.connection.internal.serial_nosync() + 1 leaveMessage.timestamp = Date() leaveMessage.presence = [ ARTPresenceMessage(clientId: "user11", action: .leave, connectionId: "another", id: "another:123:0", timestamp: Date()), @@ -2814,7 +2810,7 @@ class RealtimeClientPresenceTests: XCTestCase { // Inject an additional member into the myMember set, then force a suspended state client.simulateSuspended(beforeSuspension: { done in - channel.internal.presenceMap.localMembers.add(additionalMember) + channel.internal.presenceMap.localMembers[additionalMember.clientId!] = additionalMember done() }) expect(client.connection.state).toEventually(equal(.suspended), timeout: testTimeout) @@ -2875,7 +2871,90 @@ class RealtimeClientPresenceTests: XCTestCase { } } } + + // RTP17i, RTP17g + func test__200__Presence__PresenceMap_should_perform_re_entry_whenever_a_channel_moves_into_the_attached_state_and_presence_message_consists_of_enter_action_with_client_id_and_data() throws { + let test = Test() + let options = try AblyTests.commonAppSetup(for: test) + let client = AblyTests.newRealtime(options).client + let transport = client.internal.transport as! TestProxyTransport + defer { client.dispose(); client.close() } + + let channel = client.channels.get(test.uniqueChannelName()) + + var firstMsgId = "" + var secondMsgId = "" + let firstClient = "client1" + let secondClient = "client2" + let firstClientData = "client1data" + let secondClientData = "client2data" + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.attached) { stateChange in + channel.presence.enterClient(firstClient, data: firstClientData) + channel.presence.enterClient(secondClient, data: secondClientData) + } + channel.presence.subscribe(.enter) { presenceMessage in + if presenceMessage.clientId == firstClient { + firstMsgId = presenceMessage.id! + partialDone() + } + else if presenceMessage.clientId == secondClient { + secondMsgId = presenceMessage.id! + partialDone() + } + } + channel.attach() + } + channel.presence.unsubscribe() + + expect(channel.internal.presenceMap.localMembers).to(haveCount(2)) + + // All pending messages should complete (receive ACK or NACK) before disconnect for valid count of transport.protocolMessagesSent + client.waitForPendingMessages() + client.simulateLostConnection() + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout) + + // RTP17i + + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) + expect(channel.internal.presenceMap.localMembers).to(haveCount(2)) + + let newTransport = client.internal.transport as! TestProxyTransport + expect(newTransport).toNot(beIdenticalTo(transport)) + + var sentPresenceMessages = newTransport.protocolMessagesSent.filter({ $0.action == .presence }).compactMap { $0.presence?.first } + + expect(sentPresenceMessages).to(haveCount(2)) + + let client1PresenceMessage = try XCTUnwrap(sentPresenceMessages.first(where: { $0.clientId == firstClient })) + let client2PresenceMessage = try XCTUnwrap(sentPresenceMessages.first(where: { $0.clientId == secondClient })) + + // RTP17i - already attached with resume flag set + + let attachedMessage = ARTProtocolMessage() + attachedMessage.action = .attached + attachedMessage.channel = channel.name + attachedMessage.flags = 4 // resume flag + newTransport.receive(attachedMessage) + sentPresenceMessages = newTransport.protocolMessagesSent.filter({ $0.action == .presence }).compactMap { $0.presence?.first } + expect(sentPresenceMessages).to(haveCount(2)) // no changes in sentPresenceMessages => no presense messages sent + + // RTP17g + + expect(client1PresenceMessage.id).to(equal(firstMsgId)) + expect(client2PresenceMessage.id).to(equal(secondMsgId)) + + expect(client1PresenceMessage.action).to(equal(ARTPresenceAction.enter)) + expect(client2PresenceMessage.action).to(equal(ARTPresenceAction.enter)) + + expect(client1PresenceMessage.data as? String).to(equal(firstClientData)) + expect(client2PresenceMessage.data as? String).to(equal(secondClientData)) + } + func skipped__test__083__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__events_applied_to_presence_map__should_be_applied_to_any_LEAVE_event_with_a_connectionId_that_matches_the_current_client_s_connectionId_and_is_not_a_synthesized() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3713,7 +3792,6 @@ class RealtimeClientPresenceTests: XCTestCase { let presenceMessage = ARTProtocolMessage() presenceMessage.action = .presence presenceMessage.channel = protocolMessage.channel - presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 presenceMessage.timestamp = Date() presenceMessage.presence = presenceData @@ -3724,7 +3802,6 @@ class RealtimeClientPresenceTests: XCTestCase { endSyncMessage.action = .sync endSyncMessage.channel = protocolMessage.channel endSyncMessage.channelSerial = "validserialprefix:" // with no part after the `:` this indicates the end to the SYNC - endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 endSyncMessage.timestamp = Date() transport.setAfterIncomingMessageModifier(nil) diff --git a/Test/Tests/RealtimeClientTests.swift b/Test/Tests/RealtimeClientTests.swift index f3dac4dce..f65891763 100644 --- a/Test/Tests/RealtimeClientTests.swift +++ b/Test/Tests/RealtimeClientTests.swift @@ -50,7 +50,7 @@ class RealtimeClientTests: XCTestCase { // This test should not directly validate version against ARTDefault.version(), as // ultimately the version header has been derived from that value. - expect(transport.lastUrl!.query).to(haveParam("v", withValue: "1.2")) + expect(transport.lastUrl!.query).to(haveParam("v", withValue: "2")) done() } @@ -117,8 +117,10 @@ class RealtimeClientTests: XCTestCase { done() case .connected: self.checkError(errorInfo) - XCTAssertEqual(client.connection.recoveryKey, "\(client.connection.key ?? ""):\(client.connection.serial):\(client.internal.msgSerial)", "recoveryKey wrong formed") - options.recover = client.connection.recoveryKey + let recoveryKey = try! ARTConnectionRecoveryKey.fromJsonString(client.connection.createRecoveryKey() ?? "invalid") + XCTAssertEqual(recoveryKey.connectionKey, client.internal.connection.key) + XCTAssertEqual(recoveryKey.msgSerial, client.internal.msgSerial) + options.recover = client.connection.createRecoveryKey() done() default: break diff --git a/Test/Tests/RestClientTests.swift b/Test/Tests/RestClientTests.swift index 72a1753fa..df32f8277 100644 --- a/Test/Tests/RestClientTests.swift +++ b/Test/Tests/RestClientTests.swift @@ -162,7 +162,7 @@ class RestClientTests: XCTestCase { // This test should not directly validate version against ARTDefault.version(), as // ultimately the version header has been derived from that value. - XCTAssertEqual(version, "1.2") + XCTAssertEqual(version, "2") done() } @@ -1713,7 +1713,7 @@ class RestClientTests: XCTestCase { // This test should not directly validate version against ARTDefault.version(), as // ultimately the version header has been derived from that value. - XCTAssertEqual(headerAblyVersion, "1.2") + XCTAssertEqual(headerAblyVersion, "2") done() } diff --git a/Test/Tests/ResumeRequestResponseTests.swift b/Test/Tests/ResumeRequestResponseTests.swift deleted file mode 100644 index 90886846a..000000000 --- a/Test/Tests/ResumeRequestResponseTests.swift +++ /dev/null @@ -1,95 +0,0 @@ -import XCTest -import Ably.Private - -class ResumeRequestResponseTests: XCTestCase { - func test_valid() { - let protocolMessage = ARTProtocolMessage() - protocolMessage.action = .connected - protocolMessage.connectionId = "123" // same as currentConnectionID below - - let errorChecker = MockErrorChecker() - - let response = ResumeRequestResponse( - currentConnectionID: "123", // arbitrarily chosen - protocolMessage: protocolMessage, - errorChecker: errorChecker - ) - - XCTAssertEqual(response.type, .valid) - XCTAssertNil(response.error) - } - - func test_invalid() { - let protocolMessage = ARTProtocolMessage() - protocolMessage.action = .connected - protocolMessage.connectionId = "456" // arbitrarily chosen, different to currentConnectionID below - protocolMessage.error = .init() // arbitrarily chosen - - let errorChecker = MockErrorChecker() - - let response = ResumeRequestResponse( - currentConnectionID: "123", // arbitrarily chosen - protocolMessage: protocolMessage, - errorChecker: errorChecker - ) - - XCTAssertEqual(response.type, .invalid) - XCTAssertEqual(response.error, protocolMessage.error) - } - - func test_tokenError() { - let protocolMessage = ARTProtocolMessage() - protocolMessage.action = .error - protocolMessage.error = .init() // arbitrarily chosen - - let errorChecker = MockErrorChecker() - errorChecker.isTokenError = true - - let response = ResumeRequestResponse( - currentConnectionID: "123", // arbitrarily chosen - protocolMessage: protocolMessage, - errorChecker: errorChecker - ) - - XCTAssertEqual(response.type, .tokenError) - XCTAssertEqual(response.error, protocolMessage.error) - } - - func test_fatalError() { - let protocolMessage = ARTProtocolMessage() - protocolMessage.action = .error - protocolMessage.error = .init() // arbitrarily chosen - - let errorChecker = MockErrorChecker() - errorChecker.isTokenError = false - - let response = ResumeRequestResponse( - currentConnectionID: "123", - protocolMessage: protocolMessage, - errorChecker: errorChecker - ) - - XCTAssertEqual(response.type, .fatalError) - XCTAssertEqual(response.error, protocolMessage.error) - } - - func test_unknown() { - // For example, a CONNECTED ProtocolMessage with the same connection ID but with an error - - let protocolMessage = ARTProtocolMessage() - protocolMessage.action = .connected - protocolMessage.connectionId = "123" // same as currentConnectionID below - protocolMessage.error = .init() // arbitrarily chosen - - let errorChecker = MockErrorChecker() - - let response = ResumeRequestResponse( - currentConnectionID: "123", // arbitrarily chosen - protocolMessage: protocolMessage, - errorChecker: errorChecker - ) - - XCTAssertEqual(response.type, .unknown) - XCTAssertNil(response.error) - } -}