commit 9448e53ad968310e2e981260647760c279723f33
parent 16bd2fc90e1639aa5c70d6311ee7438f7702aff3
Author: Michael Camilleri <[email protected]>
Date: Fri, 24 Apr 2026 06:53:26 +0900
Ensure that list previews are up to date
After the recent changes to the data flow, list previews in Crossmate
were not updating when the user returned from the puzzle view to the
list view. This commit fixes that.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Diffstat:
3 files changed, 54 insertions(+), 1 deletion(-)
diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift
@@ -107,6 +107,50 @@ final class GameStore {
restore(game: game, from: entity)
}
+ /// Replays the move log for each game ID and updates the `CellEntity`
+ /// cache so that list thumbnails reflect local edits immediately after a
+ /// `MoveBuffer` flush, without waiting for the next sync cycle.
+ func replayCellCaches(for gameIDs: Set<UUID>) {
+ for gameID in gameIDs {
+ let req = NSFetchRequest<GameEntity>(entityName: "GameEntity")
+ req.predicate = NSPredicate(format: "id == %@", gameID as CVarArg)
+ req.fetchLimit = 1
+ guard let entity = try? context.fetch(req).first else { continue }
+
+ let snapReq = NSFetchRequest<SnapshotEntity>(entityName: "SnapshotEntity")
+ snapReq.predicate = NSPredicate(format: "game == %@", entity)
+ let snapshots: [Snapshot] = ((try? context.fetch(snapReq)) ?? []).compactMap { se in
+ guard let data = se.gridState,
+ let grid = try? MoveLog.decodeGridState(data) else { return nil }
+ return Snapshot(
+ gameID: gameID,
+ upToLamport: se.upToLamport,
+ grid: grid,
+ createdAt: se.createdAt ?? Date()
+ )
+ }
+
+ let moveReq = NSFetchRequest<MoveEntity>(entityName: "MoveEntity")
+ moveReq.predicate = NSPredicate(format: "game == %@", entity)
+ let moves: [Move] = ((try? context.fetch(moveReq)) ?? []).map { me in
+ Move(
+ gameID: gameID,
+ lamport: me.lamport,
+ row: Int(me.row),
+ col: Int(me.col),
+ letter: me.letter ?? "",
+ markKind: me.markKind,
+ checkedWrong: me.checkedWrong,
+ authorID: me.authorID,
+ createdAt: me.createdAt ?? Date()
+ )
+ }
+
+ let grid = MoveLog.replay(snapshot: MoveLog.latestSnapshot(from: snapshots), moves: moves)
+ updateCellCache(for: entity, from: grid)
+ }
+ }
+
// MARK: - Load a specific game
/// Loads a game by its entity ID. Sets it as the current game.
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -32,6 +32,9 @@ final class AppServices {
persistence: persistence,
sink: { moves in
await syncEngine.enqueueMoves(moves)
+ },
+ afterFlush: { gameIDs in
+ await store.replayCellCaches(for: gameIDs)
}
)
self.moveBuffer = moveBuffer
diff --git a/Crossmate/Sync/MoveBuffer.swift b/Crossmate/Sync/MoveBuffer.swift
@@ -30,6 +30,7 @@ actor MoveBuffer {
private let debounceInterval: Duration
private let persistence: PersistenceController
private let sink: @Sendable ([Move]) async -> Void
+ private let afterFlush: (@Sendable (Set<UUID>) async -> Void)?
private var buffer: [Key: Pending] = [:]
/// Insertion order so that lamports within a single flush are assigned
@@ -45,11 +46,13 @@ actor MoveBuffer {
init(
debounceInterval: Duration = .milliseconds(1500),
persistence: PersistenceController,
- sink: @escaping @Sendable ([Move]) async -> Void
+ sink: @escaping @Sendable ([Move]) async -> Void,
+ afterFlush: (@Sendable (Set<UUID>) async -> Void)? = nil
) {
self.debounceInterval = debounceInterval
self.persistence = persistence
self.sink = sink
+ self.afterFlush = afterFlush
}
/// Registers a cell edit. If the edit targets a different cell than the
@@ -119,6 +122,9 @@ actor MoveBuffer {
let moves = persistAndAssignLamports(snapshot: snapshot, order: snapshotOrder)
guard !moves.isEmpty else { return }
await sink(moves)
+ if let afterFlush {
+ await afterFlush(Set(moves.map { $0.gameID }))
+ }
}
/// Allocates lamports from each game's `lamportHighWater`, writes