diff --git a/Sources/Verge/Derived/Derived.swift b/Sources/Verge/Derived/Derived.swift index b76f1f4fc7..9ce0ea86ad 100644 --- a/Sources/Verge/Derived/Derived.swift +++ b/Sources/Verge/Derived/Derived.swift @@ -19,9 +19,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import class Foundation.NSMapTable -import class Foundation.NSString - #if canImport(Combine) import Combine #endif @@ -32,6 +29,20 @@ public protocol DerivedType: StoreType { func asDerived() -> Derived } +public protocol DerivedMaking { + + associatedtype State: Equatable + + // TODO: Remove + var state: Changes { get } + + func derived( + _ pipeline: Pipeline, + queue: some TargetQueueType + ) -> Derived where Pipeline.Input == Changes + +} + /** A container object that provides the current value and changes from the source Store. @@ -110,8 +121,8 @@ public class Derived: Store, DerivedType, @unche } // TODO: Take over state.modification & state.mutation - indirectSelf.commit("Derived") { - $0._transaction.isDerivedFromUpstream = true + indirectSelf._receive { + $1.isDerivedFromUpstream = true $0.append(traces: value.traces) $0.replace(with: newState) } @@ -218,7 +229,7 @@ extension Derived where Value : Equatable { receive: @escaping (Value) -> Void ) -> StoreSubscription { sinkState(dropsFirst: dropsFirst, queue: queue) { (changes) in - changes.ifChanged { value in + changes.ifChanged().do { value in receive(value) } } @@ -235,7 +246,7 @@ extension Derived where Value : Equatable { receive: @escaping @MainActor (Value) -> Void ) -> StoreSubscription { sinkState(dropsFirst: dropsFirst, queue: queue) { @MainActor changes in - changes.ifChanged { value in + changes.ifChanged().do { value in receive(value) } } diff --git a/Sources/Verge/Library/InoutRef.swift b/Sources/Verge/Library/InoutRef.swift index 87caf0c948..c0ae4c815c 100644 --- a/Sources/Verge/Library/InoutRef.swift +++ b/Sources/Verge/Library/InoutRef.swift @@ -117,8 +117,6 @@ public final class InoutRef { } } - public var _transaction: Transaction = .init() - // MARK: - Initializers /** diff --git a/Sources/Verge/Store/DispatcherType.swift b/Sources/Verge/Store/DispatcherType.swift index b040c420f7..3a256a74ad 100644 --- a/Sources/Verge/Store/DispatcherType.swift +++ b/Sources/Verge/Store/DispatcherType.swift @@ -298,7 +298,7 @@ extension DispatcherType { ) return try store.asStore()._receive( - mutation: { state -> Result in + mutation: { state, transaction -> Result in try state.map(keyPath: scope) { (ref: inout InoutRef) -> Result in ref.append(trace: trace) return try mutation(&ref) @@ -335,7 +335,7 @@ extension DispatcherType { mutation: (inout InoutRef) throws -> Result ) rethrows -> Result where Scope == WrappedStore.State { return try store.asStore()._receive( - mutation: { ref -> Result in + mutation: { ref, transaction -> Result in ref.append(trace: trace) return try mutation(&ref) } diff --git a/Sources/Verge/Store/IsolatedStore.swift b/Sources/Verge/Store/IsolatedStore.swift new file mode 100644 index 0000000000..fa3daa6674 --- /dev/null +++ b/Sources/Verge/Store/IsolatedStore.swift @@ -0,0 +1,544 @@ + +@MainActor +public final class MainActorStore: DerivedMaking, Sendable, MainActorStoreDriverType, ObservableObject { + + public typealias State = State + + public nonisolated var objectWillChange: ObservableObjectPublisher { + backingStore.objectWillChange + } + + public nonisolated var store: MainActorStore { self } + + public typealias Scope = State + + public nonisolated var state: Changes { + backingStore.state + } + + @_spi(Internal) + public let backingStore: Store + + public nonisolated init(initialState: State) { + self.backingStore = .init(initialState: initialState, storeOperation: .atomic(.init()), logger: nil, sanitizer: nil) + } + + public func commit( + mutation: (inout InoutRef) throws -> Result + ) rethrows -> Result { + + try backingStore._receive(mutation: { state, _ in try mutation(&state) }) + + } + + public func commit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) rethrows -> Result { + try backingStore._receive(mutation: mutation) + } + + /// Send activity + /// - Parameter activity: + public func send( + _ activity: Activity, + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line + ) { + let trace = ActivityTrace( + name: "", + file: file.description, + function: function.description, + line: line + ) + + backingStore._send(activity: activity, trace: trace) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + @_disfavoredOverload + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: some TargetQueueType, + receive: @escaping (Changes) -> Void + ) -> StoreSubscription { + return backingStore + .sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: MainActorTargetQueue = .mainIsolated(), + receive: @escaping @MainActor (Changes) -> Void + ) -> StoreSubscription { + return backingStore + .sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) + } + + public nonisolated func derived( + _ pipeline: Pipeline, + queue: some TargetQueueType = .passthrough + ) -> Derived where Pipeline.Input == Changes { + + let derived = Derived( + get: pipeline, + set: { _ in /* no operation as read only */}, + initialUpstreamState: backingStore.state, + subscribeUpstreamState: { callback in + backingStore.asStore()._primitive_sinkState( + dropsFirst: true, + queue: queue, + receive: callback + ) + }, + retainsUpstream: nil + ) + + backingStore.asStore().onDeinit { [weak derived] in + derived?.invalidate() + } + + return derived + } +} + +public final class AsyncStore: DerivedMaking, Sendable, AsyncStoreDriverType, ObservableObject { + + public typealias State = State + + public var objectWillChange: ObservableObjectPublisher { + backingStore.objectWillChange + } + + public var store: AsyncStore { self } + + public typealias Scope = State + + public var state: Changes { + backingStore.state + } + + public var latestState: Changes { + get async { + await writer.perform { _ in + backingStore.state + } + } + } + + @_spi(Internal) + public let backingStore: Store + + private let writer: AsyncStoreOperator = .init() + + public init(initialState: State) { + self.backingStore = .init(initialState: initialState, storeOperation: .atomic(.init()), logger: nil, sanitizer: nil) + } + + public func backgroundCommit( + mutation: (inout InoutRef) throws -> Result + ) async rethrows -> Result { + + try await writer.perform { _ in + try backingStore._receive(mutation: { state, _ in try mutation(&state) }) + } + + } + + public func backgroundCommit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) async rethrows -> Result { + + try await writer.perform { _ in + try backingStore._receive(mutation: mutation) + } + + } + + /// Send activity + /// - Parameter activity: + public func send( + _ activity: Activity, + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line + ) { + let trace = ActivityTrace( + name: "", + file: file.description, + function: function.description, + line: line + ) + + backingStore._send(activity: activity, trace: trace) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + @_disfavoredOverload + public func sinkState( + dropsFirst: Bool = false, + queue: some TargetQueueType, + receive: @escaping (Changes) -> Void + ) -> StoreSubscription { + return backingStore + .sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + public func sinkState( + dropsFirst: Bool = false, + queue: MainActorTargetQueue = .mainIsolated(), + receive: @escaping @MainActor (Changes) -> Void + ) -> StoreSubscription { + return backingStore + .sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) + } + + public func derived( + _ pipeline: Pipeline, + queue: some TargetQueueType = .passthrough + ) -> Derived where Pipeline.Input == Changes { + + let derived = Derived( + get: pipeline, + set: { _ in /* no operation as read only */}, + initialUpstreamState: backingStore.state, + subscribeUpstreamState: { callback in + backingStore.asStore()._primitive_sinkState( + dropsFirst: true, + queue: queue, + receive: callback + ) + }, + retainsUpstream: nil + ) + + backingStore.asStore().onDeinit { [weak derived] in + derived?.invalidate() + } + + return derived + } + +} + + +private actor AsyncStoreOperator { + + init() { + + } + + func perform(_ operation: (isolated AsyncStoreOperator) throws -> R) rethrows -> R { + try operation(self) + } + +} + +// MARK: - + +@MainActor +public protocol MainActorStoreDriverType { + + associatedtype State: StateType + associatedtype Activity + + associatedtype Scope: Equatable = State + + nonisolated var store: MainActorStore { get } + nonisolated var scope: WritableKeyPath { get } + + func commit( + mutation: (inout InoutRef) throws -> Result + ) rethrows -> Result + + func commit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) rethrows -> Result +} + +extension MainActorStoreDriverType { + /// A state that cut out from root-state with the scope key path. + public var state: Changes { + store.state.map { $0[keyPath: scope] } + } + + public var rootState: Changes { + return store.state + } + + /// Send activity + /// - Parameter activity: + public func send( + _ activity: Activity, + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line + ) { + store.send(activity, file, function, line) + } + + /// Run Mutation that created inline + /// + /// Throwable + public func commit( + mutation: (inout InoutRef) throws -> Result + ) rethrows -> Result { + + return try store.commit { ref in + try ref.map(keyPath: scope, perform: mutation) + } + + } + + public func commit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) rethrows -> Result { + + return try store.commit { ref, transaction in + try ref.map(keyPath: scope, perform: { + try mutation(&$0, &transaction) + }) + } + + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + @_disfavoredOverload + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: some TargetQueueType, + receive: @escaping (Changes) -> Void + ) -> StoreSubscription { + return store.sinkState(dropsFirst: dropsFirst, queue: queue, receive: { state in + receive(state.map({ $0[keyPath: scope] })) + }) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: MainActorTargetQueue = .mainIsolated(), + receive: @escaping @MainActor (Changes) -> Void + ) -> StoreSubscription { + return store.sinkState(dropsFirst: dropsFirst, queue: queue, receive: { state in + receive(state.map({ $0[keyPath: scope] })) + }) + } +} + +extension MainActorStoreDriverType where Scope == State { + + public nonisolated var scope: WritableKeyPath { + \State.self + } + + /// Run Mutation that created inline + /// + /// Throwable + public func commit( + mutation: (inout InoutRef) throws -> Result + ) rethrows -> Result { + + return try store.commit(mutation: mutation) + + } + +} + +// MARK: - + +public protocol AsyncStoreDriverType { + + associatedtype State: StateType + associatedtype Activity + + associatedtype Scope: Equatable = State + + var store: AsyncStore { get } + var scope: WritableKeyPath { get } + + func backgroundCommit( + mutation: (inout InoutRef) throws -> Result + ) async rethrows -> Result + + func backgroundCommit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) async rethrows -> Result +} + +extension AsyncStoreDriverType { + /// A state that cut out from root-state with the scope key path. + public nonisolated var state: Changes { + store.state.map { $0[keyPath: scope] } + } + + public nonisolated var rootState: Changes { + return store.state + } + + /// Send activity + /// - Parameter activity: + public func send( + _ activity: Activity, + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line + ) { + store.send(activity, file, function, line) + } + + /// Run Mutation that created inline + /// + /// Throwable + public func backgroundCommit( + mutation: (inout InoutRef) throws -> Result + ) async rethrows -> Result { + + return try await store.backgroundCommit { ref in + try ref.map(keyPath: scope, perform: mutation) + } + + } + + /// Run Mutation that created inline + /// + /// Throwable + public func backgroundCommit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) async rethrows -> Result { + + return try await store.backgroundCommit { ref, transaction in + try ref.map(keyPath: scope, perform: { + try mutation(&$0, &transaction) + }) + } + + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + @_disfavoredOverload + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: some TargetQueueType, + receive: @escaping (Changes) -> Void + ) -> StoreSubscription { + return store.sinkState(dropsFirst: dropsFirst, queue: queue, receive: { state in + receive(state.map({ $0[keyPath: scope] })) + }) + } + + /** + Subscribe the state that scoped + + First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. + + - Parameters: + - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. + - queue: Specify a queue to receive changes object. + - Returns: A subscriber that performs the provided closure upon receiving values. + */ + public nonisolated func sinkState( + dropsFirst: Bool = false, + queue: MainActorTargetQueue = .mainIsolated(), + receive: @escaping @MainActor (Changes) -> Void + ) -> StoreSubscription { + return store.sinkState(dropsFirst: dropsFirst, queue: queue, receive: { state in + receive(state.map({ $0[keyPath: scope] })) + }) + } +} + +extension AsyncStoreDriverType where Scope == State { + + public var scope: WritableKeyPath { + \State.self + } + + /// Run Mutation that created inline + /// + /// Throwable + public func backgroundCommit( + mutation: (inout InoutRef) throws -> Result + ) async rethrows -> Result { + + return try await store.backgroundCommit(mutation: mutation) + + } + + /// Run Mutation that created inline + /// + /// Throwable + public func backgroundCommit( + mutation: (inout InoutRef, inout Transaction) throws -> Result + ) async rethrows -> Result { + + return try await store.backgroundCommit(mutation: mutation) + + } +} + diff --git a/Sources/Verge/Store/StateType.swift b/Sources/Verge/Store/StateType.swift index d1976fed34..2e41136331 100644 --- a/Sources/Verge/Store/StateType.swift +++ b/Sources/Verge/Store/StateType.swift @@ -32,7 +32,10 @@ public protocol StateType: Equatable { It's better to use it for better performance to get the value rather than using computed property. */ @Sendable - static func reduce(modifying: inout InoutRef, current: Changes) + static func reduce( + modifying: inout InoutRef, + current: Changes + ) } extension StateType { diff --git a/Sources/Verge/Store/Store.swift b/Sources/Verge/Store/Store.swift index 2e1ad738af..7141d85f15 100644 --- a/Sources/Verge/Store/Store.swift +++ b/Sources/Verge/Store/Store.swift @@ -80,7 +80,7 @@ actor Writer { /// ``` /// You may use also `StoreWrapperType` to define State and Activity as inner types. /// -open class Store: EventEmitter<_StoreEvent>, CustomReflectable, StoreType, DispatcherType, @unchecked Sendable { +open class Store: EventEmitter<_StoreEvent>, CustomReflectable, StoreType, DispatcherType, DerivedMaking, @unchecked Sendable { public var scope: WritableKeyPath = \State.self @@ -119,12 +119,6 @@ open class Store: EventEmitter<_StoreEvent, AnyObject> = .init(keyOptions: [.copyIn, .objectPersonality], valueOptions: [.weakMemory]) - - @_spi(NormalizedStorage) - @VergeConcurrency.AtomicLazy public var _nonnull_derivedCache: NSMapTable, AnyObject> = .init(keyOptions: [.copyIn, .objectPersonality], valueOptions: [.weakMemory]) - // MARK: - Deinit deinit { @@ -198,7 +192,7 @@ open class Store: EventEmitter<_StoreEvent` inside the closure.) @inline(__always) func _receive( - mutation: (inout InoutRef) throws -> Result + mutation: (inout InoutRef, inout Transaction) throws -> Result ) rethrows -> Result { let signpost = VergeSignpostTransaction("Store.commit") @@ -465,9 +459,10 @@ extension Store { let updateResult = try withUnsafeMutablePointer(to: ¤t) { (stateMutablePointer) -> UpdateResult in + var transaction = Transaction() var inoutRef = InoutRef.init(stateMutablePointer) - let result = try mutation(&inoutRef) + let result = try mutation(&inoutRef, &transaction) valueFromMutation = result /** @@ -496,9 +491,12 @@ extension Store { with: stateMutablePointer.pointee, from: inoutRef.traces, modification: inoutRef.modification ?? .indeterminate, - transaction: inoutRef._transaction + transaction: transaction + ) + middleware.modify( + modifyingState: &inoutRef, + current: intermediate ) - middleware.modify(modifyingState: &inoutRef, current: intermediate) } /** @@ -508,7 +506,7 @@ extension Store { with: stateMutablePointer.pointee, from: inoutRef.traces, modification: inoutRef.modification ?? .indeterminate, - transaction: inoutRef._transaction + transaction: transaction ) if __sanitizer__.isRecursivelyCommitDetectionEnabled { diff --git a/Sources/Verge/Store/StoreType+BindingDerived.swift b/Sources/Verge/Store/StoreType+BindingDerived.swift index c058722c67..d16126f3df 100644 --- a/Sources/Verge/Store/StoreType+BindingDerived.swift +++ b/Sources/Verge/Store/StoreType+BindingDerived.swift @@ -85,10 +85,11 @@ extension DispatcherType { let derived = BindingDerived.init( get: BindingDerivedPipeline(backingPipeline: pipeline), set: { [weak self] state in - self?.store.asStore().commit(name, file, function, line) { - $0._transaction.isFromBindingDerived = true - set(&$0, state) - } + self?.store.asStore() + ._receive { + $1.isFromBindingDerived = true + set(&$0, state) + } }, initialUpstreamState: store.asStore().state, subscribeUpstreamState: { callback in diff --git a/Sources/VergeNormalization/Tables/Tables.Hash.swift b/Sources/VergeNormalization/Tables/Tables.Hash.swift index cf0154d3e2..6a0b54cb13 100644 --- a/Sources/VergeNormalization/Tables/Tables.Hash.swift +++ b/Sources/VergeNormalization/Tables/Tables.Hash.swift @@ -5,7 +5,7 @@ extension Tables { /** A table that stores entities with hash table. */ - public struct Hash: TableType { + public struct Hash: TableType, Sendable { public typealias Entity = Entity diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index a16e1541b5..e6215b8ac1 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -2,11 +2,29 @@ import Foundation @_spi(NormalizedStorage) import Verge @_spi(Internal) import Verge +import Verge public enum NormalizedStorageError: Swift.Error { case notFoundEntityToMakeDerived } +extension MainActorStore { + + @MainActor + public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { + return .init(store: self, storageSelector: selector) + } + +} + +extension AsyncStore { + + public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { + return .init(store: self, storageSelector: selector) + } + +} + extension StoreType { public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { @@ -19,7 +37,7 @@ extension StoreType { The entrypoint to make Derived object from the storage */ public struct NormalizedStoragePath< - Store: DispatcherType, + Store: DerivedMaking & AnyObject, _StorageSelector: StorageSelector >: ~Copyable where Store.State == _StorageSelector.Source { @@ -31,6 +49,7 @@ public struct NormalizedStoragePath< store: Store, storageSelector: _StorageSelector ) { + self.store = store self.storageSelector = storageSelector } @@ -44,13 +63,33 @@ public struct NormalizedStoragePath< tableSelector: selector ) } + + /** + Make a new Derived of a composed object from the storage. + This is an effective way to resolving relationship entities into a single object. it's like SQLite's view. + + ``` + store.normalizedStorage(.keyPath(\.db)).derived { + MyComposed( + book: $0.book.find(...) + author: $0.author.find(...) + ) + } + ``` + + This Derived makes a new composed object if the storage has updated. + There is not filters for entity tables so that Derived possibly makes a new object if not related entity has updated. + */ + public func derived(query: @escaping @Sendable (Self.Storage) -> Composed) -> Derived { + return store.derived(QueryPipeline(storageSelector: storageSelector, query: query), queue: .passthrough) + } } /** The entrypoint to make Derived object from the specific table. */ public struct NormalizedStorageTablePath< - Store: StoreType, + Store: DerivedMaking & AnyObject, _StorageSelector: StorageSelector, _TableSelector: TableSelector >: ~Copyable where _StorageSelector.Storage == _TableSelector.Storage, Store.State == _StorageSelector.Source { @@ -224,7 +263,7 @@ public struct NormalizedStorageTablePath< } } -extension StoreType { +extension DerivedMaking { fileprivate func derivedEntity< _StorageSelector: StorageSelector, @@ -238,30 +277,13 @@ extension StoreType { _StorageSelector.Source == Self.State { - return asStore().$_derivedCache.modify { cache in - - typealias _Derived = Derived.Output> - - let key = KeyObject(content: AnyHashable(selector)) - - if let cached = cache.object(forKey: key) { - return cached as! _Derived - } else { - - let new = asStore().derived( - SingleEntityPipeline( - targetIdentifier: entityID, - selector: selector - ), - queue: .passthrough - ) - - cache.setObject(new, forKey: key) - - return new as _Derived - } - - } + return derived( + SingleEntityPipeline( + targetIdentifier: entityID, + selector: selector + ), + queue: .passthrough + ) } @@ -277,30 +299,13 @@ extension StoreType { _StorageSelector.Source == Self.State { - return asStore().$_nonnull_derivedCache.modify { cache in - - typealias _Derived = Derived.Output> - - let key = KeyObject(content: AnyHashable(selector)) - - if let cached = cache.object(forKey: key) { - return cached as! _Derived - } else { - - let new = asStore().derived( - NonNullSingleEntityPipeline( - initialEntity: entity, - selector: selector - ), - queue: .passthrough - ) - - cache.setObject(new, forKey: key) - - return new as _Derived - } - - } + return derived( + NonNullSingleEntityPipeline( + initialEntity: entity, + selector: selector + ), + queue: .passthrough + ) } } diff --git a/Sources/VergeNormalizationDerived/Query.swift b/Sources/VergeNormalizationDerived/Query.swift index 385815580c..80fe7a0357 100644 --- a/Sources/VergeNormalizationDerived/Query.swift +++ b/Sources/VergeNormalizationDerived/Query.swift @@ -1,28 +1,5 @@ -extension NormalizedStoragePath { - - /** - Make a new Derived of a composed object from the storage. - This is an effective way to resolving relationship entities into a single object. it's like SQLite's view. - - ``` - store.normalizedStorage(.keyPath(\.db)).derived { - MyComposed( - book: $0.book.find(...) - author: $0.author.find(...) - ) - } - ``` - - This Derived makes a new composed object if the storage has updated. - There is not filters for entity tables so that Derived possibly makes a new object if not related entity has updated. - */ - public func derived(query: @escaping @Sendable (Self.Storage) -> Composed) -> Derived { - return store.derived(Pipeline(storageSelector: storageSelector, query: query), queue: .passthrough) - } -} - -private struct Pipeline< +struct QueryPipeline< _StorageSelector: StorageSelector, Output >: PipelineType, Sendable { diff --git a/Tests/DemoState.swift b/Tests/DemoState.swift index c4a04114a1..42a2341682 100644 --- a/Tests/DemoState.swift +++ b/Tests/DemoState.swift @@ -18,7 +18,7 @@ struct OnEquatable: Equatable, Sendable { let id = UUID() } -struct DemoState: Equatable, Sendable { +struct DemoState: StateType, Sendable { struct Inner: Equatable { var name: String = "" diff --git a/Tests/VergeNormalizationDerivedTests/DemoState.swift b/Tests/VergeNormalizationDerivedTests/DemoState.swift index 2395183397..9d836cbaf7 100644 --- a/Tests/VergeNormalizationDerivedTests/DemoState.swift +++ b/Tests/VergeNormalizationDerivedTests/DemoState.swift @@ -1,6 +1,6 @@ import VergeNormalizationDerived -struct DemoState: Equatable { +struct DemoState: StateType { var count: Int = 0 diff --git a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift index 970e808c98..00ad1f9f45 100644 --- a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift +++ b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift @@ -47,29 +47,20 @@ final class VergeNormalizationDerivedTests: XCTestCase { withExtendedLifetime(derived, {}) } - func test_cache() { + @MainActor + func test_mainActorStore() { - let store = Store( - initialState: .init() - ) + let store = MainActorStore<_, Never>(initialState: DemoState()) - let derived1 = store - .normalizedStorage(.keyPath(\.db)) - .table(.keyPath(\.book)) - .derived(from: Book.EntityID.init("1")) + store.normalizedStorage(.keyPath(\.db)) - let derived2 = store - .normalizedStorage(.keyPath(\.db)) - .table(.keyPath(\.book)) - .derived(from: Book.EntityID.init("1")) + } - let derived3 = store - .normalizedStorage(.keyPath(\.db)) - .table(.keyPath(\.book2)) - .derived(from: Book.EntityID.init("1")) + func test_asyncStore() { + + let store = AsyncStore<_, Never>(initialState: DemoState()) - XCTAssert(derived1 === derived2) - XCTAssert(derived2 !== derived3) + store.normalizedStorage(.keyPath(\.db)) } diff --git a/Tests/VergeTests/Isolated/IsolatedStoreTests.swift b/Tests/VergeTests/Isolated/IsolatedStoreTests.swift new file mode 100644 index 0000000000..9abe0f5a6e --- /dev/null +++ b/Tests/VergeTests/Isolated/IsolatedStoreTests.swift @@ -0,0 +1,142 @@ + +import Verge +import XCTest + +final class MainActorIsolatedStoreTests: XCTestCase { + + typealias TestStore = MainActorStore + + @MainActor + func test_mainActorStore_in_mainActor() { + + let store = TestStore(initialState: .init()) + + XCTAssertEqual(store.state.count, 0) + + store.commit { + $0.count = 1 + } + + XCTAssertEqual(store.state.count, 1) + + } + + nonisolated func test_mainActorStore_in_nonisolated() async { + + let store = TestStore(initialState: .init()) + + do { + let count = store.state.count + XCTAssertEqual(count, 0) + } + + await store.commit { + $0.count = 1 + } + + do { + let count = store.state.count + XCTAssertEqual(count, 1) + } + + } + + @MainActor + func test_make_derived() { + + let store = TestStore(initialState: .init()) + + let derived = store.derived(.select(\.count)) + + XCTAssertEqual(derived.state.primitive, 0) + + store.commit { + $0.count = 1 + } + + XCTAssertEqual(derived.state.primitive, 1) + + } + + final class Driver: MainActorStoreDriverType { + + typealias Store = TestStore + + nonisolated var scope: WritableKeyPath { + \.inner + } + + let store: MainActorIsolatedStoreTests.TestStore + + nonisolated init(store: MainActorIsolatedStoreTests.TestStore) { + self.store = store + } + + } + + func test_driver() async { + + let store = TestStore(initialState: .init()) + + let driver = Driver(store: store) + + await driver.commit { + $0.name = "Hiroshi" + } + + let after = await driver.state + + XCTAssertEqual(after.name, "Hiroshi") + + } + +} + +final class ViewModelTests: XCTestCase { + + @MainActor + final class ViewModel: MainActorStoreDriverType { + + struct State: StateType { + + } + + let store: MainActorStore = .init(initialState: .init()) + + } + + +} + +final class ActorIsolatedStoreTests: XCTestCase { + + @MainActor + func test_asyncStore_in_mainActor() async { + + let store = AsyncStore<_, Never>(initialState: DemoState()) + + XCTAssertEqual(store.state.count, 0) + + await store.backgroundCommit { + $0.count = 1 + } + + XCTAssertEqual(store.state.count, 1) + + } + + nonisolated func test_mainActorStore_in_nonisolated() async { + + let store = AsyncStore<_, Never>(initialState: DemoState()) + + XCTAssertEqual(store.state.count, 0) + + await store.backgroundCommit { + $0.count = 1 + } + + XCTAssertEqual(store.state.count, 1) + + } + +} diff --git a/Tests/VergeTests/MutationTraceTests.swift b/Tests/VergeTests/MutationTraceTests.swift deleted file mode 100644 index ee6e36c2f3..0000000000 --- a/Tests/VergeTests/MutationTraceTests.swift +++ /dev/null @@ -1,39 +0,0 @@ - -import XCTest - -import Verge - -final class MutationTraceTests: XCTestCase { - - func testOneCommit() { - - let store = DemoStore() - - store.commit("MyCommit") { - $0.count += 1 - } - - let state = store.state - - XCTAssertEqual(state.traces.count, 1) - XCTAssertEqual(state.traces.first!.name, "MyCommit") - } - - func testDerived() { - - let store = DemoStore() - - let derived = store - .derived(.map(\.count), queue: .passthrough) - - store.commit("From_Store") { - $0.count += 1 - } - - let value = derived.state - - XCTAssertEqual(value.traces.count, 2) - - } - -} diff --git a/Tests/VergeTests/TransactionTests.swift b/Tests/VergeTests/TransactionTests.swift index 31c823ddcf..fe7a967319 100644 --- a/Tests/VergeTests/TransactionTests.swift +++ b/Tests/VergeTests/TransactionTests.swift @@ -3,7 +3,7 @@ import XCTest final class TransactionTests: XCTestCase { - func testTransaction() { + func testTransaction() async { struct MyKey: TransactionKey { static var defaultValue: String? { @@ -11,11 +11,11 @@ final class TransactionTests: XCTestCase { } } - let store = DemoStore() + let store = AsyncStore(initialState: .init()) - store.commit { - $0._transaction[MyKey.self] = "first commit" + await store.backgroundCommit { $0.markAsModified() + $1[MyKey.self] = "first commit" } XCTAssertEqual(store.state._transaction[MyKey.self], "first commit")