Skip to content

Commit c138141

Browse files
Adebesin-Cellserhalpghostdevvclaudeautofix-ci[bot]
authored
feat: progressive loading for org packages (#1953)
Co-authored-by: Philippe Serhal <philippe.serhal@gmail.com> Co-authored-by: Willow (GHOST) <git@willow.sh> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 42fb7b0 commit c138141

3 files changed

Lines changed: 47 additions & 14 deletions

File tree

app/composables/npm/useAlgoliaSearch.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,9 @@ export function useAlgoliaSearch() {
217217
}
218218
}
219219

220-
/** Fetch metadata for specific packages by exact name using Algolia's getObjects API. */
221-
async function getPackagesByName(packageNames: string[]): Promise<NpmSearchResponse> {
222-
if (packageNames.length === 0) {
223-
return { isStale: false, objects: [], total: 0, time: new Date().toISOString() }
224-
}
220+
/** Fetch metadata for a single batch of packages (max 1000) by exact name. */
221+
async function getPackagesByNameSlice(names: string[]): Promise<NpmSearchResult[]> {
222+
if (names.length === 0) return []
225223

226224
const response = await $fetch<{ results: (AlgoliaHit | null)[] }>(
227225
`https://${algolia.appId}-dsn.algolia.net/1/indexes/*/objects`,
@@ -232,7 +230,7 @@ export function useAlgoliaSearch() {
232230
'x-algolia-application-id': algolia.appId,
233231
},
234232
body: {
235-
requests: packageNames.map(name => ({
233+
requests: names.map(name => ({
236234
indexName,
237235
objectID: name,
238236
attributesToRetrieve: ATTRIBUTES_TO_RETRIEVE,
@@ -241,11 +239,41 @@ export function useAlgoliaSearch() {
241239
},
242240
)
243241

244-
const hits = response.results.filter((r): r is AlgoliaHit => r !== null && 'name' in r)
242+
return response.results
243+
.filter((r): r is AlgoliaHit => r !== null && 'name' in r)
244+
.map(hitToSearchResult)
245+
}
246+
247+
/** Fetch metadata for specific packages by exact name using Algolia's getObjects API. */
248+
async function getPackagesByName(packageNames: string[]): Promise<NpmSearchResponse> {
249+
if (packageNames.length === 0) {
250+
return { isStale: false, objects: [], total: 0, time: new Date().toISOString() }
251+
}
252+
253+
// Algolia getObjects has a limit of 1000 objects per request, so batch if needed
254+
const BATCH_SIZE = 1000
255+
const batches: string[][] = []
256+
for (let i = 0; i < packageNames.length; i += BATCH_SIZE) {
257+
batches.push(packageNames.slice(i, i + BATCH_SIZE))
258+
}
259+
260+
// Fetch batches with concurrency limit to avoid overwhelming the API
261+
const CONCURRENCY = 3
262+
const allObjects: NpmSearchResult[] = []
263+
for (let i = 0; i < batches.length; i += CONCURRENCY) {
264+
const chunk = batches.slice(i, i + CONCURRENCY)
265+
const results = await Promise.all(chunk.map(batch => getPackagesByNameSlice(batch)))
266+
for (const result of results) {
267+
for (const pkg of result) {
268+
allObjects.push(pkg)
269+
}
270+
}
271+
}
272+
245273
return {
246274
isStale: false,
247-
objects: hits.map(hitToSearchResult),
248-
total: hits.length,
275+
objects: allObjects,
276+
total: allObjects.length,
249277
time: new Date().toISOString(),
250278
}
251279
}

app/composables/npm/useOrgPackages.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import type { NpmSearchResponse, NpmSearchResult, PackageMetaResponse } from '#shared/types'
2+
import { emptySearchResponse, metaToSearchResult } from './search-utils'
3+
import { mapWithConcurrency } from '#shared/utils/async'
4+
15
/**
26
* Fetch all packages for an npm organization.
37
*
48
* 1. Gets the authoritative package list from the npm registry (single request)
5-
* 2. Fetches metadata from Algolia by exact name (single request)
9+
* 2. Fetches metadata from Algolia by exact name (batched, max 1000 per request)
610
* 3. Falls back to lightweight server-side package-meta lookups
711
*/
812
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
@@ -32,7 +36,6 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
3236
)
3337
packageNames = packages
3438
} catch (err) {
35-
// Check if this is a 404 (org not found)
3639
if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
3740
const error = createError({
3841
statusCode: 404,
@@ -44,15 +47,14 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
4447
}
4548
throw error
4649
}
47-
// For other errors (network, etc.), return empty array to be safe
4850
packageNames = []
4951
}
5052

5153
if (packageNames.length === 0) {
5254
return emptySearchResponse()
5355
}
5456

55-
// Fetch metadata + downloads from Algolia (single request via getObjects)
57+
// Fetch metadata from Algolia (batched in chunks of 1000, parallel)
5658
if (searchProviderValue.value === 'algolia') {
5759
try {
5860
const response = await getPackagesByName(packageNames)
@@ -64,6 +66,9 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
6466
}
6567
}
6668

69+
// Staleness guard
70+
if (toValue(orgName) !== org) return emptySearchResponse()
71+
6772
// npm fallback: fetch lightweight metadata via server proxy
6873
const metaResults = await mapWithConcurrency(
6974
packageNames,

i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@
935935
"failed_to_load": "Failed to load organization packages",
936936
"no_match": "No packages match \"{query}\"",
937937
"not_found": "Organization not found",
938-
"not_found_message": "The organization \"{'@'}{name}\" does not exist on npm"
938+
"not_found_message": "The organization {'@'}{name} does not exist on npm"
939939
}
940940
},
941941
"user": {

0 commit comments

Comments
 (0)