mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 09:48:09 +00:00
fix(assets): harden history epoch guards and cursor consistency
- Guard loadMoreHistory's success assignment against a superseding reload so a stale continuation can't blank historyAssets mid-reload - Re-sync the epoch after replaceHistoryWithHeadPage so post-replace errors surface instead of being swallowed by the staleness guard - Drop historyNextCursor once paging terminates, so state never carries a live cursor alongside hasMoreHistory === false - Reset historyError before the empty-list early return in doRefreshHistoryHead; document replaceHistoryWithHeadPage's epoch bump
This commit is contained in:
@@ -907,6 +907,56 @@ describe('assetsStore - Refactored (Option A)', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('does not blank historyAssets when a stale loadMore resolves mid-reload', async () => {
|
||||
vi.mocked(fetchHistoryPage).mockResolvedValueOnce(
|
||||
mockHistoryPage(
|
||||
Array.from({ length: 10 }, (_, i) => createMockJobItem(i)),
|
||||
{ hasMore: true, nextCursor: 'cursor-1' }
|
||||
)
|
||||
)
|
||||
await store.updateHistory()
|
||||
expect(store.historyAssets).toHaveLength(10)
|
||||
|
||||
let resolveStale: (page: FetchHistoryPageResult) => void
|
||||
vi.mocked(fetchHistoryPage).mockReturnValueOnce(
|
||||
new Promise<FetchHistoryPageResult>((resolve) => {
|
||||
resolveStale = resolve
|
||||
})
|
||||
)
|
||||
const staleLoad = store.loadMoreHistory()
|
||||
|
||||
// Second reload is still in flight: it has reset allHistoryItems to []
|
||||
// and bumped the epoch, but has not repopulated historyAssets yet.
|
||||
let resolveFresh: (page: FetchHistoryPageResult) => void
|
||||
vi.mocked(fetchHistoryPage).mockReturnValueOnce(
|
||||
new Promise<FetchHistoryPageResult>((resolve) => {
|
||||
resolveFresh = resolve
|
||||
})
|
||||
)
|
||||
const freshUpdate = store.updateHistory()
|
||||
|
||||
resolveStale!(
|
||||
mockHistoryPage([createMockJobItem(50)], {
|
||||
hasMore: true,
|
||||
nextCursor: 'cursor-stale'
|
||||
})
|
||||
)
|
||||
await staleLoad
|
||||
|
||||
// The superseded loadMore must not overwrite the visible list with the
|
||||
// transient empty snapshot of the in-flight reload.
|
||||
expect(store.historyAssets).toHaveLength(10)
|
||||
|
||||
resolveFresh!(
|
||||
mockHistoryPage(
|
||||
Array.from({ length: 5 }, (_, i) => createMockJobItem(100 + i)),
|
||||
{ hasMore: true, nextCursor: 'cursor-fresh' }
|
||||
)
|
||||
)
|
||||
await freshUpdate
|
||||
expect(store.historyAssets).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('terminates the walk when the backend returns the same cursor it was given (stuck cursor)', async () => {
|
||||
// Page 1: initial load mints cursor-1
|
||||
const firstBatch = Array.from({ length: 10 }, (_, i) =>
|
||||
|
||||
@@ -284,11 +284,15 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
// advance it while still in offset mode; otherwise the offset used by the
|
||||
// recovery fallback would drift past valid rows.
|
||||
if (!historyCursorMode) historyOffset.value += page.jobs.length
|
||||
historyNextCursor.value = cursorStuck ? null : (page.nextCursor ?? null)
|
||||
hasMoreHistory.value =
|
||||
page.hasMore &&
|
||||
!cursorStuck &&
|
||||
(page.jobs.length > 0 || page.nextCursor != null)
|
||||
// Drop the cursor once paging terminates so state never carries a live
|
||||
// cursor alongside `hasMoreHistory === false`.
|
||||
historyNextCursor.value = hasMoreHistory.value
|
||||
? (page.nextCursor ?? null)
|
||||
: null
|
||||
|
||||
trimHistoryToLimit()
|
||||
|
||||
@@ -333,6 +337,7 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
const epoch = historyFetchEpoch
|
||||
try {
|
||||
await fetchHistoryAssets(true)
|
||||
if (epoch !== historyFetchEpoch) return
|
||||
historyAssets.value = allHistoryItems.value
|
||||
} catch (err) {
|
||||
if (epoch !== historyFetchEpoch) return
|
||||
@@ -350,6 +355,9 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
* A head page with no further rows spans the whole timeline, so replacing
|
||||
* local state with it also prunes jobs deleted server-side (e.g. after the
|
||||
* queue history is cleared from another surface).
|
||||
*
|
||||
* Bumps `historyFetchEpoch`, which cancels any concurrent
|
||||
* `loadMoreHistory`/`fetchHistoryAssets` continuation.
|
||||
*/
|
||||
const replaceHistoryWithHeadPage = (page: FetchHistoryPageResult) => {
|
||||
historyFetchEpoch += 1
|
||||
@@ -393,13 +401,13 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
}
|
||||
|
||||
const doRefreshHistoryHead = async () => {
|
||||
historyError.value = null
|
||||
if (!allHistoryItems.value.length) {
|
||||
await updateHistory()
|
||||
return
|
||||
}
|
||||
|
||||
historyError.value = null
|
||||
const epoch = historyFetchEpoch
|
||||
let epoch = historyFetchEpoch
|
||||
try {
|
||||
const page = await fetchHistoryJobsPage({ offset: 0 })
|
||||
if (epoch !== historyFetchEpoch) return
|
||||
@@ -425,6 +433,9 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
trimHistoryToLimit()
|
||||
} else {
|
||||
replaceHistoryWithHeadPage(page)
|
||||
// replaceHistoryWithHeadPage bumps the epoch; re-sync so the catch
|
||||
// guard below suppresses stale continuations, not genuine errors.
|
||||
epoch = historyFetchEpoch
|
||||
}
|
||||
historyAssets.value = allHistoryItems.value
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user