Merge pull request #7323 from ouchihao/codex/bug-announcement-stale-fetch

fix(announcements): ignore superseded fetch results
This commit is contained in:
Wesley Liddick
2026-09-22 21:54:38 +08:00
committed by GitHub
2 changed files with 112 additions and 1 deletions
@@ -0,0 +1,106 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAnnouncementStore } from '../announcements'
import type { UserAnnouncement } from '@/types'
const list = vi.hoisted(() => vi.fn())
vi.mock('@/api', () => ({ announcementsAPI: { list } }))
const notice = (id: number): UserAnnouncement => ({
id, title: `Notice ${id}`, content: 'Content', notify_mode: 'popup',
created_at: '2026-09-18T00:00:00Z', updated_at: '2026-09-18T00:00:00Z'
})
function pendingList() {
let resolve!: (value: UserAnnouncement[]) => void
let reject!: (reason: Error) => void
const promise = new Promise<UserAnnouncement[]>((res, rej) => { resolve = res; reject = rej })
return { promise, resolve, reject }
}
beforeEach(() => {
setActivePinia(createPinia())
vi.resetAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => vi.restoreAllMocks())
describe('announcement fetch ownership', () => {
it('does not restore announcements or popups after logout resets the store', async () => {
const store = useAnnouncementStore()
const old = pendingList()
list.mockReturnValueOnce(old.promise)
const request = store.fetchAnnouncements()
store.reset()
old.resolve([notice(1)])
await request
expect(store.announcements).toEqual([])
expect(store.currentPopup).toBeNull()
expect(store.loading).toBe(false)
})
it('keeps the new session loading when an old request finishes', async () => {
const store = useAnnouncementStore()
const old = pendingList()
const current = pendingList()
list.mockReturnValueOnce(old.promise).mockReturnValueOnce(current.promise)
const oldRequest = store.fetchAnnouncements()
store.reset()
const currentRequest = store.fetchAnnouncements()
old.resolve([])
await oldRequest
expect(store.loading).toBe(true)
current.resolve([notice(2)])
await currentRequest
expect(store.announcements.map(a => a.id)).toEqual([2])
expect(store.loading).toBe(false)
})
it('keeps the latest forced refresh when responses arrive out of order', async () => {
const store = useAnnouncementStore()
const old = pendingList()
list.mockReturnValueOnce(old.promise).mockResolvedValueOnce([notice(2)])
const oldRequest = store.fetchAnnouncements()
await store.fetchAnnouncements(true)
old.resolve([notice(1)])
await oldRequest
expect(store.announcements.map(a => a.id)).toEqual([2])
expect(store.currentPopup?.id).toBe(2)
})
it('does not clear the new session throttle when an old request rejects', async () => {
const store = useAnnouncementStore()
const old = pendingList()
list.mockReturnValueOnce(old.promise).mockResolvedValue([notice(2)])
const oldRequest = store.fetchAnnouncements()
store.reset()
await store.fetchAnnouncements()
old.reject(new Error('old request failed'))
await oldRequest
await store.fetchAnnouncements()
expect(list).toHaveBeenCalledTimes(2)
expect(store.announcements.map(a => a.id)).toEqual([2])
})
it('still retries a failed current request', async () => {
const store = useAnnouncementStore()
list.mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce([notice(1)])
await store.fetchAnnouncements()
await store.fetchAnnouncements()
expect(list).toHaveBeenCalledTimes(2)
expect(store.currentPopup?.id).toBe(1)
})
it('still throttles ordinary concurrent fetches', async () => {
const store = useAnnouncementStore()
const current = pendingList()
list.mockReturnValueOnce(current.promise)
const request = store.fetchAnnouncements()
await store.fetchAnnouncements()
expect(list).toHaveBeenCalledTimes(1)
expect(store.loading).toBe(true)
current.resolve([])
await request
expect(store.loading).toBe(false)
})
})
+6 -1
View File
@@ -15,6 +15,7 @@ export const useAnnouncementStore = defineStore('announcements', () => {
// Session-scoped dedup set — not reactive, used as plain lookup only
let shownPopupIds = new Set<number>()
let fetchGeneration = 0
// Getters
const unreadCount = computed(() =>
@@ -30,18 +31,21 @@ export const useAnnouncementStore = defineStore('announcements', () => {
// Set immediately to prevent concurrent duplicate requests
lastFetchTime.value = now
const generation = ++fetchGeneration
try {
loading.value = true
const all = await announcementsAPI.list(false)
if (generation !== fetchGeneration) return
announcements.value = all.slice(0, 20)
enqueueNewPopups()
} catch (err: any) {
if (generation !== fetchGeneration) return
// Revert throttle timestamp on failure so retry is allowed
lastFetchTime.value = 0
console.error('Failed to fetch announcements:', err)
} finally {
loading.value = false
if (generation === fetchGeneration) loading.value = false
}
}
@@ -118,6 +122,7 @@ export const useAnnouncementStore = defineStore('announcements', () => {
}
function reset() {
fetchGeneration++
announcements.value = []
lastFetchTime.value = 0
shownPopupIds = new Set()