fix(memory-quiz): prevent duplicate card processing from optimistic updates

Fix race condition where the host would skip cards due to the effect
running twice on the same card index - once for the optimistic update
and potentially again for the server update.

The issue: When the host calls nextCard(), it immediately applies an
optimistic update that changes currentCardIndex. This triggers the effect
to re-run before the timer has even finished. Since isProcessingRef was
set to false right before calling nextCard(), the effect would start
processing the next card immediately, causing cards to be skipped.

Solution: Track the last processed card index in a ref (lastProcessedIndexRef)
and skip the effect if we're trying to process the same index again. This
ensures each card is only shown once, regardless of how many times the
effect runs due to state changes.

- Add lastProcessedIndexRef to track the last card we processed
- Check at start of effect if currentCardIndex === lastProcessedIndexRef
- Skip duplicate processing to prevent race conditions
- Remove unnecessary dependency on state.quizCards[currentCardIndex]
- Add detailed logging to help debug timing issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-15 09:35:41 -05:00
parent 82ca31029c
commit 51676fc15f

View File

@@ -17,6 +17,7 @@ export function DisplayPhase() {
const [isTransitioning, setIsTransitioning] = useState(false)
const isDisplayPhaseActive = state.currentCardIndex < state.quizCards.length
const isProcessingRef = useRef(false)
const lastProcessedIndexRef = useRef(-1)
const appConfig = useAbacusConfig()
// In multiplayer room mode, only the room creator controls card timing
@@ -38,9 +39,21 @@ export function DisplayPhase() {
const progressPercentage = (state.currentCardIndex / state.quizCards.length) * 100
useEffect(() => {
// Prevent processing the same card index multiple times
// This prevents race conditions from optimistic updates
if (state.currentCardIndex === lastProcessedIndexRef.current) {
console.log(
`DisplayPhase: Skipping duplicate processing of index ${state.currentCardIndex} (lastProcessed: ${lastProcessedIndexRef.current})`
)
return
}
if (state.currentCardIndex >= state.quizCards.length) {
// Only the room creator (or local mode) triggers phase transitions
if (shouldControlTiming) {
console.log(
`DisplayPhase: All cards shown (${state.quizCards.length}), transitioning to input phase`
)
showInputPhase?.()
}
return
@@ -48,9 +61,15 @@ export function DisplayPhase() {
// Prevent multiple concurrent executions
if (isProcessingRef.current) {
console.log(
`DisplayPhase: Already processing, skipping (index: ${state.currentCardIndex}, lastProcessed: ${lastProcessedIndexRef.current})`
)
return
}
// Mark this index as being processed
lastProcessedIndexRef.current = state.currentCardIndex
const showNextCard = async () => {
isProcessingRef.current = true
const card = state.quizCards[state.currentCardIndex]
@@ -103,7 +122,6 @@ export function DisplayPhase() {
state.quizCards.length,
nextCard,
showInputPhase,
state.quizCards[state.currentCardIndex],
shouldControlTiming,
isRoomCreator,
])