commit dd54c58cbc1993836b39ae625dd1c265d63dc5f1
parent 9268ce328a22b85134d4fc9a2b4cb8a066f0f11a
Author: Michael Camilleri <[email protected]>
Date: Mon, 1 Jun 2026 16:59:20 +0900
Settle redundant journal uploads instead of retrying
Diffstat:
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -1425,11 +1425,12 @@ actor SyncEngine {
}
let ctx = persistence.container.newBackgroundContext()
ctx.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
- let (failureMessages, orphanedZones, resolvedDecisions):
- ([String], Set<CKRecordZone.ID>, Set<CKRecord.ID>) = ctx.performAndWait {
+ let (failureMessages, orphanedZones, resolvedDecisions, settledJournals):
+ ([String], Set<CKRecordZone.ID>, Set<CKRecord.ID>, Set<CKRecord.ID>) = ctx.performAndWait {
var messages: [String] = []
var orphaned = Set<CKRecordZone.ID>()
var settledDecisions = Set<CKRecord.ID>()
+ var settledJournals = Set<CKRecord.ID>()
for record in event.savedRecords {
self.writeBackSystemFields(record: record, in: ctx)
let savedName = record.recordID.recordName
@@ -1461,6 +1462,23 @@ actor SyncEngine {
// immutable record doesn't retry-loop forever.
settledDecisions.insert(failure.record.recordID)
messages.append("send: decision \(name) already present — settled")
+ } else if name.hasPrefix("journal-"),
+ err.domain == CKErrorDomain,
+ err.code == CKError.serverRecordChanged.rawValue,
+ let (gid, _, _) = RecordSerializer.parseJournalRecordName(name) {
+ // Journals are write-once at completion: "record to insert
+ // already exists" means this device's journal is already
+ // durable server-side (a backstop re-enqueue, or a first
+ // upload on a build predating the `journalUploaded` flag).
+ // There is no system-fields archive to adopt for an update
+ // and the content is frozen, so the re-send is a no-op —
+ // settle it like a Decision: drop the pending change and
+ // mark the game uploaded so `reconcilePendingJournalUploads`
+ // stops re-enqueuing it. Without this the save fails every
+ // sweep and `Pending Changes` never drains.
+ settledJournals.insert(failure.record.recordID)
+ self.markJournalUploaded(gameID: gid, in: ctx)
+ messages.append("send: journal \(name) already present — settled")
} else if self.recoverServerChangedSave(failure.error, failedRecordName: name, in: ctx) {
messages.append(
"send: recovered stale system fields for \(name) from CloudKit server record"
@@ -1483,7 +1501,7 @@ actor SyncEngine {
)
}
}
- return (messages, orphaned, settledDecisions)
+ return (messages, orphaned, settledDecisions, settledJournals)
}
if !orphanedZones.isEmpty {
await applyZoneOrphaning(orphanedZones, isPrivate: isPrivate)
@@ -1493,6 +1511,14 @@ actor SyncEngine {
pendingRecordZoneChanges: resolvedDecisions.map { .saveRecord($0) }
)
}
+ if !settledJournals.isEmpty {
+ // Drop from whichever engine owns the zone (private for solo games,
+ // shared for collaborations) — unlike decisions, journals ride both.
+ let engine = isPrivate ? privateEngine : sharedEngine
+ engine?.state.remove(
+ pendingRecordZoneChanges: settledJournals.map { .saveRecord($0) }
+ )
+ }
for message in failureMessages {
await trace(message)
}