feat(web): shared Postgres read-through cache for MusicBrainz (ApiCache)
Reopening an artist (discography), an album (tracklist), or repeating a lookup re-hit MusicBrainz every time with no cache and no throttle. Adds a generic Postgres-backed read-through cache keyed on the LOGICAL operation (not the request URL) so the worker and web app share entries. - New ApiCache(key, json, fetchedAt) model + migration (generic name so the Last.fm browse cache can ride the same table). - lib/apicache.ts cached(): read-through, TTL from fetchedAt (tunable, no migration), serve-stale-on-outage, never caches null. - Wrap the high-level MB fns with logical keys + TTLs: discography & tracklists & artist-name 30d, searchReleaseGroup 7d, searchArtists 1d. Search keys normalized (trim+lowercase) to match the worker byte-for-byte. - album-modal: a session-scoped Map<rgMbid,Track[]> so reopening is instant with zero network (complements the DB cache). - Test setup clears ApiCache between tests. web 147 tests, tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,10 @@ import { CoverArt } from "./cover-art";
|
||||
|
||||
type Track = { position: number; title: string; lengthMs: number | null };
|
||||
|
||||
// Session-scoped, per-release-group tracklist cache: reopening the same album modal is
|
||||
// instant with zero network. Complements the server-side ApiCache (which spans sessions).
|
||||
const trackCache = new Map<string, Track[]>();
|
||||
|
||||
function fmt(ms: number | null): string {
|
||||
if (ms == null) return "";
|
||||
const s = Math.round(ms / 1000);
|
||||
@@ -39,15 +43,25 @@ export function AlbumModal({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!album.rgMbid) {
|
||||
const rgMbid = album.rgMbid;
|
||||
if (!rgMbid) {
|
||||
setTracks("error");
|
||||
return;
|
||||
}
|
||||
const hit = trackCache.get(rgMbid);
|
||||
if (hit) {
|
||||
setTracks(hit);
|
||||
return;
|
||||
}
|
||||
setTracks("loading");
|
||||
let cancelled = false;
|
||||
fetch(`/api/mb/release-groups/${album.rgMbid}/tracks`)
|
||||
fetch(`/api/mb/release-groups/${rgMbid}/tracks`)
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
||||
.then((d) => !cancelled && setTracks(d.tracks))
|
||||
.then((d) => {
|
||||
if (cancelled) return;
|
||||
trackCache.set(rgMbid, d.tracks);
|
||||
setTracks(d.tracks);
|
||||
})
|
||||
.catch(() => !cancelled && setTracks("error"));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
Reference in New Issue
Block a user