diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e6eea5..93c36b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.12.0] - 2018-04-22 +### Added +- `LDClient` `isOnline` readonly property that reports the online/offline status. +- `LDClient` `setOnline` method to set the online/offline status. `setOnline` may operate asynchronously, so the client calls an optional completion block when the requested operation completes. + +### Changed +- Fixed potential memory leak with `DarklyEventSource`. + +### Removed +- `LDClient` `online` and `offline` methods. + +### Fixed +- Calling `updateUser` on `LDClient` while streaming no longer causes the SDK to request feature flags. The SDK now disconnects from the LaunchDarkly service and reconnects with the updated user. +- Calling `updateUser` on `LDClient` while polling now resets the polling timer after making a feature flag request. + +## [2.11.2] - 2018-04-06 +### Changed +- Changes the minimum required `DarklyEventSource` to version `3.2.1` in the CocoaPods podspec +- The maximum backoff time for reconnecting to the feature stream is now 1 hour. + ## [2.11.1] - 2018-03-26 ### Changed - Changes the minimum required `DarklyEventSource` to version `3.2.0` in the CocoaPods podspec diff --git a/Cartfile b/Cartfile index ac5488b3..8f6ef0b4 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "launchdarkly/ios-eventsource" >= 3.2 +github "launchdarkly/ios-eventsource" >= 3.2.3 diff --git a/Cartfile.resolved b/Cartfile.resolved index 318791ce..2d9d8b3a 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "launchdarkly/ios-eventsource" "3.2.0" +github "launchdarkly/ios-eventsource" "3.2.3" diff --git a/Darkly.xcodeproj/project.pbxproj b/Darkly.xcodeproj/project.pbxproj index 5d34dc0a..cf7a9b13 100644 --- a/Darkly.xcodeproj/project.pbxproj +++ b/Darkly.xcodeproj/project.pbxproj @@ -162,6 +162,16 @@ 8305EC8A202243D6002F20DB /* nullConfigIsANull-null-withVersion.json in Resources */ = {isa = PBXBuildFile; fileRef = 8305EC89202243D6002F20DB /* nullConfigIsANull-null-withVersion.json */; }; 8305EC8C2022440D002F20DB /* nullConfigIsANull-null-withoutVersion.json in Resources */ = {isa = PBXBuildFile; fileRef = 8305EC8B2022440D002F20DB /* nullConfigIsANull-null-withoutVersion.json */; }; 8305EC8E20227365002F20DB /* featureFlags-withoutVersions.json in Resources */ = {isa = PBXBuildFile; fileRef = 8305EC8D20227365002F20DB /* featureFlags-withoutVersions.json */; }; + 830C2AC5207579AC001D645D /* LDThrottler.h in Headers */ = {isa = PBXBuildFile; fileRef = 830C2AC3207579AC001D645D /* LDThrottler.h */; }; + 830C2AC6207579AC001D645D /* LDThrottler.h in Headers */ = {isa = PBXBuildFile; fileRef = 830C2AC3207579AC001D645D /* LDThrottler.h */; }; + 830C2AC7207579AC001D645D /* LDThrottler.h in Headers */ = {isa = PBXBuildFile; fileRef = 830C2AC3207579AC001D645D /* LDThrottler.h */; }; + 830C2AC8207579AC001D645D /* LDThrottler.h in Headers */ = {isa = PBXBuildFile; fileRef = 830C2AC3207579AC001D645D /* LDThrottler.h */; }; + 830C2AC9207579AC001D645D /* LDThrottler.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AC4207579AC001D645D /* LDThrottler.m */; }; + 830C2ACA207579AC001D645D /* LDThrottler.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AC4207579AC001D645D /* LDThrottler.m */; }; + 830C2ACB207579AC001D645D /* LDThrottler.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AC4207579AC001D645D /* LDThrottler.m */; }; + 830C2ACC207579AC001D645D /* LDThrottler.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AC4207579AC001D645D /* LDThrottler.m */; }; + 830C2ACF20757CE9001D645D /* LDThrottlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */; }; + 830C2AD220768697001D645D /* LDThrottler+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AD120768697001D645D /* LDThrottler+Testable.m */; }; 83258A3D1F323049008C2133 /* LDClientManager+EventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */; }; 83258A401F3244D0008C2133 /* LDUserBuilder+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */; }; 83258A421F32721A008C2133 /* emptyConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = 83258A411F32721A008C2133 /* emptyConfig.json */; }; @@ -368,6 +378,11 @@ 8305EC8D20227365002F20DB /* featureFlags-withoutVersions.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "featureFlags-withoutVersions.json"; sourceTree = ""; }; 830BF92F202A8854006DF9B1 /* NSJSONSerialization+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSJSONSerialization+Testable.h"; sourceTree = ""; }; 830BF930202A8855006DF9B1 /* NSJSONSerialization+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSJSONSerialization+Testable.m"; sourceTree = ""; }; + 830C2AC3207579AC001D645D /* LDThrottler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDThrottler.h; sourceTree = ""; }; + 830C2AC4207579AC001D645D /* LDThrottler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDThrottler.m; sourceTree = ""; }; + 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDThrottlerTest.m; sourceTree = ""; }; + 830C2AD020768697001D645D /* LDThrottler+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDThrottler+Testable.h"; sourceTree = ""; }; + 830C2AD120768697001D645D /* LDThrottler+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDThrottler+Testable.m"; sourceTree = ""; }; 83258A3B1F323049008C2133 /* LDClientManager+EventSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDClientManager+EventSource.h"; sourceTree = ""; }; 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDClientManager+EventSource.m"; sourceTree = ""; }; 83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDUserBuilder+Testable.h"; sourceTree = ""; }; @@ -589,6 +604,8 @@ 690346E61E68990000E45133 /* LDPollingManager.m */, 690346E71E68990000E45133 /* LDRequestManager.h */, 690346E81E68990000E45133 /* LDRequestManager.m */, + 830C2AC3207579AC001D645D /* LDThrottler.h */, + 830C2AC4207579AC001D645D /* LDThrottler.m */, 690346E91E68990000E45133 /* LDUserBuilder.h */, 690346EA1E68990000E45133 /* LDUserBuilder.m */, 690346EB1E68990000E45133 /* LDUserModel.h */, @@ -614,6 +631,7 @@ 6903471B1E689B9F00E45133 /* LDUtilTest.m */, 6903471C1E689B9F00E45133 /* LDClientManagerTest.m */, 6903471F1E689B9F00E45133 /* LDConfigTest.m */, + 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */, 690347201E689B9F00E45133 /* DarklyXCTestCase.h */, 690347221E689B9F00E45133 /* DarklyXCTestCase.m */, 690346CE1E6872EA00E45133 /* Info.plist */, @@ -726,6 +744,8 @@ 83EF678C1F98FC9200403126 /* LDFlagConfigModel+Testable.m */, 830BF92F202A8854006DF9B1 /* NSJSONSerialization+Testable.h */, 830BF930202A8855006DF9B1 /* NSJSONSerialization+Testable.m */, + 830C2AD020768697001D645D /* LDThrottler+Testable.h */, + 830C2AD120768697001D645D /* LDThrottler+Testable.m */, ); path = Categories; sourceTree = ""; @@ -791,6 +811,7 @@ 6903470D1E68990000E45133 /* NSDictionary+JSON.h in Headers */, 690347091E68990000E45133 /* LDUtil.h in Headers */, 69BAF40D1E9AAB4800747613 /* Darkly.h in Headers */, + 830C2AC5207579AC001D645D /* LDThrottler.h in Headers */, 690346F71E68990000E45133 /* LDClient.h in Headers */, 833D08CB1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83889B171F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.h in Headers */, @@ -822,6 +843,7 @@ 69A87EAA1E74712800B88B23 /* LDUserBuilder.h in Headers */, 69A87EAE1E74712800B88B23 /* LDUtil.h in Headers */, 69071F801EA2A7CC00497F93 /* Darkly.h in Headers */, + 830C2AC8207579AC001D645D /* LDThrottler.h in Headers */, 69A87E9C1E74712800B88B23 /* LDClient.h in Headers */, 833D08CE1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83889B1A1F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.h in Headers */, @@ -853,6 +875,7 @@ 69BD7E2B1E6C79910056D70F /* LDUserBuilder.h in Headers */, 69BD7E2F1E6C79910056D70F /* LDUtil.h in Headers */, 69071F7F1EA2A7CB00497F93 /* Darkly.h in Headers */, + 830C2AC7207579AC001D645D /* LDThrottler.h in Headers */, 69BD7E1D1E6C79910056D70F /* LDClient.h in Headers */, 833D08CD1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83889B191F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.h in Headers */, @@ -884,6 +907,7 @@ 69F3F6971E6BF82C00079A09 /* LDClient.h in Headers */, 69F3F6AB1E6BF82C00079A09 /* NSDictionary+JSON.h in Headers */, 69071F7E1EA2A7CA00497F93 /* Darkly.h in Headers */, + 830C2AC6207579AC001D645D /* LDThrottler.h in Headers */, 69F3F6A31E6BF82C00079A09 /* LDRequestManager.h in Headers */, 833D08CC1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83889B181F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.h in Headers */, @@ -1343,6 +1367,7 @@ 8305EC6B20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67851F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 6903470E1E68990000E45133 /* NSDictionary+JSON.m in Sources */, + 830C2AC9207579AC001D645D /* LDThrottler.m in Sources */, 690347021E68990000E45133 /* LDPollingManager.m in Sources */, 690346FC1E68990000E45133 /* LDConfig.m in Sources */, 690347061E68990000E45133 /* LDUserBuilder.m in Sources */, @@ -1361,6 +1386,7 @@ 8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */, 839956E820053081009707D1 /* LDUserModel+Testable.m in Sources */, 690347291E689B9F00E45133 /* LDEventModelTest.m in Sources */, + 830C2ACF20757CE9001D645D /* LDThrottlerTest.m in Sources */, 690347301E689B9F00E45133 /* LDConfigTest.m in Sources */, 690347261E689B9F00E45133 /* LDUserBuilderTest.m in Sources */, 832C788D1F2977B800E334A2 /* NSString+RemoveWhitespace.m in Sources */, @@ -1369,6 +1395,7 @@ 6903472B1E689B9F00E45133 /* LDPollingManagerTest.m in Sources */, 83EF67901F99365600403126 /* LDClient+Testable.m in Sources */, 83258A3D1F323049008C2133 /* LDClientManager+EventSource.m in Sources */, + 830C2AD220768697001D645D /* LDThrottler+Testable.m in Sources */, 8305EC7A20222B5E002F20DB /* NSObject+LDFlagConfigValueTest.m in Sources */, 83BE938C201A797B00DD1ED9 /* NSJSONSerialization+Testable.m in Sources */, 6903472E1E689B9F00E45133 /* LDClientManagerTest.m in Sources */, @@ -1410,6 +1437,7 @@ 8305EC6E20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67881F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69A87EAF1E74712800B88B23 /* LDUtil.m in Sources */, + 830C2ACC207579AC001D645D /* LDThrottler.m in Sources */, 69A87EA71E74712800B88B23 /* LDPollingManager.m in Sources */, 69A87E9F1E74712800B88B23 /* LDClientManager.m in Sources */, 69A87EAD1E74712800B88B23 /* LDUserModel.m in Sources */, @@ -1439,6 +1467,7 @@ 8305EC6D20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67871F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69BD7E301E6C79910056D70F /* LDUtil.m in Sources */, + 830C2ACB207579AC001D645D /* LDThrottler.m in Sources */, 69BD7E281E6C79910056D70F /* LDPollingManager.m in Sources */, 69BD7E201E6C79910056D70F /* LDClientManager.m in Sources */, 69BD7E2E1E6C79910056D70F /* LDUserModel.m in Sources */, @@ -1468,6 +1497,7 @@ 8305EC6C20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67861F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69F3F6AA1E6BF82C00079A09 /* LDUtil.m in Sources */, + 830C2ACA207579AC001D645D /* LDThrottler.m in Sources */, 69F3F6A21E6BF82C00079A09 /* LDPollingManager.m in Sources */, 69F3F69A1E6BF82C00079A09 /* LDClientManager.m in Sources */, 69F3F6A81E6BF82C00079A09 /* LDUserModel.m in Sources */, diff --git a/Darkly/DarklyConstants.h b/Darkly/DarklyConstants.h index 155d5433..68668a60 100644 --- a/Darkly/DarklyConstants.h +++ b/Darkly/DarklyConstants.h @@ -57,3 +57,4 @@ extern NSInteger const kHTTPStatusCodeUnauthorized; extern NSInteger const kHTTPStatusCodeMethodNotAllowed; extern NSInteger const kHTTPStatusCodeNotImplemented; extern NSInteger const kErrorCodeUnauthorized; +extern NSTimeInterval const kMaxThrottlingDelayInterval; diff --git a/Darkly/DarklyConstants.m b/Darkly/DarklyConstants.m index d0f48560..a1750bd7 100644 --- a/Darkly/DarklyConstants.m +++ b/Darkly/DarklyConstants.m @@ -4,7 +4,7 @@ #import "DarklyConstants.h" -NSString * const kClientVersion = @"2.11.1"; +NSString * const kClientVersion = @"2.12.0"; NSString * const kBaseUrl = @"https://app.launchdarkly.com"; NSString * const kEventsUrl = @"https://mobile.launchdarkly.com"; NSString * const kStreamUrl = @"https://clientstream.launchdarkly.com"; @@ -44,3 +44,4 @@ NSInteger const kHTTPStatusCodeMethodNotAllowed = 405; NSInteger const kHTTPStatusCodeNotImplemented = 501; NSInteger const kErrorCodeUnauthorized = -kHTTPStatusCodeUnauthorized; +NSTimeInterval const kMaxThrottlingDelayInterval = 600.0; diff --git a/Darkly/LDClient.h b/Darkly/LDClient.h index 98afbc04..fe792d89 100644 --- a/Darkly/LDClient.h +++ b/Darkly/LDClient.h @@ -20,6 +20,7 @@ @interface LDClient : NSObject +@property (nonatomic, assign, readonly) BOOL isOnline; @property(nonatomic, strong, readonly) LDUserModel *ldUser; @property(nonatomic, strong, readonly) LDConfig *ldConfig; @property (nonatomic, weak) id delegate; @@ -125,17 +126,18 @@ */ - (LDUserBuilder *)currentUserBuilder; /** - * Set the client to offline mode. No events will be synced to server. + * Set the client to online/offline mode. When online events will be synced to server. (Default) * - * @return whether offline mode was successfully updated. + * @param goOnline Desired online/offline mode for the client */ -- (BOOL)offline; +- (void)setOnline:(BOOL)goOnline; /** - * Set the client to online mode. Events will be synced to server. (Default) + * Set the client to online/offline mode. When online events will be synced to server. (Default) * - * @return whether online mode was successfully updated. + * @param goOnline Desired online/offline mode for the client + * @param completion Completion block called when setOnline completes */ -- (BOOL)online; +- (void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion; /** * Sync all events to the server. Events are synced to the server on a * regular basis, however this will force all stored events from the client diff --git a/Darkly/LDClient.m b/Darkly/LDClient.m index 4300f983..22e11ece 100644 --- a/Darkly/LDClient.m +++ b/Darkly/LDClient.m @@ -10,11 +10,15 @@ #import "LDPollingManager.h" #import "DarklyConstants.h" #import "NSThread+MainExecutable.h" +#import "LDThrottler.h" @interface LDClient() +@property (nonatomic, assign) BOOL isOnline; @property(nonatomic, strong) LDUserModel *ldUser; @property(nonatomic, strong) LDConfig *ldConfig; @property (nonatomic, assign) BOOL clientStarted; +@property (nonatomic, strong) LDThrottler *throttler; +@property (nonatomic, assign) BOOL willGoOnlineAfterDelay; @end @implementation LDClient @@ -25,6 +29,7 @@ +(LDClient *)sharedInstance static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedLDClient = [[self alloc] init]; + sharedLDClient.throttler = [[LDThrottler alloc] initWithMaxDelayInterval:kMaxThrottlingDelayInterval]; [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient selector:@selector(userUpdated) name: kLDUserUpdatedNotification object: nil]; @@ -67,27 +72,25 @@ - (BOOL)start:(LDConfig *)inputConfig withUserBuilder:(LDUserBuilder *)inputUser inputUserBuilder = inputUserBuilder ?: [[LDUserBuilder alloc] init]; self.ldUser = [inputUserBuilder build]; - [LDClientManager sharedInstance].online = YES; + [self setOnline:YES]; return YES; } - (BOOL)updateUser:(LDUserBuilder *)builder { DEBUG_LOGX(@"LDClient updateUser method called"); - if (self.clientStarted) { - if (builder) { - self.ldUser = [LDUserBuilder compareNewBuilder:builder withUser:self.ldUser]; - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager syncWithServerForConfig]; - return YES; - } else { - DEBUG_LOGX(@"LDClient updateUser needs a non-nil LDUserBuilder object"); - return NO; - } - } else { - DEBUG_LOGX(@"LDClient not started yet!"); + if (!self.clientStarted) { + DEBUG_LOGX(@"LDClient aborted updateUser: client not started"); + return NO; + } + if (!builder) { + DEBUG_LOGX(@"LDClient aborted updateUser: LDUserBuilder is nil"); return NO; } + + self.ldUser = [LDUserBuilder compareNewBuilder:builder withUser:self.ldUser]; + [[LDClientManager sharedInstance] updateUser]; + return YES; } - (LDUserBuilder *)currentUserBuilder { @@ -241,29 +244,50 @@ - (BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary } } -- (BOOL)offline -{ - DEBUG_LOGX(@"LDClient offline method called"); - if (self.clientStarted) { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:NO]; - return YES; - } else { +-(void)setOnline:(BOOL)goOnline { + [self setOnline:goOnline completion:nil]; +} + +-(void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion { + if (!self.clientStarted) { DEBUG_LOGX(@"LDClient not started yet!"); - return NO; + if (completion) { + completion(); + } + return; + } + self.willGoOnlineAfterDelay = goOnline; + if (goOnline == self.isOnline) { + DEBUG_LOG(@"LDClient setOnline:%@ aborted. LDClient is already %@", goOnline ? @"YES" : @"NO", goOnline ? @"online" : @"offline"); + if (completion) { + completion(); + } + return; + } + + if (!goOnline) { + DEBUG_LOGX(@"LDClient setOnline:NO called"); + [self _setOnline:NO completion:completion]; + return; } + [self.throttler runThrottled:^{ + if (!self.willGoOnlineAfterDelay) { + DEBUG_LOGX(@"LDClient setOnline:YES aborted. Client last received an offline request when the throttling timer expired."); + if (completion) { + completion(); + } + return; + } + DEBUG_LOGX(@"LDClient setOnline:YES called"); + [self _setOnline:YES completion:completion]; + }]; } -- (BOOL)online -{ - DEBUG_LOGX(@"LDClient online method called"); - if (self.clientStarted) { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:YES]; - return YES; - } else { - DEBUG_LOGX(@"LDClient not started yet!"); - return NO; +-(void)_setOnline:(BOOL)isOnline completion:(void(^)(void))completion { + self.isOnline = isOnline; + [[LDClientManager sharedInstance] setOnline:isOnline]; + if (completion) { + completion(); } } @@ -286,7 +310,7 @@ - (BOOL)stopClient { return NO; } - [self offline]; + [self setOnline:NO]; self.clientStarted = NO; return YES; } @@ -327,7 +351,7 @@ -(void)configFlagUpdated:(NSNotification *)notification { -(void)handleClientUnauthorizedNotification { [NSThread performOnMainThread:^{ DEBUG_LOGX(@"LDClient received Client Unauthorized notification. Taking LDClient offline."); - [self offline]; + [self setOnline:NO]; }]; } diff --git a/Darkly/LDClientManager.h b/Darkly/LDClientManager.h index fe8d1b35..84278fa2 100644 --- a/Darkly/LDClientManager.h +++ b/Darkly/LDClientManager.h @@ -31,6 +31,7 @@ - (void)processedConfig:(BOOL)success jsonConfigDictionary:(NSDictionary *)jsonConfigDictionary; - (void)startPolling; - (void)stopPolling; +- (void)updateUser; - (void)willEnterBackground; - (void)willEnterForeground; - (void)flushEvents; diff --git a/Darkly/LDClientManager.m b/Darkly/LDClientManager.m index 36649625..ea843fed 100644 --- a/Darkly/LDClientManager.m +++ b/Darkly/LDClientManager.m @@ -93,6 +93,24 @@ - (void)stopPolling { [self flushEvents]; } +-(void)updateUser { + if (!self.isOnline) { + DEBUG_LOGX(@"ClientManager updateUser aborted - manager is offline"); + return; + } + if (self.eventSource) { + [self stopEventSource]; + } + [[LDPollingManager sharedInstance] stopConfigPolling]; + + if ([[[LDClient sharedInstance] ldConfig] streaming]) { + [self configureEventSource]; + } else { + [self syncWithServerForConfig]; + [[LDPollingManager sharedInstance] startConfigPolling]; + } +} + - (void)willEnterBackground { DEBUG_LOGX(@"ClientManager entering background"); LDPollingManager *pollingMgr = [LDPollingManager sharedInstance]; @@ -141,11 +159,13 @@ - (void)configureEventSource { eventSource = [self eventSourceForUser:[LDClient sharedInstance].ldUser config:[LDClient sharedInstance].ldConfig httpHeaders:[self httpHeadersForEventSource]]; + __weak typeof(self) weakSelf = self; [eventSource onMessage:^(LDEvent *event) { - [self handlePingEvent:event]; - [self handlePutEvent:event]; - [self handlePatchEvent:event]; - [self handleDeleteEvent:event]; + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf handlePingEvent:event]; + [strongSelf handlePutEvent:event]; + [strongSelf handlePatchEvent:event]; + [strongSelf handleDeleteEvent:event]; }]; [eventSource onError:^(LDEvent *event) { @@ -278,6 +298,7 @@ - (void)postClientUnauthorizedNotification { - (void)stopEventSource { @synchronized (self) { + DEBUG_LOGX(@"ClientManager stopping event source."); [eventSource close]; eventSource = nil; } diff --git a/Darkly/LDThrottler.h b/Darkly/LDThrottler.h new file mode 100644 index 00000000..461a75e5 --- /dev/null +++ b/Darkly/LDThrottler.h @@ -0,0 +1,20 @@ +// +// LDThrottler.h +// Darkly +// +// Created by Mark Pokorny on 4/4/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@interface LDThrottler : NSObject +@property (nonatomic, assign, readonly) NSTimeInterval maxDelayInterval; +@property (nonatomic, assign, readonly) NSUInteger runAttempts; +@property (nonatomic, assign, readonly) NSTimeInterval delayInterval; +@property (nonatomic, strong, readonly) NSDate * _Nullable timerStartDate; +@property (nonatomic, strong, readonly) NSTimer * _Nullable delayTimer; + +-(nullable instancetype)initWithMaxDelayInterval:(NSTimeInterval)maxDelaySeconds; +-(void)runThrottled:(void (^_Nonnull)(void))completion; +@end diff --git a/Darkly/LDThrottler.m b/Darkly/LDThrottler.m new file mode 100644 index 00000000..c8504f74 --- /dev/null +++ b/Darkly/LDThrottler.m @@ -0,0 +1,102 @@ +// +// LDThrottler.m +// Darkly +// +// Created by Mark Pokorny on 4/4/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDThrottler.h" +#import "DarklyConstants.h" +#import "LDUtil.h" + +const NSTimeInterval minDelayInterval = 1.0; + +@interface LDThrottler() +@property (nonatomic, assign) NSTimeInterval maxDelayInterval; +@property (nonatomic, assign) NSUInteger runAttempts; +@property (nonatomic, assign) NSTimeInterval delayInterval; +@property (nonatomic, strong) NSTimer *delayTimer; +@property (nonatomic, strong) NSDate *timerStartDate; +@property (nonatomic, strong) void(^runBlock)(void); +@property (nonatomic, strong) void(^timerFiredCallback)(void); +@end + +@implementation LDThrottler +-(instancetype)initWithMaxDelayInterval:(NSTimeInterval)maxDelayInterval { + if (!(self = [super init])) { return nil; } + + self.maxDelayInterval = maxDelayInterval > 0 && maxDelayInterval <= kMaxThrottlingDelayInterval ? maxDelayInterval : kMaxThrottlingDelayInterval; + DEBUG_LOG(@"Throttler created with max delay: %0.2f", self.maxDelayInterval); + + return self; +} + +-(void)runThrottled:(void (^)(void))runBlock { + if (!runBlock) { return; } + if (self.delayInterval == self.maxDelayInterval) { + self.runAttempts += 1; + self.runBlock = runBlock; + DEBUG_LOG(@"Throttler delay interval at max. Allowing delay timer to expire. Run Attempts: %ld", (unsigned long)self.runAttempts); + return; + } + + @synchronized(self) { + if (self.delayTimer) { + [self.delayTimer invalidate]; + } + } + if (self.runAttempts == 0) { + DEBUG_LOGX(@"Throttler executing run block on first attempt."); + runBlock(); + } else { + self.runBlock = runBlock; + } + + self.runAttempts += 1; + self.delayInterval = [self delayIntervalForRunAttempts:self.runAttempts]; + self.delayTimer = [self delayTimerWithDelayInterval:self.delayInterval]; + if (self.runAttempts > 1) { + DEBUG_LOG(@"Throttler throttling run block. Run Attempts: %ld Delay: %0.2f", (unsigned long)self.runAttempts, self.delayInterval); + } +} + +-(NSTimeInterval)delayIntervalForRunAttempts:(NSUInteger)runAttempts { + if (runAttempts > log2(self.maxDelayInterval)) { return self.maxDelayInterval; } + double maxAttempts = log2(DBL_MAX); //use to prevent overflowing + NSTimeInterval exponentialBackoff = runAttempts < maxAttempts ? MIN(self.maxDelayInterval, minDelayInterval * pow(2, runAttempts)) : self.maxDelayInterval; // pow(x,y) returns x^y + NSTimeInterval jitterBackoff = arc4random_uniform(exponentialBackoff); // arc4random_uniform(upperBound) returns a double uniformly randomized between 0.0.. 1 && self.runBlock) { + DEBUG_LOGX(@"Throttler delay timer fired, executing run block."); + self.runBlock(); + } + + self.runAttempts = 0; + self.delayInterval = 0.0; + self.timerStartDate = nil; + self.delayTimer = nil; + self.runBlock = nil; + + if (!self.timerFiredCallback) { return; } + self.timerFiredCallback(); + self.timerFiredCallback = nil; + } +} + +@end diff --git a/DarklyTests/Categories/LDClient+Testable.h b/DarklyTests/Categories/LDClient+Testable.h index e21f28c6..f9a17e71 100644 --- a/DarklyTests/Categories/LDClient+Testable.h +++ b/DarklyTests/Categories/LDClient+Testable.h @@ -7,7 +7,9 @@ // #import +#import "LDThrottler.h" @interface LDClient(Testable) @property (nonatomic, assign) BOOL clientStarted; +@property (nonatomic, strong) LDThrottler *throttler; @end diff --git a/DarklyTests/Categories/LDClient+Testable.m b/DarklyTests/Categories/LDClient+Testable.m index 321910d5..409249be 100644 --- a/DarklyTests/Categories/LDClient+Testable.m +++ b/DarklyTests/Categories/LDClient+Testable.m @@ -10,4 +10,5 @@ @implementation LDClient(Testable) @dynamic clientStarted; +@dynamic throttler; @end diff --git a/DarklyTests/Categories/LDThrottler+Testable.h b/DarklyTests/Categories/LDThrottler+Testable.h new file mode 100644 index 00000000..769502aa --- /dev/null +++ b/DarklyTests/Categories/LDThrottler+Testable.h @@ -0,0 +1,13 @@ +// +// LDThrottler+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 4/5/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDThrottler.h" + +@interface LDThrottler(Testable) +@property (nonatomic, strong) void(^timerFiredCallback)(void); +@end diff --git a/DarklyTests/Categories/LDThrottler+Testable.m b/DarklyTests/Categories/LDThrottler+Testable.m new file mode 100644 index 00000000..fc0de6ad --- /dev/null +++ b/DarklyTests/Categories/LDThrottler+Testable.m @@ -0,0 +1,13 @@ +// +// LDThrottler+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 4/5/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDThrottler+Testable.h" + +@implementation LDThrottler(Testable) +@dynamic timerFiredCallback; +@end diff --git a/DarklyTests/LDClientManagerTest.m b/DarklyTests/LDClientManagerTest.m index fcc6cb81..5f2a6fa8 100644 --- a/DarklyTests/LDClientManagerTest.m +++ b/DarklyTests/LDClientManagerTest.m @@ -317,6 +317,63 @@ - (void)testStopPolling { XCTAssertNil([clientManager eventSource]); } +- (void)testUpdateUser_offline_streaming { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; + config.streaming = YES; + [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.pollingManagerMock reject] startConfigPolling]; + + [[LDClientManager sharedInstance] updateUser]; + + [self.eventSourceMock verify]; + [self.pollingManagerMock verify]; +} + +- (void)testUpdateUser_offline_polling { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; + config.streaming = NO; + [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.pollingManagerMock reject] startConfigPolling]; + + [[LDClientManager sharedInstance] updateUser]; + + [self.eventSourceMock verify]; + [self.pollingManagerMock verify]; +} + +- (void)testUpdateUser_online_streaming { + LDConfig *config = [LDClient sharedInstance].ldConfig; + config.streaming = YES; + [[LDClientManager sharedInstance] setOnline:YES]; + //The eventSourceMock registered the eventSourceWithURL call from the setOnline call above. Replace it so it can measure the updateUser response + [self.eventSourceMock stopMocking]; + self.eventSourceMock = OCMClassMock([LDEventSource class]); + OCMStub(ClassMethod([self.eventSourceMock eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]])).andReturn(self.eventSourceMock); + [[self.eventSourceMock expect] close]; + [[self.pollingManagerMock expect] stopConfigPolling]; + [[self.pollingManagerMock reject] startConfigPolling]; + + [[LDClientManager sharedInstance] updateUser]; + + //Calling verify on the mock isn't working correctly, but the macro syntax does + OCMVerify([self.eventSourceMock eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]); + [self.pollingManagerMock verify]; +} + +- (void)testUpdateUser_online_polling { + LDConfig *config = [LDClient sharedInstance].ldConfig; + config.streaming = NO; + [[LDClientManager sharedInstance] setOnline:YES]; + [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.pollingManagerMock expect] stopConfigPolling]; + [[self.pollingManagerMock expect] startConfigPolling]; + + [[LDClientManager sharedInstance] updateUser]; + + [self.eventSourceMock verify]; + [self.pollingManagerMock verify]; +} + - (void)testWillEnterBackground { LDClientManager *clientManager = [LDClientManager sharedInstance]; [clientManager willEnterBackground]; diff --git a/DarklyTests/LDClientTest.m b/DarklyTests/LDClientTest.m index 24342ed8..2fbae1f2 100644 --- a/DarklyTests/LDClientTest.m +++ b/DarklyTests/LDClientTest.m @@ -13,6 +13,7 @@ #import "LDUserBuilder+Testable.h" #import "LDClient+Testable.h" #import "NSJSONSerialization+Testable.h" +#import "LDThrottler.h" #import "OCMock.h" #import @@ -63,6 +64,7 @@ @interface LDClientTest : DarklyXCTestCase @property (nonatomic, strong) id mockLDClientManager; @property (nonatomic, strong) id mockLDDataManager; @property (nonatomic, strong) id mockLDRequestManager; +@property (nonatomic, strong) id throttlerMock; @end NSString *const kFallbackString = @"fallbackString"; @@ -87,6 +89,10 @@ - (void)setUp { id mockRequestManager = OCMClassMock([LDRequestManager class]); OCMStub(ClassMethod([mockRequestManager sharedInstance])).andReturn(mockRequestManager); self.mockLDRequestManager = mockRequestManager; + + self.throttlerMock = OCMClassMock([LDThrottler class]); + OCMStub([self.throttlerMock runThrottled:[OCMArg invokeBlock]]); + [LDClient sharedInstance].throttler = self.throttlerMock; } - (void)tearDown { @@ -118,15 +124,22 @@ - (void)testStartWithoutConfig { - (void)testStartWithValidConfig { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - LDClient *client = [LDClient sharedInstance]; - BOOL didStart = [client start:config withUserBuilder:nil]; + LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; + userBuilder.key = [[NSUUID UUID] UUIDString]; + + BOOL didStart = [[LDClient sharedInstance] start:config withUserBuilder:userBuilder]; XCTAssertTrue(didStart); } - (void)testStartWithValidConfigMultipleTimes { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - XCTAssertTrue([[LDClient sharedInstance] start:config withUserBuilder:nil]); - XCTAssertFalse([[LDClient sharedInstance] start:config withUserBuilder:nil]); + LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; + userBuilder.key = [[NSUUID UUID] UUIDString]; + + XCTAssertTrue([[LDClient sharedInstance] start:config withUserBuilder:userBuilder]); + XCTAssertFalse([[LDClient sharedInstance] start:config withUserBuilder:userBuilder]); + + [self.mockLDDataManager verify]; } - (void)testBoolVariationWithStart { @@ -478,38 +491,71 @@ - (void)testTrackWithStart { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; [[LDClient sharedInstance] start:config withUserBuilder:nil]; - OCMStub([self.dataManagerMock createCustomEvent:[OCMArg isKindOfClass:[NSString class]] withCustomValuesDictionary:[OCMArg isKindOfClass:[NSDictionary class]] user:[OCMArg any] config:[OCMArg any]]); + OCMStub([self.dataManagerMock createCustomEvent:[OCMArg isKindOfClass:[NSString class]] withCustomValuesDictionary:[OCMArg isKindOfClass:[NSDictionary class]] user:[OCMArg any] config:[OCMArg any]]); XCTAssertTrue([[LDClient sharedInstance] track:@"test" data:customData]); - OCMVerify([self.dataManagerMock createCustomEvent: @"test" withCustomValuesDictionary: customData user:[OCMArg isKindOfClass:[LDUserModel class]] config:config]); + OCMVerify([self.dataManagerMock createCustomEvent:@"test" withCustomValuesDictionary:customData user:[OCMArg isKindOfClass:[LDUserModel class]] config:config]); } -- (void)testOfflineWithoutStart { - XCTAssertFalse([[LDClient sharedInstance] offline]); -} +- (void)testSetOnline_NO_beforeStart { + [[self.mockLDClientManager reject] setOnline:[OCMArg any]]; + __block NSInteger completionCallCount = 0; -- (void)testOfflineWithStart { - [[self.mockLDClientManager expect] setOnline:YES]; + [[LDClient sharedInstance] setOnline:NO completion: ^{ + completionCallCount += 1; + }]; + XCTAssertFalse([LDClient sharedInstance].isOnline); + [self.mockLDClientManager verify]; + XCTAssertEqual(completionCallCount, 1); +} + +- (void)testSetOnline_NO_afterStart { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; [[LDClient sharedInstance] start:config withUserBuilder:nil]; - XCTAssertTrue([[LDClient sharedInstance] offline]); + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.mockLDClientManager expect] setOnline:NO]; + __block NSInteger completionCallCount = 0; + + [[LDClient sharedInstance] setOnline:NO completion: ^{ + completionCallCount += 1; + }]; + + XCTAssertFalse([LDClient sharedInstance].isOnline); [self.mockLDClientManager verify]; + [self.throttlerMock verify]; + XCTAssertEqual(completionCallCount, 1); } -- (void)testOnlineWithoutStart { - XCTAssertFalse([[LDClient sharedInstance] online]); +- (void)testSetOnline_YES_beforeStart { + [[self.mockLDClientManager reject] setOnline:[OCMArg any]]; + __block NSInteger completionCallCount = 0; + + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; + + XCTAssertFalse([LDClient sharedInstance].isOnline); + [self.mockLDClientManager verify]; + XCTAssertEqual(completionCallCount, 1); } -- (void)testOnlineWithStart { +- (void)testSetOnline_YES_afterStart { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; //sets LDClientManager online...start the mock after calling start - + [[LDClient sharedInstance] start:config withUserBuilder:nil]; + [[LDClient sharedInstance] setOnline:NO]; [[self.mockLDClientManager expect] setOnline:YES]; + __block NSInteger completionCallCount = 0; + //The throttler mock expectation is not getting fulfilled even though the LDClient does invoke it. + //Since the throttler mock is set to execute blocks, setting the expectation on the client manager mock verifies that the client is calling the throttler + + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; - XCTAssertTrue([[LDClient sharedInstance] online]); [self.mockLDClientManager verify]; + XCTAssertEqual(completionCallCount, 1); } - (void)testFlushWithoutStart { @@ -533,17 +579,21 @@ - (void)testStopClient { } - (void)testUpdateUserWithoutStart { + [[self.mockLDClientManager reject] updateUser]; XCTAssertFalse([[LDClient sharedInstance] updateUser:[[LDUserBuilder alloc] init]]); + [self.mockLDClientManager verify]; } -(void)testUpdateUserWithStart { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + [[self.mockLDClientManager expect] updateUser]; LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - - LDClient *ldClient = [LDClient sharedInstance]; - [ldClient start:config withUserBuilder:userBuilder]; + userBuilder.key = [[NSUUID UUID] UUIDString]; + [[LDClient sharedInstance] start:config withUserBuilder:nil]; + + XCTAssertTrue([[LDClient sharedInstance] updateUser:userBuilder]); - XCTAssertTrue([[LDClient sharedInstance] updateUser:[[LDUserBuilder alloc] init]]); + [self.mockLDClientManager verify]; } - (void)testCurrentUserBuilderWithoutStart { diff --git a/DarklyTests/LDThrottlerTest.m b/DarklyTests/LDThrottlerTest.m new file mode 100644 index 00000000..86a625c5 --- /dev/null +++ b/DarklyTests/LDThrottlerTest.m @@ -0,0 +1,165 @@ +// +// LDThrottlerTest.m +// DarklyTests +// +// Created by Mark Pokorny on 4/4/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import "DarklyConstants.h" +#import "LDThrottler.h" +#import "LDThrottler+Testable.h" + +@interface LDThrottlerTest: DarklyXCTestCase + +@end + +const NSTimeInterval delayInterval = 10.0; + +@implementation LDThrottlerTest +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +-(void)testInitWithMaxDelaySeconds { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:delayInterval]; + XCTAssertTrue(throttler.maxDelayInterval == delayInterval); + XCTAssertTrue(throttler.runAttempts == 0); + XCTAssertTrue(throttler.delayInterval == 0); + XCTAssertNil(throttler.delayTimer); +} + +-(void)testInitWithMaxDelaySecondsTooLong { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:kMaxThrottlingDelayInterval + 1.0]; + XCTAssertTrue(throttler.maxDelayInterval == kMaxThrottlingDelayInterval); + XCTAssertTrue(throttler.runAttempts == 0); + XCTAssertTrue(throttler.delayInterval == 0); + XCTAssertNil(throttler.delayTimer); +} + +-(void)testInitWithMaxDelaySecondsTooShort { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:0]; + XCTAssertTrue(throttler.maxDelayInterval == kMaxThrottlingDelayInterval); + XCTAssertTrue(throttler.runAttempts == 0); + XCTAssertTrue(throttler.delayInterval == 0); + XCTAssertNil(throttler.delayTimer); +} + +-(void)testRunThrottledFirstTry { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:delayInterval]; + XCTestExpectation *timerFiredCalledExpectation = [self expectationWithDescription:@"Timer fired called expectation"]; + throttler.timerFiredCallback = ^{ + [timerFiredCalledExpectation fulfill]; + }; + __block NSInteger completionBlockCallCount = 0; + __block NSDate *completionBlockCallDate; + NSDate *runThrottledStartDate = [NSDate date]; + + [throttler runThrottled:^{ + completionBlockCallCount += 1; + completionBlockCallDate = [NSDate date]; + }]; + + [self waitForExpectations:@[timerFiredCalledExpectation] timeout:2.0]; + + XCTAssertTrue(completionBlockCallCount == 1); + XCTAssertNotNil(completionBlockCallDate); + if (completionBlockCallDate) { + NSTimeInterval delayTime = [completionBlockCallDate timeIntervalSinceDate:runThrottledStartDate]; + XCTAssertTrue(delayTime < 0.1); + } + XCTAssertTrue(throttler.runAttempts == 0); + XCTAssertTrue(throttler.delayInterval == 0); + XCTAssertNil(throttler.timerStartDate); + XCTAssertNil(throttler.delayTimer); +} + +-(void)testRunThrottledSecondTry { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:delayInterval]; + XCTestExpectation *timerFiredCalledExpectation = [self expectationWithDescription:@"Timer fired called expectation"]; + throttler.timerFiredCallback = ^{ + [timerFiredCalledExpectation fulfill]; + }; + __block NSInteger completionBlockCallCount = 0; + __block NSMutableArray *completionBlockCallDates = [NSMutableArray arrayWithCapacity:2]; + NSDate *runThrottledStartDate = [NSDate date]; + + for (NSUInteger attempt = 0; attempt < 2; attempt++) { + [throttler runThrottled:^{ + completionBlockCallCount += 1; + [completionBlockCallDates addObject:[NSDate date]]; + }]; + } + + [self waitForExpectations:@[timerFiredCalledExpectation] timeout:4.0]; + + XCTAssertTrue(completionBlockCallCount == 2); + XCTAssertTrue(completionBlockCallDates.count == 2); + if (completionBlockCallDates.count == 2) { + NSTimeInterval delayTime = [completionBlockCallDates[0] timeIntervalSinceDate:runThrottledStartDate]; + XCTAssertTrue(delayTime < 0.1); + delayTime = [completionBlockCallDates[1] timeIntervalSinceDate:completionBlockCallDates[0]]; + XCTAssertTrue(delayTime < 4.0); + } + XCTAssertTrue(throttler.runAttempts == 0); + XCTAssertTrue(throttler.delayInterval == 0); + XCTAssertNil(throttler.timerStartDate); + XCTAssertNil(throttler.delayTimer); +} + +-(void)testRunThrottledMultipleTries { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:delayInterval]; + NSUInteger runAttempts = ceil(log2(delayInterval)); + + __block NSInteger completionBlockCallCount = 0; + NSDate *timerStartDate; + for (NSUInteger attempt = 0; attempt < runAttempts; attempt++) { + NSTimeInterval delayIntervalForPreviousAttempt = throttler.delayInterval; + [throttler runThrottled:^{ + completionBlockCallCount += 1; + }]; + if (attempt == 0) { + timerStartDate = throttler.timerStartDate; + } + XCTAssertTrue(delayIntervalForPreviousAttempt < throttler.delayInterval); + XCTAssertEqual(timerStartDate, throttler.timerStartDate); + XCTAssertNotNil(throttler.delayTimer); + } + //Didn't wait for the delay timer to fire, + XCTAssertTrue(completionBlockCallCount == 1); + XCTAssertTrue(throttler.runAttempts == runAttempts); +} + +-(void)testRunThrottledMaxDelay { + LDThrottler *throttler = [[LDThrottler alloc] initWithMaxDelayInterval:delayInterval]; + __block NSInteger completionBlockCallCount = 0; + NSDate *timerStartDate; + NSUInteger runAttempts = ceil(log2(delayInterval)) + 1; + + for (NSUInteger attempt = 0; attempt < runAttempts; attempt++) { + NSTimeInterval delayIntervalForPreviousAttempt = throttler.delayInterval; + [throttler runThrottled:^{ + completionBlockCallCount += 1; + }]; + if (attempt == 0) { + timerStartDate = throttler.timerStartDate; + } + XCTAssertTrue(delayIntervalForPreviousAttempt < throttler.delayInterval || throttler.delayInterval == delayInterval); + XCTAssertEqual(timerStartDate, throttler.timerStartDate); + XCTAssertNotNil(throttler.delayTimer); + } + + //Didn't wait for the delay timer to fire, + XCTAssertTrue(completionBlockCallCount == 1); + XCTAssertTrue(throttler.runAttempts == runAttempts); + XCTAssertTrue(throttler.delayInterval == delayInterval); +} + +@end diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 18745b03..93b8fcda 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "LaunchDarkly" - s.version = "2.11.1" + s.version = "2.12.0" s.summary = "iOS SDK for LaunchDarkly" s.description = <<-DESC @@ -23,13 +23,13 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.osx.deployment_target = '10.10' - s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.11.1" } + s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.12.0" } s.source_files = "Darkly/*.{h,m}" s.requires_arc = true s.subspec 'Core' do |eventSource| - eventSource.dependency 'DarklyEventSource', '~>3.2.0' + eventSource.dependency 'DarklyEventSource', '~>3.2.3' end end diff --git a/Podfile b/Podfile index bf786e3d..e56482c1 100644 --- a/Podfile +++ b/Podfile @@ -1,22 +1,22 @@ use_frameworks! target 'Darkly_iOS' do platform :ios, '8.0' - pod 'DarklyEventSource', '~> 3.2' + pod 'DarklyEventSource', '~> 3.2.3' end target 'Darkly_tvOS' do platform :tvos, '9.0' - pod 'DarklyEventSource', '~> 3.2' + pod 'DarklyEventSource', '~> 3.2.3' end target 'Darkly_watchOS' do platform :watchos, '2.0' - pod 'DarklyEventSource', '~> 3.2' + pod 'DarklyEventSource', '~> 3.2.3' end target 'Darkly_osx' do platform :osx, '10.10' - pod 'DarklyEventSource', '~> 3.2' + pod 'DarklyEventSource', '~> 3.2.3' end target 'DarklyTests' do diff --git a/Podfile.lock b/Podfile.lock index 13f4cb19..26c939e2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - DarklyEventSource (3.2.0) + - DarklyEventSource (3.2.3) - OCMock (3.4.1) - OHHTTPStubs (4.8.0): - OHHTTPStubs/Default (= 4.8.0) @@ -16,15 +16,15 @@ PODS: - OHHTTPStubs/OHPathHelpers (4.8.0) DEPENDENCIES: - - DarklyEventSource (~> 3.2) + - DarklyEventSource (~> 3.2.3) - OCMock (~> 3.1) - OHHTTPStubs (~> 4.2) SPEC CHECKSUMS: - DarklyEventSource: ad6a75c82b22bea91c75fc980a563f6d2902ce90 + DarklyEventSource: d5c8e000988ee1feac6c3ffa3dcbdfbaca10d444 OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3 OHHTTPStubs: b393565822317305b87a1440d4c7aff131679f66 -PODFILE CHECKSUM: f3f36ff2533f4e1230e409cec137989205971a1f +PODFILE CHECKSUM: 09df0b8e563d60585362751cc2450fe49d915cc9 COCOAPODS: 1.4.0 diff --git a/Pods/DarklyEventSource/LDEventSource/LDEventSource.m b/Pods/DarklyEventSource/LDEventSource/LDEventSource.m index f1f55000..b87a3727 100644 --- a/Pods/DarklyEventSource/LDEventSource/LDEventSource.m +++ b/Pods/DarklyEventSource/LDEventSource/LDEventSource.m @@ -11,7 +11,7 @@ static CGFloat const ES_RETRY_INTERVAL = 1.0; static CGFloat const ES_DEFAULT_TIMEOUT = 300.0; -static CGFloat const ES_MAX_RECONNECT_TIME = 180.0; +static CGFloat const ES_MAX_RECONNECT_TIME = 3600.0; static NSString *const ESKeyValueDelimiter = @":"; static NSString *const LDEventSeparatorLFLF = @"\n\n"; @@ -93,8 +93,9 @@ - (instancetype)initWithURL:(NSURL *)URL httpHeaders:(NSDictionary 3.2.3' end ``` @@ -117,7 +117,7 @@ $ brew install carthage To integrate EventSource into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-eventsource" +github "launchdarkly/ios-eventsource" >= 3.2.3 ``` Run `carthage` to build the framework and drag the built `EventSource.framework` into your Xcode project. diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index 13f4cb19..26c939e2 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -1,5 +1,5 @@ PODS: - - DarklyEventSource (3.2.0) + - DarklyEventSource (3.2.3) - OCMock (3.4.1) - OHHTTPStubs (4.8.0): - OHHTTPStubs/Default (= 4.8.0) @@ -16,15 +16,15 @@ PODS: - OHHTTPStubs/OHPathHelpers (4.8.0) DEPENDENCIES: - - DarklyEventSource (~> 3.2) + - DarklyEventSource (~> 3.2.3) - OCMock (~> 3.1) - OHHTTPStubs (~> 4.2) SPEC CHECKSUMS: - DarklyEventSource: ad6a75c82b22bea91c75fc980a563f6d2902ce90 + DarklyEventSource: d5c8e000988ee1feac6c3ffa3dcbdfbaca10d444 OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3 OHHTTPStubs: b393565822317305b87a1440d4c7aff131679f66 -PODFILE CHECKSUM: f3f36ff2533f4e1230e409cec137989205971a1f +PODFILE CHECKSUM: 09df0b8e563d60585362751cc2450fe49d915cc9 COCOAPODS: 1.4.0 diff --git a/Pods/Target Support Files/DarklyEventSource-iOS/Info.plist b/Pods/Target Support Files/DarklyEventSource-iOS/Info.plist index 9ae03a0c..806ba0d4 100644 --- a/Pods/Target Support Files/DarklyEventSource-iOS/Info.plist +++ b/Pods/Target Support Files/DarklyEventSource-iOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + 3.2.3 CFBundleSignature ???? CFBundleVersion diff --git a/Pods/Target Support Files/DarklyEventSource-macOS/Info.plist b/Pods/Target Support Files/DarklyEventSource-macOS/Info.plist index 9ae03a0c..806ba0d4 100644 --- a/Pods/Target Support Files/DarklyEventSource-macOS/Info.plist +++ b/Pods/Target Support Files/DarklyEventSource-macOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + 3.2.3 CFBundleSignature ???? CFBundleVersion diff --git a/Pods/Target Support Files/DarklyEventSource-tvOS/Info.plist b/Pods/Target Support Files/DarklyEventSource-tvOS/Info.plist index 9ae03a0c..806ba0d4 100644 --- a/Pods/Target Support Files/DarklyEventSource-tvOS/Info.plist +++ b/Pods/Target Support Files/DarklyEventSource-tvOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + 3.2.3 CFBundleSignature ???? CFBundleVersion diff --git a/Pods/Target Support Files/DarklyEventSource-watchOS/Info.plist b/Pods/Target Support Files/DarklyEventSource-watchOS/Info.plist index 9ae03a0c..806ba0d4 100644 --- a/Pods/Target Support Files/DarklyEventSource-watchOS/Info.plist +++ b/Pods/Target Support Files/DarklyEventSource-watchOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + 3.2.3 CFBundleSignature ???? CFBundleVersion