From aa4e97d2e732b080c9b00aec0877733addc5835f Mon Sep 17 00:00:00 2001 From: Adam Wildavsky Date: Tue, 15 Sep 2026 05:56:13 +0200 Subject: [PATCH 1/2] Unroll the two-card owner loop in parseDlmBoardPayload. Co-authored-by: Cursor --- web/dds_web.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/web/dds_web.js b/web/dds_web.js index 07d4be38..285eaeff 100644 --- a/web/dds_web.js +++ b/web/dds_web.js @@ -657,15 +657,10 @@ function parseDlmBoardPayload(letters) { return null; } const [firstCard, secondCard] = pairs[i]; - const owners = [ - firstOwner.charAt(code), - secondOwner.charAt(code), - ]; - const cards = [firstCard, secondCard]; - for (let j = 0; j < 2; j++) { - const direction = DIR_FROM_LETTER[owners[j]]; - byDirection[direction][cards[j].charAt(0)] += cards[j].charAt(1); - } + const firstDirection = DIR_FROM_LETTER[firstOwner.charAt(code)]; + byDirection[firstDirection][firstCard.charAt(0)] += firstCard.charAt(1); + const secondDirection = DIR_FROM_LETTER[secondOwner.charAt(code)]; + byDirection[secondDirection][secondCard.charAt(0)] += secondCard.charAt(1); } return dealFromDirectionMap({ From 91b2996fecf0c98e476e5f00cc2a19e3ece7630c Mon Sep 17 00:00:00 2001 From: Adam Wildavsky Date: Tue, 15 Sep 2026 06:00:45 +0200 Subject: [PATCH 2/2] Simplify scheduleDealSolve by dropping the impossible queued TOCTOU dance. Gate release and trailing restart already live in finally; single-threaded JS cannot interleave a schedule between the sync epoch check and clear. Co-authored-by: Cursor --- web/dds_web.js | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/web/dds_web.js b/web/dds_web.js index 285eaeff..13d9afd7 100644 --- a/web/dds_web.js +++ b/web/dds_web.js @@ -171,9 +171,8 @@ function scheduleDealSolve() { dealSolvePending = false; await refreshDdTable(); - if (epoch !== dealSolveEpoch) { - // Invalidation alone must not restart work; only a newer + // Invalidation alone must not restart; only a newer // scheduleDealSolve (pending) should continue. A pending // debounce will start a fresh job when it fires. if (dealSolvePending) { @@ -189,30 +188,13 @@ function scheduleDealSolve() { updateHandCardDisplays(collectHands()); } - if (epoch !== dealSolveEpoch) { - if (dealSolvePending) { - continue; - } - break; + // Stale+pending → another iteration; else exit (success or + // invalidate). Gate release lives in finally. + if (epoch !== dealSolveEpoch && dealSolvePending) { + continue; } - - // Release the gate only once the epoch is stable; if a schedule - // sneaks in between the check and the clear, take the flag back - // and loop again instead of dropping the trailing request. - dealSolveQueued = false; - - if (epoch !== dealSolveEpoch) { - dealSolveQueued = true; - if (dealSolvePending) { - continue; - } - break; - } - break; } - } catch (err) { - throw err; } finally { const restart = dealSolvePending; dealSolveQueued = false;