diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 4598e2c9..d4021a7e 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -6,17 +6,18 @@ import os // swiftlint:disable:next type_body_length public class Confidence: ConfidenceEventSender { private let clientSecret: String - private var region: ConfidenceRegion - private let parent: ConfidenceContextProvider? + private let region: ConfidenceRegion + private let debugLogger: DebugLogger? 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 let parentContextProvider: ConfidenceContextProvider? + private let contextManager: ContextManager private var cache = FlagResolution.EMPTY - private var storage: Storage + 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 +41,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 +72,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 +80,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( @@ -146,7 +137,7 @@ public class Confidence: ConfidenceEventSender { return getEvaluation(key: key, defaultValue: defaultValue).value } - func isStorageEmpty() -> Bool { + public func isStorageEmpty() -> Bool { return storage.isEmpty() } @@ -158,51 +149,9 @@ public class Confidence: ConfidenceEventSender { ) } - 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 +159,25 @@ 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 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 + let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys) debugLogger?.logContext( action: "PutContextLocal", - context: context) + context: newContext) } } @@ -247,16 +186,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)", @@ -270,19 +205,12 @@ public class Confidence: ConfidenceEventSender { 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 + let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys) do { try await self.fetchAndActivate() debugLogger?.logContext( - action: "PutContext - Done with FetchAndActivate", - context: context) + action: "PutContext", + context: newContext) } catch { debugLogger?.logMessage( message: "Error when putting context: \(error)", @@ -296,15 +224,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 +252,43 @@ 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 flush() { + eventSenderEngine.flush() + } + + private func withSemaphoreAsync(callback: @escaping () async -> Void) async { await withCheckedContinuation { continuation in DispatchQueue.global().async { self.semaphore.wait() @@ -338,13 +299,65 @@ 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 + } + } + + func getLocalContext() -> ConfidenceStruct { + contextQueue.sync { [weak self] in + guard let self = self else { + return [:] + } + return context + } + } +} + extension Confidence { public class Builder { // Must be configured or configured automatically 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 {