fix(plugins): fix async race in refreshPlugins; use cache TTL to gate re-swap metadata fetch

refreshPlugins() called searchPluginStore(true) and showNotification() immediately
after refreshInstalledPlugins() without awaiting the returned Promise, so
window.installedPlugins could still be stale when the store rendered its
Installed/Reinstall badges. Chain .then() so both run only after the fetch
completes.

In initializePlugins(), the re-swap path always passed fetchCommitInfo=false to
searchPluginStore, skipping GitHub metadata even when the 5-minute cache TTL had
expired. Add storeCacheExpired() helper and compute isReswapWarm = _reswap &&
!storeCacheExpired() so fresh metadata is fetched whenever the cache is cold,
regardless of whether the render is a first load or a tab re-swap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-04-30 16:11:05 -04:00
parent c54718af2d
commit 55a6a53fca

View File

@@ -898,6 +898,10 @@ window.currentPluginConfig = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
let storeFilteredList = []; let storeFilteredList = [];
function storeCacheExpired() {
return !cacheTimestamp || (Date.now() - cacheTimestamp >= CACHE_DURATION);
}
// ── Plugin Store Filter State ─────────────────────────────────────────── // ── Plugin Store Filter State ───────────────────────────────────────────
const storeFilterState = { const storeFilterState = {
sort: safeLocalStorage.getItem('storeSort') || 'a-z', sort: safeLocalStorage.getItem('storeSort') || 'a-z',
@@ -1214,12 +1218,13 @@ function initializePlugins() {
} }
// Load both installed plugins and plugin store. // Load both installed plugins and plugin store.
// On HTMX re-swaps use cached store data (fetchCommitInfo=false) to avoid // On HTMX re-swaps with a still-warm cache, skip GitHub metadata to avoid
// re-hitting GitHub on every tab switch; only fetch fresh on first load. // re-hitting the API on every tab switch. If the cache TTL has expired even
const isReswap = !!window.pluginManager._reswap; // during a re-swap, fetch fresh data including GitHub commit/version info.
const isReswapWarm = !!window.pluginManager._reswap && !storeCacheExpired();
window.pluginManager._reswap = false; window.pluginManager._reswap = false;
loadInstalledPlugins(); loadInstalledPlugins();
searchPluginStore(!isReswap); searchPluginStore(!isReswapWarm);
// Setup search functionality (with guard against duplicate listeners) // Setup search functionality (with guard against duplicate listeners)
const searchInput = document.getElementById('plugin-search'); const searchInput = document.getElementById('plugin-search');
@@ -5133,10 +5138,13 @@ function refreshPlugins() {
pluginStoreCache = null; pluginStoreCache = null;
cacheTimestamp = null; cacheTimestamp = null;
refreshInstalledPlugins(); // invalidates cache before fetching // refreshInstalledPlugins() is async (returns a Promise via loadInstalledPlugins).
// Fetch latest metadata from GitHub when refreshing // Only search the store and notify after window.installedPlugins is updated so
searchPluginStore(true); // that Installed/Reinstall badges reflect the freshly fetched state.
showNotification('Plugins refreshed with latest metadata from GitHub', 'success'); refreshInstalledPlugins().then(() => {
searchPluginStore(true);
showNotification('Plugins refreshed with latest metadata from GitHub', 'success');
});
} }
function restartDisplay() { function restartDisplay() {