Skip to content

Commit 686ad04

Browse files
Adebesin-Cellclaude
andcommitted
refactor: progressive org loading with incremental batches
- 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>
1 parent c99df23 commit 686ad04

File tree

2 files changed

+64
-32
lines changed

2 files changed

+64
-32
lines changed

app/composables/npm/useOrgPackages.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { emptySearchResponse, metaToSearchResult } from './search-utils'
33
import { mapWithConcurrency } from '#shared/utils/async'
44

55
/** Number of packages to fetch metadata for in the initial load */
6-
const INITIAL_BATCH_SIZE = 250
6+
const INITIAL_BATCH_SIZE = 50
77

88
/** Max names per Algolia getObjects request */
99
const ALGOLIA_BATCH_SIZE = 1000
@@ -135,8 +135,8 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
135135
{ default: emptyOrgResponse },
136136
)
137137

138-
/** Load all remaining packages that weren't fetched in the initial batch. */
139-
async function loadAll(): Promise<void> {
138+
/** Load the next batch of packages (default: 1 Algolia batch of 1000). */
139+
async function loadMore(count: number = ALGOLIA_BATCH_SIZE): Promise<void> {
140140
const loadedSet = new Set(loadedObjects.value.map(o => o.package.name))
141141
if (loadedSet.size >= allNames.value.length) return
142142

@@ -146,23 +146,29 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
146146
return
147147
}
148148

149-
loadAllPromise = _doLoadAll()
149+
loadAllPromise = _doLoadMore(count)
150150
try {
151151
await loadAllPromise
152152
} finally {
153153
loadAllPromise = null
154154
}
155155
}
156156

