2020-04-11 19:31:37 +00:00
|
|
|
//
|
|
|
|
// MastodonCachePersistentStore.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 4/11/20.
|
|
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import CoreData
|
2020-04-12 02:23:31 +00:00
|
|
|
import Pachyderm
|
2020-05-02 16:45:28 +00:00
|
|
|
import Combine
|
2020-04-11 19:31:37 +00:00
|
|
|
|
|
|
|
class MastodonCachePersistentStore: NSPersistentContainer {
|
|
|
|
|
2020-05-10 18:54:43 +00:00
|
|
|
private static let managedObjectModel: NSManagedObjectModel = {
|
|
|
|
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
|
|
|
return NSManagedObjectModel(contentsOf: url)!
|
|
|
|
}()
|
|
|
|
|
2020-05-13 23:49:35 +00:00
|
|
|
private(set) lazy var backgroundContext: NSManagedObjectContext = {
|
|
|
|
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
|
|
|
context.parent = self.viewContext
|
|
|
|
return context
|
|
|
|
}()
|
2020-04-12 02:23:31 +00:00
|
|
|
|
2021-01-18 19:29:32 +00:00
|
|
|
private(set) lazy var prefetchBackgroundContext: NSManagedObjectContext = {
|
|
|
|
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
|
|
|
context.parent = self.viewContext
|
|
|
|
return context
|
|
|
|
}()
|
|
|
|
|
2020-05-02 16:45:28 +00:00
|
|
|
let statusSubject = PassthroughSubject<String, Never>()
|
|
|
|
let accountSubject = PassthroughSubject<String, Never>()
|
2020-10-12 22:20:57 +00:00
|
|
|
let relationshipSubject = PassthroughSubject<String, Never>()
|
2020-05-11 21:57:50 +00:00
|
|
|
|
|
|
|
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
|
|
|
|
if transient {
|
|
|
|
super.init(name: "transient_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
|
|
|
|
|
|
|
let storeDescription = NSPersistentStoreDescription()
|
|
|
|
storeDescription.type = NSInMemoryStoreType
|
|
|
|
persistentStoreDescriptions = [storeDescription]
|
|
|
|
} else {
|
|
|
|
super.init(name: "\(accountInfo!.id)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
|
|
|
}
|
|
|
|
|
2020-04-11 19:31:37 +00:00
|
|
|
loadPersistentStores { (description, error) in
|
|
|
|
if let error = error {
|
|
|
|
fatalError("Unable to load persistent store: \(error)")
|
|
|
|
}
|
|
|
|
}
|
2022-05-11 02:57:46 +00:00
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext)
|
2020-04-11 19:31:37 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 02:23:31 +00:00
|
|
|
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
|
|
|
|
let context = context ?? viewContext
|
2020-04-11 19:31:37 +00:00
|
|
|
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
|
|
|
request.predicate = NSPredicate(format: "id = %@", id)
|
|
|
|
request.fetchLimit = 1
|
2020-04-12 02:23:31 +00:00
|
|
|
if let result = try? context.fetch(request), let status = result.first {
|
2020-04-11 19:31:37 +00:00
|
|
|
return status
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 03:02:12 +00:00
|
|
|
@discardableResult
|
2022-05-01 19:15:35 +00:00
|
|
|
private func upsert(status: Status, context: NSManagedObjectContext) -> StatusMO {
|
2021-05-05 21:51:11 +00:00
|
|
|
if let statusMO = self.status(for: status.id, in: context) {
|
2020-04-12 16:54:27 +00:00
|
|
|
statusMO.updateFrom(apiStatus: status, container: self)
|
2020-05-07 03:02:12 +00:00
|
|
|
return statusMO
|
2020-04-12 16:54:27 +00:00
|
|
|
} else {
|
2022-05-01 19:15:35 +00:00
|
|
|
return StatusMO(apiStatus: status, container: self, context: context)
|
2020-04-12 16:54:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-01 19:15:35 +00:00
|
|
|
func addOrUpdate(status: Status, context: NSManagedObjectContext? = nil, completion: ((StatusMO) -> Void)? = nil) {
|
2021-05-05 21:51:11 +00:00
|
|
|
let context = context ?? backgroundContext
|
|
|
|
context.perform {
|
2022-05-01 19:15:35 +00:00
|
|
|
let statusMO = self.upsert(status: status, context: context)
|
2021-05-05 21:51:11 +00:00
|
|
|
if context.hasChanges {
|
|
|
|
try! context.save()
|
2020-04-12 02:23:31 +00:00
|
|
|
}
|
2020-05-07 03:02:12 +00:00
|
|
|
completion?(statusMO)
|
2020-05-02 16:45:28 +00:00
|
|
|
self.statusSubject.send(status.id)
|
2020-04-12 02:23:31 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-11 23:10:38 +00:00
|
|
|
|
|
|
|
@MainActor
|
|
|
|
func addOrUpdateOnViewContext(status: Status) -> StatusMO {
|
|
|
|
let statusMO = self.upsert(status: status, context: viewContext)
|
|
|
|
if viewContext.hasChanges {
|
|
|
|
try! viewContext.save()
|
|
|
|
}
|
|
|
|
statusSubject.send(status.id)
|
|
|
|
return statusMO
|
|
|
|
}
|
2020-04-12 16:54:27 +00:00
|
|
|
|
|
|
|
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
|
|
|
backgroundContext.perform {
|
2022-05-01 19:15:35 +00:00
|
|
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
2020-04-12 16:54:27 +00:00
|
|
|
if self.backgroundContext.hasChanges {
|
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
2020-05-02 16:45:28 +00:00
|
|
|
statuses.forEach { self.statusSubject.send($0.id) }
|
2020-05-10 19:47:50 +00:00
|
|
|
completion?()
|
2020-04-12 16:54:27 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-11 23:10:38 +00:00
|
|
|
|
|
|
|
func addAll(statuses: [Status]) async {
|
|
|
|
return await withCheckedContinuation { continuation in
|
|
|
|
addAll(statuses: statuses) {
|
|
|
|
continuation.resume()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-12 16:54:27 +00:00
|
|
|
|
2020-04-12 02:23:31 +00:00
|
|
|
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
|
|
|
|
let context = context ?? viewContext
|
2020-04-11 19:31:37 +00:00
|
|
|
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
|
|
|
request.predicate = NSPredicate(format: "id = %@", id)
|
|
|
|
request.fetchLimit = 1
|
2020-04-12 02:23:31 +00:00
|
|
|
if let result = try? context.fetch(request), let account = result.first {
|
2020-04-11 19:31:37 +00:00
|
|
|
return account
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 03:02:12 +00:00
|
|
|
@discardableResult
|
2022-05-01 19:15:35 +00:00
|
|
|
private func upsert(account: Account) -> AccountMO {
|
2020-04-12 16:54:27 +00:00
|
|
|
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
|
|
|
|
accountMO.updateFrom(apiAccount: account, container: self)
|
2020-05-07 03:02:12 +00:00
|
|
|
return accountMO
|
2020-04-12 16:54:27 +00:00
|
|
|
} else {
|
2022-05-01 19:15:35 +00:00
|
|
|
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
|
2020-04-12 16:54:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-01 19:15:35 +00:00
|
|
|
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
|
2020-04-12 02:23:31 +00:00
|
|
|
backgroundContext.perform {
|
2022-05-01 19:15:35 +00:00
|
|
|
let accountMO = self.upsert(account: account)
|
2020-04-14 02:51:21 +00:00
|
|
|
if self.backgroundContext.hasChanges {
|
2020-04-12 02:23:31 +00:00
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
2020-05-07 03:02:12 +00:00
|
|
|
completion?(accountMO)
|
2020-05-02 16:45:28 +00:00
|
|
|
self.accountSubject.send(account.id)
|
2020-04-12 02:23:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-12 22:20:57 +00:00
|
|
|
func relationship(forAccount id: String, in context: NSManagedObjectContext? = nil) -> RelationshipMO? {
|
|
|
|
let context = context ?? viewContext
|
|
|
|
let request: NSFetchRequest<RelationshipMO> = RelationshipMO.fetchRequest()
|
|
|
|
request.predicate = NSPredicate(format: "accountID = %@", id)
|
|
|
|
request.fetchLimit = 1
|
|
|
|
if let result = try? context.fetch(request), let relationship = result.first {
|
|
|
|
return relationship
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
private func upsert(relationship: Relationship) -> RelationshipMO {
|
|
|
|
if let relationshipMO = self.relationship(forAccount: relationship.id, in: self.backgroundContext) {
|
|
|
|
relationshipMO.updateFrom(apiRelationship: relationship, container: self)
|
|
|
|
return relationshipMO
|
|
|
|
} else {
|
|
|
|
let relationshipMO = RelationshipMO(apiRelationship: relationship, container: self, context: self.backgroundContext)
|
|
|
|
return relationshipMO
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func addOrUpdate(relationship: Relationship, completion: ((RelationshipMO) -> Void)? = nil) {
|
|
|
|
backgroundContext.perform {
|
|
|
|
let relationshipMO = self.upsert(relationship: relationship)
|
|
|
|
if self.backgroundContext.hasChanges {
|
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
|
|
|
completion?(relationshipMO)
|
|
|
|
self.relationshipSubject.send(relationship.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-12 16:54:27 +00:00
|
|
|
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
|
|
|
backgroundContext.perform {
|
2022-05-01 19:15:35 +00:00
|
|
|
accounts.forEach { self.upsert(account: $0) }
|
2020-04-12 16:54:27 +00:00
|
|
|
if self.backgroundContext.hasChanges {
|
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
|
|
|
completion?()
|
2020-05-02 16:45:28 +00:00
|
|
|
accounts.forEach { self.accountSubject.send($0.id) }
|
2020-04-12 16:54:27 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-06 23:32:32 +00:00
|
|
|
|
|
|
|
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
|
|
|
backgroundContext.perform {
|
|
|
|
let statuses = notifications.compactMap { $0.status }
|
2020-05-12 01:59:46 +00:00
|
|
|
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
|
|
|
// since the status has the same account as the notification
|
|
|
|
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
2022-05-01 19:15:35 +00:00
|
|
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
|
|
|
accounts.forEach { self.upsert(account: $0) }
|
2020-05-06 23:32:32 +00:00
|
|
|
if self.backgroundContext.hasChanges {
|
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
|
|
|
completion?()
|
|
|
|
statuses.forEach { self.statusSubject.send($0.id) }
|
|
|
|
accounts.forEach { self.accountSubject.send($0.id) }
|
|
|
|
}
|
|
|
|
}
|
2020-04-12 16:54:27 +00:00
|
|
|
|
2020-05-10 19:47:50 +00:00
|
|
|
func performBatchUpdates(_ block: @escaping (_ context: NSManagedObjectContext, _ addAccounts: ([Account]) -> Void, _ addStatuses: ([Status]) -> Void) -> Void, completion: (() -> Void)? = nil) {
|
|
|
|
backgroundContext.perform {
|
|
|
|
var updatedAccounts = [String]()
|
|
|
|
var updatedStatuses = [String]()
|
|
|
|
|
|
|
|
block(self.backgroundContext, { (accounts) in
|
2022-05-01 19:15:35 +00:00
|
|
|
accounts.forEach { self.upsert(account: $0) }
|
2020-05-10 19:47:50 +00:00
|
|
|
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
|
|
|
}, { (statuses) in
|
2022-05-01 19:15:35 +00:00
|
|
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
2020-05-10 19:47:50 +00:00
|
|
|
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
|
|
|
})
|
|
|
|
|
|
|
|
updatedAccounts.forEach(self.accountSubject.send)
|
|
|
|
updatedStatuses.forEach(self.statusSubject.send)
|
|
|
|
|
|
|
|
if self.backgroundContext.hasChanges {
|
|
|
|
try! self.backgroundContext.save()
|
|
|
|
}
|
|
|
|
completion?()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-11 02:57:46 +00:00
|
|
|
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
|
|
|
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
|
|
|
if changes.hashtags {
|
|
|
|
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
|
|
|
}
|
|
|
|
if changes.instances {
|
|
|
|
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func hasChangedSavedHashtagsOrInstances(_ notification: Foundation.Notification) -> (hashtags: Bool, instances: Bool) {
|
|
|
|
var changes: (hashtags: Bool, instances: Bool) = (false, false)
|
|
|
|
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
|
|
|
for object in inserted {
|
|
|
|
if object is SavedHashtag {
|
|
|
|
changes.hashtags = true
|
|
|
|
} else if object is SavedInstance {
|
|
|
|
changes.instances = true
|
|
|
|
}
|
|
|
|
if changes.hashtags && changes.instances {
|
|
|
|
return changes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> {
|
|
|
|
for object in deleted {
|
|
|
|
if object is SavedHashtag {
|
|
|
|
changes.hashtags = true
|
|
|
|
} else if object is SavedInstance {
|
|
|
|
changes.instances = true
|
|
|
|
}
|
|
|
|
if changes.hashtags && changes.instances {
|
|
|
|
return changes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return changes
|
|
|
|
}
|
|
|
|
|
2020-04-11 19:31:37 +00:00
|
|
|
}
|