1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXErrorRecoveryManager.h" 4#import "EXKernel.h" 5 6// if the app crashes and it has not yet been 5 seconds since it loaded, don't auto refresh. 7#define EX_AUTO_REFRESH_BUFFER_BASE_SECONDS 5.0 8 9NSNotificationName const kEXErrorRecoverySetPropsNotification = @"EXErrorRecoverySetPropsNotification"; 10 11@interface EXErrorRecoveryRecord : NSObject 12 13@property (nonatomic, assign) BOOL isRecovering; 14@property (nonatomic, strong) NSError *error; 15@property (nonatomic, strong) NSDate *dtmLastLoaded; 16@property (nonatomic, strong) NSDictionary *developerInfo; 17 18@end 19 20@implementation EXErrorRecoveryRecord 21 22@end 23 24@interface EXErrorRecoveryManager () 25 26@property (nonatomic, strong) NSMutableDictionary<NSString *, EXErrorRecoveryRecord *> *experienceInfo; 27@property (nonatomic, assign) NSUInteger reloadBufferDepth; 28@property (nonatomic, strong) NSDate *dtmAnyExperienceLoaded; 29 30@end 31 32@implementation EXErrorRecoveryManager 33 34- (instancetype)init 35{ 36 if (self = [super init]) { 37 _reloadBufferDepth = 0; 38 _dtmAnyExperienceLoaded = [NSDate date]; 39 _experienceInfo = [NSMutableDictionary dictionary]; 40 [[NSNotificationCenter defaultCenter] addObserver:self 41 selector:@selector(_handleRecoveryPropsNotification:) 42 name:kEXErrorRecoverySetPropsNotification 43 object:nil]; 44 } 45 return self; 46} 47 48- (void)dealloc 49{ 50 [[NSNotificationCenter defaultCenter] removeObserver:self]; 51} 52 53- (void)setDeveloperInfo:(NSDictionary *)developerInfo forExperienceid:(NSString *)experienceId 54{ 55 if (!experienceId) { 56 NSAssert(experienceId, @"Cannot associate recovery info with a nil experience id"); 57 } 58 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 59 if (!record) { 60 record = [[EXErrorRecoveryRecord alloc] init]; 61 @synchronized (_experienceInfo) { 62 _experienceInfo[experienceId] = record; 63 } 64 } 65 record.developerInfo = developerInfo; 66} 67 68- (NSDictionary *)developerInfoForExperienceId:(NSString *)experienceId 69{ 70 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 71 if (record) { 72 return record.developerInfo; 73 } 74 return nil; 75} 76 77- (void)setDeveloperInfo:(NSDictionary *)developerInfo forScopedModule:(id)scopedModule 78{ 79 [self setDeveloperInfo:developerInfo forExperienceid:((EXScopedBridgeModule *)scopedModule).experienceId]; 80} 81 82- (void)setError:(NSError *)error forExperienceId:(NSString *)experienceId 83{ 84 if (!experienceId) { 85 NSString *kernelSuggestion = ([EXKernel isDevKernel]) ? @"Make sure you are serving the kernel." : @""; 86 NSAssert(experienceId, @"Cannot associate an error with a nil experience id. %@", kernelSuggestion); 87 } 88 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 89 if (error) { 90 if (!record) { 91 record = [[EXErrorRecoveryRecord alloc] init]; 92 @synchronized (_experienceInfo) { 93 _experienceInfo[experienceId] = record; 94 } 95 } 96 // mark this experience id as having loading problems, so future attempts will bust the cache. 97 // this flag never gets unset until the record is removed, even if the error is nullified. 98 record.isRecovering = YES; 99 } 100 if (record) { 101 record.error = error; 102 } 103} 104 105- (BOOL)errorBelongsToExperience:(NSError *)error 106{ 107 if (!error) { 108 return NO; 109 } 110 NSArray<NSString *> *experienceIds; 111 @synchronized (_experienceInfo) { 112 experienceIds = _experienceInfo.allKeys; 113 } 114 for (NSString *experienceId in experienceIds) { 115 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 116 if ([record.error isEqual:error]) { 117 return YES; 118 } 119 } 120 return NO; 121} 122 123- (void)experienceFinishedLoadingWithId:(NSString *)experienceId 124{ 125 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 126 if (!record) { 127 record = [[EXErrorRecoveryRecord alloc] init]; 128 @synchronized (_experienceInfo) { 129 _experienceInfo[experienceId] = record; 130 } 131 } 132 record.dtmLastLoaded = [NSDate date]; 133 134 // maintain a global record of when anything last loaded, used to calculate autoreload backoff. 135 _dtmAnyExperienceLoaded = [NSDate date]; 136} 137 138- (BOOL)experienceIdIsRecoveringFromError:(NSString *)experienceId 139{ 140 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 141 if (record) { 142 return record.isRecovering; 143 } 144 return NO; 145} 146 147- (BOOL)experienceIdShouldReloadOnError:(NSString *)experienceId 148{ 149 EXErrorRecoveryRecord *record = [self _recordForExperienceId:experienceId]; 150 if (record) { 151 return ([record.dtmLastLoaded timeIntervalSinceNow] < -[self reloadBufferSeconds]); 152 } 153 // if we have no knowledge of this experience, sure, try reloading right away. 154 return YES; 155} 156 157- (void)increaseAutoReloadBuffer 158{ 159 _reloadBufferDepth++; 160} 161 162#pragma mark - kernel service 163 164- (void)kernelDidRegisterBridgeWithRecord:(EXKernelBridgeRecord *)record 165{ 166 @synchronized (_experienceInfo) { 167 // if this experience had a loading error previously, consider it recovered now 168 [_experienceInfo removeObjectForKey:record.experienceId]; 169 } 170} 171 172#pragma mark - internal 173 174- (EXErrorRecoveryRecord *)_recordForExperienceId: (NSString *)experienceId; 175{ 176 EXErrorRecoveryRecord *result = nil; 177 if (experienceId) { 178 @synchronized (_experienceInfo) { 179 result = _experienceInfo[experienceId]; 180 } 181 } 182 return result; 183} 184 185- (void)_handleRecoveryPropsNotification:(NSNotification *)notif 186{ 187 NSDictionary *params = notif.userInfo; 188 [self setDeveloperInfo:params[@"props"] forExperienceid:params[@"experienceId"]]; 189} 190 191- (NSTimeInterval)reloadBufferSeconds 192{ 193 NSTimeInterval interval = MIN(60.0 * 5.0, EX_AUTO_REFRESH_BUFFER_BASE_SECONDS * pow(1.5, _reloadBufferDepth)); 194 195 // if nothing has loaded for twice our current backoff interval, reset backoff 196 if ([_dtmAnyExperienceLoaded timeIntervalSinceNow] < -(interval * 2.0)) { 197 _reloadBufferDepth = 0; 198 interval = EX_AUTO_REFRESH_BUFFER_BASE_SECONDS; 199 } 200 return interval; 201} 202 203@end 204