fix: bypass Wails RPC entirely for local search via HTTP endpoint
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user