From c22066e5d526771a93714eb5e9c349d8b6d289b5 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sun, 29 Mar 2026 17:42:10 -0400 Subject: [PATCH] fix: use Wails event for instant local search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SearchLocal RPC approach couldn't render results instantly because Wails v2 serializes Go method calls — SearchLocal would queue behind other in-flight calls. Now Search() emits a 'search:local-results' Wails event at the start of Phase 0 (before the slow MB/LB pipeline begins). The frontend listens for this event in connectedCallback and renders the local hits immediately. The event bypasses the RPC queue since it's pushed from Go, not pulled by JS. Removed the SearchLocal RPC call from the frontend entirely. --- backend/explore/explore.go | 36 +++++++++++++ .../components/explore-view/explore-view.ts | 52 ++++++++++--------- 2 files changed, 64 insertions(+), 24 deletions(-) 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); }