Skip to content

Commit 6407929

Browse files
committed
fix: improve results handling for algolia search, add types
1 parent 62b7f6e commit 6407929

4 files changed

Lines changed: 77 additions & 41 deletions

File tree

app/composables/npm/search-utils.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NpmSearchResponse, NpmSearchResult, PackageMetaResponse } from '#shared/types'
1+
import type { NpmSearchResult, PackageMetaResponse, SearchResponse } from '#shared/types'
22

33
export function metaToSearchResult(meta: PackageMetaResponse): NpmSearchResult {
44
return {
@@ -20,22 +20,16 @@ export function metaToSearchResult(meta: PackageMetaResponse): NpmSearchResult {
2020
}
2121
}
2222

23-
export function emptySearchResponse(): NpmSearchResponse {
23+
export function emptySearchResponse(): SearchResponse {
2424
return {
2525
objects: [],
26-
total: 0,
2726
totalUnlimited: 0,
27+
total: 0,
2828
isStale: false,
2929
time: new Date().toISOString(),
3030
}
3131
}
3232

33-
export interface SearchSuggestion {
34-
type: 'user' | 'org'
35-
name: string
36-
exists: boolean
37-
}
38-
3933
export type SuggestionIntent = 'user' | 'org' | 'both' | null
4034

4135
export function isValidNpmName(name: string): boolean {

app/composables/npm/useAlgoliaSearch.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
liteClient as algoliasearch,
44
type LiteClient,
55
type SearchQuery,
6-
type SearchResponse,
6+
type SearchResponse as AlgoliaSearchResponse,
77
} from 'algoliasearch/lite'
88

99
let _searchClient: LiteClient | null = null
@@ -157,16 +157,15 @@ export function useAlgoliaSearch() {
157157
],
158158
})
159159

160-
const response = results[0] as SearchResponse<AlgoliaHit> | undefined
160+
const response = results[0] as AlgoliaSearchResponse<AlgoliaHit> | undefined
161161
if (!response) {
162162
throw new Error('Algolia returned an empty response')
163163
}
164164

165165
return {
166166
isStale: false,
167167
objects: response.hits.map(hitToSearchResult),
168-
totalUnlimited: response.nbHits ?? 0,
169-
total: Math.min(SEARCH_ENGINE_HITS_LIMIT.algolia, response.nbHits ?? 0),
168+
total: response.nbHits ?? 0,
170169
time: new Date().toISOString(),
171170
}
172171
}
@@ -203,7 +202,7 @@ export function useAlgoliaSearch() {
203202
],
204203
})
205204

206-
const response = results[0] as SearchResponse<AlgoliaHit> | undefined
205+
const response = results[0] as AlgoliaSearchResponse<AlgoliaHit> | undefined
207206
if (!response) break
208207

