// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation import Combine public class Dependencies { static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "session.dependencies.codingOptions")! /// The `isRTLRetriever` is handled differently from normal dependencies because it's not really treated as such (it's more of /// a convenience thing than anything) as such it's held outside of the `DependencyStorage` @ThreadSafeObject private static var cachedIsRTLRetriever: (requiresMainThread: Bool, retriever: () -> Bool) = (false, { false }) @ThreadSafeObject private static var cachedLastCreatedInstance: Dependencies? = nil private let featureChangeSubject: PassthroughSubject<(String, String?, Any?), Never> = PassthroughSubject() @ThreadSafeObject private var storage: DependencyStorage = DependencyStorage() // MARK: - Subscript Access public subscript(singleton singleton: SingletonConfig) -> S { getOrCreate(singleton) } public subscript(cache cache: CacheConfig) -> I { getOrCreate(cache).immutable(cache: cache, using: self) } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { getOrCreate(defaults) } public subscript(feature feature: FeatureConfig) -> T { getOrCreate(feature).currentValue(using: self) } // MARK: - Global Values, Timing and Async Handling /// We should avoid using this value wherever possible because it's not properly injected (which means unit tests won't work correctly /// for anything accessed via this value) public static var unsafeNonInjected: Dependencies { cachedLastCreatedInstance ?? Dependencies() } public static var isRTL: Bool { /// Determining `isRTL` might require running on the main thread (it may need to accesses UIKit), if it requires the main thread but /// we are on a different thread then just default to `false` to prevent the background thread from potentially lagging and/or crashing guard !cachedIsRTLRetriever.requiresMainThread || Thread.isMainThread else { return false } return cachedIsRTLRetriever.retriever() } public var dateNow: Date { Date() } public var fixedTime: Int { 0 } public var forceSynchronous: Bool { false } // MARK: - Initialization private init() { Dependencies._cachedLastCreatedInstance.set(to: self) } internal init(forTesting: Bool) {} public static func createEmpty() -> Dependencies { return Dependencies() } // MARK: - Functions public func async(at fixedTime: Int, closure: @escaping () -> Void) { async(at: TimeInterval(fixedTime), closure: closure) } public func async(at timestamp: TimeInterval, closure: @escaping () -> Void) {} @discardableResult public func mutate( cache: CacheConfig, _ mutation: (M) -> R ) -> R { return getOrCreate(cache).performMap { erasedValue in guard let value: M = (erasedValue as? M) else { /// This code path should never happen (and is essentially invalid if it does) but in order to avoid neeing to return /// a nullable type or force-casting this is how we need to do things) Log.critical("Failed to convert erased cache value for '\(cache.identifier)' to expected type: \(M.self)") let fallbackValue: M = cache.createInstance(self) return mutation(fallbackValue) } return mutation(value) } } @discardableResult public func mutate( cache: CacheConfig, _ mutation: (M) throws -> R ) throws -> R { return try getOrCreate(cache).performMap { erasedValue in guard let value: M = (erasedValue as? M) else { /// This code path should never happen (and is essentially invalid if it does) but in order to avoid neeing to return /// a nullable type or force-casting this is how we need to do things) Log.critical("Failed to convert erased cache value for '\(cache.identifier)' to expected type: \(M.self)") let fallbackValue: M = cache.createInstance(self) return try mutation(fallbackValue) } return try mutation(value) } } // MARK: - Random Access Functions public func randomElement(_ collection: T) -> T.Element? { return collection.randomElement() } public func randomElement(_ elements: Set) -> T? { return elements.randomElement() } public func popRandomElement(_ elements: inout Set) -> T? { return elements.popRandomElement() } // MARK: - Instance replacing public func warmCache(cache: CacheConfig) { _ = getOrCreate(cache) } public func set(singleton: SingletonConfig, to instance: S) { threadSafeChange(for: singleton.identifier, of: .singleton) { setValue(instance, typedStorage: .singleton(instance), key: singleton.identifier) } } public func set(cache: CacheConfig, to instance: M) { threadSafeChange(for: cache.identifier, of: .cache) { let value: ThreadSafeObject = ThreadSafeObject(cache.mutableInstance(instance)) setValue(value, typedStorage: .cache(value), key: cache.identifier) } } public func remove(cache: CacheConfig) { threadSafeChange(for: cache.identifier, of: .cache) { removeValue(cache.identifier, of: .cache) } } public static func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) { _cachedIsRTLRetriever.set(to: (requiresMainThread, isRTLRetriever)) } } // MARK: - Cache Management private extension ThreadSafeObject { func immutable(cache: CacheConfig, using dependencies: Dependencies) -> I { return cache.immutableInstance( (self.wrappedValue as? M) ?? cache.createInstance(dependencies) ) } } // MARK: - Feature Management public extension Dependencies { func publisher(feature: FeatureConfig) -> AnyPublisher { return featureChangeSubject .filter { identifier, _, _ in identifier == feature.identifier } .compactMap { _, _, value in value as? T } .prepend(self[feature: feature]) // Emit the current value first .eraseToAnyPublisher() } func publisher(featureGroupChanges feature: FeatureConfig) -> AnyPublisher { return featureChangeSubject .filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier } .map { _, _, _ in () } .prepend(()) // Emit an initial value to behave similar to the above .eraseToAnyPublisher() } func featureUpdated(for feature: FeatureConfig) -> AnyPublisher { return featureChangeSubject .filter { identifier, _, _ in identifier == feature.identifier } .compactMap { _, _, value in value as? T } .eraseToAnyPublisher() } func featureGroupUpdated(for feature: FeatureConfig) -> AnyPublisher { return featureChangeSubject .filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier } .compactMap { _, _, value in value as? T } .eraseToAnyPublisher() } func hasSet(feature: FeatureConfig) -> Bool { return threadSafeChange(for: feature.identifier, of: .feature) { guard let instance: Feature = getValue(feature.identifier, of: .feature) else { return false } return instance.hasStoredValue(using: self) } } func set(feature: FeatureConfig, to updatedFeature: T?) { threadSafeChange(for: feature.identifier, of: .feature) { /// Update the cached & in-memory values let instance: Feature = ( getValue(feature.identifier, of: .feature) ?? feature.createInstance(self) ) instance.setValue(to: updatedFeature, using: self) setValue(instance, typedStorage: .feature(instance), key: feature.identifier) } /// Notify observers featureChangeSubject.send((feature.identifier, feature.groupIdentifier, updatedFeature)) } func reset(feature: FeatureConfig) { threadSafeChange(for: feature.identifier, of: .feature) { /// Reset the cached and in-memory values let instance: Feature? = getValue(feature.identifier, of: .feature) instance?.setValue(to: nil, using: self) removeValue(feature.identifier, of: .feature) } /// Notify observers featureChangeSubject.send((feature.identifier, feature.groupIdentifier, nil)) } } // MARK: - Storage Setting Convenience public extension Dependencies { subscript(singleton singleton: SingletonConfig, key key: Setting.BoolKey) -> Bool { return self[singleton: singleton] .read { db in db[key] } .defaulting(to: false) // Default to false if it doesn't exist } subscript(singleton singleton: SingletonConfig, key key: Setting.DoubleKey) -> Double? { return self[singleton: singleton].read { db in db[key] } } subscript(singleton singleton: SingletonConfig, key key: Setting.IntKey) -> Int? { return self[singleton: singleton].read { db in db[key] } } subscript(singleton singleton: SingletonConfig, key key: Setting.StringKey) -> String? { return self[singleton: singleton].read { db in db[key] } } subscript(singleton singleton: SingletonConfig, key key: Setting.DateKey) -> Date? { return self[singleton: singleton].read { db in db[key] } } subscript(singleton singleton: SingletonConfig, key key: Setting.EnumKey) -> T? { return self[singleton: singleton].read { db in db[key] } } subscript(singleton singleton: SingletonConfig, key key: Setting.EnumKey) -> T? { return self[singleton: singleton].read { db in db[key] } } } // MARK: - UserDefaults Convenience public extension Dependencies { subscript(defaults defaults: UserDefaultsConfig, key key: UserDefaults.BoolKey) -> Bool { get { return self[defaults: defaults].bool(forKey: key.rawValue) } set { self[defaults: defaults].set(newValue, forKey: key.rawValue) } } subscript(defaults defaults: UserDefaultsConfig, key key: UserDefaults.DateKey) -> Date? { get { return self[defaults: defaults].object(forKey: key.rawValue) as? Date } set { self[defaults: defaults].set(newValue, forKey: key.rawValue) } } subscript(defaults defaults: UserDefaultsConfig, key key: UserDefaults.DoubleKey) -> Double { get { return self[defaults: defaults].double(forKey: key.rawValue) } set { self[defaults: defaults].set(newValue, forKey: key.rawValue) } } subscript(defaults defaults: UserDefaultsConfig, key key: UserDefaults.IntKey) -> Int { get { return self[defaults: defaults].integer(forKey: key.rawValue) } set { self[defaults: defaults].set(newValue, forKey: key.rawValue) } } subscript(defaults defaults: UserDefaultsConfig, key key: UserDefaults.StringKey) -> String? { get { return self[defaults: defaults].string(forKey: key.rawValue) } set { self[defaults: defaults].set(newValue, forKey: key.rawValue) } } } // MARK: - DependenciesError public enum DependenciesError: Error { case missingDependencies } // MARK: - Storage Management private extension Dependencies { class DependencyStorage { var initializationLocks: [Key: NSLock] = [:] var instances: [Key: Value] = [:] struct Key: Hashable, CustomStringConvertible { enum Variant: String { case singleton case cache case userDefaults case feature func key(_ identifier: String) -> Key { return Key(identifier, of: self) } } let identifier: String let variant: Variant var description: String { "\(variant): \(identifier)" } init(_ identifier: String, of variant: Variant) { self.identifier = identifier self.variant = variant } } enum Value { case singleton(Any) case cache(ThreadSafeObject) case userDefaults(UserDefaultsType) case feature(any FeatureType) func distinctKey(for identifier: String) -> Key { switch self { case .singleton: return Key(identifier, of: .singleton) case .cache: return Key(identifier, of: .cache) case .userDefaults: return Key(identifier, of: .userDefaults) case .feature: return Key(identifier, of: .feature) } } func value(as type: T.Type) -> T? { switch self { case .singleton(let value): return value as? T case .cache(let value): return value as? T case .userDefaults(let value): return value as? T case .feature(let value): return value as? T } } } } private func getOrCreate(_ singleton: SingletonConfig) -> S { return getOrCreateInstance( identifier: singleton.identifier, constructor: .singleton { singleton.createInstance(self) } ) } private func getOrCreate(_ cache: CacheConfig) -> ThreadSafeObject { return getOrCreateInstance( identifier: cache.identifier, constructor: .cache { ThreadSafeObject(cache.mutableInstance(cache.createInstance(self))) } ) } private func getOrCreate(_ defaults: UserDefaultsConfig) -> UserDefaultsType { return getOrCreateInstance( identifier: defaults.identifier, constructor: .userDefaults { defaults.createInstance(self) } ) } private func getOrCreate(_ feature: FeatureConfig) -> Feature { return getOrCreateInstance( identifier: feature.identifier, constructor: .feature { feature.createInstance(self) } ) } // MARK: - Instance upserting /// Retrieves the current instance or, if one doesn't exist, uses the `StorageHelper.Info` to create a new instance /// and store it private func getOrCreateInstance( identifier: String, constructor: DependencyStorage.Constructor ) -> Value { /// If we already have an instance then just return that if let existingValue: Value = getValue(identifier, of: constructor.variant) { return existingValue } return threadSafeChange(for: identifier, of: constructor.variant) { /// Now that we are within a synchronized group, check to make sure an instance wasn't created while we were waiting to /// enter the group if let existingValue: Value = getValue(identifier, of: constructor.variant) { return existingValue } let result: (typedStorage: DependencyStorage.Value, value: Value) = constructor.create() setValue(result.value, typedStorage: result.typedStorage, key: identifier) return result.value } } /// Convenience method to retrieve the existing dependency instance from memory in a thread-safe way private func getValue(_ key: String, of variant: DependencyStorage.Key.Variant) -> T? { return _storage.performMap { storage in guard let typedValue: DependencyStorage.Value = storage.instances[variant.key(key)] else { return nil } guard let result: T = typedValue.value(as: T.self) else { /// If there is a value stored for the key, but it's not the right type then something has gone wrong, and we should log Log.critical("Failed to convert stored dependency '\(variant.key(key))' to expected type: \(T.self)") return nil } return result } } /// Convenience method to store a dependency instance in memory in a thread-safe way @discardableResult private func setValue(_ value: T, typedStorage: DependencyStorage.Value, key: String) -> T { return _storage.performUpdateAndMap { storage in storage.instances[typedStorage.distinctKey(for: key)] = typedStorage return (storage, value) } } /// Convenience method to remove a dependency instance from memory in a thread-safe way private func removeValue(_ key: String, of variant: DependencyStorage.Key.Variant) { _storage.performUpdate { storage in storage.instances.removeValue(forKey: variant.key(key)) return storage } } /// This function creates an `NSLock` for the given identifier which allows us to block instance creation on a per-identifier basis /// and avoid situations where multithreading could result in multiple instances of the same dependency being created concurrently /// /// **Note:** This `NSLock` is an additional mechanism on top of the `ThreadSafeObject` because the interface is a little /// simpler and we don't need to wrap every instance within `ThreadSafeObject` this way @discardableResult private func threadSafeChange(for identifier: String, of variant: DependencyStorage.Key.Variant, change: () -> T) -> T { let lock: NSLock = _storage.performUpdateAndMap { storage in if let existing = storage.initializationLocks[variant.key(identifier)] { return (storage, existing) } let lock: NSLock = NSLock() storage.initializationLocks[variant.key(identifier)] = lock return (storage, lock) } lock.lock() defer { lock.unlock() } return change() } } // MARK: - DSL private extension Dependencies.DependencyStorage { struct Constructor { let variant: Key.Variant let create: () -> (typedStorage: Dependencies.DependencyStorage.Value, value: T) static func singleton(_ constructor: @escaping () -> T) -> Constructor { return Constructor(variant: .singleton) { let instance: T = constructor() return (.singleton(instance), instance) } } static func cache(_ constructor: @escaping () -> T) -> Constructor where T: ThreadSafeObject { return Constructor(variant: .cache) { let instance: T = constructor() return (.cache(instance), instance) } } static func userDefaults(_ constructor: @escaping () -> T) -> Constructor where T == UserDefaultsType { return Constructor(variant: .userDefaults) { let instance: T = constructor() return (.userDefaults(instance), instance) } } static func feature(_ constructor: @escaping () -> T) -> Constructor where T: FeatureType { return Constructor(variant: .feature) { let instance: T = constructor() return (.feature(instance), instance) } } } }