From 050b9533e01af66abd4c61defbbdbd6fff5766aa Mon Sep 17 00:00:00 2001 From: Allard Date: Mon, 4 May 2026 23:03:22 +0800 Subject: [PATCH] perf(plugins): seed plugin routes from sessionStorage cache for instant render - Plugin manifests are now cached in sessionStorage on fetch. - On refresh, plugin routes are registered synchronously from cache, preventing unwanted redirects to /sessions. - Removes the !pluginsLoading guard from the catch-all route in App.tsx, as plugin routes are now always available on first render. - Background fetch always updates the cache and routes, so new/removed plugins are reflected after reload. - Resolves the race condition where plugin pages would redirect to /sessions on hard refresh. --- web/src/plugins/usePlugins.ts | 40 ++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/web/src/plugins/usePlugins.ts b/web/src/plugins/usePlugins.ts index 4896295891..effc5cce75 100644 --- a/web/src/plugins/usePlugins.ts +++ b/web/src/plugins/usePlugins.ts @@ -17,17 +17,51 @@ import { setPluginLoadError, } from "./registry"; +const MANIFEST_CACHE_KEY = "hermes:plugin-manifests"; + +function getCachedManifests(): PluginManifest[] | null { + try { + const raw = sessionStorage.getItem(MANIFEST_CACHE_KEY); + return raw ? (JSON.parse(raw) as PluginManifest[]) : null; + } catch { + return null; + } +} + +function cacheManifests(manifests: PluginManifest[]): void { + try { + sessionStorage.setItem(MANIFEST_CACHE_KEY, JSON.stringify(manifests)); + } catch { + // sessionStorage unavailable (private browsing, storage full, etc.) + } +} + export function usePlugins() { - const [manifests, setManifests] = useState([]); + // Lazy initialisers run once at mount — safe to read sessionStorage here. + // This avoids the "cannot access ref during render" lint error that would + // occur if we stored the cached value in a useRef and read .current in the + // useState initial value expression. + const [manifests, setManifests] = useState( + () => getCachedManifests() ?? [], + ); const [plugins, setPlugins] = useState([]); - const [loading, setLoading] = useState(true); + // Start loading=false when the cache has manifests so plugin routes are + // registered synchronously on the first render after a refresh. + // The catch-all in App.tsx is only a safety net for the very first visit + // (no cache yet). On subsequent visits this flag starts false immediately. + const [loading, setLoading] = useState( + () => getCachedManifests() === null, + ); const loadedScripts = useRef>(new Set()); - // Fetch manifests on mount. + // Always re-fetch in the background to keep the cache fresh. + // This handles: new plugins added, plugins removed, manifest changes. + // setManifests(list) will update routes if the server list differs from cache. useEffect(() => { api .getPlugins() .then((list) => { + cacheManifests(list); setManifests(list); if (list.length === 0) setLoading(false); })