// // PersistentContainer.swift // Reader // // Created by Shadowfacts on 12/24/21. // import CoreData import Fervor import OSLog class PersistentContainer: NSPersistentContainer { private static let managedObjectModel: NSManagedObjectModel = { let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")! return NSManagedObjectModel(contentsOf: url)! }() private(set) lazy var backgroundContext: NSManagedObjectContext = { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) // todo: should the background context really be parented to the view context, or should they both be direct children of the PSC? context.parent = self.viewContext return context }() private weak var fervorController: FervorController? private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer") init(account: LocalData.Account, fervorController: FervorController) { self.fervorController = fervorController super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel) loadPersistentStores { description, error in if let error = error { fatalError("Unable to load persistent store: \(error)") } } } @MainActor private func saveViewContext() async throws { if viewContext.hasChanges { try viewContext.save() } } func lastSyncDate() async throws -> Date? { return try await backgroundContext.perform { let state = try self.backgroundContext.fetch(SyncState.fetchRequest()).first return state?.lastSync } } func updateLastSyncDate(_ date: Date) async throws { try await backgroundContext.perform { if let state = try self.backgroundContext.fetch(SyncState.fetchRequest()).first { state.lastSync = date } else { let state = SyncState(context: self.backgroundContext) state.lastSync = date } try self.backgroundContext.save() } try await self.saveViewContext() } func sync(serverGroups: [Fervor.Group], serverFeeds: [Fervor.Feed]) async throws { try await backgroundContext.perform { let existingGroups = try self.backgroundContext.fetch(Group.fetchRequest()) for group in serverGroups { if let existing = existingGroups.first(where: { $0.id == group.id }) { existing.updateFromServer(group) } else { let mo = Group(context: self.backgroundContext) mo.updateFromServer(group) } } for removed in existingGroups where !serverGroups.contains(where: { $0.id == removed.id }) { self.backgroundContext.delete(removed) } let existingFeeds = try self.backgroundContext.fetch(Feed.fetchRequest()) for feed in serverFeeds { if let existing = existingFeeds.first(where: { $0.id == feed.id }) { existing.updateFromServer(feed) } else { let mo = Feed(context: self.backgroundContext) mo.updateFromServer(feed) } } if self.backgroundContext.hasChanges { try self.backgroundContext.save() } } try await self.saveViewContext() } func syncItems(_ syncUpdate: ItemsSyncUpdate) async throws { try await backgroundContext.perform { self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items") let deleteReq = Item.fetchRequest() deleteReq.predicate = NSPredicate(format: "id in %@", syncUpdate.delete) let delete = NSBatchDeleteRequest(fetchRequest: deleteReq as! NSFetchRequest) delete.resultType = .resultTypeObjectIDs let result = try self.backgroundContext.execute(delete) if let deleteResult = result as? NSBatchDeleteResult, let objectIDs = deleteResult.result as? [NSManagedObjectID] { NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs], into: [self.viewContext]) // todo: does the background/view contexts need to get saved then? } let req = Item.fetchRequest() req.predicate = NSPredicate(format: "id in %@", syncUpdate.upsert.map(\.id)) let existing = try self.backgroundContext.fetch(req) self.logger.debug("syncItems: updating \(existing.count, privacy: .public) items, inserting \(syncUpdate.upsert.count - existing.count, privacy: .public)") // todo: this feels like it'll be slow when there are many items for item in syncUpdate.upsert { if let existing = existing.first(where: { $0.id == item.id }) { existing.updateFromServer(item) } else { let mo = Item(context: self.backgroundContext) mo.updateFromServer(item) } } if self.backgroundContext.hasChanges { try self.backgroundContext.save() } } try await self.saveViewContext() } }