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