From 9c3a37f74367320043e72e65e7a4237577874536 Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Wed, 4 Dec 2024 12:52:10 -0600 Subject: [PATCH 1/8] Support for iOS 16 --- Package.swift | 4 ++-- Sources/SwiftRepo/Repository/ModelResponse.swift | 1 + .../Repository/Protocols/Query/Query.swift | 1 + .../Repository/DefaultQueryRepository.swift | 2 ++ .../Repository/Store/PersistentStore.swift | 1 + .../Repository/Store/SwiftDataStore.swift | 1 + Sources/SwiftRepo/Repository/StoreModel.swift | 9 +++++---- .../SwiftRepo/SwiftUI/LoadingControllerView.swift | 13 ++++++++++--- Sources/SwiftRepo/SwiftUI/View+Extensions.swift | 14 ++++++++++++++ 9 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 Sources/SwiftRepo/SwiftUI/View+Extensions.swift diff --git a/Package.swift b/Package.swift index 098130a..151e483 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.8 +// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "SwiftRepo", platforms: [ - .iOS("18.0"), + .iOS("16.0"), .macOS("15.0"), ], products: [ diff --git a/Sources/SwiftRepo/Repository/ModelResponse.swift b/Sources/SwiftRepo/Repository/ModelResponse.swift index 0ced35f..7d2d27d 100644 --- a/Sources/SwiftRepo/Repository/ModelResponse.swift +++ b/Sources/SwiftRepo/Repository/ModelResponse.swift @@ -11,6 +11,7 @@ import Foundation /// Partnered with a `QueryRepository` using an additional model store, `Value` will be /// propagated via an `ObservableStore` and the array of `Model`s will be placed in /// the `ModelStore`. +@available(iOS 17, *) public protocol ModelResponse { /// Can be used to propagate additional metadata related to the response via an `ObservableStore` associatedtype Value diff --git a/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift b/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift index 949ff88..0f1d319 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift @@ -129,6 +129,7 @@ public extension Query { /// - strategy: The query strategy /// - willGet: A closure that will be called if and when the query is performed. This is typically the `LoadingController.loading` function. /// - Returns: The value if the query was performed. Otherwise, `nil`. + @available(iOS 17, *) func get( id: QueryId, variables: Variables, diff --git a/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift b/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift index b2738ad..d620c0a 100644 --- a/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift @@ -84,6 +84,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { /// - modelStore: The underlying `Store` implementation to use for `QueryValue.Model`. /// - queryStrategy: The query strategy to use. /// - queryOperation: The operation to use to perform the actual query. + @available(iOS 17, *) public init( observableStore: ObservableStoreType, modelStore: any Store, @@ -148,6 +149,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { /// - modelStore: The underlying `Store` implementation to use for models. /// - queryStrategy: The query strategy to use. /// - queryOperation: The operation to use to perform the actual query. + @available(iOS 17, *) public convenience init( observableStore: ObservableStoreType, modelStore: any Store, diff --git a/Sources/SwiftRepo/Repository/Store/PersistentStore.swift b/Sources/SwiftRepo/Repository/Store/PersistentStore.swift index 5f37cdc..7de83d2 100644 --- a/Sources/SwiftRepo/Repository/Store/PersistentStore.swift +++ b/Sources/SwiftRepo/Repository/Store/PersistentStore.swift @@ -9,6 +9,7 @@ import Foundation import SwiftData /// A persistent `Store` implementation implementation using `SwiftData`. +@available(iOS 18, *) public class PersistentStore: Store { public var keys: [Key] { diff --git a/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift b/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift index 7ea95f3..d6490c8 100644 --- a/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift +++ b/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift @@ -11,6 +11,7 @@ import SwiftData import SwiftRepoCore // An implementation of `Store` that uses `SwiftData` under the hood +@available(iOS 18, *) public class SwiftDataStore: Store where Model: PersistentModel, Model.Key: Hashable & Codable { public typealias Key = Model.Key public typealias Value = Model diff --git a/Sources/SwiftRepo/Repository/StoreModel.swift b/Sources/SwiftRepo/Repository/StoreModel.swift index 5904254..0320645 100644 --- a/Sources/SwiftRepo/Repository/StoreModel.swift +++ b/Sources/SwiftRepo/Repository/StoreModel.swift @@ -11,6 +11,7 @@ import Foundation /// This interface is to be used with models that will be retrieved by the app /// through database queries, rather than published by a `QueryRepository`, /// such as when using SwiftData. +@available(iOS 17, *) public protocol StoreModel { /// The type to use as the identifier for the model associatedtype Key = any Hashable @@ -21,7 +22,7 @@ public protocol StoreModel { /// A predicate that can be used to query for the `StoreModel` static func predicate(key: Key) -> Predicate } - +@available(iOS 17, *) public extension StoreModel where Key == Data { static func predicate(key: Key) -> Predicate { @@ -30,7 +31,7 @@ public extension StoreModel where Key == Data { } } } - +@available(iOS 17, *) public extension StoreModel where Key == UUID { static func predicate(key: Key) -> Predicate { @@ -39,7 +40,7 @@ public extension StoreModel where Key == UUID { } } } - +@available(iOS 17, *) public extension StoreModel where Key == String { static func predicate(key: Key) -> Predicate { @@ -48,7 +49,7 @@ public extension StoreModel where Key == String { } } } - +@available(iOS 17, *) public extension StoreModel where Key == Int { static func predicate(key: Key) -> Predicate { diff --git a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift index 94fed60..79f0714 100644 --- a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift +++ b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift @@ -45,7 +45,8 @@ public struct LoadingControllerView some View) -> some View { + modifier(self) + } +} From 78570640679b8ad6c9a55bcef6a7e101bd3277ac Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Tue, 10 Dec 2024 11:59:25 -0600 Subject: [PATCH 2/8] Add trasnform effect --- Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift index 79f0714..64ff51f 100644 --- a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift +++ b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift @@ -71,7 +71,7 @@ public struct LoadingControllerView Date: Tue, 10 Dec 2024 12:54:45 -0600 Subject: [PATCH 3/8] Fix tests --- Package.swift | 2 +- Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift | 5 +++++ Tests/SwiftRepoTests/SwiftDataStoreTests.swift | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 151e483..50a7c0c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 5.8 import PackageDescription let package = Package( diff --git a/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift b/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift index dd83e71..e3115f0 100644 --- a/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift +++ b/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift @@ -32,6 +32,7 @@ class DefaultQueryRepositoryTests: XCTestCase { XCTAssertEqual(spy.publishedValues, [valueA1, valueA1, valueA1, valueA2]) } + @available(iOS 17, *) @MainActor func test_GetSuccess_ModelResponse() async throws { let repo = makeModelResponseStoreRepository( @@ -59,6 +60,7 @@ class DefaultQueryRepositoryTests: XCTestCase { XCTAssertEqual(try modelStore.get(key: Self.modelCId), responseB.models.last) } + @available(iOS 17, *) @MainActor func test_GetSuccess_ModelResponse_Trim() async throws { let repo = makeModelResponseStoreRepository( @@ -98,6 +100,7 @@ class DefaultQueryRepositoryTests: XCTestCase { XCTAssertEqual(spy.publishedValues.compactMap { $0 as? TestError }, [TestError(category: .failure)]) } + @available(iOS 17, *) func test_GetError_ModelResponse() async throws { let repo = makeModelResponseStoreRepository( delayedValues: DelayedValues(values: [ @@ -234,6 +237,7 @@ class DefaultQueryRepositoryTests: XCTestCase { var id: UUID var updatedAt = Date() + @available(iOS 17, *) static func predicate(key: UUID) -> Predicate { #Predicate { $0.id == key } } @@ -294,6 +298,7 @@ class DefaultQueryRepositoryTests: XCTestCase { /// Makes a repository that stores a single value per unique query ID, /// and places ModelResponse values in a separate model store. + @available(iOS 17, *) private func makeModelResponseStoreRepository( mergeStrategy: ModelStoreMergeStrategy = .upsertAppend, queryStrategy: QueryStrategy = .ifOlderThan(0.1), diff --git a/Tests/SwiftRepoTests/SwiftDataStoreTests.swift b/Tests/SwiftRepoTests/SwiftDataStoreTests.swift index b92d79a..e464dbd 100644 --- a/Tests/SwiftRepoTests/SwiftDataStoreTests.swift +++ b/Tests/SwiftRepoTests/SwiftDataStoreTests.swift @@ -9,6 +9,7 @@ import XCTest import SwiftData @testable import SwiftRepo +@available(iOS 18, *) @MainActor class SwiftDataStoreTests: XCTestCase { From 746e566e9c77256a9330cd2bc93d5ddcac299844 Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Tue, 10 Dec 2024 13:51:30 -0600 Subject: [PATCH 4/8] Fix StoreModel --- Sources/SwiftRepo/Repository/StoreModel.swift | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/Sources/SwiftRepo/Repository/StoreModel.swift b/Sources/SwiftRepo/Repository/StoreModel.swift index 48d7968..06892b6 100644 --- a/Sources/SwiftRepo/Repository/StoreModel.swift +++ b/Sources/SwiftRepo/Repository/StoreModel.swift @@ -22,43 +22,3 @@ public protocol StoreModel { /// A predicate that can be used to query for the `StoreModel` static func predicate(key: Key) -> Predicate } - -@available(iOS 17, *) -public extension StoreModel where Key == Data { - - static func predicate(key: Key) -> Predicate { - #Predicate { model in - model.id == key - } - } -} - -@available(iOS 17, *) -public extension StoreModel where Key == UUID { - - static func predicate(key: Key) -> Predicate { - #Predicate { model in - model.id == key - } - } -} - -@available(iOS 17, *) -public extension StoreModel where Key == String { - - static func predicate(key: Key) -> Predicate { - #Predicate { model in - model.id == key - } - } -} - -@available(iOS 17, *) -public extension StoreModel where Key == Int { - - static func predicate(key: Key) -> Predicate { - #Predicate { model in - model.id == key - } - } -} From db1867debf4e96e66fd15fa4677668ce225e8437 Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Tue, 10 Dec 2024 13:52:59 -0600 Subject: [PATCH 5/8] Fix tests --- Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift b/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift index ad17ad3..2668e52 100644 --- a/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift +++ b/Tests/SwiftRepoTests/DefaultQueryRepositoryTests.swift @@ -89,6 +89,7 @@ class DefaultQueryRepositoryTests: XCTestCase { XCTAssertEqual(try modelStore.get(key: Self.modelCId), responseB.models.last) } + @available(iOS 17, *) @MainActor func test_GetSuccess_ModelResponse_Merge() async throws { let repo = makeModelResponseStoreRepository( From b5f9dd067e99fb5c7a67f37581d92de058399b0e Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Tue, 10 Dec 2024 13:54:01 -0600 Subject: [PATCH 6/8] Remove unwanted space --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 50a7c0c..51bba80 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version:5.8 import PackageDescription let package = Package( From 2b2b4f4788729500737304a4614cd0a8d1711ef3 Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Tue, 10 Dec 2024 13:58:40 -0600 Subject: [PATCH 7/8] PR feedback --- Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift | 2 +- Sources/SwiftRepo/SwiftUI/View+Extensions.swift | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift index 64ff51f..8c4d62a 100644 --- a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift +++ b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift @@ -66,7 +66,7 @@ public struct LoadingControllerView some View) -> some View { - modifier(self) + /// Provides a way to introduce a code block as a view modifier. + @ViewBuilder func map(@ViewBuilder _ transform: (Self) -> Content) -> some View { + transform(self) } } From 21a07cf08ff1324c66563f606eb36850d76dd349 Mon Sep 17 00:00:00 2001 From: carlosDelaMoraFavor Date: Thu, 12 Dec 2024 12:22:46 -0600 Subject: [PATCH 8/8] Support strict concurency --- Package.swift | 5 ++- .../SwiftRepo/Loading/LoadingController.swift | 7 ++-- .../Mutation/OptimisticMutation.swift | 6 ++- .../Repository/Protocols/HasValueResult.swift | 2 +- .../Protocols/Mutation/Mutation.swift | 5 ++- .../Repository/Protocols/Query/Query.swift | 10 ++--- .../Repository/ConstantQueryRepository.swift | 2 +- .../Repository/QueryRepository.swift | 6 +-- .../Protocols/Store/ObservableStore.swift | 14 ++++--- .../Repository/Protocols/Store/Store.swift | 11 ++---- .../Repository/Query/DefaultQuery.swift | 6 +-- .../Repository/Query/QueryStoreKey.swift | 2 +- .../Repository/Query/QueryStrategy.swift | 2 +- .../DefaultConstantQueryRepository.swift | 6 +-- .../Repository/DefaultQueryRepository.swift | 29 ++++++++------- .../DefaultVariableQueryRepository.swift | 6 +-- .../Repository/PagedQueryRepository.swift | 10 ++--- .../Store/DefaultObservableStore.swift | 37 ++++++++++--------- .../Repository/Store/DictionaryStore.swift | 8 +--- .../Store/ModelStoreMergeStrategy.swift | 2 +- .../Repository/Store/NSCacheStore.swift | 2 +- .../Repository/Store/PersistentStore.swift | 5 ++- .../Repository/Store/SwiftDataStore.swift | 2 +- Sources/SwiftRepo/Repository/StoreModel.swift | 4 +- Sources/SwiftRepo/Repository/Unused.swift | 2 +- .../SwiftUI/LoadingControllerView.swift | 2 +- .../SwiftRepoCore/Protocols/Emptyable.swift | 2 +- Sources/SwiftRepoCore/Utils/Throttle.swift | 4 +- 28 files changed, 101 insertions(+), 98 deletions(-) diff --git a/Package.swift b/Package.swift index 51bba80..0fe33f8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version: 6.0 import PackageDescription let package = Package( @@ -43,5 +43,6 @@ let package = Package( .unsafeFlags(["-enable-library-evolution"]), ] ), - ] + ], + swiftLanguageModes: [.version("6.0")] ) diff --git a/Sources/SwiftRepo/Loading/LoadingController.swift b/Sources/SwiftRepo/Loading/LoadingController.swift index ebae90e..0777909 100644 --- a/Sources/SwiftRepo/Loading/LoadingController.swift +++ b/Sources/SwiftRepo/Loading/LoadingController.swift @@ -8,7 +8,8 @@ import Combine import SwiftRepoCore /// A state machine for loading, loaded, error and empty states. -public final actor LoadingController where DataType: Emptyable { +public typealias SyncEmptyable = Emptyable & Sendable +public final actor LoadingController where DataType: SyncEmptyable { // MARK: - API @@ -17,7 +18,7 @@ public final actor LoadingController where DataType: Emptyable { public private(set) lazy var state: AnyPublisher = stateSubject.eraseToAnyPublisher() /// The data loading states. - public enum State: CustomStringConvertible { + public enum State: CustomStringConvertible & Sendable { /// An initial loading state when there is no data to display. Components are responsible for displaying their own UI, /// if they choose to do so, when updating after initial data has already been loaded. For example, a list view may display /// a "pull-to-refresh" UI until the next state transition. @@ -393,7 +394,7 @@ extension LoadingController.State: Equatable where DataType: Equatable { } } -private extension Result where Success: Emptyable { +private extension Result where Success: SyncEmptyable { @MainActor func asLoadState( currentLoadState: LoadingController.LoadState, diff --git a/Sources/SwiftRepo/Repository/Mutation/OptimisticMutation.swift b/Sources/SwiftRepo/Repository/Mutation/OptimisticMutation.swift index 464f49d..5580ce2 100644 --- a/Sources/SwiftRepo/Repository/Mutation/OptimisticMutation.swift +++ b/Sources/SwiftRepo/Repository/Mutation/OptimisticMutation.swift @@ -14,7 +14,7 @@ import SwiftRepoCore /// If the remote mutation fails and there are no pending remote mutations, then the last known valid value is restored, potentially reverting optimistic local mutations. /// A valid value is defined as either the original value from the last idle period or the most recent success result from a remote mutation. public final actor OptimisticMutation: Mutation - where MutationId: Hashable, Variables: Hashable { +where MutationId: SyncHashable, Variables: SyncHashable, Value: Sendable { // MARK: - API public func mutate(id: MutationId, variables: Variables) async throws { @@ -40,6 +40,7 @@ public final actor OptimisticMutation: Mutation } } + public nonisolated func publisher(for id: MutationId) -> AnyPublisher { subject .filter { $0.mutationId == id } @@ -133,3 +134,6 @@ public final actor OptimisticMutation: Mutation } } } + +/// I do not believe that PassthroughSubject is actually sendable. We may need to create an actual sendable subject. +extension PassthroughSubject: @unchecked @retroactive Sendable {} diff --git a/Sources/SwiftRepo/Repository/Protocols/HasValueResult.swift b/Sources/SwiftRepo/Repository/Protocols/HasValueResult.swift index 147dd41..b22dfee 100644 --- a/Sources/SwiftRepo/Repository/Protocols/HasValueResult.swift +++ b/Sources/SwiftRepo/Repository/Protocols/HasValueResult.swift @@ -9,6 +9,6 @@ import Foundation /// directly in `QueryRepository`, the compiler doesn't like it and I was getting compiler segmentation faults /// compiling generated mocks. public protocol HasValueResult { - associatedtype Value + associatedtype Value: Sendable typealias ValueResult = Result } diff --git a/Sources/SwiftRepo/Repository/Protocols/Mutation/Mutation.swift b/Sources/SwiftRepo/Repository/Protocols/Mutation/Mutation.swift index b85ce93..6fd75b6 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Mutation/Mutation.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Mutation/Mutation.swift @@ -10,12 +10,13 @@ import Foundation /// are somehow causing a segmentation fault during compilation. It seems to be explicitly due to the `typealias ResultType`. /// For some reason, pushing `typealias ResultType` into a parent protocol fixes the segumentation fault. It also, unfortunately, /// forces the order of the mocked generic types to be `` rather than ``. +public typealias SyncHashable = Hashable & Sendable public protocol MutationBase { /// Mutation ID identifies a unique mutation for the purposes of optimistic updating, debouncing and providing ID-scoped publishers. - associatedtype MutationId: Hashable + associatedtype MutationId: SyncHashable /// The mutation parameters. - associatedtype Variables: Hashable + associatedtype Variables: SyncHashable /// The type of value being mutated. associatedtype Value diff --git a/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift b/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift index 6eddbee..727e687 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Query/Query.swift @@ -16,17 +16,17 @@ public enum QueryError: String, Error { /// /// In a typical usage, a repository would query remote data through an /// instance of `Query`, which would in turn be responsible for making the service call. -public protocol Query { +public protocol Query: Sendable { /// Query ID identifies a unique request for the purposes of request de-duplication, cancellation and providing ID-scoped publishers. - associatedtype QueryId: Hashable + associatedtype QueryId: SyncHashable /// The variables that provide the request parameters. When two overlapping queries are made with the same query ID and variables, /// only one request is made. When two overlapping queries are made with the same query ID and different variables, any ongoing /// request is cancelled and a new request is made with the latest variables. - associatedtype Variables: Hashable + associatedtype Variables: SyncHashable /// The response type returned by the query. - associatedtype Value + associatedtype Value: Sendable /// The result type used by publishers. typealias ResultType = QueryResult @@ -53,7 +53,7 @@ public protocol Query { } public extension Query { - typealias WillGet = () async -> Void + typealias WillGet = @Sendable () async -> Void @discardableResult /// Conditionally perform the query if needed based on the specified strategy and the state of the store. diff --git a/Sources/SwiftRepo/Repository/Protocols/Repository/ConstantQueryRepository.swift b/Sources/SwiftRepo/Repository/Protocols/Repository/ConstantQueryRepository.swift index 9232734..d0a1ca5 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Repository/ConstantQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Repository/ConstantQueryRepository.swift @@ -13,7 +13,7 @@ import SwiftRepoCore /// a single, simplified interface. An example use case is account service. Specifically getting account info, which requires no input variables. public protocol ConstantQueryRepository: HasValueResult { - associatedtype Variables: Hashable + associatedtype Variables: SyncHashable /// Performs the query, if needed, based on the query stategy of the underlying implementation. /// diff --git a/Sources/SwiftRepo/Repository/Protocols/Repository/QueryRepository.swift b/Sources/SwiftRepo/Repository/Protocols/Repository/QueryRepository.swift index 0cf846e..c617c53 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Repository/QueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Repository/QueryRepository.swift @@ -15,11 +15,11 @@ import SwiftRepoCore /// 2. File repository, where the query ID is typically some unique string, such as a UUID + prefix and the variables are a potentially temporary file URL. public protocol QueryRepository: HasValueResult { - associatedtype QueryId: Hashable + associatedtype QueryId: SyncHashable - associatedtype Variables: Hashable + associatedtype Variables: SyncHashable - associatedtype Key: Hashable + associatedtype Key: SyncHashable /// Performs the query, if needed, based on the query stategy of the underlying implementation. /// diff --git a/Sources/SwiftRepo/Repository/Protocols/Store/ObservableStore.swift b/Sources/SwiftRepo/Repository/Protocols/Store/ObservableStore.swift index 5ea3be1..3a774b2 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Store/ObservableStore.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Store/ObservableStore.swift @@ -23,7 +23,7 @@ public protocol ObservableStore: Store { /// the key and publish key can be equivalent, e.g. the query ID, if only the most recent value needs to be stored for a given key. In most cases, `QueryStoreKey` should be /// used because it provides the most responsive user experience. Query ID is a better choice if the variables are constant or cannot be relied upon to as a stable identifier /// (e.g. temporary FileStack URLs). - associatedtype PublishKey: Hashable + associatedtype PublishKey: SyncHashable /// The identifiable result type used by subscribers. typealias StoreResultType = StoreResult @@ -200,14 +200,14 @@ extension ObservableStore where Value: HasMutatedAt { AnySubscriber { subscription in subscription.request(.unlimited) } receiveValue: { value in - Task { + do { let key = value[keyPath: keyField] - if let currentMutatedAt = try await self.get(key: key)?.mutatedAt, + if let currentMutatedAt = try self.get(key: key)?.mutatedAt, value.mutatedAt <= currentMutatedAt { - return + return .unlimited } - try await self.set(key: key, value: value) - } + try self.set(key: key, value: value) + } catch {} return .unlimited } receiveCompletion: { _ in } @@ -215,3 +215,5 @@ extension ObservableStore where Value: HasMutatedAt { } extension ObservableStoreChange: Equatable where Value: Equatable {} + +extension AnySubscriber: @unchecked @retroactive Sendable {} diff --git a/Sources/SwiftRepo/Repository/Protocols/Store/Store.swift b/Sources/SwiftRepo/Repository/Protocols/Store/Store.swift index 9ceef8f..ec72682 100644 --- a/Sources/SwiftRepo/Repository/Protocols/Store/Store.swift +++ b/Sources/SwiftRepo/Repository/Protocols/Store/Store.swift @@ -6,35 +6,32 @@ import Foundation /// An interface for in-memory and/or persistent storage of key/value pairs. -public protocol Store { +@MainActor public protocol Store: Sendable { /// The type of key used by the store. - associatedtype Key: Hashable + associatedtype Key: Hashable & Sendable /// The type of value stored. - associatedtype Value + associatedtype Value: Sendable /// Set or remove a value from the cache. /// - Parameters: /// - key: the unique key /// - value: the value to store. Pass `nil` to delete any existing value. - @MainActor @discardableResult func set(key: Key, value: Value?) throws -> Value? /// Get a value from the cache. /// - Parameter key: the unique key /// - Returns: the current value contained in the store. Returns `nil` if there is no value. - @MainActor func get(key: Key) throws -> Value? /// Returns the age of the current value assigned to the given key - @MainActor func age(of key: Key) throws -> TimeInterval? /// Removes all values. Does not publish any changes. + nonisolated func clear() async throws /// Return all keys that exist in the store. - @MainActor var keys: [Key] { get throws } } diff --git a/Sources/SwiftRepo/Repository/Query/DefaultQuery.swift b/Sources/SwiftRepo/Repository/Query/DefaultQuery.swift index 7875446..fc7a5a9 100644 --- a/Sources/SwiftRepo/Repository/Query/DefaultQuery.swift +++ b/Sources/SwiftRepo/Repository/Query/DefaultQuery.swift @@ -7,7 +7,7 @@ import Combine import Foundation /// The default `Query` implementation. -public final actor DefaultQuery: Query where QueryId: Hashable, Variables: Hashable { +public final actor DefaultQuery: Query where QueryId: SyncHashable, Variables: SyncHashable, Value: Sendable { // MARK: - API public typealias ResultType = QueryResult @@ -15,7 +15,7 @@ public final actor DefaultQuery: Query where QueryId: /// Create a `DefaultQuery` given a remote operation. /// - Parameters: /// - queryOperation: a closure that performs the query operation, typically making a service call and returning the data. - public init(queryOperation: @escaping (Variables) async throws -> Value) { + public init(queryOperation: @Sendable @escaping (Variables) async throws -> Value) { self.queryOperation = queryOperation } @@ -74,7 +74,7 @@ public final actor DefaultQuery: Query where QueryId: // MARK: - Variables - private let queryOperation: (Variables) async throws -> Value + private let queryOperation: @Sendable (Variables) async throws -> Value private let subject = PassthroughSubject() private var taskCollateral: [QueryId: TaskCollateral] = [:] private var lastVariables: [QueryId: Variables] = [:] diff --git a/Sources/SwiftRepo/Repository/Query/QueryStoreKey.swift b/Sources/SwiftRepo/Repository/Query/QueryStoreKey.swift index 5dca197..ae7bd88 100644 --- a/Sources/SwiftRepo/Repository/Query/QueryStoreKey.swift +++ b/Sources/SwiftRepo/Repository/Query/QueryStoreKey.swift @@ -7,7 +7,7 @@ import Foundation /// A data model to use for storing query results by query ID and variables. This can type can be used as the store key in order to /// maintain a cache for all variables rather than just the most recently used variable. -public struct QueryStoreKey: Hashable where QueryId: Hashable, Variables: Hashable { +public struct QueryStoreKey: SyncHashable where QueryId: SyncHashable, Variables: SyncHashable { public let queryId: QueryId public let variables: Variables diff --git a/Sources/SwiftRepo/Repository/Query/QueryStrategy.swift b/Sources/SwiftRepo/Repository/Query/QueryStrategy.swift index cd419b8..f07182c 100644 --- a/Sources/SwiftRepo/Repository/Query/QueryStrategy.swift +++ b/Sources/SwiftRepo/Repository/Query/QueryStrategy.swift @@ -6,7 +6,7 @@ import Foundation /// A list of strategies for determining when stored data needs to be refreshed. -public enum QueryStrategy { +public enum QueryStrategy: Sendable { /// A new query is performed if the stored data is older than the specified `TimeInterval`. /// Stored data is provided initially, regardless of the age of the stored value. case ifOlderThan(TimeInterval) diff --git a/Sources/SwiftRepo/Repository/Repository/DefaultConstantQueryRepository.swift b/Sources/SwiftRepo/Repository/Repository/DefaultConstantQueryRepository.swift index b736799..c220bcb 100644 --- a/Sources/SwiftRepo/Repository/Repository/DefaultConstantQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Repository/DefaultConstantQueryRepository.swift @@ -9,7 +9,7 @@ import SwiftRepoCore /// The default `ConstantQueryRepository` implementation. public final class DefaultConstantQueryRepository: ConstantQueryRepository - where Variables: Hashable { +where Variables: SyncHashable, Value: Sendable { // MARK: - API public typealias QueryType = any Query @@ -43,7 +43,7 @@ public final class DefaultConstantQueryRepository: ConstantQue variables: Variables, observableStore: ObservableStoreType, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) { self.init( variables: variables, @@ -66,7 +66,7 @@ public final class DefaultConstantQueryRepository: ConstantQue public func get( errorIntent: ErrorIntent, queryStrategy: QueryStrategy?, - willGet: @escaping () async -> Void + willGet: @Sendable @escaping () async -> Void ) async { await repository.get( queryId: variables, diff --git a/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift b/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift index f02ef4f..814c9b9 100644 --- a/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Repository/DefaultQueryRepository.swift @@ -19,8 +19,8 @@ import SwiftRepoCore /// can be observed via the `QueryRepository` publisher. This publisher is essential for driving loading and error states and can publish /// any additional metadata contained in the query response. However, if there is no such data, the type can be `Unused`. The "Database" /// approach is typically used when models are value types stored in a database and values are fetched via database queries, e.g. SwiftData. -public final class DefaultQueryRepository: QueryRepository -where QueryId: Hashable, Variables: Hashable, Key: Hashable { +public final class DefaultQueryRepository: QueryRepository, Sendable +where QueryId: SyncHashable, Variables: SyncHashable, Key: SyncHashable, Value: Sendable { // MARK: - API @@ -34,13 +34,13 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { /// when the key is query ID, only one value is stored per query ID. On ther other hand, when the key is `QueryStoreKey`, /// one value is stored per unique query ID and variables. For both of these use cases, convenience initializers are provided /// that automatically supply the key factory. - public typealias KeyFactory = (_ queryId: QueryId, _ variables: Variables) -> Key + public typealias KeyFactory = @Sendable (_ queryId: QueryId, _ variables: Variables) -> Key /// A closure for extracting variables from values. This closure helps with establishing unique store keys. This is primaryliy used with /// sorting and filtering service calls where the client allows the service to select default variables and send them back in the response. These use /// cases create a condition where two variables means the same thing. This closure give the repo the ability to detect these situations as they /// happen and add key mappings to the observable store via `observableStore.addMapping(from:to:)` - public typealias ValueVariablesFactory = (_ queryId: QueryId, _ variables: Variables, _ value: FactoryValue) -> Variables + public typealias ValueVariablesFactory = @Sendable (_ queryId: QueryId, _ variables: Variables, _ value: FactoryValue) -> Variables /// Creates a "Classic" query repository. There are simplified convenience initializers, so this one is typically not called directly. public init( @@ -60,7 +60,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { try await observableStore.set(key: key, value: nil) } } - get = { key, queryId, variables, errorIntent, queryStrategy, willGet in + get = { @Sendable key, queryId, variables, errorIntent, queryStrategy, willGet in _ = try await query.get( id: queryId, variables: variables, @@ -106,7 +106,8 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { try await observableStore.set(key: key, value: nil) } } - get = { key, queryId, variables, errorIntent, queryStrategy, willGet in + + get = { @Sendable key, queryId, variables, errorIntent, queryStrategy, willGet in _ = try await query.get( id: queryId, variables: variables, @@ -133,7 +134,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { public convenience init( observableStore: ObservableStoreType, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) where Key == QueryId { self.init( observableStore: observableStore, @@ -158,7 +159,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { modelStore: any Store, mergeStrategy: ModelStoreMergeStrategy, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) where Model: StoreModel, Value: ModelResponse, Model == Value.Model, Key == QueryId, Value == Value.Value { self.init( observableStore: observableStore, @@ -179,7 +180,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { public convenience init( observableStore: any ObservableStore, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) where Key == QueryStoreKey { self.init( observableStore: observableStore, @@ -199,7 +200,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { public convenience init( observableStore: any ObservableStore, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) where Key == QueryStoreKey, Value: HasValueVariables, @@ -236,14 +237,14 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { private let observableStore: ObservableStoreType private let mergeStrategy: ModelStoreMergeStrategy! private let queryStrategy: QueryStrategy - private let keyFactory: (_ queryId: QueryId, _ variables: Variables) -> Key + private let keyFactory: @Sendable (_ queryId: QueryId, _ variables: Variables) -> Key - let preGet: ( + let preGet: @Sendable ( _ queryId: QueryId, _ variables: Variables ) async throws -> Void - private let get: ( + private let get: @Sendable ( _ key: Key, _ queryId: QueryId, _ variables: Variables, @@ -306,7 +307,7 @@ where QueryId: Hashable, Variables: Hashable, Key: Hashable { // Any errors on prefetch can be propagated through the publisher. let key = keyFactory(queryId, variables) let result = StoreResult(key: key, failure: error) - _ = observableStore.subscriber + _ = await observableStore.subscriber .receive(result) } } diff --git a/Sources/SwiftRepo/Repository/Repository/DefaultVariableQueryRepository.swift b/Sources/SwiftRepo/Repository/Repository/DefaultVariableQueryRepository.swift index b498027..2ffc8cd 100644 --- a/Sources/SwiftRepo/Repository/Repository/DefaultVariableQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Repository/DefaultVariableQueryRepository.swift @@ -9,7 +9,7 @@ import SwiftRepoCore /// The default `VariableQueryRepository` implementation. public final class DefaultVariableQueryRepository: VariableQueryRepository - where Variables: Hashable { +where Variables: SyncHashable, Value: Sendable { // MARK: - API public typealias QueryType = any Query @@ -39,7 +39,7 @@ public final class DefaultVariableQueryRepository: VariableQue public convenience init( observableStore: any ObservableStore, queryStrategy: QueryStrategy, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) { self.init( observableStore: observableStore, @@ -73,7 +73,7 @@ public final class DefaultVariableQueryRepository: VariableQue variables: Variables, errorIntent: ErrorIntent, queryStrategy: QueryStrategy? = nil, - willGet: @escaping () async -> Void + willGet: @Sendable @escaping () async -> Void ) async { await repository.get( queryId: variables, diff --git a/Sources/SwiftRepo/Repository/Repository/PagedQueryRepository.swift b/Sources/SwiftRepo/Repository/Repository/PagedQueryRepository.swift index 886879e..a70f1be 100644 --- a/Sources/SwiftRepo/Repository/Repository/PagedQueryRepository.swift +++ b/Sources/SwiftRepo/Repository/Repository/PagedQueryRepository.swift @@ -17,16 +17,16 @@ import SwiftRepoCore /// 2. When requesting a publisher. This prevents stale data from being published on view model initialization. /// 2. Using the `.ifNotStored` query strategy to ensure that, as additional pages are loaded, they are appended to the existing store. public final class PagedQueryRepository: QueryRepository - where QueryId: Hashable, Variables: Hashable, Key: Hashable, Variables: HasCursorPaginationInput { +where QueryId: SyncHashable, Variables: SyncHashable, Key: SyncHashable, Variables: HasCursorPaginationInput, Value: Sendable { // MARK: - API public typealias QueryType = any Query public typealias ObservableStoreType = any ObservableStore - public typealias KeyFactory = (_ queryId: QueryId, _ variables: Variables) -> Key + public typealias KeyFactory = @Sendable (_ queryId: QueryId, _ variables: Variables) -> Key - public typealias ValueVariablesFactory = (_ queryId: QueryId, _ variables: Variables, _ value: Value) -> Variables + public typealias ValueVariablesFactory = @Sendable (_ queryId: QueryId, _ variables: Variables, _ value: Value) -> Variables @MainActor public func get( @@ -34,7 +34,7 @@ public final class PagedQueryRepository: QueryRe variables: Variables, errorIntent: ErrorIntent, queryStrategy _: QueryStrategy? = nil, - willGet: @escaping () async -> Void + willGet: @Sendable @escaping () async -> Void ) async { // Evict stale data when getting the first page. if !variables.isPaging { @@ -81,7 +81,7 @@ public final class PagedQueryRepository: QueryRe public convenience init( observableStore: any ObservableStore, ifOlderThan: TimeInterval, - queryOperation: @escaping (Variables) async throws -> Value + queryOperation: @Sendable @escaping (Variables) async throws -> Value ) where Key == QueryStoreKey, Value: HasValueVariables, diff --git a/Sources/SwiftRepo/Repository/Store/DefaultObservableStore.swift b/Sources/SwiftRepo/Repository/Store/DefaultObservableStore.swift index f19a02f..828b0ce 100644 --- a/Sources/SwiftRepo/Repository/Store/DefaultObservableStore.swift +++ b/Sources/SwiftRepo/Repository/Store/DefaultObservableStore.swift @@ -6,7 +6,7 @@ import Combine import Foundation -public final class DefaultObservableStore: ObservableStore where Key: Hashable, PublishKey: Hashable { +public final class DefaultObservableStore: ObservableStore where Key: SyncHashable, PublishKey: SyncHashable, Value: Sendable { // MARK: - API /// A closure that converts a key into a publish key. This mapping is required in order to route changes to the relevant publishers. The most common @@ -81,17 +81,18 @@ public final class DefaultObservableStore: ObservableSto public private(set) lazy var subscriber: AnySubscriber = AnySubscriber { subscription in subscription.request(.unlimited) - } receiveValue: { unmappedResult in - Task { @MainActor [weak self] in - guard let self = self else { return } - let result = StoreResult(key: self.map(key: unmappedResult.key), result: unmappedResult.result) + } receiveValue: { [weak self] unmappedResult in + guard let self else { return .unlimited } + do { + let result = StoreResult(key: map(key: unmappedResult.key), result: unmappedResult.result) switch result.result { case let .success(value): try self.set(key: result.key, value: value) case .failure: self.subject.send(result) } - } + } catch {} + return .unlimited } receiveCompletion: { _ in } @@ -100,18 +101,17 @@ public final class DefaultObservableStore: ObservableSto AnySubscriber { subscription in subscription.request(.unlimited) } receiveValue: { value in - Task { [weak self] in - guard let self = self else { return } + do { let key = value[keyPath: keyField] - try await self.set(key: self.map(key: key), value: value) - } + try self.set(key: self.map(key: key), value: value) + } catch {} return .unlimited } receiveCompletion: { _ in } } - public func currentKey(for publishKey: PublishKey) async -> Key? { - await currentKey[publishKey] + public func currentKey(for publishKey: PublishKey) -> Key? { + currentKey[publishKey] } @MainActor @@ -148,16 +148,16 @@ public final class DefaultObservableStore: ObservableSto public func mutate(publishKey: PublishKey, mutation: (Key, Value) -> Value?) async throws { let timestamp = Date().timeIntervalSince1970 - for key in try await keys(for: publishKey) { + for key in try keys(for: publishKey) { let elapsedTime = Date().timeIntervalSince1970 - timestamp - guard let value = try await store.get(key: key), - (try await store.age(of: key) ?? TimeInterval.greatestFiniteMagnitude) > elapsedTime, + guard let value = try store.get(key: key), + (try store.age(of: key) ?? TimeInterval.greatestFiniteMagnitude) > elapsedTime, let mutatedValue = mutation(key, value) else { continue } - switch await currentKey[publishKey] == key { + switch currentKey[publishKey] == key { case true: - try await actorSet(key: key, value: mutatedValue, isSettingCurrentKey: false) + try actorSet(key: key, value: mutatedValue, isSettingCurrentKey: false) case false: - try await store.set(key: key, value: mutatedValue) + try store.set(key: key, value: mutatedValue) } } } @@ -276,3 +276,4 @@ public final class DefaultObservableStore: ObservableSto } } } + diff --git a/Sources/SwiftRepo/Repository/Store/DictionaryStore.swift b/Sources/SwiftRepo/Repository/Store/DictionaryStore.swift index 668c43c..6511eef 100644 --- a/Sources/SwiftRepo/Repository/Store/DictionaryStore.swift +++ b/Sources/SwiftRepo/Repository/Store/DictionaryStore.swift @@ -6,13 +6,12 @@ import Foundation // An in-memory implementation of `Store` that uses a `Dictionary` -public final actor DictionaryStore: Store where Key: Hashable { +@MainActor public final class DictionaryStore: Store where Key: SyncHashable, Value: Sendable { // MARK: - API /// A closure that defines how old values are merged with new values. public typealias Merge = (_ old: Value, _ new: Value) -> Value - @MainActor public func set(key: Key, value: Value?) -> Value? { switch value { case let value?: @@ -28,12 +27,10 @@ public final actor DictionaryStore: Store where Key: Hashable { return store[key]?.value } - @MainActor public func get(key: Key) -> Value? { store[key]?.value } - @MainActor public func age(of key: Key) -> TimeInterval? { store[key]?.ageOf } @@ -42,7 +39,6 @@ public final actor DictionaryStore: Store where Key: Hashable { await actorClear() } - @MainActor public var keys: [Key] { Array(store.keys) } @@ -57,13 +53,11 @@ public final actor DictionaryStore: Store where Key: Hashable { // MARK: - Variables - @MainActor private var store: [Key: TimestampedValue] = [:] private let merge: Merge? // MARK: - Accessing actor-isolated state - @MainActor private func actorClear() { store.removeAll() } diff --git a/Sources/SwiftRepo/Repository/Store/ModelStoreMergeStrategy.swift b/Sources/SwiftRepo/Repository/Store/ModelStoreMergeStrategy.swift index 61e602f..63fae50 100644 --- a/Sources/SwiftRepo/Repository/Store/ModelStoreMergeStrategy.swift +++ b/Sources/SwiftRepo/Repository/Store/ModelStoreMergeStrategy.swift @@ -8,7 +8,7 @@ import Foundation /// Options for how models are stored in the database when using `StoreModel`. -public enum ModelStoreMergeStrategy { +public enum ModelStoreMergeStrategy: Sendable { /// Adds or updates models. Existing models that aren't in the current result set remain untouched. /// Use this strategy when the result set represents an incremental update, such as new and modified records. case append diff --git a/Sources/SwiftRepo/Repository/Store/NSCacheStore.swift b/Sources/SwiftRepo/Repository/Store/NSCacheStore.swift index 21ea063..d98491a 100644 --- a/Sources/SwiftRepo/Repository/Store/NSCacheStore.swift +++ b/Sources/SwiftRepo/Repository/Store/NSCacheStore.swift @@ -6,7 +6,7 @@ import Foundation // An in-memory implementation of `Store` that uses a default `NSCache` -public final actor NSCacheStore: Store where Key: Hashable { +@MainActor public final class NSCacheStore: Store where Key: SyncHashable, Value: Sendable { // MARK: - API @MainActor diff --git a/Sources/SwiftRepo/Repository/Store/PersistentStore.swift b/Sources/SwiftRepo/Repository/Store/PersistentStore.swift index 33ead23..91eb3d8 100644 --- a/Sources/SwiftRepo/Repository/Store/PersistentStore.swift +++ b/Sources/SwiftRepo/Repository/Store/PersistentStore.swift @@ -9,8 +9,9 @@ import Foundation import SwiftData /// A persistent `Store` implementation implementation using `SwiftData`. +public typealias SyncCodable = Codable & Sendable @available(iOS 18, *) -public class PersistentStore: Store { +@MainActor public class PersistentStore: Store { public var keys: [Key] { get throws { @@ -78,7 +79,7 @@ public class PersistentStore: Store { // MARK: - Constants @Model - class TimestampedValue: StoreModel { + class TimestampedValue: StoreModel, @unchecked Sendable { #Index([\.id]) @Attribute(.unique) diff --git a/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift b/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift index 785bf76..0596742 100644 --- a/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift +++ b/Sources/SwiftRepo/Repository/Store/SwiftDataStore.swift @@ -12,7 +12,7 @@ import SwiftRepoCore // An implementation of `Store` that uses `SwiftData` under the hood @available(iOS 18, *) -public class SwiftDataStore: Store, Saveable where Model: PersistentModel, Model.Key: Hashable & Codable { +@MainActor public class SwiftDataStore: Store, Saveable where Model: PersistentModel, Model.Key: SyncHashable & Codable { public typealias Key = Model.Key public typealias Value = Model /// A closure that defines how existing values are merged into new values. diff --git a/Sources/SwiftRepo/Repository/StoreModel.swift b/Sources/SwiftRepo/Repository/StoreModel.swift index 06892b6..08807b1 100644 --- a/Sources/SwiftRepo/Repository/StoreModel.swift +++ b/Sources/SwiftRepo/Repository/StoreModel.swift @@ -12,9 +12,9 @@ import Foundation /// through database queries, rather than published by a `QueryRepository`, /// such as when using SwiftData. @available(iOS 17, *) -public protocol StoreModel { +public protocol StoreModel: Sendable { /// The type to use as the identifier for the model - associatedtype Key = any Hashable + associatedtype Key = any SyncHashable /// The identifier of the model var id: Key { get } diff --git a/Sources/SwiftRepo/Repository/Unused.swift b/Sources/SwiftRepo/Repository/Unused.swift index 8e6b148..31a48f0 100644 --- a/Sources/SwiftRepo/Repository/Unused.swift +++ b/Sources/SwiftRepo/Repository/Unused.swift @@ -11,6 +11,6 @@ import Foundation /// `Never` type in Combine. For example, the `DefaultQueryRepository` type has three generic types `QueryId`, `Variables` and `Value`. /// The query ID allows the repository to manage multiple separate queries to the same endpoint. If there is only one distinct query, the `Unused` type /// can be specified for the `QueryId` parameter instead of using some other arbitrary constant, e.g. `DefaultQueryRepository`. -public enum Unused: Hashable, Codable { +public enum Unused: SyncHashable, Codable { case unused } diff --git a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift index 8c4d62a..33f6dde 100644 --- a/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift +++ b/Sources/SwiftRepo/SwiftUI/LoadingControllerView.swift @@ -9,7 +9,7 @@ import SwiftRepoCore /// Pairs with `LoadingController` to display loading, error and empty states. public struct LoadingControllerView: View - where DataType: Emptyable & Equatable, Content: View, LoadingContent: View, ErrorContent: View, EmptyContent: View { +where DataType: SyncEmptyable & Equatable, Content: View, LoadingContent: View, ErrorContent: View, EmptyContent: View { // MARK: - API diff --git a/Sources/SwiftRepoCore/Protocols/Emptyable.swift b/Sources/SwiftRepoCore/Protocols/Emptyable.swift index 7a523a7..d3b0ccd 100644 --- a/Sources/SwiftRepoCore/Protocols/Emptyable.swift +++ b/Sources/SwiftRepoCore/Protocols/Emptyable.swift @@ -19,7 +19,7 @@ extension Bool: Emptyable { extension Array: Emptyable {} -extension Set: Emptyable {} +extension Set: Emptyable {} extension Dictionary: Emptyable {} diff --git a/Sources/SwiftRepoCore/Utils/Throttle.swift b/Sources/SwiftRepoCore/Utils/Throttle.swift index d511128..45d7d7e 100644 --- a/Sources/SwiftRepoCore/Utils/Throttle.swift +++ b/Sources/SwiftRepoCore/Utils/Throttle.swift @@ -5,10 +5,10 @@ import Foundation -public actor Throttle { +public actor Throttle { // MARK: - API - nonisolated public init(rate: Duration, callback: @escaping (T) async -> Void) { + public init(rate: Duration, callback: @escaping (T) async -> Void) { self.rate = rate self.callback = callback }