crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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:
MCrossmate/Sync/RecordSerializer.swift | 9---------
MTests/Unit/MoveLogTests.swift | 24+-----------------------
MTests/Unit/Sync/PerGameZoneTests.swift | 21+--------------------
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