diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index de5508c..793db0b 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -4,36 +4,48 @@ import Foundation /// A global singleton which holds base configuration for the OpenFeature library. /// Configuration here will be shared across all ``Client``s. public class OpenFeatureAPI { - private var stateManager = SafeStateManager() - private let eventHandler = EventHandler() + private var stateManager: SafeStateManager + private let eventHandler: EventHandler private(set) var hooks: [any Hook] = [] /// The ``OpenFeatureAPI`` singleton static public let shared = OpenFeatureAPI() public init() { + self.eventHandler = EventHandler() + self.stateManager = SafeStateManager(eventHandler: eventHandler) } + /** + Set provider and calls its `initialize` in a background thread. + Readiness can be determined from `getState` or listening for `ready` event. + */ + public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) { + stateManager.setProviderAsync(provider: provider, initialContext: initialContext) + } + + /** + Set provider and calls its `initialize`. + This async function returns when the initalize from the provider is completed. + */ + public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async { + await stateManager.setProviderBlocking(provider: provider, initialContext: initialContext) + } + + /** + Set provider and calls its `initialize` in a background thread. + Readiness can be determined from `getState` or listening for `ready` event. + */ public func setProvider(provider: FeatureProvider) { - self.setProvider(provider: provider, initialContext: nil) + setProvider(provider: provider, initialContext: nil) } - public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) { - stateManager.setProvider(provider: provider, initialContext: initialContext) - do { - try provider.initialize(initialContext: initialContext) - stateManager.update(providerStatus: .ready) - eventHandler.send(.ready) - } catch { - switch error { - case OpenFeatureError.providerFatalError: - stateManager.update(providerStatus: .fatal) - eventHandler.send(.error(errorCode: .providerFatal)) - default: - stateManager.update(providerStatus: .error) - eventHandler.send(.error(message: error.localizedDescription)) - } - } + /** + Set provider and calls its `initialize`. + This async function returns when the initalize from the provider is completed. + */ + public func setProviderAndWait(provider: FeatureProvider) async { + await setProviderAndWait(provider: provider, initialContext: nil) } public func getProvider() -> FeatureProvider? { @@ -45,17 +57,7 @@ public class OpenFeatureAPI { } public func setEvaluationContext(evaluationContext: EvaluationContext) { - do { - let oldContext = self._context - stateManager.update(evaluationContext: evaluationContext, providerStatus: .reconciling) - eventHandler.send(.reconciling) - try getProvider()?.onContextSet(oldContext: oldContext, newContext: evaluationContext) - stateManager.update(providerStatus: .ready) - eventHandler.send(.contextChanged) - } catch { - stateManager.update(providerStatus: .error) - eventHandler.send(.error(message: error.localizedDescription)) - } + stateManager.update(evaluationContext: evaluationContext) } public func getEvaluationContext() -> EvaluationContext? { @@ -86,7 +88,7 @@ public class OpenFeatureAPI { self.hooks.removeAll() } - public func getState() -> ( + internal func getState() -> ( provider: FeatureProvider?, evaluationContext: EvaluationContext?, providerStatus: ProviderStatus ) { return self.stateManager.getState() @@ -124,63 +126,88 @@ extension OpenFeatureAPI { } } -extension OpenFeatureAPI { - public func setProviderAndWait(provider: FeatureProvider) async { - await setProviderAndWait(provider: provider, initialContext: nil) - } - - public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async { - let task = Task { - var holder: [AnyCancellable] = [] - await withCheckedContinuation { continuation in - setProvider(provider: provider, initialContext: initialContext) - continuation.resume() - holder.removeAll() - } - } - await withTaskCancellationHandler { - await task.value - } onCancel: { - task.cancel() - } - } -} - /// This helper struct maintains the provider, its state and the global evaluation context /// It is designed to be thread safe on write: context and status are updated atomically, for example. /// The allowed bulk-changes are also executed in a serial fashion to guarantee thread-safety. -struct SafeStateManager { +class SafeStateManager { private let queue = DispatchQueue(label: "com.providerDescriptor.queue") private(set) var provider: FeatureProvider? - private(set) var providerSubject = CurrentValueSubject(nil) - private(set) var evaluationContext: EvaluationContext? = nil - private(set) var providerStatus: ProviderStatus = .notReady + private(set) var providerSubject: CurrentValueSubject<(any FeatureProvider)?, Never> + private(set) var evaluationContext: EvaluationContext? + private(set) var providerStatus: ProviderStatus - mutating func setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = nil) { - queue.sync { - self.provider = provider - self.providerStatus = .notReady - if let initialContext = initialContext { - self.evaluationContext = initialContext - } - providerSubject.send(provider) + private let eventHandler: EventHandler + + + init(provider: FeatureProvider? = nil, + providerSubject: CurrentValueSubject<(any FeatureProvider)?, Never> = CurrentValueSubject(nil), + evaluationContext: EvaluationContext? = nil, + providerStatus: ProviderStatus = .notReady, + eventHandler: EventHandler) + { + self.provider = provider + self.providerSubject = providerSubject + self.evaluationContext = evaluationContext + self.providerStatus = providerStatus + self.eventHandler = eventHandler + } + + func setProviderAsync(provider: FeatureProvider, initialContext: EvaluationContext? = nil) { + queue.async { + self.setProviderInternal(provider: provider, initialContext: initialContext) } } - mutating func update(evaluationContext: EvaluationContext? = nil, providerStatus: ProviderStatus? = nil) { + func setProviderBlocking(provider: FeatureProvider, initialContext: EvaluationContext? = nil) async { queue.sync { - if let newContext = evaluationContext { - self.evaluationContext = newContext + self.setProviderInternal(provider: provider, initialContext: initialContext) + } + } + + private func setProviderInternal(provider: FeatureProvider, initialContext: EvaluationContext? = nil) { + self.provider = provider + self.providerStatus = .notReady + self.providerSubject.send(provider) + + if let initialContext = initialContext { + self.evaluationContext = initialContext + } + + do { + try provider.initialize(initialContext: initialContext) + self.providerStatus = .ready + self.eventHandler.send(.ready) + } catch { + switch error { + case OpenFeatureError.providerFatalError: + self.providerStatus = .fatal + self.eventHandler.send(.error(errorCode: .providerFatal)) + default: + self.providerStatus = .error + self.eventHandler.send(.error(message: error.localizedDescription)) } + } + } - if let newStatus = providerStatus { - self.providerStatus = newStatus + func update(evaluationContext: EvaluationContext) { + queue.sync { + do { + let oldContext = self.evaluationContext + self.evaluationContext = evaluationContext + self.providerStatus = .reconciling + eventHandler.send(.reconciling) + try self.provider?.onContextSet(oldContext: oldContext, newContext: evaluationContext) + self.providerStatus = .ready + eventHandler.send(.contextChanged) + } catch { + self.providerStatus = .error + eventHandler.send(.error(message: error.localizedDescription)) } } } - mutating func clearProvider() { + func clearProvider() { queue.sync { self.provider = nil self.providerSubject.send(nil) diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift index feddd88..fe59d2f 100644 --- a/Sources/OpenFeature/OpenFeatureClient.swift +++ b/Sources/OpenFeature/OpenFeatureClient.swift @@ -73,7 +73,9 @@ extension OpenFeatureClient { switch openFeatureApiState.providerStatus { case .fatal: details.errorCode = .providerFatal - details.errorMessage = OpenFeatureError.providerFatalError(message: "unknown").description // TODO Improve this message with error details + details.errorMessage = OpenFeatureError + .providerFatalError(message: "unknown") + .description // TODO Improve this message with error details details.reason = Reason.error.rawValue return details case .notReady: @@ -86,7 +88,9 @@ extension OpenFeatureClient { return details case .error: details.errorCode = .general - details.errorMessage = OpenFeatureError.generalError(message: "unknown").description // TODO Improve this message with error details + details.errorMessage = OpenFeatureError + .generalError(message: "unknown") + .description // TODO Improve this message with error details details.reason = Reason.error.rawValue return details case .ready: diff --git a/Sources/OpenFeature/Provider/FeatureProvider.swift b/Sources/OpenFeature/Provider/FeatureProvider.swift index 7af4d04..923d8b4 100644 --- a/Sources/OpenFeature/Provider/FeatureProvider.swift +++ b/Sources/OpenFeature/Provider/FeatureProvider.swift @@ -7,7 +7,7 @@ public protocol FeatureProvider: EventPublisher { /// Called by OpenFeatureAPI whenever the new Provider is registered /// This must throw in case of error, using OpenFeature errors whenever possible - /// It is expected that the implementer is slow and blocking (e.g. network), the caller must deal with wrapping this into a background Task if necessary + /// It is expected that the implementer is slow and blocking (e.g. network) func initialize(initialContext: EvaluationContext?) throws /// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index bd7971b..6cccef5 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -84,7 +84,6 @@ final class DeveloperExperienceTests: XCTestCase { } } ) { - let initCompleteExpectation = XCTestExpectation() let provider = DoSomethingProvider() diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift index 08fff04..9f78812 100644 --- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift +++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift @@ -9,15 +9,14 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertTrue(OpenFeatureAPI.shared === OpenFeatureAPI.shared) } - func testApiSetsProvider() { + func testApiSetsProvider() async { let provider = NoOpProvider() - OpenFeatureAPI.shared.setProvider(provider: provider) + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider) XCTAssertTrue((OpenFeatureAPI.shared.getProvider() as? NoOpProvider) === provider) } - func testProviderMetadata() { - OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) - + func testProviderMetadata() async { + await OpenFeatureAPI.shared.setProviderAndWait(provider: DoSomethingProvider()) XCTAssertEqual(OpenFeatureAPI.shared.getProviderMetadata()?.name, DoSomethingProvider.name) }