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