fix: instant search via frontend library cache — no Go calls at all

The HTTP endpoint approach still crashed due to Wails asset server
issues. Replaced with a pure frontend solution: searchLibraryCache()
does a substring match against the libraryStore's cached artists and
albums arrays. This is pure JS — zero Go calls, zero RPC, zero
network — guaranteed instant.

Results appear immediately as the user types. The full MB+LB search
pipeline still runs via Wails RPC and replaces the library matches
with richer results when done.

Added cachedArtists/cachedAlbums getters to LibraryStore for
synchronous read-only access to the already-loaded data.
Removed the /api/search-local HTTP handler from the backend.
This commit is contained in:
2026-03-29 18:02:59 -04:00
parent 33d42febf1
commit 1999fdb0f4
4 changed files with 73 additions and 59 deletions
-3
View File
@@ -118,9 +118,6 @@ 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,
-33
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"log/slog"
"math"
"net/http"
"sort"
"strings"
"sync"
@@ -155,38 +154,6 @@ 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) {
defer func() {
if rv := recover(); rv != nil {
e.logger.Error("search-local handler panic", "recover", rv)
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
query := r.URL.Query().Get("q")
if query == "" {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("null"))
return
}
result := e.SearchLocal(query)
w.Header().Set("Content-Type", "application/json")
if result == nil {
_, _ = w.Write([]byte("null"))
return
}
_ = json.NewEncoder(w).Encode(result)
})
}
// ---------------------------------------------------------------------------
// MusicBrainz lookup
// ---------------------------------------------------------------------------
@@ -9,6 +9,7 @@ import type {
MBReleaseGroup,
MBRecording,
} from '@go/explore/Service';
import { libraryStore } from '../../store/library-store';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
/* ── Constants ── */
@@ -520,35 +521,74 @@ export class ExploreView extends LitElement {
const startTime = performance.now();
console.log(`[explore] search started: "${query}"`);
// 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.
// Phase 1: instant library search — pure frontend, no Go calls.
const localResults = this.searchLibraryCache(query);
if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) {
this.results = localResults;
this.loadThumbnails();
this.loadArtistImages();
const elapsed = (performance.now() - startTime).toFixed(0);
console.log(
`[explore] library results: "${query}" in ${elapsed}ms — ` +
`artists=${localResults.artists?.length ?? 0}, ` +
`albums=${localResults.releaseGroups?.length ?? 0}`,
);
}
// Phase 2: full pipeline (MB + LB + reranking) via Wails RPC.
void this.executeFullSearch(version, query, startTime);
}
/**
* Search the frontend library cache for matching artists and albums.
* Pure JS — no Go calls, guaranteed instant.
*/
private searchLibraryCache(query: string): MBSearchResult | null {
const q = query.toLowerCase();
const artists: MBArtist[] = [];
const cachedArtists = libraryStore.cachedArtists;
if (cachedArtists) {
for (const a of cachedArtists) {
if (a.Name.toLowerCase().includes(q)) {
artists.push({
mbid: '',
name: a.Name,
sortName: '',
type: 'Group',
country: '',
disambiguation: '',
score: 100,
} as MBArtist);
if (artists.length >= 5) break;
}
}
}
const releaseGroups: MBReleaseGroup[] = [];
const cachedAlbums = libraryStore.cachedAlbums;
if (cachedAlbums) {
for (const a of cachedAlbums) {
if (a.Name.toLowerCase().includes(q) || a.ArtistName.toLowerCase().includes(q)) {
releaseGroups.push({
mbid: '',
title: a.Name,
primaryType: 'Album',
artistCredit: a.ArtistName,
firstReleaseDate: a.Year ? String(a.Year) : '',
} as MBReleaseGroup);
if (releaseGroups.length >= 5) break;
}
}
}
if (artists.length === 0 && releaseGroups.length === 0) {
return null;
}
return { artists, releaseGroups, recordings: [] } as MBSearchResult;
}
private async executeFullSearch(version: number, query: string, startTime: number) {
try {
const result = await Search(query);
+10
View File
@@ -120,6 +120,16 @@ class LibraryStore {
// Returns cached data or fetches from backend on first access.
// ===================================================================
/** Synchronous access to cached artists (null if not yet loaded). */
get cachedArtists(): library.Artist[] | null {
return this.artists;
}
/** Synchronous access to cached albums (null if not yet loaded). */
get cachedAlbums(): library.Album[] | null {
return this.albums;
}
async getTracks(): Promise<library.Track[]> {
if (this.tracks !== null) {
return this.tracks;