fix code review issues: deferral date format, storeFlags SELECT, event loop leaks, GTD selection tracking
- fix deferral resurfacing using VTODOParser.formatDateOnly instead of ISO8601 (date format mismatch) - add SELECT mailbox before UID STORE in storeFlags (IMAP protocol requirement) - pass credentials to SyncCoordinator so IDLE monitoring activates - add selectedItem tracking to MailViewModel, wire List selection in GTD views - fix startPeriodicSync to sleep-first, preventing duplicate sync on launch - add deinit cleanup for EventLoopGroup in IMAPConnection, SMTPConnection - use separate IMAP client for attachment downloads, avoid shared connection interference - remove [weak self] from IMAPIdleClient actor Task to prevent orphaned connections - fix isGTDPerspective to check selectedMailbox instead of items.isEmpty - fix fetchBody to use complete RFC822 fetch instead of BODY[TEXT] - reuse single IMAP connection per ActionQueue.flush() batch - add requiresIMAP to ActionPayload for connection batching - load task categories from label store instead of hardcoded empty array - suppress NIOSSLHandler Sendable warnings via Package.swift unsafeFlags - fix unused variable warnings across codebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,18 +42,34 @@ public actor ActionQueue {
|
||||
// MARK: - Flush
|
||||
|
||||
/// Dispatch all pending actions in creation order. Called before IMAP fetch.
|
||||
/// Reuses a single IMAP connection for all IMAP actions in the batch.
|
||||
public func flush() async {
|
||||
guard let records = try? store.pendingActions(accountId: accountId) else { return }
|
||||
guard let records = try? store.pendingActions(accountId: accountId),
|
||||
!records.isEmpty else { return }
|
||||
|
||||
// Create one shared IMAP client for all IMAP actions in this flush
|
||||
let sharedImapClient = imapClientProvider()
|
||||
var imapConnected = false
|
||||
|
||||
defer {
|
||||
if imapConnected {
|
||||
Task { try? await sharedImapClient.disconnect() }
|
||||
}
|
||||
}
|
||||
|
||||
for record in records {
|
||||
guard let action = decodeAction(record) else {
|
||||
// Corrupt record — remove it
|
||||
try? store.deletePendingAction(id: record.id)
|
||||
continue
|
||||
}
|
||||
|
||||
do {
|
||||
try await dispatch(action)
|
||||
// Connect shared IMAP client on first IMAP action
|
||||
if !imapConnected, action.payload.requiresIMAP {
|
||||
try await sharedImapClient.connect()
|
||||
imapConnected = true
|
||||
}
|
||||
try await dispatch(action, sharedImapClient: sharedImapClient)
|
||||
try store.deletePendingAction(id: record.id)
|
||||
} catch {
|
||||
var updated = record
|
||||
@@ -88,26 +104,25 @@ public actor ActionQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private func dispatch(_ action: PendingAction) async throws {
|
||||
private func dispatch(_ action: PendingAction, sharedImapClient: (any IMAPClientProtocol)? = nil) async throws {
|
||||
switch action.payload {
|
||||
case .setFlags(let uid, let mailbox, let add, let remove):
|
||||
let client = imapClientProvider()
|
||||
try await client.connect()
|
||||
defer { Task { try? await client.disconnect() } }
|
||||
let client = sharedImapClient ?? imapClientProvider()
|
||||
if sharedImapClient == nil { try await client.connect() }
|
||||
defer { if sharedImapClient == nil { Task { try? await client.disconnect() } } }
|
||||
try await client.storeFlags(uid: uid, mailbox: mailbox, add: add, remove: remove)
|
||||
|
||||
case .move(let uid, let from, let to):
|
||||
let client = imapClientProvider()
|
||||
try await client.connect()
|
||||
defer { Task { try? await client.disconnect() } }
|
||||
let client = sharedImapClient ?? imapClientProvider()
|
||||
if sharedImapClient == nil { try await client.connect() }
|
||||
defer { if sharedImapClient == nil { Task { try? await client.disconnect() } } }
|
||||
try await client.moveMessage(uid: uid, from: from, to: to)
|
||||
|
||||
case .delete(let uid, let mailbox, let trashMailbox):
|
||||
let client = imapClientProvider()
|
||||
try await client.connect()
|
||||
defer { Task { try? await client.disconnect() } }
|
||||
let client = sharedImapClient ?? imapClientProvider()
|
||||
if sharedImapClient == nil { try await client.connect() }
|
||||
defer { if sharedImapClient == nil { Task { try? await client.disconnect() } } }
|
||||
if mailbox == trashMailbox {
|
||||
// Already in trash — permanent delete
|
||||
try await client.storeFlags(uid: uid, mailbox: mailbox, add: ["\\Deleted"], remove: [])
|
||||
try await client.expunge(mailbox: mailbox)
|
||||
} else {
|
||||
@@ -122,9 +137,9 @@ public actor ActionQueue {
|
||||
try await client.send(message: message)
|
||||
|
||||
case .append(let mailbox, let messageData, let flags):
|
||||
let client = imapClientProvider()
|
||||
try await client.connect()
|
||||
defer { Task { try? await client.disconnect() } }
|
||||
let client = sharedImapClient ?? imapClientProvider()
|
||||
if sharedImapClient == nil { try await client.connect() }
|
||||
defer { if sharedImapClient == nil { Task { try? await client.disconnect() } } }
|
||||
guard let data = messageData.data(using: .utf8) else {
|
||||
throw ActionQueueError.invalidMessageData
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user