feat: progressive loading for org packages#1953
feat: progressive loading for org packages#1953Adebesin-Cell wants to merge 18 commits intonpmx-dev:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds batched Algolia lookups and a new slice API, implements progressive/org-aware package loading with staleness guards and in‑flight deduplication (exposing loadAll), updates visible-items to support async expand hooks, adapts the org page to progressive loading, and adds Vite+ skill references and documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant OrgPage as "Org Page (Client UI)"
participant useOrg as "useOrgPackages"
participant Algolia
participant Server as "Server (package-meta)"
Client->>OrgPage: navigate to org page (orgName)
OrgPage->>useOrg: useOrgPackages(orgName)
Note over useOrg: capture org, fetch initial slice via getPackagesByNameSlice
useOrg->>Algolia: getPackagesByNameSlice(batch 1)
Algolia-->>useOrg: slice results
useOrg-->>OrgPage: return initial asyncData (initialObjects, totalPackages)
OrgPage->>useVisible: create visible list (onExpand -> useOrg.loadAll)
alt user expands / onExpand triggered
OrgPage->>useOrg: loadAll()
Note over useOrg: partition remainingNames into ALGOLIA_BATCH_SIZE slices
par concurrent slice fetches
useOrg->>Algolia: getPackagesByNameSlice(batch N)
Algolia-->>useOrg: results
and
useOrg->>Algolia: getPackagesByNameSlice(batch M)
Algolia-->>useOrg: results
end
useOrg->>useOrg: dedupe & append loadedObjects
useOrg->>useOrg: re-check org staleness
alt some metadata missing
useOrg->>Server: fetch package-meta fallback for remaining names
Server-->>useOrg: fallback results
end
useOrg-->>OrgPage: update asyncData with appended objects
end
Possibly Related PRs
Suggested Reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3cf16238-f272-4a67-8b0c-bc06ec8bf13a
📒 Files selected for processing (3)
app/composables/npm/useAlgoliaSearch.tsapp/composables/npm/useOrgPackages.tsapp/pages/org/[org].vue
Instead of capping at 1000 packages and blocking on all metadata, load the first 250 immediately and fetch the rest on demand. Filtering/sorting triggers loadAll() so client-side search still works on the full set.
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/composables/npm/useOrgPackages.ts (1)
249-256: Consider cancelling in-flight operations when the provider changes.The watch resets the cache and promise lock but doesn't cancel any in-flight
loadAll()operations. If a user toggles the search provider while loading is in progress, the old operation may complete and update the cache after the reset, causing inconsistent state.The existing staleness guard on
orgName(line 196) doesn't protect against provider changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eeb7210c-a4af-4e08-ad83-232fa11b004d
📒 Files selected for processing (5)
app/composables/npm/useAlgoliaSearch.tsapp/composables/npm/useOrgPackages.tsapp/pages/org/[org].vuei18n/locales/en.jsoni18n/schema.json
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/composables/npm/useAlgoliaSearch.ts (1)
217-242: Consider adding a defensive guard for the documented 1000-item limit.The JSDoc states "max 1000" but the function doesn't enforce this constraint. While callers (e.g.,
useOrgPackages.ts) correctly batch inputs to 1000 items before calling, a defensive check would make the contract self-enforcing and catch accidental misuse.🛡️ Optional defensive check
/** Fetch metadata for a single batch of packages (max 1000) by exact name. */ async function getPackagesByNameSlice(names: string[]): Promise<NpmSearchResult[]> { if (names.length === 0) return [] + if (names.length > 1000) { + throw new Error(`getPackagesByNameSlice: batch size ${names.length} exceeds Algolia limit of 1000`) + } const response = await $fetch<{ results: (AlgoliaHit | null)[] }>(
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fc7cae55-8a67-432f-a21d-e8abd23f16d9
📒 Files selected for processing (3)
app/composables/npm/useAlgoliaSearch.tsi18n/locales/en.jsoni18n/schema.json
✅ Files skipped from review due to trivial changes (2)
- i18n/locales/en.json
- i18n/schema.json
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 509b1c68-959d-48d1-aafe-959b9c10edfb
📒 Files selected for processing (4)
app/composables/npm/useOrgPackages.tsapp/pages/org/[org].vuei18n/locales/en.jsoni18n/schema.json
✅ Files skipped from review due to trivial changes (1)
- i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
- i18n/schema.json
ghostdevv
left a comment
There was a problem hiding this comment.
I couldn't find the load more button? Also does the load more load every package, or just another 250?
i18n/locales/en.json
Outdated
| "no_match": "No packages match \"{query}\"", | ||
| "not_found": "Organization not found", | ||
| "not_found_message": "The organization \"{'@'}{name}\" does not exist on npm" | ||
| "not_found_message": "The organization \"{'@'}{name}\" does not exist on npm", |
There was a problem hiding this comment.
how come there is backticks and {'@'}?
There was a problem hiding this comment.
Good catch 👏, removed the quotes.
There was a problem hiding this comment.
The @ is vue-i18n’s literal escape syntax. @ is a reserved character used for interpolation (see https://vue-i18n.intlify.dev/guide/essentials/syntax.html#special-characters), so writing @types directly would try to resolve a key called types instead of rendering the @ symbol.
It follows the same pattern used across all our locale files (e.g., atmosphere (@{handle}), Include @types in install, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hasMore now combines both signals: items hidden by useVisibleItems AND packages not yet fetched from the server. This fixes the missing load-more button when the initial batch size equals the visibility limit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extend useVisibleItems with onExpand callback + partial load support - useOrgPackages: fetch first 50 on SSR, loadMore(1000) per page, loadAll() only when filters need the full dataset - Track remaining by name (not position) for retry safety - Pagination shows real total (11,397) while loading incrementally Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| import { mapWithConcurrency } from '#shared/utils/async' | ||
|
|
||
| /** Number of packages to fetch metadata for in the initial load */ | ||
| const INITIAL_BATCH_SIZE = 50 |
There was a problem hiding this comment.
Why only 50? This is a very fast query. Are you limiting the initial data fetch to get a neat page of 50 packages displayed in the UI? That's convenient for the UI but inefficient for the network call budget.
There was a problem hiding this comment.
Ran some benchmarks against the Algolia getObjects endpoint for @types (11,397 packages):
| Initial batch size | Algolia response time (avg of 3 runs) |
|---|---|
| 50 | ~680ms |
| 100 | ~1,030ms |
| 150 | ~1,120ms |
| 200 | ~1,065ms |
| 250 | ~1,170ms |
| 500 | ~1,470ms |
| 1,000 | ~2,000ms |
This is a single Algolia getObjects POST during SSR. It blocks the HTML response. The registry fetch for the package names list is ~20ms (cached), so Algolia is the bottleneck.
50 to 100 adds ~350ms but doubles the initial content. 100 to 250 adds ~140ms more, but the response is already over a second. The default page size is 25, so 100 gives users 4 pages of content on first paint without needing a second fetch.
I can bump it to 100. It is a good balance between content density and SSR speed. The remaining packages load incrementally (1,000 per batch) as the user scrolls or paginates, or all at once when they filter or sort.
That’s weird. I did test locally, and loading more consistently triggers when you scroll to the end. If you switch to table view, you can see it paginates correctly with the total package count showing (screenshot attached).
I’ll look into why the infinite scroll load-more isn’t triggering on the preview deployment and fix it. |
shallowRef state doesn't transfer from SSR to client, so loadMore() had no package names to work with after hydration. Fix by including allPackageNames in the async data response (transferred via Nuxt payload) and reading from it instead of a separate shallowRef. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
It doesnt make much sense to split this view into pages this way while this view is a sorted list of unsorted data. That is, we get a list of all packages, and then the view we present is always sorted. Yet we are not currently sorting the data in the query. So what good is showing me the top downloaded package from a random subset of 50 packages out of 100, 500, 1k+ or whatever? I think the solution here is to query all data before rendering the page. That will still be significantly faster than the existing behavior, and is a much simpler fix. My feedback to you here is to first solve the problem much more simply. It is an extremely low bar to improve the perf here, and the only reason the perf is impacted is because it is hitting the much slower fallback codepath due to the 400 that is hit on the fast path, see #2507 The actual needed fix is batching the network calls to get around the 1k package limit, not turning this view into a paged view. |
There was a problem hiding this comment.
The issue with not fetching the next "page" on scrolling to the bottom is fixed 👍
But if you test it now you'll see what I mean in #1953 (comment)
Lets say I want to see the top downloaded package of sindresorhus org. On first load I will just see whatever comes from the first 50 results. I'll think "hmm, i didnt expect that package to be first? oh well" because as a user I am seeing a sorted list but its unclear to me that its only a subset of 50. Sure it says its showing 50 of 1600, but surely its showing me the top 50 based on my sort choice? (it is not)
But imagine I'm a very Smart user, and I know exactly how the implementatino of this sorted view works! I will simply scroll to the bottom of the list of 50! Currently, it fetches the next 1k, re-sorts the list right? I scroll back up to see what the new top one is. Now I know what the top downloaded package of the first 1050 packages are. Now I want to see the list with all of them sorted! So I scroll down, through 1050 items, to trigger it to fetch the next 1k... I rinse and repeat for however many thousand I need to get through.
My point is that the way this page is presented to the user as pre-sorted makes dynamic pagination here feel really bad as UX
I did approach the issue with batching at first. My commit fa6c5908 added Algolia batching to handle the 1k getObjects limit (the root cause of the 400 from #2507), though it was sequential and capped at 1000 packages. The 6e49401a removed the cap and made batches parallel, which is the actual performance fix. No more falling into the slow npm fallback path that was causing the hang. The progressive loading layer (first 50, load more on scroll, load all on filter) came later based on #1953 (review), suggesting we adopt the useVisibleItems composable from #2395. I agree that sorting on a partial dataset is misleading and that the batched Algolia path is fast enough to load everything upfront. I am happy to simplify back to "batch-fetch everything, let PackageList handle display." But since the progressive approach came from review feedback, it would be good to get @ghostdevv's take before I strip it out. |
|
ok i've mostly skimmed the diff so let me know if i misunderstood anything. i think @ghostdevv was suggesting However, going back to the root problem, i'm not sure we need all of this. As far as I understand, the root problem is that we send too many packages to Algolia in one request so we get an error back. The solution to that is to batch the requests, ideally with a concurrency limit (i.e. so we don't just If we do that server-side, the UI receives the full batch as far as i understand and renders it in its entirety (no paging). unrelated to the Algolia limit, it may be nicer UX to page those client-side one day. But that feels like something which needs a discussion first. So my two suggested actions are:
|
|
@43081j @jonchurch Thanks for the clear breakdown. That makes sense — Willow's suggestion was the right call given the PR had paging at the time, but the paging itself was solving a symptom rather than the root cause. I'll simplify this PR down to just the Algolia batching fix (with concurrency limit) and leave the UI as-is. I've opened #2529 to discuss client-side paging for org pages separately as James suggested. |
The root cause of npmx-dev#1946 and npmx-dev#2507 is that Algolia's getObjects API has a 1000-item limit. Sending >1000 names returns a 400, which triggers the slow npm fallback path (1 request per package). Fix: batch getPackagesByName into chunks of 1000 with a concurrency limit of 3. The org page and useOrgPackages stay unchanged — all packages are fetched upfront and sorted client-side. Client-side paging is tracked separately in npmx-dev#2529. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Summary
Addresses #1946, org pages hanging for large orgs like
@typeswith 11,000+ packages.The initial fix capped metadata fetching at 1000 packages, which stopped the hang, but it also meant everything past that limit was invisible.
What changed
loadAll()to fetch the complete dataset, so client-side search works across all packagesuseVisibleItems: added anonExpandcallback withisExpandingstate and partial-load support (return falseto keephasMoretrue for retries), as suggested by @ghostdevvPromise.allSettled), partial failures are handled gracefullygetPackagesByNameSliceextracted from Algolia composable for batchedgetObjectscalls (max 1000 per request)