├── .buckconfig ├── .gitignore ├── .swift-version ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── Store.xcscheme │ └── StoreTests.xcscheme ├── .travis.yml ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── Store │ ├── Concurrency │ ├── Atomics.swift │ ├── Locking.swift │ └── PropertyWrappers.swift │ ├── Extensions │ ├── Binding.swift │ └── KeyPath+ReadableFormat.swift │ ├── PushID.swift │ ├── Serialization │ ├── DictionaryCodable.swift │ └── Diffing.swift │ ├── Store │ ├── Store+Forwarding.swift │ ├── Store.swift │ └── StoreOptions.swift │ └── Utilities │ ├── Assign.swift │ ├── Partial.swift │ ├── PropertyObservable.swift │ ├── ReadOnly+Forwarding.swift │ └── ReadOnly.swift ├── Tests ├── LinuxMain.swift └── StoreTests │ ├── StoreTests.swift │ ├── SupportTypes.swift │ ├── UtilitiesTests.swift │ └── XCTestManifests.swift ├── docs └── store_logo.png └── format.sh /.buckconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexdrone/Store/fb51754e8f88a0ab99c7994d0d26d3374155f1fc/.buckconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | .DS_Store 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | .build/ 40 | 41 | # CocoaPods 42 | # 43 | # We recommend against adding the Pods directory to your .gitignore. However 44 | # you should judge for yourself, the pros and cons are mentioned at: 45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 46 | # 47 | # Pods/ 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | .buckd/ 68 | .build_temp/ 69 | 70 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.5 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Store.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/StoreTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: swift 3 | osx_image: xcode11 4 | 5 | install: 6 | - gem install xcpretty 7 | 8 | script: 9 | - swift package generate-xcodeproj 10 | - xcodebuild clean build test -scheme Store -destination 'platform=iOS Simulator,name=iPhone X,OS=13.0' | xcpretty 11 | - xcodebuild clean build test -scheme Store -destination 'platform=macOS,arch=x86_64' | xcpretty 12 | - xcodebuild clean build test -scheme Store -destination 'platform=tvOS Simulator,name=Apple TV 4K,OS=13.0' | xcpretty 13 | - xcodebuild clean build -scheme Store -destination 'platform=watchOS Simulator,name=Apple Watch Series 5 - 44mm,OS=6.0' | xcpretty 14 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-atomics", 6 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f", 10 | "version": "0.0.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-log", 15 | "repositoryURL": "https://github.com/apple/swift-log.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", 19 | "version": "1.4.2" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Store", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .tvOS(.v13), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 16 | .library( 17 | name: "Store", 18 | targets: ["Store"]), 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/apple/swift-atomics.git", from: "0.0.3"), 22 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") 23 | ], 24 | targets: [ 25 | .target( 26 | name: "Store", 27 | dependencies: [ 28 | .product(name: "Atomics", package: "swift-atomics"), 29 | .product(name: "Logging", package: "swift-log") 30 | ]), 31 | .testTarget( 32 | name: "StoreTests", 33 | dependencies: ["Store"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift](https://img.shields.io/badge/swift-5.5-orange.svg?style=flat)](#) [![Build Status](https://travis-ci.org/alexdrone/Store.svg?branch=master)](https://travis-ci.org/alexdrone/Store) [![Cov](https://img.shields.io/badge/coverage-45.8%25-blue.svg?style=flat)](#) [![Platform](https://img.shields.io/badge/platform-macOS%20|%20iOS%20|%20WatchOS%20|%20tvOS%20|%20Linux-red.svg?style=flat)](#) 2 | Store 3 | 4 | Unidirectional, transactional Store implementation for Swift and SwiftUI 5 | 6 | *Store is being reworked to take advantage of async/await. [See the stable pre-async version of Store here](https://github.com/alexdrone/Store/tree/legacy).* 7 | -------------------------------------------------------------------------------- /Sources/Store/Concurrency/Atomics.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | import Foundation 3 | 4 | extension ManagedAtomic: @unchecked Sendable {} 5 | -------------------------------------------------------------------------------- /Sources/Store/Concurrency/Locking.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Locking 4 | 5 | public protocol Locking { 6 | /// Create a new lock. 7 | init() 8 | 9 | /// Acquire the lock. 10 | /// 11 | /// Whenever possible, consider using `withLock` instead of this method and 12 | /// `unlock`, to simplify lock handling. 13 | func lock() 14 | 15 | /// Release the lock. 16 | /// 17 | /// Whenver possible, consider using `withLock` instead of this method and 18 | /// `lock`, to simplify lock handling. 19 | func unlock() 20 | } 21 | 22 | extension Locking { 23 | /// Acquire the lock for the duration of the given block. 24 | /// 25 | /// This convenience method should be preferred to `lock` and `unlock` in 26 | /// most situations, as it ensures that the lock will be released regardless 27 | /// of how `body` exits. 28 | @inlinable 29 | public func withLock(_ body: () throws -> T) rethrows -> T { 30 | defer { 31 | unlock() 32 | } 33 | lock() 34 | return try body() 35 | } 36 | 37 | /// Specialise Void return (for performance). 38 | @inlinable 39 | public func withLockVoid(_ body: () throws -> Void) rethrows { 40 | try self.withLock(body) 41 | } 42 | 43 | /// Async variant. 44 | @inlinable 45 | public func withLock(_ body: () async throws -> T) async rethrows -> T { 46 | defer { 47 | unlock() 48 | } 49 | lock() 50 | return try await body() 51 | } 52 | } 53 | 54 | // MARK: - Foundation Locks 55 | 56 | /// An object that coordinates the operation of multiple threads of execution within the 57 | /// same application. 58 | extension NSLock: Locking {} 59 | 60 | /// A lock that may be acquired multiple times by the same thread without causing a deadlock. 61 | extension NSRecursiveLock: Locking {} 62 | 63 | /// A lock that multiple applications on multiple hosts can use to restrict access to some 64 | /// shared resource, such as a file. 65 | extension NSConditionLock: Locking {} 66 | 67 | // MARK: - Mutex 68 | 69 | /// A mechanism that enforces limits on access to a resource when there are many threads 70 | /// of execution. 71 | public final class Mutex: Locking { 72 | private var mutex: pthread_mutex_t = { 73 | var mutex = pthread_mutex_t() 74 | pthread_mutex_init(&mutex, nil) 75 | return mutex 76 | }() 77 | 78 | public init() {} 79 | 80 | public func lock() { 81 | pthread_mutex_lock(&mutex) 82 | } 83 | 84 | public func unlock() { 85 | pthread_mutex_unlock(&mutex) 86 | } 87 | } 88 | 89 | // MARK: - UnfairLock 90 | 91 | /// A low-level lock that allows waiters to block efficiently on contention. 92 | public final class UnfairLock: Locking { 93 | private var unfairLock = os_unfair_lock_s() 94 | 95 | public init() {} 96 | 97 | public func lock() { 98 | os_unfair_lock_lock(&unfairLock) 99 | } 100 | 101 | public func unlock() { 102 | os_unfair_lock_unlock(&unfairLock) 103 | } 104 | } 105 | 106 | // MARK: - ReadersWriterLock 107 | 108 | /// A readers-writer lock provided by the platform implementation of the POSIX Threads standard. 109 | /// Read more: https://en.wikipedia.org/wiki/POSIX_Threads 110 | public final class ReadersWriterLock: @unchecked Sendable { 111 | private var rwlock: UnsafeMutablePointer 112 | 113 | public init() { 114 | rwlock = UnsafeMutablePointer.allocate(capacity: 1) 115 | assert(pthread_rwlock_init(rwlock, nil) == 0) 116 | } 117 | 118 | deinit { 119 | assert(pthread_rwlock_destroy(rwlock) == 0) 120 | rwlock.deinitialize(count: 1) 121 | rwlock.deallocate() 122 | } 123 | 124 | public func withReadLock(body: () throws -> T) rethrows -> T { 125 | pthread_rwlock_rdlock(rwlock) 126 | defer { 127 | pthread_rwlock_unlock(rwlock) 128 | } 129 | return try body() 130 | } 131 | 132 | public func withWriteLock(body: () throws -> T) rethrows -> T { 133 | pthread_rwlock_wrlock(rwlock) 134 | defer { 135 | pthread_rwlock_unlock(rwlock) 136 | } 137 | return try body() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Store/Concurrency/PropertyWrappers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Atomic 4 | 5 | @propertyWrapper 6 | public final class LockAtomic: @unchecked Sendable { 7 | private let lock: L 8 | private var value: T 9 | 10 | public init(wrappedValue: T, _ lock: L.Type) { 11 | self.lock = L.init() 12 | self.value = wrappedValue 13 | } 14 | 15 | public var wrappedValue: T { 16 | get { 17 | lock.withLock { 18 | value 19 | } 20 | } 21 | set { 22 | lock.withLock { 23 | value = newValue 24 | } 25 | } 26 | } 27 | 28 | /// Used for multi-statement atomic access to the wrapped property. 29 | /// This is especially useful to wrap index-subscripts in value type collections that 30 | /// otherwise would result in a call to get, a value copy and a subsequent call to set. 31 | public func mutate(_ block: (inout T) -> Void) { 32 | lock.withLock { 33 | block(&value) 34 | } 35 | } 36 | 37 | public var projectedValue: LockAtomic { self } 38 | } 39 | 40 | // MARK: - SyncDispatchQueueAtomic 41 | 42 | @propertyWrapper 43 | public final class SyncDispatchQueueAtomic: @unchecked Sendable { 44 | private let queue: DispatchQueue 45 | private var value: T 46 | private let concurrentReads: Bool 47 | 48 | public init(wrappedValue: T, concurrentReads: Bool = true) { 49 | self.value = wrappedValue 50 | self.concurrentReads = concurrentReads 51 | let label = "SyncDispatchQueueAtomic.\(UUID().uuidString)" 52 | self.queue = DispatchQueue(label: label, attributes: concurrentReads ? [.concurrent] : []) 53 | } 54 | 55 | public var wrappedValue: T { 56 | get { queue.sync { value } } 57 | set { queue.sync(flags: concurrentReads ? [.barrier] : []) { value = newValue } } 58 | } 59 | 60 | /// Used for multi-statement atomic access to the wrapped property. 61 | /// This is especially useful to wrap index-subscripts in value type collections that 62 | /// otherwise would result in a call to get, a value copy and a subsequent call to set. 63 | public func mutate(_ block: (inout T) -> Void) { 64 | queue.sync { 65 | block(&value) 66 | } 67 | } 68 | 69 | public var projectedValue: SyncDispatchQueueAtomic { self } 70 | } 71 | 72 | // MARK: - ReadersWriterAtomic 73 | 74 | @propertyWrapper 75 | public final class ReadersWriterAtomic: @unchecked Sendable { 76 | private let lock = ReadersWriterLock() 77 | private var value: T 78 | 79 | public init(wrappedValue: T) { 80 | self.value = wrappedValue 81 | } 82 | 83 | public var wrappedValue: T { 84 | get { 85 | lock.withReadLock { 86 | value 87 | } 88 | } 89 | set { 90 | lock.withWriteLock { 91 | self.value = newValue 92 | } 93 | } 94 | } 95 | 96 | /// Used for multi-statement atomic access to the wrapped property. 97 | /// This is especially useful to wrap index-subscripts in value type collections that 98 | /// otherwise would result in a call to get, a value copy and a subsequent call to set. 99 | public func mutate(_ block: (inout T) -> Void) { 100 | lock.withWriteLock { 101 | block(&value) 102 | } 103 | } 104 | 105 | public var projectedValue: ReadersWriterAtomic { self } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/Store/Extensions/Binding.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import Logging 4 | 5 | #if canImport(SwiftUI) 6 | import SwiftUI 7 | #else 8 | 9 | @propertyWrapper 10 | @dynamicMemberLookup 11 | public struct Binding { 12 | public var wrappedValue: Value { 13 | get { get() } 14 | nonmutating set { set(newValue, transaction) } 15 | } 16 | public var transaction = Transaction() 17 | 18 | private let get: () -> Value 19 | private let set: (Value, Transaction) -> Void 20 | 21 | public var projectedValue: Binding { self } 22 | 23 | public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { 24 | self.get = get 25 | self.set = { v, _ in set(v) } 26 | } 27 | 28 | public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void) { 29 | self.transaction = .init() 30 | self.get = get 31 | self.set = { 32 | set($0, $1) 33 | } 34 | } 35 | 36 | public static func constant(_ value: Value) -> Binding { 37 | .init(get: { value }, set: { _ in }) 38 | } 39 | 40 | public subscript( 41 | dynamicMember keyPath: WritableKeyPath 42 | ) -> Binding { 43 | .init( 44 | get: { wrappedValue[keyPath: keyPath] }, 45 | set: { wrappedValue[keyPath: keyPath] = $0 }) 46 | } 47 | 48 | public func transaction(_ transaction: Transaction) -> Binding { 49 | fatalError() 50 | } 51 | } 52 | 53 | public struct Transaction {} 54 | 55 | #endif 56 | 57 | //MARK: - Extensions 58 | 59 | /// Optional Coalescing for `Binding`. 60 | public func ?? (lhs: Binding, rhs: T) -> Binding { 61 | Binding( 62 | get: { lhs.wrappedValue ?? rhs }, 63 | set: { lhs.wrappedValue = $0 } 64 | ) 65 | } 66 | 67 | extension Binding { 68 | /// When the `Binding`'s wrapped value changes, the given closure is executed. 69 | public func onUpdate(_ closure: @escaping () -> Void) -> Binding { 70 | Binding( 71 | get: { wrappedValue }, 72 | set: { 73 | wrappedValue = $0 74 | closure() 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Store/Extensions/KeyPath+ReadableFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension KeyPath { 4 | 5 | /// Returns the human readable name for the property pointed at. 6 | public var readableFormat: String? { 7 | guard let offset = MemoryLayout.offset(of: self) else { 8 | return nil 9 | } 10 | let typePtr = unsafeBitCast(Root.self, to: UnsafeMutableRawPointer.self) 11 | let metadata = typePtr.assumingMemoryBound(to: StructMetadata.self) 12 | let kind = metadata.pointee._kind 13 | 14 | // see https://github.com/apple/swift/blob/main/include/swift/ABI/MetadataKind.def 15 | guard kind == 1 || kind == 0x200 else { 16 | assertionFailure() 17 | return nil 18 | } 19 | 20 | let typeDescriptor = metadata.pointee.typeDescriptor 21 | let numberOfFields = Int(typeDescriptor.pointee.numberOfFields) 22 | let offsets = typeDescriptor.pointee.offsetToTheFieldOffsetVector.buffer( 23 | metadata: typePtr, 24 | count: numberOfFields) 25 | 26 | guard let fieldIndex = offsets.firstIndex(of: Int32(offset)) else { 27 | return nil 28 | } 29 | return typeDescriptor.pointee 30 | .fieldDescriptor.advanced().pointee 31 | .fields.pointer().advanced(by: fieldIndex).pointee 32 | .fieldName() 33 | } 34 | } 35 | 36 | // MARK: - Memory Layout 37 | 38 | private struct StructMetadata { 39 | var _kind: Int 40 | var typeDescriptor: UnsafeMutablePointer 41 | } 42 | 43 | private struct StructTypeDescriptor { 44 | var flags: Int32 45 | var parent: Int32 46 | var mangledName: RelativePointer 47 | var accessFunctionPtr: RelativePointer 48 | var fieldDescriptor: RelativePointer 49 | var numberOfFields: Int32 50 | var offsetToTheFieldOffsetVector: RelativeBufferPointer 51 | } 52 | 53 | private struct FieldDescriptor { 54 | var mangledTypeNameOffset: Int32 55 | var superClassOffset: Int32 56 | var _kind: UInt16 57 | var fieldRecordSize: Int16 58 | var numFields: Int32 59 | var fields: Buffer 60 | 61 | struct Record { 62 | var fieldRecordFlags: Int32 63 | var _mangledTypeName: RelativePointer 64 | var _fieldName: RelativePointer 65 | 66 | mutating func fieldName() -> String { 67 | String(cString: _fieldName.advanced()) 68 | } 69 | } 70 | } 71 | 72 | // MARK: - Pointers 73 | 74 | private struct Buffer { 75 | var element: Element 76 | 77 | mutating func pointer() -> UnsafeMutablePointer { 78 | withUnsafePointer(to: &self) { 79 | UnsafeMutableRawPointer(mutating: UnsafeRawPointer($0)).assumingMemoryBound(to: Element.self) 80 | } 81 | } 82 | } 83 | 84 | private struct RelativePointer { 85 | var offset: Offset 86 | 87 | mutating func advanced() -> UnsafeMutablePointer { 88 | let offset = self.offset 89 | return withUnsafePointer(to: &self) { p in 90 | UnsafeMutableRawPointer(mutating: p) 91 | .advanced(by: numericCast(offset)) 92 | .assumingMemoryBound(to: Pointee.self) 93 | } 94 | } 95 | } 96 | 97 | private struct RelativeBufferPointer { 98 | var strides: Offset 99 | 100 | func buffer(metadata: UnsafeRawPointer, count: Int) -> UnsafeBufferPointer { 101 | let offset = numericCast(strides) * MemoryLayout.size 102 | let ptr = metadata.advanced(by: offset).assumingMemoryBound(to: Pointee.self) 103 | return UnsafeBufferPointer(start: ptr, count: count) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Store/PushID.swift: -------------------------------------------------------------------------------- 1 | // Forked from alexdrone/PushID 2 | // See LICENSE file. 3 | 4 | import Foundation 5 | 6 | /// Shorthand to PushID's 'make' function. 7 | public func makePushID() -> String { 8 | return PushID.default.make() 9 | } 10 | 11 | /// ID generator that creates 20-character string identifiers with the following properties: 12 | /// 1. They're based on timestamp so that they sort *after* any existing ids. 13 | /// 2. They contain 72-bits of random data after the timestamp so that IDs won't collide with 14 | /// other clients' IDs. 15 | /// 3. They sort *lexicographically* (so the timestamp is converted to characters that will 16 | /// sort properly). 17 | /// 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, 18 | /// the latter ones will sort after the former ones. We do this by using the previous random bits 19 | /// but "incrementing" them by 1 (only in the case of a timestamp collision). 20 | public final class PushID { 21 | 22 | public static let `default` = PushID() 23 | 24 | // MARK: Static constants 25 | 26 | /// Modeled after base64 web-safe chars, but ordered by ASCII. 27 | private static let ascChars = Array( 28 | "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz") 29 | 30 | private static let descChars = Array(ascChars.reversed()) 31 | 32 | // MARK: State 33 | 34 | /// Timestamp of last push, used to prevent local collisions if you push twice in one ms. 35 | private var lastPushTime: UInt64 = 0 36 | 37 | /// We generate 72-bits of randomness which get turned into 12 characters and appended to the 38 | /// timestamp to prevent collisions with other clients. We store the last characters we 39 | /// generated because in the event of a collision, we'll use those same characters except 40 | /// "incremented" by one. 41 | private var lastRandChars = [Int](repeating: 0, count: 12) 42 | 43 | /// For testability purposes. 44 | private let dateProvider: () -> Date 45 | 46 | /// Ensure the generator synchronization. 47 | private var lock = UnfairLock() 48 | 49 | public init(dateProvider: @escaping () -> Date = { Date() }) { 50 | self.dateProvider = dateProvider 51 | } 52 | 53 | /// Generate a new push UUID. 54 | public func make(ascending: Bool = true) -> String { 55 | let pushChars = ascending ? PushID.ascChars : PushID.descChars 56 | precondition(pushChars.count > 0) 57 | var timeStampChars = [Character](repeating: pushChars.first!, count: 8) 58 | 59 | self.lock.lock() 60 | 61 | var now = UInt64(self.dateProvider().timeIntervalSince1970 * 1000) 62 | let duplicateTime = (now == self.lastPushTime) 63 | 64 | self.lastPushTime = now 65 | 66 | for i in stride(from: 7, to: 0, by: -1) { 67 | timeStampChars[i] = pushChars[Int(now % 64)] 68 | now >>= 6 69 | } 70 | 71 | assert(now == 0, "The whole timestamp should be now converted.") 72 | var id = String(timeStampChars) 73 | 74 | if !duplicateTime { 75 | for i in 0..<12 { 76 | self.lastRandChars[i] = Int(64 * Double(arc4random()) / Double(UInt32.max)) 77 | } 78 | } else { 79 | // If the timestamp hasn't changed since last push, use the same random number, 80 | // except incremented by 1. 81 | var index: Int = 0 82 | for i in stride(from: 11, to: 0, by: -1) { 83 | index = i 84 | guard self.lastRandChars[i] == 63 else { break } 85 | self.lastRandChars[i] = 0 86 | } 87 | self.lastRandChars[index] += 1 88 | } 89 | 90 | // Appends the random characters. 91 | for i in 0..<12 { 92 | id.append(pushChars[self.lastRandChars[i]]) 93 | } 94 | assert(id.lengthOfBytes(using: .utf8) == 20, "The id lenght should be 20.") 95 | 96 | self.lock.unlock() 97 | return id 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Store/Serialization/DictionaryCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias EncodedDictionary = [String: Any] 4 | 5 | // MARK: - DictionaryEncoder 6 | 7 | open class DictionaryEncoder: Encoder { 8 | open var codingPath: [CodingKey] = [] 9 | open var userInfo: [CodingUserInfoKey: Any] = [:] 10 | private var storage = Storage() 11 | 12 | public init() {} 13 | 14 | open func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { 15 | KeyedEncodingContainer(KeyedContainer(encoder: self, codingPath: codingPath)) 16 | } 17 | 18 | open func unkeyedContainer() -> UnkeyedEncodingContainer { 19 | UnkeyedContanier(encoder: self, codingPath: codingPath) 20 | } 21 | 22 | open func singleValueContainer() -> SingleValueEncodingContainer { 23 | SingleValueContanier(encoder: self, codingPath: codingPath) 24 | } 25 | 26 | private func box(_ value: T) throws -> Any { 27 | /// @note: This results in a EXC_BAD_ACCESS on XCode 11.2 (works again in XCode 11.3). 28 | try value.encode(to: self) 29 | return storage.popContainer() 30 | } 31 | } 32 | 33 | extension DictionaryEncoder { 34 | open func encode(_ value: T) throws -> [String: Any] { 35 | do { 36 | return try castOrThrow([String: Any].self, try box(value)) 37 | } catch (let error) { 38 | throw EncodingError.invalidValue( 39 | value, 40 | EncodingError.Context( 41 | codingPath: [], 42 | debugDescription: "Top-evel \(T.self) did not encode any values.", 43 | underlyingError: error)) 44 | } 45 | } 46 | } 47 | 48 | extension DictionaryEncoder { 49 | private class KeyedContainer: KeyedEncodingContainerProtocol { 50 | private var encoder: DictionaryEncoder 51 | private(set) var codingPath: [CodingKey] 52 | private var storage: Storage 53 | 54 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { 55 | self.encoder = encoder 56 | self.codingPath = codingPath 57 | self.storage = encoder.storage 58 | storage.push(container: [:] as [String: Any]) 59 | } 60 | 61 | deinit { 62 | guard let dictionary = storage.popContainer() as? [String: Any] else { 63 | assertionFailure() 64 | return 65 | } 66 | storage.push(container: dictionary) 67 | } 68 | 69 | private func set(_ value: Any, forKey key: String) { 70 | guard var dictionary = storage.popContainer() as? [String: Any] else { 71 | assertionFailure() 72 | return 73 | } 74 | dictionary[key] = value 75 | storage.push(container: dictionary) 76 | } 77 | 78 | func encodeNil(forKey key: Key) throws {} 79 | 80 | func encode( 81 | _ value: Bool, 82 | forKey key: Key 83 | ) throws { set(value, forKey: key.stringValue) } 84 | 85 | func encode( 86 | _ value: Int, 87 | forKey key: Key 88 | ) throws { set(value, forKey: key.stringValue) } 89 | 90 | func encode( 91 | _ value: Int8, 92 | forKey key: Key 93 | ) throws { set(value, forKey: key.stringValue) } 94 | 95 | func encode( 96 | _ value: Int16, 97 | forKey key: Key 98 | ) throws { set(value, forKey: key.stringValue) } 99 | 100 | func encode( 101 | _ value: Int32, 102 | forKey key: Key 103 | ) throws { set(value, forKey: key.stringValue) } 104 | 105 | func encode( 106 | _ value: Int64, 107 | forKey key: Key 108 | ) throws { set(value, forKey: key.stringValue) } 109 | 110 | func encode( 111 | _ value: UInt, 112 | forKey key: Key 113 | ) throws { set(value, forKey: key.stringValue) } 114 | 115 | func encode( 116 | _ value: UInt8, 117 | forKey key: Key 118 | ) throws { set(value, forKey: key.stringValue) } 119 | 120 | func encode( 121 | _ value: UInt16, 122 | forKey key: Key 123 | ) throws { set(value, forKey: key.stringValue) } 124 | 125 | func encode( 126 | _ value: UInt32, 127 | forKey key: Key 128 | ) throws { set(value, forKey: key.stringValue) } 129 | 130 | func encode( 131 | _ value: UInt64, 132 | forKey key: Key 133 | ) throws { set(value, forKey: key.stringValue) } 134 | 135 | func encode( 136 | _ value: Float, 137 | forKey key: Key 138 | ) throws { set(value, forKey: key.stringValue) } 139 | 140 | func encode( 141 | _ value: Double, 142 | forKey key: Key 143 | ) throws { set(value, forKey: key.stringValue) } 144 | 145 | func encode( 146 | value: String, 147 | forKey key: Key 148 | ) throws { set(value, forKey: key.stringValue) } 149 | 150 | func encode( 151 | _ value: T, 152 | forKey key: Key 153 | ) throws { 154 | encoder.codingPath.append(key) 155 | defer { encoder.codingPath.removeLast() } 156 | set(try encoder.box(value), forKey: key.stringValue) 157 | } 158 | 159 | func nestedContainer( 160 | keyedBy keyType: NestedKey.Type, 161 | forKey key: Key 162 | ) -> KeyedEncodingContainer { 163 | codingPath.append(key) 164 | defer { codingPath.removeLast() } 165 | return KeyedEncodingContainer( 166 | KeyedContainer( 167 | encoder: encoder, 168 | codingPath: codingPath)) 169 | } 170 | 171 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 172 | codingPath.append(key) 173 | defer { codingPath.removeLast() } 174 | return UnkeyedContanier(encoder: encoder, codingPath: codingPath) 175 | } 176 | 177 | func superEncoder() -> Encoder { encoder } 178 | 179 | func superEncoder(forKey key: Key) -> Encoder { encoder } 180 | } 181 | 182 | private class UnkeyedContanier: UnkeyedEncodingContainer { 183 | var encoder: DictionaryEncoder 184 | private(set) var codingPath: [CodingKey] 185 | private var storage: Storage 186 | var count: Int { return storage.count } 187 | 188 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { 189 | self.encoder = encoder 190 | self.codingPath = codingPath 191 | self.storage = encoder.storage 192 | storage.push(container: [] as [Any]) 193 | } 194 | 195 | deinit { 196 | guard let array = storage.popContainer() as? [Any] else { 197 | assertionFailure() 198 | return 199 | } 200 | storage.push(container: array) 201 | } 202 | 203 | private func push(_ value: Any) { 204 | guard var array = storage.popContainer() as? [Any] else { 205 | assertionFailure() 206 | return 207 | } 208 | array.append(value) 209 | storage.push(container: array) 210 | } 211 | 212 | func encodeNil() throws {} 213 | 214 | func encode( 215 | _ value: Bool 216 | ) throws {} 217 | 218 | func encode( 219 | _ value: Int 220 | ) throws { push(try encoder.box(value)) } 221 | 222 | func encode( 223 | _ value: Int8 224 | ) throws { push(try encoder.box(value)) } 225 | 226 | func encode( 227 | _ value: Int16 228 | ) throws { push(try encoder.box(value)) } 229 | 230 | func encode( 231 | _ value: Int32 232 | ) throws { push(try encoder.box(value)) } 233 | 234 | func encode( 235 | _ value: Int64 236 | ) throws { push(try encoder.box(value)) } 237 | 238 | func encode( 239 | _ value: UInt 240 | ) throws { push(try encoder.box(value)) } 241 | 242 | func encode( 243 | _ value: UInt8 244 | ) throws { push(try encoder.box(value)) } 245 | 246 | func encode( 247 | _ value: UInt16 248 | ) throws { push(try encoder.box(value)) } 249 | 250 | func encode( 251 | _ value: UInt32 252 | ) throws { push(try encoder.box(value)) } 253 | 254 | func encode( 255 | _ value: UInt64 256 | ) throws { push(try encoder.box(value)) } 257 | 258 | func encode( 259 | _ value: Float 260 | ) throws { push(try encoder.box(value)) } 261 | 262 | func encode( 263 | _ value: Double 264 | ) throws { push(try encoder.box(value)) } 265 | 266 | func encode( 267 | _ value: String 268 | ) throws { push(try encoder.box(value)) } 269 | 270 | func encode(_ value: T) throws { 271 | encoder.codingPath.append(AnyCodingKey(index: count)) 272 | defer { encoder.codingPath.removeLast() } 273 | push(try encoder.box(value)) 274 | } 275 | 276 | func nestedContainer( 277 | keyedBy keyType: NestedKey.Type 278 | ) -> KeyedEncodingContainer where NestedKey: CodingKey { 279 | codingPath.append(AnyCodingKey(index: count)) 280 | defer { codingPath.removeLast() } 281 | return KeyedEncodingContainer( 282 | KeyedContainer( 283 | encoder: encoder, 284 | codingPath: codingPath)) 285 | } 286 | 287 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 288 | codingPath.append(AnyCodingKey(index: count)) 289 | defer { codingPath.removeLast() } 290 | return UnkeyedContanier(encoder: encoder, codingPath: codingPath) 291 | } 292 | 293 | func superEncoder() -> Encoder { 294 | return encoder 295 | } 296 | } 297 | 298 | private class SingleValueContanier: SingleValueEncodingContainer { 299 | var encoder: DictionaryEncoder 300 | private(set) var codingPath: [CodingKey] 301 | private var storage: Storage 302 | var count: Int { return storage.count } 303 | 304 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { 305 | self.encoder = encoder 306 | self.codingPath = codingPath 307 | self.storage = encoder.storage 308 | } 309 | 310 | private func push(_ value: Any) { 311 | guard var array = storage.popContainer() as? [Any] else { 312 | assertionFailure() 313 | return 314 | } 315 | array.append(value) 316 | storage.push(container: array) 317 | } 318 | 319 | func encodeNil() throws {} 320 | 321 | func encode( 322 | _ value: Bool 323 | ) throws { storage.push(container: value) } 324 | 325 | func encode( 326 | _ value: Int 327 | ) throws { storage.push(container: value) } 328 | 329 | func encode( 330 | _ value: Int8 331 | ) throws { storage.push(container: value) } 332 | 333 | func encode( 334 | _ value: Int16 335 | ) throws { storage.push(container: value) } 336 | 337 | func encode( 338 | _ value: Int32 339 | ) throws { storage.push(container: value) } 340 | 341 | func encode( 342 | _ value: Int64 343 | ) throws { storage.push(container: value) } 344 | 345 | func encode( 346 | _ value: UInt 347 | ) throws { storage.push(container: value) } 348 | 349 | func encode( 350 | _ value: UInt8 351 | ) throws { storage.push(container: value) } 352 | 353 | func encode( 354 | _ value: UInt16 355 | ) throws { storage.push(container: value) } 356 | 357 | func encode( 358 | _ value: UInt32 359 | ) throws { storage.push(container: value) } 360 | 361 | func encode( 362 | _ value: UInt64 363 | ) throws { storage.push(container: value) } 364 | 365 | func encode( 366 | _ value: Float 367 | ) throws { storage.push(container: value) } 368 | 369 | func encode( 370 | _ value: Double 371 | ) throws { storage.push(container: value) } 372 | 373 | func encode( 374 | _ value: String 375 | ) throws { storage.push(container: value) } 376 | 377 | func encode(_ value: T) throws { 378 | storage.push(container: try encoder.box(value)) 379 | } 380 | } 381 | } 382 | 383 | // MARK: - DictionaryDecoder 384 | 385 | open class DictionaryDecoder: Decoder { 386 | open var codingPath: [CodingKey] 387 | open var userInfo: [CodingUserInfoKey: Any] = [:] 388 | private var storage = Storage() 389 | 390 | public init() { 391 | codingPath = [] 392 | } 393 | 394 | public init(container: Any, codingPath: [CodingKey] = []) { 395 | storage.push(container: container) 396 | self.codingPath = codingPath 397 | } 398 | 399 | open func container( 400 | keyedBy type: Key.Type 401 | ) throws -> KeyedDecodingContainer { 402 | let container = try lastContainer(forType: [String: Any].self) 403 | return KeyedDecodingContainer( 404 | KeyedContainer( 405 | decoder: self, 406 | codingPath: [], 407 | container: container)) 408 | } 409 | 410 | open func unkeyedContainer() throws -> UnkeyedDecodingContainer { 411 | let container = try lastContainer(forType: [Any].self) 412 | return UnkeyedContanier(decoder: self, container: container) 413 | } 414 | 415 | open func singleValueContainer() throws -> SingleValueDecodingContainer { 416 | return SingleValueContanier(decoder: self) 417 | } 418 | 419 | private func unbox(_ value: Any, as type: T.Type) throws -> T { 420 | return try unbox(value, as: type, codingPath: codingPath) 421 | } 422 | 423 | private func unbox( 424 | _ value: Any, 425 | as type: T.Type, 426 | codingPath: [CodingKey] 427 | ) throws -> T { 428 | let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead." 429 | let error = DecodingError.typeMismatch( 430 | T.self, 431 | DecodingError.Context(codingPath: codingPath, debugDescription: description)) 432 | return try castOrThrow(T.self, value, error: error) 433 | } 434 | 435 | private func unbox(_ value: Any, as type: T.Type) throws -> T { 436 | return try unbox(value, as: type, codingPath: codingPath) 437 | } 438 | 439 | private func unbox( 440 | _ value: Any, 441 | as type: T.Type, 442 | codingPath: [CodingKey] 443 | ) throws -> T { 444 | let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead." 445 | let error = DecodingError.typeMismatch( 446 | T.self, 447 | DecodingError.Context(codingPath: codingPath, debugDescription: description)) 448 | do { 449 | return try castOrThrow(T.self, value, error: error) 450 | } catch { 451 | storage.push(container: value) 452 | defer { _ = storage.popContainer() } 453 | return try T(from: self) 454 | } 455 | } 456 | 457 | private func lastContainer(forType type: T.Type) throws -> T { 458 | guard let value = storage.last else { 459 | let description = "Expected \(type) but found nil value instead." 460 | let error = DecodingError.Context(codingPath: codingPath, debugDescription: description) 461 | throw DecodingError.valueNotFound(type, error) 462 | } 463 | return try unbox(value, as: T.self) 464 | } 465 | 466 | private func lastContainer(forType type: T.Type) throws -> T { 467 | guard let value = storage.last else { 468 | let description = "Expected \(type) but found nil value instead." 469 | let error = DecodingError.Context(codingPath: codingPath, debugDescription: description) 470 | throw DecodingError.valueNotFound(type, error) 471 | } 472 | return try unbox(value, as: T.self) 473 | } 474 | 475 | private func notFound(key: CodingKey) -> DecodingError { 476 | let error = DecodingError.Context( 477 | codingPath: codingPath, 478 | debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").") 479 | return DecodingError.keyNotFound(key, error) 480 | } 481 | } 482 | 483 | extension DictionaryDecoder { 484 | open func decode(_ type: T.Type, from container: Any) throws -> T { 485 | storage.push(container: container) 486 | return try unbox(container, as: T.self) 487 | } 488 | } 489 | 490 | extension DictionaryDecoder { 491 | private class KeyedContainer: KeyedDecodingContainerProtocol { 492 | private var decoder: DictionaryDecoder 493 | private(set) var codingPath: [CodingKey] 494 | private var container: [String: Any] 495 | 496 | init(decoder: DictionaryDecoder, codingPath: [CodingKey], container: [String: Any]) { 497 | self.decoder = decoder 498 | self.codingPath = codingPath 499 | self.container = container 500 | } 501 | 502 | var allKeys: [Key] { return container.keys.compactMap { Key(stringValue: $0) } } 503 | func contains(_ key: Key) -> Bool { return container[key.stringValue] != nil } 504 | 505 | private func find(forKey key: CodingKey) throws -> Any { 506 | return try container.tryValue(forKey: key.stringValue, error: decoder.notFound(key: key)) 507 | } 508 | 509 | func _decode(_ type: T.Type, forKey key: Key) throws -> T { 510 | let value = try find(forKey: key) 511 | decoder.codingPath.append(key) 512 | defer { decoder.codingPath.removeLast() } 513 | return try decoder.unbox(value, as: T.self) 514 | } 515 | 516 | func decodeNil( 517 | forKey key: Key 518 | ) throws -> Bool { 519 | // TODO: Verify, this broke in Xcode13b2. 520 | // decoder.notFound(key: key) 521 | return true 522 | } 523 | 524 | func decode( 525 | _ type: Bool.Type, 526 | forKey key: Key 527 | ) throws -> Bool { try _decode(type, forKey: key) } 528 | 529 | func decode( 530 | _ type: Int.Type, 531 | forKey key: Key 532 | ) throws -> Int { try _decode(type, forKey: key) } 533 | 534 | func decode( 535 | _ type: Int8.Type, 536 | forKey key: Key 537 | ) throws -> Int8 { try _decode(type, forKey: key) } 538 | 539 | func decode( 540 | _ type: Int16.Type, 541 | forKey key: Key 542 | ) throws -> Int16 { try _decode(type, forKey: key) } 543 | 544 | func decode( 545 | _ type: Int32.Type, 546 | forKey key: Key 547 | ) throws -> Int32 { try _decode(type, forKey: key) } 548 | 549 | func decode( 550 | _ type: Int64.Type, 551 | forKey key: Key 552 | ) throws -> Int64 { try _decode(type, forKey: key) } 553 | 554 | func decode( 555 | _ type: UInt.Type, 556 | forKey key: Key 557 | ) throws -> UInt { try _decode(type, forKey: key) } 558 | 559 | func decode( 560 | _ type: UInt8.Type, 561 | forKey key: Key 562 | ) throws -> UInt8 { try _decode(type, forKey: key) } 563 | 564 | func decode( 565 | _ type: UInt16.Type, 566 | forKey key: Key 567 | ) throws -> UInt16 { try _decode(type, forKey: key) } 568 | 569 | func decode( 570 | _ type: UInt32.Type, 571 | forKey key: Key 572 | ) throws -> UInt32 { try _decode(type, forKey: key) } 573 | 574 | func decode( 575 | _ type: UInt64.Type, 576 | forKey key: Key 577 | ) throws -> UInt64 { try _decode(type, forKey: key) } 578 | 579 | func decode( 580 | _ type: Float.Type, 581 | forKey key: Key 582 | ) throws -> Float { try _decode(type, forKey: key) } 583 | 584 | func decode( 585 | _ type: Double.Type, 586 | forKey key: Key 587 | ) throws -> Double { try _decode(type, forKey: key) } 588 | 589 | func decode( 590 | _ type: String.Type, 591 | forKey key: Key 592 | ) throws -> String { try _decode(type, forKey: key) } 593 | 594 | func decode( 595 | _ type: T.Type, 596 | forKey key: Key 597 | ) throws -> T { try _decode(type, forKey: key) } 598 | 599 | func nestedContainer( 600 | keyedBy type: NestedKey.Type, 601 | forKey key: Key 602 | ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { 603 | decoder.codingPath.append(key) 604 | defer { decoder.codingPath.removeLast() } 605 | let value = try find(forKey: key) 606 | let dictionary = try decoder.unbox(value, as: [String: Any].self) 607 | return KeyedDecodingContainer( 608 | KeyedContainer( 609 | decoder: decoder, 610 | codingPath: [], 611 | container: dictionary)) 612 | } 613 | 614 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 615 | decoder.codingPath.append(key) 616 | defer { decoder.codingPath.removeLast() } 617 | let value = try find(forKey: key) 618 | let array = try decoder.unbox(value, as: [Any].self) 619 | return UnkeyedContanier(decoder: decoder, container: array) 620 | } 621 | 622 | func _superDecoder(forKey key: CodingKey = AnyCodingKey.super) throws -> Decoder { 623 | decoder.codingPath.append(key) 624 | defer { decoder.codingPath.removeLast() } 625 | 626 | let value = try find(forKey: key) 627 | return DictionaryDecoder(container: value, codingPath: decoder.codingPath) 628 | } 629 | 630 | func superDecoder() throws -> Decoder { 631 | return try _superDecoder() 632 | } 633 | 634 | func superDecoder(forKey key: Key) throws -> Decoder { 635 | return try _superDecoder(forKey: key) 636 | } 637 | } 638 | 639 | private class UnkeyedContanier: UnkeyedDecodingContainer { 640 | private var decoder: DictionaryDecoder 641 | private(set) var codingPath: [CodingKey] 642 | private var container: [Any] 643 | 644 | var count: Int? { return container.count } 645 | var isAtEnd: Bool { return currentIndex >= count! } 646 | 647 | private(set) var currentIndex: Int 648 | 649 | private var currentCodingPath: [CodingKey] { 650 | return decoder.codingPath + [AnyCodingKey(index: currentIndex)] 651 | } 652 | 653 | init(decoder: DictionaryDecoder, container: [Any]) { 654 | self.decoder = decoder 655 | self.codingPath = decoder.codingPath 656 | self.container = container 657 | currentIndex = 0 658 | } 659 | 660 | private func checkIndex(_ type: T.Type) throws { 661 | if isAtEnd { 662 | let error = DecodingError.Context( 663 | codingPath: currentCodingPath, 664 | debugDescription: "container is at end.") 665 | throw DecodingError.valueNotFound(T.self, error) 666 | } 667 | } 668 | 669 | func _decode(_ type: T.Type) throws -> T { 670 | try checkIndex(type) 671 | 672 | decoder.codingPath.append(AnyCodingKey(index: currentIndex)) 673 | defer { 674 | decoder.codingPath.removeLast() 675 | currentIndex += 1 676 | } 677 | return try decoder.unbox(container[currentIndex], as: T.self) 678 | } 679 | 680 | func decodeNil() throws -> Bool { 681 | try checkIndex(Any?.self) 682 | return false 683 | } 684 | 685 | func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } 686 | func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } 687 | func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } 688 | func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } 689 | func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } 690 | func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } 691 | func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } 692 | func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } 693 | func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } 694 | func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } 695 | func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } 696 | func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } 697 | func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } 698 | func decode(_ type: String.Type) throws -> String { return try _decode(type) } 699 | func decode(_ type: T.Type) throws -> T { return try _decode(type) } 700 | 701 | func nestedContainer( 702 | keyedBy type: NestedKey.Type 703 | ) throws -> KeyedDecodingContainer { 704 | decoder.codingPath.append(AnyCodingKey(index: currentIndex)) 705 | defer { decoder.codingPath.removeLast() } 706 | try checkIndex(UnkeyedContanier.self) 707 | let value = container[currentIndex] 708 | let dictionary = try castOrThrow([String: Any].self, value) 709 | currentIndex += 1 710 | return KeyedDecodingContainer( 711 | KeyedContainer( 712 | decoder: decoder, 713 | codingPath: [], 714 | container: dictionary)) 715 | } 716 | 717 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 718 | decoder.codingPath.append(AnyCodingKey(index: currentIndex)) 719 | defer { decoder.codingPath.removeLast() } 720 | try checkIndex(UnkeyedContanier.self) 721 | let value = container[currentIndex] 722 | let array = try castOrThrow([Any].self, value) 723 | currentIndex += 1 724 | return UnkeyedContanier(decoder: decoder, container: array) 725 | } 726 | 727 | func superDecoder() throws -> Decoder { 728 | decoder.codingPath.append(AnyCodingKey(index: currentIndex)) 729 | defer { decoder.codingPath.removeLast() } 730 | try checkIndex(UnkeyedContanier.self) 731 | let value = container[currentIndex] 732 | currentIndex += 1 733 | return DictionaryDecoder(container: value, codingPath: decoder.codingPath) 734 | } 735 | } 736 | 737 | private class SingleValueContanier: SingleValueDecodingContainer { 738 | private var decoder: DictionaryDecoder 739 | private(set) var codingPath: [CodingKey] 740 | 741 | init(decoder: DictionaryDecoder) { 742 | self.decoder = decoder 743 | self.codingPath = decoder.codingPath 744 | } 745 | 746 | func _decode(_ type: T.Type) throws -> T { 747 | return try decoder.lastContainer(forType: type) 748 | } 749 | 750 | func decodeNil() -> Bool { return decoder.storage.last == nil } 751 | func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) } 752 | func decode(_ type: Int.Type) throws -> Int { return try _decode(type) } 753 | func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) } 754 | func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) } 755 | func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) } 756 | func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) } 757 | func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) } 758 | func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) } 759 | func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) } 760 | func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) } 761 | func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) } 762 | func decode(_ type: Float.Type) throws -> Float { return try _decode(type) } 763 | func decode(_ type: Double.Type) throws -> Double { return try _decode(type) } 764 | func decode(_ type: String.Type) throws -> String { return try _decode(type) } 765 | func decode(_ type: T.Type) throws -> T { return try _decode(type) } 766 | } 767 | } 768 | 769 | // MARK: - Helpers 770 | 771 | final class Storage { 772 | private(set) var containers: [Any] = [] 773 | 774 | var count: Int { 775 | containers.count 776 | } 777 | 778 | var last: Any? { 779 | containers.last 780 | } 781 | 782 | func push(container: Any) { 783 | containers.append(container) 784 | } 785 | 786 | @discardableResult func popContainer() -> Any { 787 | precondition(containers.count > 0, "Empty container stack.") 788 | return containers.popLast()! 789 | } 790 | } 791 | 792 | public enum DictionaryCodableError: Error { 793 | case cast 794 | case unwrapped 795 | case tryValue 796 | } 797 | 798 | func castOrThrow( 799 | _ resultType: T.Type, 800 | _ object: Any, 801 | error: Error = DictionaryCodableError.cast 802 | ) throws -> T { 803 | guard let returnValue = object as? T else { 804 | throw error 805 | } 806 | return returnValue 807 | } 808 | 809 | extension Optional { 810 | func unwrapOrThrow(error: Error = DictionaryCodableError.unwrapped) throws -> Wrapped { 811 | guard let unwrapped = self else { 812 | throw error 813 | } 814 | return unwrapped 815 | } 816 | } 817 | 818 | extension Dictionary { 819 | func tryValue(forKey key: Key, error: Error = DictionaryCodableError.tryValue) throws -> Value { 820 | guard let value = self[key] else { throw error } 821 | return value 822 | } 823 | } 824 | 825 | struct AnyCodingKey: CodingKey { 826 | public var stringValue: String 827 | public var intValue: Int? 828 | 829 | public init?(stringValue: String) { 830 | self.stringValue = stringValue 831 | self.intValue = nil 832 | } 833 | 834 | public init?(intValue: Int) { 835 | self.stringValue = "\(intValue)" 836 | self.intValue = intValue 837 | } 838 | 839 | init(index: Int) { 840 | self.stringValue = "Index \(index)" 841 | self.intValue = index 842 | } 843 | 844 | static let `super` = AnyCodingKey(stringValue: "super")! 845 | } 846 | 847 | func dynamicEqual(lhs: Any?, rhs: Any?) -> Bool { 848 | if let lhs = lhs as? NSNumber, let rhs = rhs as? NSNumber { 849 | return lhs == rhs 850 | } 851 | if let lhs = lhs as? String, let rhs = rhs as? String { 852 | return lhs == rhs 853 | } 854 | if let lhs = lhs as? NSArray, let rhs = rhs as? NSArray { 855 | return lhs == rhs 856 | } 857 | if let lhs = lhs as? NSDate, let rhs = rhs as? NSDate { 858 | return lhs == rhs 859 | } 860 | return false 861 | } 862 | 863 | func dynamicEncode(value: Any, encoder: Encoder) throws { 864 | // TODO 865 | } 866 | 867 | private func isEqual(type: T.Type, a: Any, b: Any) -> Bool { 868 | guard let a = a as? T, let b = b as? T else { return false } 869 | return a == b 870 | } 871 | -------------------------------------------------------------------------------- /Sources/Store/Serialization/Diffing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | public let sharedLogger = Logger(label: "io.store") 5 | 6 | @dynamicMemberLookup 7 | public final class Query { 8 | private var segments: [String] = [] 9 | private let signpostDiff: SignpostDiff 10 | 11 | init(signpostDiff: SignpostDiff) { 12 | self.signpostDiff = signpostDiff 13 | } 14 | 15 | public subscript(dynamicMember member: String) -> Query { 16 | segments.append(member) 17 | return self 18 | } 19 | 20 | /// Returns a property/property format for a given keypath format. 21 | public func toKeyPath() -> FlatEncoding.KeyPath { 22 | FlatEncoding.KeyPath(segments: segments) 23 | } 24 | 25 | /// Returns `true` if the key path was added in this signpost store update, `false` otherwise. 26 | public func toPropertyDiff() -> PropertyDiff { 27 | signpostDiff.diffs[self.toKeyPath()] ?? .unspecified 28 | } 29 | } 30 | 31 | /// A collection of changes associated to a signpost store update. 32 | @frozen public struct SignpostDiff { 33 | /// The set of (`path`, `value`) that has been **added**/**removed**/**changed**. 34 | /// 35 | /// e.g. ``` { 36 | /// user/name: , 37 | /// user/lastname: , 38 | /// tokens/1: , 39 | /// } ``` 40 | public var allDiffs: [String: PropertyDiff] { 41 | var dictionary: [String: PropertyDiff] = [:] 42 | for (key, value) in diffs { 43 | dictionary[key.path] = value 44 | } 45 | return dictionary 46 | } 47 | /// Internal storage. 48 | let diffs: [FlatEncoding.KeyPath: PropertyDiff] 49 | 50 | /// Reference to the signpost update that cause this change. 51 | public let signpost: String 52 | 53 | /// Returns the `diffs` map encoded as **JSON** data. 54 | public var json: Data { 55 | (try? sharedJSONEncoder.encode(diffs)) ?? Data() 56 | } 57 | 58 | /// Queries the diff collection for a keypath change. 59 | public func query(_ keyPath: (Query) -> Query) -> PropertyDiff { 60 | keyPath(Query(signpostDiff: self)).toPropertyDiff() 61 | } 62 | 63 | public init(signpost: String, diffs: [FlatEncoding.KeyPath: PropertyDiff]) { 64 | self.signpost = signpost 65 | self.diffs = diffs 66 | } 67 | } 68 | 69 | /// Represent a property change. 70 | /// A change can be an **addition**, a **removal** or a **value change**. 71 | public enum PropertyDiff { 72 | case added(new: Codable?) 73 | case changed(old: Codable?, new: Codable?) 74 | case removed 75 | case unspecified 76 | 77 | /// Returns `true` if the key path was added in this signpost update, `false` otherwise. 78 | public func isAdded() -> Bool { 79 | switch self { 80 | case .added: return true 81 | default: return false 82 | } 83 | } 84 | 85 | /// Returns `true` if the key path was removed in this signpost update, `false` otherwise. 86 | public func isRemoved() -> Bool { 87 | switch self { 88 | case .removed: return true 89 | default: return false 90 | } 91 | } 92 | 93 | /// Returns `true` if the key path was changed in this signpost update, `false` otherwise. 94 | public func isChanged() -> Bool { 95 | switch self { 96 | case .changed: return true 97 | default: return false 98 | } 99 | } 100 | 101 | /// Returns the new value if the property was just added in this signpost update, `nil` otherwise. 102 | public func asNewAddedValue() -> T? { 103 | switch self { 104 | case .added(let value): return value as? T 105 | default: return nil 106 | } 107 | } 108 | 109 | /// Returns the tuple (`old`, `new`) if the value of the property changed in this signpost update. 110 | public func asNewAddedValue() -> (T, T)? { 111 | switch self { 112 | case .changed(let old, let new): 113 | guard let newValue = new as? T, let oldValue = old as? T else { return nil } 114 | return (oldValue, newValue) 115 | default: return nil 116 | } 117 | } 118 | } 119 | 120 | /// Flatten down the dictionary into a path/value map. 121 | /// e.g. `{user: {name: "John", lastname: "Appleseed"}, tokens: ["foo", "bar"]` 122 | /// turns into ``` { 123 | /// user/name: "John", 124 | /// user/lastname: "Appleseed", 125 | /// tokens/0: "foo", 126 | /// tokens/1: "bar" 127 | /// } ``` 128 | public func flatten(encodedObject: EncodedDictionary) -> FlatEncoding.Dictionary { 129 | var result: FlatEncoding.Dictionary = [:] 130 | FlatEncoding.flatten(path: "", node: .dictionary(encodedObject), result: &result) 131 | return result 132 | } 133 | 134 | public enum FlatEncoding { 135 | /// A flat non-nested dictionary. 136 | /// This representation is very efficient for object diffing. 137 | /// - note: Arrays are represented by indices in the path. 138 | /// e.g. ``` { 139 | /// user/name: "John", 140 | /// user/lastname: "Appleseed", 141 | /// tokens/0: "foo", 142 | /// tokens/1: "bar" 143 | /// } ``` 144 | public typealias Dictionary = [KeyPath: Codable?] 145 | 146 | /// Represent a path in a `FlatEncoding` dictionary. 147 | public struct KeyPath: Encodable, Equatable, Hashable { 148 | /// KeyPath components separator. 149 | /// e.g. `user/address/street` or `tokens/0`. 150 | static let separator = "/" 151 | 152 | /// All of the individual components of this KeyPath. 153 | public var segments: [String] 154 | 155 | /// Whether this is an empty KeyPath or not. 156 | public var isEmpty: Bool { 157 | segments.isEmpty 158 | } 159 | 160 | /// The raw string for this KeyPath. 161 | public let path: String 162 | 163 | /// Strips off the first segment and returns a pair consisting of the first segment and the 164 | /// remaining key path. 165 | /// Returns `nil` if the key path has no segments. 166 | public func pop() -> (head: String, tail: KeyPath)? { 167 | guard !isEmpty else { return nil } 168 | var tail = segments 169 | let head = tail.removeFirst() 170 | return (head, KeyPath(segments: tail)) 171 | } 172 | 173 | /// Construct a new KeyPath from a array of components. 174 | public init(segments: [String]) { 175 | self.segments = segments 176 | self.path = segments.joined(separator: KeyPath.separator) 177 | } 178 | 179 | /// Constructs a new FlatEncoding KeyPath from a given string. 180 | /// - note: Returns `nil` if the string is in not in the format `PATH(/PATH)*` where `PATH` is 181 | /// a valid descriptor, like for example: `foo`, `foo/bar`, `foo/1/bar`. 182 | public init?(_ string: String) { 183 | guard 184 | string.range( 185 | of: "(([a-zA-Z0-9])+(\\/?))*", 186 | options: [.regularExpression, .anchored]) != nil 187 | else { 188 | return nil 189 | } 190 | path = string 191 | segments = string.components(separatedBy: KeyPath.separator) 192 | } 193 | 194 | /// Encodes this value into the given encoder. 195 | public func encode(to encoder: Encoder) throws { 196 | return try path.encode(to: encoder) 197 | } 198 | 199 | /// The raw string for this KeyPath. 200 | public var description: String { 201 | path 202 | } 203 | } 204 | 205 | // MARK: - Private 206 | 207 | /// Intermediate dictionary representation for `EncodedDictionary` ⇒ `FlatEncoding.Dictionary` 208 | fileprivate enum Node { 209 | case dictionary(_ dictionary: EncodedDictionary) 210 | case array(_ array: [Any]) 211 | } 212 | 213 | /// Private recursive flatten method. 214 | /// - note: See `flatten(encodedObject:)`. 215 | fileprivate static func flatten(path: String, node: Node, result: inout Dictionary) { 216 | let formattedPath = path.isEmpty ? "" : "\(path)\(KeyPath.separator)" 217 | func process(path: String, value: Any) { 218 | if let dictionary = value as? [String: Any] { 219 | flatten(path: path, node: .dictionary(dictionary), result: &result) 220 | } else if let array = value as? [Any] { 221 | flatten(path: path, node: .array(array), result: &result) 222 | } else { 223 | guard let keyPath = KeyPath(path) else { 224 | sharedLogger.error("Malformed FlatEncoding keypath: \(path).") 225 | return 226 | } 227 | result[keyPath] = value as? Codable 228 | } 229 | } 230 | switch node { 231 | case .dictionary(let dictionary): 232 | for (key, value) in dictionary { 233 | process(path: "\(formattedPath)\(key)", value: value) 234 | } 235 | case .array(let array): 236 | for (index, value) in array.enumerated() { 237 | process(path: "\(formattedPath)\(index)", value: value) 238 | } 239 | } 240 | } 241 | } 242 | 243 | /// Shared `JSON` Encoder. 244 | fileprivate let sharedJSONEncoder = JSONEncoder() 245 | 246 | // MARK: - PropertyDiff 247 | 248 | extension PropertyDiff: CustomStringConvertible, Encodable { 249 | 250 | public var description: String { 251 | switch self { 252 | case .added(let new): 253 | return "" 254 | case .changed(let old, let new): 255 | return "" 256 | case .removed: 257 | return "" 258 | case .unspecified: 259 | return "" 260 | } 261 | } 262 | 263 | public var value: Any? { 264 | switch self { 265 | case .added(let new): 266 | return new 267 | case .changed(_, let new): 268 | return new 269 | case .removed: 270 | return nil 271 | case .unspecified: 272 | return nil 273 | } 274 | } 275 | 276 | /// Encodes this value into the given encoder. 277 | public func encode(to encoder: Encoder) throws { 278 | switch self { 279 | case .added(let new): 280 | guard let value = new else { return } 281 | try value.encode(to: encoder) 282 | case .changed(_, let new): 283 | guard let value = new else { return } 284 | try value.encode(to: encoder) 285 | case .removed: 286 | var container = encoder.singleValueContainer() 287 | try container.encodeNil() 288 | case .unspecified: 289 | return 290 | } 291 | } 292 | } 293 | 294 | // MARK: - Extensions 295 | 296 | extension Dictionary where Key == FlatEncoding.KeyPath, Value == PropertyDiff { 297 | 298 | /// Debug description for a change set. 299 | func storeDebugDescription(short: Bool) -> String { 300 | let keys = self.keys.map { $0.path }.sorted() 301 | let threshold = 8 302 | let noChangesLeft = keys.count - threshold 303 | var countChanges = 0 304 | var formats: [String] = [] 305 | for key in keys { 306 | if short && countChanges == threshold { break } 307 | formats.append("\n\t\t· \(key): \(self[FlatEncoding.KeyPath(key)!]!)") 308 | countChanges += 1 309 | } 310 | if short && noChangesLeft > 0 { 311 | formats.append("\n\t\t<<< \(noChangesLeft) more change(s) >>>") 312 | } 313 | return "{\(formats.joined(separator: ", "))\n\t}" 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /Sources/Store/Store/Store+Forwarding.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import Logging 4 | 5 | #if canImport(SwiftUI) 6 | import SwiftUI 7 | #endif 8 | 9 | // MARK: - Extensions 10 | 11 | extension Store: Equatable where T: Equatable { 12 | public static func == (lhs: Store, rhs: Store) -> Bool { 13 | lhs.object.wrappedValue == rhs.object.wrappedValue 14 | } 15 | } 16 | 17 | extension Store: Hashable where T: Hashable { 18 | /// Hashes the essential components of this value by feeding them into the given hasher. 19 | public func hash(into hasher: inout Hasher) { 20 | object.wrappedValue.hash(into: &hasher) 21 | } 22 | } 23 | 24 | extension Store: Identifiable where T: Identifiable { 25 | /// The stable identity of the entity associated with this instance. 26 | public var id: T.ID { object.wrappedValue.id } 27 | } 28 | 29 | //MARK: - forward Publishers 30 | 31 | extension Store where T: PropertyObservableObject { 32 | /// Forwards `ObservableObject.objectWillChangeSubscriber` to this proxy. 33 | public func forwardPropertyObservablePublisher() { 34 | objectSubscriptions.insert( 35 | object.wrappedValue.propertyDidChange.sink { [weak self] change in 36 | self?.propertyDidChange.send(change) 37 | }) 38 | } 39 | } 40 | 41 | extension Store where T: ObservableObject { 42 | /// Forwards `ObservableObject.objectWillChangeSubscriber` to this proxy. 43 | public func forwardObservableObjectPublisher() { 44 | objectSubscriptions.insert( 45 | object.wrappedValue.objectWillChange.sink { [weak self] _ in 46 | guard let self = self else { return } 47 | self.objectDidChange.send() 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Store/Store/Store.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import Logging 4 | 5 | #if canImport(SwiftUI) 6 | import SwiftUI 7 | #endif 8 | 9 | /// Creates an observable store that acts as a proxy for the object passed as argument. 10 | /// 11 | /// Mutations of the wrapped object performed via `get`, `set` or the dynamic keypath subscript 12 | /// are thread-safe and trigger an event through the `objectWillChangeSubscriber` and 13 | /// `propertyDidChangeSubscriber` streams. 14 | @dynamicMemberLookup 15 | @propertyWrapper 16 | final public class Store: ObservableObject, PropertyObservableObject, @unchecked Sendable { 17 | 18 | /// Emits an event whenever any of the wrapped object has been mutated. 19 | public var propertyDidChange = PassthroughSubject() 20 | 21 | /// Subsystem logger. 22 | public private(set) lazy var logger: Logger = { 23 | let type = String(describing: T.self) 24 | let pointer = String(format: "%02x", Unmanaged.passUnretained(self).toOpaque().hashValue) 25 | let label = "io.store.\(type)(\(pointer))" 26 | return Logger(label: label) 27 | }() 28 | 29 | /// The underlying value referenced by the binding variable. 30 | /// This property provides primary access to the value's data. However, you 31 | /// don't access `wrappedValue` directly. 32 | public var wrappedValue: T { object.wrappedValue } 33 | 34 | /// The wrapped object. 35 | var object: Binding 36 | 37 | /// Internal subject used to propagate `objectWillChange` and `propertyDidChange` events. 38 | var objectDidChange = PassthroughSubject() 39 | var objectSubscriptions = Set() 40 | var subscriptions = Set() 41 | 42 | /// Synchronize the access to the wrapped object. 43 | private let objectLock: Locking 44 | 45 | /// Constructs a new proxy for the object passed as argument. 46 | public init( 47 | object: Binding, 48 | objectLock: Locking = UnfairLock(), 49 | options: StoreOptions 50 | ) { 51 | self.object = object 52 | self.objectLock = objectLock 53 | 54 | var objectWillChange = objectDidChange.eraseToAnyPublisher() 55 | let propertyDidChange = propertyDidChange.eraseToAnyPublisher() 56 | 57 | switch options.schedulingStrategy { 58 | case .debounce(let seconds): 59 | objectWillChange = 60 | objectWillChange 61 | .debounce(for: .seconds(seconds), scheduler: options.scheduler) 62 | .eraseToAnyPublisher() 63 | case .throttle(let seconds): 64 | objectWillChange = 65 | objectWillChange 66 | .throttle(for: .seconds(seconds), scheduler: options.scheduler, latest: true) 67 | .eraseToAnyPublisher() 68 | case .none: 69 | objectWillChange = 70 | objectWillChange 71 | .receive(on: options.scheduler) 72 | .eraseToAnyPublisher() 73 | } 74 | 75 | subscriptions.insert( 76 | propertyDidChange.sink { [weak self] in 77 | self?.objectDidChange.send() 78 | let property = $0.debugLabel != nil ? ".\($0.debugLabel!)" : "*" 79 | self?.logger.info("send { propertyDidChange(\(property)) }") 80 | }) 81 | subscriptions.insert( 82 | objectWillChange.sink { [weak self] in 83 | self?.objectWillChange.send() 84 | self?.logger.info("send { objectWillChange }") 85 | }) 86 | } 87 | 88 | convenience public init( 89 | object: Binding, 90 | objectLock: Locking = UnfairLock() 91 | ) { 92 | self.init( 93 | object: object, 94 | objectLock: objectLock, 95 | options: StoreOptions(scheduler: RunLoop.main, schedulingStrategy: .none)) 96 | } 97 | 98 | /// Notifies the subscribers for the wrapped object changes. 99 | private func didSetValue(keyPath: KeyPath, value: V) { 100 | propertyDidChange.send( 101 | AnyPropertyChangeEvent( 102 | object: object.wrappedValue, 103 | keyPath: keyPath, 104 | debugLabel: keyPath.readableFormat)) 105 | } 106 | 107 | /// Read the value of the property for the wrapped object. 108 | public func get(keyPath: KeyPath) -> V { 109 | objectLock.withLock { 110 | object.wrappedValue[keyPath: keyPath] 111 | } 112 | } 113 | 114 | /// Sets a new value for the property at the given keypath in the wrapped object. 115 | public func set(keyPath: WritableKeyPath, value: V, signpost: String = #function) { 116 | let oldValue = objectLock.withLock { () -> V in 117 | let oldValue = object.wrappedValue[keyPath: keyPath] 118 | object.wrappedValue[keyPath: keyPath] = value 119 | return oldValue 120 | } 121 | let keyPathReadableFormat = keyPath.readableFormat ?? "unknown" 122 | let valueChangeFormat = "\(String(describing: oldValue)) ⟶ \(value)" 123 | logger.info("set @ [\(signpost)] .\(keyPathReadableFormat) = { \(valueChangeFormat) }") 124 | didSetValue(keyPath: keyPath, value: value) 125 | } 126 | 127 | /// Perfom a batch update to the wrapped object. 128 | /// The changes applied in the `update` closure are atomic in respect of this store's 129 | /// wrapped object and a single `objectWillChange` event is being published after the update has 130 | /// been applied. 131 | public func performBatchUpdate( 132 | _ update: (inout T) async -> Void, 133 | signpost: String = #function 134 | ) async { 135 | await objectLock.withLock { 136 | await update(&object.wrappedValue) 137 | } 138 | logger.info("performBatchUpdate @ [\(signpost)]") 139 | objectDidChange.send() 140 | } 141 | 142 | /// Returns a binding to one of the properties of the wrapped object. 143 | /// The returned binding can itself be used as the argument for a new store object or can simply 144 | /// be used inside a SwiftUI view. 145 | public func binding(keyPath: WritableKeyPath) -> Binding { 146 | .init( 147 | get: { self.get(keyPath: keyPath) }, 148 | set: { self.set(keyPath: keyPath, value: $0) }) 149 | } 150 | 151 | public subscript(dynamicMember keyPath: KeyPath) -> V { 152 | get { get(keyPath: keyPath) } 153 | } 154 | 155 | public subscript(dynamicMember keyPath: WritableKeyPath) -> V { 156 | get { get(keyPath: keyPath) } 157 | set { set(keyPath: keyPath, value: newValue, signpost: "dynamic_member") } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Store/Store/StoreOptions.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public struct StoreOptions { 5 | 6 | public enum SchedulingStrategy { 7 | /// The time the publisher should wait before publishing an element. 8 | case debounce(Double) 9 | /// The interval at which to find and emit either the most recent expressed in the time system 10 | /// of the scheduler. 11 | case throttle(Double) 12 | /// Events are being emitted as they're sent through the publisher. 13 | case none 14 | } 15 | 16 | /// The scheduler on which this publisher delivers elements 17 | public let scheduler: S 18 | 19 | /// Schedule stratey. 20 | public let schedulingStrategy: SchedulingStrategy 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Store/Utilities/Assign.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// This function is used to copy the values of all enumerable own properties from one or more 4 | /// source struct to a target struct. 5 | /// - note: If the argument is a reference type the same refence is returned. 6 | public func assign(_ value: T, changes: (inout T) -> Void) -> T { 7 | var copy = value 8 | changes(©) 9 | return copy 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Store/Utilities/Partial.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Constructs a type with all properties of T set to optional. This utility will return a type 4 | /// that represents all subsets of a given type. 5 | /// 6 | /// ``` 7 | /// struct Todo { var title: String; var description: String } 8 | /// 9 | /// var partial = Partial { .success(Todo( 10 | /// title: $0.get(\Todo.title, default: "Untitled"), 11 | /// description: $0.get(\Todo.description, default: "No description"))) 12 | /// } 13 | /// 14 | /// partial.title = "A Title" 15 | /// partial.description = "A Description" 16 | /// var todo = try! partial.build().get() 17 | /// 18 | /// partial.description = "Another Descrition" 19 | /// todo = partial.merge(&todo) 20 | /// ``` 21 | /// 22 | @dynamicMemberLookup 23 | public struct Partial { 24 | /// The construction closure invoked by `build()`. 25 | public let create: (Partial) -> Result 26 | 27 | /// All of the values currently set in this partial. 28 | private var keypathToValueMap: [AnyKeyPath: Any] = [:] 29 | 30 | /// All of the `set` commands that will performed once the object is built. 31 | private var keypathToSetValueMap: [AnyKeyPath: (inout T) -> Void] = [:] 32 | 33 | public init(_ create: @escaping (Partial) -> Result) { 34 | self.create = create 35 | } 36 | 37 | /// Use `@dynamicMemberLookup` keypath subscript to store the object configuration and postpone 38 | /// the object construction. 39 | public subscript(dynamicMember keyPath: KeyPath) -> V? { 40 | get { 41 | get(keyPath) 42 | } 43 | set { 44 | keypathToValueMap[keyPath] = newValue 45 | } 46 | } 47 | /// Use `@dynamicMemberLookup` keypath subscript to store the object configuration and postpone 48 | /// the object construction. 49 | /// - note: `WritableKeyPath` properties are set on the object after construction and used 50 | /// by the `merge(:)` function. 51 | public subscript(dynamicMember keyPath: WritableKeyPath) -> V? { 52 | get { 53 | get(keyPath) 54 | } 55 | set { 56 | guard let value = newValue else { 57 | keypathToValueMap.removeValue(forKey: keyPath) 58 | keypathToSetValueMap.removeValue(forKey: keyPath) 59 | return 60 | } 61 | keypathToValueMap[keyPath] = value 62 | keypathToSetValueMap[keyPath] = { object in 63 | object[keyPath: keyPath] = value 64 | } 65 | } 66 | } 67 | 68 | /// Build the target object by using the `createInstanceClosure` passed to the constructor. 69 | public func build() -> Result { 70 | let result = create(self) 71 | switch result { 72 | case .success(var obj): 73 | for (_, setValueClosure) in keypathToSetValueMap { 74 | setValueClosure(&obj) 75 | } 76 | return result 77 | case .failure(_): 78 | return result 79 | } 80 | } 81 | 82 | /// Merge all of the properties currently set in this Partial with the destination object. 83 | public func merge(_ dest: inout T) -> T { 84 | assign(dest) { 85 | for (_, setValueClosure) in self.keypathToSetValueMap { 86 | setValueClosure(&$0) 87 | } 88 | } 89 | } 90 | 91 | /// Returns the value currently set for the given keyPath or an alternative default value. 92 | public func get(_ keyPath: KeyPath, default: V) -> V { 93 | guard let value = keypathToValueMap[keyPath] as? V else { 94 | return `default` 95 | } 96 | return value 97 | } 98 | 99 | /// Returns the value currently set for the given keyPath or `nil` 100 | public func get(_ keyPath: KeyPath) -> V? { 101 | guard let value = keypathToValueMap[keyPath] as? V else { 102 | return nil 103 | } 104 | return value 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/Store/Utilities/PropertyObservable.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol PropertyObservableObject: AnyObject { 5 | /// A publisher that emits when an object property has changed. 6 | var propertyDidChange: PassthroughSubject { get } 7 | } 8 | 9 | /// Represent an object mutation. 10 | public struct AnyPropertyChangeEvent { 11 | /// The proxy's wrapped value. 12 | public let object: Any 13 | 14 | /// The mutated keyPath. 15 | public let keyPath: AnyKeyPath? 16 | 17 | /// Optional debug label for this event. 18 | public let debugLabel: String? 19 | 20 | public init(object: Any, keyPath: AnyKeyPath? = nil, debugLabel: String? = nil) { 21 | self.object = object 22 | self.keyPath = keyPath 23 | self.debugLabel = debugLabel 24 | } 25 | 26 | /// Returns the tuple `object, value` if this property change matches the `keyPath` passed as 27 | /// argument. 28 | public func match(keyPath: KeyPath) -> (T, V)? { 29 | guard self.keyPath === keyPath, let obj = self.object as? T else { 30 | return nil 31 | } 32 | return (obj, obj[keyPath: keyPath]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Store/Utilities/ReadOnly+Forwarding.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension ReadOnly where T: PropertyObservableObject { 5 | /// Forwards the `ObservableObject.objectWillChangeSubscriber` to this proxy object. 6 | public func forwardPropertyObservableObject() { 7 | propertyDidChangeSubscriber = wrappedValue.propertyDidChange.sink { [weak self] change in 8 | self?.propertyDidChange.send(change) 9 | } 10 | } 11 | } 12 | 13 | extension ReadOnly where T: ObservableObject { 14 | /// Forwards the `ObservableObject.objectWillChangeSubscriber` to this proxy object. 15 | public func forwardObservableObject() { 16 | objectWillChangeSubscriber = wrappedValue.objectWillChange.sink { [weak self] change in 17 | self?.objectWillChange.send() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Store/Utilities/ReadOnly.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | /// Constructs a type with all properties of the given generic type `T` set to readonly, 5 | /// meaning that the properties of the constructed type cannot be reassigned. 6 | /// 7 | /// - note: A read-only object can propagate change events if the wrapped type ia an 8 | /// `ObservableObject` by calling `propagateObservableObject` at construction time. 9 | /// 10 | /// ``` 11 | /// struct Todo { var title: String; var description: String } 12 | /// let todo = Todo(title: "A Title", description: "A Description") 13 | /// let readOnlyTodo = ReadOnly(todo) 14 | /// readOnlyTodo.title // "A title" 15 | /// ``` 16 | /// 17 | @dynamicMemberLookup 18 | @propertyWrapper 19 | open class ReadOnly: ObservableObject, PropertyObservableObject { 20 | public var propertyDidChange = PassthroughSubject() 21 | public private(set) var wrappedValue: T 22 | 23 | // Observable internals. 24 | var objectWillChangeSubscriber: Cancellable? 25 | var propertyDidChangeSubscriber: Cancellable? 26 | 27 | /// Constructs a new read-only proxy for the object passed as argument. 28 | init(object: T) { 29 | wrappedValue = object 30 | } 31 | 32 | public func read(keyPath: KeyPath) -> V { 33 | wrappedValue[keyPath: keyPath] 34 | } 35 | 36 | /// Use `@dynamicMemberLookup` keypath subscript to forward the value of the proxied object. 37 | public subscript(dynamicMember keyPath: KeyPath) -> V { 38 | wrappedValue[keyPath: keyPath] 39 | } 40 | } 41 | 42 | //MARK: - forward Publishers 43 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import StoreTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += StoreTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/StoreTests/StoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | import SwiftUI 4 | @testable import Store 5 | 6 | final class StoreTests: XCTestCase { 7 | var subscriber: Cancellable? 8 | 9 | func testInitialization() { 10 | var testData = TestData() 11 | let store = Store(object: Binding(get: { testData }, set: { testData = $0 })) 12 | XCTAssert(store.constant == 1337) 13 | XCTAssert(store.label == "initial") 14 | XCTAssert(store.number == 42) 15 | store.number = 1 16 | store.label = "change" 17 | XCTAssert(store.number == 1) 18 | XCTAssert(store.label == "change") 19 | } 20 | 21 | func testStorePropertyDidChange() { 22 | var testData = TestData() 23 | let store = Store(object: Binding(get: { testData }, set: { testData = $0 })) 24 | let expectation = XCTestExpectation(description: "didChangeEvent") 25 | 26 | subscriber = store.propertyDidChange.sink { change in 27 | XCTAssert(store.label == "changed") 28 | expectation.fulfill() 29 | } 30 | store.label = "changed" 31 | wait(for: [expectation], timeout: 1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/StoreTests/SupportTypes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct TestData { 4 | let constant = 1337 5 | var label = "initial" 6 | var number = 42 7 | } 8 | -------------------------------------------------------------------------------- /Tests/StoreTests/UtilitiesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | import SwiftUI 4 | @testable import Store 5 | 6 | @available(OSX 10.15, iOS 13.0, *) 7 | final class UtilitiesTests: XCTestCase { 8 | var subscriber: Cancellable? 9 | 10 | func testReadOnly() { 11 | let proxy = ReadOnly(object: TestData()) 12 | XCTAssert(proxy.constant == 1337) 13 | XCTAssert(proxy.label == "initial") 14 | XCTAssert(proxy.number == 42) 15 | } 16 | 17 | func testPartial() { 18 | struct Todo { var title: String; var description: String } 19 | var partial = Partial { .success(Todo( 20 | title: $0.get(\Todo.title, default: "Untitled"), 21 | description: $0.get(\Todo.description, default: "No description"))) 22 | } 23 | partial.title = "A Title" 24 | partial.description = "A Description" 25 | XCTAssert(partial.title == "A Title") 26 | XCTAssert(partial.description == "A Description") 27 | var todo = try! partial.build().get() 28 | XCTAssert(todo.title == "A Title") 29 | XCTAssert(todo.description == "A Description") 30 | partial.description = "Another Descrition" 31 | XCTAssert(partial.description == "Another Descrition") 32 | todo = partial.merge(&todo) 33 | XCTAssert(todo.title == "A Title") 34 | XCTAssert(todo.description == "Another Descrition") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/StoreTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(StoreTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /docs/store_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexdrone/Store/fb51754e8f88a0ab99c7994d0d26d3374155f1fc/docs/store_logo.png -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | swift-format -m format -i -r Sources/ --------------------------------------------------------------------------------