fix: use Wails event for instant local search results

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.
This commit is contained in:
2026-03-29 17:42:10 -04:00
parent 0ce53fb87a
commit c22066e5d5
2 changed files with 64 additions and 24 deletions
+36
View File
@@ -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()
@@ -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);
}