Skip to content

Commit dd122f7

Browse files
refactor!: Add EventHandler + Combine (#29)
## This PR - Removes event APIs at the client level (will be re-introduced when multiple clients/providers are supported) - Adds an EventHandler implementation that Providers can use to emit events (a `FeatureProvider` now extends the `EventPublisher` protocol) - Adds **Combine** as the API for eventing ## Notes Following patterns implemented in the [Kotlin SDK](https://github.com/open-feature/kotlin-sdk/blob/cce8368f090b335b7b29c66479aaebb44c58ca5b/android/src/main/java/dev/openfeature/sdk/events/EventHandler.kt) as much as possible, for consistency ### Follow-up Tasks - Add `setProviderAndWait` function in OpenFeatureAPI - Events should contain more metadata than just their type - Introduce ProviderStatus as an entity separated from ProviderEvent (also for Kotlin) --------- Signed-off-by: Fabrizio Demaria <[email protected]> Signed-off-by: vahid torkaman <[email protected]> Co-authored-by: vahid torkaman <[email protected]>
1 parent dc5876c commit dd122f7

15 files changed

+252
-226
lines changed

README.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ let client = OpenFeatureAPI.shared.getClient()
9191
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
9292
```
9393

94-
Setting a new provider or setting a new evaluation context might trigger asynchronous operations (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `ProviderReady` event has been emitted (see [Eventing](#eventing) below).
94+
Setting a new provider or setting a new evaluation context might trigger asynchronous operations (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `ProviderReady` event has been sent (see [Eventing](#eventing) below).
9595

9696
## 🌟 Features
9797

@@ -174,12 +174,13 @@ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGE
174174
Please refer to the documentation of the provider you're using to see what events are supported.
175175

176176
```swift
177-
OpenFeatureAPI.shared.addHandler(
178-
observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready
179-
)
180-
181-
func readyEventEmitted(notification: NSNotification) {
182-
// to something now that the provider is ready
177+
let cancellable = OpenFeatureAPI.shared.observe().sink { event in
178+
switch event {
179+
case ProviderEvent.ready:
180+
// ...
181+
default:
182+
// ...
183+
}
183184
}
184185
```
185186

Sources/OpenFeature/Client.swift

-11
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,4 @@ public protocol Client: Features {
1111
/// Hooks are run in the order they're added in the before stage. They are run in reverse order for all
1212
/// other stages.
1313
func addHooks(_ hooks: any Hook...)
14-
15-
/// Add a handler for a particular provider event
16-
/// - Parameter observer: The object observing the event.
17-
/// - Parameter selector: The selector to call for this event.
18-
/// - Parameter event: The event to listen for.
19-
func addHandler(observer: Any, selector: Selector, event: ProviderEvent)
20-
21-
/// Remove a handler for a particular provider event
22-
/// - Parameter observer: The object observing the event.
23-
/// - Parameter event: The event being listened to.
24-
func removeHandler(observer: Any, event: ProviderEvent)
2514
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
import Combine
3+
4+
public class EventHandler: EventSender, EventPublisher {
5+
private let eventState: CurrentValueSubject<ProviderEvent, Never>
6+
7+
public init(_ state: ProviderEvent) {
8+
eventState = CurrentValueSubject<ProviderEvent, Never>(ProviderEvent.stale)
9+
}
10+
11+
public func observe() -> AnyPublisher<ProviderEvent, Never> {
12+
return eventState.eraseToAnyPublisher()
13+
}
14+
15+
public func send(
16+
_ event: ProviderEvent
17+
) {
18+
eventState.send(event)
19+
}
20+
}
21+
22+
public protocol EventPublisher {
23+
func observe() -> AnyPublisher<ProviderEvent, Never>
24+
}
25+
26+
public protocol EventSender {
27+
func send(_ event: ProviderEvent)
28+
}

Sources/OpenFeature/OpenFeatureAPI.swift

+20-38
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import Foundation
2+
import Combine
23

34
/// A global singleton which holds base configuration for the OpenFeature library.
45
/// Configuration here will be shared across all ``Client``s.
56
public class OpenFeatureAPI {
6-
private var _provider: FeatureProvider?
7+
private var _provider: FeatureProvider? {
8+
get {
9+
providerSubject.value
10+
}
11+
set {
12+
providerSubject.send(newValue)
13+
}
14+
}
715
private var _context: EvaluationContext?
816
private(set) var hooks: [any Hook] = []
9-
10-
private let providerNotificationCentre = NotificationCenter()
17+
private var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)
1118

1219
/// The ``OpenFeatureAPI`` singleton
1320
static public let shared = OpenFeatureAPI()
@@ -24,7 +31,6 @@ public class OpenFeatureAPI {
2431
if let context = initialContext {
2532
self._context = context
2633
}
27-
2834
provider.initialize(initialContext: self._context)
2935
}
3036

@@ -65,41 +71,17 @@ public class OpenFeatureAPI {
6571
public func clearHooks() {
6672
self.hooks.removeAll()
6773
}
68-
}
6974

70-
// MARK: Provider Events
71-
72-
extension OpenFeatureAPI {
73-
public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) {
74-
providerNotificationCentre.addObserver(
75-
observer,
76-
selector: selector,
77-
name: event.notification,
78-
object: nil
79-
)
80-
}
81-
82-
public func removeHandler(observer: Any, event: ProviderEvent) {
83-
providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil)
84-
}
85-
86-
public func emitEvent(
87-
_ event: ProviderEvent,
88-
provider: FeatureProvider,
89-
error: Error? = nil,
90-
details: [AnyHashable: Any]? = nil
91-
) {
92-
var userInfo: [AnyHashable: Any] = [:]
93-
userInfo[providerEventDetailsKeyProvider] = provider
94-
95-
if let error {
96-
userInfo[providerEventDetailsKeyError] = error
75+
public func observe() -> AnyPublisher<ProviderEvent, Never> {
76+
return providerSubject.map { provider in
77+
if let provider = provider {
78+
return provider.observe()
79+
} else {
80+
return Empty<ProviderEvent, Never>()
81+
.eraseToAnyPublisher()
82+
}
9783
}
98-
99-
if let details {
100-
userInfo.merge(details) { $1 } // Merge, keeping value from `details` if any conflicts
101-
}
102-
103-
providerNotificationCentre.post(name: event.notification, object: nil, userInfo: userInfo)
84+
.switchToLatest()
85+
.eraseToAnyPublisher()
10486
}
10587
}

Sources/OpenFeature/OpenFeatureClient.swift

-43
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@ public class OpenFeatureClient: Client {
1414
private var hookSupport = HookSupport()
1515
private var logger = Logger()
1616

17-
private let providerNotificationCentre = NotificationCenter()
18-
1917
public init(openFeatureApi: OpenFeatureAPI, name: String?, version: String?) {
2018
self.openFeatureApi = openFeatureApi
2119
self.name = name
2220
self.version = version
2321
self.metadata = Metadata(name: name)
24-
25-
subscribeToAllProviderEvents()
2622
}
2723

2824
public func addHooks(_ hooks: any Hook...) {
@@ -200,42 +196,3 @@ extension OpenFeatureClient {
200196
throw OpenFeatureError.generalError(message: "Unable to match default value type with flag value type")
201197
}
202198
}
203-
204-
// MARK: Events
205-
206-
extension OpenFeatureClient {
207-
public func subscribeToAllProviderEvents() {
208-
ProviderEvent.allCases.forEach { event in
209-
OpenFeatureAPI.shared.addHandler(
210-
observer: self,
211-
selector: #selector(handleProviderEvent(notification:)),
212-
event: event)
213-
}
214-
}
215-
216-
public func unsubscribeFromAllProviderEvents() {
217-
ProviderEvent.allCases.forEach { event in
218-
OpenFeatureAPI.shared.removeHandler(observer: self, event: event)
219-
}
220-
}
221-
222-
@objc public func handleProviderEvent(notification: Notification) {
223-
var userInfo: [AnyHashable: Any] = notification.userInfo ?? [:]
224-
userInfo[providerEventDetailsKeyClient] = self
225-
226-
providerNotificationCentre.post(name: notification.name, object: nil, userInfo: userInfo)
227-
}
228-
229-
public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) {
230-
providerNotificationCentre.addObserver(
231-
observer,
232-
selector: selector,
233-
name: event.notification,
234-
object: nil
235-
)
236-
}
237-
238-
public func removeHandler(observer: Any, event: ProviderEvent) {
239-
providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil)
240-
}
241-
}

Sources/OpenFeature/Provider/FeatureProvider.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// The interface implemented by upstream flag providers to resolve flags for their service.
4-
public protocol FeatureProvider {
4+
public protocol FeatureProvider: EventPublisher {
55
var hooks: [any Hook] { get }
66
var metadata: ProviderMetadata { get }
77

Sources/OpenFeature/Provider/NoOpProvider.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import Foundation
2+
import Combine
23

34
/// A ``FeatureProvider`` that simply returns the default values passed to it.
45
class NoOpProvider: FeatureProvider {
56
public static let passedInDefault = "Passed in default"
7+
private let eventHandler = EventHandler(.ready)
68

79
public enum Mode {
810
case normal
@@ -13,11 +15,11 @@ class NoOpProvider: FeatureProvider {
1315
var hooks: [any Hook] = []
1416

1517
func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) {
16-
OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self)
18+
eventHandler.send(.ready)
1719
}
1820

1921
func initialize(initialContext: EvaluationContext?) {
20-
OpenFeatureAPI.shared.emitEvent(.ready, provider: self)
22+
eventHandler.send(.ready)
2123
}
2224

2325
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
@@ -64,6 +66,10 @@ class NoOpProvider: FeatureProvider {
6466
return ProviderEvaluation(
6567
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
6668
}
69+
70+
func observe() -> AnyPublisher<ProviderEvent, Never> {
71+
return eventHandler.observe()
72+
}
6773
}
6874

6975
extension NoOpProvider {

Sources/OpenFeature/Provider/ProviderEvents.swift

-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,4 @@ public enum ProviderEvent: String, CaseIterable {
99
case error = "PROVIDER_ERROR"
1010
case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED"
1111
case stale = "PROVIDER_STALE"
12-
13-
var notification: NSNotification.Name {
14-
NSNotification.Name(rawValue)
15-
}
1612
}

Tests/OpenFeatureTests/DeveloperExperienceTests.swift

+30
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,36 @@ final class DeveloperExperienceTests: XCTestCase {
1919
XCTAssertFalse(flagValue)
2020
}
2121

22+
func testObserveGlobalEvents() {
23+
let readyExpectation = XCTestExpectation(description: "Ready")
24+
let errorExpectation = XCTestExpectation(description: "Error")
25+
let staleExpectation = XCTestExpectation(description: "Stale")
26+
var eventState = OpenFeatureAPI.shared.observe().sink { event in
27+
switch event {
28+
case ProviderEvent.ready:
29+
readyExpectation.fulfill()
30+
case ProviderEvent.error:
31+
errorExpectation.fulfill()
32+
case ProviderEvent.stale:
33+
staleExpectation.fulfill()
34+
default:
35+
XCTFail("Unexpected event")
36+
}
37+
}
38+
let provider = DoSomethingProvider()
39+
OpenFeatureAPI.shared.setProvider(provider: provider)
40+
wait(for: [readyExpectation], timeout: 5)
41+
42+
// Clearing the Provider shouldn't send further global events from it
43+
// Dropping the first event, which reflects the current state before clearing
44+
eventState = OpenFeatureAPI.shared.observe().dropFirst().sink { _ in
45+
XCTFail("Unexpected event")
46+
}
47+
OpenFeatureAPI.shared.clearProvider()
48+
provider.initialize(initialContext: MutableContext(attributes: ["Test": Value.string("Test")]))
49+
XCTAssertNotNil(eventState)
50+
}
51+
2252
func testClientHooks() {
2353
OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
2454
let client = OpenFeatureAPI.shared.getClient()

0 commit comments

Comments
 (0)