From 82ad3b9fc42ceab44b5b439a1a81f69d6bc1f5fc Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 11 May 2020 21:59:46 -0400 Subject: [PATCH] Add reference counting for accounts Closes #97 --- Tusker/Controllers/MastodonController.swift | 12 +++++++-- Tusker/CoreData/AccountMO.swift | 19 ++++++++++++++ .../MastodonCachePersistentStore.swift | 25 +++++++++++++------ Tusker/CoreData/StatusMO.swift | 2 ++ .../Tusker.xcdatamodel/contents | 4 +-- .../Profile/ProfileTableViewController.swift | 11 +++++++- .../Search/SearchResultsViewController.swift | 9 +++---- ...ActionAccountListTableViewController.swift | 17 ++++++++++++- 8 files changed, 80 insertions(+), 19 deletions(-) diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index c3dcaaf9cb..a75a0eccfa 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -84,8 +84,16 @@ class MastodonController { run(request) { response in guard case let .success(account, _) = response else { fatalError() } self.account = account - self.persistentContainer.addOrUpdate(account: account) - completion?(account) + self.persistentContainer.backgroundContext.perform { + if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) { + accountMO.updateFrom(apiAccount: account, container: self.persistentContainer) + } else { + // the first time the user's account is added to the store, + // increment its reference count so that it's never removed + self.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) + } + completion?(account) + } } } } diff --git a/Tusker/CoreData/AccountMO.swift b/Tusker/CoreData/AccountMO.swift index ff41f5ccd4..8349246f53 100644 --- a/Tusker/CoreData/AccountMO.swift +++ b/Tusker/CoreData/AccountMO.swift @@ -32,6 +32,7 @@ public final class AccountMO: NSManagedObject, AccountProtocol { @NSManaged public var locked: Bool @NSManaged public var movedCD: Bool @NSManaged public var note: String + @NSManaged public var referenceCount: Int @NSManaged public var statusesCount: Int @NSManaged public var url: URL @NSManaged public var username: String @@ -46,12 +47,30 @@ public final class AccountMO: NSManagedObject, AccountProtocol { public var bot: Bool? { botCD } public var moved: Bool? { movedCD } + func incrementReferenceCount() { + referenceCount += 1 + } + + func decrementReferenceCount() { + referenceCount -= 1 + if referenceCount <= 0 { + managedObjectContext!.delete(self) + } + } + + public override func prepareForDeletion() { + super.prepareForDeletion() + movedTo?.decrementReferenceCount() + } + } extension AccountMO { convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) { self.init(context: context) self.updateFrom(apiAccount: account, container: container) + + movedTo?.incrementReferenceCount() } func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) { diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 3065106615..419c495536 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -105,18 +105,25 @@ class MastodonCachePersistentStore: NSPersistentContainer { } @discardableResult - private func upsert(account: Account) -> AccountMO { + private func upsert(account: Account, incrementReferenceCount: Bool) -> AccountMO { if let accountMO = self.account(for: account.id, in: self.backgroundContext) { accountMO.updateFrom(apiAccount: account, container: self) + if incrementReferenceCount { + accountMO.incrementReferenceCount() + } return accountMO } else { - return AccountMO(apiAccount: account, container: self, context: self.backgroundContext) + let accountMO = AccountMO(apiAccount: account, container: self, context: self.backgroundContext) + if incrementReferenceCount { + accountMO.incrementReferenceCount() + } + return accountMO } } - func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) { + func addOrUpdate(account: Account, incrementReferenceCount: Bool, completion: ((AccountMO) -> Void)? = nil) { backgroundContext.perform { - let accountMO = self.upsert(account: account) + let accountMO = self.upsert(account: account, incrementReferenceCount: incrementReferenceCount) if self.backgroundContext.hasChanges { try! self.backgroundContext.save() } @@ -127,7 +134,7 @@ class MastodonCachePersistentStore: NSPersistentContainer { func addAll(accounts: [Account], completion: (() -> Void)? = nil) { backgroundContext.perform { - accounts.forEach { self.upsert(account: $0) } + accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) } if self.backgroundContext.hasChanges { try! self.backgroundContext.save() } @@ -139,9 +146,11 @@ class MastodonCachePersistentStore: NSPersistentContainer { func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) { backgroundContext.perform { let statuses = notifications.compactMap { $0.status } - let accounts = notifications.map { $0.account } + // 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 } statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } - accounts.forEach { self.upsert(account: $0) } + accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) } if self.backgroundContext.hasChanges { try! self.backgroundContext.save() } @@ -157,7 +166,7 @@ class MastodonCachePersistentStore: NSPersistentContainer { var updatedStatuses = [String]() block(self.backgroundContext, { (accounts) in - accounts.forEach { self.upsert(account: $0) } + accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) } updatedAccounts.append(contentsOf: accounts.map { $0.id }) }, { (statuses) in statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } diff --git a/Tusker/CoreData/StatusMO.swift b/Tusker/CoreData/StatusMO.swift index 82c0728ed4..20b963bc63 100644 --- a/Tusker/CoreData/StatusMO.swift +++ b/Tusker/CoreData/StatusMO.swift @@ -82,6 +82,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol { public override func prepareForDeletion() { super.prepareForDeletion() reblog?.decrementReferenceCount() + account.decrementReferenceCount() } } @@ -92,6 +93,7 @@ extension StatusMO { self.updateFrom(apiStatus: status, container: container) reblog?.incrementReferenceCount() + account.incrementReferenceCount() } func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) { diff --git a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents index 67a499808f..2af9efec7b 100644 --- a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents +++ b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents @@ -15,6 +15,7 @@ + @@ -49,7 +50,6 @@ - @@ -59,7 +59,7 @@ - + \ No newline at end of file diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index dd81a4042a..b5e76b1325 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -61,6 +61,15 @@ class ProfileTableViewController: EnhancedTableViewController { fatalError("init(coder:) has not been implemeneted") } + deinit { + if let id = accountID { + let container = mastodonController.persistentContainer + container.backgroundContext.perform { + container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount() + } + } + } + override func viewDidLoad() { super.viewDidLoad() @@ -90,7 +99,7 @@ class ProfileTableViewController: EnhancedTableViewController { } return } - self.mastodonController.persistentContainer.addOrUpdate(account: account) { (_) in + self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (_) in DispatchQueue.main.async { self.updateAccountUI() self.tableView.reloadData() diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index ba4b7fff8c..d91522f5ae 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -134,11 +134,10 @@ class SearchResultsViewController: EnhancedTableViewController { var snapshot = NSDiffableDataSourceSnapshot() self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in - // todo: reference count accounts -// oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in -// guard case let .account(id) = item else { return } -// self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() -// } + oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in + guard case let .account(id) = item else { return } + self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() + } oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in guard case let .status(id, _) = item else { return } diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index 7690352b19..0145b887e8 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -58,6 +58,17 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { fatalError("init(coder:) has not been implemented") } + deinit { + if let accountIDs = self.accountIDs { + let container = self.mastodonController.persistentContainer + container.backgroundContext.perform { + for id in accountIDs { + container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount() + } + } + } + } + override func viewDidLoad() { super.viewDidLoad() @@ -71,7 +82,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) - if accountIDs == nil { + if let accountIDs = accountIDs { + accountIDs.forEach { (id) in + self.mastodonController.persistentContainer.account(for: id)?.incrementReferenceCount() + } + } else { // account IDs haven't been set, so perform a request to load them guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)")