fix sync: INSERT OR IGNORE for duplicate messages, fix leaked IMAP continuations

- insertMessages uses INSERT OR IGNORE on (mailboxId, uid) conflict instead of
  crashing on UNIQUE constraint when messages are re-fetched after restart
- IMAPResponseHandler.sendCommand resumes any leaked previous continuation before
  registering a new one, preventing DuplicateCommandTag errors
- add channelInactive handler to resume pending continuations on connection drop
- add error type to sync failure log for better diagnostics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 11:58:29 +01:00
parent 1e39a2bd43
commit ff91e397e8
3 changed files with 20 additions and 3 deletions

View File

@@ -207,8 +207,10 @@ final class MailViewModel {
try await coordinator.syncNow()
syncState = coordinator.syncState
} catch {
errorMessage = error.localizedDescription
syncState = .error(error.localizedDescription)
let desc = "\(type(of: error)): \(error.localizedDescription)"
print("[MailViewModel] syncNow failed: \(desc)")
errorMessage = desc
syncState = .error(desc)
}
}

View File

@@ -53,6 +53,15 @@ final class IMAPResponseHandler: ChannelInboundHandler, RemovableChannelHandler,
}
}
func channelInactive(context: ChannelHandlerContext) {
let error = IMAPError.notConnected
continuation?.resume(throwing: error)
continuation = nil
greetingContinuation?.resume(throwing: error)
greetingContinuation = nil
context.fireChannelInactive()
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
continuation?.resume(throwing: error)
continuation = nil
@@ -68,6 +77,11 @@ final class IMAPResponseHandler: ChannelInboundHandler, RemovableChannelHandler,
}
func sendCommand(tag: String, continuation cont: CheckedContinuation<[Response], Error>) {
// Resume any leaked continuation from a previous command to avoid
// "SWIFT TASK CONTINUATION MISUSE: leaked its continuation"
if let old = continuation {
old.resume(throwing: IMAPError.serverError("Previous command interrupted"))
}
expectedTag = tag
continuation = cont
buffer = []

View File

@@ -58,7 +58,8 @@ public final class MailStore: Sendable {
public func insertMessages(_ messages: [MessageRecord]) throws {
try dbWriter.write { db in
for message in messages {
try message.save(db)
// INSERT OR IGNORE: skip if (mailboxId, uid) already exists from a prior sync
try message.insert(db, onConflict: .ignore)
}
}
}