Skip to content

Commit ec549f9

Browse files
committed
fix: apply limits to algolia and npm search output, fix visual issues
1 parent 03f7ecd commit ec549f9

13 files changed

Lines changed: 100 additions & 52 deletions

File tree

app/components/Package/Card.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ const numberFormatter = useNumberFormatter()
161161
</div>
162162

163163
<ul
164-
role="list"
165164
v-if="result.package.keywords?.length"
166165
:aria-label="$t('package.card.keywords')"
167166
class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0 pointer-events-none items-center"

app/components/PaginationControls.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import type { PageSize, PaginationMode, ViewMode } from '#shared/types/preferences'
33
import { PAGE_SIZE_OPTIONS } from '#shared/types/preferences'
44
5+
const ALL_PAGES_VISIBLE_TRESHOLD = 7
6+
57
const props = defineProps<{
68
totalItems: number
79
/** When in table view, force pagination mode (no infinite scroll for tables) */
@@ -63,7 +65,7 @@ const visiblePages = computed(() => {
6365
const current = currentPage.value
6466
const pages: (number | 'ellipsis')[] = []
6567
66-
if (total <= 7) {
68+
if (total <= ALL_PAGES_VISIBLE_TRESHOLD) {
6769
// Show all pages
6870
for (let i = 1; i <= total; i++) {
6971
pages.push(i)
@@ -97,6 +99,11 @@ const visiblePages = computed(() => {
9799
return pages
98100
})
99101
102+
// disable last page button to prevent TOO MANY REQUESTS error
103+
function isPageButtonDisabled(page: number): boolean {
104+
return totalPages.value > ALL_PAGES_VISIBLE_TRESHOLD && page > currentPage.value + 2
105+
}
106+
100107
function handlePageSizeChange(event: Event) {
101108
const target = event.target as HTMLSelectElement
102109
const value = target.value
@@ -167,8 +174,8 @@ function handlePageSizeChange(event: Event) {
167174
<span class="text-sm font-mono text-fg-muted">
168175
{{
169176
$t('filters.pagination.showing', {
170-
start: startItem,
171-
end: endItem,
177+
start: $n(startItem),
178+
end: $n(endItem),
172179
total: $n(totalItems),
173180
})
174181
}}
@@ -197,6 +204,7 @@ function handlePageSizeChange(event: Event) {
197204
<button
198205
v-else
199206
type="button"
207+
:disabled="isPageButtonDisabled(page)"
200208
class="min-w-[32px] h-8 px-2 font-mono text-sm rounded transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
201209
:class="
202210
page === currentPage

app/composables/npm/search-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function emptySearchResponse(): NpmSearchResponse {
2424
return {
2525
objects: [],
2626
total: 0,
27+
totalUnlimited: 0,
2728
isStale: false,
2829
time: new Date().toISOString(),
2930
}

app/composables/npm/useAlgoliaSearch.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ export function useAlgoliaSearch() {
165165
return {
166166
isStale: false,
167167
objects: response.hits.map(hitToSearchResult),
168-
total: response.nbHits ?? 0,
168+
totalUnlimited: response.nbHits ?? 0,
169+
total: Math.min(SEARCH_ENGINE_HITS_LIMIT.algolia, response.nbHits ?? 0),
169170
time: new Date().toISOString(),
170171
}
171172
}
@@ -326,7 +327,8 @@ export function useAlgoliaSearch() {
326327
const searchResult: NpmSearchResponse = {
327328
isStale: false,
328329
objects: mainResponse.hits.map(hitToSearchResult),
329-
total: mainResponse.nbHits ?? 0,
330+
total: Math.min(SEARCH_ENGINE_HITS_LIMIT.algolia, mainResponse.nbHits ?? 0),
331+
totalUnlimited: mainResponse.nbHits ?? 0,
330332
time: new Date().toISOString(),
331333
}
332334

app/composables/npm/useSearch.ts

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
2-
import type { SearchProvider } from '~/composables/useSettings'
1+
import type { NpmSearchResponse, NpmSearchResult, SearchProvider } from '#shared/types'
32
import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch'
43
import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils'
54
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
65

6+
export const SEARCH_ENGINE_HITS_LIMIT: Record<SearchProvider, number> = {
7+
algolia: 1000,
8+
npm: 5000,
9+
} as const
10+
711
function emptySearchPayload() {
812
return {
913
searchResponse: emptySearchResponse(),
@@ -25,6 +29,14 @@ export interface UseSearchConfig {
2529
suggestions?: boolean
2630
}
2731

32+
interface SearchResponseCache {
33+
query: string
34+
provider: SearchProvider
35+
objects: NpmSearchResult[]
36+
totalUnlimited: number
37+
total: number
38+
}
39+
2840
export function useSearch(
2941
query: MaybeRefOrGetter<string>,
3042
searchProvider: MaybeRefOrGetter<SearchProvider>,
@@ -38,12 +50,7 @@ export function useSearch(
3850
checkUserExists: checkUserNpm,
3951
} = useNpmSearch()
4052

41-
const cache = shallowRef<{
42-
query: string
43-
provider: SearchProvider
44-
objects: NpmSearchResult[]
45-
total: number
46-
} | null>(null)
53+
const cache = shallowRef<SearchResponseCache | null>(null)
4754

4855
const isLoadingMore = shallowRef(false)
4956
const isRateLimited = shallowRef(false)
@@ -54,6 +61,23 @@ export function useSearch(
5461
const existenceCache = shallowRef<Record<string, boolean>>({})
5562
const suggestionRequestId = shallowRef(0)
5663

64+
function setCache(objects: NpmSearchResult[] | null, total: number = 0): void {
65+
if (objects === null) {
66+
cache.value = null
67+
return
68+
}
69+
70+
const provider = toValue(searchProvider)
71+
72+
cache.value = {
73+
query: toValue(query),
74+
provider,
75+
objects,
76+
totalUnlimited: total,
77+
total: Math.min(total, SEARCH_ENGINE_HITS_LIMIT[provider]),
78+
}
79+
}
80+
5781
/**
5882
* Determine which extra checks to include in the Algolia multi-search.
5983
* Returns `undefined` when nothing uncached needs checking.
@@ -154,7 +178,7 @@ export function useSearch(
154178
}
155179

156180
const opts = toValue(options)
157-
cache.value = null
181+
setCache(null)
158182

159183
if (provider === 'algolia') {
160184
const checks = config.suggestions ? buildAlgoliaChecks(q) : undefined
@@ -197,12 +221,7 @@ export function useSearch(
197221
return emptySearchPayload()
198222
}
199223

200-
cache.value = {
201-
query: q,
202-
provider,
203-
objects: response.objects,
204-
total: response.total,
205-
}
224+
setCache(response.objects, response.total)
206225

207226
isRateLimited.value = false
208227
return {
@@ -230,25 +249,20 @@ export function useSearch(
230249
const provider = toValue(searchProvider)
231250

232251
if (!q) {
233-
cache.value = null
252+
setCache(null)
234253
return
235254
}
236255

237256
if (cache.value && (cache.value.query !== q || cache.value.provider !== provider)) {
238-
cache.value = null
257+
setCache(null)
239258
await asyncData.refresh()
240259
return
241260
}
242261

243262
// Seed cache from asyncData for Algolia (which skips cache on initial fetch)
244263
if (!cache.value && asyncData.data.value) {
245264
const { searchResponse } = asyncData.data.value
246-
cache.value = {
247-
query: q,
248-
provider,
249-
objects: [...searchResponse.objects],
250-
total: searchResponse.total,
251-
}
265+
setCache([...searchResponse.objects], searchResponse.total)
252266
}
253267

254268
const currentCount = cache.value?.objects.length ?? 0
@@ -270,25 +284,17 @@ export function useSearch(
270284
if (cache.value && cache.value.query === q && cache.value.provider === provider) {
271285
const existingNames = new Set(cache.value.objects.map(obj => obj.package.name))
272286
const newObjects = response.objects.filter(obj => !existingNames.has(obj.package.name))
273-
cache.value = {
274-
query: q,
275-
provider,
276-
objects: [...cache.value.objects, ...newObjects],
277-
total: response.total,
278-
}
287+
288+
setCache([...cache.value.objects, ...newObjects], response.total)
279289
} else {
280-
cache.value = {
281-
query: q,
282-
provider,
283-
objects: response.objects,
284-
total: response.total,
285-
}
290+
setCache(response.objects, response.total)
286291
}
287292

288293
if (
289294
cache.value &&
290295
cache.value.objects.length < targetSize &&
291-
cache.value.objects.length < cache.value.total
296+
cache.value.objects.length < cache.value.total &&
297+
cache.value.objects.length < SEARCH_ENGINE_HITS_LIMIT[provider] // additional protection from infinite loop
292298
) {
293299
await fetchMore(targetSize)
294300
}
@@ -310,7 +316,7 @@ export function useSearch(
310316
watch(
311317
() => toValue(searchProvider),
312318
async () => {
313-
cache.value = null
319+
setCache(null)
314320
existenceCache.value = {}
315321
await asyncData.refresh()
316322
const targetSize = toValue(options).size
@@ -326,6 +332,7 @@ export function useSearch(
326332
isStale: false,
327333
objects: cache.value.objects,
328334
total: cache.value.total,
335+
totalUnlimited: cache.value.totalUnlimited,
329336
time: new Date().toISOString(),
330337
}
331338
}

app/composables/npm/useUserPackages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
1+
import type { NpmSearchResponse, NpmSearchResult, SearchProvider } from '#shared/types'
22
import { emptySearchResponse } from './search-utils'
33

44
/** Default page size for incremental loading (npm registry path) */
@@ -38,7 +38,7 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
3838

3939
/** Tracks which provider actually served the current data (may differ from
4040
* searchProvider when Algolia returns empty and we fall through to npm) */
41-
const activeProvider = shallowRef<'npm' | 'algolia'>(searchProviderValue.value)
41+
const activeProvider = shallowRef<SearchProvider>(searchProviderValue.value)
4242

4343
const cache = shallowRef<{
4444
username: string

app/composables/useGlobalSearch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
4545
)
4646

4747
// Updates URL when search query changes (immediately for instantSearch or after Enter hit otherwise)
48-
const updateUrlQueryImpl = (value: string, provider: 'npm' | 'algolia') => {
48+
const updateUrlQueryImpl = (value: string, provider: SearchProvider) => {
4949
const isSameQuery = route.query.q === value && route.query.p === provider
5050
// Don't navigate away from pages that use ?q for local filtering
5151
if ((pagesWithLocalFilter.has(route.name as string) && place === 'content') || isSameQuery) {

app/composables/useSettings.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import type { RemovableRef } from '@vueuse/core'
22
import { useLocalStorage } from '@vueuse/core'
33
import { ACCENT_COLORS } from '#shared/utils/constants'
44
import type { LocaleObject } from '@nuxtjs/i18n'
5+
import type { SearchProvider } from '#shared/types'
56
import { BACKGROUND_THEMES } from '#shared/utils/constants'
67

78
type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
89

910
type AccentColorId = keyof typeof ACCENT_COLORS.light
1011

11-
/** Available search providers */
12-
export type SearchProvider = 'npm' | 'algolia'
13-
1412
/**
1513
* Application settings stored in localStorage
1614
*/

app/pages/search.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ const effectiveTotal = computed(() => {
252252
return displayResults.value.length
253253
})
254254
255+
const resultsLimitAppliedText = computed<string>(() => {
256+
console.log(effectiveTotal.value, visibleResults.value?.totalUnlimited)
257+
if (isRelevanceSort.value && effectiveTotal.value < visibleResults.value?.totalUnlimited) {
258+
const total = { total: $n(visibleResults.value?.totalUnlimited) }
259+
260+
return searchProvider.value === 'npm'
261+
? $t('search.more_results_available_npm', total)
262+
: $t('search.more_results_available_algolia', total)
263+
}
264+
// do not show hint if results limit is not reached
265+
return ''
266+
})
267+
255268
// Handle filter chip removal
256269
function handleClearFilter(chip: FilterChip) {
257270
clearFilter(chip)
@@ -784,12 +797,19 @@ onBeforeUnmount(() => {
784797
effectiveTotal,
785798
)
786799
}}
800+
<TooltipApp
801+
v-if="resultsLimitAppliedText"
802+
position="top"
803+
:text="resultsLimitAppliedText"
804+
>
805+
<span class="i-lucide:info w-3 h-3 text-fg-subtle cursor-help" aria-hidden="true" />
806+
</TooltipApp>
787807
</p>
788808
</div>
789809

790810
<div v-else-if="status === 'success' || status === 'error'" class="py-12">
791811
<p class="text-fg-muted font-mono mb-6 text-center">
792-
{{ $t('search.no_results', { query }) }}
812+
{{ $t('search.no_results', { query: committedQuery }) }}
793813
</p>
794814

795815
<div v-if="validatedSuggestions.length > 0" class="max-w-md mx-auto mb-6 space-y-3">

i18n/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@
7171
"instant_search_off": "off",
7272
"instant_search_turn_on": "turn on",
7373
"instant_search_turn_off": "turn off",
74-
"instant_search_advisory": "{label} {state} — {action}"
74+
"instant_search_advisory": "{label} {state} — {action}",
75+
"more_results_available_npm": "There are {total} results for this search, but npm registry limits search output. Refine your query to narrow results.",
76+
"more_results_available_algolia": "There are {total} results for this search. Algolia search is capped at 1,000 — switch to npm Registry for up to 5,000 results, or refine your query."
7577
},
7678
"nav": {
7779
"main_navigation": "Main",

0 commit comments

Comments
 (0)