frenzy-ios/Reader/CoreData/PersistentContainer.swift

171 lines
7.4 KiB
Swift
Raw Normal View History

2021-12-25 19:04:45 +00:00
//
// PersistentContainer.swift
// Reader
//
// Created by Shadowfacts on 12/24/21.
//
import CoreData
import Fervor
2022-01-09 22:13:30 +00:00
import OSLog
2021-12-25 19:04:45 +00:00
// todo: is this actually sendable?
class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
2021-12-25 19:04:45 +00:00
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)
2022-01-09 16:11:52 +00:00
// todo: should the background context really be parented to the view context, or should they both be direct children of the PSC?
2021-12-25 19:04:45 +00:00
context.parent = self.viewContext
return context
}()
weak var fervorController: FervorController?
2022-01-09 22:13:30 +00:00
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
init(account: LocalData.Account) {
2022-03-06 20:34:30 +00:00
// slashes the base64 string turn into subdirectories which we don't want
let name = account.id.base64EncodedString().replacingOccurrences(of: "/", with: "_")
super.init(name: name, managedObjectModel: PersistentContainer.managedObjectModel)
2021-12-25 19:04:45 +00:00
2022-06-15 22:33:40 +00:00
let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.shadowfacts.Reader")!
let containerAppSupportURL = groupContainerURL
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Application Support", isDirectory: true)
try! FileManager.default.createDirectory(at: containerAppSupportURL, withIntermediateDirectories: true)
let containerStoreURL = containerAppSupportURL
.appendingPathComponent(name)
.appendingPathExtension("sqlite")
if let existingAppSupport = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
let existingStore = existingAppSupport
.appendingPathComponent(name)
.appendingPathExtension("sqlite")
.relativePath
.removingPercentEncoding!
if FileManager.default.fileExists(atPath: existingStore) {
for ext in ["", "-shm", "-wal"] {
try! FileManager.default.moveItem(atPath: existingStore + ext, toPath: containerStoreURL.relativePath + ext)
}
}
}
let desc = NSPersistentStoreDescription(url: containerStoreURL)
desc.type = NSSQLiteStoreType
persistentStoreDescriptions = [desc]
2021-12-25 19:04:45 +00:00
loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent store: \(error)")
}
}
}
2022-01-09 23:46:42 +00:00
@MainActor
func saveViewContext() throws {
2022-01-09 23:46:42 +00:00
if viewContext.hasChanges {
try viewContext.save()
}
}
2022-01-09 22:13:30 +00:00
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()
}
2022-01-09 23:46:42 +00:00
try await self.saveViewContext()
2022-01-09 22:13:30 +00:00
}
2021-12-25 19:04:45 +00:00
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)
}
}
for removed in existingFeeds where !serverFeeds.contains(where: { $0.id == removed.id }) {
self.backgroundContext.delete(removed)
}
2021-12-25 19:04:45 +00:00
if self.backgroundContext.hasChanges {
try self.backgroundContext.save()
}
}
2022-01-09 23:46:42 +00:00
try await self.saveViewContext()
2021-12-25 19:04:45 +00:00
}
2022-01-27 03:37:10 +00:00
func syncItems(_ syncUpdate: ItemsSyncUpdate, setSyncState: @escaping (FervorController.SyncState) -> Void) async throws {
2022-01-09 22:13:30 +00:00
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<NSFetchRequestResult>)
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])
2022-01-09 22:13:30 +00:00
// 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
2022-01-27 03:37:10 +00:00
for (index, item) in syncUpdate.upsert.enumerated() {
setSyncState(.updateItems(current: index, total: syncUpdate.upsert.count))
2022-01-09 22:13:30 +00:00
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()
}
}
2022-01-09 23:46:42 +00:00
try await self.saveViewContext()
2022-01-09 22:13:30 +00:00
}
2021-12-25 19:04:45 +00:00
}