Last active
April 22, 2016 08:26
-
-
Save kean/c9b7b72b089328b38d62383b9b3fde5a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "GMAutoRetryController.h" | |
#import "NSTimer+GMBlocks.h" | |
#import "NSError+Fitmeup.h" | |
#import "GMNetworkReachability.h" | |
#import "GMRepository.h" | |
typedef NS_ENUM(NSInteger, _GMARCState) { | |
_GMARCStateSuspended, // initial state | |
_GMARCStateExecuting, | |
_GMARCStateWaitingRetry, | |
_GMARCStateCancelled | |
}; | |
@interface GMAutoRetryController () | |
@property (nonatomic) _GMARCState state; | |
@end | |
@implementation GMAutoRetryController { | |
NSTimeInterval _retryDelay; | |
NSTimer *__weak _retryTimer; | |
} | |
- (void)dealloc { | |
[_retryTimer invalidate]; | |
} | |
- (nonnull instancetype)initWithBlock:(void (^)(GMAutoRetryController *))block { | |
GM_CREATE_NONNULL_SELF([super init]); | |
_state = _GMARCStateSuspended; | |
_retryDelayIncreaseRate = 2; | |
_maximumRetryDelay = 64; | |
_initialRetryDelay = 8; | |
_currentAttemptCount = 0; | |
_maximumAttemptCount = GMAutoRetryControllerInfiniteAttemptCount; | |
_block = block; | |
return self; | |
} | |
- (void)resume { | |
self.state = _GMARCStateExecuting; | |
} | |
- (void)cancel { | |
self.state = _GMARCStateCancelled; | |
} | |
- (BOOL)scheduleAutoRetryWithError:(nullable NSError *)error { | |
if ([self shouldScheduleAutoRetryWithError:error]) { | |
_currentError = error; | |
self.state = _GMARCStateWaitingRetry; | |
return YES; | |
} else { | |
// execution can still be restarted manually | |
self.state = _GMARCStateSuspended; | |
return NO; | |
} | |
} | |
// TODO: Should this method return NO when error is nil? | |
- (BOOL)shouldScheduleAutoRetryWithError:(nullable NSError *)error { | |
return self.currentAttemptCount < self.maximumAttemptCount; | |
} | |
#pragma mark - FSM | |
- (BOOL)_isValidNextState:(_GMARCState)nextState { | |
switch (self.state) { | |
case _GMARCStateSuspended: | |
return (nextState == _GMARCStateExecuting || | |
nextState == _GMARCStateCancelled); | |
case _GMARCStateExecuting: | |
return (nextState == _GMARCStateWaitingRetry || | |
nextState == _GMARCStateCancelled); | |
case _GMARCStateWaitingRetry: | |
return (nextState == _GMARCStateExecuting || | |
nextState == _GMARCStateCancelled); | |
default: | |
return NO; | |
} | |
} | |
- (void)setState:(_GMARCState)state { | |
@synchronized(self) { | |
if ([self _isValidNextState:state]) { | |
[self _executeExitActionForState:_state]; | |
_state = state; | |
[self _executeEnterActionForState:state]; | |
} | |
} | |
} | |
- (void)_executeExitActionForState:(_GMARCState)state { | |
if (state == _GMARCStateWaitingRetry) { | |
[_retryTimer invalidate]; | |
} | |
} | |
- (void)_executeEnterActionForState:(_GMARCState)state { | |
if (state == _GMARCStateExecuting) { | |
_currentAttemptCount++; | |
self.block(self); | |
} | |
if (state == _GMARCStateWaitingRetry) { | |
[self _scheduleRetryTimer]; | |
} | |
} | |
#pragma mark - Timer | |
- (void)_scheduleRetryTimer { | |
typeof(self) __weak weakSelf = self; | |
NSTimeInterval timeInterval = [self _retryDelay]; | |
NSTimer *timer = [NSTimer timerWithTimeInterval:timeInterval block:^(NSTimer *timer) { | |
[weakSelf _retryTimerDidFire:timer]; | |
} userInfo:nil repeats:NO]; | |
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; | |
_retryTimer = timer; | |
} | |
- (void)_retryTimerDidFire:(NSTimer *)timer { | |
self.state = _GMARCStateExecuting; | |
} | |
/*! Returns current retry delay and increments it afterwards. | |
*/ | |
- (NSTimeInterval)_retryDelay { | |
NSTimeInterval delay = _retryDelay > 0 ? _retryDelay : self.initialRetryDelay; | |
_retryDelay = MIN(delay * self.retryDelayIncreaseRate, self.maximumRetryDelay); | |
return delay; | |
} | |
@end | |
@implementation GMURLAutoRetryController | |
- (void)dealloc { | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
- (nonnull instancetype)initWithBlock:(void (^ __nullable)(GMAutoRetryController * __nonnull))block { | |
GM_CREATE_NONNULL_SELF([super initWithBlock:block]); | |
_allowsFastRetries = YES; | |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_networkReachabilityDidChange:) name:GMNetworkReachabilyDidChangeNotification object:GMRepository.networkReachability]; | |
return self; | |
} | |
- (BOOL)shouldScheduleAutoRetryWithError:(nullable NSError *)error { | |
if ([error.domain isEqualToString:NSURLErrorDomain] && | |
error.code == NSURLErrorCancelled) { | |
return NO; | |
} | |
return [super shouldScheduleAutoRetryWithError:error]; | |
} | |
- (void)_networkReachabilityDidChange:(NSNotification *)notification { | |
if (!self.allowsFastRetries) { | |
return; | |
} | |
GMNetworkReachability *reachability = notification.object; | |
if (!reachability.reachable) { | |
return; | |
} | |
if (![self.currentError gm_isInternetConnectionError]) { | |
return; | |
} | |
[self resume]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment