Reconcile every Discover decision from the server before the next
CI / ci (push) Successful in 2m54s
CI / ci (pull_request) Successful in 2m38s

Replace the hand-maintained optimistic bookkeeping in useDecide with one
rule: a decision takes its card off the deck (and, on Try it, adds its
dinner to the Quest log) when the exit finishes; however it settles, the
deck and the Quest log are read again and the decision stays pending
until those reads land, so the input lock keeps the next decision out.
No decision cancels a read.

A failed decision whose Side quest the reconciled deck still has goes
back on top with a Retry; otherwise the banner says it was decided
already, with no Retry. ErrorBanner's Retry is now optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 03:21:54 -04:00
co-authored by Claude Opus 5.5
parent 3319434bd1
commit f26cfd5129
5 changed files with 147 additions and 111 deletions
+23 -10
View File
@@ -90,10 +90,12 @@ function QuestLogPill({ dinners, ref }: { dinners: { id: string; name: string }[
// at a time from the top card. A decision from the buttons, the ← / → keys
// or a swipe of the top card throws it out (motion.ts) and sends the
// decision once it is gone, taking the card off the deck at once (see
// useDecide); Retry sends a refused one again. Input is
// locked from the press until the decision settles: the lock is a ref, set
// before anything starts, so a second press before the next render does
// nothing, and busy mirrors it to disable the buttons.
// useDecide); Retry sends a failed one again while the deck read since still
// has its Side quest pending. Input is locked from the press until the
// decision settles, which is once the deck and the Quest log have been read
// again after it, so no decision starts under those reads. The lock is a
// ref, set before anything starts, so a second press before the next render
// does nothing, and busy mirrors it to disable the buttons.
export function DiscoverPage() {
const queryClient = useQueryClient()
const deck = useDeck()
@@ -150,8 +152,9 @@ export function DiscoverPage() {
setBusy(true)
return true
}, [])
// send records a decision. A refused one puts the card back; a saved one
// that started a replenish batch follows its job.
// send records a decision. A failed one stops the card's exit, so a card
// useDecide put back on the deck is at rest; a saved one that started a
// replenish batch follows its job.
const send = useCallback(
(variables: { quest: SideQuest; choice: Choice }) => {
leaving.current = variables.quest.id
@@ -314,6 +317,11 @@ export function DiscoverPage() {
void queryClient.invalidateQueries({ queryKey: DECK_KEY })
}, [queryClient, job.status, stored])
// A failed decision can be retried only while the deck, read again after
// it, still has its Side quest pending: anything else decided it already.
const failed = decision.isError ? decision.variables.quest.id : null
const retryable = failed !== null && !deck.isError && deck.data !== undefined && deck.data.some((q) => q.id === failed)
let body
if (deck.isError) {
body = <ErrorBanner onRetry={() => void deck.refetch()}>Couldn't load your side quests. {deck.error.message}</ErrorBanner>
@@ -380,10 +388,15 @@ export function DiscoverPage() {
<ErrorBanner onRetry={() => void questLog.refetch()}>Couldn't load the Quest log. {questLog.error.message}</ErrorBanner>
)}
{body}
{/* A refused decision is shown whatever the deck has come to since. */}
{decision.isError && (
<ErrorBanner onRetry={() => submit(decision.variables!)}>Couldn't save that choice. {decision.error.message}</ErrorBanner>
)}
{/* A failed decision is shown whatever the deck has come to since. */}
{decision.isError &&
(retryable ? (
<ErrorBanner onRetry={() => submit(decision.variables)}>Couldn't save that choice. {decision.error.message}</ErrorBanner>
) : deck.isError ? (
<ErrorBanner>Couldn't save that choice. {decision.error.message}</ErrorBanner>
) : (
<ErrorBanner>That side quest has been decided already. {decision.error.message}</ErrorBanner>
))}
</div>
)
}