Skip to content

Commit ceecba1

Browse files
committed
add retries for updating the notifications, to account for latency in creating the notification on the server side
1 parent ef8cdcf commit ceecba1

2 files changed

Lines changed: 79 additions & 10 deletions

File tree

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,78 @@
1-
import { useCallback, useEffect, useState } from 'react'
1+
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { needsUpdateStore } from './needsUpdateStore'
33
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
44
import { User } from '@/users/domain/models/User'
55
import { useNeedsUpdate } from '@/notifications/domain/hooks/useNeedsUpdate'
66
import { getUnreadNotificationsCount } from '@/notifications/domain/useCases/getUnreadNotificationsCount'
77

88
const POLLING_INTERVAL = 30000 // 30 seconds
9+
const INVALIDATION_RETRY_INTERVAL = 1000
10+
const INVALIDATION_RETRY_ATTEMPTS = 6
911
export function useUnreadCount(user: User, notificationRepository: NotificationRepository) {
1012
const [unreadCount, setUnreadCount] = useState(0)
13+
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
14+
const refreshCycleRef = useRef(0)
1115

1216
const needsUpdate = useNeedsUpdate()
13-
const fetchUnread = useCallback(async () => {
14-
if (user) {
15-
const count = await getUnreadNotificationsCount(notificationRepository)
16-
setUnreadCount(count)
17+
18+
const clearRetryTimeout = useCallback(() => {
19+
if (retryTimeoutRef.current) {
20+
clearTimeout(retryTimeoutRef.current)
21+
retryTimeoutRef.current = null
1722
}
18-
needsUpdateStore.setNeedsUpdate(false)
19-
}, [user, notificationRepository])
23+
}, [])
24+
25+
const fetchUnread = useCallback(
26+
async (refreshCycleId?: number) => {
27+
if (user) {
28+
const count = await getUnreadNotificationsCount(notificationRepository)
29+
if (refreshCycleId === undefined || refreshCycleRef.current === refreshCycleId) {
30+
setUnreadCount(count)
31+
}
32+
}
33+
},
34+
[user, notificationRepository]
35+
)
36+
2037
useEffect(() => {
2138
if (needsUpdate) {
22-
void fetchUnread()
39+
const refreshCycleId = refreshCycleRef.current + 1
40+
refreshCycleRef.current = refreshCycleId
41+
clearRetryTimeout()
42+
needsUpdateStore.setNeedsUpdate(false)
43+
44+
const runRefreshCycle = async (attempt: number) => {
45+
await fetchUnread(refreshCycleId)
46+
47+
if (refreshCycleRef.current !== refreshCycleId) {
48+
return
49+
}
50+
51+
if (attempt < INVALIDATION_RETRY_ATTEMPTS - 1) {
52+
retryTimeoutRef.current = setTimeout(() => {
53+
void runRefreshCycle(attempt + 1)
54+
}, INVALIDATION_RETRY_INTERVAL)
55+
}
56+
}
57+
58+
void runRefreshCycle(0)
2359
}
24-
}, [needsUpdate, fetchUnread, notificationRepository])
60+
}, [needsUpdate, fetchUnread, clearRetryTimeout])
61+
2562
// Polling trigger
2663
useEffect(() => {
2764
const interval = setInterval(() => {
2865
void fetchUnread()
2966
}, POLLING_INTERVAL)
3067

3168
return () => clearInterval(interval)
32-
}, [fetchUnread, notificationRepository])
69+
}, [fetchUnread])
3370

3471
useEffect(() => {
3572
void fetchUnread() // run once when the component mounts
3673
}, [fetchUnread])
3774

75+
useEffect(() => clearRetryTimeout, [clearRetryTimeout])
76+
3877
return unreadCount
3978
}

tests/component/sections/layout/header/LoggedInHeaderActions.spec.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,36 @@ describe('LoggedInHeaderActions', () => {
163163
cy.get('[data-testid="unread-notifications-badge"]').should('not.exist')
164164
})
165165

166+
it('retries unread notification refresh after an invalidation when the first fetch is stale', () => {
167+
const unreadCountStub = cy.stub()
168+
unreadCountStub.onFirstCall().resolves(3)
169+
unreadCountStub.onSecondCall().resolves(3)
170+
unreadCountStub.onThirdCall().resolves(5)
171+
notificationRepository.getUnreadNotificationsCount = unreadCountStub
172+
173+
cy.clock()
174+
cy.mountAuthenticated(
175+
<LoggedInHeaderActions
176+
user={testUser}
177+
collectionRepository={collectionRepository}
178+
notificationRepository={notificationRepository}
179+
/>
180+
)
181+
182+
cy.get('[data-testid="unread-notifications-badge"]').should('exist').and('contain', '3')
183+
cy.then(() => {
184+
needsUpdateStore.setNeedsUpdate(true)
185+
})
186+
187+
cy.wrap(unreadCountStub).should('have.been.calledTwice')
188+
cy.get('[data-testid="unread-notifications-badge"]').should('exist').and('contain', '3')
189+
190+
cy.tick(1_000)
191+
192+
cy.wrap(unreadCountStub).should('have.been.calledThrice')
193+
cy.get('[data-testid="unread-notifications-badge"]').should('exist').and('contain', '5')
194+
})
195+
166196
it('calls the logout function when clicking the logout button', () => {
167197
collectionRepository.getUserPermissions = cy.stub().resolves(userPermissionsMock)
168198
collectionRepository.getById = cy.stub().resolves(CollectionMother.create())

0 commit comments

Comments
 (0)