diff --git a/src/modules/notifications/notifications.test.mjs b/src/modules/notifications/notifications.test.mjs index 1c6adaf..462fc14 100644 --- a/src/modules/notifications/notifications.test.mjs +++ b/src/modules/notifications/notifications.test.mjs @@ -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"/) diff --git a/src/modules/notifications/store/notifications.test.mjs b/src/modules/notifications/store/notifications.test.mjs new file mode 100644 index 0000000..f41f6df --- /dev/null +++ b/src/modules/notifications/store/notifications.test.mjs @@ -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, '') +}) diff --git a/src/modules/notifications/store/notifications.ts b/src/modules/notifications/store/notifications.ts index ebe0f92..d9514e0 100644 --- a/src/modules/notifications/store/notifications.ts +++ b/src/modules/notifications/store/notifications.ts @@ -9,7 +9,19 @@ export const useNotificationsStore = defineStore('client-notifications', () => { const hasUnread = computed(() => (unreadCount.value ?? 0) > 0) - async function refreshUnreadCount() { + let pending: Promise | null = null + + async function refreshUnreadCount(afterMutation = false): Promise { + 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 { diff --git a/src/modules/notifications/views/NotificationDetailView.vue b/src/modules/notifications/views/NotificationDetailView.vue index 9be7bcc..4593170 100644 --- a/src/modules/notifications/views/NotificationDetailView.vue +++ b/src/modules/notifications/views/NotificationDetailView.vue @@ -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(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()}) diff --git a/src/modules/notifications/views/NotificationsView.vue b/src/modules/notifications/views/NotificationsView.vue index 9f934b1..f128956 100644 --- a/src/modules/notifications/views/NotificationsView.vue +++ b/src/modules/notifications/views/NotificationsView.vue @@ -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 {