commit f105f11ab105ea5342e4ecb52a739a931fa81153
parent 2df79942d9cf4f9ad750337bc6c1aa828f95593a
Author: Michael Camilleri <[email protected]>
Date: Wed, 29 Apr 2026 08:34:42 +0900
Drop parent references from Move, Snapshot and Player records
Owner-side pushes to a shared game zone were failing with CKError BadSyntax
(2006) with the message 'Chaining supported for hierarchical sharing only'.
RecordSerializer was stamping every Move, Snapshot, and Player record with a
record.parent reference pointing at the owning Game record, but ShareController
creates a zone-wide CKShare via CKShare(recordZoneID:). CloudKit only accepts
parent chaining for hierarchical sharing rooted at a single record; on a
zone-wide share the chain is rejected. The result in practice was that the
share owner's edits never reached the server, while participants--writing
through the shared database, where the same validation does not fire--synced
normally. Diagnostics logs from a two-user session showed every owner-side
`private sent` batch failing with this error and zero successful saves, which
matched the reported symptom that one player could not see the other's changes.
The three record types are removed-parent only--they live in the same
zone as the Game record and the zone-wide share already covers them, so
no parent reference is needed.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
3 files changed, 2 insertions(+), 52 deletions(-)
diff --git a/Crossmate/Sync/RecordSerializer.swift b/Crossmate/Sync/RecordSerializer.swift
@@ -87,9 +87,6 @@ enum RecordSerializer {
record["authorID"] = move.authorID as CKRecordValue?
record["createdAt"] = move.createdAt as CKRecordValue
- let gameName = recordName(forGameID: move.gameID)
- let parentID = CKRecord.ID(recordName: gameName, zoneID: zone)
- record.parent = CKRecord.Reference(recordID: parentID, action: .none)
return record
}
@@ -113,9 +110,6 @@ enum RecordSerializer {
record["createdAt"] = snapshot.createdAt as CKRecordValue
record["gridState"] = try MoveLog.encodeGridState(snapshot.grid) as CKRecordValue
- let gameName = recordName(forGameID: snapshot.gameID)
- let parentID = CKRecord.ID(recordName: gameName, zoneID: zone)
- record.parent = CKRecord.Reference(recordID: parentID, action: .none)
return record
}
@@ -181,9 +175,6 @@ enum RecordSerializer {
record["selDir"] = nil
}
- let gameName = self.recordName(forGameID: gameID)
- let parentID = CKRecord.ID(recordName: gameName, zoneID: zone)
- record.parent = CKRecord.Reference(recordID: parentID, action: .none)
return record
}
diff --git a/Tests/Unit/MoveLogTests.swift b/Tests/Unit/MoveLogTests.swift
@@ -252,29 +252,7 @@ struct RecordSerializerMoveSnapshotTests {
#expect(parsed == snapshot)
}
- @Test("Move record sets parent reference to the owning game")
- func moveRecordHasGameParent() {
- let move = Move(
- gameID: gameID,
- lamport: 1,
- row: 0,
- col: 0,
- letter: "A",
- markKind: 0,
- checkedWrong: false,
- authorID: nil,
- createdAt: Date()
- )
- let record = RecordSerializer.moveRecord(
- from: move,
- zone: zoneID,
- systemFields: nil
- )
- let expectedParentName = RecordSerializer.recordName(forGameID: gameID)
- #expect(record.parent?.recordID.recordName == expectedParentName)
- }
-
- @Test("Parsing rejects records with the wrong record type")
+@Test("Parsing rejects records with the wrong record type")
func parseRejectsWrongRecordType() {
let zoneID = RecordSerializer.zoneID(for: gameID)
let recordID = CKRecord.ID(recordName: RecordSerializer.recordName(forMoveInGame: gameID, lamport: 1), zoneID: zoneID)
diff --git a/Tests/Unit/Sync/PerGameZoneTests.swift b/Tests/Unit/Sync/PerGameZoneTests.swift
@@ -28,26 +28,7 @@ struct PerGameZoneTests {
#expect(zone.ownerName == "_someOwnerID")
}
- @Test("moveRecord parent reference targets the per-game zone")
- func moveRecordParentZone() {
- let gameID = UUID()
- let zoneID = RecordSerializer.zoneID(for: gameID)
- let move = Move(
- gameID: gameID,
- lamport: 1,
- row: 0, col: 0,
- letter: "A",
- markKind: 0,
- checkedWrong: false,
- authorID: nil,
- createdAt: Date()
- )
- let record = RecordSerializer.moveRecord(from: move, zone: zoneID, systemFields: nil)
- #expect(record.recordID.zoneID.zoneName == "game-\(gameID.uuidString)")
- #expect(record.parent?.recordID.zoneID.zoneName == "game-\(gameID.uuidString)")
- }
-
- @Test("recordName(forGameID:) embedded in zoneID zoneName")
+@Test("recordName(forGameID:) embedded in zoneID zoneName")
func gameRecordNameMatchesZoneName() {
let id = UUID()
let zoneName = RecordSerializer.zoneID(for: id).zoneName