From 6ff0f0e76a20bd7c3263573486a51c594f84ff81 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:53:46 +0200 Subject: [PATCH] feat: stale-while-revalidate cache for /listening/ page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On page load, read last known data from localStorage and render instantly (no skeleton for returning visitors). Fresh data fetches run in background and update the UI reactively when complete. - apiFetch() writes each successful response to localStorage (key: lp:{path}) - readCache() reads cached response, null on miss or parse error - listeningPage._loadCaches() + listeningWidget._loadCaches() populate state from cache on init, setting loading=false immediately if cache exists - Cached now-playing status always set to null — prevents stale "Now Playing" green indicator; fresh fetch updates the real status within seconds - All localStorage access wrapped in try/catch (private mode safe) --- js/listening.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/js/listening.js b/js/listening.js index 14e58e0..480b99e 100644 --- a/js/listening.js +++ b/js/listening.js @@ -10,7 +10,18 @@ document.addEventListener("alpine:init", () => { async function apiFetch(path) { const r = await fetch(path); if (!r.ok) throw new Error(`HTTP ${r.status}`); - return r.json(); + const data = await r.json(); + try { localStorage.setItem("lp:" + path, JSON.stringify({ t: Date.now(), d: data })); } catch {} + return data; + } + + // Read cached API response; returns null on miss or parse error + function readCache(path) { + try { + const s = localStorage.getItem("lp:" + path); + if (!s) return null; + return JSON.parse(s).d ?? null; + } catch { return null; } } function mergeListens(fw = [], lfm = []) { @@ -48,7 +59,20 @@ document.addEventListener("alpine:init", () => { lfmNowPlaying: null, lfmScrobbles: [], + _loadCaches() { + const fwNp = readCache("/funkwhale/api/now-playing"); + const fwList = readCache("/funkwhale/api/listenings?limit=2"); + const lfmNp = readCache("/lastfmapi/api/now-playing"); + const lfmSc = readCache("/lastfmapi/api/scrobbles?limit=2"); + if (fwNp) this.fwNowPlaying = { ...fwNp, status: null }; // no stale "Now Playing" + if (fwList) this.fwListenings = fwList.listenings || []; + if (lfmNp) this.lfmNowPlaying = { ...lfmNp, status: null }; + if (lfmSc) this.lfmScrobbles = lfmSc.scrobbles || []; + return !!(fwNp || fwList || lfmNp || lfmSc); + }, + async init() { + if (this._loadCaches()) this.loading = false; try { const [fwNp, fwList, lfmNp, lfmList] = await Promise.allSettled([ apiFetch("/funkwhale/api/now-playing"), @@ -141,7 +165,37 @@ document.addEventListener("alpine:init", () => { lfm: { nowPlaying: null, scrobbles: [], loved: [], stats: null }, activeSource: "all", + _loadCaches() { + const fwNp = readCache("/funkwhale/api/now-playing"); + const fwList = readCache("/funkwhale/api/listenings"); + const fwFav = readCache("/funkwhale/api/favorites"); + const fwSt = readCache("/funkwhale/api/stats"); + const lfmNp = readCache("/lastfmapi/api/now-playing"); + const lfmSc = readCache("/lastfmapi/api/scrobbles?period=alltime&limit=20"); + const lfmLv = readCache("/lastfmapi/api/loved?limit=10"); + const lfmSt = readCache("/lastfmapi/api/stats?period=alltime"); + + const hasFw = fwNp || fwList || fwFav || fwSt; + const hasLfm = lfmNp || lfmSc || lfmLv || lfmSt; + + if (hasFw) { + if (fwNp) this.fw.nowPlaying = { ...fwNp, status: null }; // no stale "Now Playing" + if (fwList) this.fw.listenings = fwList.listenings || []; + if (fwFav) this.fw.favorites = fwFav.favorites || []; + if (fwSt) this.fw.stats = fwSt; + this.fwLoading = false; + } + if (hasLfm) { + if (lfmNp) this.lfm.nowPlaying = { ...lfmNp, status: null }; + if (lfmSc) this.lfm.scrobbles = lfmSc.scrobbles || []; + if (lfmLv) this.lfm.loved = lfmLv.loved || []; + if (lfmSt) this.lfm.stats = lfmSt; + this.lfmLoading = false; + } + }, + async init() { + this._loadCaches(); await Promise.all([this._fetchFunkwhale(), this._fetchLastfm()]); },