From 95114f081edc45a6fee9c5bf911a03cce831c07d Mon Sep 17 00:00:00 2001 From: Joe Cieslik <5600929+torchhound@users.noreply.github.com> Date: Fri, 25 Oct 2019 13:46:08 -0700 Subject: [PATCH] Prepare 4.2.0 release (#196) * Changes user-device to device model identifier (#75) * installs new deviceModel into EnvironmentReporter and renames old deviceModel to deviceType * use CwSysCtl for macos model * Replaced entire flag store ff array instead of in place manipulation * Updated CHANGELOG and bumped version number * Added correct copy behavior to FlagStore delete and update * Improved CHANGELOG entry for 4.0.1 * Changed version to 4.1.0, updated CHANGELOG * Update 4.1.0 release date * installs ios-eventsource 4.0.2 * installs CocoaPods 1.7.2 * clears swift 5 update warning * updates circle config to use xcode 10.2.1 * advances version to 4.1.1 * installs Nimble 8.0.2 * updates SwiftLint to 0.33.0 * Update 4.1.1 release date * Improved CHANGELOG 4.1.1 description * Made CHANGELOG 4.1.1 more consistent * 4.1.2 release, updated version numbers and CHANGELOG * Made 4.1.2 CHANGELOG entry more descriptive * Rebuilt Connection Status, added unit tests, fixed warnings * Changed guard to equals in Dictionary extension, changed ConnectionInformation to struct * Fixed threading issues and reference semantics * Improved ConnectionInformation toString, fixed background behavior * Added network connectivity check, changed TimeIntervals to Optionals * Added Connection Status files to correct Target Membership, added conditional compilation of network connectivity check for WatchOS * PR review changes * More PR review changes * Even more PR review changes * Removed ConnectionInformationCaching, removed redundant variables * Simplified synchronizing error behavior for connection status * DRY up setupListeners * Fixed string cases for LastConnectionFailureReason Codable * Forgot to add polling last successful connection * Store is now in Cache dir, removed listeners and 1 ldclient.shared, changed TimeInterval to Date, changed two inout's to var's * Added ConnectionModeChangedObserver * Fixed confusing docstring, removed unnecessary flag synchronizer * Changed getValue and toString to description * Changed description to computed property * Removed connectionModeCheck * Improved background connection status behavior * Changed ConnectionInformation description to computed property * Added new identify method, updated swiftlint rules * Made Identify actually change the user, fixed unit tests * Removed unnecessary _user assignment * Move swiftlint disable to LDClient, move user assignment into identifyInternal * Added private setOnline and go functions for identify * Forgot to replace go in guard with goIdentify * Copied user property unit tests for identify * Added optimization to not call setOnline when there is no completion and wasOnline is false * Testing identifyInternal change in IH * requested PR changes * Fixed convertCachedData call count, some PR feedback fixes * Simplified unit tests, added comment about thread safety * Changed lastSuccessfulConnection to lastKnownFlagValidity, added on stream close listener * Fixed handler * Remove unnecessary import * Changed eventSource access level * Reset lastKnownFlagValidity to nil when we make a successful stream connection * Made comment about lastKnownFlagValidity having a value more clear * Changed guard let to if let in DarklyService EventSource extension * Updated README, CHANGELOG, and podspec for 4.2.0 * Made 4.2.0 CHANGELOG entry more detailed --- CHANGELOG.md | 10 + LaunchDarkly.podspec | 4 +- LaunchDarkly.xcodeproj/project.pbxproj | 40 +++ .../GeneratedCode/mocks.generated.swift | 30 +++ .../LaunchDarkly/Extensions/Dictionary.swift | 6 +- LaunchDarkly/LaunchDarkly/LDClient.swift | 223 +++++++++++----- LaunchDarkly/LaunchDarkly/LDCommon.swift | 2 + .../Models/ConnectionInformation.swift | 198 ++++++++++++++ .../ConnectionModeChangeObserver.swift | 19 ++ .../Networking/DarklyService.swift | 19 +- .../ObjectiveC/ObjcLDClient.swift | 7 +- .../Cache/ConnectionInformationStore.swift | 40 +++ .../ClientServiceFactory.swift | 5 + .../Service Objects/FlagChangeNotifier.swift | 21 ++ .../Service Objects/FlagSynchronizer.swift | 13 + .../Service Objects/NetworkReporter.swift | 44 +++ .../LaunchDarklyTests/LDClientSpec.swift | 251 ++++++++++++++++++ .../Mocks/ClientServiceMockFactory.swift | 4 + Pods/Pods.xcodeproj/project.pbxproj | 1 + README.md | 4 +- 20 files changed, 862 insertions(+), 79 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift create mode 100644 LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift create mode 100644 LaunchDarkly/LaunchDarkly/Service Objects/Cache/ConnectionInformationStore.swift create mode 100644 LaunchDarkly/LaunchDarkly/Service Objects/NetworkReporter.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9f0e3e..8ce7cee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. ### Multiple Environment clients Version 4.0.0 does not support multiple environments. If you use version `2.14.0` or later and set `LDConfig`'s `secondaryMobileKeys` you will not be able to migrate to version `4.0.0`. Multiple Environments will be added in a future release to the Swift SDK. +## [4.2.0] - 2019-10-25 +### Added +- The `identify` function allows a completion to be called after a user is updated. +- The Connection Status API allows greater introspection into the current LaunchDarkly connection and the health of local flags. + • This feature adds a new class called `ConnectionInformation` that contains properties that keep track of the current connection mode e.g. streaming or polling, when and how a connection failed, and the last time flags were updated. This class can be accessed from `LDClient.shared.getConnectionInformation`. + • Additionally, a new observer function called `observeCurrentConnectionMode` allows your application to listen to changes in the SDK's connection to LaunchDarkly. + +### Changed +- The `user` property is now deprecated in favor of the `identify` function. + ## [4.1.2] - 2019-07-11 ### Fixed - WatchKit is now conditionally imported in WatchOS only, to fix an error in Xcode 11. diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 0b64678e..6d9bb024 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "4.1.2" + ld.version = "4.2.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC @@ -25,7 +25,7 @@ Pod::Spec.new do |ld| ld.tvos.deployment_target = "9.0" ld.osx.deployment_target = "10.10" - ld.source = { :git => "https://github.com/launchdarkly/ios-client-sdk.git", :tag => '4.1.2'} + ld.source = { :git => "https://github.com/launchdarkly/ios-client-sdk.git", :tag => '4.2.0'} ld.source_files = "LaunchDarkly/LaunchDarkly/**/*.{h,m,swift}" diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 1d284a34..16b07bee 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -298,6 +298,22 @@ 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */; }; 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */; }; 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */; }; + C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884623033B3600420721 /* ConnectionInformationStore.swift */; }; + C408884923033B7500420721 /* ConnectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884823033B7500420721 /* ConnectionInformation.swift */; }; + C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884823033B7500420721 /* ConnectionInformation.swift */; }; + C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884823033B7500420721 /* ConnectionInformation.swift */; }; + C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884823033B7500420721 /* ConnectionInformation.swift */; }; + C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884623033B3600420721 /* ConnectionInformationStore.swift */; }; + C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884623033B3600420721 /* ConnectionInformationStore.swift */; }; + C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C408884623033B3600420721 /* ConnectionInformationStore.swift */; }; + C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A4092315AA4D00145710 /* NetworkReporter.swift */; }; + C443A40B2315AA4D00145710 /* NetworkReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A4092315AA4D00145710 /* NetworkReporter.swift */; }; + C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A4092315AA4D00145710 /* NetworkReporter.swift */; }; + C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A4092315AA4D00145710 /* NetworkReporter.swift */; }; + C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; + C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; + C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; + C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; E48F5215B96AE48D10185962 /* Pods_LaunchDarkly_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB95B7FEBDC1E23F47304829 /* Pods_LaunchDarkly_tvOS.framework */; }; /* End PBXBuildFile section */ @@ -440,6 +456,10 @@ 94D29EF04A706E975E771E84 /* Pods_LaunchDarkly_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LaunchDarkly_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AC953A896A624F4525C218E5 /* Pods-LaunchDarkly_watchOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LaunchDarkly_watchOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LaunchDarkly_watchOS/Pods-LaunchDarkly_watchOS.debug.xcconfig"; sourceTree = ""; }; B0A56C29F8C0E59F338F9A07 /* Pods-LaunchDarkly_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LaunchDarkly_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-LaunchDarkly_tvOS/Pods-LaunchDarkly_tvOS.release.xcconfig"; sourceTree = ""; }; + C408884623033B3600420721 /* ConnectionInformationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionInformationStore.swift; sourceTree = ""; }; + C408884823033B7500420721 /* ConnectionInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionInformation.swift; sourceTree = ""; }; + C443A4092315AA4D00145710 /* NetworkReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReporter.swift; sourceTree = ""; }; + C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionModeChangeObserver.swift; sourceTree = ""; }; D58D143F8FD161584B3FF3AF /* Pods-LaunchDarklyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LaunchDarklyTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-LaunchDarklyTests/Pods-LaunchDarklyTests.release.xcconfig"; sourceTree = ""; }; D6840A437019F1CB72997480 /* Pods-LaunchDarkly_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LaunchDarkly_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-LaunchDarkly_iOS/Pods-LaunchDarkly_iOS.release.xcconfig"; sourceTree = ""; }; D8204934C417AFCE089F38BC /* Pods-LaunchDarklyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LaunchDarklyTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LaunchDarklyTests/Pods-LaunchDarklyTests.debug.xcconfig"; sourceTree = ""; }; @@ -613,6 +633,7 @@ 8354AC742243168800CDE602 /* Cache */ = { isa = PBXGroup; children = ( + C408884623033B3600420721 /* ConnectionInformationStore.swift */, 83D559731FD87CC9002D10C8 /* KeyedValueCache.swift */, 8354AC6F2243166900CDE602 /* UserEnvironmentFlagCache.swift */, 832D68A1224A38FC005F052A /* CacheConverter.swift */, @@ -706,6 +727,7 @@ 8354EFDE1F26380700C05156 /* Event.swift */, 83883DD4220B68A000EEAB95 /* ErrorObserver.swift */, 8354AC5F224150C300CDE602 /* Cache */, + C408884823033B7500420721 /* ConnectionInformation.swift */, ); path = Models; sourceTree = ""; @@ -811,6 +833,7 @@ 8358F25D1F474E5900ECE1AF /* LDChangedFlag.swift */, 8358F2611F47747F00ECE1AF /* FlagChangeObserver.swift */, 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */, + C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */, ); path = FlagChange; sourceTree = ""; @@ -900,6 +923,7 @@ 8347BB0B21F147E100E56BCD /* LDTimer.swift */, 83883DD9220B6A9A00EEAB95 /* ErrorNotifier.swift */, 8354AC742243168800CDE602 /* Cache */, + C443A4092315AA4D00145710 /* NetworkReporter.swift */, ); path = "Service Objects"; sourceTree = ""; @@ -1373,7 +1397,9 @@ 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, 83906A7921190B4000D7D3C5 /* FlagValueCounter.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, + C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, + C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83906A7A21190B4E00D7D3C5 /* EventTrackingContext.swift in Sources */, 831188652113AE4600D77CB5 /* Date.swift in Sources */, 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, @@ -1381,6 +1407,7 @@ 8311886D2113AE6700D77CB5 /* ObjcLDVariationValue.swift in Sources */, 8354AC642241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D1522E224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, + C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831188662113AE4A00D77CB5 /* AnyComparer.swift in Sources */, 831188492113ADD400D77CB5 /* LDFlagBaseTypeConvertible.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, @@ -1391,6 +1418,7 @@ 8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */, 8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */, 8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */, + C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */, 8354AC732243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 831188612113AE3700D77CB5 /* Optional.swift in Sources */, @@ -1427,12 +1455,15 @@ 831EF34820655E730001C643 /* LDFlagValueConvertible.swift in Sources */, 831EF34920655E730001C643 /* LDFlagBaseTypeConvertible.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, + C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, 8354AC722243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, + C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, 831AAE2E20A9E4F600B46DBA /* Throttler.swift in Sources */, @@ -1448,6 +1479,7 @@ 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, 8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */, + C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, 831EF35E20655E730001C643 /* Dictionary.swift in Sources */, 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */, @@ -1494,6 +1526,7 @@ 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, 83A2D6241F51CD7A00EA3BD4 /* LDUser.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, + C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, @@ -1501,7 +1534,9 @@ 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */, + C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 83F0A5621FB4D66600550A95 /* AnyComparer.swift in Sources */, + C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83EBCBA320D9A1F3003A7142 /* FlagValueCounter.swift in Sources */, 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, @@ -1518,6 +1553,7 @@ 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, + C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */, 83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */, 83EF678A1F97CFEC00403126 /* Dictionary.swift in Sources */, 8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */, @@ -1622,7 +1658,9 @@ 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, 83EBCBA420D9A1F3003A7142 /* FlagValueCounter.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, + C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 83D9EC882062DEAB004D7FA6 /* FlagChangeNotifier.swift in Sources */, + C443A40B2315AA4D00145710 /* NetworkReporter.swift in Sources */, 83D9EC892062DEAB004D7FA6 /* EventReporter.swift in Sources */, 83D9EC8A2062DEAB004D7FA6 /* FlagStore.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, @@ -1630,6 +1668,7 @@ 8354AC622241511D00CDE602 /* CacheableEnvironmentFlags.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, 83D1522C224D91B90054B6D4 /* DeprecatedCacheModelV3.swift in Sources */, + C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, 83D9EC8D2062DEAB004D7FA6 /* DarklyService.swift in Sources */, 83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */, 83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */, @@ -1640,6 +1679,7 @@ 83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */, 8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */, 8354AC712243166900CDE602 /* UserEnvironmentFlagCache.swift in Sources */, + C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */, 83D9EC932062DEAB004D7FA6 /* Array.swift in Sources */, 83D9EC942062DEAB004D7FA6 /* JSONSerialization.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 4916c14e..b7e8b3f2 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -43,6 +43,16 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider { onErrorEventCallback?() } + // MARK: onReadyStateChangedEvent + var onReadyStateChangedEventCallCount = 0 + var onReadyStateChangedEventCallback: (() -> Void)? + var onReadyStateChangedEventReceivedHandler: LDEventSourceEventHandler? + func onReadyStateChangedEvent(_ handler: LDEventSourceEventHandler?) { + onReadyStateChangedEventCallCount += 1 + onReadyStateChangedEventReceivedHandler = handler + onReadyStateChangedEventCallback?() + } + // MARK: open var openCallCount = 0 var openCallback: (() -> Void)? @@ -405,6 +415,16 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { addFlagsUnchangedObserverCallback?() } + // MARK: addConnectionModeChangedObserver + var addConnectionModeChangedObserverCallCount = 0 + var addConnectionModeChangedObserverCallback: (() -> Void)? + var addConnectionModeChangedObserverReceivedObserver: ConnectionModeChangedObserver? + func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) { + addConnectionModeChangedObserverCallCount += 1 + addConnectionModeChangedObserverReceivedObserver = observer + addConnectionModeChangedObserverCallback?() + } + // MARK: removeObserver var removeObserverCallCount = 0 var removeObserverCallback: (() -> Void)? @@ -415,6 +435,16 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { removeObserverCallback?() } + // MARK: notifyConnectionModeChangedObservers + var notifyConnectionModeChangedObserversCallCount = 0 + var notifyConnectionModeChangedObserversCallback: (() -> Void)? + var notifyConnectionModeChangedObserversReceivedConnectionMode: ConnectionInformation.ConnectionMode? + func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) { + notifyConnectionModeChangedObserversCallCount += 1 + notifyConnectionModeChangedObserversReceivedConnectionMode = connectionMode + notifyConnectionModeChangedObserversCallback?() + } + // MARK: notifyObservers var notifyObserversCallCount = 0 var notifyObserversCallback: (() -> Void)? diff --git a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift index 225ba6ff..781dafe6 100644 --- a/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift +++ b/LaunchDarkly/LaunchDarkly/Extensions/Dictionary.swift @@ -78,11 +78,7 @@ extension Optional where Wrapped == [String: Any] { public static func == (lhs: [String: Any]?, rhs: [String: Any]?) -> Bool { guard let lhs = lhs else { - guard let _ = rhs - else { - return true - } - return false + return rhs == nil } guard let rhs = rhs else { diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 2b962c2b..7d19e21a 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -43,6 +43,8 @@ enum LDClientRunMode { ```` The `changedFlag` passed in to the closure contains the old and new value, and the old and new valueSource. */ +// swiftlint:disable type_body_length +// swiftlint:disable file_length public class LDClient { // MARK: - State Controls and Indicators @@ -63,11 +65,30 @@ public class LDClient { didSet { flagSynchronizer.isOnline = isOnline eventReporter.isOnline = isOnline + if isOnline != oldValue { + connectionInformation = ConnectionInformation.onlineSetCheck(connectionInformation: connectionInformation, ldClient: self, config: config) + } } } //Keeps the state of the last setOnline goOnline parameter, used for throttling calls to set the SDK online private var lastSetOnlineCallValue = false + + //Stores ConnectionInformation in UserDefaults on change + var connectionInformation: ConnectionInformation { + didSet { + Log.debug(connectionInformation.description) + ConnectionInformationStore.storeConnectionInformation(connectionInformation: connectionInformation) + if connectionInformation.currentConnectionMode != oldValue.currentConnectionMode { + flagChangeNotifier.notifyConnectionModeChangedObservers(connectionMode: connectionInformation.currentConnectionMode) + } + } + } + + //Returns an object containing information about successful and/or failed polling or streaming connections to LaunchDarkly + public func getConnectionInformation() -> ConnectionInformation { + return connectionInformation + } /** Set the LDClient online/offline. @@ -101,13 +122,44 @@ public class LDClient { self.go(online: self.lastSetOnlineCallValue && self.canGoOnline, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: self.lastSetOnlineCallValue), completion: completion) } } + + private func setOnlineIdentify(_ goOnline: Bool, completion: (() -> Void)? = nil) { + lastSetOnlineCallValue = goOnline + guard goOnline, canGoOnline + else { + //go offline, which is not throttled + go(online: false, reasonOnlineUnavailable: reasonOnlineUnavailable(goOnline: goOnline), completion: completion) + return + } + + throttler.runThrottled { + //since going online was throttled, check the last called setOnline value and whether we can go online + self.goIdentify(online: self.lastSetOnlineCallValue && self.canGoOnline, reasonOnlineUnavailable: self.reasonOnlineUnavailable(goOnline: self.lastSetOnlineCallValue), completion: completion) + } + } + + private func goIdentify(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { + let owner = "SetOnlineOwner" as AnyObject + if completion != nil { + observeAll(owner: owner) { _ in + completion?() + self.stopObserving(owner: owner) + } + observeFlagsUnchanged(owner: owner) { + completion?() + self.stopObserving(owner: owner) + } + } + isOnline = goOnline + Log.debug(typeName(and: "setOnline", appending: ": ") + (reasonOnlineUnavailable.isEmpty ? "\(self.isOnline)." : "true aborted.") + reasonOnlineUnavailable) + } private var canGoOnline: Bool { return hasStarted && isInSupportedRunMode && !config.mobileKey.isEmpty } - private var isInSupportedRunMode: Bool { - return runMode == .foreground || allowBackgroundFlagUpdates + var isInSupportedRunMode: Bool { + return runMode == .foreground || config.enableBackgroundUpdates } private func go(online goOnline: Bool, reasonOnlineUnavailable: String, completion:(() -> Void)?) { @@ -153,7 +205,6 @@ public class LDClient { Log.debug(typeName(and: #function) + "new config set") let wasOnline = isOnline setOnline(false) - convertCachedData(skipDuringStart: isStarting) if let cachedFlags = flagCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: config.mobileKey), !cachedFlags.isEmpty { user.flagStore.replaceStore(newFlags: cachedFlags, source: .cache, completion: nil) @@ -167,7 +218,7 @@ public class LDClient { } /** - The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + This method of changing the user is deprecated.The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. @@ -176,34 +227,60 @@ public class LDClient { When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). To change both the `config` and `user`, set the LDClient offline, set both properties, then set the LDClient online. */ public var user: LDUser { - didSet { - Log.debug(typeName(and: #function) + "new user set with key: " + user.key ) - let wasOnline = isOnline - setOnline(false) - - if hasStarted { - eventReporter.recordSummaryEvent() - } - convertCachedData(skipDuringStart: isStarting) - if let cachedFlags = flagCache.retrieveFeatureFlags(forUserWithKey: user.key, andMobileKey: config.mobileKey), !cachedFlags.isEmpty { - user.flagStore.replaceStore(newFlags: cachedFlags, source: .cache, completion: nil) - } - service = serviceFactory.makeDarklyServiceProvider(config: config, user: user) - service.clearFlagResponseCache() - - if hasStarted { - eventReporter.record(Event.identifyEvent(user: user)) - } - - setOnline(wasOnline) + get { + return _user + } + @available(*, deprecated, message: "Please use the identify method instead") + set { + Log.debug("Setting the user property is deprecated, please use the identify method instead") + identify(user: newValue) } } + + private var _user: LDUser + + /** + The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. + + Normally, the client app should create and set the LDUser and pass that into `start(config: user: completion:)`. + + The client app can change the LDUser by getting the `user`, adjusting the values, and passing it to the LDClient method identify. This allows client apps to collect information over time from the user and update as information is collected. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information. If the client app does not create a LDUser, LDClient creates an anonymous default user, which can affect the feature flags delivered to the LDClient. + + When a new user is set, the LDClient goes offline and sets the new user. If the client was online when the new user was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). To change both the `config` and `user`, set the LDClient offline, set both properties, then set the LDClient online. A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new user are ready. + + This operation is not thread safe. You may want to use a DispatchQueue if calling `identify` from multiple threads. + + - parameter user: The LDUser set with the desired user. + - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional) + */ + public func identify(user: LDUser, completion: (() -> Void)? = nil) { + _user = user + Log.debug(typeName(and: #function) + "new user set with key: " + _user.key ) + let wasOnline = isOnline + setOnline(false) + + if hasStarted { + eventReporter.recordSummaryEvent() + } + convertCachedData(skipDuringStart: isStarting) + if let cachedFlags = flagCache.retrieveFeatureFlags(forUserWithKey: _user.key, andMobileKey: config.mobileKey), !cachedFlags.isEmpty { + _user.flagStore.replaceStore(newFlags: cachedFlags, source: .cache, completion: nil) + } + service = serviceFactory.makeDarklyServiceProvider(config: config, user: _user) + service.clearFlagResponseCache() + + if hasStarted { + eventReporter.record(Event.identifyEvent(user: _user)) + } + + setOnlineIdentify(wasOnline, completion: completion) + } private(set) var service: DarklyServiceProvider { didSet { Log.debug(typeName(and: #function) + "new service set") eventReporter.service = service - flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: effectiveStreamingMode(runMode: runMode, config: config), + flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, service: service, @@ -241,7 +318,8 @@ public class LDClient { let startUser = user ?? self.user cacheConverter.convertCacheData(for: startUser, and: config) //Convert before updating the user so any deprecated cached data is converted to the current model self.config = config - self.user = startUser + identify(user: startUser) + self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: serviceFactory) setOnline((wasStarted && wasOnline) || (!wasStarted && self.config.startOnline)) { Log.debug(self.typeName(and: #function, appending: ": ") + "started") @@ -258,20 +336,6 @@ public class LDClient { cacheConverter.convertCacheData(for: user, and: config) } - private func effectiveStreamingMode(runMode: LDClientRunMode, config: LDConfig) -> LDStreamingMode { - var reason = "" - let streamingMode: LDStreamingMode = (runMode == .foreground || allowBackgroundFlagUpdates) && config.streamingMode == .streaming && config.allowStreamingMode ? .streaming : .polling - if config.streamingMode == .streaming && runMode != .foreground && !allowBackgroundFlagUpdates { - reason = " LDClient is in background mode with background updates disabled." - } - if reason.isEmpty && config.streamingMode == .streaming && !config.allowStreamingMode { - reason = " LDConfig disallowed streaming mode. " - reason += !environmentReporter.operatingSystem.isStreamingEnabled ? "Streaming is not allowed on \(environmentReporter.operatingSystem)." : "Unknown reason." - } - Log.debug(typeName(and: #function, appending: ": ") + "\(streamingMode)\(reason)") - return streamingMode - } - /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning fallback values. @@ -527,7 +591,7 @@ public class LDClient { /** Sets a handler for the specified flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values, and old and new flag value source. See `LDChangedFlag` for details. - The SDK retains only weak references to the owner, which allows the client app to freely destroy owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. + The SDK retains only weak references to the owner, which allows the client app to freely destroy observer owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. The SDK executes handlers on the main thread. @@ -548,7 +612,7 @@ public class LDClient { ```` - parameter key: The LDFlagKey for the flag to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. + - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The closure the SDK will execute when the feature flag changes. */ public func observe(key: LDFlagKey, owner: LDObserverOwner, handler: @escaping LDFlagChangeHandler) { @@ -559,7 +623,7 @@ public class LDClient { /** Sets a handler for the specified flag keys executed on the specified owner. If any observed flag's value changes, executes the handler 1 time, passing in a dictionary of [LDFlagKey: LDChangedFlag] containing the old and new flag values, and old and new flag value source. See `LDChangedFlag` for details. - The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. + The SDK retains only weak references to owner, which allows the client app to freely destroy observer owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. The SDK executes handlers on the main thread. @@ -578,7 +642,7 @@ public class LDClient { ```` - parameter keys: An array of LDFlagKeys for the flags to observe. - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. + - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagCollectionChangeHandler the SDK will execute 1 time when any of the observed feature flags change. */ public func observe(keys: [LDFlagKey], owner: LDObserverOwner, handler: @escaping LDFlagCollectionChangeHandler) { @@ -589,7 +653,7 @@ public class LDClient { /** Sets a handler for all flag keys executed on the specified owner. If any flag's value changes, executes the handler 1 time, passing in a dictionary of [LDFlagKey: LDChangedFlag] containing the old and new flag values, and old and new flag value source. See `LDChangedFlag` for details. - The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. + The SDK retains only weak references to owner, which allows the client app to freely destroy observer owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. The SDK executes handlers on the main thread. @@ -607,7 +671,7 @@ public class LDClient { } ```` - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. + - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagCollectionChangeHandler the SDK will execute 1 time when any of the observed feature flags change. */ public func observeAll(owner: LDObserverOwner, handler: @escaping LDFlagCollectionChangeHandler) { @@ -620,7 +684,7 @@ public class LDClient { This handler can only ever be called when the LDClient is polling. - The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. + The SDK retains only weak references to owner, which allows the client app to freely destroy observer owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. The SDK executes handlers on the main thread. @@ -633,13 +697,37 @@ public class LDClient { } ```` - - parameter owner: The LDFlagChangeOwner which will execute the handler. The SDK retains a weak reference to the owner. + - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. - parameter handler: The LDFlagsUnchangedHandler the SDK will execute 1 time when a flag request completes with no flags changed. */ public func observeFlagsUnchanged(owner: LDObserverOwner, handler: @escaping LDFlagsUnchangedHandler) { Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") flagChangeNotifier.addFlagsUnchangedObserver(FlagsUnchangedObserver(owner: owner, flagsUnchangedHandler: handler)) } + + /** + Sets a handler executed when ConnectionInformation.currentConnectionMode changes. + + The SDK retains only weak references to owner, which allows the client app to freely destroy change owners without issues. Client apps should use a capture list specifying `[weak self]` inside handlers to avoid retain cycles causing a memory leak. + + The SDK executes handlers on the main thread. + + SeeAlso: `stopObserving(owner:)` + + ### Usage + ```` + LDClient.shared.observeCurrentConnectionMode(owner: self) { [weak self] in + //do something after ConnectionMode was updated. + } + ```` + + - parameter owner: The LDObserverOwner which will execute the handler. The SDK retains a weak reference to the owner. + - parameter handler: The LDConnectionModeChangedHandler the SDK will execute 1 time when ConnectionInformation.currentConnectionMode is changed. + */ + public func observeCurrentConnectionMode(owner: LDObserverOwner, handler: @escaping LDConnectionModeChangedHandler) { + Log.debug(typeName(and: #function) + " owner: \(String(describing: owner))") + flagChangeNotifier.addConnectionModeChangedObserver(ConnectionModeChangedObserver(owner: owner, connectionModeChangedHandler: handler)) + } /** Removes all observers for the given owner, including the flagsUnchangedObserver @@ -667,6 +755,7 @@ public class LDClient { case let .success(flagDictionary, streamingEvent): let oldFlags = user.flagStore.featureFlags let oldFlagSource = user.flagStore.flagValueSource + connectionInformation = ConnectionInformation.checkEstablishingStreaming(connectionInformation: connectionInformation) switch streamingEvent { case nil, .ping?, .put?: user.flagStore.replaceStore(newFlags: flagDictionary, source: .server) { @@ -692,6 +781,7 @@ public class LDClient { Log.debug(logPrefix + "LDClient is unauthorized") setOnline(false) } + connectionInformation = ConnectionInformation.synchronizingErrorCheck(synchronizingError: synchronizingError, connectionInformation: connectionInformation) DispatchQueue.main.async { self.errorNotifier.notifyObservers(of: synchronizingError) } @@ -756,6 +846,11 @@ public class LDClient { process(synchronizingError, logPrefix: typeName(and: #function, appending: ": ")) } } + + @objc private func didCloseEventSource() { + Log.debug(typeName(and: #function)) + self.connectionInformation = ConnectionInformation.lastSuccessfulConnectionCheck(connectionInformation: self.connectionInformation) + } // MARK: - Foreground / Background notification @@ -787,6 +882,7 @@ public class LDClient { if runMode == .background { eventReporter.reportEvents() } + eventReporter.isOnline = isOnline && runMode == .foreground let willSetSynchronizerOnline = isOnline && isInSupportedRunMode @@ -794,7 +890,9 @@ public class LDClient { //if it does match, keeping the synchronizer precludes an extra flag request if !flagSynchronizerConfigMatchesConfigAndRunMode { flagSynchronizer.isOnline = false - flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: effectiveStreamingMode(runMode: runMode, config: config), + let streamingModeVar = ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self) + connectionInformation = ConnectionInformation.backgroundBehavior(connectionInformation: connectionInformation, streamingMode: streamingModeVar, goOnline: willSetSynchronizerOnline) + flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: streamingModeVar, pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, service: service, @@ -803,14 +901,12 @@ public class LDClient { flagSynchronizer.isOnline = willSetSynchronizerOnline } } + private var flagSynchronizerConfigMatchesConfigAndRunMode: Bool { - return flagSynchronizer.streamingMode == effectiveStreamingMode(runMode: runMode, config: config) + return flagSynchronizer.streamingMode == ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self) && (flagSynchronizer.streamingMode == .streaming || flagSynchronizer.streamingMode == .polling && flagSynchronizer.pollingInterval == config.flagPollingInterval(runMode: runMode)) } - private var allowBackgroundFlagUpdates: Bool { - return config.enableBackgroundUpdates && environmentReporter.operatingSystem.isBackgroundEnabled - } private(set) var flagCache: FeatureFlagCaching private(set) var cacheConverter: CacheConverting @@ -833,14 +929,15 @@ public class LDClient { //dummy objects replaced by client at start config = LDConfig(mobileKey: "", environmentReporter: environmentReporter) - user = LDUser(environmentReporter: environmentReporter) - service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) + _user = LDUser(environmentReporter: environmentReporter) + service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: _user) + eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service) + errorNotifier = self.serviceFactory.makeErrorNotifier() + connectionInformation = self.serviceFactory.makeConnectionInformation() flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: .polling, pollingInterval: config.flagPollingInterval, useReport: config.useReport, service: service) - eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service) - errorNotifier = self.serviceFactory.makeErrorNotifier() if let backgroundNotification = environmentReporter.backgroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil) @@ -848,8 +945,10 @@ public class LDClient { if let foregroundNotification = environmentReporter.foregroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil) } - - //Since eventReporter lasts the life of the singleton, we can configure it here...swift requires the client to be instantiated before we can pass the onSyncComplete method + + NotificationCenter.default.addObserver(self, selector: #selector(didCloseEventSource), name: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil) + + //Since eventReporter lasts the life of the singleton, we can configure it here...swift requires the client to be instantiated before we can pass the onSyncComplete method eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service, onSyncComplete: onEventSyncComplete) } @@ -858,16 +957,18 @@ public class LDClient { //Setting these inside the init do not trigger the didSet closures self.runMode = runMode self.config = config - self.user = user + self.isStarting = true + identify(user: user) //dummy objects replaced by client at start service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) //didSet not triggered here - flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: effectiveStreamingMode(runMode: runMode, config: config), + flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, service: service, onSyncComplete: onFlagSyncComplete) eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service, onSyncComplete: onEventSyncComplete) + self.isStarting = false } } diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 09b48352..8ac28bc5 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -19,6 +19,8 @@ public typealias LDFlagChangeHandler = (LDChangedFlag) -> Void public typealias LDFlagCollectionChangeHandler = ([LDFlagKey: LDChangedFlag]) -> Void ///A closure used to notify an observer owner that a feature flag request resulted in no changes to any feature flag. public typealias LDFlagsUnchangedHandler = () -> Void +///A closure used to notify an observer owner that the current connection mode has changed. +public typealias LDConnectionModeChangedHandler = (ConnectionInformation.ConnectionMode) -> Void ///A closure used to notify an observer owner that an error occurred during feature flag processing. public typealias LDErrorHandler = (Error) -> Void diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift new file mode 100644 index 00000000..90776235 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -0,0 +1,198 @@ +// +// ConnectionInformation.swift +// LaunchDarkly_iOS +// +// Created by Joe Cieslik on 8/13/19. +// Copyright © 2019 Catamorphic Co. All rights reserved. +// + +import Foundation + +public struct ConnectionInformation: Codable, CustomStringConvertible { + public enum ConnectionMode: String, Codable { + case streaming, offline, establishingStreamingConnection, polling + } + + public enum LastConnectionFailureReason: Codable, CustomStringConvertible { + public var description: String { + switch self { + case .unauthorized: + return "unauthorized" + case .none: + return "none" + case .httpError: + return "httpError: " + String(self.httpValue ?? ConnectionInformation.Constants.noCode) + case .unknownError: + return "unknownError: " + (self.unknownValue ?? ConnectionInformation.Constants.unknownError) + } + } + + case unauthorized, httpError(Int), unknownError(String), none //We need .none for a non-failable initializer to conform to Codable + + var unknownValue: String? { + guard case let .unknownError(value) = self else { return nil } + return value + } + + var httpValue: Int? { + guard case let .httpError(value) = self else { return nil } + return value + } + } + + public struct Constants { + static let noCode: Int = 0 + static let unknownError: String = "Unknown Error" + static let decodeError: String = "Unable to Decode error." + } + + //lastKnownFlagValidity is nil if either no connection has ever been successfully made or if the SDK has an active streaming connection. It will have a value if 1) in polling mode and at least one poll has completed successfully, or 2) if in streaming mode whenever the streaming connection closes. + public internal(set) var lastKnownFlagValidity: Date? + public internal(set) var lastFailedConnection: Date? + public internal(set) var currentConnectionMode: ConnectionMode + public internal(set) var lastConnectionFailureReason: LastConnectionFailureReason + + init(currentConnectionMode: ConnectionMode, lastConnectionFailureReason: LastConnectionFailureReason, lastKnownFlagValidity: Date? = nil, lastFailedConnection: Date? = nil) { + self.currentConnectionMode = currentConnectionMode + self.lastConnectionFailureReason = lastConnectionFailureReason + self.lastKnownFlagValidity = lastKnownFlagValidity + self.lastFailedConnection = lastFailedConnection + } + + //Returns ConnectionInformation as a prettyfied string + public var description: String { + var connInfoString: String = "" + connInfoString.append("Current Connection Mode: \(currentConnectionMode.rawValue) | ") + connInfoString.append("Last Connection Failure Reason: \(lastConnectionFailureReason.description) | ") + connInfoString.append("Last Successful Connection: \(lastKnownFlagValidity?.debugDescription ?? "NONE") | ") + connInfoString.append("Last Failed Connection: \(lastFailedConnection?.debugDescription ?? "NONE")") + return connInfoString + } + + //Restores ConnectionInformation from UserDefaults if it exists + static func uncacheConnectionInformation(config: LDConfig, ldClient: LDClient, clientServiceFactory: ClientServiceCreating) -> ConnectionInformation { + var connectionInformation = ConnectionInformationStore.retrieveStoredConnectionInformation() ?? clientServiceFactory.makeConnectionInformation() + connectionInformation = onlineSetCheck(connectionInformation: connectionInformation, ldClient: ldClient, config: config) + return connectionInformation + } + + //Used for updating lastSuccessfulConnection when connected to streaming and connection closes + static func lastSuccessfulConnectionCheck(connectionInformation: ConnectionInformation) -> ConnectionInformation { + var connectionInformationVar = connectionInformation + if connectionInformationVar.currentConnectionMode == ConnectionInformation.ConnectionMode.streaming { + connectionInformationVar.lastKnownFlagValidity = Date() + } + return connectionInformationVar + } + + //Used for updating ConnectionInformation inside of LDClient.setOnline + static func onlineSetCheck(connectionInformation: ConnectionInformation, ldClient: LDClient, config: LDConfig) -> ConnectionInformation { + var connectionInformationVar = connectionInformation + if ldClient.isOnline && NetworkReporter.isConnectedToNetwork() { + connectionInformationVar.currentConnectionMode = effectiveStreamingMode(config: config, ldClient: ldClient) == LDStreamingMode.streaming ? ConnectionInformation.ConnectionMode.establishingStreamingConnection : ConnectionInformation.ConnectionMode.polling + } else { + connectionInformationVar.currentConnectionMode = ConnectionInformation.ConnectionMode.offline + } + return connectionInformationVar + } + + //Used for parsing SynchronizingError in LDClient.process + static func synchronizingErrorCheck(synchronizingError: SynchronizingError, connectionInformation: ConnectionInformation) -> ConnectionInformation { + var connectionInformationVar = connectionInformation + if synchronizingError.isClientUnauthorized { + connectionInformationVar.lastConnectionFailureReason = ConnectionInformation.LastConnectionFailureReason.unauthorized + } else { + switch synchronizingError { + case .request(let error): + let errorString = error as? String ?? Constants.unknownError + connectionInformationVar.lastConnectionFailureReason = ConnectionInformation.LastConnectionFailureReason.unknownError(errorString) + case .response(let urlResponse): + let statusCode = (urlResponse as? HTTPURLResponse)?.statusCode + connectionInformationVar.lastConnectionFailureReason = ConnectionInformation.LastConnectionFailureReason.httpError(statusCode ?? ConnectionInformation.Constants.noCode) + default: break + } + } + connectionInformationVar.lastFailedConnection = Date() + return connectionInformationVar + } + + //This function is used to ensure we switch from establishing a streaming connection to streaming once we are connected. + static func checkEstablishingStreaming(connectionInformation: ConnectionInformation) -> ConnectionInformation { + var connectionInformationVar = connectionInformation + if connectionInformationVar.currentConnectionMode == ConnectionInformation.ConnectionMode.establishingStreamingConnection { + connectionInformationVar.currentConnectionMode = ConnectionInformation.ConnectionMode.streaming + connectionInformationVar.lastKnownFlagValidity = nil + } + if connectionInformationVar.currentConnectionMode == ConnectionInformation.ConnectionMode.polling { + connectionInformationVar.lastKnownFlagValidity = Date() + } + return connectionInformationVar + } + + static func effectiveStreamingMode(config: LDConfig, ldClient: LDClient) -> LDStreamingMode { + var reason = "" + let streamingMode: LDStreamingMode = ldClient.isInSupportedRunMode && config.streamingMode == .streaming && config.allowStreamingMode ? .streaming : .polling + if config.streamingMode == .streaming && !ldClient.isInSupportedRunMode { + reason = " LDClient is in background mode with background updates disabled." + } + if reason.isEmpty && config.streamingMode == .streaming && !config.allowStreamingMode { + reason = " LDConfig disallowed streaming mode. " + reason += !ldClient.environmentReporter.operatingSystem.isStreamingEnabled ? "Streaming is not allowed on \(ldClient.environmentReporter.operatingSystem)." : "Unknown reason." + } + Log.debug(ldClient.typeName(and: #function, appending: ": ") + "\(streamingMode)\(reason)") + return streamingMode + } + + static func backgroundBehavior(connectionInformation: ConnectionInformation, streamingMode: LDStreamingMode, goOnline: Bool) -> ConnectionInformation { + var connectionInformationVar = connectionInformation + if !goOnline { + connectionInformationVar.currentConnectionMode = .offline + } else if streamingMode == .streaming { + connectionInformationVar.currentConnectionMode = .establishingStreamingConnection + } else if streamingMode == .polling { + connectionInformationVar.currentConnectionMode = .polling + } + return connectionInformationVar + } +} + +extension ConnectionInformation.LastConnectionFailureReason { + private enum CodingKeys: String, CodingKey { + case type, payload + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "unknownError": + let payload = try? container.decode(String.self, forKey: .payload) + self = .unknownError(payload ?? ConnectionInformation.Constants.decodeError) + case "httpError": + let payload = try? container.decode(Int.self, forKey: .payload) + self = .httpError(payload ?? ConnectionInformation.Constants.noCode) + case "unauthorized": + self = .unauthorized + default: + self = .none + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .unknownError(let error): + try container.encode("unknownError", forKey: .type) + try container.encode(error, forKey: .payload) + case .httpError(let code): + try container.encode("httpError", forKey: .type) + try container.encode(code, forKey: .payload) + case .unauthorized: + try container.encode("unauthorized", forKey: .type) + case .none: + try container.encode("none", forKey: .type) + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift new file mode 100644 index 00000000..855f3b8d --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift @@ -0,0 +1,19 @@ +// +// ConnectionModeChangeObserver.swift +// LaunchDarkly +// +// Created by Joe Cieslik on 8/29/19. +// Copyright © 2019 Catamorphic Co. All rights reserved. +// + +import Foundation + +struct ConnectionModeChangedObserver { + weak private(set) var owner: LDObserverOwner? + let connectionModeChangedHandler: LDConnectionModeChangedHandler? + + init(owner: LDObserverOwner, connectionModeChangedHandler: @escaping LDConnectionModeChangedHandler) { + self.owner = owner + self.connectionModeChangedHandler = connectionModeChangedHandler + } +} diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 0f0823b5..5f93dfb7 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -25,25 +25,28 @@ protocol DarklyServiceProvider: class { protocol DarklyStreamingProvider: class { func onMessageEvent(_ handler: LDEventSourceEventHandler?) func onErrorEvent(_ handler: LDEventSourceEventHandler?) + func onReadyStateChangedEvent(_ handler: LDEventSourceEventHandler?) func open() func close() } extension LDEventSource: DarklyStreamingProvider { func onMessageEvent(_ handler: LDEventSourceEventHandler?) { - guard let handler = handler - else { - return + if let handler = handler { + self.onMessage(handler) } - self.onMessage(handler) } func onErrorEvent(_ handler: LDEventSourceEventHandler?) { - guard let handler = handler - else { - return + if let handler = handler { + self.onError(handler) + } + } + + func onReadyStateChangedEvent(_ handler: LDEventSourceEventHandler?) { + if let handler = handler { + self.onReadyStateChanged(handler) } - self.onError(handler) } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 93147e3c..0114e7be 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -131,10 +131,15 @@ public final class ObjcLDClient: NSObject { get { return LDClient.shared.user.objcLdUser } + @available(*, deprecated, message: "Please use the identify method instead") set { - LDClient.shared.user = newValue.user + LDClient.shared.identify(user: newValue.user) } } + + @objc public func identify(user: ObjcLDUser) { + LDClient.shared.identify(user: user.user) + } /** Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. diff --git a/LaunchDarkly/LaunchDarkly/Service Objects/Cache/ConnectionInformationStore.swift b/LaunchDarkly/LaunchDarkly/Service Objects/Cache/ConnectionInformationStore.swift new file mode 100644 index 00000000..ad066a88 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Service Objects/Cache/ConnectionInformationStore.swift @@ -0,0 +1,40 @@ +// +// ConnectionInformationStore.swift +// LaunchDarkly_iOS +// +// Created by Joe Cieslik on 8/13/19. +// Copyright © 2019 Catamorphic Co. All rights reserved. +// + +import Foundation + +final class ConnectionInformationStore { + private static let connectionInformationKey = "com.launchDarkly.ConnectionInformationStore.connectionInformationKey" + + static func retrieveStoredConnectionInformation() -> ConnectionInformation? { + return UserDefaults.standard.retrieve(object: ConnectionInformation.self, fromKey: ConnectionInformationStore.connectionInformationKey) + } + + static func storeConnectionInformation(connectionInformation: ConnectionInformation) { + UserDefaults.standard.save(customObject: connectionInformation, forKey: ConnectionInformationStore.connectionInformationKey) + } +} + +private extension UserDefaults { + func save(customObject object: T, forKey key: String) { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(object) { + self.set(encoded, forKey: key) + } + } + + func retrieve(object type: T.Type, fromKey key: String) -> T? { + guard let data = self.data(forKey: key), + let object = try? JSONDecoder().decode(type, from: data) + else { + Log.debug("Couldnt decode object: \(key)") + return nil + } + return object + } +} diff --git a/LaunchDarkly/LaunchDarkly/Service Objects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/Service Objects/ClientServiceFactory.swift index c1bd223e..5a45168a 100644 --- a/LaunchDarkly/LaunchDarkly/Service Objects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/Service Objects/ClientServiceFactory.swift @@ -29,6 +29,7 @@ protocol ClientServiceCreating { func makeEnvironmentReporter() -> EnvironmentReporting func makeThrottler(maxDelay: TimeInterval, environmentReporter: EnvironmentReporting) -> Throttling func makeErrorNotifier() -> ErrorNotifying + func makeConnectionInformation() -> ConnectionInformation } final class ClientServiceFactory: ClientServiceCreating { @@ -100,4 +101,8 @@ final class ClientServiceFactory: ClientServiceCreating { func makeErrorNotifier() -> ErrorNotifying { return ErrorNotifier() } + + func makeConnectionInformation() -> ConnectionInformation { + return ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) + } } diff --git a/LaunchDarkly/LaunchDarkly/Service Objects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/Service Objects/FlagChangeNotifier.swift index 53f852dc..27b51598 100644 --- a/LaunchDarkly/LaunchDarkly/Service Objects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/Service Objects/FlagChangeNotifier.swift @@ -12,17 +12,20 @@ import Foundation protocol FlagChangeNotifying { func addFlagChangeObserver(_ observer: FlagChangeObserver) func addFlagsUnchangedObserver(_ observer: FlagsUnchangedObserver) + func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) //sourcery: noMock func removeObserver(_ key: LDFlagKey, owner: LDObserverOwner) func removeObserver(_ keys: [LDFlagKey], owner: LDObserverOwner) //sourcery: noMock func removeObserver(owner: LDObserverOwner) + func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) func notifyObservers(user: LDUser, oldFlags: [LDFlagKey: FeatureFlag], oldFlagSource: LDFlagValueSource) } final class FlagChangeNotifier: FlagChangeNotifying { private var flagChangeObservers = [FlagChangeObserver]() private var flagsUnchangedObservers = [FlagsUnchangedObserver]() + private var connectionModeChangedObservers = [ConnectionModeChangedObserver]() func addFlagChangeObserver(_ observer: FlagChangeObserver) { Log.debug(typeName(and: #function) + "observer: \(observer)") @@ -33,6 +36,11 @@ final class FlagChangeNotifier: FlagChangeNotifying { Log.debug(typeName(and: #function) + "observer: \(observer)") flagsUnchangedObservers.append(observer) } + + func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) { + Log.debug(typeName(and: #function) + "observer: \(observer)") + connectionModeChangedObservers.append(observer) + } ///Removes any change handling closures for flag.key from owner func removeObserver(_ key: LDFlagKey, owner: LDObserverOwner) { @@ -57,6 +65,19 @@ final class FlagChangeNotifier: FlagChangeNotifying { flagsUnchangedObservers = flagsUnchangedObservers.filter { (observer) in observer.owner !== owner } + connectionModeChangedObservers = connectionModeChangedObservers.filter { (observer) in + observer.owner !== owner + } + } + + func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) { + connectionModeChangedObservers.forEach { (connectionModeChangedObserver) in + if let connectionModeChangedHandler = connectionModeChangedObserver.connectionModeChangedHandler { + DispatchQueue.main.async { + connectionModeChangedHandler(connectionMode) + } + } + } } func notifyObservers(user: LDUser, oldFlags: [LDFlagKey: FeatureFlag], oldFlagSource: LDFlagValueSource) { diff --git a/LaunchDarkly/LaunchDarkly/Service Objects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/Service Objects/FlagSynchronizer.swift index c17ad79b..e9f4d6f4 100644 --- a/LaunchDarkly/LaunchDarkly/Service Objects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/Service Objects/FlagSynchronizer.swift @@ -68,6 +68,7 @@ extension DarklyEventSource.LDEvent { class FlagSynchronizer: LDFlagSynchronizing { struct Constants { fileprivate static let queueName = "LaunchDarkly.FlagSynchronizer.syncQueue" + static let didCloseEventSourceName = "didCloseEventSource" } let service: DarklyServiceProvider @@ -142,10 +143,22 @@ class FlagSynchronizer: LDFlagSynchronizing { Log.debug(typeName(and: #function) + "aborted. " + reason) return } + Log.debug(typeName(and: #function)) eventSource = service.createEventSource(useReport: useReport) //The LDConfig.connectionTimeout should NOT be set here. Heartbeat is sent every 3m. ES default timeout is 5m. This is an async operation. //LDEventSource reacts to connection errors by closing the connection and establishing a new one after an exponentially increasing wait. That makes it self healing. //While we could keep the LDEventSource state, there's not much we can do to help it connect. If it can't connect, it's likely we won't be able to poll the server either...so it seems best to just do nothing and let it heal itself. + eventSource?.onReadyStateChangedEvent { [self] (event) in + guard let event = event + else { + Log.debug(self.typeName(and: #function) + "onReadyStateChangedEvent handler aborted. No streaming event.") + return + } + if event.readyState == DarklyEventSource.kEventStateClosed { + Log.debug(self.typeName(and: #function) + "EventSource closed") + NotificationCenter.default.post(name: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil) + } + } eventSource?.onMessageEvent { [weak self] (event) in self?.process(event) } diff --git a/LaunchDarkly/LaunchDarkly/Service Objects/NetworkReporter.swift b/LaunchDarkly/LaunchDarkly/Service Objects/NetworkReporter.swift new file mode 100644 index 00000000..9f2e3f6d --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Service Objects/NetworkReporter.swift @@ -0,0 +1,44 @@ +// +// File.swift +// LaunchDarkly +// +// Created by Joe Cieslik on 8/27/19. +// Copyright © 2019 Catamorphic Co. All rights reserved. +// + +import Foundation +#if canImport(SystemConfiguration) +import SystemConfiguration +#endif + +class NetworkReporter { + #if canImport(SystemConfiguration) + //Sourced from: https://stackoverflow.com/a/39782859 + static func isConnectedToNetwork() -> Bool { + var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + } + + var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) + if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { + return false + } + + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + let reachability = (isReachable && !needsConnection) + + return reachability + } + #else + static func isConnectedToNetwork() -> Bool { + return true + } + #endif +} diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index fd76cfe0..f11d3eed 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -102,9 +102,16 @@ final class LDClientSpec: QuickSpec { var flagsUnchangedObserver: FlagsUnchangedObserver? { return changeNotifierMock?.addFlagsUnchangedObserverReceivedObserver } + var connectionModeChangedCallCount = 0 + var connectionModeChangedObserver: ConnectionModeChangedObserver? { + return changeNotifierMock?.addConnectionModeChangedObserverReceivedObserver + } var flagsUnchangedHandler: LDFlagsUnchangedHandler? { return flagsUnchangedObserver?.flagsUnchangedHandler } + var connectionModeChangedHandler: LDConnectionModeChangedHandler? { + return connectionModeChangedObserver?.connectionModeChangedHandler + } var errorObserver: ErrorObserver? { return errorNotifierMock.addErrorObserverReceivedObserver } @@ -206,6 +213,7 @@ final class LDClientSpec: QuickSpec { streamingModeSpec() reportEventsSpec() allFlagValuesSpec() + connectionInformationSpec() } private func startSpec() { @@ -1045,6 +1053,175 @@ final class LDClientSpec: QuickSpec { } } } + + describe("set user with identify") { + var newUser: LDUser! + beforeEach { + testContext = TestContext() + } + context("when the client is online") { + beforeEach { + testContext.config.startOnline = true + testContext.subject.start(config: testContext.config, user: testContext.user) + testContext.eventReporterMock.recordSummaryEventCallCount = 0 //calling start sets the user, which calls eventReporter.recordSummaryEvent() + testContext.featureFlagCachingMock.reset() + testContext.cacheConvertingMock.reset() + + newUser = LDUser.stub() + testContext.subject.identify(user: newUser) + } + it("changes to the new user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("leaves the client online") { + expect(testContext.subject.isOnline) == true + expect(testContext.subject.eventReporter.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == true + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records identify and summary events") { + expect(testContext.eventReporterMock.recordSummaryEventCallCount) == 1 + expect(testContext.eventReporterMock.recordReceivedArguments?.event.kind == .identify).to(beTrue()) + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when the client is offline") { + beforeEach { + testContext.config.startOnline = false + testContext.subject.start(config: testContext.config, user: testContext.user) + testContext.eventReporterMock.recordSummaryEventCallCount = 0 //calling start sets the user, which calls eventReporter.recordSummaryEvent() + testContext.featureFlagCachingMock.reset() + testContext.cacheConvertingMock.reset() + + newUser = LDUser.stub() + testContext.subject.identify(user: newUser) + } + it("changes to the new user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("leaves the client offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.eventReporter.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == false + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records identify and summary events") { + expect(testContext.eventReporterMock.recordSummaryEventCallCount) == 1 + expect(testContext.eventReporterMock.recordReceivedArguments?.event.kind == .identify).to(beTrue()) + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when the client is not started") { + beforeEach { + newUser = LDUser.stub() + testContext.subject.identify(user: newUser) + } + it("changes to the new user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("leaves the client offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.eventReporter.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == false + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("does not record any event") { + expect(testContext.eventReporterMock.recordSummaryEventCallCount) == 0 + expect(testContext.eventReporterMock.recordCallCount) == 0 + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when the new user has cached feature flags") { + beforeEach { + testContext.config.startOnline = false //offline makes no request to update flags... + testContext.subject.start(config: testContext.config, user: testContext.user) + testContext.eventReporterMock.recordSummaryEventCallCount = 0 //calling start sets the user, which calls eventReporter.recordSummaryEvent() + testContext.featureFlagCachingMock.reset() + newUser = LDUser.stub() + testContext.featureFlagCachingMock.retrieveFeatureFlagsReturnValue = newUser.featureFlags + testContext.cacheConvertingMock.reset() + testContext.subject.identify(user: newUser) + } + it("restores the cached users feature flags") { + expect(testContext.subject.user) == newUser + expect(newUser.flagStoreMock.replaceStoreCallCount) == 1 + expect(newUser.flagStoreMock.replaceStoreReceivedArguments?.newFlags?.flagCollection) == newUser.featureFlags + expect(newUser.flagStoreMock.replaceStoreReceivedArguments?.source) == .cache + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when the client is starting") { + beforeEach { + testContext.subject.setIsStarting(true) + newUser = LDUser.stub() + testContext.subject.identify(user: newUser) + } + it("changes to the new user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceMock.clearFlagResponseCacheCallCount) == 1 + expect(testContext.makeFlagSynchronizerService?.user) == newUser + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("leaves the client offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.eventReporter.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == false + } + it("does not record any event") { + expect(testContext.eventReporterMock.recordSummaryEventCallCount) == 0 + expect(testContext.eventReporterMock.recordCallCount) == 0 + } + it("does not convert cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 0 + } + } + } } private func setOnlineSpec() { @@ -1806,6 +1983,25 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagsUnchangedCallCount) == 1 } } + + describe("observeConnectionModeChanged") { + var testContext: TestContext! + beforeEach { + testContext = TestContext() + testContext.subject.start(config: testContext.config, user: testContext.user) + + testContext.subject.observeCurrentConnectionMode(owner: self, handler: {_ in + testContext.connectionModeChangedCallCount += 1 + }) + } + it("registers a ConnectionModeChanged observer") { + expect(testContext.changeNotifierMock.addConnectionModeChangedObserverCallCount) == 1 + expect(testContext.connectionModeChangedObserver?.owner) === self + expect(testContext.connectionModeChangedHandler).toNot(beNil()) + testContext.connectionModeChangedHandler?(ConnectionInformation.ConnectionMode.offline) + expect(testContext.connectionModeChangedCallCount) == 1 + } + } describe("observeError") { var testContext: TestContext! @@ -2817,6 +3013,61 @@ final class LDClientSpec: QuickSpec { } } } + + private func connectionInformationSpec() { + var testContext: TestContext! + + describe("ConnectionInformation") { + context("when client was started in foreground") { + beforeEach { + testContext = TestContext(startOnline: true, runMode: .foreground) + testContext.config.streamingMode = .streaming + testContext.subject.start(config: testContext.config) + } + it("returns a ConnectionInformation object with currentConnectionMode.establishingStreamingConnection") { + expect(testContext.subject.isOnline) == true + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.establishingStreamingConnection)) + expect(testContext.subject.connectionInformation.lastConnectionFailureReason.description).to(equal("none")) + } + it("returns a String from toString") { + expect(testContext.subject.connectionInformation.description).to(beAKindOf(String.self)) + } + } + context("when client was started in background") { + beforeEach { + testContext = TestContext(startOnline: true, runMode: .background) + testContext.config.streamingMode = .streaming + testContext.subject.start(config: testContext.config) + } + it("returns a ConnectionInformation object with currentConnectionMode.offline") { + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) + } + it("returns a String from toString") { + expect(testContext.subject.connectionInformation.description).to(beAKindOf(String.self)) + } + } + context("when offline and client started") { + beforeEach { + testContext = TestContext(startOnline: false) + testContext.subject.start(config: testContext.config) + } + it("leaves the sdk offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.eventReporterMock.isOnline) == false + expect(testContext.flagSynchronizerMock.isOnline) == false + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) + } + } + context("when client was not started") { + beforeEach { + testContext = TestContext() + } + it("returns nil") { + expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) + } + } + } + } } extension FeatureFlagCachingMock { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index d7a9d681..312cf61e 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -120,4 +120,8 @@ final class ClientServiceMockFactory: ClientServiceCreating { func makeErrorNotifier() -> ErrorNotifying { return ErrorNotifyingMock() } + + func makeConnectionInformation() -> ConnectionInformation { + return ConnectionInformation(currentConnectionMode: .offline, lastConnectionFailureReason: .none) + } } diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 4160f9fa..f50fa897 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -1233,6 +1233,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = CF1408CF629C7361332E53B88F7BD30C; productRefGroup = B88838C69B8F356A80CFF112DD837451 /* Products */; diff --git a/README.md b/README.md index 494369a4..716e9ac1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ $ gem install cocoapods ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '4.1.2' + pod 'LaunchDarkly', '4.2.0' end ``` @@ -70,7 +70,7 @@ $ brew install carthage To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" "4.1.2" +github "launchdarkly/ios-client-sdk" "4.2.0" ``` Run `carthage update` to build the framework. Optionally, specify the `--platform` to build only the frameworks that support your platform(s).