Skip to content

Commit d483706

Browse files
committed
fix(assets): defer a loadMore started while a refresh is in flight
Splitting the shared in-flight guard fixed a refresh being swallowed by a pending loadMore, but it also dropped the protection the shared guard gave in the other direction. On main a loadMore issued while a refresh was pending returned the refresh's promise and did nothing. It now issues a real request, and because the refresh has already rewound the offset and cursor to the head, that request re-fetches the first page. Every id dedupes against the just-refreshed seenIds, so `fresh` is empty and `flatOutputHasMore` is stranded at false, dead-ending pagination even though the server reported more pages. The epoch guard does not catch this: the loadMore captures the epoch after the refresh has already incremented it, so the two match on settle. Defer to the in-flight refresh instead, mirroring the branch directly above that defers to an in-flight loadMore. Not reachable through the one current consumer, which gates loadMore on `!loading`, but the store should not depend on callers for this.
1 parent a1279be commit d483706

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

src/stores/assetsStore.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,6 +2511,33 @@ describe('assetsStore - Flat Output Assets (cloud-only)', () => {
25112511
}
25122512
})
25132513

2514+
it('defers a loadMore started during a refresh instead of stranding hasMore', async () => {
2515+
const { store } = await setupStoreWithFirstPage()
2516+
const refreshPage = deferredPage()
2517+
2518+
vi.mocked(assetService.getAssetsPageByTag).mockReturnValueOnce(
2519+
refreshPage.promise
2520+
)
2521+
2522+
const refreshResult = store.updateFlatOutputs()
2523+
const loadMoreResult = store.loadMoreFlatOutputs()
2524+
2525+
// The refresh already rewound the offset to the head, so the loadMore has
2526+
// no next page to ask for and must not issue a second request.
2527+
expect(vi.mocked(assetService.getAssetsPageByTag)).toHaveBeenCalledTimes(
2528+
1
2529+
)
2530+
2531+
const head = Array.from({ length: FLAT_OUTPUT_PAGE_SIZE }, (_, i) =>
2532+
makeAsset(`b${i}`, `g${i}.png`)
2533+
)
2534+
refreshPage.resolve(makePage(head, { hasMore: true }))
2535+
await Promise.all([refreshResult, loadMoreResult])
2536+
2537+
expect(store.flatOutputHasMore).toBe(true)
2538+
expect(store.flatOutputAssets).toHaveLength(FLAT_OUTPUT_PAGE_SIZE)
2539+
})
2540+
25142541
it('concurrent refreshes deduplicate the network call and produce consistent state', async () => {
25152542
const page = deferredPage()
25162543
vi.mocked(assetService.getAssetsPageByTag).mockReturnValueOnce(

src/stores/assetsStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,14 @@ export const useAssetsStore = defineStore('assets', () => {
329329
await flatOutputLoadMoreInFlight.value
330330
return
331331
}
332+
// A refresh has already reset the offset and cursor to the head, so there
333+
// is no next page to ask for yet; the refresh itself delivers that page.
334+
// Issuing one anyway re-fetches the head, which dedupes to nothing and
335+
// would strand `flatOutputHasMore` at false.
336+
if (flatOutputRefreshInFlight.value) {
337+
await flatOutputRefreshInFlight.value
338+
return
339+
}
332340

333341
flatOutputError.value = null
334342

0 commit comments

Comments
 (0)