feat: add timeout and abort handling to command palette search
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ let selectedIndex = 0;
|
||||
let fetchTimeout: number | undefined;
|
||||
let lastQuery = '';
|
||||
let continueExpanded = false;
|
||||
let activeRequestController: AbortController | undefined;
|
||||
const responseCache = new Map<string, CommandPaletteItem[]>();
|
||||
|
||||
const iconPaths: Record<string, string> = {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{$selectedCount := len $selectedGenreIDs}}
|
||||
<div class="flex flex-wrap items-center gap-3" hx-boost="true" hx-target="#browse-content" hx-swap="outerHTML" hx-push-url="true">
|
||||
<div class="min-w-50 flex-1">
|
||||
<form action="/browse" method="GET" id="browse-search-form" hx-get="/browse" hx-trigger="submit, keyup changed delay:120ms from:#search">
|
||||
<form action="/browse" method="GET" id="browse-search-form" hx-get="/browse" hx-trigger="submit, input changed delay:350ms from:#search, search from:#search" hx-sync="this:replace">
|
||||
<input
|
||||
id="search"
|
||||
name="q"
|
||||
|
||||
Reference in New Issue
Block a user