diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 64aadbc4..782b46d7 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -3,6 +3,7 @@ import Foundation import Combine import os +// swiftlint:disable:next type_body_length public class Confidence: ConfidenceEventSender { private let clientSecret: String private var region: ConfidenceRegion @@ -48,28 +49,8 @@ public class Confidence: ConfidenceEventSender { self.remoteFlagResolver = remoteFlagResolver self.debugLogger = debugLogger if let visitorId { - putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) + putContextLocal(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) } - - contextChanges().sink { [weak self] context in - guard let self = self else { - return - } - self.currentFetchTask?.cancel() - self.currentFetchTask = Task { - do { - let context = self.getContext() - try await self.fetchAndActivate() - self.contextReconciliatedChanges.send(context.hash()) - } catch { - debugLogger?.logMessage( - message: "\(error)", - isWarning: true - ) - } - } - } - .store(in: &cancellables) } /** @@ -214,10 +195,10 @@ public class Confidence: ConfidenceEventSender { if let contextProducer = producer as? ConfidenceContextProducer { contextProducer.produceContexts() .sink { [weak self] context in - guard let self = self else { - return + Task { [weak self] in + guard let self = self else { return } + await self.putContext(context: context) } - self.putContext(context: context) } .store(in: &cancellables) } @@ -238,28 +219,59 @@ public class Confidence: ConfidenceEventSender { return reconciledCtx } - public func putContext(key: String, value: ConfidenceValue) { - withLock { confidence in + public func putContext(key: String, value: ConfidenceValue) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value map[key] = value confidence.contextSubject.value = map - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) + } } } - public func putContext(context: ConfidenceStruct) { + + public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { withLock { confidence in var map = confidence.contextSubject.value + for removedKey in removedKeys { + map.removeValue(forKey: removedKey) + } for entry in context { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + confidence.debugLogger?.logContext( + action: "PutContext", + context: confidence.contextSubject.value) } } - public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { - withLock { confidence in + public func putContext(context: ConfidenceStruct) async { + await withLockAsync { confidence in + var map = confidence.contextSubject.value + for entry in context { + map.updateValue(entry.value, forKey: entry.key) + } + confidence.contextSubject.value = map + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext( + action: "PutContext & FetchAndActivate", + context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage( + message: "Error when putting context: \(error)", + isWarning: true) + } + } + } + + public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value for removedKey in removedKeys { map.removeValue(forKey: removedKey) @@ -268,17 +280,35 @@ public class Confidence: ConfidenceEventSender { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext( + action: "PutContext & FetchAndActivate", + context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage( + message: "Error when putting context: \(error)", + isWarning: true) + } } } - public func removeKey(key: String) { - withLock { confidence in + public func removeContext(key: String) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value map.removeValue(forKey: key) confidence.contextSubject.value = map confidence.removedContextKeys.insert(key) - confidence.debugLogger?.logContext(action: "RemoveContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext( + action: "RemoveContextKey & FetchAndActivate", + context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage( + message: "Error when removing context key: \(error)", + isWarning: true) + } } } @@ -304,6 +334,21 @@ public class Confidence: ConfidenceEventSender { callback(self) } } + + private func withLockAsync(callback: @escaping (Confidence) async -> Void) async { + await withCheckedContinuation { continuation in + contextSubjectQueue.async { [weak self] in + guard let self = self else { + continuation.resume() + return + } + Task { + await callback(self) // Await the async closure + continuation.resume() + } + } + } + } } extension Confidence { diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index 18e7b829..f595a6e9 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -21,12 +21,12 @@ public protocol ConfidenceEventSender: ConfidenceContextProvider { /** Adds/override entry to local context data */ - func putContext(key: String, value: ConfidenceValue) + func putContext(key: String, value: ConfidenceValue) async /** Removes entry from localcontext data It hides entries with this key from parents' data (without modifying parents' data) */ - func removeKey(key: String) + func removeContext(key: String) async /** Creates a child event sender instance that maintains access to its parent's data */ diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 538518a3..e055e7ef 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -86,7 +86,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func updateConfidenceContext(context: EvaluationContext, removedKeys: [String] = []) { - confidence.putContext(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys) + confidence.putContextLocal(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys) } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws diff --git a/Tests/ConfidenceTests/ConfidenceContextTests.swift b/Tests/ConfidenceTests/ConfidenceContextTests.swift index 155c952d..9e087b39 100644 --- a/Tests/ConfidenceTests/ConfidenceContextTests.swift +++ b/Tests/ConfidenceTests/ConfidenceContextTests.swift @@ -30,7 +30,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testWithContextUpdateParent() { + func testWithContextUpdateParent() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -51,7 +51,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceParent.putContext( + await confidenceParent.putContext( key: "k3", value: ConfidenceValue(string: "v3")) let expected = [ @@ -62,7 +62,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testUpdateLocalContext() { + func testUpdateLocalContext() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -80,7 +80,7 @@ final class ConfidenceContextTests: XCTestCase { parent: nil, debugLogger: nil ) - confidence.putContext( + await confidence.putContext( key: "k1", value: ConfidenceValue(string: "v3")) let expected = [ @@ -89,7 +89,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidence.getContext(), expected) } - func testUpdateLocalContextWithoutOverride() { + func testUpdateLocalContextWithoutOverride() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -110,7 +110,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceChild.putContext( + await confidenceChild.putContext( key: "k2", value: ConfidenceValue(string: "v4")) let expected = [ @@ -120,7 +120,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testUpdateParentContextWithOverride() { + func testUpdateParentContextWithOverride() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -141,7 +141,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceParent.putContext( + await confidenceParent.putContext( key: "k2", value: ConfidenceValue(string: "v4")) let expected = [ @@ -235,7 +235,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testRemoveContextEntryFromParentAndChildThenUpdate() { + func testRemoveContextEntryFromParentAndChildThenUpdate() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -260,7 +260,7 @@ final class ConfidenceContextTests: XCTestCase { ] ) confidenceChild.removeKey(key: "k1") - confidenceChild.putContext(key: "k1", value: ConfidenceValue(string: "v4")) + await confidenceChild.putContext(key: "k1", value: ConfidenceValue(string: "v4")) let expected = [ "k2": ConfidenceValue(string: "v2"), "k1": ConfidenceValue(string: "v4"), diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 51dc65d5..09adda67 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -98,9 +98,9 @@ class ConfidenceTest: XCTestCase { // Initialize allows to start listening for context changes in "confidence" // Let the internal "resolve" finish await fulfillment(of: [resolve1Completed], timeout: 5.0) - confidence.putContext(key: "new", value: ConfidenceValue(string: "value")) + await confidence.putContext(key: "new", value: ConfidenceValue(string: "value")) await fulfillment(of: [resolve2Started], timeout: 5.0) // Ensure resolve 2 starts before 3 - confidence.putContext(key: "new2", value: ConfidenceValue(string: "value2")) + await confidence.putContext(key: "new2", value: ConfidenceValue(string: "value2")) await fulfillment(of: [resolve3Completed], timeout: 5.0) resolve2Continues.fulfill() // Allow second resolve to continue, regardless if cancelled or not await fulfillment(of: [resolve2Cancelled], timeout: 5.0) // Second resolve is cancelled @@ -142,14 +142,8 @@ class ConfidenceTest: XCTestCase { resolveReason: .match) ] - let expectation = expectation(description: "context is synced") - let cancellable = confidence.contextReconciliatedChanges.sink { _ in - expectation.fulfill() - } - confidence.putContext(context: ["targeting_key": .init(string: "user2")]) - await fulfillment(of: [expectation], timeout: 1) - cancellable.cancel() + await confidence.putContext(context: ["targeting_key": .init(string: "user2")]) let evaluation = confidence.getEvaluation( key: "flag.size", defaultValue: 0) @@ -400,7 +394,7 @@ class ConfidenceTest: XCTestCase { .build() try await confidence.fetchAndActivate() - confidence.putContext(context: ["hello": .init(string: "world")]) + await confidence.putContext(context: ["hello": .init(string: "world")]) let evaluation = confidence.getEvaluation( key: "flag.size", defaultValue: 0)