fix: coalesce concurrent notification count reads

This commit is contained in:
2026-10-12 00:24:21 +08:00
parent 5751973c83
commit 3692f9d27f
5 changed files with 66 additions and 5 deletions
@@ -36,7 +36,7 @@ test('keeps customer message actions first and covers the requested local states
assert.match(source, /notification-state--error/)
assert.match(source, /暂无消息/)
assert.doesNotMatch(source, /清空已读|删除消息/)
assert.match(source, /Promise\.all\(\[load\(\), notifications\.refreshUnreadCount\(\)\]\)/)
assert.match(source, /Promise\.all\(\[load\(\), notifications\.refreshUnreadCount\(true\)\]\)/)
assert.match(source, /auth\.hasPermission\('merchant:notification:read'\)/)
assert.match(source, /auth\.hasPermission\('merchant:notification:read-all'\)/)
assert.match(source, /v-if="canMarkAllRead"/)
@@ -0,0 +1,49 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
import { computed, ref } from 'vue'
function setup() {
const requests = []
const source = readFileSync(new URL('./notifications.ts', import.meta.url), 'utf8').replace(/^import [^\r\n]*\r?\n/gm, '').replace('export const', 'const')
const code = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const store = new Function('computed', 'ref', 'defineStore', 'getMyUnreadNotificationCount', code + '\nreturn useNotificationsStore()')(computed, ref, (_id, factory) => factory, () => new Promise((resolve, reject) => requests.push({ resolve, reject })))
return { store, requests }
}
test('shell and page concurrent reads share one pending request', async () => {
const { store, requests } = setup()
const a = store.refreshUnreadCount(), b = store.refreshUnreadCount()
assert.equal(requests.length, 1)
requests[0].resolve(5)
await Promise.all([a, b])
assert.equal(store.unreadCount.value, 5)
assert.equal(store.loading.value, false)
})
test('post-mutation refresh reads again after a pending pre-mutation read', async () => {
const { store, requests } = setup()
const a = store.refreshUnreadCount(), b = store.refreshUnreadCount(true)
assert.equal(requests.length, 1)
requests[0].resolve(5)
await a
await Promise.resolve()
assert.equal(requests.length, 2)
requests[1].resolve(0)
await b
assert.equal(store.unreadCount.value, 0)
})
test('failed request releases the pending guard for retry', async () => {
const { store, requests } = setup()
const a = store.refreshUnreadCount()
requests[0].reject(new Error('network'))
await a
const b = store.refreshUnreadCount()
assert.equal(requests.length, 2)
requests[1].resolve(2)
await b
assert.equal(store.unreadCount.value, 2)
assert.equal(store.errorMessage.value, '')
})
@@ -9,7 +9,19 @@ export const useNotificationsStore = defineStore('client-notifications', () => {
const hasUnread = computed(() => (unreadCount.value ?? 0) > 0)
async function refreshUnreadCount() {
let pending: Promise<void> | null = null
async function refreshUnreadCount(afterMutation = false): Promise<void> {
if (pending) {
if (!afterMutation) return pending
await pending
return refreshUnreadCount()
}
pending = loadUnreadCount().finally(() => { pending = null })
return pending
}
async function loadUnreadCount() {
loading.value = true
errorMessage.value = ''
try {
@@ -10,7 +10,7 @@ import type { NotificationDetail } from '../contracts'
import { notificationCategoryLabel, notificationSentAt } from '../presentation'
const route = useRoute(); const router = useRouter(); const auth = useAuthStore(); const notifications = useNotificationsStore(); const detail = ref<NotificationDetail | null>(null); const loading = ref(false); const errorMessage = ref(''); const readErrorMessage = ref('')
const canMarkRead = computed(() => auth.hasPermission('merchant:notification:read'))
async function markReadIfAllowed(){if(!detail.value||detail.value.status!=='UNREAD'||!canMarkRead.value)return;try{await markMyNotificationRead(detail.value.id);detail.value={...detail.value,status:'READ'};await notifications.refreshUnreadCount()}catch(error){readErrorMessage.value=error instanceof ApiError?error.message:'消息已加载,但自动标记已读失败'}}
async function markReadIfAllowed(){if(!detail.value||detail.value.status!=='UNREAD'||!canMarkRead.value)return;try{await markMyNotificationRead(detail.value.id);detail.value={...detail.value,status:'READ'};await notifications.refreshUnreadCount(true)}catch(error){readErrorMessage.value=error instanceof ApiError?error.message:'消息已加载,但自动标记已读失败'}}
async function load(){loading.value=true;errorMessage.value='';readErrorMessage.value='';try{detail.value=await getMyNotification(String(route.params.notificationId));await markReadIfAllowed()}catch(error){detail.value=null;errorMessage.value=error instanceof ApiError?error.message:'消息加载失败,请稍后重试'}finally{loading.value=false}}
onMounted(()=>{void load()})
</script>
@@ -78,7 +78,7 @@ async function markRead(row: NotificationSummary) {
if (!canMarkRead.value) return
try {
await markMyNotificationRead(row.id)
await Promise.all([load(), notifications.refreshUnreadCount()])
await Promise.all([load(), notifications.refreshUnreadCount(true)])
} catch (error) {
errorMessage.value = messageOf(error)
}
@@ -89,7 +89,7 @@ async function markAllRead() {
markingAll.value = true
try {
await markAllMyNotificationsRead()
await Promise.all([load(), notifications.refreshUnreadCount()])
await Promise.all([load(), notifications.refreshUnreadCount(true)])
} catch (error) {
errorMessage.value = messageOf(error)
} finally {