crossmate

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

commit f53f637f85b8b258bdfc6c1dc8edbe2a0ab2dcbb
parent 43dbd8f3d2a8ae02d5fd788329f33314e49b57bb
Author: Michael Camilleri <[email protected]>
Date:   Tue, 16 Jun 2026 06:44:51 +0900

Refresh the library row when a puzzle upgrade rewrites its grid

When a bundled puzzle was upgraded in place, Game List could keep
showing the old title and silhouette until some unrelated cell activity
touched the game. The library row is memoised by GameSummaryCache,
whose key leant on updatedAt as a single proxy for 'the summary might
have changed', on the basis that MovesUpdater bumps updatedAt
atomically with every cell write. But replacePuzzleSource rewrites the
title and the cached summary fields — publisher, date, grid dimensions
and the block mask — without bumping updatedAt, so the stale cached
summary survived and the row never repainted.

This commit adds the puzzle-structure fields (title, cached publisher
and date, grid width and height, and the block mask) to the cache key,
so a hit now requires those to match as well and an upgrade refreshes
the row immediately. Bumping updatedAt in the upgrade path was the other
option but was rejected: updatedAt is also the Game List sort key and
the cell-merge last-writer-wins watermark, so nudging it would jerk a
silently-upgraded game to the top of the list and inject a meaningless
timestamp into the merge comparison. The filled-cell thumbnail stays
proxied by updatedAt as before, since that genuinely does move with each
cell write.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Persistence/GameStore.swift | 24+++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift @@ -156,8 +156,14 @@ struct GameCloudDeletion: Sendable, Equatable { /// entity's fields actually change. The cache key intentionally uses fast /// scalar/string fields so a hit never has to fault the `cells` /// relationship; `MovesUpdater` bumps `updatedAt` atomically with cell -/// writes, so it acts as a faithful proxy for "thumbnail might have -/// changed". +/// writes, so it acts as a faithful proxy for "the filled-cell thumbnail +/// might have changed". +/// +/// The puzzle-structure fields (`title`, cached publisher/date, grid dims, +/// block mask) are keyed directly because they are *not* proxied by +/// `updatedAt`: `replacePuzzleSource` rewrites them during an NYT-style +/// upgrade without bumping `updatedAt`, so the row would otherwise render +/// the old title/grid until unrelated cell activity nudged the proxy. @MainActor final class GameSummaryCache { private struct Key: Equatable { @@ -168,6 +174,12 @@ final class GameSummaryCache { let scope: Int16 let shareName: String? let revoked: Bool + let title: String? + let publisher: String? + let puzzleDate: Date? + let gridWidth: Int16 + let gridHeight: Int16 + let blockMask: Data? } private var entries: [NSManagedObjectID: (key: Key, summary: GameSummary)] = [:] @@ -179,7 +191,13 @@ final class GameSummaryCache { readThrough: entity.readThroughAt, scope: entity.databaseScope, shareName: entity.ckShareRecordName, - revoked: entity.isAccessRevoked + revoked: entity.isAccessRevoked, + title: entity.title, + publisher: entity.cachedPublisher, + puzzleDate: entity.cachedPuzzleDate, + gridWidth: entity.gridWidth, + gridHeight: entity.gridHeight, + blockMask: entity.blockMask ) if let hit = entries[entity.objectID], hit.key == key { return hit.summary