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