diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 8e036e21e..68c8d452b 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -61,13 +61,15 @@ public protocol AttachmentsManagerWorkable { @MainActor func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask } +/// Transactionable +extension AttachmentsManagerWorker: TransactionablePassthrough {} + // MARK: - AttachmentsManagerWorker public final class AttachmentsManagerWorker { weak var updateDelegate: AttachmentsContentUpdatable? private let mailboxManager: MailboxManager - private let backgroundRealm: BackgroundRealm private let draftLocalUUID: String public let transactionExecutor: Transactionable @@ -111,25 +113,22 @@ public final class AttachmentsManagerWorker { } public init(draftLocalUUID: String, mailboxManager: MailboxManager) { - backgroundRealm = BackgroundRealm(configuration: mailboxManager.realmConfiguration) self.draftLocalUUID = draftLocalUUID self.mailboxManager = mailboxManager - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: mailboxManager.realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) } func addLocalAttachment(attachment: Attachment) async -> Attachment? { attachmentUploadTasks[attachment.uuid] = await AttachmentUploadTask() var detached: Attachment? - await backgroundRealm.execute { realm in - try? realm.write { - guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { - return - } - - draftInContext.attachments.append(attachment) + try? writeTransaction { writableRealm in + guard let draftInContext = writableRealm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { + return } + draftInContext.attachments.append(attachment) detached = attachment.detached() } @@ -170,19 +169,17 @@ public final class AttachmentsManagerWorker { attachmentUploadTasks.removeValue(forKey: oldAttachmentUUID) } - await backgroundRealm.execute { realm in - try? realm.write { - guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { - return - } - - guard let liveOldAttachment = draftInContext.attachments.first(where: { $0.uuid == oldAttachmentUUID }) else { - return - } + try? writeTransaction { writableRealm in + guard let draftInContext = writableRealm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { + return + } - // We need to update every field of the local attachment because embedded objects don't have a primary key - liveOldAttachment.update(with: newAttachment) + guard let liveOldAttachment = draftInContext.attachments.first(where: { $0.uuid == oldAttachmentUUID }) else { + return } + + // We need to update every field of the local attachment because embedded objects don't have a primary key + liveOldAttachment.update(with: newAttachment) } await updateDelegate?.contentWillChange() @@ -348,18 +345,16 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } public func removeAttachment(_ attachmentUUID: String) async { - await backgroundRealm.execute { realm in - try? realm.write { - guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { - return - } - - guard let liveAttachment = draftInContext.attachments.first(where: { $0.uuid == attachmentUUID }) else { - return - } + try? writeTransaction { writableRealm in + guard let draftInContext = writableRealm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { + return + } - realm.delete(liveAttachment) + guard let liveAttachment = draftInContext.attachments.first(where: { $0.uuid == attachmentUUID }) else { + return } + + writableRealm.delete(liveAttachment) } await attachmentUploadTasks[attachmentUUID]?.task?.cancel() @@ -384,31 +379,29 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { let allSanitizedHtmlString = await allSanitizedHtml(in: htmlAttachments).joined(separator: "") // Mutate Draft - await backgroundRealm.execute { realm in - try? realm.write { - guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { - return - } - - // Title if any usable - var modified = false - if draftInContext.subject.isEmpty, - !anyUsableTitle.isEmpty { - draftInContext.subject = anyUsableTitle - modified = true - } + try? writeTransaction { writableRealm in + guard let draftInContext = writableRealm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else { + return + } - if !allSanitizedHtmlString.isEmpty { - draftInContext.body = allSanitizedHtmlString + draftInContext.body - modified = true - } + // Title if any usable + var modified = false + if draftInContext.subject.isEmpty, + !anyUsableTitle.isEmpty { + draftInContext.subject = anyUsableTitle + modified = true + } - guard modified else { - return - } + if !allSanitizedHtmlString.isEmpty { + draftInContext.body = allSanitizedHtmlString + draftInContext.body + modified = true + } - realm.add(draftInContext, update: .modified) + guard modified else { + return } + + writableRealm.add(draftInContext, update: .modified) } } diff --git a/MailCore/Cache/ContactManager/ContactManager.swift b/MailCore/Cache/ContactManager/ContactManager.swift index 79b8bb9b8..0e0439de7 100644 --- a/MailCore/Cache/ContactManager/ContactManager.swift +++ b/MailCore/Cache/ContactManager/ContactManager.swift @@ -68,7 +68,6 @@ public final class ContactManager: ObservableObject, ContactManageable { public static let constants = ContactManagerConstants() public let realmConfiguration: Realm.Configuration - private let backgroundRealm: BackgroundRealm public let transactionExecutor: Transactionable let apiFetcher: MailApiFetcher @@ -85,8 +84,8 @@ public final class ContactManager: ObservableObject, ContactManageable { AddressBook.self ] ) - backgroundRealm = BackgroundRealm(configuration: realmConfiguration) - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) excludeRealmFromBackup() } @@ -123,10 +122,8 @@ public final class ContactManager: ObservableObject, ContactManageable { // Process addressBooks let addressBooks = try await addressBooksRequest - await backgroundRealm.execute { realm in - try? realm.safeWrite { - realm.add(addressBooks, update: .modified) - } + try? writeTransaction { writableRealm in + writableRealm.add(addressBooks, update: .modified) } await backgroundTaskTracker.end() diff --git a/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift index 353fd4801..449553cfa 100644 --- a/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift +++ b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift @@ -29,8 +29,6 @@ public final class MailboxInfosManager { private static let currentDbVersion: UInt64 = 7 private let dbName = "MailboxInfos.realm" - private let backgroundRealm: BackgroundRealm - public let realmConfiguration: Realm.Configuration public let transactionExecutor: Transactionable @@ -51,8 +49,8 @@ public final class MailboxInfosManager { objectTypes: [Mailbox.self, MailboxPermissions.self, Quotas.self, ExternalMailInfo.self] ) - backgroundRealm = BackgroundRealm(configuration: realmConfiguration) - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) excludeRealmFromBackup() } @@ -78,15 +76,13 @@ public final class MailboxInfosManager { } mailboxRemovedIds = mailboxRemoved.map(\.objectId) - } - await backgroundRealm.execute { realm in let detachedMailboxes = mailboxes.map { $0.detached() } - try? realm.write { - realm.delete(realm.objects(Mailbox.self).filter("objectId IN %@", mailboxRemovedIds)) - realm.add(detachedMailboxes, update: .modified) - } + let mailboxes = writableRealm.objects(Mailbox.self).filter("objectId IN %@", mailboxRemovedIds) + writableRealm.delete(mailboxes) + writableRealm.add(detachedMailboxes, update: .modified) } + return mailboxRemoved } @@ -138,11 +134,9 @@ public final class MailboxInfosManager { } public func updateUnseen(unseenMessages: Int, for mailbox: Mailbox) async { - await backgroundRealm.execute { realm in - let freshMailbox = mailbox.fresh(using: realm) - try? realm.safeWrite { - freshMailbox?.unseenMessages = unseenMessages - } + try? writeTransaction { writableRealm in + let freshMailbox = mailbox.fresh(using: writableRealm) + freshMailbox?.unseenMessages = unseenMessages } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift b/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift index e73aa4d7e..dafbf03b8 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift @@ -67,12 +67,11 @@ extension MailboxManager { } private func saveCalendarEventResponse(to messageUid: String, eventResponse: CalendarEventResponse) async { - await backgroundRealm.execute { realm in - if let liveMessage = realm.object(ofType: Message.self, forPrimaryKey: messageUid) { - try? realm.safeWrite { - liveMessage.calendarEventResponse = eventResponse - } + try? writeTransaction { writableRealm in + guard let liveMessage = writableRealm.object(ofType: Message.self, forPrimaryKey: messageUid) else { + return } + liveMessage.calendarEventResponse = eventResponse } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+DB.swift b/MailCore/Cache/MailboxManager/MailboxManager+DB.swift index f3925e893..e6eeb2bf4 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+DB.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+DB.swift @@ -21,20 +21,18 @@ import Foundation public extension MailboxManager { func cleanRealm() { Task { - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in + let threads = writableRealm.objects(Thread.self) + writableRealm.delete(threads) - let folders = realm.objects(Folder.self) - let threads = realm.objects(Thread.self) - let messages = realm.objects(Message.self) + let messages = writableRealm.objects(Message.self) + writableRealm.delete(messages) - try? realm.safeWrite { - realm.delete(threads) - realm.delete(messages) - for folder in folders { - folder.cursor = nil - folder.resetHistoryInfo() - folder.computeUnreadCount() - } + let folders = writableRealm.objects(Folder.self) + for folder in folders { + folder.cursor = nil + folder.resetHistoryInfo() + folder.computeUnreadCount() } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift b/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift index 521d7d2f3..46050f4f8 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift @@ -81,17 +81,16 @@ public extension MailboxManager { func save(draft: Draft) async throws { do { let saveResponse = try await apiFetcher.save(mailbox: mailbox, draft: draft) - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in // Update draft in Realm - guard let liveDraft = realm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { + guard let liveDraft = writableRealm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { self.logError(.missingDraft) return } - try? realm.safeWrite { - liveDraft.remoteUUID = saveResponse.uuid - liveDraft.messageUid = saveResponse.uid - liveDraft.action = nil - } + + liveDraft.remoteUUID = saveResponse.uuid + liveDraft.messageUid = saveResponse.uid + liveDraft.action = nil } } catch let error as MailApiError { // Do not delete draft on invalid identity @@ -131,14 +130,13 @@ public extension MailboxManager { } func deleteLocally(draft: Draft) async throws { - await backgroundRealm.execute { realm in - guard let liveDraft = realm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { + try? writeTransaction { writableRealm in + guard let liveDraft = writableRealm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { self.logError(.missingDraft) return } - try? realm.safeWrite { - realm.delete(liveDraft) - } + + writableRealm.delete(liveDraft) } } @@ -150,14 +148,12 @@ public extension MailboxManager { let existingMessageUids = Set(draftFolder.threads.flatMap(\.messages).map(\.uid)) - await backgroundRealm.execute { realm in - try? realm.safeWrite { - let noActionDrafts = realm.objects(Draft.self).where { $0.action == nil } - for draft in noActionDrafts { - if let messageUid = draft.messageUid, - !existingMessageUids.contains(messageUid) { - realm.delete(draft) - } + try? writeTransaction { writableRealm in + let noActionDrafts = writableRealm.objects(Draft.self).where { $0.action == nil } + for draft in noActionDrafts { + if let messageUid = draft.messageUid, + !existingMessageUids.contains(messageUid) { + writableRealm.delete(draft) } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift index ba8326283..6dd0350a2 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift @@ -26,50 +26,42 @@ import RealmSwift public extension MailboxManager { /// Get all remote folders in DB func refreshAllFolders() async throws { - let backgroundTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) - - // Network check guard ReachabilityListener.instance.currentStatus != .offline else { return } - // Get from API let folderResult = try await apiFetcher.folders(mailbox: mailbox) let newFolders = getSubFolders(from: folderResult) - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in + // Update folders in Realm for folder in newFolders { - self.keepCacheAttributes(for: folder, using: realm) + self.keepCacheAttributes(for: folder, using: writableRealm) } // Get from Realm - let cachedFolders = realm.objects(Folder.self) - - // Update folders in Realm - try? realm.safeWrite { - // Remove old folders - realm.add(folderResult, update: .modified) - let toDeleteFolders = Set(cachedFolders).subtracting(Set(newFolders)) - .filter { $0.remoteId != Constants.searchFolderId } - var toDeleteThreads = [Thread]() - - // Threads contains in folders to delete - let mayBeDeletedThreads = Set(toDeleteFolders.flatMap(\.threads)) - // Messages contains in folders to delete - let toDeleteMessages = Set(toDeleteFolders.flatMap(\.messages)) - - // Delete thread if all his messages are deleted - for thread in mayBeDeletedThreads where Set(thread.messages).isSubset(of: toDeleteMessages) { - toDeleteThreads.append(thread) - } - - realm.delete(toDeleteMessages) - realm.delete(toDeleteThreads) - realm.delete(toDeleteFolders) + let cachedFolders = writableRealm.objects(Folder.self) + + // Remove old folders + writableRealm.add(folderResult, update: .modified) + let toDeleteFolders = Set(cachedFolders).subtracting(Set(newFolders)) + .filter { $0.remoteId != Constants.searchFolderId } + var toDeleteThreads = [Thread]() + + // Threads contains in folders to delete + let mayBeDeletedThreads = Set(toDeleteFolders.flatMap(\.threads)) + // Messages contains in folders to delete + let toDeleteMessages = Set(toDeleteFolders.flatMap(\.messages)) + + // Delete thread if all his messages are deleted + for thread in mayBeDeletedThreads where Set(thread.messages).isSubset(of: toDeleteMessages) { + toDeleteThreads.append(thread) } - } - await backgroundTracker.end() + writableRealm.delete(toDeleteMessages) + writableRealm.delete(toDeleteThreads) + writableRealm.delete(toDeleteFolders) + } } /// Get the folder with the corresponding role in Realm. @@ -96,13 +88,10 @@ public extension MailboxManager { func createFolder(name: String, parent: Folder?) async throws -> Folder { var folder = try await apiFetcher.create(mailbox: mailbox, folder: NewFolder(name: name, path: parent?.path)) - - await backgroundRealm.execute { realm in - try? realm.safeWrite { - realm.add(folder) - if let parent { - parent.fresh(using: realm)?.children.insert(folder) - } + try writeTransaction { writableRealm in + writableRealm.add(folder) + if let parent { + parent.fresh(using: writableRealm)?.children.insert(folder) } folder = folder.freeze() } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Local.swift b/MailCore/Cache/MailboxManager/MailboxManager+Local.swift index 698f1735f..e79c7ea23 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Local.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Local.swift @@ -43,43 +43,39 @@ public extension MailboxManager { } func updateLocally(_ type: UpdateType, value: Bool, messages: [Message]) async { - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in var updateThreads = Set() - try? realm.write { - for message in messages { - guard let liveMessage = realm.object(ofType: Message.self, forPrimaryKey: message.uid) else { - continue - } + for message in messages { + guard let liveMessage = writableRealm.object(ofType: Message.self, forPrimaryKey: message.uid) else { + continue + } - type.update(message: liveMessage, with: value) + type.update(message: liveMessage, with: value) - for thread in liveMessage.threads { - updateThreads.insert(thread) - } + for thread in liveMessage.threads { + updateThreads.insert(thread) } + } - for thread in updateThreads { - guard let liveThread = realm.object(ofType: Thread.self, forPrimaryKey: thread.uid) else { - continue - } - - type.update(thread: liveThread) + for thread in updateThreads { + guard let liveThread = writableRealm.object(ofType: Thread.self, forPrimaryKey: thread.uid) else { + continue } + + type.update(thread: liveThread) } } } func markMovedLocally(_ movedLocally: Bool, threads: [Thread]) async { - await backgroundRealm.execute { realm in - try? realm.write { - for thread in threads { - guard let liveThread = realm.object(ofType: Thread.self, forPrimaryKey: thread.uid) else { - continue - } - - liveThread.isMovedOutLocally = movedLocally + try? writeTransaction { writableRealm in + for thread in threads { + guard let liveThread = writableRealm.object(ofType: Thread.self, forPrimaryKey: thread.uid) else { + continue } + + liveThread.isMovedOutLocally = movedLocally } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Message.swift b/MailCore/Cache/MailboxManager/MailboxManager+Message.swift index 600c3f678..1802d320b 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Message.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Message.swift @@ -29,11 +29,9 @@ public extension MailboxManager { let completedMessage = try await apiFetcher.message(message: message) completedMessage.fullyDownloaded = true - await backgroundRealm.execute { realm in - // Update message in Realm - try? realm.safeWrite { - realm.add(completedMessage, update: .modified) - } + // Update message in Realm + try? writeTransaction { writableRealm in + writableRealm.add(completedMessage, update: .modified) } } @@ -45,12 +43,12 @@ public extension MailboxManager { let data = try await apiFetcher.attachment(attachment: attachment) let safeAttachment = ThreadSafeReference(to: attachment) - await backgroundRealm.execute { realm in - if let liveAttachment = realm.resolve(safeAttachment) { - try? realm.safeWrite { - liveAttachment.saved = true - } + try? writeTransaction { writableRealm in + guard let liveAttachment = writableRealm.resolve(safeAttachment) else { + return } + + liveAttachment.saved = true } return data diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Search.swift b/MailCore/Cache/MailboxManager/MailboxManager+Search.swift index 217c28984..1097362a4 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Search.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Search.swift @@ -42,21 +42,21 @@ public extension MailboxManager { return searchFolder } - func clearSearchResults(searchFolder: Folder, using realm: Realm) { - try? realm.safeWrite { - realm.delete(realm.objects(Message.self).where { $0.fromSearch == true }) - realm.delete(realm.objects(Thread.self).where { $0.fromSearch == true }) - searchFolder.threads.removeAll() - } + func clearSearchResults(searchFolder: Folder, writableRealm: Realm) { + writableRealm.delete(writableRealm.objects(Message.self).where { $0.fromSearch == true }) + writableRealm.delete(writableRealm.objects(Thread.self).where { $0.fromSearch == true }) + searchFolder.threads.removeAll() } func clearSearchResults() async { - await backgroundRealm.execute { realm in - guard let searchFolder = realm.objects(Folder.self).where({ $0.remoteId == Constants.searchFolderId }).first else { + try? writeTransaction { writableRealm in + guard let searchFolder = writableRealm.objects(Folder.self) + .where({ $0.remoteId == Constants.searchFolderId }) + .first else { return } - self.clearSearchResults(searchFolder: searchFolder, using: realm) + self.clearSearchResults(searchFolder: searchFolder, writableRealm: writableRealm) } } @@ -85,11 +85,12 @@ public extension MailboxManager { } private func prepareAndSaveSearchThreads(threadResult: ThreadResult, searchFolder: Folder?) async { - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in for thread in threadResult.threads ?? [] { - thread.makeFromSearch(using: realm) + thread.makeFromSearch(using: writableRealm) - for message in thread.messages where realm.object(ofType: Message.self, forPrimaryKey: message.uid) == nil { + for message in thread.messages + where writableRealm.object(ofType: Message.self, forPrimaryKey: message.uid) == nil { message.fromSearch = true } } @@ -102,13 +103,13 @@ public extension MailboxManager { func searchThreadsOffline(searchFolder: Folder?, filterFolderId: String, searchFilters: [SearchCondition]) async { - await backgroundRealm.execute { realm in - guard let searchFolder = searchFolder?.fresh(using: realm) else { + try? writeTransaction { writableRealm in + guard let searchFolder = searchFolder?.fresh(using: writableRealm) else { self.logError(.missingFolder) return } - self.clearSearchResults(searchFolder: searchFolder, using: realm) + self.clearSearchResults(searchFolder: searchFolder, writableRealm: writableRealm) var predicates: [NSPredicate] = [] for searchFilter in searchFilters { @@ -146,53 +147,48 @@ public extension MailboxManager { } let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) - - let filteredMessages = realm.objects(Message.self).filter(compoundPredicate) + let filteredMessages = writableRealm.objects(Message.self).filter(compoundPredicate) // Update thread in Realm - try? realm.safeWrite { - for message in filteredMessages { - let newMessage = message.detached() - newMessage.uid = "offline\(newMessage.uid)" - newMessage.fromSearch = true - - let newThread = Thread( - uid: "offlineThread\(message.uid)", - messages: [newMessage], - unseenMessages: 0, - from: Array(message.from.detached()), - to: Array(message.to.detached()), - date: newMessage.date, - hasAttachments: newMessage.hasAttachments, - hasDrafts: newMessage.isDraft, - flagged: newMessage.flagged, - answered: newMessage.answered, - forwarded: newMessage.forwarded - ) - newThread.makeFromSearch(using: realm) - newThread.subject = message.subject - searchFolder.threads.insert(newThread) - } + for message in filteredMessages { + let newMessage = message.detached() + newMessage.uid = "offline\(newMessage.uid)" + newMessage.fromSearch = true + + let newThread = Thread( + uid: "offlineThread\(message.uid)", + messages: [newMessage], + unseenMessages: 0, + from: Array(message.from.detached()), + to: Array(message.to.detached()), + date: newMessage.date, + hasAttachments: newMessage.hasAttachments, + hasDrafts: newMessage.isDraft, + flagged: newMessage.flagged, + answered: newMessage.answered, + forwarded: newMessage.forwarded + ) + newThread.makeFromSearch(using: writableRealm) + newThread.subject = message.subject + searchFolder.threads.insert(newThread) } } } func addToSearchHistory(value: String) async { - return await backgroundRealm.execute { realm in - try? realm.safeWrite { - let searchHistory: SearchHistory - if let existingSearchHistory = realm.objects(SearchHistory.self).first { - searchHistory = existingSearchHistory - } else { - searchHistory = SearchHistory() - realm.add(searchHistory) - } + try? writeTransaction { writableRealm in + let searchHistory: SearchHistory + if let existingSearchHistory = writableRealm.objects(SearchHistory.self).first { + searchHistory = existingSearchHistory + } else { + searchHistory = SearchHistory() + writableRealm.add(searchHistory) + } - if let indexToRemove = searchHistory.history.firstIndex(of: value) { - searchHistory.history.remove(at: indexToRemove) - } - searchHistory.history.insert(value, at: 0) + if let indexToRemove = searchHistory.history.firstIndex(of: value) { + searchHistory.history.remove(at: indexToRemove) } + searchHistory.history.insert(value, at: 0) } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift index 710c8c4a2..d08849708 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift @@ -102,36 +102,35 @@ public extension MailboxManager { guard !Task.isCancelled else { return } - await backgroundRealm.execute { realm in - guard let folder = folder.fresh(using: realm) else { + try? writeTransaction { writableRealm in + guard let folder = folder.fresh(using: writableRealm) else { self.logError(.missingFolder) return } - try? realm.safeWrite { - if previousCursor == nil && messagesUids.addedShortUids.count < Constants.pageSize { - folder.completeHistoryInfo() - } - if let newUnreadCount = messagesUids.folderUnreadCount { - folder.remoteUnreadCount = newUnreadCount - } - folder.computeUnreadCount() - folder.cursor = messagesUids.cursor - folder.lastUpdate = Date() + + if previousCursor == nil && messagesUids.addedShortUids.count < Constants.pageSize { + folder.completeHistoryInfo() } + if let newUnreadCount = messagesUids.folderUnreadCount { + folder.remoteUnreadCount = newUnreadCount + } + folder.computeUnreadCount() + folder.cursor = messagesUids.cursor + folder.lastUpdate = Date() SentryDebug.searchForOrphanMessages( folderId: folder.remoteId, - using: realm, + using: writableRealm, previousCursor: previousCursor, newCursor: messagesUids.cursor ) SentryDebug.searchForOrphanThreads( - using: realm, + using: writableRealm, previousCursor: previousCursor, newCursor: messagesUids.cursor ) - self.deleteOrphanMessagesAndThreads(realm, folderId: folder.remoteId) + self.deleteOrphanMessagesAndThreads(writableRealm: writableRealm, folderId: folder.remoteId) } if previousCursor != nil { @@ -253,31 +252,27 @@ public extension MailboxManager { switch direction { case .previous: - return await backgroundRealm.execute { realm in - let freshFolder = folder.fresh(using: realm) + var onePage = false + try? writeTransaction { writableRealm in + let freshFolder = folder.fresh(using: writableRealm) if messagesUids.addedShortUids.count < Constants.pageSize || messagesUids.addedShortUids.contains("1") { - try? realm.safeWrite { - freshFolder?.completeHistoryInfo() - } - return false + freshFolder?.completeHistoryInfo() + onePage = false } else { - try? realm.safeWrite { - freshFolder?.remainingOldMessagesToFetch -= Constants.pageSize - } - return true + freshFolder?.remainingOldMessagesToFetch -= Constants.pageSize + onePage = true } } + return onePage case .following: break case .none: - await backgroundRealm.execute { realm in - let freshFolder = folder.fresh(using: realm) - try? realm.safeWrite { - freshFolder?.resetHistoryInfo() + try? writeTransaction { writableRealm in + let freshFolder = folder.fresh(using: writableRealm) + freshFolder?.resetHistoryInfo() - if messagesUids.addedShortUids.count < Constants.pageSize { - freshFolder?.completeHistoryInfo() - } + if messagesUids.addedShortUids.count < Constants.pageSize { + freshFolder?.completeHistoryInfo() } } } @@ -327,89 +322,88 @@ public extension MailboxManager { } private func deleteMessages(uids: [String]) async { - guard !uids.isEmpty && !Task.isCancelled else { return } - - let backgroundTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) - await backgroundRealm.execute { realm in - let batchSize = 100 - for index in stride(from: 0, to: uids.count, by: batchSize) { - autoreleasepool { - let uidsBatch = Array(uids[index ..< min(index + batchSize, uids.count)]) - - let messagesToDelete = realm.objects(Message.self).where { $0.uid.in(uidsBatch) } - var threadsToUpdate = Set() - var threadsToDelete = Set() - var draftsToDelete = Set() - - for message in messagesToDelete { - if let draft = self.draft(messageUid: message.uid, using: realm) { - draftsToDelete.insert(draft) - } - for parent in message.threads { - threadsToUpdate.insert(parent) - } + guard !uids.isEmpty, + !Task.isCancelled else { + return + } + + // Making sure the system will not terminate the app between batches + let expiringActivity = ExpiringActivity() + expiringActivity.start() + + let batchSize = 100 + for index in stride(from: 0, to: uids.count, by: batchSize) { + try? writeTransaction { writableRealm in + let uidsBatch = Array(uids[index ..< min(index + batchSize, uids.count)]) + + let messagesToDelete = writableRealm.objects(Message.self).where { $0.uid.in(uidsBatch) } + var threadsToUpdate = Set() + var threadsToDelete = Set() + var draftsToDelete = Set() + + for message in messagesToDelete { + if let draft = self.draft(messageUid: message.uid, using: writableRealm) { + draftsToDelete.insert(draft) + } + for parent in message.threads { + threadsToUpdate.insert(parent) } + } - let foldersToUpdate = Set(threadsToUpdate.compactMap(\.folder)) + let foldersToUpdate = Set(threadsToUpdate.compactMap(\.folder)) - try? realm.safeWrite { - for draft in draftsToDelete { - if draft.action == nil { - realm.delete(draft) - } else { - draft.remoteUUID = "" - } - } + for draft in draftsToDelete { + if draft.action == nil { + writableRealm.delete(draft) + } else { + draft.remoteUUID = "" + } + } - realm.delete(messagesToDelete) - for thread in threadsToUpdate where !thread.isInvalidated { - if thread.messageInFolderCount == 0 { - threadsToDelete.insert(thread) - } else { - do { - try thread.recomputeOrFail() - } catch { - threadsToDelete.insert(thread) - SentryDebug.threadHasNilLastMessageFromFolderDate(thread: thread) - } - } - } - realm.delete(threadsToDelete) - for updateFolder in foldersToUpdate { - updateFolder.computeUnreadCount() + writableRealm.delete(messagesToDelete) + for thread in threadsToUpdate where !thread.isInvalidated { + if thread.messageInFolderCount == 0 { + threadsToDelete.insert(thread) + } else { + do { + try thread.recomputeOrFail() + } catch { + threadsToDelete.insert(thread) + SentryDebug.threadHasNilLastMessageFromFolderDate(thread: thread) } } } + writableRealm.delete(threadsToDelete) + for updateFolder in foldersToUpdate { + updateFolder.computeUnreadCount() + } } } - await backgroundTracker.end() + + expiringActivity.endAll() } private func updateMessages(updates: [MessageFlags], folder: Folder) async { guard !Task.isCancelled else { return } - let backgroundTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) - await backgroundRealm.execute { realm in + try? writeTransaction { writableRealm in var threadsToUpdate = Set() - try? realm.safeWrite { - for update in updates { - let uid = Constants.longUid(from: String(update.shortUid), folderId: folder.remoteId) - if let message = realm.object(ofType: Message.self, forPrimaryKey: uid) { - message.answered = update.answered - message.flagged = update.isFavorite - message.forwarded = update.forwarded - message.scheduled = update.scheduled - message.seen = update.seen - - for parent in message.threads { - threadsToUpdate.insert(parent) - } + for update in updates { + let uid = Constants.longUid(from: String(update.shortUid), folderId: folder.remoteId) + if let message = writableRealm.object(ofType: Message.self, forPrimaryKey: uid) { + message.answered = update.answered + message.flagged = update.isFavorite + message.forwarded = update.forwarded + message.scheduled = update.scheduled + message.seen = update.seen + + for parent in message.threads { + threadsToUpdate.insert(parent) } } - self.updateThreads(threads: threadsToUpdate, realm: realm) } + self.updateThreads(threads: threadsToUpdate, realm: writableRealm) } - await backgroundTracker.end() } private func addMessages(shortUids: [String], folder: Folder, newCursor: String?) async throws { @@ -422,11 +416,11 @@ public extension MailboxManager { messageUids: uniqueUids ) - let backgroundTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) - await backgroundRealm.execute { [self] realm in - if let folder = folder.fresh(using: realm) { - createThreads(messageByUids: messageByUidsResult, folder: folder, using: realm) + try? writeTransaction { writableRealm in + if let folder = folder.fresh(using: writableRealm) { + createThreads(messageByUids: messageByUidsResult, folder: folder, writableRealm: writableRealm) } + SentryDebug.sendMissingMessagesSentry( sentUids: uniqueUids, receivedMessages: messageByUidsResult.messages, @@ -434,7 +428,6 @@ public extension MailboxManager { newCursor: newCursor ) } - await backgroundTracker.end() } // MARK: - Thread creation @@ -444,42 +437,40 @@ public extension MailboxManager { /// - messageByUids: MessageByUidsResult (list of message) /// - folder: Given folder /// - realm: Given realm - private func createThreads(messageByUids: MessageByUidsResult, folder: Folder, using realm: Realm) { + private func createThreads(messageByUids: MessageByUidsResult, folder: Folder, writableRealm: Realm) { var threadsToUpdate = Set() - try? realm.safeWrite { - for message in messageByUids.messages { - guard realm.object(ofType: Message.self, forPrimaryKey: message.uid) == nil else { - SentrySDK.capture(message: "Found already existing message") { scope in - scope.setContext(value: ["Message": ["uid": message.uid, - "messageId": message.messageId], - "Folder": ["id": message.folder?.remoteId, - "name": message.folder?.matomoName, - "cursor": message.folder?.cursor]], - key: "Message context") - } - continue - } - message.inTrash = folder.role == .trash - message.computeReference() - - let isThreadMode = UserDefaults.shared.threadMode == .conversation - if isThreadMode { - createConversationThread( - message: message, - folder: folder, - threadsToUpdate: &threadsToUpdate, - using: realm - ) - } else { - createSingleMessageThread(message: message, folder: folder, threadsToUpdate: &threadsToUpdate) + for message in messageByUids.messages { + guard writableRealm.object(ofType: Message.self, forPrimaryKey: message.uid) == nil else { + SentrySDK.capture(message: "Found already existing message") { scope in + scope.setContext(value: ["Message": ["uid": message.uid, + "messageId": message.messageId], + "Folder": ["id": message.folder?.remoteId, + "name": message.folder?.matomoName, + "cursor": message.folder?.cursor]], + key: "Message context") } + continue + } + message.inTrash = folder.role == .trash + message.computeReference() + + let isThreadMode = UserDefaults.shared.threadMode == .conversation + if isThreadMode { + createConversationThread( + message: message, + folder: folder, + threadsToUpdate: &threadsToUpdate, + using: writableRealm + ) + } else { + createSingleMessageThread(message: message, folder: folder, threadsToUpdate: &threadsToUpdate) + } - if let message = realm.objects(Message.self).where({ $0.uid == message.uid }).first { - folder.messages.insert(message) - } + if let message = writableRealm.objects(Message.self).where({ $0.uid == message.uid }).first { + folder.messages.insert(message) } - self.updateThreads(threads: threadsToUpdate, realm: realm) } + updateThreads(threads: threadsToUpdate, realm: writableRealm) } /// Add the given message to existing compatible threads + Create a new thread if needed @@ -585,35 +576,31 @@ public extension MailboxManager { DDLogWarn("resetHistoryInfo because of lostOffsetMessageError") SentryDebug.addResetingFolderBreadcrumb(folder: folder) - await backgroundRealm.execute { realm in - guard let folder = folder.fresh(using: realm) else { + try? writeTransaction { writableRealm in + guard let folder = folder.fresh(using: writableRealm) else { self.logError(.missingFolder) return } - try? realm.write { - realm.delete(folder.messages) - realm.delete(folder.threads) - folder.lastUpdate = nil - folder.unreadCount = 0 - folder.remainingOldMessagesToFetch = Constants.messageQuantityLimit - folder.isHistoryComplete = false - folder.cursor = nil - } + writableRealm.delete(folder.messages) + writableRealm.delete(folder.threads) + folder.lastUpdate = nil + folder.unreadCount = 0 + folder.remainingOldMessagesToFetch = Constants.messageQuantityLimit + folder.isHistoryComplete = false + folder.cursor = nil } try await messages(folder: folder, isRetrying: true) } } - private func deleteOrphanMessagesAndThreads(_ realm: Realm, folderId: String) { - let orphanMessages = realm.objects(Message.self).where { $0.folderId == folderId } + private func deleteOrphanMessagesAndThreads(writableRealm: Realm, folderId: String) { + let orphanMessages = writableRealm.objects(Message.self).where { $0.folderId == folderId } .filter { $0.threads.isEmpty && $0.threadsDuplicatedIn.isEmpty } - let orphanThreads = realm.objects(Thread.self).filter { $0.folder == nil } + let orphanThreads = writableRealm.objects(Thread.self).filter { $0.folder == nil } - try? realm.safeWrite { - realm.delete(orphanMessages) - realm.delete(orphanThreads) - } + writableRealm.delete(orphanMessages) + writableRealm.delete(orphanThreads) } private func removeDuplicatedThreads( @@ -677,8 +664,8 @@ public extension MailboxManager { // MARK: - Other func saveSearchThreads(result: ThreadResult, searchFolder: Folder) async { - await backgroundRealm.execute { realm in - guard let searchFolder = searchFolder.fresh(using: realm) else { + try? writeTransaction { writableRealm in + guard let searchFolder = searchFolder.fresh(using: writableRealm) else { self.logError(.missingFolder) return } @@ -688,23 +675,20 @@ public extension MailboxManager { for thread in fetchedThreads { for message in thread.messages { - self.keepCacheAttributes(for: message, keepProperties: .standard, using: realm) + self.keepCacheAttributes(for: message, keepProperties: .standard, using: writableRealm) } } if result.currentOffset == 0 { - self.clearSearchResults(searchFolder: searchFolder, using: realm) - } + self.clearSearchResults(searchFolder: searchFolder, writableRealm: writableRealm) - // Update thread in Realm - try? realm.safeWrite { + // Update thread in Realm // Clean old threads after fetching first page - if result.currentOffset == 0 { - searchFolder.lastUpdate = Date() - } - realm.add(fetchedThreads, update: .modified) - searchFolder.threads.insert(objectsIn: fetchedThreads) + searchFolder.lastUpdate = Date() } + + writableRealm.add(fetchedThreads, update: .modified) + searchFolder.threads.insert(objectsIn: fetchedThreads) } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager.swift b/MailCore/Cache/MailboxManager/MailboxManager.swift index 8b801e7f1..79659b9f1 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager.swift @@ -29,7 +29,6 @@ public final class MailboxManager: ObservableObject, MailboxManageable { @LazyInjectService var mailboxInfosManager: MailboxInfosManager lazy var refreshActor = RefreshActor(mailboxManager: self) - let backgroundRealm: BackgroundRealm public static let constants = MailboxManagerConstants() @@ -113,8 +112,9 @@ public final class MailboxManager: ObservableObject, MailboxManageable { Attendee.self ] ) - backgroundRealm = BackgroundRealm(configuration: realmConfiguration) - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) excludeRealmFromBackup() } diff --git a/MailCore/Cache/RefreshActor.swift b/MailCore/Cache/RefreshActor.swift index fec511076..8b54809fd 100644 --- a/MailCore/Cache/RefreshActor.swift +++ b/MailCore/Cache/RefreshActor.swift @@ -100,13 +100,13 @@ public actor RefreshActor { }?.isDefaultReply = true } - await mailboxManager.backgroundRealm.execute { realm in + try? mailboxManager.writeTransaction { writableRealm in let signaturesToDelete: Set // no longer present server side let signaturesToUpdate: [Signature] // updated signatures let signaturesToAdd: [Signature] // new signatures // fetch all local signatures - let existingSignatures = Array(realm.objects(Signature.self)) + let existingSignatures = Array(writableRealm.objects(Signature.self)) // filter out signatures that may no longer be valid realm objects updatedSignatures = updatedSignatures.filter { !$0.isInvalidated } @@ -126,11 +126,9 @@ public actor RefreshActor { // NOTE: local drafts in `signaturesToDelete` should be migrated to use the new default signature. // Update signatures in Realm - try? realm.safeWrite { - realm.add(signaturesToUpdate, update: .modified) - realm.delete(signaturesToDelete) - realm.add(signaturesToAdd, update: .modified) - } + writableRealm.add(signaturesToUpdate, update: .modified) + writableRealm.delete(signaturesToDelete) + writableRealm.add(signaturesToAdd, update: .modified) } } } diff --git a/MailCore/Utils/Model/Realm/BackgroundRealm.swift b/MailCore/Utils/Model/Realm/BackgroundRealm.swift deleted file mode 100644 index ccafdc5cb..000000000 --- a/MailCore/Utils/Model/Realm/BackgroundRealm.swift +++ /dev/null @@ -1,58 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import InfomaniakCore -import RealmSwift -import Sentry - -/// Conforming to `RealmAccessible` to get a standard `.getRealm` function -extension BackgroundRealm: MailCoreRealmAccessible {} - -/// Async await db transactions. Can provide a Realm. -public final class BackgroundRealm { - private let queue: DispatchQueue - - public let realmConfiguration: Realm.Configuration - - public init(configuration: Realm.Configuration) { - guard let fileURL = configuration.fileURL else { - fatalError("Realm configurations without file URL not supported") - } - realmConfiguration = configuration - queue = DispatchQueue(label: "com.infomaniak.mail.\(fileURL.lastPathComponent)", autoreleaseFrequency: .workItem) - } - - public func execute(_ block: @escaping (Realm) -> T, completion: @escaping (T) -> Void) { - let expiringActivity = ExpiringActivity() - expiringActivity.start() - queue.async { - let realm = self.getRealm() - completion(block(realm)) - expiringActivity.endAll() - } - } - - public func execute(_ block: @escaping (Realm) -> T) async -> T { - return await withCheckedContinuation { (continuation: CheckedContinuation) in - execute(block) { result in - continuation.resume(returning: result) - } - } - } -} diff --git a/MailCore/Utils/Model/Realm/RealmAccessible.swift b/MailCore/Utils/Model/Realm/RealmAccessible.swift index 1e2355545..27fc05502 100644 --- a/MailCore/Utils/Model/Realm/RealmAccessible.swift +++ b/MailCore/Utils/Model/Realm/RealmAccessible.swift @@ -77,3 +77,8 @@ public extension RealmConfigurable { try? realmFolderURL.setResourceValues(metadata) } } + +/// Some type conforming to MailCoreRealmAccessible +struct MailCoreRealmAccessor: MailCoreRealmAccessible { + var realmConfiguration: Realm.Configuration +} diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index cdd3b8451..96121a125 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -33,8 +33,8 @@ struct MCKContactManageable_FolderListViewModel: ContactManageable, MCKTransacti init(realmConfiguration: RealmSwift.Realm.Configuration) { self.realmConfiguration = realmConfiguration - let backgroundRealm = BackgroundRealm(configuration: realmConfiguration) - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) } func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection { [] } diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index c5df910b1..c32298f27 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -35,8 +35,8 @@ struct MCKContactManageable_SearchViewModel: ContactManageable, MCKTransactionab init(realmConfiguration: RealmSwift.Realm.Configuration) { self.realmConfiguration = realmConfiguration - let backgroundRealm = BackgroundRealm(configuration: realmConfiguration) - transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + let realmAccessor = MailCoreRealmAccessor(realmConfiguration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: realmAccessor) } func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection { [] }