commit 42feaecb07d3ea34ca4e117e8fac91f7b91bff1a
parent b6a48d0044e5f5f4171fa02d30176029a6ccbbde
Author: Michael Camilleri <[email protected]>
Date: Thu, 14 May 2026 11:28:49 +0900
Skip grid refresh when Moves save self-echoes
Receiving the device's own Moves record back through the inbound apply path
should not trigger a grid refresh. applyMovesRecord already protects the local
row from being clobbered with a stale server copy, but it still flagged the
game as moves-updated, which fires refreshCurrentGame and restores game.squares
from MovesEntity. When the user is mid-word, the most recent keystroke can sit
in MovesUpdater's 500ms debounce buffer before it reaches MovesEntity; a
refresh in that window briefly wipes the letter from the visible grid until a
later refresh restores it.
applyMovesRecord now returns whether the cells were adopted, and the inbound
callers only mark the game as moves-updated when the body actually ran.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/Crossmate/Sync/RecordSerializer.swift b/Crossmate/Sync/RecordSerializer.swift
@@ -375,13 +375,16 @@ enum RecordSerializer {
/// Upserts the `MovesEntity` for `value`. The cells blob is taken straight
/// off the record so any forward-compat fields the encoder added are
/// preserved verbatim. Bumps the parent `GameEntity.updatedAt` if the
- /// record is fresher.
+ /// record is fresher. Returns `true` when cells/updatedAt were adopted,
+ /// `false` when the local-device-row guard short-circuited the body so
+ /// callers can skip the downstream grid refresh.
+ @discardableResult
static func applyMovesRecord(
_ record: CKRecord,
value: MovesValue,
to ctx: NSManagedObjectContext,
localAuthorID: String? = nil
- ) {
+ ) -> Bool {
let ckName = record.recordID.recordName
let req = NSFetchRequest<MovesEntity>(entityName: "MovesEntity")
req.predicate = NSPredicate(format: "ckRecordName == %@", ckName)
@@ -414,7 +417,7 @@ enum RecordSerializer {
entity.deviceID = value.deviceID
let isLocalDeviceRow = value.authorID == localAuthorID
&& value.deviceID == localDeviceID
- guard !foundExisting || !isLocalDeviceRow else { return }
+ guard !foundExisting || !isLocalDeviceRow else { return false }
entity.updatedAt = value.updatedAt
entity.cells = (record["cells"] as? Data) ?? Data()
@@ -422,6 +425,7 @@ enum RecordSerializer {
game.updatedAt.map({ $0 < value.updatedAt }) ?? true {
game.updatedAt = value.updatedAt
}
+ return true
}
// MARK: - System fields encode/decode
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -1035,13 +1035,13 @@ actor SyncEngine {
if let id = entity.id { affected.insert(id) }
case "Moves":
if let value = RecordSerializer.parseMovesRecord(record) {
- RecordSerializer.applyMovesRecord(
+ let cellsChanged = RecordSerializer.applyMovesRecord(
record,
value: value,
to: ctx,
localAuthorID: localAuthorID
)
- movesUpdated.insert(value.gameID)
+ if cellsChanged { movesUpdated.insert(value.gameID) }
affected.insert(value.gameID)
}
case "Player":
@@ -1772,13 +1772,13 @@ actor SyncEngine {
if let id = entity.id { affected.insert(id) }
case "Moves":
if let value = RecordSerializer.parseMovesRecord(record) {
- RecordSerializer.applyMovesRecord(
+ let cellsChanged = RecordSerializer.applyMovesRecord(
record,
value: value,
to: ctx,
localAuthorID: localAuthorID
)
- movesUpdated.insert(value.gameID)
+ if cellsChanged { movesUpdated.insert(value.gameID) }
affected.insert(value.gameID)
}
case "Player":