@@ -2,33 +2,12 @@ import type { NpmSearchResponse, NpmSearchResult, PackageMetaResponse } from '#s
22import { emptySearchResponse , metaToSearchResult } from './search-utils'
33import { mapWithConcurrency } from '#shared/utils/async'
44
5- /** Number of packages to fetch metadata for in the initial load */
6- const INITIAL_BATCH_SIZE = 50
7-
8- /** Max names per Algolia getObjects request */
9- const ALGOLIA_BATCH_SIZE = 1000
10-
11- export interface OrgPackagesResponse extends NpmSearchResponse {
12- /** Total number of packages in the org (may exceed objects.length before loadAll) */
13- totalPackages : number
14- /** All package names in the org (used by loadMore to know what to fetch next) */
15- allPackageNames : string [ ]
16- }
17-
18- function emptyOrgResponse ( ) : OrgPackagesResponse {
19- return {
20- ...emptySearchResponse ( ) ,
21- totalPackages : 0 ,
22- allPackageNames : [ ] ,
23- }
24- }
25-
265/**
27- * Fetch packages for an npm organization with progressive loading .
6+ * Fetch all packages for an npm organization.
287 *
298 * 1. Gets the authoritative package list from the npm registry (single request)
30- * 2. Fetches metadata for the first batch immediately (fast SSR )
31- * 3. Remaining packages are loaded on-demand via `loadAll()`
9+ * 2. Fetches metadata from Algolia by exact name (batched, max 1000 per request )
10+ * 3. Falls back to lightweight server-side package-meta lookups
3211 */
3312export function useOrgPackages ( orgName : MaybeRefOrGetter < string > ) {
3413 const route = useRoute ( )
@@ -38,22 +17,17 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
3817 if ( p === 'npm' || searchProvider . value === 'npm' ) return 'npm'
3918 return 'algolia'
4019 } )
41- const { getPackagesByNameSlice } = useAlgoliaSearch ( )
42-
43- const loadedObjects = shallowRef < NpmSearchResult [ ] > ( [ ] )
44-
45- // Promise lock — scoped inside the composable to avoid cross-instance sharing
46- let loadAllPromise : Promise < void > | null = null
20+ const { getPackagesByName } = useAlgoliaSearch ( )
4721
4822 const asyncData = useLazyAsyncData (
4923 ( ) => `org-packages:${ searchProviderValue . value } :${ toValue ( orgName ) } ` ,
5024 async ( { ssrContext } , { signal } ) => {
5125 const org = toValue ( orgName )
5226 if ( ! org ) {
53- return emptyOrgResponse ( )
27+ return emptySearchResponse ( )
5428 }
5529
56- // Get the authoritative package list from the npm registry
30+ // Get the authoritative package list from the npm registry (single request)
5731 let packageNames : string [ ]
5832 try {
5933 const { packages } = await $fetch < { packages : string [ ] ; count : number } > (
@@ -77,127 +51,32 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
7751 }
7852
7953 if ( packageNames . length === 0 ) {
80- loadedObjects . value = [ ]
81- return emptyOrgResponse ( )
54+ return emptySearchResponse ( )
8255 }
8356
84- const initialNames = packageNames . slice ( 0 , INITIAL_BATCH_SIZE )
85-
86- // Fetch metadata for first batch
87- let initialObjects : NpmSearchResult [ ] = [ ]
88-
57+ // Fetch metadata from Algolia (batched in chunks of 1000, parallel)
8958 if ( searchProviderValue . value === 'algolia' ) {
9059 try {
91- initialObjects = await getPackagesByNameSlice ( initialNames )
60+ const response = await getPackagesByName ( packageNames )
61+ if ( response . objects . length > 0 ) {
62+ return response
63+ }
9264 } catch {
93- // Fall through to npm fallback
65+ // Fall through to npm registry path
9466 }
9567 }
9668
9769 // Staleness guard
98- if ( toValue ( orgName ) !== org ) return emptyOrgResponse ( )
99-
100- // npm fallback for initial batch
101- if ( initialObjects . length === 0 ) {
102- const metaResults = await mapWithConcurrency (
103- initialNames ,
104- async name => {
105- try {
106- return await $fetch < PackageMetaResponse > (
107- `/api/registry/package-meta/${ encodePackageName ( name ) } ` ,
108- { signal } ,
109- )
110- } catch {
111- return null
112- }
113- } ,
114- 10 ,
115- )
116-
117- if ( toValue ( orgName ) !== org ) return emptyOrgResponse ( )
118-
119- initialObjects = metaResults
120- . filter ( ( meta ) : meta is PackageMetaResponse => meta !== null )
121- . map ( metaToSearchResult )
122- }
123-
124- loadedObjects . value = initialObjects
125-
126- return {
127- isStale : false ,
128- objects : initialObjects ,
129- total : initialObjects . length ,
130- totalPackages : packageNames . length ,
131- allPackageNames : packageNames ,
132- time : new Date ( ) . toISOString ( ) ,
133- } satisfies OrgPackagesResponse
134- } ,
135- { default : emptyOrgResponse } ,
136- )
137-
138- /** Read allPackageNames from async data (survives SSR→client hydration via Nuxt payload). */
139- function allPackageNames ( ) : string [ ] {
140- return asyncData . data . value ?. allPackageNames ?? [ ]
141- }
142-
143- /** Load the next batch of packages (default: 1 Algolia batch of 1000). */
144- async function loadMore ( count : number = ALGOLIA_BATCH_SIZE ) : Promise < void > {
145- const loadedSet = new Set ( loadedObjects . value . map ( o => o . package . name ) )
146- if ( loadedSet . size >= allPackageNames ( ) . length ) return
147-
148- // Reuse in-flight promise to prevent duplicate fetches
149- if ( loadAllPromise ) {
150- await loadAllPromise
151- return
152- }
153-
154- loadAllPromise = _doLoadMore ( count )
155- try {
156- await loadAllPromise
157- } finally {
158- loadAllPromise = null
159- }
160- }
161-
162- /** Load ALL remaining packages (used when filters need the full dataset). */
163- async function loadAll ( ) : Promise < void > {
164- const remaining = allPackageNames ( ) . length - loadedObjects . value . length
165- if ( remaining <= 0 ) return
166- await loadMore ( remaining )
167- }
168-
169- async function _doLoadMore ( count : number ) : Promise < void > {
170- const names = allPackageNames ( )
171- const current = loadedObjects . value
172- const loadedSet = new Set ( current . map ( o => o . package . name ) )
173- const remainingNames = names . filter ( n => ! loadedSet . has ( n ) ) . slice ( 0 , count )
174- if ( remainingNames . length === 0 ) return
175-
176- const org = toValue ( orgName )
177- let newObjects : NpmSearchResult [ ] = [ ]
178-
179- if ( searchProviderValue . value === 'algolia' ) {
180- const batches : string [ ] [ ] = [ ]
181- for ( let i = 0 ; i < remainingNames . length ; i += ALGOLIA_BATCH_SIZE ) {
182- batches . push ( remainingNames . slice ( i , i + ALGOLIA_BATCH_SIZE ) )
183- }
70+ if ( toValue ( orgName ) !== org ) return emptySearchResponse ( )
18471
185- const results = await Promise . allSettled ( batches . map ( batch => getPackagesByNameSlice ( batch ) ) )
186-
187- if ( toValue ( orgName ) !== org ) return
188-
189- for ( const result of results ) {
190- if ( result . status === 'fulfilled' ) {
191- newObjects . push ( ...result . value )
192- }
193- }
194- } else {
72+ // npm fallback: fetch lightweight metadata via server proxy
19573 const metaResults = await mapWithConcurrency (
196- remainingNames ,
74+ packageNames ,
19775 async name => {
19876 try {
19977 return await $fetch < PackageMetaResponse > (
20078 `/api/registry/package-meta/${ encodePackageName ( name ) } ` ,
79+ { signal } ,
20180 )
20281 } catch {
20382 return null
@@ -206,33 +85,19 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
20685 10 ,
20786 )
20887
209- if ( toValue ( orgName ) !== org ) return
210-
211- newObjects = metaResults
88+ const results : NpmSearchResult [ ] = metaResults
21289 . filter ( ( meta ) : meta is PackageMetaResponse => meta !== null )
21390 . map ( metaToSearchResult )
214- }
21591
216- if ( newObjects . length > 0 ) {
217- const deduped = newObjects . filter ( o => ! loadedSet . has ( o . package . name ) )
218- const all = [ ...current , ...deduped ]
219- loadedObjects . value = all
220-
221- // Update asyncData so the page sees the new objects
222- asyncData . data . value = {
92+ return {
22393 isStale : false ,
224- objects : all ,
225- total : all . length ,
226- totalPackages : names . length ,
227- allPackageNames : names ,
94+ objects : results ,
95+ total : results . length ,
22896 time : new Date ( ) . toISOString ( ) ,
229- }
230- }
231- }
97+ } satisfies NpmSearchResponse
98+ } ,
99+ { default : emptySearchResponse } ,
100+ )
232101
233- return {
234- ...asyncData ,
235- loadMore,
236- loadAll,
237- }
102+ return asyncData
238103}
0 commit comments