From c45ef2a61355420dc1fc409b2e064a1b91326be9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:03:42 +0200 Subject: [PATCH] fix(search): runSearch bricht pending Debounce ab Enter waehrend Debounce-Fenster feuerte bislang eine zweite Fetch fuer dieselbe Query. Race-Guard greift nicht, weil q identisch ist. runSearch clearTimeout am Anfang behebt's, neuer Unit-Test sichert es. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/client/search.svelte.ts | 2 ++ tests/unit/search-store.test.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/lib/client/search.svelte.ts b/src/lib/client/search.svelte.ts index 9272859..3b4ceaf 100644 --- a/src/lib/client/search.svelte.ts +++ b/src/lib/client/search.svelte.ts @@ -73,6 +73,8 @@ export class SearchStore { } async runSearch(q: string): Promise { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = null; this.localExhausted = false; this.webPageno = 0; this.webExhausted = false; diff --git a/tests/unit/search-store.test.ts b/tests/unit/search-store.test.ts index 744c457..c73c5bd 100644 --- a/tests/unit/search-store.test.ts +++ b/tests/unit/search-store.test.ts @@ -221,6 +221,22 @@ describe('SearchStore', () => { expect(fetchImpl.mock.calls[1][0]).toMatch(/&domains=chefkoch\.de/); }); + it('runSearch(q) cancels pending debounce to avoid double-fetch', async () => { + vi.useFakeTimers(); + const fetchImpl = mockFetch([ + { body: { hits: [{ id: 1, title: 'immediate', description: null, image_path: null, source_domain: null, avg_stars: null, last_cooked_at: null }] } } + ]); + const store = new SearchStore({ fetchImpl, debounceMs: 300 }); + store.query = 'meal'; + store.runDebounced(); // schedules the 300ms timer + // Before the timer fires, call runSearch immediately (e.g. form submit). + await store.runSearch('meal'); + expect(fetchImpl).toHaveBeenCalledTimes(1); + // Now advance past the original debounce — timer must not still fire. + await vi.advanceTimersByTimeAsync(400); + expect(fetchImpl).toHaveBeenCalledTimes(1); + }); + it('reSearch: immediate re-run with current query on filter change', async () => { vi.useFakeTimers(); let filter = '';