// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation // MARK: - Atomic /// The `Atomic` wrapper is a generic wrapper providing a thread-safe way to get and set a value /// /// A write-up on the need for this class and it's approaches can be found at these links: /// https://www.vadimbulavin.com/atomic-properties/ /// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/ /// there is also another approach which can be taken but it requires separate types for collections and results in /// a somewhat inconsistent interface between different `Atomic` wrappers /// /// We use a Read-write lock approach because the `DispatchQueue` approach means mutating the property /// occurs on a different thread, and GRDB requires it's changes to be executed on specific threads so using a lock /// is more compatible (and Read-write locks allow for concurrent reads which shouldn't be a huge issue but could /// help reduce cases of blocking) @propertyWrapper public class Atomic { private var value: Value private let lock: ReadWriteLock = ReadWriteLock() /// In order to change the value you **must** use the `mutate` function public var wrappedValue: Value { lock.readLock() let result: Value = value lock.unlock() return result } /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections public var projectedValue: Atomic { return self } // MARK: - Initialization public init(_ initialValue: Value) { self.value = initialValue } public init(wrappedValue: Value) { self.value = wrappedValue } // MARK: - Functions @discardableResult public func mutate(_ mutation: (inout Value) -> T) -> T { lock.writeLock() let result: T = mutation(&value) lock.unlock() return result } @discardableResult public func mutate(_ mutation: (inout Value) throws -> T) throws -> T { let result: T do { lock.writeLock() result = try mutation(&value) lock.unlock() } catch { lock.unlock() throw error } return result } } extension Atomic where Value: CustomDebugStringConvertible { var debugDescription: String { return value.debugDescription } } // MARK: - ReadWriteLock private class ReadWriteLock { private var rwlock: pthread_rwlock_t // Need to do this in a proper init function instead of a lazy variable or it can indefinitely // hang on XCode 15 when trying to retrieve a lock (potentially due to optimisations?) init() { rwlock = pthread_rwlock_t() pthread_rwlock_init(&rwlock, nil) } func writeLock() { pthread_rwlock_wrlock(&rwlock) } func readLock() { pthread_rwlock_rdlock(&rwlock) } func unlock() { pthread_rwlock_unlock(&rwlock) } }