mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
491 lines
20 KiB
Swift
491 lines
20 KiB
Swift
// 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<S>(singleton singleton: SingletonConfig<S>) -> S { getOrCreate(singleton) }
|
|
public subscript<M, I>(cache cache: CacheConfig<M, I>) -> I { getOrCreate(cache).immutable(cache: cache, using: self) }
|
|
public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { getOrCreate(defaults) }
|
|
public subscript<T: FeatureOption>(feature feature: FeatureConfig<T>) -> 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<M, I, R>(
|
|
cache: CacheConfig<M, I>,
|
|
_ 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<M, I, R>(
|
|
cache: CacheConfig<M, I>,
|
|
_ 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<T: Collection>(_ collection: T) -> T.Element? {
|
|
return collection.randomElement()
|
|
}
|
|
|
|
public func randomElement<T>(_ elements: Set<T>) -> T? {
|
|
return elements.randomElement()
|
|
}
|
|
|
|
public func popRandomElement<T>(_ elements: inout Set<T>) -> T? {
|
|
return elements.popRandomElement()
|
|
}
|
|
|
|
// MARK: - Instance replacing
|
|
|
|
public func warmCache<M, I>(cache: CacheConfig<M, I>) {
|
|
_ = getOrCreate(cache)
|
|
}
|
|
|
|
public func set<S>(singleton: SingletonConfig<S>, to instance: S) {
|
|
threadSafeChange(for: singleton.identifier, of: .singleton) {
|
|
setValue(instance, typedStorage: .singleton(instance), key: singleton.identifier)
|
|
}
|
|
}
|
|
|
|
public func set<M, I>(cache: CacheConfig<M, I>, to instance: M) {
|
|
threadSafeChange(for: cache.identifier, of: .cache) {
|
|
let value: ThreadSafeObject<MutableCacheType> = ThreadSafeObject(cache.mutableInstance(instance))
|
|
setValue(value, typedStorage: .cache(value), key: cache.identifier)
|
|
}
|
|
}
|
|
|
|
public func remove<M, I>(cache: CacheConfig<M, I>) {
|
|
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<MutableCacheType> {
|
|
func immutable<M, I>(cache: CacheConfig<M, I>, using dependencies: Dependencies) -> I {
|
|
return cache.immutableInstance(
|
|
(self.wrappedValue as? M) ??
|
|
cache.createInstance(dependencies)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Feature Management
|
|
|
|
public extension Dependencies {
|
|
func publisher<T: FeatureOption>(feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
|
|
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<T: FeatureOption>(featureGroupChanges feature: FeatureConfig<T>) -> AnyPublisher<Void, Never> {
|
|
return featureChangeSubject
|
|
.filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier }
|
|
.map { _, _, _ in () }
|
|
.prepend(()) // Emit an initial value to behave similar to the above
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
func featureUpdated<T: FeatureOption>(for feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
|
|
return featureChangeSubject
|
|
.filter { identifier, _, _ in identifier == feature.identifier }
|
|
.compactMap { _, _, value in value as? T }
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
func featureGroupUpdated<T: FeatureOption>(for feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
|
|
return featureChangeSubject
|
|
.filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier }
|
|
.compactMap { _, _, value in value as? T }
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
func hasSet<T: FeatureOption>(feature: FeatureConfig<T>) -> Bool {
|
|
return threadSafeChange(for: feature.identifier, of: .feature) {
|
|
guard let instance: Feature<T> = getValue(feature.identifier, of: .feature) else {
|
|
return false
|
|
}
|
|
|
|
return instance.hasStoredValue(using: self)
|
|
}
|
|
}
|
|
|
|
func set<T: FeatureOption>(feature: FeatureConfig<T>, to updatedFeature: T?) {
|
|
threadSafeChange(for: feature.identifier, of: .feature) {
|
|
/// Update the cached & in-memory values
|
|
let instance: Feature<T> = (
|
|
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<T: FeatureOption>(feature: FeatureConfig<T>) {
|
|
threadSafeChange(for: feature.identifier, of: .feature) {
|
|
/// Reset the cached and in-memory values
|
|
let instance: Feature<T>? = 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<Storage>, 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<Storage>, key key: Setting.DoubleKey) -> Double? {
|
|
return self[singleton: singleton].read { db in db[key] }
|
|
}
|
|
|
|
subscript(singleton singleton: SingletonConfig<Storage>, key key: Setting.IntKey) -> Int? {
|
|
return self[singleton: singleton].read { db in db[key] }
|
|
}
|
|
|
|
subscript(singleton singleton: SingletonConfig<Storage>, key key: Setting.StringKey) -> String? {
|
|
return self[singleton: singleton].read { db in db[key] }
|
|
}
|
|
|
|
subscript(singleton singleton: SingletonConfig<Storage>, key key: Setting.DateKey) -> Date? {
|
|
return self[singleton: singleton].read { db in db[key] }
|
|
}
|
|
|
|
subscript<T: EnumIntSetting>(singleton singleton: SingletonConfig<Storage>, key key: Setting.EnumKey) -> T? {
|
|
return self[singleton: singleton].read { db in db[key] }
|
|
}
|
|
|
|
subscript<T: EnumStringSetting>(singleton singleton: SingletonConfig<Storage>, 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<MutableCacheType>)
|
|
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<T>(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<S>(_ singleton: SingletonConfig<S>) -> S {
|
|
return getOrCreateInstance(
|
|
identifier: singleton.identifier,
|
|
constructor: .singleton { singleton.createInstance(self) }
|
|
)
|
|
}
|
|
|
|
private func getOrCreate<M, I>(_ cache: CacheConfig<M, I>) -> ThreadSafeObject<MutableCacheType> {
|
|
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<T: FeatureOption>(_ feature: FeatureConfig<T>) -> Feature<T> {
|
|
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<Value>` to create a new instance
|
|
/// and store it
|
|
private func getOrCreateInstance<Value>(
|
|
identifier: String,
|
|
constructor: DependencyStorage.Constructor<Value>
|
|
) -> 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<T>(_ 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<T>(_ 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<T>` because the interface is a little
|
|
/// simpler and we don't need to wrap every instance within `ThreadSafeObject<T>` this way
|
|
@discardableResult private func threadSafeChange<T>(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<T> {
|
|
let variant: Key.Variant
|
|
let create: () -> (typedStorage: Dependencies.DependencyStorage.Value, value: T)
|
|
|
|
static func singleton(_ constructor: @escaping () -> T) -> Constructor<T> {
|
|
return Constructor(variant: .singleton) {
|
|
let instance: T = constructor()
|
|
|
|
return (.singleton(instance), instance)
|
|
}
|
|
}
|
|
|
|
static func cache(_ constructor: @escaping () -> T) -> Constructor<T> where T: ThreadSafeObject<MutableCacheType> {
|
|
return Constructor(variant: .cache) {
|
|
let instance: T = constructor()
|
|
|
|
return (.cache(instance), instance)
|
|
}
|
|
}
|
|
|
|
static func userDefaults(_ constructor: @escaping () -> T) -> Constructor<T> where T == UserDefaultsType {
|
|
return Constructor(variant: .userDefaults) {
|
|
let instance: T = constructor()
|
|
|
|
return (.userDefaults(instance), instance)
|
|
}
|
|
}
|
|
|
|
static func feature(_ constructor: @escaping () -> T) -> Constructor<T> where T: FeatureType {
|
|
return Constructor(variant: .feature) {
|
|
let instance: T = constructor()
|
|
|
|
return (.feature(instance), instance)
|
|
}
|
|
}
|
|
}
|
|
}
|