From edb28bc81d87a73c48ae533b9c05470d4909de9e Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Tue, 7 Nov 2023 15:10:16 +0000 Subject: [PATCH 1/3] Added isEmpty func to Storage --- .../Cache/DefaultStorage.swift | 52 ++++++++++++------- .../ConfidenceProvider/Cache/Storage.swift | 2 + .../ConfidenceFeatureProvider.swift | 4 ++ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift index dd87d27a..25ed5f0c 100644 --- a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift +++ b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift @@ -28,25 +28,14 @@ public class DefaultStorage: Storage { } public func load(defaultValue: T) throws -> T where T: Decodable { - try storageQueue.sync { - let configUrl = try getConfigUrl() - guard FileManager.default.fileExists(atPath: configUrl.backport.path) else { - return defaultValue - } - - let data = try { - do { - return try Data(contentsOf: configUrl) - } catch { - throw ConfidenceError.cacheError(message: "Unable to load cache file: \(error)") - } - }() + guard let data = try read() else { + return defaultValue + } - do { - return try JSONDecoder().decode(T.self, from: data) - } catch { - throw ConfidenceError.corruptedCache(message: "Unable to decode: \(error)") - } + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw ConfidenceError.corruptedCache(message: "Unable to decode: \(error)") } } @@ -65,6 +54,33 @@ public class DefaultStorage: Storage { } } + public func isEmpty() -> Bool { + guard let data = try? read() else { + return false + } + + return data.isEmpty + } + + func read() throws -> Data? { + try storageQueue.sync { + let configUrl = try getConfigUrl() + guard FileManager.default.fileExists(atPath: configUrl.backport.path) else { + return nil + } + + let data = try { + do { + return try Data(contentsOf: configUrl) + } catch { + throw ConfidenceError.cacheError(message: "Unable to load cache file: \(error)") + } + }() + + return data + } + } + func getConfigUrl() throws -> URL { guard let applicationSupportUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) diff --git a/Sources/ConfidenceProvider/Cache/Storage.swift b/Sources/ConfidenceProvider/Cache/Storage.swift index 6b1b99f4..d1133a22 100644 --- a/Sources/ConfidenceProvider/Cache/Storage.swift +++ b/Sources/ConfidenceProvider/Cache/Storage.swift @@ -6,4 +6,6 @@ public protocol Storage { func load(defaultValue: T) throws -> T where T: Decodable func clear() throws + + func isEmpty() -> Bool } diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 74bb31e1..39c183dd 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -89,6 +89,10 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } + public func isStorageEmpty() -> Bool { + storage.isEmpty() + } + public func onContextSet( oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext From 359d1d8262270266b9740a220a6a08037873d1dd Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Tue, 7 Nov 2023 16:37:51 +0000 Subject: [PATCH 2/3] Updated storage fake, fixed some warnings --- .../CacheDataInteractorTests.swift | 4 +-- .../CacheDataTests.swift | 20 ++++++------- .../Helpers/CacheDataUtility.swift | 2 +- .../Helpers/StorageMock.swift | 28 +++++++++++++------ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Tests/ConfidenceProviderTests/CacheDataInteractorTests.swift b/Tests/ConfidenceProviderTests/CacheDataInteractorTests.swift index ab6a1249..30ba8ace 100644 --- a/Tests/ConfidenceProviderTests/CacheDataInteractorTests.swift +++ b/Tests/ConfidenceProviderTests/CacheDataInteractorTests.swift @@ -34,7 +34,7 @@ final class CacheDataInteractorTests: XCTestCase { Task { // When cache data add method is called - await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date()) + _ = await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date()) // Then event is added with let cache = await cacheDataInteractor.cache @@ -49,7 +49,7 @@ final class CacheDataInteractorTests: XCTestCase { Task { // When cache data add method is called - await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date()) + _ = await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date()) // Then event is added with let cache = await cacheDataInteractor.cache diff --git a/Tests/ConfidenceProviderTests/CacheDataTests.swift b/Tests/ConfidenceProviderTests/CacheDataTests.swift index 34fb789a..38ef7789 100644 --- a/Tests/ConfidenceProviderTests/CacheDataTests.swift +++ b/Tests/ConfidenceProviderTests/CacheDataTests.swift @@ -11,7 +11,7 @@ final class CacheDataTests: XCTestCase { // When add event is called let applyTime = Date() - cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) + _ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) // Then cache data has one resolve event XCTAssertEqual(cacheData.resolveEvents.count, 1) @@ -29,7 +29,7 @@ final class CacheDataTests: XCTestCase { var cacheData = CacheData(resolveToken: "token1", events: []) // When add event is called with token that already exist in cache data - cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) + _ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) // Then cache data has one resolve event XCTAssertEqual(cacheData.resolveEvents.count, 1) @@ -46,9 +46,9 @@ final class CacheDataTests: XCTestCase { var cacheData = try CacheDataUtility.prefilledCacheData() // When add event is called 3 times with token that already exist in cache data - cacheData.add(resolveToken: "token0", flagName: "flagName", applyTime: Date()) - cacheData.add(resolveToken: "token0", flagName: "flagName2", applyTime: Date()) - cacheData.add(resolveToken: "token0", flagName: "flagName3", applyTime: Date()) + _ = cacheData.add(resolveToken: "token0", flagName: "flagName", applyTime: Date()) + _ = cacheData.add(resolveToken: "token0", flagName: "flagName2", applyTime: Date()) + _ = cacheData.add(resolveToken: "token0", flagName: "flagName3", applyTime: Date()) // Then cache data has 6 apply events XCTAssertEqual(cacheData.resolveEvents.first?.events.count, 6) @@ -58,11 +58,11 @@ final class CacheDataTests: XCTestCase { // Given pre filled cache data let applyTime = Date(timeIntervalSince1970: 1000) var cacheData = CacheData(resolveToken: "token1", events: []) - cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) + _ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime) // When add event is called with token and flagName that already exist in cache let applyTimeOther = Date(timeIntervalSince1970: 3000) - cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTimeOther) + _ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTimeOther) // Then apply record is not overriden let applyEvent = try XCTUnwrap(cacheData.resolveEvents.first?.events.first) @@ -75,9 +75,9 @@ final class CacheDataTests: XCTestCase { let date = Date(timeIntervalSince1970: 2000) // When add event is called 3 times with different tokens - cacheData.add(resolveToken: "token1", flagName: "prefilled", applyTime: date) - cacheData.add(resolveToken: "token2", flagName: "prefilled", applyTime: date) - cacheData.add(resolveToken: "token3", flagName: "prefilled", applyTime: date) + _ = cacheData.add(resolveToken: "token1", flagName: "prefilled", applyTime: date) + _ = cacheData.add(resolveToken: "token2", flagName: "prefilled", applyTime: date) + _ = cacheData.add(resolveToken: "token3", flagName: "prefilled", applyTime: date) // Then cache data has 4 resolve event XCTAssertEqual(cacheData.resolveEvents.count, 4) diff --git a/Tests/ConfidenceProviderTests/Helpers/CacheDataUtility.swift b/Tests/ConfidenceProviderTests/Helpers/CacheDataUtility.swift index 99aea167..025dee3a 100644 --- a/Tests/ConfidenceProviderTests/Helpers/CacheDataUtility.swift +++ b/Tests/ConfidenceProviderTests/Helpers/CacheDataUtility.swift @@ -26,7 +26,7 @@ enum CacheDataUtility { let applyEvent = FlagApply(name: flagName, applyTime: date) applyEvents.append(applyEvent) } else { - cacheData.add(resolveToken: resolveToken, flagName: flagName, applyTime: date) + _ = cacheData.add(resolveToken: resolveToken, flagName: flagName, applyTime: date) } } diff --git a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift index a883248f..bc98ec1f 100644 --- a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift +++ b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift @@ -6,8 +6,8 @@ import XCTest class StorageMock: Storage { var data = "" - var saveExpectation: XCTestExpectation? + private let storageQueue = DispatchQueue(label: "com.confidence.storagemock") convenience init(data: Encodable) throws { self.init() @@ -15,20 +15,32 @@ class StorageMock: Storage { } func save(data: Encodable) throws { - let dataB = try JSONEncoder().encode(data) - self.data = String(data: dataB, encoding: .utf8) ?? "" + try storageQueue.sync { + let dataB = try JSONEncoder().encode(data) + self.data = String(data: dataB, encoding: .utf8) ?? "" - saveExpectation?.fulfill() + saveExpectation?.fulfill() + } } func load(defaultValue: T) throws -> T where T: Decodable { - if data.isEmpty { - return defaultValue + try storageQueue.sync { + if data.isEmpty { + return defaultValue + } + return try JSONDecoder().decode(T.self, from: data.data) } - return try JSONDecoder().decode(T.self, from: data.data) } func clear() throws { - data = "" + storageQueue.sync { + data = "" + } + } + + func isEmpty() -> Bool { + storageQueue.sync { + return data.isEmpty + } } } From 3b368771f25e2bd1175d004248df9a919d6d3a1e Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Tue, 14 Nov 2023 12:01:57 +0000 Subject: [PATCH 3/3] static storage check, added to demo app and readme --- .../ConfidenceDemoApp/ConfidenceDemoApp.swift | 10 ++++++++- .../ConfidenceDemoApp/ContentView.swift | 2 +- README.md | 9 ++++++++ .../Cache/DefaultStorage.swift | 16 +++++++++++++- .../ConfidenceFeatureProvider.swift | 22 +++++++++++++------ 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index 3b3eb586..3bb4737f 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -19,10 +19,18 @@ extension ConfidenceDemoApp { guard let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] else { return } + + // If we have no cache, then do a fetch first. + var initializationStratgey: InitializationStrategy = .activateAndFetchAsync + if ConfidenceFeatureProvider.isStorageEmpty() { + initializationStratgey = .fetchAndActivate + } + let provider = ConfidenceFeatureProvider .Builder(credentials: .clientSecret(secret: secret)) - .with(initializationStrategy: .activateAndFetchAsync) + .with(initializationStrategy: initializationStratgey) .build() + // NOTE: Using a random UUID for each app start is not advised and can result in getting stale values. let ctx = MutableContext(targetingKey: UUID.init().uuidString, structure: MutableStructure()) OpenFeatureAPI.shared.setProvider(provider: provider, initialContext: ctx) } diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift index e4c263df..bced791a 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift @@ -18,7 +18,7 @@ struct ContentView: View { text.text = OpenFeatureAPI .shared .getClient() - .getStringValue(key: "hawkflag.color", defaultValue: "ERROR") + .getStringValue(key: "swift-demoapp.color", defaultValue: "ERROR") if text.text == "Green" { color.color = .green } else if text.text == "Yellow" { diff --git a/README.md b/README.md index 4eda7125..ffb198a6 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ The evaluation context is the way for the client to specify contextual data that The `setProvider()` function is synchronous and returns immediately, however this does not mean that the provider is ready to be used. An asynchronous network request to the Confidence backend to fetch all the flags configured for your application must be completed by the provider first. The provider will then emit a _READY_ event indicating you can start resolving flags. +A ultity function is available on the provider to check if the current storage has any values stored - this can be used to determine the best initialization strategy. +```swift +// If we have no cache, then do a fetch first. +var initializationStratgey: InitializationStrategy = .activateAndFetchAsync +if ConfidenceFeatureProvider.isStorageEmpty() { + initializationStratgey = .fetchAndActivate +} +``` + To listen for the _READY_ event, you can add an event handler via the `OpenFeatureAPI` shared instance: ```swift func providerReady(notification: Notification) { diff --git a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift index 25ed5f0c..d4942f73 100644 --- a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift +++ b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift @@ -56,7 +56,7 @@ public class DefaultStorage: Storage { public func isEmpty() -> Bool { guard let data = try? read() else { - return false + return true } return data.isEmpty @@ -97,3 +97,17 @@ public class DefaultStorage: Storage { components: resolverCacheBundleId, "\(bundleIdentifier)", filePath) } } + +extension DefaultStorage { + public static func resolverFlagsCache() -> DefaultStorage { + DefaultStorage(filePath: "resolver.flags.cache") + } + + public static func resolverApplyCache() -> DefaultStorage { + DefaultStorage(filePath: "resolver.apply.cache") + } + + public static func applierFlagsCache() -> DefaultStorage { + DefaultStorage(filePath: "applier.flags.cache") + } +} diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 39c183dd..43c3f313 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -89,10 +89,6 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } - public func isStorageEmpty() -> Bool { - storage.isEmpty() - } - public func onContextSet( oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext @@ -382,16 +378,28 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } +// MARK: Storage + +extension ConfidenceFeatureProvider { + public static func isStorageEmpty( + storage: Storage = DefaultStorage.resolverFlagsCache() + ) -> Bool { + storage.isEmpty() + } +} + +// MARK: Builder + extension ConfidenceFeatureProvider { public struct Builder { var options: ConfidenceClientOptions var session: URLSession? var localOverrides: [String: LocalOverride] = [:] - var storage: Storage = DefaultStorage(filePath: "resolver.flags.cache") + var storage: Storage = DefaultStorage.resolverFlagsCache() var cache: ProviderCache? var flagApplier: (any FlagApplier)? var initializationStrategy: InitializationStrategy = .fetchAndActivate - var applyStorage: Storage = DefaultStorage(filePath: "resolver.apply.cache") + var applyStorage: Storage = DefaultStorage.resolverApplyCache() /// Initializes the builder with the given credentails. /// @@ -565,7 +573,7 @@ extension ConfidenceFeatureProvider { flagApplier ?? FlagApplierWithRetries( httpClient: NetworkClient(region: options.region), - storage: DefaultStorage(filePath: "applier.flags.cache"), + storage: DefaultStorage.applierFlagsCache(), options: options )