commit c5b5e6280d4b6b1100679b6d015f5835b89b5b6c
parent 66547492b54fbf359c3d21b8b8f8b7b87958d3cf
Author: Michael Camilleri <[email protected]>
Date: Thu, 14 May 2026 14:35:37 +0900
Avoid updated timestamps in Game records being overwritten with earlier timestamps
Diffstat:
2 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/Crossmate/Sync/RecordSerializer.swift b/Crossmate/Sync/RecordSerializer.swift
@@ -342,12 +342,18 @@ enum RecordSerializer {
}
// Seed createdAt/updatedAt from the server record so the library
- // can order newly-arrived games. The CKRecord timestamps are the
- // source of truth when we don't have a local creation event.
+ // can order newly-arrived games. After that, Game records may lag
+ // behind fresher Moves timestamps, so never move updatedAt backward.
if entity.createdAt == nil {
entity.createdAt = record.creationDate ?? Date()
}
- entity.updatedAt = record.modificationDate ?? entity.updatedAt ?? Date()
+ if let modificationDate = record.modificationDate {
+ if entity.updatedAt.map({ $0 < modificationDate }) ?? true {
+ entity.updatedAt = modificationDate
+ }
+ } else if entity.updatedAt == nil {
+ entity.updatedAt = Date()
+ }
entity.ckRecordName = recordName
entity.ckSystemFields = encodeSystemFields(of: record)
diff --git a/Tests/Unit/RecordSerializerTests.swift b/Tests/Unit/RecordSerializerTests.swift
@@ -135,6 +135,35 @@ struct RecordSerializerTests {
#expect(merged.title == "Updated") // mutable field updated
}
+ @Test("applyGameRecord does not lower an existing updatedAt")
+ @MainActor func applyGameRecordPreservesFresherUpdatedAt() throws {
+ let persistence = makeTestPersistence()
+ let ctx = persistence.viewContext
+ let gameID = UUID()
+ let zone = RecordSerializer.zoneID(for: gameID)
+ let recordName = RecordSerializer.recordName(forGameID: gameID)
+ let recordID = CKRecord.ID(recordName: recordName, zoneID: zone)
+
+ let entity = GameEntity(context: ctx)
+ let newerUpdatedAt = Date(timeIntervalSince1970: 1_700_000_500)
+ entity.id = gameID
+ entity.ckRecordName = recordName
+ entity.title = "Local"
+ entity.puzzleSource = ""
+ entity.createdAt = Date(timeIntervalSince1970: 1_700_000_000)
+ entity.updatedAt = newerUpdatedAt
+
+ let record = CKRecord(recordType: "Game", recordID: recordID)
+ record["title"] = "Remote" as CKRecordValue
+
+ let merged = RecordSerializer.applyGameRecord(record, to: ctx)
+ try ctx.save()
+
+ #expect(merged === entity)
+ #expect(merged.title == "Remote")
+ #expect(merged.updatedAt == newerUpdatedAt)
+ }
+
// MARK: - System fields round-trip
@Test("Encode and decode system fields preserves record type and zone")