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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user