From 6105770420025617f74bec843ca5d448a6c53ad4 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 31 Jul 2026 00:52:11 -0400 Subject: [PATCH] fix(02-review): WR-01 decouple Discover's featured-banner data from app-catalog dedup race Marketplace.vue and Discover.vue both register a useCachedResource against the shared 'app-catalog' key with different fetchers; resources.ts's in-flight dedup means whichever view's fetcher wins a given race governs the shared entry, silently dropping Discover's catalogFeatured side effect when Marketplace's simpler fetcher wins. Give the featured-banner payload its own cache key ('app-catalog:featured') subscribed only by Discover.vue, so it always gets its own data regardless of which view's fetcher wins the shared 'app-catalog' race. fetchAppCatalog() already memoizes internally (1h TTL + localStorage fallback), so this is normally a cache hit rather than an extra network request. The shared 'app-catalog' key and its dedup behavior are unchanged. Co-Authored-By: Claude --- neode-ui/src/views/Discover.vue | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index 618fd7a9..13915d40 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -269,7 +269,6 @@ const searchQuery = ref('') // fetch (02-04). Both views populate the same cache entry, whichever one // happens to trigger a given fetch — the store doesn't care which caller's // fetcher ran, only the resulting cached value. -const catalogFeatured = ref(null) const catalogResource = useCachedResource({ key: 'app-catalog', // Preserves this view's existing dynamic-catalog-first behavior (Marketplace's @@ -278,17 +277,28 @@ const catalogResource = useCachedResource({ fetcher: async () => { const catalog = await fetchAppCatalog() if (catalog) { - catalogFeatured.value = catalog.featured if (import.meta.env.DEV) console.log('Loaded app catalog from registry:', catalog.apps.length, 'apps') return catalog.apps } - catalogFeatured.value = null if (import.meta.env.DEV) console.log('Using hardcoded app list (catalog.json unavailable)') return getCuratedAppList() }, ttlMs: 300_000, persist: true, }) +// Featured banner payload lives on its own cache key (WR-01), deliberately +// NOT derived from catalogResource's fetcher: 'app-catalog' is a shared key +// with Marketplace.vue, whose simpler getCuratedAppList()-only fetcher can +// win the in-flight dedup race and never populate the featured payload. +// fetchAppCatalog() has its own internal memoization (1h TTL + localStorage +// fallback), so this second subscription is normally a cache hit, not an +// extra network round trip. +const catalogFeatured = useCachedResource({ + key: 'app-catalog:featured', + fetcher: async () => (await fetchAppCatalog())?.featured ?? null, + ttlMs: 300_000, + persist: true, +}).data const communityApps = computed(() => catalogResource.data.value ?? []) const loadingCommunity = computed(() => catalogResource.entry.loadState === 'loading') // Keep-last-value error banner (D-07): a failed background refresh never