commit 6c3d17d1ac04cf27f55751cac132404800502d12
parent 397967d5c19b6ac75408a8bfa6ef6ebd71abd602
Author: Michael Camilleri <[email protected]>
Date: Sat, 20 Jun 2026 14:52:44 +0900
Recover root Game conflicts while sharing
Sharing a newly imported puzzle could fail when the share preparation
save raced the pending CKSyncEngine create for the same root Game
record. CloudKit accepted one insert and rejected the other as
serverRecordChanged, surfacing 'record to insert already exists' instead
of letting the user create the share.
This commit treats that conflict as a recoverable existing-record path.
The share controller adopts the server Game record from the conflict
payload, or refetches it when the payload is absent, repopulates it with
the local fields, and saves again with the server change tag before
writing the system fields back locally.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/Crossmate/Sync/ShareController.swift b/Crossmate/Sync/ShareController.swift
@@ -861,7 +861,23 @@ final class ShareController {
from: entity,
includePuzzleSource: includePuzzleSource
)
- let saved = try await container.privateCloudDatabase.save(record)
+ let saved: CKRecord
+ do {
+ saved = try await container.privateCloudDatabase.save(record)
+ } catch let error as CKError where error.code == .serverRecordChanged {
+ let serverRecord: CKRecord
+ if let conflictRecord = (error as NSError).userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord {
+ serverRecord = conflictRecord
+ } else {
+ serverRecord = try await container.privateCloudDatabase.record(for: recordID)
+ }
+ RecordSerializer.populateGameRecord(
+ serverRecord,
+ from: entity,
+ includePuzzleSource: serverRecord["puzzleSource"] == nil
+ )
+ saved = try await container.privateCloudDatabase.save(serverRecord)
+ }
entity.ckSystemFields = RecordSerializer.encodeSystemFields(of: saved)
entity.lastSyncedAt = Date()
if entity.ckZoneName == nil {