crossmate

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

commit c563c29c90d683cc79fe8b6beca73d02e7447c6b
parent b421eb5c1e8dbbde7790dee6030028a608a65c87
Author: Michael Camilleri <[email protected]>
Date:   Tue, 28 Apr 2026 11:49:10 +0900

Fix puzzle timestamp updating

When puzzles were updated, this was not always being reflected in the
game list. This commit fixes that.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Sync/MoveBuffer.swift | 3+++
MCrossmate/Sync/SyncEngine.swift | 4++++
MTests/Unit/MoveBufferTests.swift | 28++++++++++++++++++++++++++++
3 files changed, 35 insertions(+), 0 deletions(-)

diff --git a/Crossmate/Sync/MoveBuffer.swift b/Crossmate/Sync/MoveBuffer.swift @@ -157,6 +157,9 @@ actor MoveBuffer { let lamport = game.lamportHighWater + 1 game.lamportHighWater = lamport + if game.updatedAt.map({ $0 < pending.enqueuedAt }) ?? true { + game.updatedAt = pending.enqueuedAt + } let entity = MoveEntity(context: context) entity.game = game diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift @@ -471,6 +471,10 @@ actor SyncEngine { if let game = entity.game, move.lamport > game.lamportHighWater { game.lamportHighWater = move.lamport } + if let game = entity.game, + game.updatedAt.map({ $0 < move.createdAt }) ?? true { + game.updatedAt = move.createdAt + } } private nonisolated func applyPlayerRecord( diff --git a/Tests/Unit/MoveBufferTests.swift b/Tests/Unit/MoveBufferTests.swift @@ -105,6 +105,24 @@ struct MoveBufferTests { #expect(highWater == 11) } + @Test("Flushed moves bump the parent game's updatedAt timestamp") + func flushedMovesUpdateGameTimestamp() async throws { + let (persistence, gameID) = try makePersistenceWithGame() + let before = try #require(fetchUpdatedAt(gameID: gameID, persistence: persistence)) + let buffer = MoveBuffer( + debounceInterval: .seconds(10), + persistence: persistence, + sink: { _ in } + ) + + try await Task.sleep(for: .milliseconds(10)) + await buffer.enqueue(gameID: gameID, row: 0, col: 0, letter: "X", markKind: 0, checkedWrong: false, authorID: nil) + await buffer.flush() + + let after = try #require(fetchUpdatedAt(gameID: gameID, persistence: persistence)) + #expect(after > before) + } + @Test("Debounce coalesces rapid same-cell enqueues into one flush") func debounceCoalescesRapidEnqueues() async throws { let (persistence, gameID) = try makePersistenceWithGame() @@ -206,6 +224,16 @@ struct MoveBufferTests { } } + private func fetchUpdatedAt(gameID: UUID, persistence: PersistenceController) -> Date? { + let context = persistence.container.newBackgroundContext() + return context.performAndWait { + let request = NSFetchRequest<GameEntity>(entityName: "GameEntity") + request.predicate = NSPredicate(format: "id == %@", gameID as CVarArg) + request.fetchLimit = 1 + return try? context.fetch(request).first?.updatedAt + } + } + /// Extracts `MoveEntity` field values inside the background context so /// no `NSManagedObject` escapes its owning context. struct MoveValues {