diff --git a/DevCycle.xcodeproj/project.pbxproj b/DevCycle.xcodeproj/project.pbxproj index 3696f281..5cd2e805 100644 --- a/DevCycle.xcodeproj/project.pbxproj +++ b/DevCycle.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0B721685290B21CD004D0AB7 /* SSEMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B721684290B21CD004D0AB7 /* SSEMessage.swift */; }; 0F2DD75A279EFA6B00540C9D /* CustomData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2DD759279EFA6B00540C9D /* CustomData.swift */; }; + 0FD643D52A8680E4004BB036 /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FD643D42A8680E4004BB036 /* EventEmitter.swift */; }; 52133B2028DDFB260007691D /* RequestConsolidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52133B1F28DDFB260007691D /* RequestConsolidatorTests.swift */; }; 52133B2228DE00BC0007691D /* ProcessConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52133B2128DE00BC0007691D /* ProcessConfig.swift */; }; 52133B2428DE0FEB0007691D /* GetTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52133B2328DE0FEB0007691D /* GetTestConfig.swift */; }; @@ -82,6 +83,7 @@ /* Begin PBXFileReference section */ 0B721684290B21CD004D0AB7 /* SSEMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSEMessage.swift; sourceTree = ""; }; 0F2DD759279EFA6B00540C9D /* CustomData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomData.swift; sourceTree = ""; }; + 0FD643D42A8680E4004BB036 /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = ""; }; 52133B1F28DDFB260007691D /* RequestConsolidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestConsolidatorTests.swift; sourceTree = ""; }; 52133B2128DE00BC0007691D /* ProcessConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessConfig.swift; sourceTree = ""; }; 52133B2328DE0FEB0007691D /* GetTestConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTestConfig.swift; sourceTree = ""; }; @@ -166,6 +168,7 @@ 529CE32228DCBEC2009AB137 /* RequestConsolidator.swift */, 52133B2128DE00BC0007691D /* ProcessConfig.swift */, 5226DF05290C588900630745 /* NotificationNames.swift */, + 0FD643D42A8680E4004BB036 /* EventEmitter.swift */, ); path = Utils; sourceTree = ""; @@ -444,6 +447,7 @@ 5276C9F0275E682B00B9A324 /* DevCycleOptions.swift in Sources */, 524D58242770F78B00D7CC56 /* ObjCDVCVariable.swift in Sources */, 52133B2228DE00BC0007691D /* ProcessConfig.swift in Sources */, + 0FD643D52A8680E4004BB036 /* EventEmitter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DevCycle/DVCVariable.swift b/DevCycle/DVCVariable.swift index d9cb2e2f..fe92e0a9 100644 --- a/DevCycle/DVCVariable.swift +++ b/DevCycle/DVCVariable.swift @@ -107,8 +107,8 @@ public class DVCVariable { let oldValue = self.value self.value = value self.isDefaulted = false - if let handler = self.handler, - !isEqual(oldValue, variable.value) { + + if let handler = self.handler, !isEqual(oldValue, variable.value) { handler(value) } } else { @@ -136,7 +136,11 @@ public class DVCVariable { } private func addNotificationObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(propertyChange(notification:)), name: Notification.Name(NotificationNames.NewUserConfig), object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(propertyChange(notification:)), + name: Notification.Name(NotificationNames.NewUserConfig), + object: nil) } public func onUpdate(handler: @escaping VariableValueHandler) -> DVCVariable { diff --git a/DevCycle/DevCycleClient.swift b/DevCycle/DevCycleClient.swift index 854de052..4ca14b6d 100644 --- a/DevCycle/DevCycleClient.swift +++ b/DevCycle/DevCycleClient.swift @@ -28,6 +28,9 @@ public typealias IdentifyCompletedHandler = (Error?, [String: Variable]?) -> Voi public typealias FlushCompletedHandler = (Error?) -> Void public typealias CloseCompletedHandler = () -> Void +internal typealias VariableInstanceDic = [String: NSMapTable] + + public class DevCycleClient { var sdkKey: String? var user: DevCycleUser? @@ -42,6 +45,7 @@ public class DevCycleClient { private var enableEdgeDB: Bool = false var inactivityDelayMS: Double = 120000 + let eventEmitter: EventEmitter = EventEmitter() private var service: DevCycleServiceProtocol? private var cacheService: CacheServiceProtocol = CacheService() private var cache: Cache? @@ -49,7 +53,7 @@ public class DevCycleClient { private var flushTimer: Timer? private var closed: Bool = false private var inactivityWorkItem: DispatchWorkItem? - private var variableInstanceDictonary = [String: NSMapTable]() + private var variableInstanceDictonary = VariableInstanceDic() private var isConfigCached: Bool = false private var disableAutomaticEventLogging: Bool = false private var disableCustomEventLogging: Bool = false @@ -140,39 +144,40 @@ public class DevCycleClient { self.service?.getConfig(user: user, enableEdgeDB: self.enableEdgeDB, extraParams: nil, completion: { [weak self] config, error in guard let self = self else { return } + var requestErrored = false + if let error = error { Log.error("Error getting config: \(error)", tags: ["setup"]) self.cache = self.cacheService.load() + + self.eventEmitter.emit(EventEmitValues.error(error)) + requestErrored = true } else { if let config = config { Log.debug("Config: \(config)", tags: ["setup"]) } - self.config?.userConfig = config - self.isConfigCached = false - - self.cacheUser(user: user) + self.setUserConfig(config) + self.cacheUser(user) - if (self.checkIfEdgeDBEnabled(config: config!, enableEdgeDB: self.enableEdgeDB)) { - if (!(user.isAnonymous ?? false)) { - self.service?.saveEntity(user: user, completion: { data, response, error in - if error != nil { - Log.error("Error saving user entity for \(user). Error: \(String(describing: error))") - } else { - Log.info("Saved user entity") - } - }) - } + if (self.checkIfEdgeDBEnabled(config: config!, enableEdgeDB: self.enableEdgeDB) && !(user.isAnonymous ?? false)) { + self.service?.saveEntity(user: user, completion: { data, response, error in + if error != nil { + Log.error("Error saving user entity for \(user). Error: \(String(describing: error))") + } else { + Log.info("Saved user entity") + } + }) } } - self.setupSSEConnection() + self.configCompletionHandlers.forEach { handler in handler(error) } + self.configCompletionHandlers = [] - for handler in self.configCompletionHandlers { - handler(error) - } - callback?(error) self.initialized = true - self.configCompletionHandlers = [] + callback?(error) + self.eventEmitter.emit(EventEmitValues.initialized(!requestErrored)) + + self.setupSSEConnection() }) self.flushTimer = Timer.scheduledTimer( @@ -209,17 +214,40 @@ public class DevCycleClient { let extraParams = RequestParams(sse: sse, lastModified: lastModified, etag: etag) self.service?.getConfig(user: lastIdentifiedUser, enableEdgeDB: self.enableEdgeDB, extraParams: extraParams, completion: { [weak self] config, error in guard let self = self else { return } + if let error = error { Log.error("Error getting config: \(error)", tags: ["refetchConfig"]) + self.eventEmitter.emit(EventEmitValues.error(error)) } else { - self.config?.userConfig = config - self.isConfigCached = false + self.setUserConfig(config) } }) } } + + private func setUserConfig(_ config: UserConfig?) { + let oldConfig = self.config + self.config?.userConfig = config + self.isConfigCached = false + + if let config = config { + self.eventEmitter.emitFeatureUpdates( + oldFeatures: oldConfig?.userConfig?.features, + newFeatures: config.features + ) + self.eventEmitter.emitVariableUpdates( + oldVariables: oldConfig?.userConfig?.variables, + newVariables: config.variables, + variableInstanceDic: self.variableInstanceDictonary + ) + + if oldConfig == nil || config.etag != oldConfig?.userConfig?.etag { + self.eventEmitter.emit(EventEmitValues.configUpdated(config.variables)) + } + } + } - private func cacheUser(user: DevCycleUser) { + private func cacheUser(_ user: DevCycleUser) { self.cacheService.save(user: user) if user.isAnonymous == true, let userId = user.userId { self.cacheService.setAnonUserId(anonUserId: userId) @@ -307,8 +335,9 @@ public class DevCycleClient { return getVariable(key: key, defaultValue: defaultValue) } + private let regex = try? NSRegularExpression(pattern: ".*[^a-z0-9(\\-)(_)].*") + func getVariable(key: String, defaultValue: T) -> DVCVariable { - let regex = try? NSRegularExpression(pattern: ".*[^a-z0-9(\\-)(_)].*") if (regex?.firstMatch(in: key, range: NSMakeRange(0, key.count)) != nil) { Log.error("The variable key \(key) is invalid. It must contain only lowercase letters, numbers, hyphens and underscores. The default value will always be returned for this call.") return DVCVariable( @@ -347,6 +376,8 @@ public class DevCycleClient { self.eventQueue.updateAggregateEvents(variableKey: variable.key, variableIsDefaulted: variable.isDefaulted) } + + self.eventEmitter.emit(EventEmitValues.variableEvaluated(variable.key, variable)) return variable } } @@ -367,18 +398,19 @@ public class DevCycleClient { self.service?.getConfig(user: updateUser, enableEdgeDB: self.enableEdgeDB, extraParams: nil, completion: { [weak self] config, error in guard let self = self else { return } + if let error = error { Log.error("Error getting config: \(error)", tags: ["identify"]) self.cache = self.cacheService.load() + self.eventEmitter.emit(EventEmitValues.error(error)) } else { if let config = config { Log.debug("Config: \(config)", tags: ["identify"]) } - self.config?.userConfig = config - self.isConfigCached = false + self.setUserConfig(config) } self.user = user - self.cacheUser(user: user) + self.cacheUser(user) callback?(error, config?.variables) }) } @@ -395,21 +427,23 @@ public class DevCycleClient { self.service?.getConfig(user: anonUser, enableEdgeDB: self.enableEdgeDB, extraParams: nil, completion: { [weak self] config, error in guard let self = self else { return } - guard error == nil else { + + if let error = error { if let previousAnonUserId = cachedAnonUserId { self.cacheService.setAnonUserId(anonUserId: previousAnonUserId) } + self.eventEmitter.emit(EventEmitValues.error(error)) callback?(error, nil) return } if let config = config { Log.debug("Config: \(config)", tags: ["reset"]) + self.eventEmitter.emit(EventEmitValues.configUpdated(config.variables)) } - self.config?.userConfig = config - self.isConfigCached = false + self.setUserConfig(config) self.user = anonUser - self.cacheUser(user: anonUser) + self.cacheUser(anonUser) callback?(error, config?.variables) }) } @@ -421,6 +455,44 @@ public class DevCycleClient { public func allVariables() -> [String: Variable] { return self.config?.userConfig?.variables ?? [:] } + + public func subscribe(_ handler: InitializedEventHandler) { + self.eventEmitter.subscribe(EventHandlers.initialized(handler)) + } + public func subscribe(_ handler: ErrorEventHandler) { + self.eventEmitter.subscribe(EventHandlers.error(handler)) + } + public func subscribe(_ handler: ConfigUpdatedEventHandler) { + self.eventEmitter.subscribe(EventHandlers.configUpdated(handler)) + } + public func subscribe(_ handler: VariableUpdatedHandler) { + self.eventEmitter.subscribe(EventHandlers.variableUpdated(handler)) + } + public func subscribe(_ handler: VariableEvaluatedHandler) { + self.eventEmitter.subscribe(EventHandlers.variableEvaluated(handler)) + } + public func subscribe(_ handler: FeatureUpdatedHandler) { + self.eventEmitter.subscribe(EventHandlers.featureUpdated(handler)) + } + + public func unsubscribe(_ handler: InitializedEventHandler) { + self.eventEmitter.unsubscribe(EventHandlers.initialized(handler)) + } + public func unsubscribe(_ handler: ErrorEventHandler) { + self.eventEmitter.unsubscribe(EventHandlers.error(handler)) + } + public func unsubscribe(_ handler: ConfigUpdatedEventHandler) { + self.eventEmitter.unsubscribe(EventHandlers.configUpdated(handler)) + } + public func unsubscribe(_ handler: VariableUpdatedHandler) { + self.eventEmitter.unsubscribe(EventHandlers.variableUpdated(handler)) + } + public func unsubscribe(_ handler: VariableEvaluatedHandler) { + self.eventEmitter.unsubscribe(EventHandlers.variableEvaluated(handler)) + } + public func unsubscribe(_ handler: FeatureUpdatedHandler) { + self.eventEmitter.unsubscribe(EventHandlers.featureUpdated(handler)) + } public func track(_ event: DevCycleEvent) { if (self.closed) { diff --git a/DevCycle/Models/UserConfig.swift b/DevCycle/Models/UserConfig.swift index 3571bcab..aaa21a91 100644 --- a/DevCycle/Models/UserConfig.swift +++ b/DevCycle/Models/UserConfig.swift @@ -6,6 +6,9 @@ import Foundation +public typealias VariableSet = [String: Variable] +public typealias FeatureSet = [String: Feature] + enum UserConfigError: Error { case MissingInConfig(String) case MissingProperty(String) @@ -16,17 +19,27 @@ public struct UserConfig { var project: Project var environment: Environment var featureVariationMap: [String: String] - var features: [String: Feature] - var variables: [String: Variable] + var features: FeatureSet + var variables: VariableSet var sse: SSE? var etag: String? - init(from dictionary: [String:Any]) throws { - guard let environment = dictionary["environment"] as? [String: Any] else { throw UserConfigError.MissingInConfig("environment") } - guard let project = dictionary["project"] as? [String: Any] else { throw UserConfigError.MissingInConfig("project") } - guard let featureVariationMap = dictionary["featureVariationMap"] as? [String: String] else { throw UserConfigError.MissingInConfig("featureVariationMap") } - guard var featureMap = dictionary["features"] as? [String: Any] else { throw UserConfigError.MissingInConfig("features") } - guard var variablesMap = dictionary["variables"] as? [String: Any] else { throw UserConfigError.MissingInConfig("variables") } + init(from dictionary: [String: Any]) throws { + guard let environment = dictionary["environment"] as? [String: Any] else { + throw UserConfigError.MissingInConfig("environment") + } + guard let project = dictionary["project"] as? [String: Any] else { + throw UserConfigError.MissingInConfig("project") + } + guard let featureVariationMap = dictionary["featureVariationMap"] as? [String: String] else { + throw UserConfigError.MissingInConfig("featureVariationMap") + } + guard var featureMap = dictionary["features"] as? [String: Any] else { + throw UserConfigError.MissingInConfig("features") + } + guard var variablesMap = dictionary["variables"] as? [String: Any] else { + throw UserConfigError.MissingInConfig("variables") + } let sse = dictionary["sse"] as? [String: Any] let etag = dictionary["etag"] as? String @@ -47,23 +60,21 @@ public struct UserConfig { let variableKeys = Array(variablesMap.keys) for key in featureKeys { - if let featureDict = featureMap[key] as? [String:String] - { + if let featureDict = featureMap[key] as? [String: String] { let feature = try Feature(from: featureDict) featureMap[key] = feature } } for key in variableKeys { - if let variableDict = variablesMap[key] as? [String:Any] - { + if let variableDict = variablesMap[key] as? [String: Any] { let variable = try Variable(from: variableDict) variablesMap[key] = variable } } - self.features = featureMap as! [String: Feature] - self.variables = variablesMap as! [String: Variable] + self.features = featureMap as! FeatureSet + self.variables = variablesMap as! VariableSet } } @@ -73,8 +84,12 @@ public struct Project { var settings: Settings init (from dictionary: [String: Any]) throws { - guard let key = dictionary["key"] as? String else { throw UserConfigError.MissingProperty("key in Project") } - guard let id = dictionary["_id"] as? String else { throw UserConfigError.MissingProperty("_id in Project") } + guard let key = dictionary["key"] as? String else { + throw UserConfigError.MissingProperty("key in Project") + } + guard let id = dictionary["_id"] as? String else { + throw UserConfigError.MissingProperty("_id in Project") + } let settings = dictionary["settings"] as? [String:Any] self._id = id self.key = key @@ -117,8 +132,12 @@ public struct Environment { var key: String init (from dictionary: [String: Any]) throws { - guard let key = dictionary["key"] as? String else { throw UserConfigError.MissingProperty("key in Environment") } - guard let id = dictionary["_id"] as? String else { throw UserConfigError.MissingProperty("_id in Environment") } + guard let key = dictionary["key"] as? String else { + throw UserConfigError.MissingProperty("key in Environment") + } + guard let id = dictionary["_id"] as? String else { + throw UserConfigError.MissingProperty("_id in Environment") + } self._id = id self.key = key } @@ -134,12 +153,24 @@ public struct Feature { public var evalReason: String? init (from dictionary: [String: String]) throws { - guard let id = dictionary["_id"] else { throw UserConfigError.MissingProperty("_id in Feature object") } - guard let variation = dictionary["_variation"] else { throw UserConfigError.MissingProperty("_variation in Feature object") } - guard let key = dictionary["key"] else { throw UserConfigError.MissingProperty("key in Feature object") } - guard let type = dictionary["type"] else { throw UserConfigError.MissingProperty("type in Feature object") } - guard let variationKey = dictionary["variationKey"] else { throw UserConfigError.MissingProperty("variationKey in Feature object") } - guard let variationName = dictionary["variationName"] else { throw UserConfigError.MissingProperty("variationName in Feature object") } + guard let id = dictionary["_id"] else { + throw UserConfigError.MissingProperty("_id in Feature object") + } + guard let variation = dictionary["_variation"] else { + throw UserConfigError.MissingProperty("_variation in Feature object") + } + guard let key = dictionary["key"] else { + throw UserConfigError.MissingProperty("key in Feature object") + } + guard let type = dictionary["type"] else { + throw UserConfigError.MissingProperty("type in Feature object") + } + guard let variationKey = dictionary["variationKey"] else { + throw UserConfigError.MissingProperty("variationKey in Feature object") + } + guard let variationName = dictionary["variationName"] else { + throw UserConfigError.MissingProperty("variationName in Feature object") + } self._id = id self._variation = variation self.key = key @@ -150,7 +181,7 @@ public struct Feature { } } -public struct Variable { +public struct Variable: Equatable { public var _id: String public var key: String public var type: DVCVariableTypes @@ -170,7 +201,10 @@ public struct Variable { guard let varType = DVCVariableTypes(rawValue: type) else { throw UserConfigError.InvalidVariableType("invalid Variable type: \(type)") } - guard let value = dictionary["value"] else { throw UserConfigError.MissingProperty("value in Variable object") } + guard let value = dictionary["value"] else { + throw UserConfigError.MissingProperty("value in Variable object") + } + self._id = id self.key = key self.type = varType @@ -182,4 +216,28 @@ public struct Variable { self.value = value } } + + public static func == (lhs: Variable, rhs: Variable) -> Bool { + return lhs._id == rhs._id && + lhs.key == rhs.key && + compareValues(lhs: lhs, rhs: rhs) && + lhs.evalReason == rhs.evalReason + } + + private static func compareValues(lhs: Variable, rhs: Variable) -> Bool { + guard lhs.type == rhs.type else { + return false + } + + switch lhs.type { + case .Boolean: + return (lhs.value as! Bool) == (rhs.value as! Bool) + case .String: + return (lhs.value as! String) == (rhs.value as! String) + case .Number: + return (lhs.value as! NSNumber) == (rhs.value as! NSNumber) + case .JSON: + return (lhs.value as! NSDictionary) == (rhs.value as! NSDictionary) + } + } } diff --git a/DevCycle/SSEConnection.swift b/DevCycle/SSEConnection.swift index cc2de5d7..ef0ae5df 100644 --- a/DevCycle/SSEConnection.swift +++ b/DevCycle/SSEConnection.swift @@ -9,6 +9,8 @@ import Foundation import LDSwiftEventSource +typealias LDEventHandler = LDSwiftEventSource.EventHandler + public typealias MessageHandler = (String) -> Void protocol SSEConnectionProtocol { @@ -63,7 +65,7 @@ class SSEConnection: SSEConnectionProtocol { } } -class Handler: EventHandler { +class Handler: LDEventHandler { private var handler: MessageHandler init(handler: @escaping MessageHandler) { diff --git a/DevCycle/Utils/EventEmitter.swift b/DevCycle/Utils/EventEmitter.swift new file mode 100644 index 00000000..d3d9e07e --- /dev/null +++ b/DevCycle/Utils/EventEmitter.swift @@ -0,0 +1,224 @@ +// +// EventEmitter.swift +// DevCycle +// + +public typealias ErrorHandlerCallback = (Error) -> Void +public typealias InitializedHandlerCallback = (Bool) -> Void +public typealias ConfigUpdatedHandlerCallback = (VariableSet) -> Void +public typealias VariableUpdatedHandlerCallback = (String, Variable?) -> Void +public typealias VariableEvaluatedHandlerCallback = (String, DVCVariable) -> Void +public typealias FeatureUpdatedHandlerCallback = (String, Feature?) -> Void + +public class BaseHandler: Equatable { + public static func == (lhs: BaseHandler, rhs: BaseHandler) -> Bool { + return lhs === rhs + } + + let callback: T + + public init(_ handler: T) { + self.callback = handler + } +} + +public class BaseHandlerWithKey: BaseHandler { + let key: String? + + public init(key: String?, handler: T) { + self.key = key + super.init(handler) + } +} + +public typealias ErrorEventHandler = BaseHandler +public typealias InitializedEventHandler = BaseHandler +public typealias ConfigUpdatedEventHandler = BaseHandler +public typealias VariableUpdatedHandler = BaseHandlerWithKey +public typealias VariableEvaluatedHandler = BaseHandlerWithKey +public typealias FeatureUpdatedHandler = BaseHandlerWithKey + +enum EventHandlers { + case error(ErrorEventHandler) + case initialized(InitializedEventHandler) + case configUpdated(ConfigUpdatedEventHandler) + case variableUpdated(VariableUpdatedHandler) + case variableEvaluated(VariableEvaluatedHandler) + case featureUpdated(FeatureUpdatedHandler) +} + +enum EventEmitValues { + case error(Error) + case initialized(Bool) + case configUpdated(VariableSet) + case variableUpdated(String, Variable?) + case variableEvaluated(String, DVCVariable) + case featureUpdated(String, Feature?) +} + +class EventEmitter { + var errorHandlers: [ErrorEventHandler] = [] + var initHandlers: [InitializedEventHandler] = [] + var configUpdatedHandlers: [ConfigUpdatedEventHandler] = [] + var variableUpdatedHandlers: [String : [VariableUpdatedHandler]] = [:] + var allVariableUpdatedHandlers: [VariableUpdatedHandler] = [] + var variableEvaluatedHandlers: [String : [VariableEvaluatedHandler]] = [:] + var allVariableEvaluatedHandlers: [VariableEvaluatedHandler] = [] + var featureUpdatedHandlers: [String : [FeatureUpdatedHandler]] = [:] + var allFeatureUpdatedHandlers: [FeatureUpdatedHandler] = [] + + func subscribe(_ handler: EventHandlers) { + switch handler { + case .error(let handler): + self.errorHandlers.append(handler) + case .initialized(let handler): + self.initHandlers.append(handler) + case .configUpdated(let handler): + self.configUpdatedHandlers.append(handler) + case .variableUpdated(let handler): + if let key = handler.key { + subscribeByKey(key, handler: handler, handlersByKey: &self.variableUpdatedHandlers) + } else { + self.allVariableUpdatedHandlers.append(handler) + } + case .variableEvaluated(let handler): + if let key = handler.key { + subscribeByKey(key, handler: handler, handlersByKey: &self.variableEvaluatedHandlers) + } else { + self.allVariableEvaluatedHandlers.append(handler) + } + case .featureUpdated(let handler): + if let key = handler.key { + subscribeByKey(key, handler: handler, handlersByKey: &self.featureUpdatedHandlers) + } else { + self.allFeatureUpdatedHandlers.append(handler) + } + } + } + + private func subscribeByKey(_ key: String, handler: T, handlersByKey: inout [String: [T]]) { + if var handlers = handlersByKey[key] { + handlers.append(handler) + } else { + handlersByKey[key] = [handler] + } + } + + func unsubscribe(_ handler: EventHandlers) { + switch handler { + case .error(let handler): + unsubscribeHandler(handler, handlers: &self.errorHandlers) + case .initialized(let handler): + unsubscribeHandler(handler, handlers: &self.initHandlers) + case .configUpdated(let handler): + unsubscribeHandler(handler, handlers: &self.configUpdatedHandlers) + case .variableUpdated(let handler): + if let key = handler.key { + unsubscribeHandlerByKey(key, handler: handler, handlersByKey: &self.variableUpdatedHandlers) + } else { + unsubscribeHandler(handler, handlers: &self.allVariableUpdatedHandlers) + } + case .variableEvaluated(let handler): + if let key = handler.key { + unsubscribeHandlerByKey(key, handler: handler, handlersByKey: &self.variableEvaluatedHandlers) + } else { + unsubscribeHandler(handler, handlers: &self.allVariableEvaluatedHandlers) + } + case .featureUpdated(let handler): + if let key = handler.key { + unsubscribeHandlerByKey(key, handler: handler, handlersByKey: &self.featureUpdatedHandlers) + } else { + unsubscribeHandler(handler, handlers: &self.allFeatureUpdatedHandlers) + } + } + } + + private func unsubscribeHandlerByKey(_ key: String, handler: T, handlersByKey: inout [String: [T]]) { + if var handlers = handlersByKey[key] { + unsubscribeHandler(handler, handlers: &handlers) + } + } + + private func unsubscribeHandler(_ handler: T, handlers: inout [T]) { + if let index = handlers.firstIndex(where: { $0 == handler }) { + handlers.remove(at: index) + } + } + + func emitFeatureUpdates(oldFeatures: FeatureSet?, newFeatures: FeatureSet) { + if self.featureUpdatedHandlers.count == 0 + && self.allFeatureUpdatedHandlers.count == 0 { + return + } + guard let oldFeatures = oldFeatures else { + newFeatures.forEach { (key: String, variable: Feature) in + self.emit(EventEmitValues.featureUpdated(key, variable)) + } + return + } + + let keys = Set(Array(oldFeatures.keys) + Array(newFeatures.keys)) + keys.forEach { key in + let oldFeatureVar = oldFeatures[key]?._variation + let newFeature = newFeatures[key] + let newFeatureVar = newFeature?._variation + + if oldFeatureVar != newFeatureVar { + self.emit(EventEmitValues.featureUpdated(key, newFeature)) + } + } + } + + func emitVariableUpdates( + oldVariables: VariableSet?, + newVariables: VariableSet, + variableInstanceDic: VariableInstanceDic + ) { + if self.variableUpdatedHandlers.count == 0 + && self.allVariableUpdatedHandlers.count == 0 { + return + } + + guard let oldVariables = oldVariables else { + newVariables.forEach { (key: String, variable: Variable) in + self.emit(EventEmitValues.variableUpdated(key, variable)) + } + return + } + + let keys = Set(Array(oldVariables.keys) + Array(newVariables.keys)) + keys.forEach { key in + let oldVariable = oldVariables[key] + let newVariable = newVariables[key] + + if oldVariable != newVariable { + self.emit(EventEmitValues.variableUpdated(key, newVariable)) + } + } + } + + func emit(_ emitValues: EventEmitValues) { + switch emitValues { + case .error(let err): + self.errorHandlers.forEach { handler in handler.callback(err) } + case .initialized(let initialized): + self.initHandlers.forEach { handler in handler.callback(initialized) } + case .configUpdated(let variableSet): + self.configUpdatedHandlers.forEach { handler in handler.callback(variableSet) } + case .variableUpdated(let key, let variable): + if let handlers = self.variableUpdatedHandlers[key] { + handlers.forEach { handler in handler.callback(key, variable) } + } + case .variableEvaluated(let key, let variable): + if let handlers = self.variableEvaluatedHandlers[key] { + handlers.forEach { handler in handler.callback(key, variable) } + } + allVariableEvaluatedHandlers.forEach { handler in handler.callback(key, variable) } + case .featureUpdated(let key, let feature): + if let handlers = self.featureUpdatedHandlers[key] { + handlers.forEach { handler in handler.callback(key, feature) } + } + allFeatureUpdatedHandlers.forEach { handler in handler.callback(key, feature) } + } + } +} diff --git a/Examples/DevCycle-iOS-Example-App-Swift/DevCycle-iOS-Example-App-Swift/DevCycleManager.swift b/Examples/DevCycle-iOS-Example-App-Swift/DevCycle-iOS-Example-App-Swift/DevCycleManager.swift index 13027a39..062520e4 100644 --- a/Examples/DevCycle-iOS-Example-App-Swift/DevCycle-iOS-Example-App-Swift/DevCycleManager.swift +++ b/Examples/DevCycle-iOS-Example-App-Swift/DevCycle-iOS-Example-App-Swift/DevCycleManager.swift @@ -30,5 +30,47 @@ class DevCycleManager { return } self.client = client + + let errHandler = ErrorEventHandler { error in + print("DevCycle Error: \(error.localizedDescription)") + } + client.subscribe(errHandler) + + let initHandler = InitializedEventHandler { success in + print("DevCycle Initialized: \(success) from subscription") + } + client.subscribe(initHandler) + + let configHandler = ConfigUpdatedEventHandler { variables in + print("DevCycle Config Updated: \(variables)") + } + client.subscribe(configHandler) + + let variableUpdatedHandler = VariableUpdatedHandler(key: nil) { key, variable in + print("DevCycle Variable \(key) Updated: \(variable)") + } + client.subscribe(variableUpdatedHandler) + + let variableEvalHandler = VariableEvaluatedHandler(key: nil) { key, variable in + print("DevCycle Variable \(key) Evaluated: \(variable)") + } + client.subscribe(variableEvalHandler) + + let featureUpdatedHandler = FeatureUpdatedHandler(key: nil) { key, feature in + print("DevCycle Feature \(key) Updated: \(feature)") + } + client.subscribe(featureUpdatedHandler) + + + + Timer.scheduledTimer(withTimeInterval: TimeInterval(15), repeats: false) { timer in + print("Cleanup handlers") + client.unsubscribe(errHandler) + client.unsubscribe(initHandler) + client.unsubscribe(configHandler) + client.unsubscribe(variableUpdatedHandler) + client.unsubscribe(variableEvalHandler) + client.unsubscribe(featureUpdatedHandler) + } } }