diff --git a/app/composables/npm/useUserPackages.ts b/app/composables/npm/useUserPackages.ts index ed9f859382..f5490c79f6 100644 --- a/app/composables/npm/useUserPackages.ts +++ b/app/composables/npm/useUserPackages.ts @@ -1,3 +1,5 @@ +import type { NpmSearchResponse } from '#shared/types' + /** Default page size for incremental loading (npm registry path) */ const PAGE_SIZE = 50 as const @@ -128,8 +130,9 @@ export function useUserPackages(username: MaybeRefOrGetter) { return } - const currentCount = cache.value?.objects.length ?? 0 - const total = Math.min(cache.value?.total ?? Infinity, MAX_RESULTS) + const currentData = getCurrentUserPackages(user) + const currentCount = currentData?.objects.length ?? 0 + const total = Math.min(currentData?.total ?? Infinity, MAX_RESULTS) if (currentCount >= total) return @@ -153,20 +156,13 @@ export function useUserPackages(username: MaybeRefOrGetter) { // Guard against stale response if (user !== toValue(username) || activeProvider.value !== 'npm') return - if (cache.value && cache.value.username === user) { - const existingNames = new Set(cache.value.objects.map(obj => obj.package.name)) - const newObjects = response.objects.filter(obj => !existingNames.has(obj.package.name)) - cache.value = { - username: user, - objects: [...cache.value.objects, ...newObjects], - total: response.total, - } - } else { - cache.value = { - username: user, - objects: response.objects, - total: response.total, - } + const existingObjects = getCurrentUserPackages(user)?.objects ?? [] + const existingNames = new Set(existingObjects.map(obj => obj.package.name)) + const newObjects = response.objects.filter(obj => !existingNames.has(obj.package.name)) + cache.value = { + username: user, + objects: [...existingObjects, ...newObjects], + total: response.total, } } finally { if (manageLoadingState) isLoadingMore.value = false @@ -205,6 +201,21 @@ export function useUserPackages(username: MaybeRefOrGetter) { }, ) + function getCurrentUserPackages(user: string) { + if (cache.value && cache.value.username === user) { + return cache.value + } + + const response = asyncData.data.value + if (!response) return null + + return { + username: user, + objects: response.objects, + total: response.total, + } + } + // Computed data that uses cache (only if it belongs to the current username) const data = computed(() => { const user = toValue(username) @@ -224,10 +235,11 @@ export function useUserPackages(username: MaybeRefOrGetter) { if (!toValue(username)) return false // Algolia fetches everything in one request; only npm needs pagination if (activeProvider.value !== 'npm') return false - if (!cache.value) return true + const currentData = getCurrentUserPackages(toValue(username)) + if (!currentData) return false // npm path: more available if we haven't hit the server total or our cap - const fetched = cache.value.objects.length - const available = cache.value.total + const fetched = currentData.objects.length + const available = currentData.total return fetched < available && fetched < MAX_RESULTS }) diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index c1454355f3..55c9caadd0 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -1,7 +1,7 @@ import type { RemovableRef } from '@vueuse/core' -import { useLocalStorage } from '@vueuse/core' -import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants' import type { LocaleObject } from '@nuxtjs/i18n' +import { useLocalStorage, useMounted } from '@vueuse/core' +import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants' import { BACKGROUND_THEMES } from '#shared/utils/constants' type BackgroundThemeId = keyof typeof BACKGROUND_THEMES @@ -197,9 +197,10 @@ export function useAccentColor() { */ export function useSearchProvider() { const { settings } = useSettings() + const isMounted = useMounted() const searchProvider = computed({ - get: () => settings.value.searchProvider, + get: () => (isMounted.value ? settings.value.searchProvider : DEFAULT_SETTINGS.searchProvider), set: (value: SearchProvider) => { settings.value.searchProvider = value }, diff --git a/app/pages/~[username]/index.vue b/app/pages/~[username]/index.vue index 5c4bb941ae..e56d7efc4e 100644 --- a/app/pages/~[username]/index.vue +++ b/app/pages/~[username]/index.vue @@ -180,7 +180,7 @@ defineOgImage( diff --git a/test/e2e/hydration.spec.ts b/test/e2e/hydration.spec.ts index 0f1c6ae9e4..be5644544f 100644 --- a/test/e2e/hydration.spec.ts +++ b/test/e2e/hydration.spec.ts @@ -113,6 +113,16 @@ test.describe('Hydration', () => { }) } }) + + test('user packages page with npm search provider', async ({ page, goto, hydrationErrors }) => { + await injectLocalStorage(page, { + 'npmx-settings': JSON.stringify({ searchProvider: 'npm' }), + }) + + await goto('/~qwerzl', { waitUntil: 'hydration' }) + + expect(hydrationErrors).toEqual([]) + }) }) async function injectLocalStorage(page: Page, entries: Record) {