From e461e4ce2ca93cce9511eadd6f977e625a0b2c64 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Tue, 10 Feb 2026 22:20:30 +0000 Subject: [PATCH 01/11] fix: correct search data transferring server to client --- app/composables/npm/useSearch.ts | 65 +++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/app/composables/npm/useSearch.ts b/app/composables/npm/useSearch.ts index 39f33e65c..5b525ce8e 100644 --- a/app/composables/npm/useSearch.ts +++ b/app/composables/npm/useSearch.ts @@ -4,6 +4,14 @@ import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch' import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils' import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name' +function emptySearchPayload() { + return { + searchResponse: emptySearchResponse(), + suggestions: [] as SearchSuggestion[], + packageAvailability: null as { name: string; available: boolean } | null, + } +} + export interface SearchOptions { size?: number } @@ -142,7 +150,7 @@ export function useSearch( if (!q.trim()) { isRateLimited.value = false - return emptySearchResponse() + return emptySearchPayload() } const opts = toValue(options) @@ -156,29 +164,37 @@ export function useSearch( const result = await algoliaMultiSearch(q, { size: opts.size ?? 25 }, checks) if (q !== toValue(query)) { - return emptySearchResponse() + return emptySearchPayload() } isRateLimited.value = false processAlgoliaChecks(q, checks, result) - return result.search + return { + searchResponse: result.search, + suggestions: suggestions.value, + packageAvailability: packageAvailability.value, + } } const response = await searchAlgolia(q, { size: opts.size ?? 25 }) if (q !== toValue(query)) { - return emptySearchResponse() + return emptySearchPayload() } isRateLimited.value = false - return response + return { + searchResponse: response, + suggestions: [], + packageAvailability: null, + } } try { const response = await searchNpm(q, { size: opts.size ?? 25 }, signal) if (q !== toValue(query)) { - return emptySearchResponse() + return emptySearchPayload() } cache.value = { @@ -189,7 +205,11 @@ export function useSearch( } isRateLimited.value = false - return response + return { + searchResponse: response, + suggestions: [], + packageAvailability: null, + } } catch (error: unknown) { const errorMessage = (error as { message?: string })?.message || String(error) const isRateLimitError = @@ -197,12 +217,29 @@ export function useSearch( if (isRateLimitError) { isRateLimited.value = true - return emptySearchResponse() + return emptySearchPayload() } throw error } }, - { default: emptySearchResponse }, + { default: emptySearchPayload }, + ) + + watch( + [() => asyncData.data.value.suggestions, () => suggestions.value], + ([payloadSuggestions, localSuggestions]) => { + if (!payloadSuggestions.length && !localSuggestions.length) return + suggestions.value = payloadSuggestions.length ? payloadSuggestions : localSuggestions + }, + { immediate: true }, + ) + + watch( + [() => asyncData.data.value?.packageAvailability, () => packageAvailability.value], + ([payloadPackageAvailability, localPackageAvailability]) => { + packageAvailability.value = payloadPackageAvailability || localPackageAvailability + }, + { immediate: true }, ) async function fetchMore(targetSize: number): Promise { @@ -222,12 +259,12 @@ export function useSearch( // Seed cache from asyncData for Algolia (which skips cache on initial fetch) if (!cache.value && asyncData.data.value) { - const d = asyncData.data.value + const { searchResponse } = asyncData.data.value cache.value = { query: q, provider, - objects: [...d.objects], - total: d.total, + objects: [...searchResponse.objects], + total: searchResponse.total, } } @@ -306,10 +343,10 @@ export function useSearch( time: new Date().toISOString(), } } - return asyncData.data.value + return asyncData.data.value?.searchResponse ?? null }) - if (import.meta.client && asyncData.data.value?.isStale) { + if (import.meta.client && asyncData.data.value?.searchResponse.isStale) { onMounted(() => { asyncData.refresh() }) From 552240ba32d3aa8e2a21d2292efa7453162eee1e Mon Sep 17 00:00:00 2001 From: Vordgi Date: Tue, 10 Feb 2026 22:31:39 +0000 Subject: [PATCH 02/11] fix: correct search data transferring client to server --- app/composables/useSettings.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 31476415e..b822b455b 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -115,11 +115,18 @@ export function useAccentColor() { * Composable for managing the search provider setting. */ export function useSearchProvider() { + const cookie = useCookie('search-provider', { + secure: true, + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 30, + path: '/', + }) const { settings } = useSettings() const searchProvider = computed({ - get: () => settings.value.searchProvider, + get: () => (cookie.value === 'npm' ? 'npm' : settings.value.searchProvider), set: (value: SearchProvider) => { + cookie.value = value settings.value.searchProvider = value }, }) From 73c4d8429c08d4aca76a678c303e9d339c0bbea0 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Tue, 10 Feb 2026 23:15:19 +0000 Subject: [PATCH 03/11] fix: correct npm search data transferring server to client --- app/composables/npm/useSearch.ts | 225 ++++++++++++++++--------------- 1 file changed, 115 insertions(+), 110 deletions(-) diff --git a/app/composables/npm/useSearch.ts b/app/composables/npm/useSearch.ts index 5b525ce8e..77820a472 100644 --- a/app/composables/npm/useSearch.ts +++ b/app/composables/npm/useSearch.ts @@ -52,7 +52,7 @@ export function useSearch( const suggestionsLoading = shallowRef(false) const packageAvailability = shallowRef<{ name: string; available: boolean } | null>(null) const existenceCache = shallowRef>({}) - let suggestionRequestId = 0 + const suggestionRequestId = shallowRef(0) /** * Determine which extra checks to include in the Algolia multi-search. @@ -225,23 +225,6 @@ export function useSearch( { default: emptySearchPayload }, ) - watch( - [() => asyncData.data.value.suggestions, () => suggestions.value], - ([payloadSuggestions, localSuggestions]) => { - if (!payloadSuggestions.length && !localSuggestions.length) return - suggestions.value = payloadSuggestions.length ? payloadSuggestions : localSuggestions - }, - { immediate: true }, - ) - - watch( - [() => asyncData.data.value?.packageAvailability, () => packageAvailability.value], - ([payloadPackageAvailability, localPackageAvailability]) => { - packageAvailability.value = payloadPackageAvailability || localPackageAvailability - }, - { immediate: true }, - ) - async function fetchMore(targetSize: number): Promise { const q = toValue(query).trim() const provider = searchProvider.value @@ -346,118 +329,140 @@ export function useSearch( return asyncData.data.value?.searchResponse ?? null }) - if (import.meta.client && asyncData.data.value?.searchResponse.isStale) { - onMounted(() => { - asyncData.refresh() - }) - } - const hasMore = computed(() => { if (!cache.value) return true return cache.value.objects.length < cache.value.total }) - // npm suggestion checking (Algolia handles suggestions inside the search handler above) - if (config.suggestions) { - async function validateSuggestionsNpm(q: string) { - const requestId = ++suggestionRequestId - const { intent, name } = parseSuggestionIntent(q) - - const trimmed = q.trim() - if (isValidNewPackageName(trimmed)) { - checkPackageExists(trimmed) - .then(exists => { - if (trimmed === toValue(query).trim()) { - packageAvailability.value = { name: trimmed, available: !exists } - } - }) - .catch(() => { - packageAvailability.value = null - }) - } else { - packageAvailability.value = null - } + async function validateSuggestionsNpm(q: string) { + const requestId = ++suggestionRequestId.value + const { intent, name } = parseSuggestionIntent(q) + let availability: { name: string; available: boolean } | null = null - if (!intent || !name) { - suggestions.value = [] - suggestionsLoading.value = false - return - } + const trimmed = q.trim() + if (isValidNewPackageName(trimmed)) { + checkPackageExists(trimmed) + .then(exists => { + if (trimmed === toValue(query).trim()) { + availability = { name: trimmed, available: !exists } + packageAvailability.value = availability + } + }) + .catch(() => { + availability = null + }) + } else { + availability = null + } - suggestionsLoading.value = true - const result: SearchSuggestion[] = [] - const lowerName = name.toLowerCase() + if (!intent || !name) { + suggestionsLoading.value = false + return { suggestions: [], packageAvailability: availability } + } - try { - const wantOrg = intent === 'org' || intent === 'both' - const wantUser = intent === 'user' || intent === 'both' - - const promises: Promise[] = [] - - if (wantOrg && existenceCache.value[`org:${lowerName}`] === undefined) { - promises.push( - checkOrgNpm(name) - .then(exists => { - existenceCache.value = { ...existenceCache.value, [`org:${lowerName}`]: exists } - }) - .catch(() => { - existenceCache.value = { ...existenceCache.value, [`org:${lowerName}`]: false } - }), - ) - } + suggestionsLoading.value = true + const result: SearchSuggestion[] = [] + const lowerName = name.toLowerCase() - if (wantUser && existenceCache.value[`user:${lowerName}`] === undefined) { - promises.push( - checkUserNpm(name) - .then(exists => { - existenceCache.value = { ...existenceCache.value, [`user:${lowerName}`]: exists } - }) - .catch(() => { - existenceCache.value = { ...existenceCache.value, [`user:${lowerName}`]: false } - }), - ) - } + try { + const wantOrg = intent === 'org' || intent === 'both' + const wantUser = intent === 'user' || intent === 'both' - if (promises.length > 0) { - await Promise.all(promises) - } + const promises: Promise[] = [] - if (requestId !== suggestionRequestId) return + if (wantOrg && existenceCache.value[`org:${lowerName}`] === undefined) { + promises.push( + checkOrgNpm(name) + .then(exists => { + existenceCache.value = { ...existenceCache.value, [`org:${lowerName}`]: exists } + }) + .catch(() => { + existenceCache.value = { ...existenceCache.value, [`org:${lowerName}`]: false } + }), + ) + } - const isOrg = wantOrg && existenceCache.value[`org:${lowerName}`] - const isUser = wantUser && existenceCache.value[`user:${lowerName}`] + if (wantUser && existenceCache.value[`user:${lowerName}`] === undefined) { + promises.push( + checkUserNpm(name) + .then(exists => { + existenceCache.value = { ...existenceCache.value, [`user:${lowerName}`]: exists } + }) + .catch(() => { + existenceCache.value = { ...existenceCache.value, [`user:${lowerName}`]: false } + }), + ) + } - if (isOrg) { - result.push({ type: 'org', name, exists: true }) - } - if (isUser && !isOrg) { - result.push({ type: 'user', name, exists: true }) - } - } finally { - if (requestId === suggestionRequestId) { - suggestionsLoading.value = false - } + if (promises.length > 0) { + await Promise.all(promises) } - if (requestId === suggestionRequestId) { - suggestions.value = result + if (requestId !== suggestionRequestId.value) + return { suggestions: [], packageAvailability: availability } + + const isOrg = wantOrg && existenceCache.value[`org:${lowerName}`] + const isUser = wantUser && existenceCache.value[`user:${lowerName}`] + + if (isOrg) { + result.push({ type: 'org', name, exists: true }) + } + if (isUser && !isOrg) { + result.push({ type: 'user', name, exists: true }) + } + } finally { + if (requestId === suggestionRequestId.value) { + suggestionsLoading.value = false } } - watch( - () => toValue(query), - q => { - if (searchProvider.value !== 'algolia') { - validateSuggestionsNpm(q) - } - }, - { immediate: true }, - ) + if (requestId === suggestionRequestId.value) { + suggestions.value = result + return { suggestions: result, packageAvailability: availability } + } + + return { suggestions: [], packageAvailability: availability } + } + + const npmSuggestions = useLazyAsyncData( + () => `npm-suggestions:${searchProvider.value}:${toValue(query)}`, + async () => { + const q = toValue(query).trim() + if (searchProvider.value === 'algolia' || !q) + return { suggestions: [], packageAvailability: null } + const { intent, name } = parseSuggestionIntent(q) + if (!intent || !name) return { suggestions: [], packageAvailability: null } + return validateSuggestionsNpm(q) + }, + { default: () => ({ suggestions: [], packageAvailability: null }) }, + ) - watch(searchProvider, () => { - if (searchProvider.value !== 'algolia') { - validateSuggestionsNpm(toValue(query)) + watch( + [() => asyncData.data.value.suggestions, () => npmSuggestions.data.value.suggestions], + ([algoliaSuggestions, npmSuggestions]) => { + if (algoliaSuggestions.length || npmSuggestions.length) { + suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestions } + }, + { immediate: true }, + ) + + watch( + [ + () => asyncData.data.value?.packageAvailability, + () => npmSuggestions.data.value.packageAvailability, + ], + ([algoliaPackageAvailability, npmPackageAvailability]) => { + if (algoliaPackageAvailability || npmPackageAvailability) { + packageAvailability.value = algoliaPackageAvailability || npmPackageAvailability + } + }, + { immediate: true }, + ) + + if (import.meta.client && asyncData.data.value?.searchResponse.isStale) { + onMounted(() => { + asyncData.refresh() }) } From 1f6de931b1f5454cc7da4ca3b3dd4851096def8d Mon Sep 17 00:00:00 2001 From: Vordgi Date: Wed, 11 Feb 2026 00:34:07 +0000 Subject: [PATCH 04/11] fix: apply suggestion about check-package-exist --- app/composables/npm/useSearch.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/composables/npm/useSearch.ts b/app/composables/npm/useSearch.ts index 77820a472..7c623dbbc 100644 --- a/app/composables/npm/useSearch.ts +++ b/app/composables/npm/useSearch.ts @@ -339,18 +339,22 @@ export function useSearch( const { intent, name } = parseSuggestionIntent(q) let availability: { name: string; available: boolean } | null = null + const promises: Promise[] = [] + const trimmed = q.trim() if (isValidNewPackageName(trimmed)) { - checkPackageExists(trimmed) - .then(exists => { - if (trimmed === toValue(query).trim()) { - availability = { name: trimmed, available: !exists } - packageAvailability.value = availability - } - }) - .catch(() => { - availability = null - }) + promises.push( + checkPackageExists(trimmed) + .then(exists => { + if (trimmed === toValue(query).trim()) { + availability = { name: trimmed, available: !exists } + packageAvailability.value = availability + } + }) + .catch(() => { + availability = null + }), + ) } else { availability = null } @@ -368,8 +372,6 @@ export function useSearch( const wantOrg = intent === 'org' || intent === 'both' const wantUser = intent === 'user' || intent === 'both' - const promises: Promise[] = [] - if (wantOrg && existenceCache.value[`org:${lowerName}`] === undefined) { promises.push( checkOrgNpm(name) From d62de7c16e14e0269f29a7db47d22c3ee9699527 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Wed, 11 Feb 2026 01:00:47 +0000 Subject: [PATCH 05/11] fix: rename arg in use-search --- app/composables/npm/useSearch.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/composables/npm/useSearch.ts b/app/composables/npm/useSearch.ts index 7c623dbbc..bc3099e99 100644 --- a/app/composables/npm/useSearch.ts +++ b/app/composables/npm/useSearch.ts @@ -441,9 +441,9 @@ export function useSearch( watch( [() => asyncData.data.value.suggestions, () => npmSuggestions.data.value.suggestions], - ([algoliaSuggestions, npmSuggestions]) => { - if (algoliaSuggestions.length || npmSuggestions.length) { - suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestions + ([algoliaSuggestions, npmSuggestionsValue]) => { + if (algoliaSuggestions.length || npmSuggestionsValue.length) { + suggestions.value = algoliaSuggestions.length ? algoliaSuggestions : npmSuggestionsValue } }, { immediate: true }, From e181dd2b0ee511abf660b0754de72742f0b173d4 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Wed, 11 Feb 2026 01:58:47 +0000 Subject: [PATCH 06/11] fix: use query param for search provider instead of cookie --- app/components/Compare/PackageSelector.vue | 3 +- app/components/Header/SearchBox.vue | 18 +++++++--- .../SearchProviderToggle.client.vue | 34 ++++++++++++++----- app/composables/npm/useOrgPackages.ts | 10 ++++-- app/composables/npm/useSearch.ts | 33 ++++++++++-------- app/composables/npm/useUserPackages.ts | 29 ++++++++++------ app/composables/useSettings.ts | 9 +---- app/pages/index.vue | 6 ++-- app/pages/search.vue | 19 +++++++---- 9 files changed, 104 insertions(+), 57 deletions(-) diff --git a/app/components/Compare/PackageSelector.vue b/app/components/Compare/PackageSelector.vue index efcce9877..6475c9c3e 100644 --- a/app/components/Compare/PackageSelector.vue +++ b/app/components/Compare/PackageSelector.vue @@ -15,7 +15,8 @@ const inputValue = shallowRef('') const isInputFocused = shallowRef(false) // Use the shared search composable (supports both npm and Algolia providers) -const { data: searchData, status } = useSearch(inputValue, { size: 15 }) +const { searchProvider } = useSearchProvider() +const { data: searchData, status } = useSearch(inputValue, searchProvider, { size: 15 }) const isSearching = computed(() => status.value === 'pending') diff --git a/app/components/Header/SearchBox.vue b/app/components/Header/SearchBox.vue index 51e2c3567..993c2c105 100644 --- a/app/components/Header/SearchBox.vue +++ b/app/components/Header/SearchBox.vue @@ -15,7 +15,13 @@ const emit = defineEmits(['blur', 'focus']) const router = useRouter() const route = useRoute() -const { isAlgolia } = useSearchProvider() +// The actual search provider (from URL, used for API calls) +const searchProviderParam = computed(() => { + const p = normalizeSearchParam(route.query.p) + return p === 'npm' ? 'npm' : 'algolia' +}) +const { searchProvider } = useSearchProvider() +const searchProviderValue = computed(() => searchProviderParam.value || searchProvider.value) const isSearchFocused = shallowRef(false) @@ -35,7 +41,7 @@ function updateUrlQueryImpl(value: string) { return } if (route.name === 'search') { - router.replace({ query: { q: value || undefined } }) + router.replace({ query: { q: value || undefined, p: searchProviderValue.value } }) return } if (!value) { @@ -46,6 +52,7 @@ function updateUrlQueryImpl(value: string) { name: 'search', query: { q: value, + p: searchProviderValue.value, }, }) } @@ -54,9 +61,11 @@ const updateUrlQueryNpm = debounce(updateUrlQueryImpl, 250) const updateUrlQueryAlgolia = debounce(updateUrlQueryImpl, 80) const updateUrlQuery = Object.assign( - (value: string) => (isAlgolia.value ? updateUrlQueryAlgolia : updateUrlQueryNpm)(value), + (value: string) => + (searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm)(value), { - flush: () => (isAlgolia.value ? updateUrlQueryAlgolia : updateUrlQueryNpm).flush(), + flush: () => + (searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm).flush(), }, ) @@ -85,6 +94,7 @@ function handleSubmit() { name: 'search', query: { q: searchQuery.value, + p: searchProviderValue.value, }, }) } else { diff --git a/app/components/SearchProviderToggle.client.vue b/app/components/SearchProviderToggle.client.vue index bb1dff5d1..461d132ae 100644 --- a/app/components/SearchProviderToggle.client.vue +++ b/app/components/SearchProviderToggle.client.vue @@ -1,5 +1,12 @@