From 1ccf00e752d5bb3d20c4dd06e0b7f59f5e42c213 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Fri, 25 Sep 2026 07:09:56 -0400 Subject: [PATCH] Ensure sections with Force Refresh/no cache timer render stale cards when Show Stale Items is enabled --- scripts/apiHelper.js | 30 +++++++++++++++++++++++++----- scripts/cardBuilder.js | 39 ++++++++++++++++++++++++++++++--------- scripts/sectionHelper.js | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/scripts/apiHelper.js b/scripts/apiHelper.js index 9017e93..9a82052 100644 --- a/scripts/apiHelper.js +++ b/scripts/apiHelper.js @@ -596,15 +596,35 @@ }); // 3. Lazy data promise — do not start fetchData until ensureData()/dataPromise is used + // Stale-while-revalidate: return usable cache immediately; refresh in _refreshPromise + const hasUsableCachedPayload = (data) => { + if (!data) return false; + if (Array.isArray(data)) return data.length > 0; + if (Array.isArray(data.Items)) return data.Items.length > 0; + return false; + }; + result.ensureData = function() { if (!result._dataPromise) { result._dataPromise = result.isStalePromise.then(isStale => { - if (isStale) { - LOG(`[Fetch] Refreshing data for: ${url}...`); - return fetchData(); + if (!isStale) { + LOG(`[Fetch] Using cached data for: ${url}...`); + return result.data; } - LOG(`[Fetch] Using cached data for: ${url}...`); - return result.data; + + const refreshPromise = fetchData().then((fresh) => { + if (fresh != null) result.data = fresh; + return fresh; + }); + + if (hasUsableCachedPayload(result.data)) { + result._refreshPromise = refreshPromise; + LOG(`[Fetch] Returning stale cache; refreshing in background: ${url}...`); + return result.data; + } + + LOG(`[Fetch] Refreshing data for: ${url}...`); + return refreshPromise; }); } return result._dataPromise; diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 18197e9..d7141c7 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -7915,7 +7915,11 @@ dataPromise.then(result => { if (!sectionElement.isConnected) return; - sectionElement.dataset.refreshing = 'false'; + const refreshPromise = section.result?.refreshPromise; + const hasPendingRefresh = !!refreshPromise; + + // Keep refreshing indicator while a background revalidate is in flight + sectionElement.dataset.refreshing = hasPendingRefresh ? 'true' : 'false'; let items = result?.Items ?? result ?? []; if (!Array.isArray(items)) items = []; @@ -7929,17 +7933,34 @@ const margin = sectionEnhanceObserverRootMargin || '30% 0px 30% 0px'; if (!deferUntilVisible || isSectionWithinEnhanceMargin(sectionElement, margin)) { applyProgressiveEnhancement(sectionElement, section, result, enhanceOptions); - return; + } else { + // Data ready but section left the enhance margin — wait until it re-enters + delete sectionElement.dataset.enhanceScheduled; + sectionEnhancePending.set(sectionElement, { + section, + options: enhanceOptions, + pendingResult: result + }); + observeSectionForEnhance(sectionElement); } - // Data ready but section left the enhance margin — wait until it re-enters - delete sectionElement.dataset.enhanceScheduled; - sectionEnhancePending.set(sectionElement, { - section, - options: enhanceOptions, - pendingResult: result + if (!hasPendingRefresh) return; + + refreshPromise.then((fresh) => { + if (!sectionElement.isConnected) return; + sectionElement.dataset.refreshing = 'false'; + // Prefer live DOM apply; if still pending observe, update pending snapshot + const pending = sectionEnhancePending.get(sectionElement); + if (pending && pending.pendingResult !== undefined) { + pending.pendingResult = fresh; + return; + } + applyProgressiveEnhancement(sectionElement, section, fresh, enhanceOptions); + }).catch((err) => { + console.warn('[KefinTweaks CardBuilder] Background refresh failed:', section?.config?.id, err); + if (!sectionElement.isConnected) return; + sectionElement.dataset.refreshing = 'false'; }); - observeSectionForEnhance(sectionElement); }).catch((err) => { console.error('[KefinTweaks CardBuilder] Progressive enhance failed:', section?.config?.id, err); if (!sectionElement.isConnected) return; diff --git a/scripts/sectionHelper.js b/scripts/sectionHelper.js index ba84228..01387e9 100644 --- a/scripts/sectionHelper.js +++ b/scripts/sectionHelper.js @@ -255,6 +255,12 @@ }; let mappedDataPromise = null; + const result = { + data: { Items: [] }, + isStale: true, + ensureData: null + }; + const ensureData = () => { if (!mappedDataPromise) { mappedDataPromise = (async () => { @@ -300,6 +306,24 @@ const raw = typeof merged.result?.ensureData === 'function' ? await merged.result.ensureData() : await merged.result?.dataPromise; + + const hasRefresh = results.some((r) => r && r._refreshPromise); + if (hasRefresh) { + result.refreshPromise = Promise.all( + results.map((r) => r._refreshPromise || Promise.resolve(r.data)) + ).then((allData) => { + const itemsByFreshQuery = []; + allData.forEach((data) => { + if (data?.Items) itemsByFreshQuery.push(data.Items); + else if (Array.isArray(data)) itemsByFreshQuery.push(data); + }); + const mergedFresh = window.cardBuilder?.postProcessItemsByQuery + ? window.cardBuilder.postProcessItemsByQuery(sectionConfig, itemsByFreshQuery) + : itemsByFreshQuery.flat(); + return postProcess(sectionConfig, mergedFresh); + }); + } + return postProcess(sectionConfig, raw); } @@ -307,17 +331,20 @@ const raw = typeof queryResult.ensureData === 'function' ? await queryResult.ensureData() : await queryResult.dataPromise; + + if (queryResult._refreshPromise) { + result.refreshPromise = queryResult._refreshPromise.then((fresh) => + postProcess(sectionConfig, fresh) + ); + } + return postProcess(sectionConfig, raw); })(); } return mappedDataPromise; }; - const result = { - data: { Items: [] }, - isStale: true, - ensureData - }; + result.ensureData = ensureData; Object.defineProperty(result, 'dataPromise', { configurable: true, enumerable: true,