@@ -2,33 +2,27 @@ 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 = 250
7-
85/** Max names per Algolia getObjects request */
96const ALGOLIA_BATCH_SIZE = 1000
107
118export interface OrgPackagesResponse extends NpmSearchResponse {
12- /** Total number of packages in the org (may exceed objects.length if not all loaded yet) */
9+ /** Total number of packages in the org */
1310 totalPackages : number
14- /** Whether there are more packages that haven't been loaded yet */
15- isTruncated : boolean
1611}
1712
1813function emptyOrgResponse ( ) : OrgPackagesResponse {
1914 return {
2015 ...emptySearchResponse ( ) ,
2116 totalPackages : 0 ,
22- isTruncated : false ,
2317 }
2418}
2519
2620/**
27- * Fetch packages for an npm organization with progressive loading .
21+ * Fetch all packages for an npm organization.
2822 *
2923 * 1. Gets the authoritative package list from the npm registry (single request)
30- * 2. Fetches metadata for the first batch immediately
31- * 3. Remaining packages are loaded on-demand via `loadAll()`
24+ * 2. Fetches metadata from Algolia in batches (max 1000 per request)
25+ * 3. Falls back to lightweight server-side package-meta lookups
3226 */
3327export function useOrgPackages ( orgName : MaybeRefOrGetter < string > ) {
3428 const route = useRoute ( )
@@ -40,24 +34,6 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
4034 } )
4135 const { getPackagesByNameSlice } = useAlgoliaSearch ( )
4236
43- // --- Progressive loading state ---
44- const cache = shallowRef < {
45- org : string
46- allNames : string [ ]
47- objects : NpmSearchResult [ ]
48- totalPackages : number
49- } | null > ( null )
50-
51- const isLoadingMore = shallowRef ( false )
52-
53- const hasMore = computed ( ( ) => {
54- if ( ! cache . value ) return false
55- return cache . value . objects . length < cache . value . allNames . length
56- } )
57-
58- // Promise lock to prevent duplicate loadAll calls
59- let loadAllPromise : Promise < void > | null = null
60-
6137 const asyncData = useLazyAsyncData (
6238 ( ) => `org-packages:${ searchProviderValue . value } :${ toValue ( orgName ) } ` ,
6339 async ( { ssrContext } , { signal } ) => {
@@ -90,19 +66,24 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
9066 }
9167
9268 if ( packageNames . length === 0 ) {
93- cache . value = { org, allNames : [ ] , objects : [ ] , totalPackages : 0 }
9469 return emptyOrgResponse ( )
9570 }
9671
9772 const totalPackages = packageNames . length
98- const initialNames = packageNames . slice ( 0 , INITIAL_BATCH_SIZE )
9973
100- // Fetch metadata for first batch
101- let initialObjects : NpmSearchResult [ ] = [ ]
74+ // Fetch metadata from Algolia in batches
75+ let objects : NpmSearchResult [ ] = [ ]
10276
10377 if ( searchProviderValue . value === 'algolia' ) {
10478 try {
105- initialObjects = await getPackagesByNameSlice ( initialNames )
79+ const batches : string [ ] [ ] = [ ]
80+ for ( let i = 0 ; i < packageNames . length ; i += ALGOLIA_BATCH_SIZE ) {
81+ batches . push ( packageNames . slice ( i , i + ALGOLIA_BATCH_SIZE ) )
82+ }
83+
84+ const results = await Promise . all ( batches . map ( batch => getPackagesByNameSlice ( batch ) ) )
85+
86+ objects = results . flat ( )
10687 } catch {
10788 // Fall through to npm fallback
10889 }
@@ -111,10 +92,10 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
11192 // Staleness guard
11293 if ( toValue ( orgName ) !== org ) return emptyOrgResponse ( )
11394
114- // npm fallback for initial batch
115- if ( initialObjects . length === 0 ) {
95+ // npm fallback
96+ if ( objects . length === 0 ) {
11697 const metaResults = await mapWithConcurrency (
117- initialNames ,
98+ packageNames ,
11899 async name => {
119100 try {
120101 return await $fetch < PackageMetaResponse > (
@@ -130,152 +111,21 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
130111
131112 if ( toValue ( orgName ) !== org ) return emptyOrgResponse ( )
132113
133- initialObjects = metaResults
114+ objects = metaResults
134115 . filter ( ( meta ) : meta is PackageMetaResponse => meta !== null )
135116 . map ( metaToSearchResult )
136117 }
137118
138- cache . value = {
139- org,
140- allNames : packageNames ,
141- objects : initialObjects ,
142- totalPackages,
143- }
144-
145119 return {
146120 isStale : false ,
147- objects : initialObjects ,
148- total : initialObjects . length ,
121+ objects,
122+ total : objects . length ,
149123 totalPackages,
150- isTruncated : packageNames . length > initialObjects . length ,
151124 time : new Date ( ) . toISOString ( ) ,
152125 } satisfies OrgPackagesResponse
153126 } ,
154127 { default : emptyOrgResponse } ,
155128 )
156129
157- /** Load all remaining packages that weren't fetched in the initial batch */
158- async function loadAll ( ) : Promise < void > {
159- if ( ! hasMore . value ) return
160-
161- // Reuse existing promise if already running
162- if ( loadAllPromise ) {
163- await loadAllPromise
164- return
165- }
166-
167- loadAllPromise = _doLoadAll ( )
168- try {
169- await loadAllPromise
170- } finally {
171- loadAllPromise = null
172- }
173- }
174-
175- async function _doLoadAll ( ) : Promise < void > {
176- const currentCache = cache . value
177- if ( ! currentCache || currentCache . objects . length >= currentCache . allNames . length ) return
178-
179- const org = currentCache . org
180- isLoadingMore . value = true
181-
182- try {
183- const remainingNames = currentCache . allNames . slice ( currentCache . objects . length )
184-
185- if ( searchProviderValue . value === 'algolia' ) {
186- // Split remaining into batches and fetch in parallel
187- const batches : string [ ] [ ] = [ ]
188- for ( let i = 0 ; i < remainingNames . length ; i += ALGOLIA_BATCH_SIZE ) {
189- batches . push ( remainingNames . slice ( i , i + ALGOLIA_BATCH_SIZE ) )
190- }
191-
192- const results = await Promise . allSettled (
193- batches . map ( batch => getPackagesByNameSlice ( batch ) ) ,
194- )
195-
196- if ( toValue ( orgName ) !== org ) return
197-
198- const newObjects : NpmSearchResult [ ] = [ ]
199- for ( const result of results ) {
200- if ( result . status === 'fulfilled' ) {
201- newObjects . push ( ...result . value )
202- }
203- }
204-
205- if ( newObjects . length > 0 ) {
206- const existingNames = new Set ( currentCache . objects . map ( o => o . package . name ) )
207- const deduped = newObjects . filter ( o => ! existingNames . has ( o . package . name ) )
208- cache . value = {
209- ...currentCache ,
210- objects : [ ...currentCache . objects , ...deduped ] ,
211- }
212- }
213- } else {
214- // npm fallback: fetch with concurrency
215- const metaResults = await mapWithConcurrency (
216- remainingNames ,
217- async name => {
218- try {
219- return await $fetch < PackageMetaResponse > (
220- `/api/registry/package-meta/${ encodePackageName ( name ) } ` ,
221- )
222- } catch {
223- return null
224- }
225- } ,
226- 10 ,
227- )
228-
229- if ( toValue ( orgName ) !== org ) return
230-
231- const newObjects = metaResults
232- . filter ( ( meta ) : meta is PackageMetaResponse => meta !== null )
233- . map ( metaToSearchResult )
234-
235- if ( newObjects . length > 0 ) {
236- const existingNames = new Set ( currentCache . objects . map ( o => o . package . name ) )
237- const deduped = newObjects . filter ( o => ! existingNames . has ( o . package . name ) )
238- cache . value = {
239- ...currentCache ,
240- objects : [ ...currentCache . objects , ...deduped ] ,
241- }
242- }
243- }
244- } finally {
245- isLoadingMore . value = false
246- }
247- }
248-
249- // Reset cache when provider changes
250- watch (
251- ( ) => searchProviderValue . value ,
252- ( ) => {
253- cache . value = null
254- loadAllPromise = null
255- } ,
256- )
257-
258- // Computed data that prefers cache
259- const data = computed < OrgPackagesResponse | null > ( ( ) => {
260- const org = toValue ( orgName )
261- if ( cache . value && cache . value . org === org ) {
262- return {
263- isStale : false ,
264- objects : cache . value . objects ,
265- total : cache . value . objects . length ,
266- totalPackages : cache . value . totalPackages ,
267- isTruncated : cache . value . objects . length < cache . value . allNames . length ,
268- time : new Date ( ) . toISOString ( ) ,
269- }
270- }
271- return asyncData . data . value
272- } )
273-
274- return {
275- ...asyncData ,
276- data,
277- isLoadingMore,
278- hasMore,
279- loadAll,
280- }
130+ return asyncData
281131}
0 commit comments