diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 3123d61..949b74c 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -783,6 +783,14 @@ B5F8496D234898240029D57B /* ListSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8496B234898240029D57B /* ListSnapshot.swift */; }; B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8496B234898240029D57B /* ListSnapshot.swift */; }; B5F8496F234898240029D57B /* ListSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8496B234898240029D57B /* ListSnapshot.swift */; }; + B5F9C093287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */; }; + B5F9C094287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */; }; + B5F9C095287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */; }; + B5F9C096287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */; }; + B5F9C098287850D6007AAD2E /* MigrationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C097287850D6007AAD2E /* MigrationProgress.swift */; }; + B5F9C099287850D6007AAD2E /* MigrationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C097287850D6007AAD2E /* MigrationProgress.swift */; }; + B5F9C09A287850D6007AAD2E /* MigrationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C097287850D6007AAD2E /* MigrationProgress.swift */; }; + B5F9C09B287850D6007AAD2E /* MigrationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9C097287850D6007AAD2E /* MigrationProgress.swift */; }; B5FAD6A91B50A4B400714891 /* Progress+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6A81B50A4B300714891 /* Progress+Convenience.swift */; }; B5FAD6AC1B51285300714891 /* Internals.MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */; }; B5FE4DA21C8481E100FA6A91 /* StorageInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FE4DA11C8481E100FA6A91 /* StorageInterface.swift */; }; @@ -1061,6 +1069,8 @@ B5F5848628633741001F57ED /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; B5F8496B234898240029D57B /* ListSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSnapshot.swift; sourceTree = ""; }; B5F849702348A6690029D57B /* EnvironmentValues+DataSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+DataSources.swift"; sourceTree = ""; }; + B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataStack+Concurrency.swift"; sourceTree = ""; }; + B5F9C097287850D6007AAD2E /* MigrationProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationProgress.swift; sourceTree = ""; }; B5FAD6A81B50A4B300714891 /* Progress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Progress+Convenience.swift"; sourceTree = ""; }; B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Internals.MigrationManager.swift; sourceTree = ""; }; B5FE4DA11C8481E100FA6A91 /* StorageInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageInterface.swift; sourceTree = ""; }; @@ -1430,6 +1440,7 @@ B5D1E22B19FA9FBC003B2874 /* CoreStoreError.swift */, B549F6721E56A92800FBAB2D /* CoreDataNativeType.swift */, B5D339F01E94AF5800C880DE /* CoreStoreStrings.swift */, + B5F9C091287849CB007AAD2E /* Swift Concurrency */, B5C795BE25D933C200BDACC1 /* Reactive Programming */, B52FEC722596DB6400368BFB /* SwiftUI */, B5E84EDA1AFF84500064E85B /* Setup */, @@ -1500,6 +1511,7 @@ B5B866EC25F4800800335476 /* DataStack.AddStoragePublisher.swift */, B5944EFA25E8E8DA001D1D81 /* ListPublisher.SnapshotPublisher.swift */, B5B866DF25E9048000335476 /* ObjectPublisher.SnapshotPublisher.swift */, + B5F9C097287850D6007AAD2E /* MigrationProgress.swift */, ); name = "Reactive Programming"; sourceTree = ""; @@ -1679,6 +1691,14 @@ name = Internals; sourceTree = ""; }; + B5F9C091287849CB007AAD2E /* Swift Concurrency */ = { + isa = PBXGroup; + children = ( + B5F9C092287849E0007AAD2E /* DataStack+Concurrency.swift */, + ); + name = "Swift Concurrency"; + sourceTree = ""; + }; B5FE4DA01C84818B00FA6A91 /* StorageInterfaces */ = { isa = PBXGroup; children = ( @@ -2052,6 +2072,7 @@ B5F1DA8D1B9AA97D007C5CBB /* ImportableObject.swift in Sources */, B56965241B356B820075EE4A /* MigrationResult.swift in Sources */, B5C7958F25D7D18000BDACC1 /* ListState.swift in Sources */, + B5F9C098287850D6007AAD2E /* MigrationProgress.swift in Sources */, B5FE4DAC1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, B5E41EC01EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5A1DAC81F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, @@ -2073,6 +2094,7 @@ B50E175723517DE4004F033C /* Differentiable.swift in Sources */, B59AFF411C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */, + B5F9C093287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */, B5BF7FC6234D7E460070E741 /* ObjectSnapshot.swift in Sources */, B5E84EF71AFF846E0064E85B /* UnsafeDataTransaction.swift in Sources */, B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */, @@ -2266,6 +2288,7 @@ B56923C51EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, 82BA18C91C4BBD5900A0916E /* MigrationType.swift in Sources */, B5D8CA772346EAEE0055D7D1 /* DataStack+DataSources.swift in Sources */, + B5F9C099287850D6007AAD2E /* MigrationProgress.swift in Sources */, 82BA18D01C4BBD7100A0916E /* Internals.MigrationManager.swift in Sources */, B5DE5231230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, B56E4ED523CDB54A00E1708C /* FieldProtocol.swift in Sources */, @@ -2287,6 +2310,7 @@ 82BA18BF1C4BBD5300A0916E /* SectionBy.swift in Sources */, B5D339ED1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, B509D7D423C84E1900F42824 /* Transformable.Required.swift in Sources */, + B5F9C094287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */, 82BA18AC1C4BBD3100A0916E /* SynchronousDataTransaction.swift in Sources */, B50C3EE623D153EA00B29880 /* Field.Coded.swift in Sources */, 82BA18C41C4BBD5300A0916E /* ListMonitor.swift in Sources */, @@ -2480,6 +2504,7 @@ B5D8CA792346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */, B56E4ED723CDB54A00E1708C /* FieldProtocol.swift in Sources */, B56923C71EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, + B5F9C09B287850D6007AAD2E /* MigrationProgress.swift in Sources */, B5944EFE25E8E8DA001D1D81 /* ListPublisher.SnapshotPublisher.swift in Sources */, B5DE5233230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, B52DD1A51BE1F92F00949AFE /* ImportableUniqueObject.swift in Sources */, @@ -2501,6 +2526,7 @@ B52F74321E9B50D0005F3DAC /* SchemaHistory.swift in Sources */, B509D7D623C84E1900F42824 /* Transformable.Required.swift in Sources */, B50C3EE823D153EA00B29880 /* Field.Coded.swift in Sources */, + B5F9C096287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */, B5D339EF1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, B52DD19F1BE1F92C00949AFE /* SynchronousDataTransaction.swift in Sources */, B52DD1CB1BE1F94600949AFE /* Internals.WeakObject.swift in Sources */, @@ -2694,6 +2720,7 @@ B56321931BD65216006C9394 /* DataStack+Querying.swift in Sources */, B5D8CA782346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */, B56923C61EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, + B5F9C09A287850D6007AAD2E /* MigrationProgress.swift in Sources */, B56321A71BD65216006C9394 /* MigrationResult.swift in Sources */, B56E4ED623CDB54A00E1708C /* FieldProtocol.swift in Sources */, B5DE5232230BDA1300A22534 /* CoreStoreDefaults.swift in Sources */, @@ -2715,6 +2742,7 @@ B52F74311E9B50D0005F3DAC /* SchemaHistory.swift in Sources */, B563218F1BD65216006C9394 /* ImportableObject.swift in Sources */, B509D7D523C84E1900F42824 /* Transformable.Required.swift in Sources */, + B5F9C095287849E0007AAD2E /* DataStack+Concurrency.swift in Sources */, B56321991BD65216006C9394 /* OrderBy.swift in Sources */, B50C3EE723D153EA00B29880 /* Field.Coded.swift in Sources */, B5D339EE1E9495E500C880DE /* CoreStoreObject+Querying.swift in Sources */, diff --git a/Sources/DataStack+Concurrency.swift b/Sources/DataStack+Concurrency.swift new file mode 100644 index 0000000..51150bc --- /dev/null +++ b/Sources/DataStack+Concurrency.swift @@ -0,0 +1,367 @@ +// +// DataStack+Concurrency.swift +// CoreStore +// +// Copyright © 2021 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + + +// MARK: - DataStack + +extension DataStack { + + // MARK: Public + + /** + Swift concurrency utilities for the `DataStack` are exposed through this namespace + */ + public var async: DataStack.AsyncNamespace { + + return .init(self) + } + + // MARK: - ReactiveNamespace + + /** + Swift concurrency for the `DataStack` are exposed through this namespace. Extend this type if you need to add other `async` utilities for `DataStack`. + */ + public struct AsyncNamespace { + + // MARK: Public + + /** + The `DataStack` instance + */ + public let base: DataStack + + + // MARK: Internal + + internal init(_ base: DataStack) { + + self.base = base + } + } +} + + +// MARK: - DataStack.AsyncNamespace + +extension DataStack.AsyncNamespace { + + // MARK: Public + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `addStorage(...)` API. Asynchronously adds a `StorageInterface` to the stack. + ``` + let storage = try await dataStack.async.addStorage( + InMemoryStore(configuration: "Config1") + ) + ``` + - parameter storage: the storage + - returns: The `StorageInterface` instance added to the `DataStack`. Note that the `StorageInterface` event value may not always be the same instance as the parameter argument if a previous `StorageInterface` was already added at the same URL and with the same configuration. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func addStorage( + _ storage: T + ) async throws -> T { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.addStorage( + storage, + completion: continuation.resume(with:) + ) + } + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `addStorage(...)` API. Asynchronously adds a `LocalStorage` to the stack. Migrations are also initiated by default. The event emits `MigrationProgress` `enum` values. + ``` + for try await migrationProgress in dataStack.async.addStorage( + SQLiteStore( + fileName: "core_data.sqlite", + configuration: "Config1" + ) + ) { + + print("\(round(migrationProgress.fractionCompleted * 100)) %") // 0.0 ~ 1.0 + } + ``` + - parameter storage: the local storage + - returns: An `AsyncThrowingStream` that emits a `MigrationProgress` value with metadata for migration progress. Note that the `LocalStorage` event value may not always be the same instance as the parameter argument if a previous `LocalStorage` was already added at the same URL and with the same configuration. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func addStorage( + _ storage: T + ) -> AsyncThrowingStream, Swift.Error> { + + return .init( + bufferingPolicy: .unbounded, + { continuation in + + var progress: Progress? = nil + progress = self.base.addStorage( + storage, + completion: { result in + + progress?.setProgressHandler(nil) + + switch result { + + case .success(let storage): + continuation.yield( + .finished( + storage: storage, + migrationRequired: progress != nil + ) + ) + continuation.finish() + + case .failure(let error): + continuation.finish( + throwing: error + ) + } + } + ) + if let progress = progress { + + progress.setProgressHandler { progress in + + continuation.yield( + .migrating( + storage: storage, + progressObject: progress + ) + ) + } + } + } + ) + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `importObject(...)` API. Creates an `ImportableObject` by importing from the specified import source. The event value will be the object instance correctly associated for the `DataStack`. + ``` + let object = try await dataStack.async.importObject( + Into(), + source: ["name": "John"] + ) + ``` + - parameter into: an `Into` clause specifying the entity type + - parameter source: the object to import values from + - returns: The object instance correctly associated for the `DataStack` if the object was imported successfully, or `nil` if the `ImportableObject` ignored the `source`. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func importObject( + _ into: Into, + source: O.ImportSource + ) async throws -> O? { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.perform( + asynchronous: { (transaction) -> O? in + + return try transaction.importObject( + into, + source: source + ) + }, + success: { + + continuation.resume( + with: .success($0.flatMap(self.base.fetchExisting)) + ) + }, + failure: continuation.resume(throwing:) + ) + } + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `importObject(...)` API. Updates an existing `ImportableObject` by importing values from the specified import source. The event value will be the object instance correctly associated for the `DataStack`. + ``` + let importedPerson = try await dataStack.async.importObject( + existingPerson, + source: ["name": "John", "age": 30] + ) + ``` + - parameter object: the object to update + - parameter source: the object to import values from + - returns: The object instance correctly associated for the `DataStack` if the object was imported successfully, or `nil` if the `ImportableObject` ignored the `source`. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func importObject( + _ object: O, + source: O.ImportSource + ) async throws -> O? { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.perform( + asynchronous: { (transaction) -> O? in + + guard let object = transaction.edit(object) else { + + try transaction.cancel() + } + try transaction.importObject( + object, + source: source + ) + return object + }, + success: { + + continuation.resume( + with: .success($0.flatMap(self.base.fetchExisting)) + ) + }, + failure: continuation.resume(throwing:) + ) + } + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `importUniqueObject(...)` API. Updates an existing `ImportableUniqueObject` or creates a new instance by importing from the specified import source. The event value will be the object instance correctly associated for the `DataStack`. + ``` + let person = try await dataStack.async.importUniqueObject( + Into(), + source: ["name": "John", "age": 30] + ) + ``` + - parameter into: an `Into` clause specifying the entity type + - parameter source: the object to import values from + - returns: The object instance correctly associated for the `DataStack` if the object was imported successfully, or `nil` if the `ImportableUniqueObject` ignored the `source`. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func importUniqueObject( + _ into: Into, + source: O.ImportSource + ) async throws -> O? { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.perform( + asynchronous: { (transaction) -> O? in + + return try transaction.importUniqueObject( + into, + source: source + ) + }, + success: { + + continuation.resume( + with: .success($0.flatMap(self.base.fetchExisting)) + ) + }, + failure: continuation.resume(throwing:) + ) + } + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `importUniqueObjects(...)` API. Updates existing `ImportableUniqueObject`s or creates them by importing from the specified array of import sources. `ImportableUniqueObject` methods are called on the objects in the same order as they are in the `sourceArray`, and are returned in an array with that same order. The event values will be object instances correctly associated for the `DataStack`. + ``` + let people = try await dataStack.async.importUniqueObjects( + Into(), + sourceArray: [ + ["name": "John"], + ["name": "Bob"], + ["name": "Joe"] + ] + ) + ``` + - Warning: If `sourceArray` contains multiple import sources with same ID, no merging will occur and ONLY THE LAST duplicate will be imported. + - parameter into: an `Into` clause specifying the entity type + - parameter sourceArray: the array of objects to import values from + - parameter preProcess: a closure that lets the caller tweak the internal `UniqueIDType`-to-`ImportSource` mapping to be used for importing. Callers can remove from/add to/update `mapping` and return the updated array from the closure. + - returns: The imported objects correctly associated for the `DataStack`. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func importUniqueObjects( + _ into: Into, + sourceArray: S, + preProcess: @escaping (_ mapping: [O.UniqueIDType: O.ImportSource]) throws -> [O.UniqueIDType: O.ImportSource] = { $0 } + ) async throws -> [O] + where S.Iterator.Element == O.ImportSource { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.perform( + asynchronous: { (transaction) -> [O] in + + return try transaction.importUniqueObjects( + into, + sourceArray: sourceArray, + preProcess: preProcess + ) + }, + success: { + + continuation.resume( + with: .success(self.base.fetchExisting($0)) + ) + }, + failure: continuation.resume(throwing:) + ) + } + } + + /** + Swift concurrency extension for `CoreStore.DataStack`'s `perform(asynchronous:...)` API. Performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. The event value will be the value returned from the `task` closure. Any errors thrown from inside the `task` will be wrapped in a `CoreStoreError` before being thrown from the `async` method. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`. + ``` + let result = try await dataStack.async.perform( + asynchronous: { (transaction) -> (inserted: Set, deleted: Set) in + + // ... + return ( + transaction.insertedObjects(), + transaction.deletedObjects() + ) + } + ) + let inserted = dataStack.fetchExisting(result.inserted) + let deleted = dataStack.fetchExisting(result.deleted) + ``` + - parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. + - returns: The value returned from the `task` closure. + - throws: A `CoreStoreError` value indicating the failure reason + */ + public func perform( + _ asynchronous: @escaping (AsynchronousDataTransaction) throws -> Output + ) async throws -> Output { + + return try await withCheckedThrowingContinuation { continuation in + + self.base.perform( + asynchronous: asynchronous, + completion: continuation.resume(with:) + ) + } + } +} diff --git a/Sources/DataStack+Reactive.swift b/Sources/DataStack+Reactive.swift index b6baec1..d771bb7 100644 --- a/Sources/DataStack+Reactive.swift +++ b/Sources/DataStack+Reactive.swift @@ -93,7 +93,9 @@ extension DataStack.ReactiveNamespace { - parameter storage: the storage - returns: A `Future` that emits a `StorageInterface` instance added to the `DataStack`. Note that the `StorageInterface` event value may not always be the same instance as the parameter argument if a previous `StorageInterface` was already added at the same URL and with the same configuration. */ - public func addStorage(_ storage: T) -> Future { + public func addStorage( + _ storage: T + ) -> Future { return .init { (promise) in @@ -115,7 +117,7 @@ extension DataStack.ReactiveNamespace { } /** - Reactive extension for `CoreStore.DataStack`'s `addStorage(...)` API. Asynchronously adds a `LocalStorage` to the stack. Migrations are also initiated by default. The event emits `DataStack.AddStoragePublisher.MigrationProgress` `enum` values. + Reactive extension for `CoreStore.DataStack`'s `addStorage(...)` API. Asynchronously adds a `LocalStorage` to the stack. Migrations are also initiated by default. The event emits `MigrationProgress` `enum` values. ``` dataStack.reactive .addStorage( @@ -135,7 +137,7 @@ extension DataStack.ReactiveNamespace { .store(in: &cancellables) ``` - parameter storage: the local storage - - returns: A `DataStack.AddStoragePublisher` that emits a `DataStack.AddStoragePublisher.MigrationProgress` value with metadata for migration progress. Note that the `LocalStorage` event value may not always be the same instance as the parameter argument if a previous `LocalStorage` was already added at the same URL and with the same configuration. + - returns: A `DataStack.AddStoragePublisher` that emits a `MigrationProgress` value with metadata for migration progress. Note that the `LocalStorage` event value may not always be the same instance as the parameter argument if a previous `LocalStorage` was already added at the same URL and with the same configuration. */ public func addStorage(_ storage: T) -> DataStack.AddStoragePublisher { diff --git a/Sources/DataStack.AddStoragePublisher.swift b/Sources/DataStack.AddStoragePublisher.swift index b54ee73..69ea664 100644 --- a/Sources/DataStack.AddStoragePublisher.swift +++ b/Sources/DataStack.AddStoragePublisher.swift @@ -49,7 +49,7 @@ extension DataStack { // MARK: Publisher - public typealias Output = MigrationProgress + public typealias Output = CoreStore.MigrationProgress public typealias Failure = CoreStoreError public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { @@ -63,56 +63,6 @@ extension DataStack { ) } - // MARK: - MigrationProgress - - /** - A `MigrationProgress` contains info on a `LocalStorage`'s setup progress. - - - SeeAlso: DataStack.reactive.addStorage(_:) - */ - public enum MigrationProgress { - - /** - The `LocalStorage` is currently being migrated - */ - case migrating(storage: Storage, progressObject: Progress) - - /** - The `LocalStorage` has been added to the `DataStack` and is ready for reading and writing - */ - case finished(storage: Storage, migrationRequired: Bool) - - /** - The fraction of the overall work completed by the migration. Returns a value between 0.0 and 1.0, inclusive. - */ - public var fractionCompleted: Double { - - switch self { - - case .migrating(_, let progressObject): - return progressObject.fractionCompleted - - case .finished: - return 1 - } - } - - /** - Returns `true` if the storage was successfully added to the stack, `false` otherwise. - */ - public var isCompleted: Bool { - - switch self { - - case .migrating: - return false - - case .finished: - return true - } - } - } - // MARK: - AddStorageSubscriber @@ -232,6 +182,12 @@ extension DataStack { private let storage: Storage private var subscriber: S? } + + + // MARK: Deprecated + + @available(*, deprecated, renamed: "MigrationProgress") + public typealias MigrationProgress = CoreStore.MigrationProgress } } diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index 02807a5..29c7945 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -46,7 +46,11 @@ public final class DataStack: Equatable { - parameter bundle: an optional bundle to load .xcdatamodeld models from. If not specified, the main bundle will be used. - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack. */ - public convenience init(xcodeModelName: XcodeDataModelFileName = DataStack.applicationName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) { + public convenience init( + xcodeModelName: XcodeDataModelFileName = DataStack.applicationName, + bundle: Bundle = Bundle.main, + migrationChain: MigrationChain = nil + ) { self.init( schemaHistory: SchemaHistory( @@ -79,7 +83,11 @@ public final class DataStack: Equatable { - parameter otherSchema: a list of other `DynamicSchema` instances that represent present/previous/future model versions, in any order - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack. */ - public convenience init(_ schema: DynamicSchema, _ otherSchema: DynamicSchema..., migrationChain: MigrationChain = nil) { + public convenience init( + _ schema: DynamicSchema, + _ otherSchema: DynamicSchema..., + migrationChain: MigrationChain = nil + ) { self.init( schemaHistory: SchemaHistory( @@ -108,7 +116,9 @@ public final class DataStack: Equatable { ``` - parameter schemaHistory: the `SchemaHistory` for the stack */ - public required init(schemaHistory: SchemaHistory) { + public required init( + schemaHistory: SchemaHistory + ) { self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: schemaHistory.rawModel) self.rootSavingContext = NSManagedObjectContext.rootSavingContextForCoordinator(self.coordinator) @@ -139,7 +149,9 @@ public final class DataStack: Equatable { /** Returns the entity name-to-class type mapping from the `DataStack`'s model. */ - public func entityTypesByName(for type: NSManagedObject.Type) -> [EntityName: NSManagedObject.Type] { + public func entityTypesByName( + for type: NSManagedObject.Type + ) -> [EntityName: NSManagedObject.Type] { var entityTypesByName: [EntityName: NSManagedObject.Type] = [:] for (entityIdentifier, entityDescription) in self.schemaHistory.entityDescriptionsByEntityIdentifier { @@ -163,7 +175,9 @@ public final class DataStack: Equatable { /** Returns the entity name-to-class type mapping from the `DataStack`'s model. */ - public func entityTypesByName(for type: CoreStoreObject.Type) -> [EntityName: CoreStoreObject.Type] { + public func entityTypesByName( + for type: CoreStoreObject.Type + ) -> [EntityName: CoreStoreObject.Type] { var entityTypesByName: [EntityName: CoreStoreObject.Type] = [:] for (entityIdentifier, entityDescription) in self.schemaHistory.entityDescriptionsByEntityIdentifier { @@ -191,7 +205,9 @@ public final class DataStack: Equatable { /** Returns the `NSEntityDescription` for the specified `NSManagedObject` subclass. */ - public func entityDescription(for type: NSManagedObject.Type) -> NSEntityDescription? { + public func entityDescription( + for type: NSManagedObject.Type + ) -> NSEntityDescription? { return self.entityDescription(for: Internals.EntityIdentifier(type)) } @@ -199,7 +215,9 @@ public final class DataStack: Equatable { /** Returns the `NSEntityDescription` for the specified `CoreStoreObject` subclass. */ - public func entityDescription(for type: CoreStoreObject.Type) -> NSEntityDescription? { + public func entityDescription( + for type: CoreStoreObject.Type + ) -> NSEntityDescription? { return self.entityDescription(for: Internals.EntityIdentifier(type)) } @@ -207,7 +225,9 @@ public final class DataStack: Equatable { /** Returns the `NSManagedObjectID` for the specified object URI if it exists in the persistent store. */ - public func objectID(forURIRepresentation url: URL) -> NSManagedObjectID? { + public func objectID( + forURIRepresentation url: URL + ) -> NSManagedObjectID? { return self.coordinator.managedObjectID(forURIRepresentation: url) } @@ -236,7 +256,9 @@ public final class DataStack: Equatable { - returns: the `StorageInterface` added to the stack */ @discardableResult - public func addStorageAndWait(_ storage: T) throws -> T { + public func addStorageAndWait( + _ storage: T + ) throws -> T { do { @@ -275,7 +297,9 @@ public final class DataStack: Equatable { - returns: the local storage added to the stack. Note that this may not always be the same instance as the parameter argument if a previous `LocalStorage` was already added at the same URL and with the same configuration. */ @discardableResult - public func addStorageAndWait(_ storage: T) throws -> T { + public func addStorageAndWait( + _ storage: T + ) throws -> T { return try self.coordinator.performSynchronously { @@ -374,7 +398,9 @@ public final class DataStack: Equatable { Prepares deinitializing the `DataStack` by removing all persistent stores. This is not necessary, but can help silence SQLite warnings when actively releasing and recreating `DataStack`s. - parameter completion: the closure to execute after all persistent stores are removed */ - public func unsafeRemoveAllPersistentStores(completion: @escaping () -> Void = {}) { + public func unsafeRemoveAllPersistentStores( + completion: @escaping () -> Void = {} + ) { let coordinator = self.coordinator coordinator.performAsynchronously { @@ -441,7 +467,7 @@ public final class DataStack: Equatable { internal let mainContext: NSManagedObjectContext internal let schemaHistory: SchemaHistory internal let childTransactionQueue = DispatchQueue.serial("com.coreStore.dataStack.childTransactionQueue", qos: .utility) - internal let storeMetadataUpdateQueue = DispatchQueue.concurrent("com.coreStore.persistentStoreBarrierQueue", qos: .userInteractive) + internal let storeMetadataLock: NSRecursiveLock = .init() internal let migrationQueue: OperationQueue = Internals.with { let migrationQueue = OperationQueue() @@ -452,56 +478,68 @@ public final class DataStack: Equatable { return migrationQueue } - internal func persistentStoreForStorage(_ storage: StorageInterface) -> NSPersistentStore? { + internal func persistentStoreForStorage( + _ storage: StorageInterface + ) -> NSPersistentStore? { return self.coordinator.persistentStores .filter { $0.storageInterface === storage } .first } - internal func persistentStores(for entityIdentifier: Internals.EntityIdentifier) -> [NSPersistentStore]? { - - var returnValue: [NSPersistentStore]? = nil - self.storeMetadataUpdateQueue.sync(flags: .barrier) { - - returnValue = self.finalConfigurationsByEntityIdentifier[entityIdentifier]? - .map({ self.persistentStoresByFinalConfiguration[$0]! }) ?? [] + internal func persistentStores( + for entityIdentifier: Internals.EntityIdentifier + ) -> [NSPersistentStore]? { + + self.storeMetadataLock.lock() + defer { + self.storeMetadataLock.unlock() } - return returnValue + return self.finalConfigurationsByEntityIdentifier[entityIdentifier]? + .map({ self.persistentStoresByFinalConfiguration[$0]! }) ?? [] } - internal func persistentStore(for entityIdentifier: Internals.EntityIdentifier, configuration: ModelConfiguration, inferStoreIfPossible: Bool) -> (store: NSPersistentStore?, isAmbiguous: Bool) { - - return self.storeMetadataUpdateQueue.sync(flags: .barrier) { () -> (store: NSPersistentStore?, isAmbiguous: Bool) in - - let configurationsForEntity = self.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? [] - if let configuration = configuration { - - if configurationsForEntity.contains(configuration) { - - return (store: self.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false) - } - else if !inferStoreIfPossible { - - return (store: nil, isAmbiguous: false) - } + internal func persistentStore( + for entityIdentifier: Internals.EntityIdentifier, + configuration: ModelConfiguration, + inferStoreIfPossible: Bool + ) -> (store: NSPersistentStore?, isAmbiguous: Bool) { + + self.storeMetadataLock.lock() + defer { + self.storeMetadataLock.unlock() + } + let configurationsForEntity = self.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? [] + if let configuration = configuration { + + if configurationsForEntity.contains(configuration) { + + return (store: self.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false) } - - switch configurationsForEntity.count { - - case 0: + else if !inferStoreIfPossible { + return (store: nil, isAmbiguous: false) - - case 1 where inferStoreIfPossible: - return (store: self.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false) - - default: - return (store: nil, isAmbiguous: true) } } + + switch configurationsForEntity.count { + + case 0: + return (store: nil, isAmbiguous: false) + + case 1 where inferStoreIfPossible: + return (store: self.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false) + + default: + return (store: nil, isAmbiguous: true) + } } - internal func createPersistentStoreFromStorage(_ storage: StorageInterface, finalURL: URL?, finalStoreOptions: [AnyHashable: Any]?) throws -> NSPersistentStore { + internal func createPersistentStoreFromStorage( + _ storage: StorageInterface, + finalURL: URL?, + finalStoreOptions: [AnyHashable: Any]? + ) throws -> NSPersistentStore { let persistentStore = try self.coordinator.addPersistentStore( ofType: type(of: storage).storeType, @@ -510,8 +548,13 @@ public final class DataStack: Equatable { options: finalStoreOptions ) persistentStore.storageInterface = storage - - self.storeMetadataUpdateQueue.async(flags: .barrier) { + + do { + + self.storeMetadataLock.lock() + defer { + self.storeMetadataLock.unlock() + } let configurationName = persistentStore.configurationName self.persistentStoresByFinalConfiguration[configurationName] = persistentStore @@ -534,7 +577,9 @@ public final class DataStack: Equatable { return persistentStore } - internal func entityDescription(for entityIdentifier: Internals.EntityIdentifier) -> NSEntityDescription? { + internal func entityDescription( + for entityIdentifier: Internals.EntityIdentifier + ) -> NSEntityDescription? { return self.schemaHistory.entityDescriptionsByEntityIdentifier[entityIdentifier] } diff --git a/Sources/MigrationProgress.swift b/Sources/MigrationProgress.swift new file mode 100644 index 0000000..c46e4bf --- /dev/null +++ b/Sources/MigrationProgress.swift @@ -0,0 +1,79 @@ +// +// MigrationProgress.swift +// CoreStore +// +// Copyright © 2021 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import CoreData +import Foundation + + +// MARK: - MigrationProgress + +/** + A `MigrationProgress` contains info on a `LocalStorage`'s setup progress. + + - SeeAlso: DataStack.reactive.addStorage(_:) + - SeeAlso: DataStack.async.addStorage(_:) + */ +public enum MigrationProgress { + + /** + The `LocalStorage` is currently being migrated + */ + case migrating(storage: T, progressObject: Progress) + + /** + The `LocalStorage` has been added to the `DataStack` and is ready for reading and writing + */ + case finished(storage: T, migrationRequired: Bool) + + /** + The fraction of the overall work completed by the migration. Returns a value between 0.0 and 1.0, inclusive. + */ + public var fractionCompleted: Double { + + switch self { + + case .migrating(_, let progressObject): + return progressObject.fractionCompleted + + case .finished: + return 1 + } + } + + /** + Returns `true` if the storage was successfully added to the stack, `false` otherwise. + */ + public var isCompleted: Bool { + + switch self { + + case .migrating: + return false + + case .finished: + return true + } + } +}