diff --git a/backend/app.go b/backend/app.go index b72d72f..68fcdff 100644 --- a/backend/app.go +++ b/backend/app.go @@ -118,6 +118,9 @@ func NewYellowJacketApp( yjApp.assetHandler.RegisterHandler("/artist-images/", artistImgHandler) } + // Register local search endpoint — bypasses Wails RPC serialization. + yjApp.assetHandler.RegisterHandler("/api/search-local", yjApp.explore.SearchLocalHandler()) + // create playlist service yjApp.playlist = playlist.NewService( yjApp.logger, yjApp.database, yjApp.appConfig, diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 348de18..c9f0f73 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -5,6 +5,7 @@ import ( "encoding/json" "log/slog" "math" + "net/http" "sort" "strings" "sync" @@ -156,6 +157,29 @@ func (e *Service) SearchLocal(query string) *MBSearchResult { return &result } +// SearchLocalHandler returns an http.Handler that serves local +// index search results as JSON. This bypasses the Wails RPC +// serialization queue, ensuring sub-millisecond response times. +func (e *Service) SearchLocalHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + result := e.SearchLocal(query) + if result == nil { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("null")) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(result) + }) +} + // --------------------------------------------------------------------------- // MusicBrainz lookup // --------------------------------------------------------------------------- diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index 310c30d..be6b994 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -2,7 +2,6 @@ import { LitElement, html, css, nothing } from 'lit'; import { customElement, state, query as litQuery } from 'lit/decorators.js'; import { designTokens } from '../../styles/tokens.css'; import { Search, GetThumbnails, GetArtistImageURL, CheckLibraryMBIDs } from '@go/explore/Service'; -import { EventsOn } from '@runtime/runtime'; import type { ThumbnailRequest } from '@go/explore/Service'; import type { MBSearchResult, @@ -70,28 +69,6 @@ export class ExploreView extends LitElement { @litQuery('input') private inputEl!: HTMLInputElement; - /* ── Lifecycle ── */ - - override connectedCallback() { - super.connectedCallback(); - EventsOn('search:local-results', (local: MBSearchResult) => { - // Only apply if we're actively loading (a search is in flight). - if (!this.loading) return; - if (local.artists?.length || local.releaseGroups?.length || local.recordings?.length) { - this.results = local; - this.loadThumbnails(); - this.loadArtistImages(); - this.checkLibrary(); - console.log( - `[explore] local results via event — ` + - `artists=${local.artists?.length ?? 0}, ` + - `albums=${local.releaseGroups?.length ?? 0}, ` + - `tracks=${local.recordings?.length ?? 0}`, - ); - } - }); - } - /* ── Styles ── */ static override styles = [ @@ -543,11 +520,32 @@ export class ExploreView extends LitElement { const startTime = performance.now(); console.log(`[explore] search started: "${query}"`); - // Local index results arrive via the 'search:local-results' event - // emitted by the backend at the start of Search(), before the - // slow MB/LB pipeline runs. No separate RPC call needed. + // Phase 1: fetch local index results via HTTP (bypasses Wails + // RPC serialization — guaranteed instant). + try { + const resp = await fetch(`/api/search-local?q=${encodeURIComponent(query)}`); + if (version !== this.searchVersion) return; + if (resp.ok) { + const local = await resp.json() as MBSearchResult | null; + if (local && (local.artists?.length || local.releaseGroups?.length || local.recordings?.length)) { + this.results = local; + this.loadThumbnails(); + this.loadArtistImages(); + this.checkLibrary(); + const elapsed = (performance.now() - startTime).toFixed(0); + console.log( + `[explore] local results: "${query}" in ${elapsed}ms — ` + + `artists=${local.artists?.length ?? 0}, ` + + `albums=${local.releaseGroups?.length ?? 0}, ` + + `tracks=${local.recordings?.length ?? 0}`, + ); + } + } + } catch { + // Local search failed — continue to full search. + } - // Full pipeline (MB + LB + reranking). + // Phase 2: full pipeline (MB + LB + reranking) via Wails RPC. void this.executeFullSearch(version, query, startTime); }