From c3e16a9fd2af6fd124e53eedb7d56fde6f1f7d2e Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sun, 29 Mar 2026 17:48:01 -0400 Subject: [PATCH] fix: bypass Wails RPC entirely for local search via HTTP endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both SearchLocal RPC and Wails events were blocked by Wails v2's Go call serialization. When the indexer or other Go calls were in-flight, even a 1ms Go function couldn't return to JS. New approach: registered /api/search-local as an HTTP handler on the Wails asset server. The frontend fetches it directly via fetch() — this runs on Go's HTTP server goroutine pool, completely independent of Wails RPC serialization. The fetch completes in milliseconds regardless of what other Go calls are queued. The full Search() pipeline still runs via Wails RPC and replaces the local results when done. --- backend/app.go | 3 ++ backend/explore/explore.go | 24 +++++++++ .../components/explore-view/explore-view.ts | 52 +++++++++---------- 3 files changed, 52 insertions(+), 27 deletions(-) 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); }