Skip to content

fix(theme-store): let only the newest theme search write its results - #750

Merged
nedtwigg merged 2 commits into
mainfrom
fix/theme-store-stale-search-results
Sep 22, 2026
Merged

nedtwigg merged 2 commits into
mainfrom
fix/theme-store-stale-search-results

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

ThemeStoreDialog applies a theme search's result whether or not anyone is still waiting for it. Three searches can be stale by the time they resolve, and all three have already left searchThemes, so the debounce cancel added in #295 — which only stops searches that have not started — reaches none of them:

  • A request in flight when the store closes. The component renders null while closed but never unmounts, so the late setResults sticks. Reopening renders the previous query's rows underneath "Search for a VS Code theme to install", because the empty-state line is gated on query (which the reset cleared) while the rows are gated on results (which the late response refilled).
  • A superseded query answering last. Type "dra", wait out the 300ms debounce, then type "nord": if the first request is slower, its response overwrites the second's and the list stops matching the box.
  • A request superseded by emptying the box. Clearing the input is itself a search, and the request it supersedes no longer owns the spinner it turned on — so loading has to be cleared by the path that invalidated it, or the dialog sits on "Searching..." above an empty box.

All three are closed by one searchEpoch ref, bumped on every search and every close. doSearch captures its epoch and gates all three state writes — setResults, setError, setLoading — on still holding the newest one; the empty-query exit clears loading itself, since it is the newest search. That is a smaller mechanism than an AbortController per request and covers the close case, which aborting the fetch alone would not: by then the response can already be in the microtask queue.

Verification. Three tests added to the existing ThemeStoreDialog.test.tsx, resolving mocked searchThemes promises by hand so the ordering is explicit. Each fails without the fix with the symptom described — Received: "…Search for a VS Code theme to installVSDracula Officialtest - 1 downloadsInstall" for the close case, the older query's row for the ordering case, Received: "…Searching..." for the emptied box — and all pass with it. lib's tsc -b is clean and the rest of its suite is unchanged.

Local suite state

The full lib vitest run is 3678 passed / 4 failed. The four are src/host/remote/native-direct-peer.test.ts, which negotiates a real WebRTC channel over node-datachannel; they time out waiting for ICE in this sandbox and fail identically with the change stashed, so they are an environment limit here rather than a result of this diff. CI on both pushed commits is green.

Surfaced by the nightly code-quality survey. The third race was introduced by this PR's own first commit and caught by the draft review on it; e647f86c closes it.

Two stale writers reach `results`/`error`/`loading` in ThemeStoreDialog, both
of them searches that have already started, which the existing debounce cancel
cannot stop:

- A request still in flight when the store closes resolves afterwards and
  repopulates the list the close's reset effect just cleared. The component
  only renders null while closed, so the state survives: reopening shows the
  previous query's rows under the empty-state line.
- An earlier query whose response lands after a later one's overwrites the
  newer results, so the list no longer matches the box.

One epoch ref, bumped on every search and on every close, gates all three
state writes on still being the newest search.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 22, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: e647f86
Status: ✅  Deploy successful!
Preview URL: https://e4a8e929.mouseterm.pages.dev
Branch Preview URL: https://fix-theme-store-stale-search.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark the PR ready when you want the full review.

The epoch gate is the right mechanism and both described races are closed by it, but moving the bump above the !value.trim() early return leaves one exit where loading is never cleared: emptying the search box while a request is in flight invalidates that request's finally, and nothing else in that path turns the spinner off. The dialog then sits on "Searching..." with an empty box — the Search for a VS Code theme to install empty state is gated on !loading, so it stays suppressed until the next completed search or a close/reopen. On main the same sequence instead repopulated the cleared box with the old query's rows, so this path swaps one wrong state for another rather than being fixed; it's worth a third test alongside the two you added. Inline suggestion below.

Comment thread lib/src/components/theme-picker/ThemeStoreDialog.tsx
The epoch is taken before the empty-query early return, so `doSearch('')`
supersedes an in-flight request and gates off its `setLoading(false)` —
and that branch returned without touching `loading`, leaving the dialog
on "Searching..." with an empty box until the next completed search.
The empty-query path is the newest search, so it clears the spinner itself.
@nedtwigg
nedtwigg marked this pull request as ready for review September 22, 2026 20:27
@nedtwigg
nedtwigg merged commit dd39875 into main Sep 22, 2026
13 checks passed
@nedtwigg
nedtwigg deleted the fix/theme-store-stale-search-results branch September 22, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants