diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index d721f254..351a462b 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -6,7 +6,7 @@ import Foundation typealias DestroyHandler = () -> Void public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClient, @unchecked Sendable { - + private var storageContainer: SplitStorageContainer private var key: Key private let config: SplitClientConfig @@ -17,6 +17,7 @@ public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClie private var isClientDestroyed = false private let eventsTracker: EventsTracker private weak var clientManager: SplitClientManager? + @objc public var listener: SplitClientEventListener? var initStopwatch: Stopwatch? @@ -87,6 +88,27 @@ extension DefaultSplitClient { } eventsManager.register(event: event, task: task) } + + // MARK: Events Listeners with Medatadata + @objc public func addEventsListener(listener: SplitClientEventListener) { + if let l = listener.onSdkReady { + registerEvent(.sdkReady, action: l) + } + + if let l = listener.onSdkReadyFromCache { + registerEvent(.sdkReadyFromCache, action: l) + } + + if let l = listener.onSdkUpdate { + registerEvent(.sdkUpdated, action: l) + } + } + + private func registerEvent(_ event: SplitEvent, action: @escaping (T) -> Void) { + guard let factory = clientManager?.splitFactory else { return } + let task = SplitEventActionTask(action: action, event: event, runInBackground: true, factory: factory, queue: nil) + eventsManager.register(event: event, task: task) + } } // MARK: Treatment / Evaluation diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index c668fa5e..2075fd78 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -53,12 +53,18 @@ class FailedClient: SplitClient { [:] } + // MARK: Events func on(event: SplitEvent, execute action: @escaping SplitAction) {} func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} + + // MARK: Events Listeners with Medatadata + var listener: (any SplitClientEventListener)? + @objc public func addEventsListener(listener: SplitClientEventListener) {} + // MARK: Track func track(trafficType: String, eventType: String) -> Bool { false } diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 607e761a..18920d36 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -41,20 +41,21 @@ import Foundation /// public final class LocalhostSplitClient: NSObject, SplitClient { - + private let splitsStorage: SplitsStorage private let mySegmentsStorage = EmptyMySegmentsStorage() private let eventsManager: SplitEventsManager? private var evaluator: Evaluator private let key: Key - weak var clientManger: SplitClientManager? + weak var clientManager: SplitClientManager? + @objc public var listener: SplitClientEventListener? init(key: Key, splitsStorage: SplitsStorage, clientManager: SplitClientManager?, eventsManager: SplitEventsManager? = nil, evaluator: Evaluator) { self.eventsManager = eventsManager self.key = key self.splitsStorage = splitsStorage - self.clientManger = clientManager + self.clientManager = clientManager self.evaluator = evaluator super.init() @@ -117,6 +118,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { return results } + // MARK: Events public func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { on(event: event, runInBackground: runInBackground, queue: nil, execute: action) } @@ -131,7 +133,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { private func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { - guard let factory = clientManger?.splitFactory else { return } + guard let factory = clientManager?.splitFactory else { return } if let eventsManager = self.eventsManager { let task = SplitEventActionTask(action: action, event: event, runInBackground: runInBackground, @@ -140,7 +142,29 @@ public final class LocalhostSplitClient: NSObject, SplitClient { eventsManager.register(event: event, task: task) } } + + // MARK: Events Listeners with Medatadata + @objc public func addEventsListener(listener: SplitClientEventListener) { + if let l = listener.onSdkReady { + registerEvent(.sdkReady, action: l) + } + + if let l = listener.onSdkReadyFromCache { + registerEvent(.sdkReadyFromCache, action: l) + } + + if let l = listener.onSdkUpdate { + registerEvent(.sdkUpdated, action: l) + } + } + + private func registerEvent(_ event: SplitEvent, action: @escaping (T) -> Void) { + guard let factory = clientManager?.splitFactory else { return } + let task = SplitEventActionTask(action: action, event: event, runInBackground: true, factory: factory, queue: nil) + eventsManager?.register(event: event, task: task) + } + // MARK: Track public func track(trafficType: String, eventType: String) -> Bool { true } diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index 56b8ba1a..bc330d29 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -9,7 +9,16 @@ import Foundation public typealias SplitAction = () -> Void -public typealias SplitActionWithMetadata = (EventMetadata) -> Void +public typealias SplitActionWithMetadata = (T) -> Void + +@objc public protocol SplitClientEventListener: AnyObject { + @objc(onSdkReady:) + optional func onSdkReady(_ metadata: SdkReadyMetadata) + @objc(onSdkReadyFromCache:) + optional func onSdkReadyFromCache(_ metadata: SdkReadyFromCacheMetadata) + @objc(onSdkUpdate:) + optional func onSdkUpdate(_ metadata: SdkUpdateMetadata) +} @objc public protocol SplitClient { @@ -37,6 +46,11 @@ public typealias SplitActionWithMetadata = (EventMetadata) -> Void func on(event: SplitEvent, execute action: @escaping SplitAction) func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) + + // MARK: Events with Metadata + @objc var listener: SplitClientEventListener? { get set } + @objc(addEventsListener:) + func addEventsListener(listener: SplitClientEventListener) // MARK: Track feature func track(trafficType: String, eventType: String) -> Bool diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index 2d0e48a3..e29d002a 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -5,18 +5,27 @@ import Foundation class SplitEventActionTask: SplitEventTask, @unchecked Sendable { private var eventHandler: SplitAction? - private var eventHandlerWithMetadata: SplitActionWithMetadata? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? var event: SplitEvent var runInBackground: Bool = false var factory: SplitFactory - init(action: @escaping SplitActionWithMetadata, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { - self.eventHandlerWithMetadata = action - self.event = event - self.runInBackground = runInBackground - self.queue = queue - self.factory = factory + init(action: @escaping SplitActionWithMetadata, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory + + // Metadata: "swap" for concrete type and ensure type is correct for this event + self.eventHandlerWithMetadata = { metadata in + guard let typed = metadata as? T else { + Logger.e("Wrong metadata type for this event (\(event.toString())).") + return + } + action(typed) + } } init(action: @escaping SplitAction, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { diff --git a/Split/Events/SplitMetadata.swift b/Split/Events/SplitMetadata.swift index afe38623..efa6a638 100644 --- a/Split/Events/SplitMetadata.swift +++ b/Split/Events/SplitMetadata.swift @@ -2,7 +2,7 @@ import Foundation -@objc public protocol EventMetadata: Sendable {} +@objc public protocol EventMetadata: Sendable, NSObjectProtocol {} // MARK: UPDATE /// Represents the type of SDK update that triggered a metadata callback. diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index 7992e09b..ada32efd 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -10,10 +10,11 @@ import Foundation @testable import Split class InternalSplitClientStub: InternalSplitClient, @unchecked Sendable { - + var splitsStorage: SplitsStorage? var mySegmentsStorage: MySegmentsStorage? var myLargeSegmentsStorage: MySegmentsStorage? + var listener: SplitClientEventListener? init(splitsStorage: SplitsStorage?, mySegmentsStorage: MySegmentsStorage?, @@ -95,14 +96,13 @@ class InternalSplitClientStub: InternalSplitClient, @unchecked Sendable { return ["": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, execute action: @escaping SplitAction) {} + + func addEventsListener(listener: SplitClientEventListener) {} func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index b3568b9d..697c061a 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -11,6 +11,8 @@ import Foundation class SplitClientStub: SplitClient, @unchecked Sendable { + @objc public var listener: SplitClientEventListener? + func getTreatment(_ split: String, attributes: [String : Any]?) -> String { return SplitConstants.control } @@ -95,6 +97,9 @@ class SplitClientStub: SplitClient, @unchecked Sendable { func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { } + func addEventsListener(listener: SplitClientEventListener) { + } + func track(trafficType: String, eventType: String) -> Bool { return true } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 3eff3ebd..1b3eccfa 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -306,13 +306,74 @@ class SplitEventsManagerTest: XCTestCase, @unchecked Sendable { eventManager.stop() } + func testSdkReadyWithMetadata() { + let taskExp = XCTestExpectation() + + let timestamp: Int64 = 1000 + let freshInstall = true + + let metadata = SdkReadyMetadata(lastUpdateTimestamp: timestamp, isInitialCacheLoad: freshInstall) + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(handlerMetadata.lastUpdateTimestamp, timestamp) + XCTAssertEqual(handlerMetadata.isInitialCacheLoad, freshInstall) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, + event: .sdkReady, + runInBackground: false, + factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), + queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 1) + } + + func testSdkUpdateWithMetadata() { + let taskExp = XCTestExpectation() + + let type: SdkUpdateMetadataType = .FLAGS_UPDATE + let names = ["Flag1", "FLAG2"] + + let metadata = SdkUpdateMetadata(type: type, names: names) + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(handlerMetadata.type, type) + XCTAssertEqual(handlerMetadata.names, names) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 1) + } + + func testSdkReadyFromCacheWithMetadata() { + let taskExp = XCTestExpectation() + + let freshInstall = true + let metadata = SdkReadyFromCacheMetadata(isInitialCacheLoad: freshInstall) + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(handlerMetadata.isInitialCacheLoad, freshInstall) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 1) + } + // MARK: Helpers func currentTimestamp() -> Int { return Int(Date().unixTimestamp()) } func sdkTask(exp: XCTestExpectation) -> TestTask { - return TestTask(exp: exp) + TestTask(exp: exp) } } @@ -321,7 +382,7 @@ class TestTask: SplitEventActionTask, @unchecked Sendable { var taskTriggered = false let label: String var exp: XCTestExpectation? - init(exp: XCTestExpectation?, label: String = "", action: SplitActionWithMetadata? = nil, metadata: EventMetadata? = nil) { + init(exp: XCTestExpectation?, label: String = "", action: SplitActionWithMetadata? = nil, metadata: EventMetadata? = nil) { self.exp = exp self.label = label super.init(action: action ?? { _ in }, event: .sdkReady, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey))