From 04ba309d3dab58d75a7721611903edca3724f432 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:23:47 +0000 Subject: [PATCH 01/27] New EventEmitter (using NSNotificationCenter) - In most ways that matter NSNotificationCenter is thread safe. You can add/remove observers from any thread and you can post notifications from any thread. --- Source/ARTEventEmitter+Private.h | 20 +- Source/ARTEventEmitter.h | 85 +++++--- Source/ARTEventEmitter.m | 341 ++++++++++++++++++------------- Source/ARTGCD.h | 4 +- Source/ARTGCD.m | 14 +- Spec/Utilities.swift | 27 +-- 6 files changed, 282 insertions(+), 209 deletions(-) diff --git a/Source/ARTEventEmitter+Private.h b/Source/ARTEventEmitter+Private.h index cacc783c6..66ae2f10e 100644 --- a/Source/ARTEventEmitter+Private.h +++ b/Source/ARTEventEmitter+Private.h @@ -7,24 +7,18 @@ // #include "ARTEventEmitter.h" -#include "CompatibilityMacros.h" -ART_ASSUME_NONNULL_BEGIN +NS_ASSUME_NONNULL_BEGIN -@interface __GENERIC(ARTEventEmitterEntry, ItemType) : NSObject +@interface ARTEventEmitter () -@property (readwrite, strong, nonatomic) __GENERIC(ARTEventListener, ItemType) *listener; -@property (readwrite, nonatomic) BOOL once; +@property (nonatomic, readonly) NSNotificationCenter *notificationCenter; +@property (nonatomic, readonly) dispatch_queue_t queue; -- (instancetype)initWithListener:(__GENERIC(ARTEventListener, ItemType) *)listener once:(BOOL)once; +@property (readonly, atomic) NSMutableDictionary *> *listeners; +@property (readonly, atomic) NSMutableArray *anyListeners; @end -@interface __GENERIC(ARTEventEmitter, EventType, ItemType) () +NS_ASSUME_NONNULL_END -@property (readwrite, atomic) __GENERIC(NSMutableDictionary, EventType, __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *) *listeners; -@property (readwrite, atomic) __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *anyListeners; - -@end - -ART_ASSUME_NONNULL_END diff --git a/Source/ARTEventEmitter.h b/Source/ARTEventEmitter.h index f9e6cd4fe..9f0c318b6 100644 --- a/Source/ARTEventEmitter.h +++ b/Source/ARTEventEmitter.h @@ -7,36 +7,60 @@ // #import -#import "ARTTypes.h" @class ARTRealtime; +@class ARTEventEmitter; -ART_ASSUME_NONNULL_BEGIN +NS_ASSUME_NONNULL_BEGIN -@interface __GENERIC(ARTEventListener, ItemType) : NSObject +@protocol ARTEventIdentification +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; +- (NSString *)identification; +@end + +@interface ARTEvent : NSObject + +- (instancetype)initWithString:(NSString *)value; ++ (instancetype)newWithString:(NSString *)value; + +@end + +#pragma mark - ARTEventListener + +@interface ARTEventListener : NSObject -- (void)call:(ItemType)argument; +@property (nonatomic, readonly) NSString *eventId; +@property (nonatomic, readonly) id token; +@property (nonatomic, readonly) NSUInteger count; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithId:(NSString *)eventId token:(id)token handler:(ARTEventEmitter *)eventHandler center:(NSNotificationCenter *)center; + +- (ARTEventListener *)setTimer:(NSTimeInterval)timeoutDeadline onTimeout:(void (^)())timeoutBlock; +- (void)startTimer; +- (void)stopTimer; @end -@interface __GENERIC(ARTEventEmitter, EventType, ItemType) : NSObject +#pragma mark - ARTEventEmitter + +@interface ARTEventEmitter, ItemType> : NSObject - (instancetype)init; - (instancetype)initWithQueue:(dispatch_queue_t)queue; -- (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb; -- (void)off:(EventType)event listener:(__GENERIC(ARTEventListener, ItemType) *)listener; -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener; +- (void)off:(EventType)event listener:(ARTEventListener *)listener; +- (void)off:(ARTEventListener *)listener; - (void)off; -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^__art_nullable)())onTimeout; - -- (void)emit:(EventType)event with:(ItemType __art_nullable)data; +- (void)emit:(nullable EventType)event with:(nullable ItemType)data; @end @@ -44,54 +68,49 @@ ART_ASSUME_NONNULL_BEGIN // This way you can automatically "implement the EventEmitter pattern" for a class // as the spec says. It's supposed to be used together with ART_EMBED_IMPLEMENTATION_EVENT_EMITTER // in the implementation of the class. -#define ART_EMBED_INTERFACE_EVENT_EMITTER(EventType, ItemType) - (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb;\ +#define ART_EMBED_INTERFACE_EVENT_EMITTER(EventType, ItemType) - (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb;\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb;\ \ -- (void)off:(EventType)event listener:(__GENERIC(ARTEventListener, ItemType) *)listener;\ -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener;\ -- (void)off;\ -\ -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^__art_nullable)())onTimeout; +- (void)off:(EventType)event listener:(ARTEventListener *)listener;\ +- (void)off:(ARTEventListener *)listener;\ +- (void)off; // This macro adds methods to a class implementation that just bridge calls to an internal // instance variable, which must be called _eventEmitter, of type ARTEventEmitter *. // It's supposed to be used together with ART_EMBED_IMPLEMENTATION_EVENT_EMITTER in the // header file of the class. -#define ART_EMBED_IMPLEMENTATION_EVENT_EMITTER(EventType, ItemType) - (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ +#define ART_EMBED_IMPLEMENTATION_EVENT_EMITTER(EventType, ItemType) - (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter on:event callback:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter on:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter once:event callback:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter once:cb];\ }\ \ -- (void)off:(EventType)event listener:listener {\ +- (void)off:(EventType)event listener:(ARTEventListener *)listener {\ [_eventEmitter off:event listener:listener];\ }\ \ -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener {\ +- (void)off:(ARTEventListener *)listener {\ [_eventEmitter off:listener];\ }\ - (void)off {\ [_eventEmitter off];\ }\ -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout {\ -return [_eventEmitter timed:listener deadline:deadline onTimeout:onTimeout];\ -}\ \ - (void)emit:(EventType)event with:(ItemType)data {\ [_eventEmitter emit:event with:data];\ } -ART_ASSUME_NONNULL_END +NS_ASSUME_NONNULL_END diff --git a/Source/ARTEventEmitter.m b/Source/ARTEventEmitter.m index f76068778..6c3642b0b 100644 --- a/Source/ARTEventEmitter.m +++ b/Source/ARTEventEmitter.m @@ -11,6 +11,7 @@ #import "ARTRealtime.h" #import "ARTRealtime+Private.h" #import "ARTRealtimeChannel.h" +#import "ARTGCD.h" @implementation NSMutableArray (AsSet) @@ -29,77 +30,112 @@ - (void)artRemoveWhere:(BOOL (^)(id))cond { @end -@interface ARTEventListener () +#pragma mark - ARTEvent -- (instancetype)initWithBlock:(void (^)(id __art_nonnull))block queue:(dispatch_queue_t)queue; -- (void)setTimerWithDeadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout; -- (void)off; +@implementation ARTEvent { + NSString *_value; +} +- (instancetype)initWithString:(NSString *)value { + if (self == [super init]) { + _value = value; + } + return self; +} + ++ (instancetype)newWithString:(NSString *)value { + return [[self alloc] initWithString:value]; +} + +- (NSString *)identification { + return _value; +} + +@end + +#pragma mark - ARTEventListener + +@interface ARTEventListener () +@property (readonly) BOOL timerIsRunning; +@property (readonly) BOOL hasTimer; @end @implementation ARTEventListener { - void (^_block)(id __art_nonnull); - _Nonnull dispatch_queue_t _queue; - _Nullable dispatch_block_t _timerBlock; + __weak NSNotificationCenter *_center; + __weak ARTEventEmitter *_eventHandler; + NSTimeInterval _timeoutDeadline; + void (^_timeoutBlock)(); + dispatch_block_t _work; } -- (instancetype)initWithBlock:(void (^)(id __art_nonnull))block queue:(dispatch_queue_t)queue { - self = [self init]; - if (self) { - _block = block; - _queue = queue; +- (instancetype)initWithId:(NSString *)eventId token:(id)token handler:(ARTEventEmitter *)eventHandler center:(NSNotificationCenter *)center { + if (self = [super init]) { + _eventId = eventId; + _token = token; + _center = center; + _eventHandler = eventHandler; + _timeoutDeadline = 0; + _timeoutBlock = nil; + _timerIsRunning = false; } return self; } -- (void)call:(id)argument { - [self cancelTimer]; - _block(argument); +- (void)dealloc { + [self removeObserver]; } -- (void)setTimerWithDeadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - [self cancelTimer]; - _timerBlock = dispatch_block_create(0, ^{ - onTimeout(); - }); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, deadline * NSEC_PER_SEC), _queue, _timerBlock); +- (void)removeObserver { + [self stopTimer]; + [_center removeObserver:_token]; } -- (void)cancelTimer { - if (_timerBlock) { - dispatch_block_cancel(_timerBlock); - } +- (BOOL)handled { + return _count++ > 0; } -- (void)off { - [self cancelTimer]; +- (ARTEventListener *)setTimer:(NSTimeInterval)timeoutDeadline onTimeout:(void (^)())timeoutBlock { + if (_timeoutBlock) { + NSAssert(false, @"timer is already set"); + } + _timeoutBlock = timeoutBlock; + _timeoutDeadline = timeoutDeadline; + return self; } -@end +- (void)timeout { + [_eventHandler off:self]; + if (_timeoutBlock) { + _timeoutBlock(); + } +} -@implementation ARTEventEmitterEntry +- (BOOL)hasTimer { + return _timeoutBlock != nil; +} -- (instancetype)initWithListener:(ARTEventListener *)listener once:(BOOL)once { - self = [self init]; - if (self) { - _listener = listener; - _once = once; +- (void)startTimer { + if (_timerIsRunning) { + NSAssert(false, @"timer is already running"); } - return self; + _timerIsRunning = true; + __weak typeof(self) weakSelf = self; + _work = artDispatchScheduled(_timeoutDeadline, [_eventHandler queue], ^{ + [weakSelf timeout]; + }); } -- (BOOL)isEqual:(id)object { - if ([object isKindOfClass:[self class]]) { - return self == object || self.listener == ((ARTEventEmitterEntry *)object).listener; - } - return self.listener == object; +- (void)stopTimer { + artDispatchCancel(nil); + artDispatchCancel(_work); + _timerIsRunning = false; } @end -@implementation ARTEventEmitter { - _Nonnull dispatch_queue_t _queue; -} +#pragma mark - ARTEventEmitter + +@implementation ARTEventEmitter - (instancetype)init { return [self initWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)]; @@ -108,59 +144,98 @@ - (instancetype)init { - (instancetype)initWithQueue:(dispatch_queue_t)queue { self = [super init]; if (self) { + _notificationCenter = [[NSNotificationCenter alloc] init]; _queue = queue; [self resetListeners]; } return self; } -- (ARTEventListener *)on:(id)event callback:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:false] event:event]; - return listener; -} - -- (ARTEventListener *)once:(id)event callback:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:true] event:event]; - return listener; +- (ARTEventListener *)on:(id)event callback:(void (^)(id __art_nonnull))cb { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + __weak __block ARTEventListener *weakListener; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + [weakListener stopTimer]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self addObject:eventToken toArrayWithKey:eventToken.eventId inDictionary:self.listeners]; + return eventToken; } -- (void)addOnEntry:(ARTEventEmitterEntry *)entry event:(id)event { - [self addObject:entry toArrayWithKey:event inDictionary:self.listeners]; +- (ARTEventListener *)once:(id)event callback:(void (^)(id __art_nonnull))cb { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + __weak __block ARTEventListener *weakListener; + __weak typeof(self) weakSelf = self; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + if ([weakListener handled]) return; + [weakListener removeObserver]; + [weakSelf removeObject:weakListener fromArrayWithKey:[weakListener eventId] inDictionary:[weakSelf listeners]]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self addObject:eventToken toArrayWithKey:eventToken.eventId inDictionary:self.listeners]; + return eventToken; } - (ARTEventListener *)on:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnAllEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:false]]; - return listener; + NSString *eventId = [NSString stringWithFormat:@"%p", self]; + __weak __block ARTEventListener *weakListener; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + [weakListener stopTimer]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self.anyListeners addObject:eventToken]; + return eventToken; } - (ARTEventListener *)once:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnAllEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:true]]; - return listener; -} - -- (void)addOnAllEntry:(ARTEventEmitterEntry *)entry { - [self.anyListeners addObject:entry]; + NSString *eventId = [NSString stringWithFormat:@"%p", self]; + __weak __block ARTEventListener *weakListener; + __weak typeof(self) weakSelf = self; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + if ([weakListener handled]) return; + [weakListener removeObserver]; + [[weakSelf anyListeners] removeObject:weakListener]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self.anyListeners addObject:eventToken]; + return eventToken; } -- (void)off:(id)event listener:(ARTEventListener *)listener { - [listener off]; - [self removeObject:listener fromArrayWithKey:event inDictionary:self.listeners where:^BOOL(id entry) { - return ((ARTEventEmitterEntry *)entry).listener == listener; - }]; +- (void)off:(id)event listener:(ARTEventListener *)listener { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + if (![eventId isEqualToString:listener.eventId]) return; + [listener removeObserver]; + @synchronized (_listeners) { + [self.listeners[listener.eventId] removeObject:listener]; + if ([self.listeners[listener.eventId] firstObject] == nil) { + [self.listeners removeObjectForKey:listener.eventId]; + } + } } - (void)off:(ARTEventListener *)listener { - [listener off]; - BOOL (^cond)(id) = ^BOOL(id entry) { - return ((ARTEventEmitterEntry *)entry).listener == listener; - }; - [self.anyListeners artRemoveWhere:cond]; - for (id event in [self.listeners allKeys]) { - [self removeObject:listener fromArrayWithKey:event inDictionary:self.listeners where:cond]; + [listener removeObserver]; + @synchronized (_listeners) { + [self.listeners[listener.eventId] removeObject:listener]; + } + @synchronized (_anyListeners) { + [self.anyListeners removeObject:listener]; } } @@ -169,101 +244,75 @@ - (void)off { } - (void)resetListeners { - for (NSArray *entries in [_listeners allValues]) { - for (ARTEventEmitterEntry *entry in entries) { - [entry.listener off]; + @synchronized (_listeners) { + for (NSArray *items in [_listeners allValues]) { + for (ARTEventListener *item in items) { + [item removeObserver]; + } } - } - for (ARTEventEmitterEntry *entry in _anyListeners) { - [entry.listener off]; + [_listeners removeAllObjects]; } _listeners = [[NSMutableDictionary alloc] init]; + + @synchronized (_anyListeners) { + for (ARTEventListener *item in _anyListeners) { + [item removeObserver]; + } + [_anyListeners removeAllObjects]; + } _anyListeners = [[NSMutableArray alloc] init]; } -- (void)emit:(id)event with:(id)data { - NSMutableArray *toCall = [[NSMutableArray alloc] init]; - NSMutableArray *toRemoveFromListeners = [[NSMutableArray alloc] init]; - NSMutableArray *toRemoveFromTotalListeners = [[NSMutableArray alloc] init]; - @try { - for (ARTEventEmitterEntry *entry in [self.listeners objectForKey:event]) { - if (entry.once) { - [toRemoveFromListeners addObject:entry]; - } - [toCall addObject:entry]; - } - - for (ARTEventEmitterEntry *entry in self.anyListeners) { - if (entry.once) { - [toRemoveFromTotalListeners addObject:entry]; - } - [toCall addObject:entry]; - } +- (void)emit:(id)event with:(id)data { + NSString *eventId; + if (event) { + eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + [self.notificationCenter postNotificationName:eventId object:data]; + [self.notificationCenter postNotificationName:[NSString stringWithFormat:@"%p", self] object:data]; } - @finally { - for (ARTEventEmitterEntry *entry in toRemoveFromListeners) { - [self removeObject:entry fromArrayWithKey:event inDictionary:self.listeners]; - [entry.listener off]; - } - for (ARTEventEmitterEntry *entry in toRemoveFromTotalListeners) { - @synchronized(self.anyListeners) { - [self.anyListeners removeObject:entry]; - } - [entry.listener off]; - } - for (ARTEventEmitterEntry *entry in toCall) { - [entry.listener call:data]; - } + else { + eventId = [NSString stringWithFormat:@"%p", self]; + [self.notificationCenter postNotificationName:eventId object:data]; } } - (void)addObject:(id)obj toArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - array = [[NSMutableArray alloc] init]; - [dict setObject:array forKey:key]; + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:key]; + } + if ([array indexOfObject:obj] == NSNotFound) { + [array addObject:obj]; + } } - [array addObject:obj]; } - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - return; - } - @synchronized(array) { + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + return; + } [array removeObject:obj]; - } - if ([array count] == 0) { - @synchronized(dict) { + if ([array count] == 0) { [dict removeObjectForKey:key]; } } } - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict where:(BOOL(^)(id))cond { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - return; - } - @synchronized(array) { + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + return; + } [array artRemoveWhere:cond]; - } - if ([array count] == 0) { - @synchronized(dict) { + if ([array count] == 0) { [dict removeObjectForKey:key]; } } } -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - __weak ARTEventEmitter *s = self; - __weak ARTEventListener *weakListener = listener; - [listener setTimerWithDeadline:deadline onTimeout:^void() { - [s off:weakListener]; - if (onTimeout) onTimeout(); - }]; - return listener; -} - @end diff --git a/Source/ARTGCD.h b/Source/ARTGCD.h index ddcb5498b..d43a8d57f 100644 --- a/Source/ARTGCD.h +++ b/Source/ARTGCD.h @@ -14,7 +14,9 @@ void artDispatchSpecifyMainQueue(); void artDispatchMainQueue(dispatch_block_t block); void artDispatchGlobalQueue(dispatch_block_t block); -dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduledOnMainQueue(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduledOnGlobalQueue(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_queue_t queue, dispatch_block_t block); void artDispatchCancel(dispatch_block_t block); #endif /* ARTGCD_h */ diff --git a/Source/ARTGCD.m b/Source/ARTGCD.m index 94798a4cc..2faf4ce86 100644 --- a/Source/ARTGCD.m +++ b/Source/ARTGCD.m @@ -30,12 +30,20 @@ void artDispatchGlobalQueue(dispatch_block_t block) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); } -dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block) { +dispatch_block_t artDispatchScheduledOnMainQueue(NSTimeInterval seconds, dispatch_block_t block) { + return artDispatchScheduled(seconds, dispatch_get_main_queue(), block); +} + +dispatch_block_t artDispatchScheduledOnGlobalQueue(NSTimeInterval seconds, dispatch_block_t block) { + return artDispatchScheduled(seconds, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_queue_t queue, dispatch_block_t block) { dispatch_block_t work = dispatch_block_create(0, block); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * seconds)), dispatch_get_main_queue(), work); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * seconds)), queue, work); return work; } void artDispatchCancel(dispatch_block_t block) { - dispatch_block_cancel(block); + if (block) dispatch_block_cancel(block); } diff --git a/Spec/Utilities.swift b/Spec/Utilities.swift index e5e0beb3b..02db3d62c 100644 --- a/Spec/Utilities.swift +++ b/Spec/Utilities.swift @@ -22,8 +22,8 @@ class Utilities: QuickSpec { var receivedBarOnce: Int? var receivedAll: Int? var receivedAllOnce: Int? - var listenerFoo1: ARTEventListener? - var listenerAll: ARTEventListener? + weak var listenerFoo1: ARTEventListener? + weak var listenerAll: ARTEventListener? beforeEach { eventEmitter = ARTEventEmitter() @@ -57,7 +57,7 @@ class Utilities: QuickSpec { eventEmitter.emit("qux", with:789) - expect(receivedAll).to(equal(789)) + expect(receivedAll).toEventually(equal(789), timeout: testTimeout) } it("should only call once listeners once for its event") { @@ -98,9 +98,9 @@ class Utilities: QuickSpec { } it("should remove the timeout") { - eventEmitter.timed(listenerFoo1!, deadline: 0.1, onTimeout: { + listenerFoo1!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) + }).startTimer() eventEmitter.off(listenerFoo1!) waitUntil(timeout: 0.3) { done in delay(0.15) { @@ -118,7 +118,7 @@ class Utilities: QuickSpec { expect(receivedFoo1).to(equal(111)) expect(receivedAll).to(equal(111)) } - + it("should stop receive events if off matches the listener's criteria") { eventEmitter.off("foo", listener: listenerFoo1!) eventEmitter.emit("foo", with: 111) @@ -152,12 +152,12 @@ class Utilities: QuickSpec { } it("should remove all timeouts") { - eventEmitter.timed(listenerFoo1!, deadline: 0.1, onTimeout: { + listenerFoo1!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) - eventEmitter.timed(listenerAll!, deadline: 0.1, onTimeout: { + }).startTimer() + listenerAll!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) + }).startTimer() eventEmitter.off() waitUntil(timeout: 0.3) { done in delay(0.15) { @@ -169,7 +169,7 @@ class Utilities: QuickSpec { context("the timed method") { it("should not call onTimeout if the deadline isn't reached") { - eventEmitter.timed(listenerFoo1!, deadline: 0.2, onTimeout: { + weak var timer = listenerFoo1!.setTimer(0.2, onTimeout: { fail("onTimeout callback shouldn't have been called") }) waitUntil(timeout: 0.4) { done in @@ -180,16 +180,17 @@ class Utilities: QuickSpec { done() } } + timer?.startTimer() } } it("should call onTimeout and off the listener if the deadline is reached") { var calledOnTimeout = false let beforeEmitting = NSDate() - eventEmitter.timed(listenerFoo1!, deadline: 0.3, onTimeout: { + listenerFoo1!.setTimer(0.3, onTimeout: { calledOnTimeout = true expect(NSDate()).to(beCloseTo(beforeEmitting.dateByAddingTimeInterval(0.3), within: 0.2)) - }) + }).startTimer() waitUntil(timeout: 0.5) { done in delay(0.35) { expect(calledOnTimeout).to(beTrue()) From f34482568114f5e04a81cadcda1ebd5203160ac7 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:31:14 +0000 Subject: [PATCH 02/27] Events for the EventEmitter --- Source/ARTAuth+Private.h | 9 +- Source/ARTAuth.m | 27 +++++- Source/ARTConnection+Private.h | 2 +- Source/ARTConnection.h | 7 ++ Source/ARTConnection.m | 34 ++++--- Source/ARTPresenceMap.m | 50 +++++++++-- Source/ARTPresenceMessage.h | 8 ++ Source/ARTPresenceMessage.m | 14 +++ Source/ARTRealtime+Private.h | 4 +- Source/ARTRealtime.m | 85 +++++++++--------- Source/ARTRealtimeChannel+Private.h | 7 +- Source/ARTRealtimeChannel.h | 19 ++-- Source/ARTRealtimeChannel.m | 132 +++++++++++++++------------- Source/ARTRealtimePresence.h | 12 +-- Source/ARTRealtimePresence.m | 16 ++-- Source/ARTTypes.h | 4 + Source/ARTTypes.m | 10 +++ 17 files changed, 287 insertions(+), 153 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index d94470ef5..22939bf0f 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -18,7 +18,7 @@ ART_ASSUME_NONNULL_BEGIN /// Messages related to the ARTAuth @protocol ARTAuthDelegate -@property (nonatomic, readonly) __GENERIC(ARTEventEmitter, NSNumber * /*ARTAuthorizationState*/, id) *authorizationEmitter; +@property (nonatomic, readonly) ARTEventEmitter *authorizationEmitter; - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails; @end @@ -70,4 +70,11 @@ ART_ASSUME_NONNULL_BEGIN @end +#pragma mark - ARTEvent + +@interface ARTEvent (AuthorizationState) +- (instancetype)initWithAuthorizationState:(ARTAuthorizationState)value; ++ (instancetype)newWithAuthorizationState:(ARTAuthorizationState)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 04edfcb03..37ec2e7e0 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -377,11 +377,11 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp if (lastDelegate) { // Only the last request should remain [lastDelegate.authorizationEmitter off]; - [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationSucceeded] callback:^(id null) { + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] callback:^(id null) { successBlock(_tokenDetails); [lastDelegate.authorizationEmitter off]; }]; - [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationFailed] callback:^(NSError *error) { + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] callback:^(NSError *error) { failureBlock(error); [lastDelegate.authorizationEmitter off]; }]; @@ -506,3 +506,26 @@ - (void)toTokenDetails:(ARTAuth *)auth callback:(void (^)(ARTTokenDetails * _Nul } @end + +NSString *ARTAuthorizationStateToStr(ARTAuthorizationState state) { + switch (state) { + case ARTAuthorizationSucceeded: + return @"Succeeded"; //0 + case ARTAuthorizationFailed: + return @"Failed"; //1 + } +} + +#pragma mark - ARTEvent + +@implementation ARTEvent (AuthorizationState) + +- (instancetype)initWithAuthorizationState:(ARTAuthorizationState)value { + return [self initWithString:[NSString stringWithFormat:@"ARTAuthorizationState%@", ARTAuthorizationStateToStr(value)]]; +} + ++ (instancetype)newWithAuthorizationState:(ARTAuthorizationState)value { + return [[self alloc] initWithAuthorizationState:value]; +} + +@end diff --git a/Source/ARTConnection+Private.h b/Source/ARTConnection+Private.h index d991b96c4..e713d9ab4 100644 --- a/Source/ARTConnection+Private.h +++ b/Source/ARTConnection+Private.h @@ -17,7 +17,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTConnection () -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *eventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *eventEmitter; @property(weak, nonatomic) ARTRealtime* realtime; @end diff --git a/Source/ARTConnection.h b/Source/ARTConnection.h index 4bec48dab..7802ccbf3 100644 --- a/Source/ARTConnection.h +++ b/Source/ARTConnection.h @@ -35,4 +35,11 @@ ART_EMBED_INTERFACE_EVENT_EMITTER(ARTRealtimeConnectionEvent, ARTConnectionState @end +#pragma mark - ARTEvent + +@interface ARTEvent (ConnectionEvent) +- (instancetype)initWithConnectionEvent:(ARTRealtimeConnectionEvent)value; ++ (instancetype)newWithConnectionEvent:(ARTRealtimeConnectionEvent)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index 376bca888..c4ae34002 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -78,39 +78,49 @@ - (NSString *)getRecoveryKey { } } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { - return [_eventEmitter on:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)on:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { + return [_eventEmitter on:[ARTEvent newWithConnectionEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(void (^)(ARTConnectionStateChange *))cb { +- (ARTEventListener *)on:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { - return [_eventEmitter once:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)once:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { + return [_eventEmitter once:[ARTEvent newWithConnectionEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(void (^)(ARTConnectionStateChange *))cb { +- (ARTEventListener *)once:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter once:cb]; } - (void)off { [_eventEmitter off]; } -- (void)off:(ARTRealtimeConnectionEvent)event listener:listener { - [_eventEmitter off:[NSNumber numberWithInt:event] listener:listener]; +- (void)off:(ARTRealtimeConnectionEvent)event listener:(ARTEventListener *)listener { + [_eventEmitter off:[ARTEvent newWithConnectionEvent:event] listener:listener]; } -- (void)off:(__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)listener { +- (void)off:(ARTEventListener *)listener { [_eventEmitter off:listener]; } - (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)data { - [_eventEmitter emit:[NSNumber numberWithInt:event] with:data]; + [_eventEmitter emit:[ARTEvent newWithConnectionEvent:event] with:data]; } -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - return [_eventEmitter timed:listener deadline:deadline onTimeout:onTimeout]; +@end + +#pragma mark - ARTEvent + +@implementation ARTEvent (ConnectionEvent) + +- (instancetype)initWithConnectionEvent:(ARTRealtimeConnectionEvent)value { + return [self initWithString:[NSString stringWithFormat:@"ARTRealtimeConnectionEvent%@", ARTRealtimeConnectionEventToStr(value)]]; +} + ++ (instancetype)newWithConnectionEvent:(ARTRealtimeConnectionEvent)value { + return [[self alloc] initWithConnectionEvent:value]; } @end diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 0b8bc4c65..963f4e6cb 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -19,9 +19,33 @@ typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { ARTPresenceSyncFailed //ItemType: ARTErrorInfo* }; +NSString *ARTPresenceSyncStateToStr(ARTPresenceSyncState state) { + switch (state) { + case ARTPresenceSyncInitialized: + return @"Initialized"; //0 + case ARTPresenceSyncStarted: + return @"Started"; //1 + case ARTPresenceSyncEnded: + return @"Ended"; //2 + case ARTPresenceSyncFailed: + return @"Failed"; //3 + } +} + +#pragma mark - ARTEvent + +@interface ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value; ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value; + +@end + +#pragma mark - ARTPresenceMap + @interface ARTPresenceMap () { ARTPresenceSyncState _syncState; - ARTEventEmitter *_syncEventEmitter; + ARTEventEmitter *_syncEventEmitter; NSMutableDictionary *_members; NSMutableSet *_localMembers; } @@ -184,7 +208,7 @@ - (void)reset { - (void)startSync { _syncSessionId++; _syncState = ARTPresenceSyncStarted; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncStarted] with:nil]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; } - (void)endSync { @@ -192,20 +216,20 @@ - (void)endSync { [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; [self reenterLocalMembersMissingFromSync]; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncEnded] with:[_members allValues]]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; } - (void)failsSync:(ARTErrorInfo *)error { [self reset]; _syncState = ARTPresenceSyncFailed; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncFailed] with:error]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] with:error]; [_syncEventEmitter off]; } - (void)onceSyncEnds:(void (^)(NSArray *))callback { if (self.syncInProgress) { - [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncEnded] callback:callback]; + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] callback:callback]; } else { callback([_members allValues]); @@ -214,7 +238,7 @@ - (void)onceSyncEnds:(void (^)(NSArray *))callback { - (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback { if (self.syncInProgress) { - [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncFailed] callback:callback]; + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] callback:callback]; } } @@ -233,3 +257,17 @@ - (NSString *)memberKey:(ARTPresenceMessage *) message { } @end + +#pragma mark - ARTEvent + +@implementation ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value { + return [self initWithString:[NSString stringWithFormat:@"ARTPresenceSyncState%@", ARTPresenceSyncStateToStr(value)]]; +} + ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value { + return [[self alloc] initWithPresenceSyncState:value]; +} + +@end diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index c0a044ee6..93150533c 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -7,6 +7,7 @@ // #import "ARTBaseMessage.h" +#import "ARTEventEmitter.h" /// Presence action type typedef NS_ENUM(NSUInteger, ARTPresenceAction) { @@ -32,4 +33,11 @@ ART_ASSUME_NONNULL_BEGIN @end +#pragma mark - ARTEvent + +@interface ARTEvent (PresenceAction) +- (instancetype)initWithPresenceAction:(ARTPresenceAction)value; ++ (instancetype)newWithPresenceAction:(ARTPresenceAction)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index a3d51041c..145ade075 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -86,3 +86,17 @@ - (NSUInteger)hash { return @"Update"; //4 } } + +#pragma mark - ARTEvent + +@implementation ARTEvent (PresenceAction) + +- (instancetype)initWithPresenceAction:(ARTPresenceAction)value { + return [self initWithString:[NSString stringWithFormat:@"ARTPresenceAction%@", ARTPresenceActionToStr(value)]]; +} + ++ (instancetype)newWithPresenceAction:(ARTPresenceAction)value { + return [[self alloc] initWithPresenceAction:value]; +} + +@end diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index fa907b64d..a4a359278 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -25,8 +25,8 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *internalEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNull *, NSNull *) *connectedEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, ARTConnectionStateChange *) *internalEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, NSNull *) *connectedEventEmitter; // State properties - (BOOL)shouldSendEvents; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 33282754d..28885658c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -46,13 +46,15 @@ - (void)setRetryIn:(NSTimeInterval)retryIn; @implementation ARTRealtime { BOOL _resuming; BOOL _renewingToken; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_pingEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_pingEventEmitter; NSDate *_startedReconnection; Class _transportClass; Class _reachabilityClass; id _transport; ARTFallback *_fallbacks; _Nonnull dispatch_queue_t _eventQueue; + __weak ARTEventListener *_connectingTimeoutListener; + dispatch_block_t _authenitcatingTimeoutWork; } @synthesize authorizationEmitter = _authorizationEmitter; @@ -247,9 +249,9 @@ - (void)ping:(void (^)(ARTErrorInfo *)) cb { }]; return; } - [_pingEventEmitter timed:[_pingEventEmitter once:cb] deadline:[ARTDefault realtimeRequestTimeout] onTimeout:^{ + [[[_pingEventEmitter once:cb] setTimer:[ARTDefault realtimeRequestTimeout] onTimeout:^{ cb([ARTErrorInfo createWithCode:ARTCodeErrorConnectionTimedOut status:ARTStateConnectionFailed message:@"timed out"]); - }]; + }] startTimer]; [self.transport sendPing]; } } @@ -276,14 +278,11 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo [self.connection setErrorReason:errorInfo]; } - dispatch_semaphore_t waitingForCurrentEventSemaphore = [self transitionSideEffects:stateChange]; + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; - [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; + [_internalEventEmitter emit:[ARTEvent newWithConnectionEvent:(ARTRealtimeConnectionEvent)state] with:stateChange]; - if (waitingForCurrentEventSemaphore) { - // Current event is handled. Start running timeouts. - dispatch_semaphore_signal(waitingForCurrentEventSemaphore); - } + [stateChangeEventListener startTimer]; } - (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { @@ -296,25 +295,24 @@ - (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:self.connection.state previous:self.connection.state event:ARTRealtimeConnectionEventUpdate reason:errorInfo retryIn:0]; - dispatch_semaphore_t semaphore = [self transitionSideEffects:stateChange]; + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; - if (semaphore) { - dispatch_semaphore_signal(semaphore); - } + [stateChangeEventListener startTimer]; } -- (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChange *)stateChange { +- (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateChange { ARTStatus *status = nil; - dispatch_semaphore_t waitingForCurrentEventSemaphore = nil; + ARTEventListener *stateChangeEventListener = nil; // Do not increase the reference count (avoid retain cycles): // i.e. the `unlessStateChangesBefore` is setting a timer and if the `ARTRealtime` instance is released before that timer, then it could create a leak. __weak __typeof(self) weakSelf = self; switch (stateChange.current) { case ARTRealtimeConnecting: { - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [weakSelf onConnectionTimeOut]; }]; + _connectingTimeoutListener = stateChangeEventListener; if (!_reachability) { _reachability = [[_reachabilityClass alloc] initWithLogger:self.logger]; @@ -362,7 +360,7 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } case ARTRealtimeClosing: { [_reachability off]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [weakSelf transition:ARTRealtimeClosed]; }]; [self.transport sendClose]; @@ -376,7 +374,7 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang _connection.id = nil; _transport = nil; self.rest.prioritizedHost = nil; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been closed"]]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been closed"]]; break; case ARTRealtimeFailed: status = [ARTStatus state:ARTStateConnectionFailed info:stateChange.reason]; @@ -384,7 +382,7 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang self.transport.delegate = nil; _transport = nil; self.rest.prioritizedHost = nil; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:stateChange.reason]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:stateChange.reason]; break; case ARTRealtimeDisconnected: { if (!_startedReconnection) { @@ -396,7 +394,9 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang }]; } if ([[NSDate date] timeIntervalSinceDate:_startedReconnection] >= _connectionStateTtl) { - [self transition:ARTRealtimeSuspended withErrorInfo:stateChange.reason]; + artDispatchScheduled(0, _eventQueue, ^{ + [self transition:ARTRealtimeSuspended withErrorInfo:stateChange.reason]; + }); return nil; } @@ -404,7 +404,7 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.disconnectedRetryTimeout]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:stateChange.retryIn do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; }]; break; @@ -414,10 +414,10 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.suspendedRetryTimeout]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:stateChange.retryIn do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; }]; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; break; } case ARTRealtimeConnected: { @@ -431,8 +431,8 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } }]; } - [_connectedEventEmitter emit:[NSNull null] with:nil]; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationSucceeded] with:nil]; + [_connectedEventEmitter emit:nil with:nil]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] with:nil]; break; } case ARTRealtimeInitialized: @@ -481,26 +481,17 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } [self.connection emit:stateChange.event with:stateChange]; - return waitingForCurrentEventSemaphore; + return stateChangeEventListener; } -- (_Nonnull dispatch_semaphore_t)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback __attribute__((warn_unused_result)) { - // Defer until next event loop execution so that any event emitted in the current one doesn't cancel the timeout. - ARTRealtimeConnectionState state = self.connection.state; - // Timeout should be dispatched after current event. - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), _eventQueue, ^{ - // Wait until the current event is done. - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); - if (state != self.connection.state) { - // Already changed; Ignore the timer. - return; +- (ARTEventListener *)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback __attribute__((warn_unused_result)) { + return [[_internalEventEmitter once:^(ARTConnectionStateChange *change) { + // Any state change cancels the timeout. + }] setTimer:deadline onTimeout:^{ + if (callback) { + callback(); } - [_internalEventEmitter timed:[_internalEventEmitter once:^(ARTConnectionStateChange *change) { - // Any state change cancels the timeout. - }] deadline:deadline onTimeout:callback]; - }); - return semaphore; + }]; } - (void)onHeartbeat { @@ -509,7 +500,7 @@ - (void)onHeartbeat { NSString *msg = [NSString stringWithFormat:@"ARTRealtime received a ping when in state %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; [self.logger warn:@"R:%p %@", self, msg]; } - [_pingEventEmitter emit:[NSNull null] with:nil]; + [_pingEventEmitter emit:nil with:nil]; } - (void)onConnected:(ARTProtocolMessage *)message { @@ -684,8 +675,10 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { // Transport instance couldn't exist anymore when `authorize` completes or reaches time out. __weak __typeof(self) weakSelf = self; - dispatch_block_t work = artDispatchScheduled([ARTDefault realtimeRequestTimeout], ^{ + // Schedule timeout handler + _authenitcatingTimeoutWork = artDispatchScheduled([ARTDefault realtimeRequestTimeout], _eventQueue, ^{ [weakSelf onConnectionTimeOut]; + // FIXME: should cancel the auth request as well. }); // Deactivate use of `ARTAuthDelegate`: `authorize` should complete without waiting for a CONNECTED state. @@ -694,7 +687,9 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { @try { [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { // Cancel scheduled work - artDispatchCancel(work); + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; + // It's still valid? switch ([[weakSelf connection] state]) { case ARTRealtimeClosing: diff --git a/Source/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index e20f4ad4b..dce8d5f4e 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -23,9 +23,10 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, strong, nonatomic) NSMutableArray *queuedMessages; @property (readwrite, strong, nonatomic, art_nullable) NSString *attachSerial; @property (readonly, getter=getClientId) NSString *clientId; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTChannelStateChange *) *statesEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSString *, ARTMessage *) *messagesEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTPresenceMessage *) *presenceEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *internalEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *statesEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter, ARTMessage *> *messagesEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *presenceEventEmitter; @property (readwrite, strong, nonatomic) ARTPresenceMap *presenceMap; @property (readwrite, assign, nonatomic) ARTPresenceAction lastPresenceAction; diff --git a/Source/ARTRealtimeChannel.h b/Source/ARTRealtimeChannel.h index 2d972d350..896b9735d 100644 --- a/Source/ARTRealtimeChannel.h +++ b/Source/ARTRealtimeChannel.h @@ -31,14 +31,14 @@ ART_ASSUME_NONNULL_BEGIN - (void)detach; - (void)detach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))callback; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(void (^)(ARTMessage *message))callback; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(NSString *)name callback:(void (^)(ARTMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(NSString *)name onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(void (^)(ARTMessage *message))callback; +- (ARTEventListener *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(NSString *)name callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(NSString *)name onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; - (void)unsubscribe; -- (void)unsubscribe:(__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)listener; -- (void)unsubscribe:(NSString *)name listener:(__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)listener; +- (void)unsubscribe:(ARTEventListener *__art_nullable)listener; +- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *__art_nullable)listener; - (BOOL)history:(ARTRealtimeHistoryQuery *__art_nullable)query callback:(void(^)(__GENERIC(ARTPaginatedResult, ARTMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback error:(NSError *__art_nullable *__art_nullable)errorPtr; @@ -46,4 +46,11 @@ ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTChannelStateChange *) @end +#pragma mark - ARTEvent + +@interface ARTEvent (ChannelEvent) +- (instancetype)initWithChannelEvent:(ARTChannelEvent)value; ++ (instancetype)newWithChannelEvent:(ARTChannelEvent)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index d03584674..1d50b49ab 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -26,13 +26,14 @@ #import "ARTDefault.h" #import "ARTRest.h" #import "ARTClientOptions.h" +#import "ARTTypes.h" @interface ARTRealtimeChannel () { ARTRealtimePresence *_realtimePresence; CFRunLoopTimerRef _attachTimer; CFRunLoopTimerRef _detachTimer; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_attachedEventEmitter; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_detachedEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_attachedEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_detachedEventEmitter; } @end @@ -58,6 +59,7 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _presenceEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _attachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _detachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; + _internalEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; } return self; } @@ -199,7 +201,7 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt } else { [self addToQueue:pm callback:queuedCallback]; - [self.realtime.internalEventEmitter once:[NSNumber numberWithInteger:ARTRealtimeConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { + [self.realtime.internalEventEmitter once:[ARTEvent newWithConnectionEvent:ARTRealtimeConnectionEventConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { [weakSelf sendQueuedMessages]; }]; } @@ -257,11 +259,11 @@ - (void)throwOnDisconnectedOrFailed { } } -- (ARTEventListener *)subscribe:(void (^)(ARTMessage * _Nonnull))callback { +- (ARTEventListener *)subscribe:(void (^)(ARTMessage * _Nonnull))callback { return [self subscribeWithAttachCallback:nil callback:callback]; } -- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { if (self.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -270,11 +272,11 @@ - (void)throwOnDisconnectedOrFailed { return [self.messagesEventEmitter on:cb]; } -- (ARTEventListener *)subscribe:(NSString *)name callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(NSString *)name callback:(void (^)(ARTMessage * _Nonnull))cb { return [self subscribe:name onAttach:nil callback:cb]; } -- (ARTEventListener *)subscribe:(NSString *)name onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(NSString *)name onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { if (self.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -287,47 +289,45 @@ - (void)unsubscribe { [self.messagesEventEmitter off]; } -- (void)unsubscribe:(ARTEventListener *)listener { +- (void)unsubscribe:(ARTEventListener *)listener { [self.messagesEventEmitter off:listener]; } -- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *)listener { +- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *)listener { [self.messagesEventEmitter off:name listener:listener]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { - return [self.statesEventEmitter on:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)on:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { + return [self.statesEventEmitter on:[ARTEvent newWithChannelEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(void (^)(ARTChannelStateChange *))cb { +- (ARTEventListener *)on:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { - return [self.statesEventEmitter once:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)once:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { + return [self.statesEventEmitter once:[ARTEvent newWithChannelEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(void (^)(ARTChannelStateChange *))cb { +- (ARTEventListener *)once:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter once:cb]; } - (void)off { [self.statesEventEmitter off]; } + - (void)off:(ARTChannelEvent)event listener:listener { - [self.statesEventEmitter off:[NSNumber numberWithInt:event] listener:listener]; + [self.statesEventEmitter off:[ARTEvent newWithChannelEvent:event] listener:listener]; } -- (void)off:(__GENERIC(ARTEventListener, ARTChannelStateChange *) *)listener { +- (void)off:(ARTEventListener *)listener { [self.statesEventEmitter off:listener]; } - (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data { - [self.statesEventEmitter emit:[NSNumber numberWithInt:event] with:data]; -} - -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - return [self.statesEventEmitter timed:listener deadline:deadline onTimeout:onTimeout]; + [self.statesEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; + [self.internalEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; } - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { @@ -341,14 +341,14 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { switch (state) { case ARTRealtimeChannelSuspended: - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [_attachedEventEmitter emit:nil with:status.errorInfo]; break; case ARTRealtimeChannelDetached: [self.presenceMap failsSync:status.errorInfo]; break; case ARTRealtimeChannelFailed: - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; - [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [_attachedEventEmitter emit:nil with:status.errorInfo]; + [_detachedEventEmitter emit:nil with:status.errorInfo]; [self.presenceMap failsSync:status.errorInfo]; break; default: @@ -359,25 +359,18 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { } - (void)dealloc { - if (self.statesEventEmitter) { - [self.statesEventEmitter off]; - } + [_statesEventEmitter off]; + [_internalEventEmitter off]; } -- (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { - // Defer until next event loop execution so that any event emitted in the current - // one doesn't cancel the timeout. - ARTRealtimeChannelState state = self.state; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), _eventQueue, ^{ - if (state != self.state) { - // Already changed; do nothing. - return; +- (ARTEventListener *)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { + return [[self.internalEventEmitter once:^(ARTChannelStateChange *stateChange) { + // Any state change cancels the timeout. + }] setTimer:deadline onTimeout:^{ + if (callback) { + callback(); } - // FIXME: should not use the global listener for internal purpose - [self timed:[self once:^(ARTChannelStateChange *stateChange) { - // Any state change cancels the timeout. - }] deadline:deadline onTimeout:callback]; - }); + }]; } /** @@ -425,9 +418,15 @@ - (ARTRealtimeChannelState)state { } - (void)setAttached:(ARTProtocolMessage *)message { - if (self.state == ARTRealtimeChannelFailed) { - return; + switch (self.state) { + case ARTRealtimeChannelDetaching: + case ARTRealtimeChannelFailed: + // Ignore + return; + default: + break; } + self.attachSerial = message.channelSerial; if (message.hasPresence) { @@ -455,7 +454,7 @@ - (void)setAttached:(ARTProtocolMessage *)message { ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; [self transition:ARTRealtimeChannelAttached status:status]; - [_attachedEventEmitter emit:[NSNull null] with:nil]; + [_attachedEventEmitter emit:nil with:nil]; } - (void)setDetached:(ARTProtocolMessage *)message { @@ -483,7 +482,7 @@ - (void)setDetached:(ARTProtocolMessage *)message { ARTErrorInfo *errorInfo = message.error ? message.error : [ARTErrorInfo createWithCode:0 message:@"channel has detached"]; ARTStatus *reason = [ARTStatus state:ARTStateNotAttached info:errorInfo]; [self detachChannel:reason]; - [_detachedEventEmitter emit:[NSNull null] with:nil]; + [_detachedEventEmitter emit:nil with:nil]; } - (void)detachChannel:(ARTStatus *)status { @@ -504,11 +503,12 @@ - (void)setSuspended:(ARTStatus *)status retryIn:(NSTimeInterval)retryTimeout { [self failQueuedMessages:status]; [self transition:ARTRealtimeChannelSuspended status:status]; __weak __typeof(self) weakSelf = self; - [self unlessStateChangesBefore:retryTimeout do:^{ + [[self unlessStateChangesBefore:retryTimeout do:^{ [weakSelf reattach:^(ARTErrorInfo *errorInfo) { - [weakSelf setSuspended:[ARTStatus state:ARTStateError info:errorInfo]]; + ARTStatus *status = [ARTStatus state:ARTStateError info:errorInfo]; + [weakSelf setSuspended:status]; } withReason:nil]; - }]; + }] startTimer]; } - (void)onMessage:(ARTProtocolMessage *)message { @@ -594,7 +594,7 @@ - (void)onSync:(ARTProtocolMessage *)message { } - (void)broadcastPresence:(ARTPresenceMessage *)pm { - [self.presenceEventEmitter emit:[NSNumber numberWithUnsignedInteger:pm.action] with:pm]; + [self.presenceEventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; } - (void)onError:(ARTProtocolMessage *)msg { @@ -673,16 +673,14 @@ - (void)attachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { attachMessage.action = ARTProtocolMessageAttach; attachMessage.channel = self.name; - __block BOOL timeouted = false; - [self.realtime send:attachMessage callback:nil]; - [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - timeouted = true; + [[self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + // Timeout ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateAttachTimedOut message:@"attach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateAttachTimedOut info:errorInfo]; [self setSuspended:status]; - }]; + }] startTimer]; if (![self.realtime shouldQueueEvents]) { ARTEventListener *reconnectedListener = [self.realtime.connectedEventEmitter once:^(NSNull *n) { @@ -750,21 +748,19 @@ - (void)detachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { detachMessage.action = ARTProtocolMessageDetach; detachMessage.channel = self.name; - __block BOOL timeouted = false; - [self.realtime send:detachMessage callback:nil]; - [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - timeouted = true; + [[self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + // Timeout ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateDetachTimedOut message:@"detach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateDetachTimedOut info:errorInfo]; [self transition:ARTRealtimeChannelAttached status:status]; - [_detachedEventEmitter emit:[NSNull null] with:errorInfo]; - }]; + [_detachedEventEmitter emit:nil with:errorInfo]; + }] startTimer]; if (![self.realtime shouldQueueEvents]) { ARTEventListener *reconnectedListener = [self.realtime.connectedEventEmitter once:^(NSNull *n) { - // Disconnected and connected while attaching, re-detach. + // Disconnected and connected while detaching, re-detach. [self detachAfterChecks:callback]; }]; [_detachedEventEmitter once:^(ARTErrorInfo *err) { @@ -843,3 +839,17 @@ - (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage * } @end + +#pragma mark - ARTEvent + +@implementation ARTEvent (ChannelEvent) + +- (instancetype)initWithChannelEvent:(ARTChannelEvent)value { + return [self initWithString:[NSString stringWithFormat:@"ARTChannelEvent%@",ARTChannelEventToStr(value)]]; +} + ++ (instancetype)newWithChannelEvent:(ARTChannelEvent)value { + return [[self alloc] initWithChannelEvent:value]; +} + +@end diff --git a/Source/ARTRealtimePresence.h b/Source/ARTRealtimePresence.h index 7d8d7c8da..1a67e42f0 100644 --- a/Source/ARTRealtimePresence.h +++ b/Source/ARTRealtimePresence.h @@ -45,14 +45,14 @@ ART_ASSUME_NONNULL_BEGIN - (void)leaveClient:(NSString *)clientId data:(id __art_nullable)data; - (void)leaveClient:(NSString *)clientId data:(id __art_nullable)data callback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(void (^)(ARTPresenceMessage *message))callback; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(ARTPresenceAction)action onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(void (^)(ARTPresenceMessage *message))callback; +- (ARTEventListener *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(ARTPresenceAction)action onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; - (void)unsubscribe; -- (void)unsubscribe:(__GENERIC(ARTEventListener, ARTPresenceMessage *) *)listener; -- (void)unsubscribe:(ARTPresenceAction)action listener:(__GENERIC(ARTEventListener, ARTPresenceMessage *) *)listener; +- (void)unsubscribe:(ARTEventListener *)listener; +- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener; - (void)history:(void(^)(__GENERIC(ARTPaginatedResult, ARTPresenceMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback; - (BOOL)history:(ARTRealtimeHistoryQuery *__art_nullable)query callback:(void(^)(__GENERIC(ARTPaginatedResult, ARTPresenceMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback error:(NSError *__art_nullable *__art_nullable)errorPtr; diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 91c47c230..67c4998dd 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -196,11 +196,11 @@ - (BOOL)getSyncComplete { return _channel.presenceMap.syncComplete; } -- (ARTEventListener *)subscribe:(void (^)(ARTPresenceMessage * _Nonnull))callback { +- (ARTEventListener *)subscribe:(void (^)(ARTPresenceMessage * _Nonnull))callback { return [self subscribeWithAttachCallback:nil callback:callback]; } -- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { if (_channel.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -209,29 +209,29 @@ - (BOOL)getSyncComplete { return [_channel.presenceEventEmitter on:cb]; } -- (ARTEventListener *)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { return [self subscribe:action onAttach:nil callback:cb]; } -- (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { if (_channel.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; } [_channel attach:onAttach]; - return [_channel.presenceEventEmitter on:[NSNumber numberWithUnsignedInteger:action] callback:cb]; + return [_channel.presenceEventEmitter on:[ARTEvent newWithPresenceAction:action] callback:cb]; } - (void)unsubscribe { [_channel.presenceEventEmitter off]; } -- (void)unsubscribe:(ARTEventListener *)listener { +- (void)unsubscribe:(ARTEventListener *)listener { [_channel.presenceEventEmitter off:listener]; } -- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener { - [_channel.presenceEventEmitter off:[NSNumber numberWithUnsignedInteger:action] listener:listener]; +- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener { + [_channel.presenceEventEmitter off:[ARTEvent newWithPresenceAction:action] listener:listener]; } @end diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 29c6b1a48..42b7c083c 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -9,6 +9,7 @@ #import #import "CompatibilityMacros.h" #import "ARTStatus.h" +#import "ARTEventEmitter.h" @class ARTStatus; @class ARTHttpResponse; @@ -179,6 +180,9 @@ NSString *generateNonce(); - (NSDictionary *__art_nullable)toJSON:(NSError *__art_nullable *__art_nullable)error; @end +@interface NSString (ARTEventIdentification) +@end + @interface NSString (ARTJsonCompatible) @end diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index e8c048e1c..700e788b7 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -142,6 +142,16 @@ - (NSString *)description { @end +#pragma mark - ARTEventIdentification + +@implementation NSString (ARTEventIdentification) + +- (NSString *)identification { + return self; +} + +@end + #pragma mark - ARTJsonCompatible @implementation NSString (ARTJsonCompatible) From 4260a4c27b5c3b6fbbb0e53e7ec8d6cf2f6d587d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:32:01 +0000 Subject: [PATCH 03/27] Fix: should cancel timers when connection times out --- Source/ARTRealtime.m | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 28885658c..24e90aaf5 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -583,7 +583,7 @@ - (void)onClosed { [self transition:ARTRealtimeClosed]; break; default: - NSAssert(false, @"Invalid Realtime state transitioning to Closed: expected Closing or Closed"); + NSAssert(false, @"Invalid Realtime state transitioning to Closed: expected Closing or Closed, has %@", ARTRealtimeConnectionStateToStr(self.connection.state)); break; } } @@ -597,7 +597,7 @@ - (void)onAuth { [self transportReconnectWithRenewedToken]; break; default: - [self.logger error:@"Invalid Realtime state: expected Connecting or Connected"]; + [self.logger error:@"Invalid Realtime state: expected Connecting or Connected, has %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; break; } } @@ -618,6 +618,13 @@ - (void)onError:(ARTProtocolMessage *)message { } - (void)onConnectionTimeOut { + // Cancel connecting scheduled work + [_connectingTimeoutListener stopTimer]; + _connectingTimeoutListener = nil; + // Cancel auth scheduled work + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; + ARTErrorInfo *error; if (self.auth.authorizing && (self.options.authUrl || self.options.authCallback)) { error = [ARTErrorInfo createWithCode:ARTCodeErrorAuthConfiguredProviderFailure status:ARTStateConnectionFailed message:@"timed out"]; From 6f4787ab720caab6fdef5850b21d0e0556d783eb Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:32:39 +0000 Subject: [PATCH 04/27] Fix: new state change can occur before receiving publishing acknowledgement --- Source/ARTRealtimeChannel.m | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 1d50b49ab..2f893055f 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -225,16 +225,27 @@ - (void)addToQueue:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb } - (void)sendMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb { - __block BOOL gotFailure = false; NSString *oldConnectionId = self.realtime.connection.id; + ARTProtocolMessage *pmSent = (ARTProtocolMessage *)[pm copy]; + + __block BOOL connectionStateHasChanged = false; __block ARTEventListener *listener = [self.realtime.internalEventEmitter on:^(ARTConnectionStateChange *stateChange) { - if (!(stateChange.current == ARTRealtimeClosed || stateChange.current == ARTRealtimeFailed - || (stateChange.current == ARTRealtimeConnected && ![oldConnectionId isEqual:self.realtime.connection.id] /* connection state lost */))) { + if (!(stateChange.current == ARTRealtimeClosed || + stateChange.current == ARTRealtimeFailed || + (stateChange.current == ARTRealtimeConnected && ![oldConnectionId isEqual:self.realtime.connection.id] /* connection state lost */))) { + // Ok return; } - gotFailure = true; + connectionStateHasChanged = true; [self.realtime.internalEventEmitter off:listener]; if (!cb) return; + + if (stateChange.current == ARTRealtimeClosed && stateChange.reason == nil && pmSent.action == ARTProtocolMessageClose) { + // No ack/nack is expected. + cb([ARTStatus state:ARTStateOk]); + return; + } + ARTErrorInfo *reason = stateChange.reason ? stateChange.reason : [ARTErrorInfo createWithCode:0 message:@"connection broken before receiving publishing acknowledgement."]; cb([ARTStatus state:ARTStateError info:reason]); }]; @@ -244,8 +255,9 @@ - (void)sendMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb } [self.realtime send:pm callback:^(ARTStatus *status) { + // New state change can occur before receiving publishing acknowledgement. [self.realtime.internalEventEmitter off:listener]; - if (cb && !gotFailure) cb(status); + if (cb && !connectionStateHasChanged) cb(status); }]; } From b38fa555ca2452af6b68a8e9d0c36f668941d8d1 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:36:27 +0000 Subject: [PATCH 05/27] Test suite: async forced transitions --- Spec/RealtimeClient.swift | 4 ++- Spec/RealtimeClientChannel.swift | 7 +++-- Spec/RealtimeClientConnection.swift | 43 +++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 02e87b1ab..96418187f 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -336,7 +336,9 @@ class RealtimeClient: QuickSpec { if start == nil { // Force - client.onSuspended() + delay(0) { + client.onSuspended() + } } case .Suspended: start = NSDate() diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index b020d3634..58a18c3cd 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -558,12 +558,15 @@ class RealtimeClientChannel: QuickSpec { } } - client.simulateSuspended(beforeSuspension: { done in + waitUntil(timeout: testTimeout) { done in channel.once(.Suspended) { stateChange in expect(stateChange?.reason).to(beNil()) done() } - }) + delay(0) { + client.onSuspended() + } + } expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 669d99fa5..990cc603a 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -257,12 +257,16 @@ class RealtimeClientConnection: QuickSpec { } case .Connected: if alreadyClosed { - client.onSuspended() + delay(0) { + client.onSuspended() + } } else if alreadyDisconnected { client.close() } else { events += [state] - client.onDisconnected() + delay(0) { + client.onDisconnected() + } } case .Disconnected: events += [state] @@ -577,11 +581,12 @@ class RealtimeClientConnection: QuickSpec { } } + var i = 0 waitUntil(timeout: testTimeout) { done in // Sends 50 messages from different clients to the same channel // 50 messages for 50 clients = 50*50 total messages // echo is off, so we need to subtract one message per client - let partialDone = AblyTests.splitDone(max*max - max, done: done) + let total = max*max - max for client in disposable { let channel = client.channels.get(channelName) expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) @@ -589,7 +594,10 @@ class RealtimeClientConnection: QuickSpec { channel.subscribe { message in expect(message.data as? String).to(equal("message_string")) sync.lock() - partialDone() + i += 1 + if i == total { + done() + } sync.unlock() } @@ -1064,6 +1072,13 @@ class RealtimeClientConnection: QuickSpec { } } } + + // This verifies that the pending message as been released and the publish callback is called only once! + waitUntil(timeout: testTimeout) { done in + delay(1.0) { + done() + } + } } it("connection state enters FAILED") { @@ -1181,6 +1196,7 @@ class RealtimeClientConnection: QuickSpec { } var ids = [String]() let max = 25 + let sync = NSLock() waitUntil(timeout: testTimeout) { done in for _ in 1...max { @@ -1197,11 +1213,13 @@ class RealtimeClientConnection: QuickSpec { return } expect(ids).toNot(contain(connectionId)) - ids.append(connectionId) + sync.lock() + ids.append(connectionId) if ids.count == max { done() } + sync.unlock() currentConnection.off() currentConnection.close() @@ -2052,6 +2070,10 @@ class RealtimeClientConnection: QuickSpec { options.autoConnect = false let expectedTime = 3.0 + options.authCallback = { tokenParams, completion in + // Ignore `completion` closure to force a time out + } + let previousConnectionStateTtl = ARTDefault.connectionStateTtl() defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } ARTDefault.setConnectionStateTtl(expectedTime) @@ -2099,11 +2121,14 @@ class RealtimeClientConnection: QuickSpec { // RTN14e it("connection state has been in the DISCONNECTED state for more than the default connectionStateTtl should change the state to SUSPENDED") { let options = AblyTests.commonAppSetup() - options.realtimeHost = "10.255.255.1" //non-routable IP address options.disconnectedRetryTimeout = 0.1 options.suspendedRetryTimeout = 0.5 options.autoConnect = false - let expectedTime = 1.0 + let expectedTime: NSTimeInterval = 1.0 + + options.authCallback = { _ in + // Force a timeout + } let previousConnectionStateTtl = ARTDefault.connectionStateTtl() defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } @@ -2562,7 +2587,9 @@ class RealtimeClientConnection: QuickSpec { fail("Shouldn't be called") } } - client.onDisconnected() + delay(0) { + client.onDisconnected() + } client.connection.once(.Connected) { _ in resumed = true channel.testSuite_injectIntoMethodBefore(#selector(channel.sendQueuedMessages)) { From 2cfc1b1243b5d3cb2e70e44bb85e6c5f32c4ad90 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:38:56 +0000 Subject: [PATCH 06/27] Test suite: ack order --- Spec/RealtimeClientChannel.swift | 39 +++++++++++++++++++---------- Spec/RealtimeClientConnection.swift | 30 +++++++++------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 58a18c3cd..7111ad263 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -2077,15 +2077,20 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") var resultClientId: String? - channel.subscribe() { message in - resultClientId = message.clientId - } let message = ARTMessage(name: nil, data: "message") message.clientId = "client_string" - channel.publish([message]) { errorInfo in - expect(errorInfo).to(beNil()) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe() { message in + resultClientId = message.clientId + partialDone() + } + channel.publish([message]) { errorInfo in + expect(errorInfo).to(beNil()) + partialDone() + } } expect(resultClientId).toEventually(equal(message.clientId), timeout: testTimeout) @@ -2305,12 +2310,17 @@ class RealtimeClientChannel: QuickSpec { let message = ARTMessage(name: nil, data: "message", clientId: options.clientId!) var resultClientId: String? - channel.subscribe() { message in - resultClientId = message.clientId - } - channel.publish([message]) { error in - expect(error).to(beNil()) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe() { message in + resultClientId = message.clientId + partialDone() + } + channel.publish([message]) { error in + expect(error).to(beNil()) + partialDone() + } } expect(resultClientId).toEventually(equal(message.clientId), timeout: testTimeout) @@ -2350,12 +2360,14 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") let message = ARTMessage(name: nil, data: "message", clientId: "john") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel.subscribe() { received in expect(received.clientId).to(equal(message.clientId)) - done() + partialDone() } channel.publish([message]) { error in expect(error).to(beNil()) + partialDone() } } } @@ -2388,15 +2400,16 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel.subscribe { message in expect(message.name).to(equal("event")) expect(message.data as? NSObject).to(equal("data")) expect(message.clientId).to(equal("foo")) - done() + partialDone() } - channel.publish("event", data: "data", clientId: "foo") { errorInfo in expect(errorInfo).to(beNil()) + partialDone() } } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 990cc603a..1f498de87 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -541,13 +541,6 @@ class RealtimeClientConnection: QuickSpec { } } - class TotalReach { - // Easy way to create an atomic var - static var shared = 0 - // This prevents others from using the default '()' initializer - private init() {} - } - // RTN5 it("basic operations should work simultaneously") { let options = AblyTests.commonAppSetup() @@ -1062,8 +1055,11 @@ class RealtimeClientConnection: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.attach() { error in expect(error).to(beNil()) - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) + channel.publish(nil, data: "message", callback: { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("connection broken before receiving publishing acknowledgement")) done() }) // Wait until the message is pushed to Ably first @@ -2198,22 +2194,20 @@ class RealtimeClientConnection: QuickSpec { } waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) client1.connection.once(.Connecting) { _ in expect(client1.resuming).to(beTrue()) - done() + partialDone() } - } - - waitUntil(timeout: testTimeout) { done in client1.connection.once(.Connected) { _ in expect(client1.resuming).to(beFalse()) expect(client1.connection.id).toNot(equal(firstConnection.id)) expect(client1.connection.key).toNot(equal(firstConnection.key)) - done() + partialDone() } } - - expect(states).to(equal([.Connecting, .Connected, .Disconnected, .Connecting, .Connected])) + + expect(states).toEventually(equal([.Connecting, .Connected, .Disconnected, .Connecting, .Connected]), timeout: testTimeout) } // RTN15b @@ -2566,7 +2560,7 @@ class RealtimeClientConnection: QuickSpec { // RTN15f it("ACK and NACK responses for published messages can only ever be received on the transport connection on which those messages were sent") { let options = AblyTests.commonAppSetup() - options.disconnectedRetryTimeout = 0.5 + options.disconnectedRetryTimeout = 1.5 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") @@ -3431,7 +3425,7 @@ class RealtimeClientConnection: QuickSpec { // RTN19b it("should resent the DETACH message if there are any pending channels") { let options = AblyTests.commonAppSetup() - options.disconnectedRetryTimeout = 0.1 + options.disconnectedRetryTimeout = 1.0 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") From fa43d0039909ad3b6290273af691b7bc128a55bd Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 11:39:57 +0000 Subject: [PATCH 07/27] Test suite: stop when there's no internet --- Spec/TestUtilities.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 55c7f7e51..e671f1e17 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -111,8 +111,7 @@ class AblyTests { let (responseData, responseError, _) = NSURLSessionServerTrustSync().get(request) if let error = responseError { - XCTFail(error.localizedDescription) - return options + fatalError(error.localizedDescription) } testApplication = JSON(data: responseData!) @@ -279,6 +278,9 @@ class NSURLSessionServerTrustSync: NSObject, NSURLSessionDelegate, NSURLSessionT responseError = error httpResponse = response } + else if let error = error { + responseError = error + } requestCompleted = true } task.resume() From 1dc13f07a8edbd1ec7923d1b7b1683baa62e7a29 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 10 Mar 2017 19:38:56 +0000 Subject: [PATCH 08/27] Fix: instance objects released to soon --- Examples/Tests/TestsTests/TestsTests.swift | 6 ++++-- Tests/ARTRealtimeRecoverTest.m | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/Tests/TestsTests/TestsTests.swift b/Examples/Tests/TestsTests/TestsTests.swift index e6cff7f7f..8a7f405f3 100644 --- a/Examples/Tests/TestsTests/TestsTests.swift +++ b/Examples/Tests/TestsTests/TestsTests.swift @@ -64,8 +64,9 @@ class TestsTests: XCTestCase { self.waitForExpectationsWithTimeout(10, handler: nil) let backgroundRealtimeExpectation = self.expectationWithDescription("Realtime in a Background Queue") + var realtime: ARTRealtime! //strong reference NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let realtime = ARTRealtime(key: key as String) + realtime = ARTRealtime(key: key as String) realtime.channels.get("foo").attach { _ in defer { backgroundRealtimeExpectation.fulfill() } } @@ -73,8 +74,9 @@ class TestsTests: XCTestCase { self.waitForExpectationsWithTimeout(10, handler: nil) let backgroundRestExpectation = self.expectationWithDescription("Rest in a Background Queue") + var rest: ARTRest! //strong reference NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let rest = ARTRest(key: key as String) + rest = ARTRest(key: key as String) rest.channels.get("foo").history { _ in defer { backgroundRestExpectation.fulfill() } } diff --git a/Tests/ARTRealtimeRecoverTest.m b/Tests/ARTRealtimeRecoverTest.m index 47f099ffa..8b79b81da 100644 --- a/Tests/ARTRealtimeRecoverTest.m +++ b/Tests/ARTRealtimeRecoverTest.m @@ -53,9 +53,10 @@ - (void)testRecoverDisconnected { [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; __weak XCTestExpectation *expectation2 = [self expectationWithDescription:[NSString stringWithFormat:@"%s-2", __FUNCTION__]]; + __block ARTRealtime *realtimeNonRecovered; [realtime.connection once:ARTRealtimeConnectionEventDisconnected callback:^(ARTConnectionStateChange *stateChange) { options.recover = nil; - ARTRealtime *realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; + realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *c2 = [realtimeNonRecovered.channels get:channelName]; // Sending other message to the same channel to check if the recovered connection receives it [c2 publish:nil data:c2Message callback:^(ARTErrorInfo *errorInfo) { From 44bf0fe7b682858caf8f3db793f6ef8ba5bd8db4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 12 Mar 2017 16:28:54 +0000 Subject: [PATCH 09/27] Performed a static analysis from Xcode --- Source/ARTAuth+Private.h | 2 +- Source/ARTAuth.m | 5 ++++- Source/ARTChannels+Private.h | 2 +- Source/ARTConnection.m | 2 +- Source/ARTConnectionDetails.m | 2 +- Source/ARTCrypto+Private.h | 2 +- Source/ARTDataEncoder.h | 2 +- Source/ARTEventEmitter.m | 2 +- Source/ARTFallback.h | 2 +- Source/ARTFallback.m | 4 ++-- Source/ARTHttp.h | 6 +++--- Source/ARTJsonLikeEncoder.m | 3 +++ Source/ARTOSReachability.m | 5 ++--- Source/ARTRealtime.m | 3 +-- Source/ARTRealtimeTransport.h | 2 +- Source/ARTRest+Private.h | 2 +- Source/ARTRestPresence.h | 4 ++-- Source/ARTStats.h | 20 ++++++++++---------- Source/ARTStatus.h | 2 +- Source/ARTTokenParams.h | 6 +++--- 20 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index 22939bf0f..6c7645b1c 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -42,7 +42,7 @@ ART_ASSUME_NONNULL_BEGIN - (ARTTokenParams *)mergeParams:(ARTTokenParams *)customParams; - (NSURL *)buildURL:(ARTAuthOptions *)options withParams:(ARTTokenParams *)params; -- (NSMutableURLRequest *)buildRequest:(ARTAuthOptions *)options withParams:(ARTTokenParams *)params; +- (NSMutableURLRequest *)buildRequest:(nullable ARTAuthOptions *)options withParams:(nullable ARTTokenParams *)params; // Execute the received ARTTokenRequest - (void)executeTokenRequest:(ARTTokenRequest *)tokenRequest callback:(void (^)(ARTTokenDetails *__art_nullable tokenDetails, NSError *__art_nullable error))callback; diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 37ec2e7e0..916faad2f 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -402,7 +402,10 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp _tokenDetails = tokenDetails; _method = ARTAuthMethodToken; - if (lastDelegate) { + if (!tokenDetails) { + failureBlock([ARTErrorInfo createWithCode:0 message:@"Token details are empty"]); + } + else if (lastDelegate) { [lastDelegate auth:self didAuthorize:tokenDetails]; } else { diff --git a/Source/ARTChannels+Private.h b/Source/ARTChannels+Private.h index eb37ccbfb..7e53f5d9c 100644 --- a/Source/ARTChannels+Private.h +++ b/Source/ARTChannels+Private.h @@ -17,7 +17,7 @@ extern NSString* (^__art_nullable ARTChannels_getChannelNamePrefix)(); @protocol ARTChannelsDelegate -- (id)makeChannel:(NSString *)channel options:(ARTChannelOptions *)options; +- (id)makeChannel:(NSString *)channel options:(nullable ARTChannelOptions *)options; @end diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index c4ae34002..b8a571dbe 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -20,7 +20,7 @@ @implementation ARTConnection { } - (instancetype)initWithRealtime:(ARTRealtime *)realtime { - if (self == [super init]) { + if (self = [super init]) { _queue = dispatch_queue_create("io.ably.realtime.connection", DISPATCH_QUEUE_SERIAL); _eventEmitter = [[ARTEventEmitter alloc] initWithQueue:_queue]; _realtime = realtime; diff --git a/Source/ARTConnectionDetails.m b/Source/ARTConnectionDetails.m index db7e45f1d..6489c6528 100644 --- a/Source/ARTConnectionDetails.m +++ b/Source/ARTConnectionDetails.m @@ -17,7 +17,7 @@ - (instancetype)initWithClientId:(NSString *__art_nullable)clientId maxInboundRate:(NSInteger)maxInboundRate connectionStateTtl:(NSTimeInterval)connectionStateTtl serverId:(NSString *)serverId { - if (self == [super init]) { + if (self = [super init]) { _clientId = clientId; _connectionKey = connectionKey; _maxMessageSize = maxMessageSize; diff --git a/Source/ARTCrypto+Private.h b/Source/ARTCrypto+Private.h index 395bb5368..b9b86157d 100644 --- a/Source/ARTCrypto+Private.h +++ b/Source/ARTCrypto+Private.h @@ -50,7 +50,7 @@ ART_ASSUME_NONNULL_BEGIN + (int)defaultKeyLength; + (int)defaultBlockLength; -+ (NSData *)generateSecureRandomData:(size_t)length; ++ (nullable NSData *)generateSecureRandomData:(size_t)length; + (id)cipherWithParams:(ARTCipherParams *)params; diff --git a/Source/ARTDataEncoder.h b/Source/ARTDataEncoder.h index e9e886790..01d1ca750 100644 --- a/Source/ARTDataEncoder.h +++ b/Source/ARTDataEncoder.h @@ -37,7 +37,7 @@ ART_ASSUME_NONNULL_BEGIN + (NSString *)artAddEncoding:(NSString *)encoding toString:(NSString *__art_nullable)s; - (NSString *)artLastEncoding; -- (NSString *)artRemoveLastEncoding; +- (nullable NSString *)artRemoveLastEncoding; @end diff --git a/Source/ARTEventEmitter.m b/Source/ARTEventEmitter.m index 6c3642b0b..48c3948fc 100644 --- a/Source/ARTEventEmitter.m +++ b/Source/ARTEventEmitter.m @@ -37,7 +37,7 @@ @implementation ARTEvent { } - (instancetype)initWithString:(NSString *)value { - if (self == [super init]) { + if (self = [super init]) { _value = value; } return self; diff --git a/Source/ARTFallback.h b/Source/ARTFallback.h index 4301fe4a0..10e2483ed 100644 --- a/Source/ARTFallback.h +++ b/Source/ARTFallback.h @@ -29,7 +29,7 @@ ART_ASSUME_NONNULL_BEGIN /** returns a random fallback host, returns null when all hosts have been popped. */ --(NSString *) popFallbackHost; +- (nullable NSString *)popFallbackHost; @end diff --git a/Source/ARTFallback.m b/Source/ARTFallback.m index 9d76151fe..5aeb283e4 100644 --- a/Source/ARTFallback.m +++ b/Source/ARTFallback.m @@ -53,10 +53,10 @@ - (instancetype)init { } - (NSString *)popFallbackHost { - if([self.hosts count] ==0) { + if ([self.hosts count] ==0) { return nil; } - NSString *host= [self.hosts lastObject]; + NSString *host = [self.hosts lastObject]; [self.hosts removeLastObject]; return host; } diff --git a/Source/ARTHttp.h b/Source/ARTHttp.h index a87dfea67..6cf6db6d1 100644 --- a/Source/ARTHttp.h +++ b/Source/ARTHttp.h @@ -38,9 +38,9 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTHttpResponse : NSObject @property (readonly, assign, nonatomic) int status; -@property (readwrite, strong, nonatomic) ARTErrorInfo *error; -@property (art_nullable, readonly, strong, nonatomic) NSDictionary *headers; -@property (art_nullable, readonly, strong, nonatomic) NSData *body; +@property (nullable, readwrite, nonatomic) ARTErrorInfo *error; +@property (nullable, readonly, nonatomic) NSDictionary *headers; +@property (nullable, readonly, nonatomic) NSData *body; - (instancetype)init; - (instancetype)initWithStatus:(int)status headers:(art_nullable NSDictionary *)headers body:(art_nullable NSData *)body; diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 5614be074..49a67ec00 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -668,6 +668,9 @@ - (ARTStatsResourceCount *)statsResourceCountFromDictionary:(NSDictionary *)inpu - (NSError *)decodeError:(NSData *)error { NSDictionary *decodedError = [[self decodeDictionary:error] valueForKey:@"error"]; + if (!decodedError) { + return nil; + } NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"", NSLocalizedFailureReasonErrorKey: decodedError[@"message"], diff --git a/Source/ARTOSReachability.m b/Source/ARTOSReachability.m index 2aebbe1e9..411d3a857 100644 --- a/Source/ARTOSReachability.m +++ b/Source/ARTOSReachability.m @@ -25,8 +25,7 @@ @implementation ARTOSReachability { } - (instancetype)initWithLogger:(ARTLog *)logger { - self = [super self]; - if (self) { + if (self = [super init]) { _logger = logger; if (ARTOSReachability_instances == nil) { _instances = [[NSMutableDictionary alloc] init]; @@ -84,4 +83,4 @@ - (void)dealloc { } } -@end \ No newline at end of file +@end diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 24e90aaf5..4ed06b285 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -448,11 +448,11 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } } } else if (![self shouldQueueEvents]) { - [self failQueuedMessages:status]; ARTStatus *channelStatus = status; if (!channelStatus) { channelStatus = [self defaultError]; } + [self failQueuedMessages:channelStatus]; // For every Channel for (ARTRealtimeChannel* channel in self.channels) { switch (channel.state) { @@ -919,7 +919,6 @@ - (void)nack:(ARTProtocolMessage *)message { // we can handle it gracefully by only processing the // relevant portion of the response count -= (int)(self.pendingMessageStartSerial - serial); - serial = self.pendingMessageStartSerial; } NSRange nackRange; diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 0da99049c..18974d1e7 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -66,7 +66,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { @protocol ARTRealtimeTransport -- (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial; +- (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(nullable NSString *)resumeKey connectionSerial:(nullable NSNumber *)connectionSerial; @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; diff --git a/Source/ARTRest+Private.h b/Source/ARTRest+Private.h index c99f65b2f..9cd45b325 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -44,7 +44,7 @@ ART_ASSUME_NONNULL_BEGIN - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthentication)authOption completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; -- (id)internetIsUp:(void (^)(BOOL isUp))cb; +- (nullable id)internetIsUp:(void (^)(BOOL isUp))cb; @end diff --git a/Source/ARTRestPresence.h b/Source/ARTRestPresence.h index d7c9150a2..71efa5e39 100644 --- a/Source/ARTRestPresence.h +++ b/Source/ARTRestPresence.h @@ -17,8 +17,8 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTPresenceQuery : NSObject @property (nonatomic, readwrite) NSUInteger limit; -@property (nonatomic, strong, readwrite) NSString *clientId; -@property (nonatomic, strong, readwrite) NSString *connectionId; +@property (nullable, nonatomic, strong, readwrite) NSString *clientId; +@property (nullable, nonatomic, strong, readwrite) NSString *connectionId; - (instancetype)init; - (instancetype)initWithClientId:(NSString *__art_nullable)clientId connectionId:(NSString *__art_nullable)connectionId; diff --git a/Source/ARTStats.h b/Source/ARTStats.h index 60670f090..4806d60b9 100644 --- a/Source/ARTStats.h +++ b/Source/ARTStats.h @@ -44,9 +44,9 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsMessageCount *presence; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsMessageCount *)all - messages:(ARTStatsMessageCount *)messages - presence:(ARTStatsMessageCount *)presence; +- (instancetype)initWithAll:(nullable ARTStatsMessageCount *)all + messages:(nullable ARTStatsMessageCount *)messages + presence:(nullable ARTStatsMessageCount *)presence; + (instancetype)empty; @@ -60,10 +60,10 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsMessageTypes *webhook; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsMessageTypes *)all - realtime:(ARTStatsMessageTypes *)realtime - rest:(ARTStatsMessageTypes *)rest - webhook:(ARTStatsMessageTypes *)webhook; +- (instancetype)initWithAll:(nullable ARTStatsMessageTypes *)all + realtime:(nullable ARTStatsMessageTypes *)realtime + rest:(nullable ARTStatsMessageTypes *)rest + webhook:(nullable ARTStatsMessageTypes *)webhook; + (instancetype)empty; @@ -95,9 +95,9 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsResourceCount *tls; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsResourceCount *)all - plain:(ARTStatsResourceCount *)plain - tls:(ARTStatsResourceCount *)tls; +- (instancetype)initWithAll:(nullable ARTStatsResourceCount *)all + plain:(nullable ARTStatsResourceCount *)plain + tls:(nullable ARTStatsResourceCount *)tls; + (instancetype)empty; diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 7e7c53c38..fb3b8f1fe 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -39,7 +39,7 @@ typedef CF_ENUM(NSUInteger, ARTCodeError) { // FIXME: check hard coded errors ARTCodeErrorAPIKeyMissing = 80001, ARTCodeErrorConnectionTimedOut = 80014, - ARTCodeErrorAuthConfiguredProviderFailure = 80019 + ARTCodeErrorAuthConfiguredProviderFailure = 80019, }; ART_ASSUME_NONNULL_BEGIN diff --git a/Source/ARTTokenParams.h b/Source/ARTTokenParams.h index b53127967..16e170cd0 100644 --- a/Source/ARTTokenParams.h +++ b/Source/ARTTokenParams.h @@ -32,14 +32,14 @@ ART_ASSUME_NONNULL_BEGIN /** A clientId to associate with this token. */ -@property (art_nullable, nonatomic, copy, readwrite) NSString *clientId; +@property (nullable, nonatomic, copy, readwrite) NSString *clientId; /** Timestamp (in millis since the epoch) of this request. Timestamps, in conjunction with the nonce, are used to prevent n requests from being replayed. */ -@property (art_nullable, nonatomic, copy, readwrite) NSDate *timestamp; +@property (nullable, nonatomic, copy, readwrite) NSDate *timestamp; -@property (nonatomic, readonly, strong) NSString *nonce; +@property (nullable, nonatomic, readonly, strong) NSString *nonce; - (instancetype)init; - (instancetype)initWithClientId:(NSString *__art_nullable)clientId; From 345f16d55da3646067f160065f692792c92f98f2 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 12 Mar 2017 23:36:45 +0000 Subject: [PATCH 10/27] fixup! Test suite: ack order --- Spec/RealtimeClientConnection.swift | 4 +++- Spec/RealtimeClientPresence.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 1f498de87..890667749 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2785,18 +2785,20 @@ class RealtimeClientConnection: QuickSpec { defer { client.dispose(); client.close() } let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.Connected) { _ in expect(client.connection.serial).to(equal(-1)) expect(client.connection.recoveryKey).to(equal("\(client.connection.key!):\(client.connection.serial)")) } channel.publish(nil, data: "message") { error in expect(error).to(beNil()) + partialDone() } channel.subscribe { message in expect(message.data as? String).to(equal("message")) expect(client.connection.serial).to(equal(0)) channel.unsubscribe() - done() + partialDone() } } } diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 9ae631b62..05fdcc216 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -626,10 +626,12 @@ class RealtimeClientPresence: QuickSpec { let channel2 = client2.channels.get(channel1.name) waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel2.presence.enterClient("Client 2", data: nil) { error in expect(error).to(beNil()) expect(channel2.queuedMessages).to(haveCount(0)) expect(channel2.state).to(equal(ARTRealtimeChannelState.Attached)) + partialDone() } channel2.presence.subscribe(.Enter) { _ in if channel2.presence.syncComplete { @@ -639,7 +641,7 @@ class RealtimeClientPresence: QuickSpec { expect(channel2.presenceMap.members).to(haveCount(1)) } channel2.presence.unsubscribe() - done() + partialDone() } expect(channel2.queuedMessages).to(haveCount(1)) From 676c26b1a7434d91175427fc228c5998a556ee0d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 12 Mar 2017 23:39:52 +0000 Subject: [PATCH 11/27] Memory leak: call session invalidate to dispose of its strong reference to the delegate --- Source/ARTEventEmitter.h | 2 +- Source/ARTHttp.m | 24 +++++++++++++++--------- Source/ARTRest+Private.h | 2 +- Source/ARTURLSessionServerTrust.h | 2 ++ Source/ARTURLSessionServerTrust.m | 4 ++++ 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Source/ARTEventEmitter.h b/Source/ARTEventEmitter.h index 9f0c318b6..50877ff54 100644 --- a/Source/ARTEventEmitter.h +++ b/Source/ARTEventEmitter.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTEventListener : NSObject @property (nonatomic, readonly) NSString *eventId; -@property (nonatomic, readonly) id token; +@property (weak, nonatomic, readonly) id token; @property (nonatomic, readonly) NSUInteger count; - (instancetype)init NS_UNAVAILABLE; diff --git a/Source/ARTHttp.m b/Source/ARTHttp.m index 4c0700c8d..d39cb1ce9 100644 --- a/Source/ARTHttp.m +++ b/Source/ARTHttp.m @@ -163,6 +163,10 @@ - (instancetype)init { return self; } +- (void)dealloc { + [_urlSession invalidateAndCancel]; +} + - (instancetype)initWithBaseUrl:(NSURL *)baseUrl { self = [self init]; if (self) { @@ -175,18 +179,19 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT [self.logger debug:@"%@ %@", request.HTTPMethod, request.URL.absoluteString]; [self.logger verbose:@"Headers %@", request.allHTTPHeaderFields]; + __weak typeof(self) weakSelf = self; [_urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; dispatch_async(_queue, ^{ if (error) { - [self.logger error:@"%@ %@: error %@", request.HTTPMethod, request.URL.absoluteString, error]; + [[weakSelf logger] error:@"%@ %@: error %@", request.HTTPMethod, request.URL.absoluteString, error]; } else { - [self.logger debug:@"%@ %@: statusCode %ld", request.HTTPMethod, request.URL.absoluteString, (long)httpResponse.statusCode]; - [self.logger verbose:@"Headers %@", httpResponse.allHeaderFields]; + [[weakSelf logger] debug:@"%@ %@: statusCode %ld", request.HTTPMethod, request.URL.absoluteString, (long)httpResponse.statusCode]; + [[weakSelf logger] verbose:@"Headers %@", httpResponse.allHeaderFields]; NSString *headerErrorMessage = httpResponse.allHeaderFields[@"X-Ably-ErrorMessage"]; if (headerErrorMessage && ![headerErrorMessage isEqualToString:@""]) { - [self.logger warn:@"%@", headerErrorMessage]; + [[weakSelf logger] warn:@"%@", headerErrorMessage]; } } callback(httpResponse, data, error); @@ -216,19 +221,20 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT request.HTTPBody = artRequest.body; [self.logger debug:@"ARTHttp: makeRequest %@", [request allHTTPHeaderFields]]; - [self.urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + __weak typeof(self) weakSelf = self; + [_urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - [self.logger verbose:@"ARTHttp: Got response %@, err %@", response, error]; + [[weakSelf logger] verbose:@"ARTHttp: Got response %@, err %@", response, error]; if(error) { - [self.logger error:@"ARTHttp receieved error: %@", error]; + [[weakSelf logger] error:@"ARTHttp receieved error: %@", error]; cb([ARTHttpResponse responseWithStatus:500 headers:nil body:nil]); } else { if (httpResponse) { int status = (int)httpResponse.statusCode; - [self.logger debug:@"ARTHttp response status is %d", status]; - [self.logger verbose:@"ARTHttp received response %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]]; + [[weakSelf logger] debug:@"ARTHttp response status is %d", status]; + [[weakSelf logger] verbose:@"ARTHttp received response %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]]; dispatch_async(_queue, ^{ cb([ARTHttpResponse responseWithStatus:status headers:httpResponse.allHeaderFields body:data]); diff --git a/Source/ARTRest+Private.h b/Source/ARTRest+Private.h index 9cd45b325..2de208be9 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -25,7 +25,7 @@ ART_ASSUME_NONNULL_BEGIN // Private prioritized host for testing only (overrides the current `restHost`) @property (readwrite, strong, nonatomic, art_nullable) NSString *prioritizedHost; -@property (nonatomic, strong) id httpExecutor; +@property (nonatomic, weak) id httpExecutor; @property (nonatomic, readonly, getter=getBaseUrl) NSURL *baseUrl; diff --git a/Source/ARTURLSessionServerTrust.h b/Source/ARTURLSessionServerTrust.h index a5ab366e6..3f96a82ca 100644 --- a/Source/ARTURLSessionServerTrust.h +++ b/Source/ARTURLSessionServerTrust.h @@ -15,6 +15,8 @@ ART_ASSUME_NONNULL_BEGIN - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; +- (void)invalidateAndCancel; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTURLSessionServerTrust.m b/Source/ARTURLSessionServerTrust.m index 5bbde01d5..429b11515 100644 --- a/Source/ARTURLSessionServerTrust.m +++ b/Source/ARTURLSessionServerTrust.m @@ -23,6 +23,10 @@ - (instancetype)init { return self; } +- (void)invalidateAndCancel { + [_session invalidateAndCancel]; +} + - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { NSURLSessionDataTask *task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { callback((NSHTTPURLResponse *)response, data, error); From 8bdb646cea7b76da9c26af9f943a6165d96fc155 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 15 Mar 2017 10:03:41 +0000 Subject: [PATCH 12/27] fixup! Test suite: ack order --- Tests/ARTRealtimePresenceTest.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index b6924a37b..fbe6ecd07 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -767,15 +767,20 @@ - (void)testEnterClient { ARTRealtimeChannel *channel = [realtime.channels get:@"channelName"]; [channel.presence enterClient:clientId data:nil callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); - [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { + [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { XCTAssert(!error); XCTAssertEqual(2, members.count); ARTPresenceMessage *m0 = [members objectAtIndex:0]; - XCTAssertEqualObjects(m0.clientId, clientId2); + // cannot guarantee the order + if (![m0.clientId isEqualToString:clientId2] && ![m0.clientId isEqualToString:clientId]) { + XCTFail(@"clientId1 is different from what's expected"); + } ARTPresenceMessage *m1 = [members objectAtIndex:1]; - XCTAssertEqualObjects(m1.clientId, clientId); + if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId]) { + XCTFail(@"clientId2 is different from what's expected"); + } [expectation fulfill]; }]; }]; From 39d8b20f3a4afa7e8bc7d48141094459acd11f4c Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 15 Mar 2017 10:47:43 +0000 Subject: [PATCH 13/27] Fix RTN19a: guarantee of a new transport (check transport reference) --- Spec/RealtimeClientConnection.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 890667749..a6c4ab166 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -3368,27 +3368,23 @@ class RealtimeClientConnection: QuickSpec { let channel = client.channels.get("test") let transport = client.transport as! TestProxyTransport - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - waitUntil(timeout: testTimeout) { done in channel.attach { _ in done() } } waitUntil(timeout: testTimeout) { done in - transport.ignoreSends = true channel.publish(nil, data: "message") { error in expect(error).to(beNil()) guard let newTransport = client.transport as? TestProxyTransport else { fail("Transport is nil"); done(); return } + expect(newTransport).toNot(beIdenticalTo(transport)) expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(newTransport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) - expect(transport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(0)) - expect(transport.protocolMessagesSentIgnored.filter{ $0.action == .Message }).to(haveCount(1)) + expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(newTransport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(1)) done() } - transport.ignoreSends = false client.onDisconnected() } } From 7fa81ec4e4195dbbb15ca4436c2e24ef6e4ce016 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 15 Mar 2017 10:08:33 +0000 Subject: [PATCH 14/27] Fix: ACK or NACK has not yet been received for a message, the client should consider the delivery of those messages as failed --- Source/ARTQueuedMessage.m | 4 ++ Source/ARTRealtime+Private.h | 2 +- Source/ARTRealtime.m | 71 +++++++++++++++-------------- Source/ARTStatus.m | 4 +- Spec/RealtimeClientConnection.swift | 47 +++++++++++-------- 5 files changed, 74 insertions(+), 54 deletions(-) diff --git a/Source/ARTQueuedMessage.m b/Source/ARTQueuedMessage.m index 693061003..d97c1eb29 100644 --- a/Source/ARTQueuedMessage.m +++ b/Source/ARTQueuedMessage.m @@ -25,6 +25,10 @@ - (instancetype)initWithProtocolMessage:(ARTProtocolMessage *)msg callback:(void return self; } +- (NSString *)description { + return [self.msg description]; +} + - (BOOL)mergeFrom:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb { if ([self.msg mergeFrom:msg]) { if (cb) { diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index a4a359278..0064bc5f0 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -55,7 +55,7 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *queuedMessages; /// List of pending messages waiting for ACK/NACK action to confirm the success receipt and acceptance. -@property (readonly, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *pendingMessages; +@property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *pendingMessages; /// First `msgSerial` pending message. @property (readwrite, assign, nonatomic) int64_t pendingMessageStartSerial; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 4ed06b285..9d55cd53a 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -355,7 +355,6 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } }]; } - break; } case ARTRealtimeClosing: { @@ -422,14 +421,12 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } case ARTRealtimeConnected: { _fallbacks = nil; - __GENERIC(NSArray, ARTQueuedMessage *) *pending = self.pendingMessages; - _pendingMessages = [[NSMutableArray alloc] init]; - for (ARTQueuedMessage *queued in pending) { - [self send:queued.msg callback:^(ARTStatus *__art_nonnull status) { - for (id cb in queued.cbs) { - ((void(^)(ARTStatus *__art_nonnull))cb)(status); - } - }]; + if (stateChange.reason) { + ARTStatus *status = [ARTStatus state:ARTStateError info:[stateChange.reason copy]]; + [self failPendingMessages:status]; + } + else { + [self resendPendingMessages]; } [_connectedEventEmitter emit:nil with:nil]; [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] with:nil]; @@ -454,28 +451,18 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } [self failQueuedMessages:channelStatus]; // For every Channel - for (ARTRealtimeChannel* channel in self.channels) { - switch (channel.state) { - case ARTRealtimeChannelInitialized: - case ARTRealtimeChannelAttaching: - case ARTRealtimeChannelAttached: - case ARTRealtimeChannelFailed: - if (stateChange.current == ARTRealtimeClosing) { - //do nothing. Closed state is coming. - } - else if (stateChange.current == ARTRealtimeClosed) { - [channel detachChannel:[ARTStatus state:ARTStateOk]]; - } - else if (stateChange.current == ARTRealtimeSuspended) { - [channel setSuspended:channelStatus]; - } - else { - [channel setFailed:channelStatus]; - } - break; - default: - [channel setSuspended:channelStatus]; - break; + for (ARTRealtimeChannel *channel in self.channels) { + if (stateChange.current == ARTRealtimeClosing) { + //do nothing. Closed state is coming. + } + else if (stateChange.current == ARTRealtimeClosed) { + [channel detachChannel:[ARTStatus state:ARTStateOk]]; + } + else if (stateChange.current == ARTRealtimeSuspended) { + [channel setSuspended:channelStatus]; + } + else { + [channel setFailed:channelStatus]; } } } @@ -839,6 +826,24 @@ - (void)send:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb { } } +- (void)resendPendingMessages { + NSArray *pms = self.pendingMessages; + self.pendingMessages = [NSMutableArray array]; + for (ARTQueuedMessage *pendingMessage in pms) { + [self send:pendingMessage.msg callback:^(ARTStatus *status) { + pendingMessage.cb(status); + }]; + } +} + +- (void)failPendingMessages:(ARTStatus *)status { + NSArray *pms = self.pendingMessages; + self.pendingMessages = [NSMutableArray array]; + for (ARTQueuedMessage *pendingMessage in pms) { + pendingMessage.cb(status); + } +} + - (void)sendQueuedMessages { NSArray *qms = self.queuedMessages; self.queuedMessages = [NSMutableArray array]; @@ -848,11 +853,11 @@ - (void)sendQueuedMessages { } } -- (void)failQueuedMessages:(ARTStatus *)error { +- (void)failQueuedMessages:(ARTStatus *)status { NSArray *qms = self.queuedMessages; self.queuedMessages = [NSMutableArray array]; for (ARTQueuedMessage *message in qms) { - message.cb(error); + message.cb(status); } } diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index c852d5c1b..d262d7ebb 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -51,7 +51,7 @@ - (NSInteger)getStatus { } - (NSString *)description { - return [NSString stringWithFormat:@"ARTErrorInfo with code %ld, message: %@", (long)self.statusCode, self.message]; + return [NSString stringWithFormat:@"ARTErrorInfo with code %ld, message: %@", (long)self.code, self.message]; } @end @@ -92,4 +92,4 @@ -(void) setErrorInfo:(ARTErrorInfo *)errorInfo { _errorInfo = errorInfo; } -@end \ No newline at end of file +@end diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index a6c4ab166..7c53e2d95 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -891,7 +891,6 @@ class RealtimeClientConnection: QuickSpec { expect(error).toNot(beNil()) partialDone() } - } expect(client.msgSerial) == 5 @@ -1122,26 +1121,38 @@ class RealtimeClientConnection: QuickSpec { let transport = client.transport as! TestProxyTransport transport.actionsIgnored += [.Ack, .Nack] - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - var gotPublishedCallback = false - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - gotPublishedCallback = true - }) - - let oldConnectionId = client.connection.id! - // Wait until the message is pushed to Ably first waitUntil(timeout: testTimeout) { done in - delay(1.0) { done() } + channel.attach() { _ in + done() + } } - client.simulateLostConnectionAndState() - expect(gotPublishedCallback).to(beFalse()) - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - expect(client.connection.id).toNot(equal(oldConnectionId)) - expect(gotPublishedCallback).to(beTrue()) + 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 + } + expect(error.code) == 80008 + 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) { stateChange in + expect(client.connection.id).toNot(equal(oldConnectionId)) + partialDone() + } + client.simulateLostConnectionAndState() + } + } } } From 98c9a2f0ab7a69ba2de8f0293d1a040fa0447982 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 15 Mar 2017 10:49:22 +0000 Subject: [PATCH 15/27] Enhance RTN14b: better timings --- .travis.yml | 2 +- Spec/RealtimeClientConnection.swift | 121 +++++++++++----------------- 2 files changed, 48 insertions(+), 75 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3df771a21..f11159ef9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,5 +15,5 @@ script: # Use `travis_wait` when a long running command or compile step regularly takes longer than 10 minutes without producing any output. # It writes a short line to the build log every minute for 20 minutes, extending the amount of time your command has to finish. # Prefix `travis_wait` with a greater number to extend the wait time. - - travis_wait 30 scan --scheme "Ably" --open_report false + - travis_wait 30 scan --scheme "Ably" --open_report false --devices 'iPhone 6s' - bash ./Scripts/run_examples_tests.sh diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 7c53e2d95..7712e0956 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -1870,9 +1870,11 @@ class RealtimeClientConnection: QuickSpec { let options = AblyTests.commonAppSetup() options.autoConnect = false options.authCallback = { tokenParams, callback in - callback(getTestTokenDetails(key: options.key, capability: tokenParams.capability, ttl: tokenParams.ttl), nil) + delay(0) { + callback(getTestTokenDetails(key: options.key, capability: tokenParams.capability, ttl: tokenParams.ttl), nil) + } } - let tokenTtl = 1.0 + let tokenTtl = 3.0 options.token = getTestToken(key: options.key, ttl: tokenTtl) let client = ARTRealtime(options: options) @@ -1882,61 +1884,45 @@ class RealtimeClientConnection: QuickSpec { client.close() } - // Let the token expire waitUntil(timeout: testTimeout) { done in - delay(tokenTtl) { - done() - } - } - - var transport: TestProxyTransport! + // Let the token expire + client.connection.once(.Disconnected) { stateChange in + guard let reason = stateChange?.reason else { + fail("Token error is missing"); done(); return + } + reason.code == 40142 - waitUntil(timeout: testTimeout) { done in - client.connection.on { stateChange in - let stateChange = stateChange! - let state = stateChange.current - let errorInfo = stateChange.reason - switch state { - case .Connected: - expect(errorInfo).to(beNil()) - // New token - expect(client.auth.tokenDetails!.token).toNot(equal(options.token)) - done() - case .Failed, .Disconnected, .Suspended: - fail("Should not emit error (\(errorInfo))") - done() - default: - break + client.connection.on { stateChange in + let stateChange = stateChange! + let state = stateChange.current + let errorInfo = stateChange.reason + switch state { + case .Connected: + expect(errorInfo).to(beNil()) + // New token + expect(client.auth.tokenDetails!.token).toNot(equal(options.token)) + done() + case .Failed, .Suspended: + fail("Should not emit error (\(errorInfo))") + done() + default: + break + } } } client.connect() - transport = client.transport as! TestProxyTransport - } - - let failures = transport.protocolMessagesReceived.filter({ $0.action == .Error }) - - if failures.count != 1 { - fail("Should have only one connection request fail") - return } - - expect(failures[0].error!.code).to(equal(40142)) //Token expired } it("should transition to Failed when the token renewal fails") { let options = AblyTests.commonAppSetup() options.autoConnect = false - let tokenTtl = 1.0 + let tokenTtl = 3.0 let tokenDetails = getTestTokenDetails(key: options.key, capability: nil, ttl: tokenTtl)! options.token = tokenDetails.token options.authCallback = { tokenParams, callback in - callback(tokenDetails, nil) // Return the same expired token again. - } - - // Let the token expire - waitUntil(timeout: testTimeout) { done in - delay(tokenTtl) { - done() + delay(0) { + callback(tokenDetails, nil) // Return the same expired token again. } } @@ -1947,41 +1933,28 @@ class RealtimeClientConnection: QuickSpec { client.close() } - client.connect() - let firstTransport = client.transport as! TestProxyTransport - expect(client.transport).toEventuallyNot(beIdenticalTo(firstTransport), timeout: testTimeout) - let newTransport = client.transport as! TestProxyTransport - waitUntil(timeout: testTimeout) { done in - client.connection.on { stateChange in - let stateChange = stateChange! - let state = stateChange.current - let errorInfo = stateChange.reason - switch state { - case .Connected: - fail("Should not be connected") - done() - case .Failed, .Disconnected, .Suspended: - guard let errorInfo = errorInfo else { - fail("ErrorInfo is nil"); done(); return - } - expect(errorInfo.code).to(equal(40142)) //Token expired - done() - default: - break + let partialDone = AblyTests.splitDone(3, done: done) + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + client.connection.once(.Disconnected) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return; } + expect(reason.code) == 40142 + partialDone() } + client.connection.once(.Failed) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return; + } + expect(reason.code) == 40142 + partialDone() + } + client.connect() } - - let failures = firstTransport.protocolMessagesReceived.filter({ $0.action == .Error }) + newTransport.protocolMessagesReceived.filter({ $0.action == .Error }) - - if failures.count != 2 { - fail("Should have two connection request fail") - return - } - - expect(failures[0].error!.code).to(equal(40142)) - expect(failures[1].error!.code).to(equal(40142)) } it("should transition to Failed state because the token is invalid and not renewable") { From 388d6b252e4cf6c100b4633c0d878bb777958760 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 15 Mar 2017 17:21:57 +0000 Subject: [PATCH 16/27] Fix: REST and Realtime, wait for last operation to release the object --- Source/ARTHttp.m | 20 +++++++++----------- Source/ARTRealtime.m | 11 ++--------- Source/ARTRealtimeTransport.h | 2 +- Source/ARTURLSessionServerTrust.h | 2 +- Source/ARTURLSessionServerTrust.m | 4 ++-- Source/ARTWebSocketTransport.h | 1 - Source/ARTWebSocketTransport.m | 6 ++++++ Spec/RealtimeClient.swift | 6 ++---- Spec/RestClient.swift | 2 +- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/Source/ARTHttp.m b/Source/ARTHttp.m index d39cb1ce9..efc69488e 100644 --- a/Source/ARTHttp.m +++ b/Source/ARTHttp.m @@ -164,7 +164,7 @@ - (instancetype)init { } - (void)dealloc { - [_urlSession invalidateAndCancel]; + [_urlSession finishTasksAndInvalidate]; } - (instancetype)initWithBaseUrl:(NSURL *)baseUrl { @@ -179,19 +179,18 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT [self.logger debug:@"%@ %@", request.HTTPMethod, request.URL.absoluteString]; [self.logger verbose:@"Headers %@", request.allHTTPHeaderFields]; - __weak typeof(self) weakSelf = self; [_urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; dispatch_async(_queue, ^{ if (error) { - [[weakSelf logger] error:@"%@ %@: error %@", request.HTTPMethod, request.URL.absoluteString, error]; + [self.logger error:@"%@ %@: error %@", request.HTTPMethod, request.URL.absoluteString, error]; } else { - [[weakSelf logger] debug:@"%@ %@: statusCode %ld", request.HTTPMethod, request.URL.absoluteString, (long)httpResponse.statusCode]; - [[weakSelf logger] verbose:@"Headers %@", httpResponse.allHeaderFields]; + [self.logger debug:@"%@ %@: statusCode %ld", request.HTTPMethod, request.URL.absoluteString, (long)httpResponse.statusCode]; + [self.logger verbose:@"Headers %@", httpResponse.allHeaderFields]; NSString *headerErrorMessage = httpResponse.allHeaderFields[@"X-Ably-ErrorMessage"]; if (headerErrorMessage && ![headerErrorMessage isEqualToString:@""]) { - [[weakSelf logger] warn:@"%@", headerErrorMessage]; + [self.logger warn:@"%@", headerErrorMessage]; } } callback(httpResponse, data, error); @@ -221,20 +220,19 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT request.HTTPBody = artRequest.body; [self.logger debug:@"ARTHttp: makeRequest %@", [request allHTTPHeaderFields]]; - __weak typeof(self) weakSelf = self; [_urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - [[weakSelf logger] verbose:@"ARTHttp: Got response %@, err %@", response, error]; + [self.logger verbose:@"ARTHttp: Got response %@, err %@", response, error]; if(error) { - [[weakSelf logger] error:@"ARTHttp receieved error: %@", error]; + [self.logger error:@"ARTHttp receieved error: %@", error]; cb([ARTHttpResponse responseWithStatus:500 headers:nil body:nil]); } else { if (httpResponse) { int status = (int)httpResponse.statusCode; - [[weakSelf logger] debug:@"ARTHttp response status is %d", status]; - [[weakSelf logger] verbose:@"ARTHttp received response %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]]; + [self.logger debug:@"ARTHttp response status is %d", status]; + [self.logger verbose:@"ARTHttp received response %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]]; dispatch_async(_queue, ^{ cb([ARTHttpResponse responseWithStatus:status headers:httpResponse.allHeaderFields body:data]); diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 9d55cd53a..ff30b4b19 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -121,6 +121,8 @@ - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails { // Halt the current connection and reconnect with the most recent token [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p halt current connection and reconnect with %@", _rest, tokenDetails]; [_transport abort:[ARTStatus state:ARTStateOk]]; + _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + _transport.delegate = self; [_transport connectWithToken:tokenDetails.token]; } break; @@ -188,11 +190,6 @@ - (void)dealloc { [_internalEventEmitter off]; } - if (_transport) { - _transport.delegate = nil; - [_transport close]; - } - _transport = nil; self.rest.prioritizedHost = nil; } @@ -368,7 +365,6 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha case ARTRealtimeClosed: [_reachability off]; [self.transport close]; - self.transport.delegate = nil; _connection.key = nil; _connection.id = nil; _transport = nil; @@ -378,7 +374,6 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha case ARTRealtimeFailed: status = [ARTStatus state:ARTStateConnectionFailed info:stateChange.reason]; [self.transport abort:status]; - self.transport.delegate = nil; _transport = nil; self.rest.prioritizedHost = nil; [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:stateChange.reason]; @@ -400,7 +395,6 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } [self.transport close]; - self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.disconnectedRetryTimeout]; stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ @@ -410,7 +404,6 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha } case ARTRealtimeSuspended: { [self.transport close]; - self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.suspendedRetryTimeout]; stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 18974d1e7..dc8731c08 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -71,7 +71,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; @property (readonly, assign, nonatomic) ARTRealtimeTransportState state; -@property (readwrite, weak, nonatomic) id delegate; +@property (nullable, readwrite, strong, nonatomic) id delegate; - (void)send:(ARTProtocolMessage *)msg; - (void)receive:(ARTProtocolMessage *)msg; diff --git a/Source/ARTURLSessionServerTrust.h b/Source/ARTURLSessionServerTrust.h index 3f96a82ca..2a4c3e225 100644 --- a/Source/ARTURLSessionServerTrust.h +++ b/Source/ARTURLSessionServerTrust.h @@ -15,7 +15,7 @@ ART_ASSUME_NONNULL_BEGIN - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; -- (void)invalidateAndCancel; +- (void)finishTasksAndInvalidate; @end diff --git a/Source/ARTURLSessionServerTrust.m b/Source/ARTURLSessionServerTrust.m index 429b11515..93a97058e 100644 --- a/Source/ARTURLSessionServerTrust.m +++ b/Source/ARTURLSessionServerTrust.m @@ -23,8 +23,8 @@ - (instancetype)init { return self; } -- (void)invalidateAndCancel { - [_session invalidateAndCancel]; +- (void)finishTasksAndInvalidate { + [_session finishTasksAndInvalidate]; } - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { diff --git a/Source/ARTWebSocketTransport.h b/Source/ARTWebSocketTransport.h index baa222ea4..73b36a52f 100644 --- a/Source/ARTWebSocketTransport.h +++ b/Source/ARTWebSocketTransport.h @@ -23,7 +23,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; -@property (readwrite, weak, nonatomic) id delegate; @end diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index f53dde6d5..c00d3c9d6 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -37,6 +37,7 @@ }; @implementation ARTWebSocketTransport { + id _delegate; ARTRealtimeTransportState _state; /** A dispatch queue for firing the events. @@ -44,6 +45,8 @@ @implementation ARTWebSocketTransport { _Nonnull dispatch_queue_t _workQueue; } +@synthesize delegate = _delegate; + - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { self = [super init]; if (self) { @@ -65,6 +68,7 @@ - (void)dealloc { [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p dealloc", _delegate, self]; self.websocket.delegate = nil; self.websocket = nil; + self.delegate = nil; } - (void)send:(ARTProtocolMessage *)msg { @@ -189,6 +193,7 @@ - (void)close { self.websocket.delegate = nil; [self.websocket closeWithCode:ARTWsCloseNormal reason:@"Normal Closure"]; self.websocket = nil; + self.delegate = nil; } - (void)abort:(ARTStatus *)reason { @@ -201,6 +206,7 @@ - (void)abort:(ARTStatus *)reason { [self.websocket closeWithCode:ARTWsAbnormalClose reason:@"Abnormal Closure"]; } self.websocket = nil; + self.delegate = nil; } - (void)setHost:(NSString *)host { diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 96418187f..138a1a021 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -1216,14 +1216,12 @@ class RealtimeClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { - let options = AblyTests.commonAppSetup() - options.autoConnect = false - let realtime = ARTRealtime(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - realtime.connect() + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) realtime.channels.get("foo").attach { error in expect(error).to(beNil()) + realtime.close() done() } }.resume() diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index e257c03ca..1076ee176 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -1312,9 +1312,9 @@ class RestClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { let options = AblyTests.commonAppSetup() - let rest = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in + let rest = ARTRest(options: options) rest.channels.get("foo").history { _ in done() } From 5de21e908ccc7a55368ba4de8377a3028107ae9c Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 16 Mar 2017 16:30:56 +0000 Subject: [PATCH 17/27] fixup! Test suite: ack order --- Tests/ARTRealtimePresenceTest.m | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index fbe6ecd07..c682d247d 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -763,27 +763,36 @@ - (void)testEnterClient { NSString *clientId = @"otherClientId"; NSString *clientId2 = @"yetAnotherClientId"; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; + void(^partialFulfill)() = [ARTTestUtil splitFulfillFrom:self expectation:expectation in:4]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"channelName"]; [channel.presence enterClient:clientId data:nil callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); - [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { - XCTAssertNil(errorInfo); - [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { - XCTAssert(!error); - XCTAssertEqual(2, members.count); - ARTPresenceMessage *m0 = [members objectAtIndex:0]; - // cannot guarantee the order - if (![m0.clientId isEqualToString:clientId2] && ![m0.clientId isEqualToString:clientId]) { - XCTFail(@"clientId1 is different from what's expected"); - } - ARTPresenceMessage *m1 = [members objectAtIndex:1]; - if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId]) { - XCTFail(@"clientId2 is different from what's expected"); - } - [expectation fulfill]; - }]; - }]; + partialFulfill(); + }]; + [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { + XCTAssertNil(errorInfo); + partialFulfill(); + }]; + [channel.presence subscribe:^(ARTPresenceMessage *message) { + partialFulfill(); + }]; + [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; + + __weak XCTestExpectation *expectationPresenceGet = [self expectationWithDescription:[NSString stringWithFormat:@"%s-PresenceGet", __FUNCTION__]]; + [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { + XCTAssert(!error); + XCTAssertEqual(2, members.count); + ARTPresenceMessage *m0 = [members objectAtIndex:0]; + // cannot guarantee the order + if (![m0.clientId isEqualToString:clientId2] && ![m0.clientId isEqualToString:clientId]) { + XCTFail(@"clientId1 is different from what's expected"); + } + ARTPresenceMessage *m1 = [members objectAtIndex:1]; + if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId]) { + XCTFail(@"clientId2 is different from what's expected"); + } + [expectationPresenceGet fulfill]; }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; [realtime testSuite_waitForConnectionToClose:self]; From 17bca960320e4f9bfc665d4dca35e45f91caa1b7 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 17 Mar 2017 02:20:43 +0000 Subject: [PATCH 18/27] Fix: cancel timers when a connection gets closed --- Source/ARTRealtime.m | 22 +++++++++++++ Source/ARTWebSocketTransport.m | 4 +-- Spec/RealtimeClientConnection.swift | 49 +++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index ff30b4b19..82d4b9fba 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -53,6 +53,8 @@ @implementation ARTRealtime { id _transport; ARTFallback *_fallbacks; _Nonnull dispatch_queue_t _eventQueue; + __weak ARTEventListener *_connectionRetryFromSuspendedListener; + __weak ARTEventListener *_connectionRetryFromDisconnectedListener; __weak ARTEventListener *_connectingTimeoutListener; dispatch_block_t _authenitcatingTimeoutWork; } @@ -202,6 +204,8 @@ - (void)connect { } - (void)close { + [self cancelTimers]; + switch (self.connection.state) { case ARTRealtimeInitialized: case ARTRealtimeClosing: @@ -399,7 +403,9 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha [stateChange setRetryIn:self.options.disconnectedRetryTimeout]; stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; + _connectionRetryFromDisconnectedListener = nil; }]; + _connectionRetryFromDisconnectedListener = stateChangeEventListener; break; } case ARTRealtimeSuspended: { @@ -408,7 +414,9 @@ - (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateCha [stateChange setRetryIn:self.options.suspendedRetryTimeout]; stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; + _connectionRetryFromSuspendedListener = nil; }]; + _connectionRetryFromSuspendedListener = stateChangeEventListener; [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; break; } @@ -597,6 +605,20 @@ - (void)onError:(ARTProtocolMessage *)message { } } +- (void)cancelTimers { + [_connectionRetryFromSuspendedListener stopTimer]; + _connectionRetryFromSuspendedListener = nil; + NSLog(@"%p", _connectionRetryFromDisconnectedListener); + [_connectionRetryFromDisconnectedListener stopTimer]; + _connectionRetryFromDisconnectedListener = nil; + // Cancel connecting scheduled work + [_connectingTimeoutListener stopTimer]; + _connectingTimeoutListener = nil; + // Cancel auth scheduled work + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; +} + - (void)onConnectionTimeOut { // Cancel connecting scheduled work [_connectingTimeoutListener stopTimer]; diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index c00d3c9d6..d5da8dcbf 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -189,14 +189,15 @@ - (void)sendPing { } - (void)close { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; [self.websocket closeWithCode:ARTWsCloseNormal reason:@"Normal Closure"]; self.websocket = nil; - self.delegate = nil; } - (void)abort:(ARTStatus *)reason { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; if (reason.errorInfo) { @@ -206,7 +207,6 @@ - (void)abort:(ARTStatus *)reason { [self.websocket closeWithCode:ARTWsAbnormalClose reason:@"Abnormal Closure"]; } self.websocket = nil; - self.delegate = nil; } - (void)setHost:(NSString *)host { diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 7712e0956..1aed2ebd8 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2136,6 +2136,55 @@ class RealtimeClientConnection: QuickSpec { } } + it("on CLOSE the connection should stop connection retries") { + let options = AblyTests.commonAppSetup() + options.disconnectedRetryTimeout = 0.1 + options.suspendedRetryTimeout = 0.5 + options.autoConnect = false + let expectedTime: NSTimeInterval = 1.0 + + options.authCallback = { _ in + // Force a timeout + } + + let previousConnectionStateTtl = ARTDefault.connectionStateTtl() + defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } + ARTDefault.setConnectionStateTtl(expectedTime) + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(0.1) + + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.on(.Suspended) { stateChange in + expect(client.connection.errorReason!.message).to(contain("timed out")) + + let start = NSDate() + client.connection.once(.Connecting) { stateChange in + let end = NSDate() + expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.suspendedRetryTimeout, within: 0.5)) + done() + } + } + client.connect() + } + + client.close() + + // Check if the connection gets closed + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connecting) { stateChange in + fail("Should be closing the connection"); done(); return + } + delay(2.0) { + done() + } + } + } + } // RTN15 From d533e2036c2bf594eec6fc6c07e4c9a28717d6c9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 17 Mar 2017 14:44:50 +0000 Subject: [PATCH 19/27] fixup! Test suite: ack order --- Tests/ARTRealtimePresenceTest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index c682d247d..21374fb27 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -789,7 +789,7 @@ - (void)testEnterClient { XCTFail(@"clientId1 is different from what's expected"); } ARTPresenceMessage *m1 = [members objectAtIndex:1]; - if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId]) { + if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId2]) { XCTFail(@"clientId2 is different from what's expected"); } [expectationPresenceGet fulfill]; From b68b80635900865c78a71bd6c528aa11afe580b9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 17 Mar 2017 18:28:33 +0000 Subject: [PATCH 20/27] Test suite: timings --- Spec/RealtimeClient.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 138a1a021..5b3107f6d 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -945,7 +945,9 @@ class RealtimeClient: QuickSpec { } let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { - client.close() + delay(0) { + client.close() + } } defer { hook.remove() } From e812cda4bde111b2c55a96d9198c6b4812758665 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 21 Mar 2017 11:27:37 +0000 Subject: [PATCH 21/27] fixup! Enhance RTN14b: better timings --- Spec/RealtimeClientConnection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 1aed2ebd8..7ae3b7b80 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -1890,7 +1890,7 @@ class RealtimeClientConnection: QuickSpec { guard let reason = stateChange?.reason else { fail("Token error is missing"); done(); return } - reason.code == 40142 + expect(reason.code) == 40142 client.connection.on { stateChange in let stateChange = stateChange! From 3f9558ee5f759cf798e4722506827446aac49d11 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Mar 2017 15:45:27 +0000 Subject: [PATCH 22/27] Test suite: close connections --- Spec/RealtimeClient.swift | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 5b3107f6d..91dc38ccb 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -49,6 +49,7 @@ class RealtimeClient: QuickSpec { options.clientId = "client_string" let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -68,7 +69,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() } //RTC1a @@ -90,6 +90,7 @@ class RealtimeClient: QuickSpec { // First connection let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -113,6 +114,7 @@ class RealtimeClient: QuickSpec { // New connection let newClient = ARTRealtime(options: options) + defer { newClient.close() } waitUntil(timeout: testTimeout) { done in newClient.connection.on { stateChange in @@ -131,8 +133,6 @@ class RealtimeClient: QuickSpec { } } } - newClient.close() - client.close() } //RTC1d @@ -141,6 +141,7 @@ class RealtimeClient: QuickSpec { options.realtimeHost = "fake.ably.io" options.autoConnect = false let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } waitUntil(timeout: testTimeout) { done in client.connection.once(.Connecting) { _ in @@ -195,7 +196,6 @@ class RealtimeClient: QuickSpec { }) expect(client.channels.get("test")).toNot(beNil()) - client.close() } context("Auth object") { @@ -204,9 +204,8 @@ class RealtimeClient: QuickSpec { it("should provide access to the Auth object") { let options = AblyTests.commonAppSetup() let client = ARTRealtime(options: options) - + defer { client.close() } expect(client.auth.options.key).to(equal(options.key)) - client.close() } // RTC4a @@ -214,6 +213,7 @@ class RealtimeClient: QuickSpec { let options = AblyTests.commonAppSetup() options.clientId = "client_string" let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -233,7 +233,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() } } @@ -244,6 +243,7 @@ class RealtimeClient: QuickSpec { // RTC5a it("should present an async interface") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } // Async waitUntil(timeout: testTimeout) { done in // Proxy from `client.rest.stats` @@ -252,12 +252,12 @@ class RealtimeClient: QuickSpec { done() }) } - client.close() } // RTC5b it("should accept all the same params as RestClient") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } var paginatedResult: ARTPaginatedResult? // Realtime @@ -287,7 +287,6 @@ class RealtimeClient: QuickSpec { expect(paginated.items.count).to(equal(paginatedResult!.items.count)) }) } - client.close() } } @@ -295,6 +294,7 @@ class RealtimeClient: QuickSpec { // RTC6a it("should present an async interface") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } // Async waitUntil(timeout: testTimeout) { done in // Proxy from `client.rest.time` @@ -303,7 +303,6 @@ class RealtimeClient: QuickSpec { done() }) } - client.close() } } @@ -313,6 +312,7 @@ class RealtimeClient: QuickSpec { options.suspendedRetryTimeout = 6.0 let client = ARTRealtime(options: options) + defer { client.close() } var start: NSDate? var endInterval: UInt? @@ -347,7 +347,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() if let secs = endInterval { expect(secs).to(beLessThanOrEqualTo(UInt(options.suspendedRetryTimeout))) From 8ad250ff9536c56d0d4cd5324e23af7657d93f27 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Mar 2017 15:46:06 +0000 Subject: [PATCH 23/27] Fix: turn off immediately reachability when close occurs --- Source/ARTRealtime+Private.h | 2 +- Source/ARTRealtime.m | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index 0064bc5f0..d193ad387 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -43,7 +43,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () @property (readwrite, strong, nonatomic) ARTRest *rest; -@property (readonly, getter=getTransport, art_nullable) id transport; +@property (readonly, nullable) id transport; @property (readonly, strong, nonatomic, art_nonnull) id reachability; @property (readonly, getter=getLogger) ARTLog *logger; @property (nonatomic) NSTimeInterval connectionStateTtl; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 82d4b9fba..318614124 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -144,7 +144,7 @@ - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails { } } -- (id)getTransport { +- (id)transport { return _transport; } @@ -204,6 +204,7 @@ - (void)connect { } - (void)close { + [_reachability off]; [self cancelTimers]; switch (self.connection.state) { @@ -608,7 +609,6 @@ - (void)onError:(ARTProtocolMessage *)message { - (void)cancelTimers { [_connectionRetryFromSuspendedListener stopTimer]; _connectionRetryFromSuspendedListener = nil; - NSLog(@"%p", _connectionRetryFromDisconnectedListener); [_connectionRetryFromDisconnectedListener stopTimer]; _connectionRetryFromDisconnectedListener = nil; // Cancel connecting scheduled work @@ -719,7 +719,7 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; _transport.delegate = self; } - [[weakSelf getTransport] connectWithToken:tokenDetails.token]; + [[weakSelf transport] connectWithToken:tokenDetails.token]; }]; } @finally { From 49efb8aeae372daff183ea341a33ea8061eb011e Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Mar 2017 17:49:28 +0000 Subject: [PATCH 24/27] Fix RTC1d: wait for host is not reachable error --- Spec/RealtimeClient.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 91dc38ccb..f051d5aab 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -143,7 +143,8 @@ class RealtimeClient: QuickSpec { let client = ARTRealtime(options: options) defer { client.dispose(); client.close() } - waitUntil(timeout: testTimeout) { done in + waitUntil(timeout: testTimeout * 2) { done in + let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.Connecting) { _ in guard let webSocketTransport = client.transport as? ARTWebSocketTransport else { fail("Transport should be of type ARTWebSocketTransport"); done() @@ -151,11 +152,18 @@ class RealtimeClient: QuickSpec { } expect(webSocketTransport.websocketURL).toNot(beNil()) expect(webSocketTransport.websocketURL?.host).to(equal("fake.ably.io")) - done() + partialDone() + } + client.connection.once(.Failed) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return + } + expect(reason.code) == Int(CFNetworkErrors.CFHostErrorUnknown.rawValue) + expect(reason.message).to(contain("kCFErrorDomainCFNetwork")) + partialDone() } client.connect() } - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) } //RTC1e From 3501f439bb38ef706be5393bfcb9b395ccbdf566 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Mar 2017 22:57:51 +0000 Subject: [PATCH 25/27] fixup! Test suite: ack order --- Tests/ARTRealtimeMessageTest.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/ARTRealtimeMessageTest.m b/Tests/ARTRealtimeMessageTest.m index 28c41baba..f9008de8e 100644 --- a/Tests/ARTRealtimeMessageTest.m +++ b/Tests/ARTRealtimeMessageTest.m @@ -145,19 +145,21 @@ - (void)testEchoMessagesDefault { NSString *message2 = @"message2"; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; + void(^partialDone)() = [ARTTestUtil splitFulfillFrom:self expectation:expectation in:3]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtime *realtime2 = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:channelName]; __block bool gotMessage1 = false; [channel subscribe:^(ARTMessage *message) { - if([[message data] isEqualToString:message1]) { + if ([[message data] isEqualToString:message1]) { gotMessage1 = true; + partialDone(); } else { XCTAssertTrue(gotMessage1); XCTAssertEqualObjects([message data], message2); - [expectation fulfill]; + partialDone(); } }]; [channel publish:nil data:message1 callback:^(ARTErrorInfo *errorInfo) { @@ -165,6 +167,7 @@ - (void)testEchoMessagesDefault { ARTRealtimeChannel *channel2 = [realtime2.channels get:channelName]; [channel2 publish:nil data:message2 callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); + partialDone(); }]; }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; From b0e5f94d1ffe70a95def769f645b75cb2462ee7d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Mar 2017 22:58:57 +0000 Subject: [PATCH 26/27] Travis update --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f11159ef9..963bcf974 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,5 +15,5 @@ script: # Use `travis_wait` when a long running command or compile step regularly takes longer than 10 minutes without producing any output. # It writes a short line to the build log every minute for 20 minutes, extending the amount of time your command has to finish. # Prefix `travis_wait` with a greater number to extend the wait time. - - travis_wait 30 scan --scheme "Ably" --open_report false --devices 'iPhone 6s' + - scan --scheme "Ably" --open_report false --devices "iPhone 6s" - bash ./Scripts/run_examples_tests.sh From 257454a5ae431626ab25a277d03f9b5f46a64e50 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 23 Mar 2017 11:20:07 +0000 Subject: [PATCH 27/27] Fix RTN19a --- Spec/RealtimeClientConnection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 7ae3b7b80..cad4b5909 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -3394,7 +3394,6 @@ class RealtimeClientConnection: QuickSpec { // RTN19a it("should resend any ProtocolMessage that is awaiting a ACK/NACK") { let options = AblyTests.commonAppSetup() - options.logLevel = .Debug options.disconnectedRetryTimeout = 0.1 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } @@ -3412,6 +3411,7 @@ class RealtimeClientConnection: QuickSpec { fail("Transport is nil"); done(); return } expect(newTransport).toNot(beIdenticalTo(transport)) + expect(transport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(1)) expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(newTransport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1))