diff --git a/internal/anime/handler/handler.go b/internal/anime/handler/handler.go index 4da2405..167c828 100644 --- a/internal/anime/handler/handler.go +++ b/internal/anime/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + "context" "fmt" "mal/internal/db" "mal/internal/domain" @@ -8,6 +9,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/gin-gonic/gin" ) @@ -458,7 +460,10 @@ func (h *AnimeHandler) commandPaletteNavigationItems(query string) []commandPale } func (h *AnimeHandler) commandPaletteAnimeResults(c *gin.Context, query string) []commandPaletteItem { - res, err := h.svc.SearchAdvanced(c.Request.Context(), query, "", "", "", "", nil, true, 1, 5) + searchCtx, cancel := context.WithTimeout(c.Request.Context(), 1500*time.Millisecond) + defer cancel() + + res, err := h.svc.SearchAdvanced(searchCtx, query, "", "", "", "", nil, true, 1, 5) if err != nil { return nil } diff --git a/static/search.ts b/static/search.ts index b3c826b..c42da88 100644 --- a/static/search.ts +++ b/static/search.ts @@ -27,6 +27,7 @@ let selectedIndex = 0; let fetchTimeout: number | undefined; let lastQuery = ''; let continueExpanded = false; +let activeRequestController: AbortController | undefined; const responseCache = new Map(); const iconPaths: Record = { @@ -71,6 +72,15 @@ const clearResults = (): void => { paletteResults?.replaceChildren(); }; +const buildSearchActionItem = (query: string): CommandPaletteItem => ({ + id: 'search:' + query.toLowerCase(), + type: 'search', + label: `Search anime for "${query}"`, + subtitle: 'Browse', + href: '/browse?q=' + encodeURIComponent(query), + icon: 'search', +}); + const buildIcon = (item: CommandPaletteItem): HTMLElement => { if (isSafeImageUrl(item.image)) { const img = document.createElement('img'); @@ -315,16 +325,34 @@ const renderItems = (items: CommandPaletteItem[]): void => { selectItem(0); }; +const renderPendingQuery = (query: string): void => { + if (!query) { + return; + } + + renderItems([buildSearchActionItem(query)]); +}; + const fetchPaletteItems = (query: string): void => { lastQuery = query; + if (activeRequestController) { + activeRequestController.abort(); + activeRequestController = undefined; + } + const cached = responseCache.get(query); if (cached) { renderItems(cached); return; } - fetch('/api/command-palette?q=' + encodeURIComponent(query)) + renderPendingQuery(query); + + const controller = new AbortController(); + activeRequestController = controller; + + fetch('/api/command-palette?q=' + encodeURIComponent(query), { signal: controller.signal }) .then((res: Response) => { if (!res.ok) { return []; @@ -332,13 +360,18 @@ const fetchPaletteItems = (query: string): void => { return res.json(); }) .then((items: CommandPaletteItem[]) => { - if (query !== lastQuery) { + if (controller.signal.aborted || query !== lastQuery) { return; } + activeRequestController = undefined; responseCache.set(query, items); renderItems(items); }) .catch((err: unknown) => { + if (controller.signal.aborted) { + return; + } + activeRequestController = undefined; console.error('Command palette error:', err); renderItems([]); }); @@ -350,7 +383,7 @@ const scheduleFetch = (): void => { } const query = paletteInput?.value.trim() || ''; - fetchTimeout = window.setTimeout(() => fetchPaletteItems(query), query.length >= 2 ? 180 : 0); + fetchTimeout = window.setTimeout(() => fetchPaletteItems(query), query.length >= 2 ? 300 : 120); }; const openPalette = (): void => { @@ -375,6 +408,10 @@ const closePalette = (): void => { paletteDialog.classList.add('hidden'); paletteDialog.classList.remove('flex'); paletteDialog.setAttribute('aria-hidden', 'true'); + if (activeRequestController) { + activeRequestController.abort(); + activeRequestController = undefined; + } paletteInput.value = ''; allPaletteItems = []; paletteItems = []; diff --git a/templates/components/filter_bar.gohtml b/templates/components/filter_bar.gohtml index eadcc61..e01268a 100644 --- a/templates/components/filter_bar.gohtml +++ b/templates/components/filter_bar.gohtml @@ -3,7 +3,7 @@ {{$selectedCount := len $selectedGenreIDs}}
-
+