ios/AuthenticatorBridgeKit/AuthenticatorBridgeDataStore.swift

166 lines
6.4 KiB
Swift

import BitwardenKit
@preconcurrency import CoreData
// MARK: - AuthenticatorStoreType
/// A type of data store.
///
public enum AuthenticatorBridgeStoreType {
/// The data store is stored only in memory and isn't persisted to the device. This is used for
/// unit testing.
case memory
/// The data store is persisted to the device.
case persisted
}
// MARK: - AuthenticatorDataStore
/// The CoreData model name used within `AuthenticatorBridgeDataStore`.
private let authenticatorBridgeModelName = "Bitwarden-Authenticator"
/// A data store that manages persisting data across app launches in Core Data.
/// This is currently marked `@unchecked Sendable` because of how we ensure thread safety of the `backgroundContext`
/// property. Once we have a minimum version of iOS 16 or higher, we can migrate to the `Synchronization` framework
/// and make this more properly `Sendable`.
///
public final nonisolated class AuthenticatorBridgeDataStore: @unchecked Sendable {
// MARK: Type Properties
/// The managed object model representing the entities in the database schema. CoreData throws
/// warnings if this is instantiated multiple times (e.g. in tests), which is fixed by making
/// it static.
private static let managedObjectModel: NSManagedObjectModel = {
#if SWIFT_PACKAGE
let bundle = Bundle.module
#else
let bundle = Bundle(for: AuthenticatorBridgeDataStore.self)
#endif
let modelURL = bundle.url(forResource: authenticatorBridgeModelName, withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
return managedObjectModel
}()
// MARK: Properties
/// A thread-safe lock for `backgroundContext`. Once we have a minimum of iOS 16, we can use an
/// `OSAllocatedUnfairLock` instead.
private let _backgroundContextLock = DispatchQueue(label: "backgroundContext.lock")
/// A private backing for `backgroundContext`. The `backgroundContext` variable provides thread-safe access, and
/// is what should be used. Once we have a minimum of iOS 16, this can be converted to an `OSAllocatedUnfairLock`,
/// and remove the need for the additional `_backgroundContextLock`.
private var _backgroundContext: NSManagedObjectContext?
/// A managed object context which executes on a background queue.
/// This is the thread-safe version of the backing variable `_backgroundContext`,
/// and initializes that property lazily.
public var backgroundContext: NSManagedObjectContext {
_backgroundContextLock.sync {
if let context = _backgroundContext {
return context
}
let newContext = persistentContainer.newBackgroundContext()
newContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
_backgroundContext = newContext
return newContext
}
}
/// The service used by the application to report non-fatal errors.
let errorReporter: ErrorReporter
/// The Core Data persistent container.
public let persistentContainer: NSPersistentContainer
// MARK: Initialization
/// Initialize a `AuthenticatorBridgeDataStore`.
///
/// - Parameters:
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - groupIdentifier: The app group identifier for the shared resource.
/// - storeType: The type of store to create.
///
public init(
errorReporter: ErrorReporter,
groupIdentifier: String,
storeType: AuthenticatorBridgeStoreType = .persisted,
) {
self.errorReporter = errorReporter
persistentContainer = NSPersistentContainer(
name: authenticatorBridgeModelName,
managedObjectModel: Self.managedObjectModel,
)
let storeDescription: NSPersistentStoreDescription
switch storeType {
case .memory:
storeDescription = NSPersistentStoreDescription(url: URL(fileURLWithPath: "/dev/null"))
case .persisted:
let storeURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)!
.appendingPathComponent("\(authenticatorBridgeModelName).sqlite")
storeDescription = NSPersistentStoreDescription(url: storeURL)
}
persistentContainer.persistentStoreDescriptions = [storeDescription]
persistentContainer.loadPersistentStores { _, error in
if let error {
errorReporter.log(error: error)
}
}
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
}
// MARK: Methods
/// Executes a batch delete request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch delete request to perform.
///
public func executeBatchDelete(_ request: NSBatchDeleteRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: request,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
/// Executes a batch insert request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch insert request to perform.
///
public func executeBatchInsert(_ request: NSBatchInsertRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchInsertRequest: request,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
/// Executes a batch delete and batch insert request and merges the changes into the background
/// and view contexts.
///
/// - Parameters:
/// - deleteRequest: The batch delete request to perform.
/// - insertRequest: The batch insert request to perform.
///
public func executeBatchReplace(
deleteRequest: NSBatchDeleteRequest,
insertRequest: NSBatchInsertRequest,
) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: deleteRequest,
batchInsertRequest: insertRequest,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
}