crossmate

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

commit 6a360059c435bee2c8e8d21589aec43072fbacfb
parent 19772412fa3d29376a77791825f1805d4c59c688
Author: Michael Camilleri <[email protected]>
Date:   Thu, 23 Jul 2026 22:00:10 +0900

Remove bugfix migrations for Chronicles

Diffstat:
MCrossmate/Services/AppServices.swift | 11-----------
MCrossmate/Sync/SyncEngine.swift | 70----------------------------------------------------------------------
MShared/NotificationState.swift | 12------------
MTests/Unit/ArchiveTests.swift | 22----------------------
4 files changed, 0 insertions(+), 115 deletions(-)

diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -1351,17 +1351,6 @@ final class AppServices { syncMonitor.note("iCloud sync disabled — engine startup skipped") return } - - // TEMPORARY v1.1 MIGRATION: early Chronicle builds opened their local - // projections as live games and consequently wrote Player records into - // the private Chronicle zone. The permanent lifecycle gate lives with - // PuzzleDisplayView; this one-shot repair can later be removed as one - // block together with `purgeChroniclePlayers_v1` and its - // NotificationState flag. - Task { [syncEngine] in - await syncEngine.purgeChroniclePlayers_v1() - } - // One-shot migration of pre-existing single-zone friendships to the // two-mailbox model. Runs only once the engine is up (it enqueues zone // creates, a share, and a bootstrap ping) and only with sync enabled. diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift @@ -1048,76 +1048,6 @@ actor SyncEngine { } } - /// TEMPORARY v1.1 MIGRATION: removes `Player` records accidentally written - /// into the private Chronicle zone by early Chronicle builds. Remove this - /// method together with its AppServices startup call and NotificationState - /// flag after those builds have aged out. - func purgeChroniclePlayers_v1() async { - guard NotificationState.chroniclePlayerPurgeNeeded() else { return } - - // Cancel unsent writes first; otherwise a queued clock/presence update - // could recreate a record immediately after the server-side deletion. - if let privateEngine { - let pendingSaves = privateEngine.state.pendingRecordZoneChanges.filter { change in - guard case .saveRecord(let recordID) = change else { return false } - return Self.isChroniclePlayerRecordID(recordID) - } - if !pendingSaves.isEmpty { - privateEngine.state.remove(pendingRecordZoneChanges: pendingSaves) - } - } - - do { - let records = try await chroniclePlayerRecords() - try await deleteRecords( - withIDs: records.map(\.recordID), - in: container.privateCloudDatabase - ) - NotificationState.markChroniclePlayersPurged() - await trace( - "Chronicle Player purge: cancelled pending saves and deleted " + - "\(records.count) record(s)" - ) - } catch { - await trace("Chronicle Player purge failed: \(describe(error))") - } - } - - /// Enumerates the zone's change history rather than issuing a CKQuery. - /// Production does not have a queryable `recordName` index for Player, but - /// a zone-change fetch needs no schema index and is already the mechanism - /// used by the storage audit. - private func chroniclePlayerRecords() async throws -> [CKRecord] { - let database = container.privateCloudDatabase - var records: [CKRecord] = [] - var token: CKServerChangeToken? - var moreComing = true - - while moreComing { - let page = try await database.recordZoneChanges( - inZoneWith: Archive.zoneID, - since: token, - desiredKeys: [] - ) - token = page.changeToken - moreComing = page.moreComing - for result in page.modificationResultsByID.values { - let record = try result.get().record - if record.recordType == "Player" { - records.append(record) - } - } - } - return records - } - - /// Pure classifier shared with tests so the migration cannot remove a - /// Player record from a live game zone. - nonisolated static func isChroniclePlayerRecordID(_ recordID: CKRecord.ID) -> Bool { - recordID.zoneID == Archive.zoneID - && RecordSerializer.parsePlayerRecordName(recordID.recordName) != nil - } - private func purgeLegacyPlayPings( in zoneIDs: [CKRecordZone.ID], database: CKDatabase diff --git a/Shared/NotificationState.swift b/Shared/NotificationState.swift @@ -106,7 +106,6 @@ enum NotificationState { private static let staleHailPurgeKey = "migration.staleHailPurge.v1" private static let debugPreviewFriendPurgeKey = "migration.debugPreviewFriendPurge.v1" private static let legacyPlayPingPurgeKey = "migration.legacyPlayPingPurge.v1" - private static let chroniclePlayerPurgeKey = "migration.chroniclePlayerPurge.v1" private static let friendMailboxMigrationKey = "migration.friendMailboxes.v1" /// True if the one-shot cleanup of legacy `.opened`/`.closed` lease pings @@ -173,17 +172,6 @@ enum NotificationState { defaults?.set(true, forKey: legacyPlayPingPurgeKey) } - /// True until this device has successfully removed the `Player` records - /// written into the Chronicle zone by the first v1.1 builds. - static func chroniclePlayerPurgeNeeded() -> Bool { - defaults?.bool(forKey: chroniclePlayerPurgeKey) == false - } - - /// Records that the Chronicle-zone Player cleanup completed successfully. - static func markChroniclePlayersPurged() { - defaults?.set(true, forKey: chroniclePlayerPurgeKey) - } - /// True if the one-shot migration of pre-mailbox friendships (a single /// elected-owner shared zone) to the two-mailbox model has not yet run on /// this device. diff --git a/Tests/Unit/ArchiveTests.swift b/Tests/Unit/ArchiveTests.swift @@ -98,28 +98,6 @@ struct ArchiveTests { private let aliceKey = JournalDeviceKey(authorID: "alice", deviceID: "deviceA") private let bobKey = JournalDeviceKey(authorID: "bob", deviceID: "deviceB") - @Test("Chronicle Player cleanup is confined to Player IDs in the archive zone") - func chroniclePlayerCleanupScope() { - let gameID = UUID() - let playerName = "player-\(gameID.uuidString)-alice" - let archivePlayer = CKRecord.ID(recordName: playerName, zoneID: Archive.zoneID) - let livePlayer = CKRecord.ID( - recordName: playerName, - zoneID: CKRecordZone.ID( - zoneName: gameID.uuidString, - ownerName: CKCurrentUserDefaultName - ) - ) - let chronicle = CKRecord.ID( - recordName: "chronicle-\(gameID.uuidString)", - zoneID: Archive.zoneID - ) - - #expect(SyncEngine.isChroniclePlayerRecordID(archivePlayer)) - #expect(!SyncEngine.isChroniclePlayerRecordID(livePlayer)) - #expect(!SyncEngine.isChroniclePlayerRecordID(chronicle)) - } - private func sampleSnapshot(originalGameID: UUID) -> Archive.Snapshot { Archive.Snapshot( originalGameID: originalGameID,