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 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-31 00:52:11 -04:00
parent 57989dfc58
commit 6105770420

View File

@ -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<CatalogFeatured | null>(null)
const catalogResource = useCachedResource<MarketplaceApp[]>({
key: 'app-catalog',
// Preserves this view's existing dynamic-catalog-first behavior (Marketplace's
@ -278,17 +277,28 @@ const catalogResource = useCachedResource<MarketplaceApp[]>({
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<CatalogFeatured | null>({
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