209208
serverTotal = response.nbHits ?? 0
@@ -319,36 +318,37 @@ export function useAlgoliaSearch() {
319318

320319
const { results } = await client.search({ requests })
321320

322-
const mainResponse = results[0] as SearchResponse<AlgoliaHit> | undefined
321+
const mainResponse = results[0] as AlgoliaSearchResponse<AlgoliaHit> | undefined
323322
if (!mainResponse) {
324323
throw new Error('Algolia returned an empty response')
325324
}
326325

327326
const searchResult: NpmSearchResponse = {
328327
isStale: false,
329328
objects: mainResponse.hits.map(hitToSearchResult),
330-
total: Math.min(SEARCH_ENGINE_HITS_LIMIT.algolia, mainResponse.nbHits ?? 0),
331-
totalUnlimited: mainResponse.nbHits ?? 0,
329+
total: mainResponse.nbHits ?? 0,
332330
time: new Date().toISOString(),
333331
}
334332

335333
let orgExists = false
336334
if (orgQueryIndex >= 0 && checks?.name) {
337-
const orgResponse = results[orgQueryIndex] as SearchResponse<AlgoliaHit> | undefined
335+
const orgResponse = results[orgQueryIndex] as AlgoliaSearchResponse<AlgoliaHit> | undefined
338336
const scopePrefix = `@${checks.name.toLowerCase()}/`
339337
orgExists =
340338
orgResponse?.hits?.some(h => h.name?.toLowerCase().startsWith(scopePrefix)) ?? false
341339
}
342340

343341
let userExists = false
344342
if (userQueryIndex >= 0) {
345-
const userResponse = results[userQueryIndex] as SearchResponse<AlgoliaHit> | undefined
343+
const userResponse = results[userQueryIndex] as AlgoliaSearchResponse<AlgoliaHit> | undefined
346344
userExists = (userResponse?.nbHits ?? 0) > 0
347345
}
348346

349347
let packageExists: boolean | null = null
350348
if (packageQueryIndex >= 0) {
351-
const pkgResponse = results[packageQueryIndex] as SearchResponse<AlgoliaHit> | undefined
349+
const pkgResponse = results[packageQueryIndex] as
350+
| AlgoliaSearchResponse<AlgoliaHit>
351+
| undefined
352352
packageExists = (pkgResponse?.nbHits ?? 0) > 0
353353
}
354354

app/composables/npm/useSearch.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import type { NpmSearchResponse, NpmSearchResult, SearchProvider } from '#shared/types'
1+
import type {
2+
NpmSearchResponse,
3+
NpmSearchResult,
4+
SearchProvider,
5+
SearchResponse,
6+
SearchResult,
7+
SearchSuggestion,
8+
} from '#shared/types'
29
import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch'
3-
import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils'
10+
import { emptySearchResponse, parseSuggestionIntent } from './search-utils'
411
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
512

613
export const SEARCH_ENGINE_HITS_LIMIT: Record<SearchProvider, number> = {
714
algolia: 1000,
815
npm: 5000,
916
} as const
1017

11-
function emptySearchPayload() {
12-
return {
13-
searchResponse: emptySearchResponse(),
14-
suggestions: [] as SearchSuggestion[],
15-
packageAvailability: null as { name: string; available: boolean } | null,
16-
}
17-
}
18+
const DEFAULT_INITIAL_SEARCH_LIMIT = 25
1819

1920
export interface SearchOptions {
2021
size?: number
@@ -78,6 +79,25 @@ export function useSearch(
7879
}
7980
}
8081

82+
function prepareSearchResponse(response: NpmSearchResponse | SearchResponse): SearchResponse {
83+
const totalUnlimited: number =
84+
'totalUnlimited' in response ? response.totalUnlimited : response.total
85+
86+
return {
87+
...response,
88+
totalUnlimited,
89+
total: Math.min(totalUnlimited, SEARCH_ENGINE_HITS_LIMIT[toValue(searchProvider)]),
90+
}
91+
}
92+
93+
function emptySearchPayload(): SearchResult {
94+
return {
95+
searchResponse: emptySearchResponse(),
96+
suggestions: [],
97+
packageAvailability: null,
98+
}
99+
}
100+
81101
/**
82102
* Determine which extra checks to include in the Algolia multi-search.
83103
* Returns `undefined` when nothing uncached needs checking.
@@ -166,7 +186,7 @@ export function useSearch(
166186
suggestionsLoading.value = false
167187
}
168188

169-
const asyncData = useLazyAsyncData(
189+
const asyncData = useLazyAsyncData<SearchResult>(
170190
() => `search:${toValue(searchProvider)}:${toValue(query)}`,
171191
async (_nuxtApp, { signal }) => {
172192
const q = toValue(query)
@@ -185,7 +205,11 @@ export function useSearch(
185205

186206
if (config.suggestions) {
187207
suggestionsLoading.value = true
188-
const result = await algoliaMultiSearch(q, { size: opts.size ?? 25 }, checks)
208+
const result = await algoliaMultiSearch(
209+
q,
210+
{ size: opts.size ?? DEFAULT_INITIAL_SEARCH_LIMIT },
211+
checks,
212+
)
189213

190214
if (q !== toValue(query)) {
191215
return emptySearchPayload()
@@ -194,28 +218,32 @@ export function useSearch(
194218
isRateLimited.value = false
195219
processAlgoliaChecks(q, checks, result)
196220
return {
197-
searchResponse: result.search,
221+
searchResponse: prepareSearchResponse(result.search),
198222
suggestions: suggestions.value,
199223
packageAvailability: packageAvailability.value,
200224
}
201225
}
202226

203-
const response = await searchAlgolia(q, { size: opts.size ?? 25 })
227+
const response = await searchAlgolia(q, { size: opts.size ?? DEFAULT_INITIAL_SEARCH_LIMIT })
204228

205229
if (q !== toValue(query)) {
206230
return emptySearchPayload()
207231
}
208232

209233
isRateLimited.value = false
210234
return {
211-
searchResponse: response,
235+
searchResponse: prepareSearchResponse(response),
212236
suggestions: [],
213237
packageAvailability: null,
214238
}
215239
}
216240

217241
try {
218-
const response = await searchNpm(q, { size: opts.size ?? 25 }, signal)
242+
const response = await searchNpm(
243+
q,
244+
{ size: opts.size ?? DEFAULT_INITIAL_SEARCH_LIMIT },
245+
signal,
246+
)
219247

220248
if (q !== toValue(query)) {
221249
return emptySearchPayload()
@@ -225,7 +253,7 @@ export function useSearch(
225253

226254
isRateLimited.value = false
227255
return {
228-
searchResponse: response,
256+
searchResponse: prepareSearchResponse(response),
229257
suggestions: [],
230258
packageAvailability: null,
231259
}
@@ -261,8 +289,7 @@ export function useSearch(
261289

262290
// Seed cache from asyncData for Algolia (which skips cache on initial fetch)
263291
if (!cache.value && asyncData.data.value) {
264-
const { searchResponse } = asyncData.data.value
265-
setCache([...searchResponse.objects], searchResponse.total)
292+
setCache([...searchResponse.objects], searchResponse.totalUnlimited)
266293
}
267294

268295
const currentCount = cache.value?.objects.length ?? 0
@@ -326,7 +353,7 @@ export function useSearch(
326353
},
327354
)
328355

329-
const data = computed<NpmSearchResponse | null>(() => {
356+
const data = computed<SearchResponse | null>(() => {
330357
if (cache.value) {
331358
return {
332359
isStale: false,
@@ -339,7 +366,7 @@ export function useSearch(
339366
return asyncData.data.value?.searchResponse ?? null
340367
})
341368

342-
const hasMore = computed(() => {
369+
const hasMore = computed<boolean>(() => {
343370
if (!cache.value) return true
344371
return cache.value.objects.length < cache.value.total
345372
})

shared/types/npm-registry.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,26 @@ export interface NpmPerson {
124124
export interface NpmSearchResponse {
125125
isStale: boolean
126126
objects: NpmSearchResult[]
127-
totalUnlimited?: number
128127
total: number
129128
time: string
130129
}
131130

131+
export interface SearchResponse extends NpmSearchResponse {
132+
totalUnlimited: number
133+
}
134+
135+
export interface SearchSuggestion {
136+
type: 'user' | 'org'
137+
name: string
138+
exists: boolean
139+
}
140+
141+
export interface SearchResult {
142+
searchResponse: SearchResponse
143+
suggestions: SearchSuggestion[]
144+
packageAvailability: { name: string; available: boolean } | null
145+
}
146+
132147
export interface NpmSearchResult {
133148
package: NpmSearchPackage
134149
score: NpmSearchScore

0 commit comments

Comments
 (0)