diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 4598e2c9..96f040d5 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -3,20 +3,25 @@ import Foundation import Combine import os -// swiftlint:disable:next type_body_length public class Confidence: ConfidenceEventSender { + // User configurations private let clientSecret: String - private var region: ConfidenceRegion - private let parent: ConfidenceContextProvider? + private let region: ConfidenceRegion + private let debugLogger: DebugLogger? + + // Resources related to managing context and flags + private let parentContextProvider: ConfidenceContextProvider? + private let contextManager: ContextManager + private var cache = FlagResolution.EMPTY + + // Core components managing internal SDK functionality private let eventSenderEngine: EventSenderEngine - private var context: ConfidenceStruct = [:] - private var removedContextKeys: Set = Set() - private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache") + private let storage: Storage private let flagApplier: FlagApplier - private var cache = FlagResolution.EMPTY - private var storage: Storage + + // Synchronization and task management resources private var cancellables = Set() - private let debugLogger: DebugLogger? + private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache") private let semaphore = DispatchSemaphore(value: 1) // Internal for testing @@ -40,9 +45,8 @@ public class Confidence: ConfidenceEventSender { self.clientSecret = clientSecret self.region = region self.storage = storage - self.context = context - self.parent = parent - self.storage = storage + self.contextManager = ContextManager(initialContext: context) + self.parentContextProvider = parent self.flagApplier = flagApplier self.remoteFlagResolver = remoteFlagResolver self.debugLogger = debugLogger @@ -72,14 +76,7 @@ public class Confidence: ConfidenceEventSender { Fetching is best-effort, so no error is propagated. Errors can still be thrown if something goes wrong access data on disk. */ public func fetchAndActivate() async throws { - do { - try await internalFetch() - } catch { - debugLogger?.logMessage( - message: "\(error)", - isWarning: true - ) - } + await asyncFetch() try activate() } @@ -87,20 +84,18 @@ public class Confidence: ConfidenceEventSender { Fetch latest flag evaluations and store them on disk. Note that "activate" must be called for this data to be made available in the app session. */ - public func asyncFetch() { - Task { - do { - try await internalFetch() - } catch { - debugLogger?.logMessage( - message: "\(error )", - isWarning: true - ) - } + public func asyncFetch() async { + do { + try await internalFetch() + } catch { + debugLogger?.logMessage( + message: "\(error )", + isWarning: true + ) } } - func internalFetch() async throws { + private func internalFetch() async throws { let context = getContext() let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context) let resolution = FlagResolution( @@ -111,6 +106,13 @@ public class Confidence: ConfidenceEventSender { try storage.save(data: resolution) } + /** + Returns true if any flag is found in storage. + */ + public func isStorageEmpty() -> Bool { + return storage.isEmpty() + } + /** Get evaluation data for a specific flag. Evaluation data includes the variant's name and reason/error information. - Parameter key:expects dot-notation to retrieve a specific entry in the flag's value, e.g. "flagname.myentry" @@ -146,63 +148,9 @@ public class Confidence: ConfidenceEventSender { return getEvaluation(key: key, defaultValue: defaultValue).value } - func isStorageEmpty() -> Bool { - return storage.isEmpty() - } - - public func track(eventName: String, data: ConfidenceStruct) throws { - try eventSenderEngine.emit( - eventName: eventName, - data: data, - context: getContext() - ) - } - - public func track(producer: ConfidenceProducer) { - if let eventProducer = producer as? ConfidenceEventProducer { - eventProducer.produceEvents() - .sink { [weak self] event in - guard let self = self else { - return - } - do { - try self.track(eventName: event.name, data: event.data) - if event.shouldFlush { - eventSenderEngine.flush() - } - } catch { - Logger(subsystem: "com.confidence", category: "track").warning( - "Error from EventProducer, failed to track event: \(event.name)") - } - } - .store(in: &cancellables) - } - - if let contextProducer = producer as? ConfidenceContextProducer { - contextProducer.produceContexts() - .sink { [weak self] context in - Task { [weak self] in - guard let self = self else { return } - await self.putContext(context: context) - } - } - .store(in: &cancellables) - } - } - - public func flush() { - eventSenderEngine.flush() - } - public func getContext() -> ConfidenceStruct { - let parentContext = parent?.getContext() ?? [:] - var reconciledCtx = parentContext.filter { - !removedContextKeys.contains($0.key) - } - self.context.forEach { entry in - reconciledCtx.updateValue(entry.value, forKey: entry.key) - } - return reconciledCtx + let parentContext = parentContextProvider?.getContext() ?? [:] + return contextManager.getContext(parentContext: parentContext) } public func putContext(key: String, value: ConfidenceValue) async { @@ -210,35 +158,28 @@ public class Confidence: ConfidenceEventSender { guard let self = self else { return } - var map = self.context - map[key] = value - context = map + let newContext = contextManager.updateContext(withValues: [key: value], removedKeys: []) do { try await self.fetchAndActivate() - debugLogger?.logContext(action: "PutContext", context: context) + debugLogger?.logContext(action: "PutContext", context: newContext) } catch { debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) } } } - - public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { - withSemaphore { [weak self] in + public func putContext(context: ConfidenceStruct, removedKeys: [String] = []) async { + await withSemaphoreAsync { [weak self] in guard let self = self else { return } - var map = self.context - for removedKey in removedKeys { - map.removeValue(forKey: removedKey) - } - for entry in context { - map.updateValue(entry.value, forKey: entry.key) + let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys) + do { + try await self.fetchAndActivate() + debugLogger?.logContext(action: "PutContext", context: newContext) + } catch { + debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) } - self.context = map - debugLogger?.logContext( - action: "PutContextLocal", - context: context) } } @@ -247,16 +188,12 @@ public class Confidence: ConfidenceEventSender { guard let self = self else { return } - var map = self.context - for entry in context { - map.updateValue(entry.value, forKey: entry.key) - } - self.context = map + let newContext = contextManager.updateContext(withValues: context, removedKeys: []) do { try await fetchAndActivate() debugLogger?.logContext( - action: "PutContext - Done with FetchAndActivate", - context: context) + action: "PutContext", + context: newContext) } catch { debugLogger?.logMessage( message: "Error when putting context: \(error)", @@ -265,30 +202,14 @@ public class Confidence: ConfidenceEventSender { } } - public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) async { - await withSemaphoreAsync { [weak self] in - guard let self = self else { - return - } - var map = self.context - for removedKey in removedKeys { - map.removeValue(forKey: removedKey) - } - for entry in context { - map.updateValue(entry.value, forKey: entry.key) - } - self.context = map - do { - try await self.fetchAndActivate() - debugLogger?.logContext( - action: "PutContext - Done with FetchAndActivate", - context: context) - } catch { - debugLogger?.logMessage( - message: "Error when putting context: \(error)", - isWarning: true) - } - } + /** + Adds/override entry to local context data. Does not trigger fetchAndActivate after the context change. + */ + public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { + let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys) + debugLogger?.logContext( + action: "PutContextLocal", + context: newContext) } public func removeContext(key: String) async { @@ -296,15 +217,12 @@ public class Confidence: ConfidenceEventSender { guard let self = self else { return } - var map = self.context - map.removeValue(forKey: key) - context = map - removedContextKeys.insert(key) + let newContext = contextManager.updateContext(withValues: [:], removedKeys: [key]) do { try await self.fetchAndActivate() debugLogger?.logContext( - action: "RemoveContext - Done with FetchAndActivate", - context: context) + action: "RemoveContext", + context: newContext) } catch { debugLogger?.logMessage( message: "Error when removing context key: \(error)", @@ -327,7 +245,51 @@ public class Confidence: ConfidenceEventSender { ) } - func withSemaphoreAsync(callback: @escaping () async -> Void) async { + public func track(producer: ConfidenceProducer) { + if let eventProducer = producer as? ConfidenceEventProducer { + eventProducer.produceEvents() + .sink { [weak self] event in + guard let self = self else { + return + } + do { + try self.track(eventName: event.name, data: event.data) + if event.shouldFlush { + eventSenderEngine.flush() + } + } catch { + Logger(subsystem: "com.confidence", category: "track").warning( + "Error from EventProducer, failed to track event: \(event.name)") + } + } + .store(in: &cancellables) + } + + if let contextProducer = producer as? ConfidenceContextProducer { + contextProducer.produceContexts() + .sink { [weak self] context in + Task { [weak self] in + guard let self = self else { return } + await self.putContext(context: context) + } + } + .store(in: &cancellables) + } + } + + public func track(eventName: String, data: ConfidenceStruct) throws { + try eventSenderEngine.emit( + eventName: eventName, + data: data, + context: getContext() + ) + } + + public func flush() { + eventSenderEngine.flush() + } + + private func withSemaphoreAsync(callback: @escaping () async -> Void) async { await withCheckedContinuation { continuation in DispatchQueue.global().async { self.semaphore.wait() @@ -338,13 +300,56 @@ public class Confidence: ConfidenceEventSender { semaphore.signal() } - func withSemaphore(callback: @escaping () -> Void) { + private func withSemaphore(callback: @escaping () -> Void) { self.semaphore.wait() callback() semaphore.signal() } } +private class ContextManager { + private var context: ConfidenceStruct = [:] + private var removedContextKeys: Set = Set() + private let contextQueue = DispatchQueue(label: "com.confidence.queue.context") + + public init(initialContext: ConfidenceStruct) { + context = initialContext + } + + func updateContext(withValues: ConfidenceStruct, removedKeys: [String]) -> ConfidenceStruct { + contextQueue.sync { [weak self] in + guard let self = self else { + return [:] + } + var map = self.context + for removedKey in removedKeys { + map.removeValue(forKey: removedKey) + removedContextKeys.insert(removedKey) + } + for entry in withValues { + map.updateValue(entry.value, forKey: entry.key) + } + self.context = map + return self.context + } + } + + func getContext(parentContext: ConfidenceStruct) -> ConfidenceStruct { + contextQueue.sync { [weak self] in + guard let self = self else { + return [:] + } + var reconciledCtx = parentContext.filter { + !self.removedContextKeys.contains($0.key) + } + context.forEach { entry in + reconciledCtx.updateValue(entry.value, forKey: entry.key) + } + return reconciledCtx + } + } +} + extension Confidence { public class Builder { // Must be configured or configured automatically diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index f595a6e9..cc6a23d0 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -20,14 +20,25 @@ public protocol ConfidenceEventSender: ConfidenceContextProvider { func flush() /** Adds/override entry to local context data + Triggers fetchAndActivate after the context change */ func putContext(key: String, value: ConfidenceValue) async /** + Adds/override entry to local context data + Triggers fetchAndActivate after the context change + */ + func putContext(context: ConfidenceStruct) async + /** Removes entry from localcontext data It hides entries with this key from parents' data (without modifying parents' data) + Triggers fetchAndActivate after the context change */ func removeContext(key: String) async /** + Combination of putContext and removeContext + */ + func putContext(context: ConfidenceStruct, removedKeys: [String]) async + /** Creates a child event sender instance that maintains access to its parent's data */ func withContext(_ context: ConfidenceStruct) -> ConfidenceEventSender diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index e055e7ef..0adbec02 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -49,7 +49,9 @@ public class ConfidenceFeatureProvider: FeatureProvider { if initializationStrategy == .activateAndFetchAsync { try confidence.activate() eventHandler.send(.ready) - confidence.asyncFetch() + Task { + await confidence.asyncFetch() + } } else { Task { do { diff --git a/api/Confidence_public_api.json b/api/Confidence_public_api.json index 5bffb952..1403b9e0 100644 --- a/api/Confidence_public_api.json +++ b/api/Confidence_public_api.json @@ -12,7 +12,11 @@ }, { "name": "asyncFetch()", - "declaration": "public func asyncFetch()" + "declaration": "public func asyncFetch() async" + }, + { + "name": "isStorageEmpty()", + "declaration": "public func isStorageEmpty() -> Bool" }, { "name": "getEvaluation(key:defaultValue:)", @@ -22,18 +26,6 @@ "name": "getValue(key:defaultValue:)", "declaration": "public func getValue(key: String, defaultValue: T) -> T" }, - { - "name": "track(eventName:data:)", - "declaration": "public func track(eventName: String, data: ConfidenceStruct) throws" - }, - { - "name": "track(producer:)", - "declaration": "public func track(producer: ConfidenceProducer)" - }, - { - "name": "flush()", - "declaration": "public func flush()" - }, { "name": "getContext()", "declaration": "public func getContext() -> ConfidenceStruct" @@ -43,16 +35,16 @@ "declaration": "public func putContext(key: String, value: ConfidenceValue) async" }, { - "name": "putContextLocal(context:removeKeys:)", - "declaration": "public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = [])" + "name": "putContext(context:removedKeys:)", + "declaration": "public func putContext(context: ConfidenceStruct, removedKeys: [String] = []) async" }, { "name": "putContext(context:)", "declaration": "public func putContext(context: ConfidenceStruct) async" }, { - "name": "putContext(context:removeKeys:)", - "declaration": "public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) async" + "name": "putContextLocal(context:removeKeys:)", + "declaration": "public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = [])" }, { "name": "removeContext(key:)", @@ -61,6 +53,18 @@ { "name": "withContext(_:)", "declaration": "public func withContext(_ context: ConfidenceStruct) -> ConfidenceEventSender" + }, + { + "name": "track(producer:)", + "declaration": "public func track(producer: ConfidenceProducer)" + }, + { + "name": "track(eventName:data:)", + "declaration": "public func track(eventName: String, data: ConfidenceStruct) throws" + }, + { + "name": "flush()", + "declaration": "public func flush()" } ] },