Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions app/composables/npm/useUserPackages.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -128,8 +130,9 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
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

Expand All @@ -153,20 +156,13 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
// 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
Expand Down Expand Up @@ -205,6 +201,21 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
},
)

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<NpmSearchResponse | null>(() => {
const user = toValue(username)
Expand All @@ -224,10 +235,11 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
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
})

Expand Down
7 changes: 4 additions & 3 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
},
Expand Down
2 changes: 1 addition & 1 deletion app/pages/~[username]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ defineOgImage(

<!-- Loading state (only on initial load, not when we already have data) -->
<LoadingSpinner
v-if="status === 'pending' && packages.length === 0 && !error"
v-if="(status === 'idle' || status === 'pending') && packages.length === 0 && !error"
:text="$t('common.loading_packages')"
/>

Expand Down
10 changes: 10 additions & 0 deletions test/e2e/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>) {
Expand Down
Loading