From 223707159c641202cbfa457aa906bd2fae05bc48 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 6 May 2021 08:28:44 +0900 Subject: [PATCH] create AppGroupsManager --- CoreStore.xcodeproj/project.pbxproj | 10 + Playground_iOS.playground/Contents.swift | 2 - Sources/BaseDataTransaction.swift | 5 +- Sources/CoreStoreDefaults.swift | 5 +- Sources/CoreStoreError.swift | 2 +- Sources/CoreStoreManagedObject.swift | 5 +- Sources/CoreStoreSchema.swift | 5 +- Sources/DataStack+Migration.swift | 22 +- Sources/DataStack+Transaction.swift | 5 +- Sources/DataStack.swift | 19 +- Sources/InMemoryStore.swift | 4 +- Sources/Internals.AppGroupsManager.swift | 406 ++++++++++++++++++ Sources/Internals.swift | 24 +- Sources/NSManagedObjectContext+Setup.swift | 4 +- .../NSManagedObjectContext+Transaction.swift | 2 +- Sources/SQLiteStore.swift | 58 ++- Sources/StorageInterface.swift | 4 +- 17 files changed, 545 insertions(+), 37 deletions(-) create mode 100644 Sources/Internals.AppGroupsManager.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 8331ce2..4f18530 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -586,6 +586,10 @@ B56E4EE523CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; B56E4EE623CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; B56E4EE723CEDF0900E1708C /* Field.Virtual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */; }; + B56ED34C263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */; }; + B56ED34D263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */; }; + B56ED34E263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */; }; + B56ED34F263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */; }; B57D27BE1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27BF1D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; B57D27C01D0BBE8200539C58 /* BaseTestDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */; }; @@ -1129,6 +1133,7 @@ B56E4ED823CEB8E700E1708C /* FieldStorableType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStorableType.swift; sourceTree = ""; }; B56E4EDE23CEBCF000E1708C /* FieldOptionalType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldOptionalType.swift; sourceTree = ""; }; B56E4EE323CEDF0900E1708C /* Field.Virtual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Virtual.swift; sourceTree = ""; }; + B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.AppGroupsManager.swift; sourceTree = ""; }; B57D27BD1D0BBE8200539C58 /* BaseTestDataTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestDataTestCase.swift; sourceTree = ""; }; B57D27C11D0BC20100539C58 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; B57E6FA123D302FA000FD031 /* Field.Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.Relationship.swift; sourceTree = ""; }; @@ -1973,6 +1978,7 @@ B533C4DA1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift */, B5DE522A230BD7CC00A22534 /* Internals.swift */, B50C3F0223D1B01C00B29880 /* Internals.AnyFieldCoder.swift */, + B56ED34B263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift */, B5C976E61C6E3A5900B1AF90 /* Internals.CoreStoreFetchedResultsController.swift */, B5474D142227C08700B21FEC /* Internals.CoreStoreFetchRequest.swift */, B5BF7FAC234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift */, @@ -2391,6 +2397,7 @@ B56923F01EB827F6007C4DC9 /* XcodeSchemaMappingProvider.swift in Sources */, B5E84F121AFF847B0064E85B /* OrderBy.swift in Sources */, B5635D142356C39500B80E6B /* DiffableDataSource.CollectionViewAdapter-UIKit.swift in Sources */, + B56ED34C263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */, B5E84F361AFF85470064E85B /* NSManagedObjectContext+Setup.swift in Sources */, B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, @@ -2644,6 +2651,7 @@ B546F9741C9C553300D5AC55 /* SetupResult.swift in Sources */, B5831F4022126FEC00D8604C /* KeyPathGenericBindings.swift in Sources */, B53CA9A31EF1EF1600E0F440 /* PartialObject.swift in Sources */, + B56ED34D263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */, 82BA18DD1C4BBE1400A0916E /* NSFetchedResultsController+Convenience.swift in Sources */, B5831F432212700400D8604C /* Where.Expression.swift in Sources */, B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, @@ -2897,6 +2905,7 @@ B5E1B5AC1CAA49E2007FD580 /* CSDataStack+Migrating.swift in Sources */, B52DD1961BE1F92500949AFE /* DataStack.swift in Sources */, B5ECDBFD1CA804FD00C7F112 /* NSManagedObjectContext+ObjectiveC.swift in Sources */, + B56ED34F263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */, B52DD1BD1BE1F94300949AFE /* NSManagedObject+Convenience.swift in Sources */, B5831F4222126FED00D8604C /* KeyPathGenericBindings.swift in Sources */, B53CA9A51EF1EF1600E0F440 /* PartialObject.swift in Sources */, @@ -3150,6 +3159,7 @@ B5831F4122126FEC00D8604C /* KeyPathGenericBindings.swift in Sources */, B53CA9A41EF1EF1600E0F440 /* PartialObject.swift in Sources */, B5202CFD1C046E8400DED140 /* NSFetchedResultsController+Convenience.swift in Sources */, + B56ED34E263FF64B00ACCCB8 /* Internals.AppGroupsManager.swift in Sources */, B5FE4DA91C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B5831F442212700500D8604C /* Where.Expression.swift in Sources */, B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, diff --git a/Playground_iOS.playground/Contents.swift b/Playground_iOS.playground/Contents.swift index 92b37e6..9115679 100644 --- a/Playground_iOS.playground/Contents.swift +++ b/Playground_iOS.playground/Contents.swift @@ -92,5 +92,3 @@ dataStack.addStorage( } } ) - - diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index cae9c84..66279ef 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -429,7 +429,10 @@ public /*abstract*/ class BaseDataTransaction { internal let context: NSManagedObjectContext internal let transactionQueue: DispatchQueue - internal let childTransactionQueue = DispatchQueue.serial("com.corestore.datastack.childTransactionQueue", qos: .utility) + internal let childTransactionQueue = DispatchQueue.serial( + Internals.libReverseDomain("BaseDataTransaction.childTransactionQueue"), + qos: .utility + ) internal let supportsUndo: Bool internal let bypassesQueueing: Bool internal var isCommitted = false diff --git a/Sources/CoreStoreDefaults.swift b/Sources/CoreStoreDefaults.swift index 26ddec6..6450803 100644 --- a/Sources/CoreStoreDefaults.swift +++ b/Sources/CoreStoreDefaults.swift @@ -69,7 +69,10 @@ public enum CoreStoreDefaults { // MARK: Private - private static let defaultStackBarrierQueue = DispatchQueue.concurrent("com.coreStore.defaultStackBarrierQueue", qos: .userInteractive) + private static let defaultStackBarrierQueue = DispatchQueue.concurrent( + Internals.libReverseDomain("CoreStoreDefaults.defaultStackBarrierQueue"), + qos: .userInteractive + ) private static var defaultStackInstance: DataStack? } diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index e2a5dd9..c22ce8b 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -270,7 +270,7 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { The `NSError` error domain string for `CSError`. */ @nonobjc -public let CoreStoreErrorDomain = "com.corestore.error" +public let CoreStoreErrorDomain = Internals.libReverseDomain("error") // MARK: - CoreStoreErrorCode diff --git a/Sources/CoreStoreManagedObject.swift b/Sources/CoreStoreManagedObject.swift index d663ecb..3ef61ef 100644 --- a/Sources/CoreStoreManagedObject.swift +++ b/Sources/CoreStoreManagedObject.swift @@ -48,6 +48,9 @@ import Foundation private enum Static { - static let queue = DispatchQueue.concurrent("com.coreStore.coreStoreManagerObjectBarrierQueue", qos: .userInteractive) + static let queue = DispatchQueue.concurrent( + Internals.libReverseDomain("CoreStoreManagerObject.barrierQueue"), + qos: .userInteractive + ) static var cache: [ObjectIdentifier: [KeyPathString: Set]] = [:] } diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index 98150f5..0d2ce2b 100644 --- a/Sources/CoreStoreSchema.swift +++ b/Sources/CoreStoreSchema.swift @@ -254,7 +254,10 @@ public final class CoreStoreSchema: DynamicSchema { // MARK: Private - private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.coreStoreDataModelBarrierQueue", qos: .userInteractive) + private static let barrierQueue = DispatchQueue.concurrent( + Internals.libReverseDomain("CoreStoreSchema.barrierQueue"), + qos: .userInteractive + ) private let allEntities: Set diff --git a/Sources/DataStack+Migration.swift b/Sources/DataStack+Migration.swift index 856e376..e1e9ff5 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -115,6 +115,24 @@ extension DataStack { return self.coordinator.performSynchronously { + do { + + try storage.cs_willBeAdded(toDataStack: self) + } + catch { + + let storeError = CoreStoreError(error) + Internals.log( + storeError, + "Failed to preparing to add \(Internals.typeName(storage)) at \"\(fileURL)\"." + ) + DispatchQueue.main.async { + + completion(.failure(storeError)) + } + return nil + } + if let _ = self.persistentStoreForStorage(storage) { DispatchQueue.main.async { @@ -562,7 +580,7 @@ extension DataStack { do { let timerQueue = DispatchQueue( - label: "DataStack.lightweightMigration.timerQueue", + label: Internals.libReverseDomain("DataStack.lightweightMigration.timerQueue"), qos: .utility, attributes: [] ) @@ -614,7 +632,7 @@ extension DataStack { } let fileManager = FileManager.default let temporaryDirectoryURL = fileManager.temporaryDirectory - .appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack") + .appendingPathComponent(Internals.bundleTag()) .appendingPathComponent(ProcessInfo().globallyUniqueString) try! fileManager.createDirectory( diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index a7eae2d..52b3844 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -148,7 +148,10 @@ extension DataStack { return UnsafeDataTransaction( mainContext: self.rootSavingContext, - queue: DispatchQueue.serial("com.coreStore.dataStack.unsafeTransactionQueue", qos: .userInitiated), + queue: DispatchQueue.serial( + Internals.libReverseDomain("UnsafeDataTransaction.queue"), + qos: .userInitiated + ), supportsUndo: supportsUndo ) } diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index 4d23d19..b49298c 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -440,15 +440,24 @@ public final class DataStack: Equatable { internal let rootSavingContext: NSManagedObjectContext 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 childTransactionQueue = DispatchQueue.serial( + Internals.libReverseDomain("DataStack.childTransactionQueue"), + qos: .utility + ) + internal let storeMetadataUpdateQueue = DispatchQueue.concurrent( + Internals.libReverseDomain("DataStack.persistentStoreBarrierQueue"), + qos: .userInteractive + ) internal let migrationQueue: OperationQueue = Internals.with { let migrationQueue = OperationQueue() migrationQueue.maxConcurrentOperationCount = 1 - migrationQueue.name = "com.coreStore.migrationOperationQueue" + migrationQueue.name = Internals.libReverseDomain("DataStack.migrationOperationQueue") migrationQueue.qualityOfService = .utility - migrationQueue.underlyingQueue = DispatchQueue.serial("com.coreStore.migrationQueue", qos: .userInitiated) + migrationQueue.underlyingQueue = DispatchQueue.serial( + Internals.libReverseDomain("DataStack.migrationQueue"), + qos: .userInitiated + ) return migrationQueue } @@ -530,7 +539,7 @@ public final class DataStack: Equatable { self.finalConfigurationsByEntityIdentifier[entityIdentifier]?.insert(configurationName) } } - storage.cs_didAddToDataStack(self) + try storage.cs_didAddToDataStack(self) return persistentStore } diff --git a/Sources/InMemoryStore.swift b/Sources/InMemoryStore.swift index 3b3a5f2..4016ae6 100644 --- a/Sources/InMemoryStore.swift +++ b/Sources/InMemoryStore.swift @@ -71,7 +71,7 @@ public final class InMemoryStore: StorageInterface { /** Do not call directly. Used by the `DataStack` internally. */ - public func cs_didAddToDataStack(_ dataStack: DataStack) { + public func cs_didAddToDataStack(_ dataStack: DataStack) throws { self.dataStack = dataStack } @@ -79,7 +79,7 @@ public final class InMemoryStore: StorageInterface { /** Do not call directly. Used by the `DataStack` internally. */ - public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { + public func cs_didRemoveFromDataStack(_ dataStack: DataStack) throws { self.dataStack = nil } diff --git a/Sources/Internals.AppGroupsManager.swift b/Sources/Internals.AppGroupsManager.swift new file mode 100644 index 0000000..3a30eef --- /dev/null +++ b/Sources/Internals.AppGroupsManager.swift @@ -0,0 +1,406 @@ +// +// Internals.AppGroupsManager.swift +// CoreStore +// +// Copyright © 2020 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 + + +// MARK: - Internals + +extension Internals { + + // MARK: - AppGroupsManager + + internal enum AppGroupsManager { + + // MARK: Internal + + internal typealias BundleID = String + + internal typealias StoreID = UUID + + @discardableResult + internal static func register( + appGroupIdentifier: String, + subdirectory: String?, + fileName: String + ) throws -> StoreID { + + let bundleID = self.bundleID() + let indexMetadataURL = self.indexMetadataURL( + appGroupIdentifier: appGroupIdentifier + ) + return try self.metadata( + forWritingAt: indexMetadataURL, + initializer: IndexMetadata.init, + { metadata in + + return metadata.fetchOrCreateStoreID( + bundleID: bundleID, + subdirectory: subdirectory, + fileName: fileName + ) + } + ) + } + + internal static func existingToken( + appGroupIdentifier: String, + subdirectory: String, + fileName: String + ) throws -> NSPersistentHistoryToken? { + + let bundleID = self.bundleID() + let indexMetadataURL = self.indexMetadataURL( + appGroupIdentifier: appGroupIdentifier + ) + guard + let storeID = try self.metadata( + forReadingAt: indexMetadataURL, + { (metadata: IndexMetadata) in + + return metadata.fetchStoreID( + bundleID: bundleID, + subdirectory: subdirectory, + fileName: fileName + ) + } + ) + else { + + return nil + } + let storageMetadataURL = self.storageMetadataURL( + appGroupIdentifier: appGroupIdentifier, + bundleID: bundleID, + storeID: storeID + ) + return try self.metadata( + forReadingAt: storageMetadataURL, + { (metadata: StorageMetadata) in + + return metadata.persistentHistoryToken + } + ) + } + + internal static func setExistingToken( + _ newToken: NSPersistentHistoryToken, + appGroupIdentifier: String, + subdirectory: String, + fileName: String + ) throws { + + let bundleID = self.bundleID() + let indexMetadataURL = self.indexMetadataURL( + appGroupIdentifier: appGroupIdentifier + ) + guard + let storeID = try self.metadata( + forReadingAt: indexMetadataURL, + { (metadata: IndexMetadata) in + + return metadata.fetchStoreID( + bundleID: bundleID, + subdirectory: subdirectory, + fileName: fileName + ) + } + ) + else { + + return + } + let storageMetadataURL = self.storageMetadataURL( + appGroupIdentifier: appGroupIdentifier, + bundleID: bundleID, + storeID: storeID + ) + try self.metadata( + forWritingAt: storageMetadataURL, + initializer: StorageMetadata.init, + { metadata in + + metadata.persistentHistoryToken = newToken + } + ) + } + + + // MARK: Private + + private static func appGroupContainerURL( + appGroupIdentifier: String + ) -> URL { + + guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { + + Internals.abort("Failed to join app group named \"\(appGroupIdentifier)\". Make sure that this app is registered into this app group through the entitlements file.") + } + return containerURL + .appendingPathComponent(Internals.libReverseDomain(), isDirectory: true) + } + + private static func indexMetadataURL( + appGroupIdentifier: String + ) -> URL { + + return self.appGroupContainerURL(appGroupIdentifier: appGroupIdentifier) + .appendingPathComponent("index.meta", isDirectory: false) + } + + private static func storageMetadataURL( + appGroupIdentifier: String, + bundleID: BundleID, + storeID: StoreID + ) -> URL { + + return self + .appGroupContainerURL(appGroupIdentifier: appGroupIdentifier) + .appendingPathComponent(bundleID, isDirectory: true) + .appendingPathComponent(storeID.uuidString, isDirectory: false) + .appendingPathExtension("meta") + } + + private static func metadata( + forReadingAt url: URL, + _ task: @escaping (Metadata) -> Result? + ) throws -> Result? { + + let fileCoordinator = NSFileCoordinator() + var fileCoordinatorError: NSError? + var accessorError: Error? + var result: Result? + fileCoordinator.coordinate( + readingItemAt: url, + options: .withoutChanges, + error: &fileCoordinatorError, + byAccessor: { url in + + do { + + guard let metadata: Metadata = try self.loadMetadata(lockedURL: url) else { + + return + } + result = task(metadata) + } + catch { + + accessorError = error + } + } + ) + if let fileCoordinatorError = fileCoordinatorError { + + throw CoreStoreError(fileCoordinatorError) + } + else if let accessorError = accessorError { + + throw CoreStoreError(accessorError) + } + else { + + return result + } + } + + private static func metadata( + forWritingAt url: URL, + initializer: @escaping () -> Metadata, + _ task: @escaping (inout Metadata) -> Result + ) throws -> Result { + + let fileManager = FileManager.default + try? fileManager.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true, + attributes: nil + ) + + let fileCoordinator = NSFileCoordinator() + var fileCoordinatorError: NSError? + var accessorError: Error? + var result: Result? + fileCoordinator.coordinate( + writingItemAt: url, + options: .forReplacing, + error: &fileCoordinatorError, + byAccessor: { url in + + do { + + var metadata: Metadata = try self.loadMetadata(lockedURL: url) + ?? initializer() + result = task(&metadata) + try self.saveMetadata(metadata, lockedURL: url) + } + catch { + + accessorError = error + } + } + ) + if let fileCoordinatorError = fileCoordinatorError { + + throw CoreStoreError(fileCoordinatorError) + } + else if let accessorError = accessorError { + + throw CoreStoreError(accessorError) + } + else { + + return result! + } + } + + private static func bundleID() -> String { + + guard let bundleID = Bundle.main.bundleIdentifier else { + + Internals.abort("App Group containers can only be used for bundled projects.") + } + return bundleID + } + + private static func loadMetadata( + lockedURL url: URL + ) throws -> Metadata? { + + let decoder = PropertyListDecoder() + guard let data = try? Data(contentsOf: url) else { + + return nil + } + return try decoder.decode(Metadata.self, from: data) + } + + private static func saveMetadata( + _ metadata: Metadata, + lockedURL url: URL + ) throws { + + let encoder = PropertyListEncoder() + let data = try encoder.encode(metadata) + try data.write(to: url, options: .atomic) + } + + + // MARK: - IndexMetadata + + fileprivate struct IndexMetadata: Codable { + + // MARK: FilePrivate + + fileprivate func fetchStoreID( + bundleID: BundleID, + subdirectory: String?, + fileName: String + ) -> StoreID? { + + let fileTag = Self.createFileTag(subdirectory: subdirectory, fileName: fileName) + return self.contents[bundleID, default: [:]][fileTag] + } + + fileprivate mutating func fetchOrCreateStoreID( + bundleID: BundleID, + subdirectory: String?, + fileName: String + ) -> StoreID { + + let fileTag = Self.createFileTag(subdirectory: subdirectory, fileName: fileName) + return self.contents[bundleID, default: [:]][fileTag, default: UUID()] + } + + // MARK: Codable + + private enum CodingKeys: String, CodingKey { + + case contents = "contents" + } + + // MARK: Private + + private typealias FileTag = String + + private var contents: [BundleID: [FileTag: UUID]] = [:] + + private static func createFileTag(subdirectory: String?, fileName: String) -> FileTag { + + guard let subdirectory = subdirectory else { + + return fileName + } + return (subdirectory as NSString).appendingPathComponent(fileName) + } + } + + + // MARK: - StorageMetadata + + fileprivate struct StorageMetadata: Codable { + + // MARK: FilePrivate + + fileprivate var persistentHistoryToken: NSPersistentHistoryToken? { + + get { + + return self.persistentHistoryTokenData.flatMap { + + return try! NSKeyedUnarchiver.unarchivedObject( + ofClass: NSPersistentHistoryToken.self, + from: $0 + ) + } + } + set { + + self.persistentHistoryTokenData = newValue.map { + + return try! NSKeyedArchiver.archivedData( + withRootObject: $0, + requiringSecureCoding: true + ) + } + } + } + + + // MARK: Codable + + private enum CodingKeys: String, CodingKey { + + case persistentHistoryTokenData = "persistent_history_token_data" + } + + + // MARK: Private + + private var persistentHistoryTokenData: Data? + } + } +} + diff --git a/Sources/Internals.swift b/Sources/Internals.swift index 0c48a84..24b8b29 100644 --- a/Sources/Internals.swift +++ b/Sources/Internals.swift @@ -29,11 +29,31 @@ import Foundation @usableFromInline internal enum Internals { + + // MARK: Namespacing + + @inline(__always) + internal static func libReverseDomain() -> String { + + return "com.johnestropia.corestore" + } + + @inline(__always) + internal static func libReverseDomain(_ suffix: String) -> String { + + return "com.johnestropia.corestore.\(suffix)" + } + + @inline(__always) + internal static func bundleTag() -> String { + + return Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack" + } // MARK: Associated Objects - - @inline(__always) + /// type(of:) doesn't return the dynamic type anymore, use this to guarantee correct dispatch of class methods + @inline(__always) internal static func dynamicObjectType(of instance: T) -> T.Type { return object_getClass(instance) as! T.Type diff --git a/Sources/NSManagedObjectContext+Setup.swift b/Sources/NSManagedObjectContext+Setup.swift index cb82bce..4e721da 100644 --- a/Sources/NSManagedObjectContext+Setup.swift +++ b/Sources/NSManagedObjectContext+Setup.swift @@ -67,7 +67,7 @@ extension NSManagedObjectContext { context.persistentStoreCoordinator = coordinator context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.undoManager = nil - context.setupForCoreStoreWithContextName("com.corestore.rootcontext") + context.setupForCoreStoreWithContextName(Internals.libReverseDomain("rootContext")) #if os(iOS) || os(macOS) @@ -102,7 +102,7 @@ extension NSManagedObjectContext { context.parent = rootContext context.mergePolicy = NSRollbackMergePolicy context.undoManager = nil - context.setupForCoreStoreWithContextName("com.corestore.maincontext") + context.setupForCoreStoreWithContextName(Internals.libReverseDomain("mainContext")) context.observerForDidSaveNotification = Internals.NotificationObserver( notificationName: NSNotification.Name.NSManagedObjectContextDidSave, object: rootContext, diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index 4bbcdb5..3992d44 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -132,7 +132,7 @@ extension NSManagedObjectContext { let context = NSManagedObjectContext(concurrencyType: concurrencyType) context.parent = self context.parentStack = self.parentStack - context.setupForCoreStoreWithContextName("com.corestore.temporarycontext") + context.setupForCoreStoreWithContextName(Internals.libReverseDomain("temporaryContext")) context.shouldCascadeSavesToParent = (self.parentStack?.rootSavingContext == self) context.retainsRegisteredObjects = true diff --git a/Sources/SQLiteStore.swift b/Sources/SQLiteStore.swift index 6f4ad1c..560f50b 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -83,7 +83,7 @@ public final class SQLiteStore: LocalStorage { } /** - Initializes an SQLite store interface with a device-wide shared persistent store using a registered App Group Identifier. This store does not use remote persistent history tracking, and should be used only in the context of App-Extension shared stores. + Initializes an SQLite store interface with a device-wide shared persistent store using a registered App Group Identifier. This store does not use remote persistent history tracking, and should be used only in the context of App Groups and App Extensions. - Important: The app will be force-terminated if the `appGroupIdentifier` is not registered for the app. - parameter appGroupIdentifier: the App Group identifier registered for this application. The app will be force-terminated if this identifier is not registered for the app. @@ -200,20 +200,51 @@ public final class SQLiteStore: LocalStorage { public let configuration: ModelConfiguration /** - The options dictionary for the `NSPersistentStore`. For `SQLiteStore`s, this is always set to - ``` - [NSSQLitePragmasOption: ["journal_mode": "WAL"]] - ``` + The options dictionary for the `NSPersistentStore`. */ - public let storeOptions: [AnyHashable: Any]? = [ - NSSQLitePragmasOption: ["journal_mode": "WAL"], - NSBinaryStoreInsecureDecodingCompatibilityOption: true - ] + public var storeOptions: [AnyHashable: Any]? { + + var options: [AnyHashable: Any] = [ + NSSQLitePragmasOption: ["journal_mode": "WAL"], + NSBinaryStoreInsecureDecodingCompatibilityOption: true + ] + switch self.container { + + case .appGroup: + options[NSPersistentHistoryTrackingKey] = true + if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { + + options[NSPersistentStoreRemoteChangeNotificationPostOptionKey] = true + } + #warning("TODO: handle remote changes for iOS 11 & 12") + + case .custom, + .default, + .legacy: + break + } + return options + } /** Do not call directly. Used by the `DataStack` internally. */ - public func cs_didAddToDataStack(_ dataStack: DataStack) { + public func cs_didAddToDataStack(_ dataStack: DataStack) throws { + + switch self.container { + + case .appGroup(let appGroupIdentifier, let subdirectory, let fileName): + try Internals.AppGroupsManager.register( + appGroupIdentifier: appGroupIdentifier, + subdirectory: subdirectory, + fileName: fileName + ) + + case .custom, + .default, + .legacy: + break + } self.dataStack = dataStack } @@ -297,7 +328,7 @@ public final class SQLiteStore: LocalStorage { do { let trashURL = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) - .appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", isDirectory: true) + .appendingPathComponent(Internals.bundleTag(), isDirectory: true) .appendingPathComponent("trash", isDirectory: true) try fileManager.createDirectory( at: trashURL, @@ -353,6 +384,8 @@ public final class SQLiteStore: LocalStorage { // MARK: Internal + internal let container: Container + internal static let defaultRootDirectory: URL = Internals.with { #if os(tvOS) @@ -366,7 +399,7 @@ public final class SQLiteStore: LocalStorage { in: .userDomainMask).first! return defaultSystemDirectory.appendingPathComponent( - Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", + Internals.bundleTag(), isDirectory: true ) } @@ -404,7 +437,6 @@ public final class SQLiteStore: LocalStorage { // MARK: Private private weak var dataStack: DataStack? - private let container: Container private init( container: Container, diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index a94a49f..105fd41 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -54,12 +54,12 @@ public protocol StorageInterface: AnyObject { /** Do not call directly. Used by the `DataStack` internally. */ - func cs_didAddToDataStack(_ dataStack: DataStack) + func cs_didAddToDataStack(_ dataStack: DataStack) throws /** Do not call directly. Used by the `DataStack` internally. */ - func cs_didRemoveFromDataStack(_ dataStack: DataStack) + func cs_didRemoveFromDataStack(_ dataStack: DataStack) throws }