157-
async function _doLoadAll(): Promise<void> {
157+
/** Load ALL remaining packages (used when filters need the full dataset). */
158+
async function loadAll(): Promise<void> {
159+
const remaining = allNames.value.length - loadedObjects.value.length
160+
if (remaining <= 0) return
161+
await loadMore(remaining)
162+
}
163+
164+
async function _doLoadMore(count: number): Promise<void> {
158165
const names = allNames.value
159166
const current = loadedObjects.value
160167
const loadedSet = new Set(current.map(o => o.package.name))
161-
const remainingNames = names.filter(n => !loadedSet.has(n))
168+
const remainingNames = names.filter(n => !loadedSet.has(n)).slice(0, count)
162169
if (remainingNames.length === 0) return
163170

164171
const org = toValue(orgName)
165-
166172
let newObjects: NpmSearchResult[] = []
167173

168174
if (searchProviderValue.value === 'algolia') {
@@ -203,8 +209,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
203209
}
204210

205211
if (newObjects.length > 0) {
206-
const existingNames = new Set(current.map(o => o.package.name))
207-
const deduped = newObjects.filter(o => !existingNames.has(o.package.name))
212+
const deduped = newObjects.filter(o => !loadedSet.has(o.package.name))
208213
const all = [...current, ...deduped]
209214
loadedObjects.value = all
210215

@@ -221,6 +226,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
221226

222227
return {
223228
...asyncData,
229+
loadMore,
224230
loadAll,
225231
}
226232
}

app/pages/org/[org].vue

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const orgName = computed(() => route.params.org.toLowerCase())
1515
const { isConnected } = useConnector()
1616
1717
// Fetch packages progressively (first 250 on SSR, rest on demand via loadAll)
18-
const { data: results, status, error, loadAll } = useOrgPackages(orgName)
18+
const { data: results, status, error, loadMore, loadAll } = useOrgPackages(orgName)
1919
2020
// Handle 404 errors reactively (since we're not awaiting)
2121
watch(
@@ -32,28 +32,41 @@ watch(
3232
{ immediate: true },
3333
)
3434
35-
const allPackages = computed(() => results.value?.objects ?? [])
35+
const packages = computed(() => results.value?.objects ?? [])
3636
const totalPackages = computed(() => results.value?.totalPackages ?? 0)
37-
38-
// Show first 250 packages initially; expanding triggers loadAll() to fetch remaining data
39-
const {
40-
visibleItems: packages,
41-
hasMore: hasMoreVisible,
42-
isExpanding,
43-
expand,
44-
} = useVisibleItems(allPackages, 250, { onExpand: loadAll })
4537
const packageCount = computed(() => packages.value.length)
4638
47-
// hasMore combines both: hidden in-memory items AND unfetched server data
48-
const hasMore = computed(
49-
() => hasMoreVisible.value || allPackages.value.length < totalPackages.value,
50-
)
39+
// Progressive loading: first 50 on SSR, rest fetched on demand via loadAll()
40+
const hasMore = computed(() => packages.value.length < totalPackages.value)
41+
const isLoadingMore = shallowRef(false)
42+
43+
/** Fetch the next batch of packages (1000 at a time). */
44+
async function fetchNextBatch() {
45+
if (!hasMore.value || isLoadingMore.value) return
46+
isLoadingMore.value = true
47+
try {
48+
await loadMore()
49+
} finally {
50+
isLoadingMore.value = false
51+
}
52+
}
53+
54+
/** Fetch ALL remaining packages (needed for client-side filtering). */
55+
async function fetchAllRemaining() {
56+
if (!hasMore.value || isLoadingMore.value) return
57+
isLoadingMore.value = true
58+
try {
59+
await loadAll()
60+
} finally {
61+
isLoadingMore.value = false
62+
}
63+
}
5164
5265
// Preferences (persisted to localStorage)
5366
const { viewMode, paginationMode, pageSize, columns, toggleColumn, resetColumns } =
5467
usePackageListPreferences()
5568
56-
// Structured filters and sorting (operates on visible set; expand() widens it)
69+
// Structured filters and sorting (operates on all loaded packages)
5770
const {
5871
filters,
5972
sortOption,
@@ -77,20 +90,25 @@ const {
7790
// Pagination state
7891
const currentPage = shallowRef(1)
7992
93+
// Total items accounts for unfetched packages so pagination shows the real count
94+
const paginationTotal = computed(() =>
95+
hasMore.value ? totalPackages.value : sortedPackages.value.length,
96+
)
97+
8098
// Calculate total pages
8199
const totalPages = computed(() => {
82-
return Math.ceil(sortedPackages.value.length / pageSize.value)
100+
return Math.ceil(paginationTotal.value / pageSize.value)
83101
})
84102
85-
// Reset to page 1 when filters change; expand so filtering works across all packages
103+
// Reset to page 1 when filters change; load all packages so client-side filtering works
86104
watch([filters, sortOption], () => {
87105
currentPage.value = 1
88106
if (
89107
filters.value.text ||
90108
filters.value.keywords.length > 0 ||
91109
sortOption.value !== 'updated-desc'
92110
) {
93-
expand()
111+
fetchAllRemaining()
94112
}
95113
})
96114
@@ -101,6 +119,14 @@ watch(totalPages, newTotal => {
101119
}
102120
})
103121
122+
// Load next batch when user navigates beyond what's loaded
123+
watch(currentPage, page => {
124+
const loadedPages = Math.ceil(sortedPackages.value.length / pageSize.value)
125+
if (page > loadedPages && hasMore.value) {
126+
fetchNextBatch()
127+
}
128+
})
129+
104130
// Debounced URL update for filter/sort
105131
const updateUrl = debounce((updates: { filter?: string; sort?: string }) => {
106132
router.replace({
@@ -273,7 +299,7 @@ defineOgImageComponent('Default', {
273299

274300
<!-- Loading state (only when no packages loaded yet) -->
275301
<LoadingSpinner
276-
v-if="status === 'pending' && allPackages.length === 0"
302+
v-if="status === 'pending' && packages.length === 0"
277303
:text="$t('common.loading_packages')"
278304
/>
279305

@@ -288,7 +314,7 @@ defineOgImageComponent('Default', {
288314
</div>
289315

290316
<!-- Empty state -->
291-
<div v-else-if="allPackages.length === 0" class="py-12 text-center">
317+
<div v-else-if="packageCount === 0 && totalPackages === 0" class="py-12 text-center">
292318
<p class="text-fg-muted font-mono">
293319
{{ $t('org.page.no_packages') }} <span class="text-fg">@{{ orgName }}</span>
294320
</p>
@@ -337,15 +363,15 @@ defineOgImageComponent('Default', {
337363
<PackageList
338364
:results="sortedPackages"
339365
:has-more="hasMore"
340-
:is-loading="isExpanding"
366+
:is-loading="isLoadingMore"
341367
:view-mode="viewMode"
342368
:columns="columns"
343369
:filters="filters"
344370
v-model:sort-option="sortOption"
345371
:pagination-mode="paginationMode"
346372
:page-size="pageSize"
347373
:current-page="currentPage"
348-
@load-more="expand"
374+
@load-more="fetchNextBatch"
349375
@click-keyword="toggleKeyword"
350376
/>
351377

@@ -354,7 +380,7 @@ defineOgImageComponent('Default', {
354380
v-model:mode="paginationMode"
355381
v-model:page-size="pageSize"
356382
v-model:current-page="currentPage"
357-
:total-items="sortedPackages.length"
383+
:total-items="paginationTotal"
358384
:view-mode="viewMode"
359385
/>
360386
</template>

0 commit comments

Comments
 (0)