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)
|
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
|
// create playlist service
|
||||||
yjApp.playlist = playlist.NewService(
|
yjApp.playlist = playlist.NewService(
|
||||||
yjApp.logger, yjApp.database, yjApp.appConfig,
|
yjApp.logger, yjApp.database, yjApp.appConfig,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -155,38 +154,6 @@ func (e *Service) SearchLocal(query string) *MBSearchResult {
|
|||||||
return &result
|
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
|
// MusicBrainz lookup
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
MBReleaseGroup,
|
MBReleaseGroup,
|
||||||
MBRecording,
|
MBRecording,
|
||||||
} from '@go/explore/Service';
|
} from '@go/explore/Service';
|
||||||
|
import { libraryStore } from '../../store/library-store';
|
||||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
|
|
||||||
/* ── Constants ── */
|
/* ── Constants ── */
|
||||||
@@ -520,35 +521,74 @@ export class ExploreView extends LitElement {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
console.log(`[explore] search started: "${query}"`);
|
console.log(`[explore] search started: "${query}"`);
|
||||||
|
|
||||||
// Phase 1: fetch local index results via HTTP (bypasses Wails
|
// Phase 1: instant library search — pure frontend, no Go calls.
|
||||||
// RPC serialization — guaranteed instant).
|
const localResults = this.searchLibraryCache(query);
|
||||||
try {
|
if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) {
|
||||||
const resp = await fetch(`/api/search-local?q=${encodeURIComponent(query)}`);
|
this.results = localResults;
|
||||||
if (version !== this.searchVersion) return;
|
this.loadThumbnails();
|
||||||
if (resp.ok) {
|
this.loadArtistImages();
|
||||||
const local = await resp.json() as MBSearchResult | null;
|
const elapsed = (performance.now() - startTime).toFixed(0);
|
||||||
if (local && (local.artists?.length || local.releaseGroups?.length || local.recordings?.length)) {
|
console.log(
|
||||||
this.results = local;
|
`[explore] library results: "${query}" in ${elapsed}ms — ` +
|
||||||
this.loadThumbnails();
|
`artists=${localResults.artists?.length ?? 0}, ` +
|
||||||
this.loadArtistImages();
|
`albums=${localResults.releaseGroups?.length ?? 0}`,
|
||||||
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 2: full pipeline (MB + LB + reranking) via Wails RPC.
|
// Phase 2: full pipeline (MB + LB + reranking) via Wails RPC.
|
||||||
void this.executeFullSearch(version, query, startTime);
|
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) {
|
private async executeFullSearch(version: number, query: string, startTime: number) {
|
||||||
try {
|
try {
|
||||||
const result = await Search(query);
|
const result = await Search(query);
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ class LibraryStore {
|
|||||||
// Returns cached data or fetches from backend on first access.
|
// 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[]> {
|
async getTracks(): Promise<library.Track[]> {
|
||||||
if (this.tracks !== null) {
|
if (this.tracks !== null) {
|
||||||
return this.tracks;
|
return this.tracks;
|
||||||
|
|||||||
Reference in New Issue
Block a user