Skip to content

Commit 5a16af4

Browse files
Adebesin-Cellclaude
andcommitted
refactor: simplify org packages to fetch all with batched Algolia
Remove progressive loading in favor of fetching all packages upfront using batched Algolia requests (max 1000 per batch, parallel). This is the actual fix for the @types hang — batched parallel fetches complete in ~1-2s vs the original sequential approach. PackageList already handles display (pagination, infinite scroll, virtual scrolling) so no additional visibility layer is needed. Removes OrgPackagesResponse, totalPackages, isTruncated, hasMore, isLoadingMore, loadAll, cache, and the showing_packages i18n key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5686329 commit 5a16af4

File tree

7 files changed

+109
-230
lines changed

7 files changed

+109
-230
lines changed

.claude/skills/vite-plus

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../node_modules/vite-plus/skills/vite-plus

.trae/skills/vite-plus

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../node_modules/vite-plus/skills/vite-plus

CLAUDE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!--VITE PLUS START-->
2+
3+
# Using Vite+, the Unified Toolchain for the Web
4+
5+
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`.
6+
7+
## Vite+ Workflow
8+
9+
`vp` is a global binary that handles the full development lifecycle. Run `vp help` to print a list of commands and `vp <command> --help` for information about a specific command.
10+
11+
### Start
12+
13+
- create - Create a new project from a template
14+
- migrate - Migrate an existing project to Vite+
15+
- config - Configure hooks and agent integration
16+
- staged - Run linters on staged files
17+
- install (`i`) - Install dependencies
18+
- env - Manage Node.js versions
19+
20+
### Develop
21+
22+
- dev - Run the development server
23+
- check - Run format, lint, and TypeScript type checks
24+
- lint - Lint code
25+
- fmt - Format code
26+
- test - Run tests
27+
28+
### Execute
29+
30+
- run - Run monorepo tasks
31+
- exec - Execute a command from local `node_modules/.bin`
32+
- dlx - Execute a package binary without installing it as a dependency
33+
- cache - Manage the task cache
34+
35+
### Build
36+
37+
- build - Build for production
38+
- pack - Build libraries
39+
- preview - Preview production build
40+
41+
### Manage Dependencies
42+
43+
Vite+ automatically detects and wraps the underlying package manager such as pnpm, npm, or Yarn through the `packageManager` field in `package.json` or package manager-specific lockfiles.
44+
45+
- add - Add packages to dependencies
46+
- remove (`rm`, `un`, `uninstall`) - Remove packages from dependencies
47+
- update (`up`) - Update packages to latest versions
48+
- dedupe - Deduplicate dependencies
49+
- outdated - Check for outdated packages
50+
- list (`ls`) - List installed packages
51+
- why (`explain`) - Show why a package is installed
52+
- info (`view`, `show`) - View package information from the registry
53+
- link (`ln`) / unlink - Manage local package links
54+
- pm - Forward a command to the package manager
55+
56+
### Maintain
57+
58+
- upgrade - Update `vp` itself to the latest version
59+
60+
These commands map to their corresponding tools. For example, `vp dev --port 3000` runs Vite's dev server and works the same as Vite. `vp test` runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using `vp --version`. This is useful when researching documentation, features, and bugs.
61+
62+
## Common Pitfalls
63+
64+
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
65+
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. They do not exist. Use `vp test` and `vp lint` instead.
66+
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vp test` command, run it using `vp run test`.
67+
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
68+
- **Use Vite+ wrappers for one-off binaries:** Use `vp dlx` instead of package-manager-specific `dlx`/`npx` commands.
69+
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
70+
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.
71+
72+
## Review Checklist for Agents
73+
74+
- [ ] Run `vp install` after pulling remote changes and before getting started.
75+
- [ ] Run `vp check` and `vp test` to validate changes.
76+
<!--VITE PLUS END-->
Lines changed: 26 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,16 @@
1-
import type { NpmSearchResponse, NpmSearchResult, PackageMetaResponse } from '#shared/types'
1+
import type { NpmSearchResult, PackageMetaResponse } from '#shared/types'
22
import { emptySearchResponse, metaToSearchResult } from './search-utils'
33
import { 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 */
96
const ALGOLIA_BATCH_SIZE = 1000
107

11-
export interface OrgPackagesResponse extends NpmSearchResponse {
12-
/** Total number of packages in the org (may exceed objects.length if not all loaded yet) */
13-
totalPackages: number
14-
/** Whether there are more packages that haven't been loaded yet */
15-
isTruncated: boolean
16-
}
17-
18-
function emptyOrgResponse(): OrgPackagesResponse {
19-
return {
20-
...emptySearchResponse(),
21-
totalPackages: 0,
22-
isTruncated: false,
23-
}
24-
}
25-
268
/**
27-
* Fetch packages for an npm organization with progressive loading.
9+
* Fetch all packages for an npm organization.
2810
*
2911
* 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()`
12+
* 2. Fetches metadata from Algolia in batches (max 1000 per request)
13+
* 3. Falls back to lightweight server-side package-meta lookups
3214
*/
3315
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
3416
const route = useRoute()
@@ -40,30 +22,12 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
4022
})
4123
const { getPackagesByNameSlice } = useAlgoliaSearch()
4224

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-
6125
const asyncData = useLazyAsyncData(
6226
() => `org-packages:${searchProviderValue.value}:${toValue(orgName)}`,
6327
async ({ ssrContext }, { signal }) => {
6428
const org = toValue(orgName)
6529
if (!org) {
66-
return emptyOrgResponse()
30+
return emptySearchResponse()
6731
}
6832

6933
// Get the authoritative package list from the npm registry
@@ -90,31 +54,34 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
9054
}
9155

9256
if (packageNames.length === 0) {
93-
cache.value = { org, allNames: [], objects: [], totalPackages: 0 }
94-
return emptyOrgResponse()
57+
return emptySearchResponse()
9558
}
9659

97-
const totalPackages = packageNames.length
98-
const initialNames = packageNames.slice(0, INITIAL_BATCH_SIZE)
99-
100-
// Fetch metadata for first batch
101-
let initialObjects: NpmSearchResult[] = []
60+
// Fetch metadata from Algolia in batches
61+
let objects: NpmSearchResult[] = []
10262

10363
if (searchProviderValue.value === 'algolia') {
10464
try {
105-
initialObjects = await getPackagesByNameSlice(initialNames)
65+
const batches: string[][] = []
66+
for (let i = 0; i < packageNames.length; i += ALGOLIA_BATCH_SIZE) {
67+
batches.push(packageNames.slice(i, i + ALGOLIA_BATCH_SIZE))
68+
}
69+
70+
const results = await Promise.all(batches.map(batch => getPackagesByNameSlice(batch)))
71+
72+
objects = results.flat()
10673
} catch {
10774
// Fall through to npm fallback
10875
}
10976
}
11077

11178
// Staleness guard
112-
if (toValue(orgName) !== org) return emptyOrgResponse()
79+
if (toValue(orgName) !== org) return emptySearchResponse()
11380

114-
// npm fallback for initial batch
115-
if (initialObjects.length === 0) {
81+
// npm fallback
82+
if (objects.length === 0) {
11683
const metaResults = await mapWithConcurrency(
117-
initialNames,
84+
packageNames,
11885
async name => {
11986
try {
12087
return await $fetch<PackageMetaResponse>(
@@ -128,154 +95,22 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
12895
10,
12996
)
13097

131-
if (toValue(orgName) !== org) return emptyOrgResponse()
98+
if (toValue(orgName) !== org) return emptySearchResponse()
13299

133-
initialObjects = metaResults
100+
objects = metaResults
134101
.filter((meta): meta is PackageMetaResponse => meta !== null)
135102
.map(metaToSearchResult)
136103
}
137104

138-
cache.value = {
139-
org,
140-
allNames: packageNames,
141-
objects: initialObjects,
142-
totalPackages,
143-
}
144-
145105
return {
146106
isStale: false,
147-
objects: initialObjects,
148-
total: initialObjects.length,
149-
totalPackages,
150-
isTruncated: packageNames.length > initialObjects.length,
107+
objects,
108+
total: objects.length,
151109
time: new Date().toISOString(),
152-
} satisfies OrgPackagesResponse
153-
},
154-
{ default: emptyOrgResponse },
155-
)
156-
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-
}
243110
}
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
255111
},
112+
{ default: emptySearchResponse },
256113
)
257114

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-
}
115+
return asyncData
281116
}

0 commit comments

Comments
 (0)