diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 075d21a..348de18 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -10,6 +10,8 @@ import ( "sync" "time" + "github.com/wailsapp/wails/v2/pkg/runtime" + "yellowjacket/backend/database" ) @@ -337,6 +339,40 @@ func (e *Service) Search(query string) (*MBSearchResult, error) { "elapsed", p0Dur.Round(time.Millisecond), ) + // Emit local results immediately via event so the frontend can + // render them while the full pipeline runs. This avoids the + // Wails RPC serialization bottleneck that blocks SearchLocal. + if len(indexHits) > 0 { + var localResult MBSearchResult + mergeIndexHits(&localResult, indexHits) + + // Remove SPAs from local results. + if len(localResult.Artists) > 0 { + filtered := localResult.Artists[:0] + for _, a := range localResult.Artists { + if !mbSpecialPurposeArtists[a.MBID] { + filtered = append(filtered, a) + } + } + + localResult.Artists = filtered + } + + if len(localResult.Artists) > maxResults { + localResult.Artists = localResult.Artists[:maxResults] + } + + if len(localResult.ReleaseGroups) > maxResults { + localResult.ReleaseGroups = localResult.ReleaseGroups[:maxResults] + } + + if len(localResult.Recordings) > maxResults { + localResult.Recordings = localResult.Recordings[:maxResults] + } + + runtime.EventsEmit(e.ctx, "search:local-results", localResult) + } + // Phase 1: concurrent MB search (3 goroutines) with a deadline // so a slow MusicBrainz server doesn't hold up the whole search. p1Start := time.Now() diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index 314b5dd..310c30d 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -1,7 +1,8 @@ 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, SearchLocal, GetThumbnails, GetArtistImageURL, CheckLibraryMBIDs } from '@go/explore/Service'; +import { Search, GetThumbnails, GetArtistImageURL, CheckLibraryMBIDs } from '@go/explore/Service'; +import { EventsOn } from '@runtime/runtime'; import type { ThumbnailRequest } from '@go/explore/Service'; import type { MBSearchResult, @@ -69,6 +70,28 @@ 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 = [ @@ -520,30 +543,11 @@ export class ExploreView extends LitElement { const startTime = performance.now(); console.log(`[explore] search started: "${query}"`); - // Phase 1: show local index hits instantly (no network). - try { - const local = await SearchLocal(query); - if (version !== this.searchVersion) return; - if (local && (local.artists?.length || local.releaseGroups?.length || local.recordings?.length)) { - this.results = local; - this.loading = false; - 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. - } + // 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 2: full pipeline (MB + LB + reranking). - // Fire-and-forget so the local results render immediately. + // Full pipeline (MB + LB + reranking). void this.executeFullSearch(version, query, startTime); }