ISSUE-825 Improve refresh without WS (#826)

This commit is contained in:
Benoit TELLIER
2026-04-24 10:05:42 +02:00
committed by GitHub
parent 126fac845b
commit 138cfd7f4e
6 changed files with 165 additions and 19 deletions
@@ -60,6 +60,7 @@ describe('websocket messages storm', () => {
;(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange)
;(store.getState as jest.Mock).mockReturnValue(mockState)
window.WS_DEBOUNCE_PERIOD_MS = 500
window.WS_SKIP_DELAY_MS = 0
mockAccumulators.calendarsToRefresh = new Map<string, any>()
mockAccumulators.calendarsToHide = new Set()
mockAccumulators.shouldRefreshCalendarListRef.current = false
@@ -53,6 +53,7 @@ describe('updateCalendars', () => {
mockAccumulators.currentDebouncePeriod = 0
mockAccumulators.shouldRefreshCalendarListRef.current = false
mockAccumulators.debouncedUpdateFn = jest.fn()
window.WS_SKIP_DELAY_MS = 0
})
it('should not dispatch for non-object messages', () => {
+4 -1
View File
@@ -1,4 +1,5 @@
import eventsCalendar from '@/features/Calendars/CalendarSlice'
import { postMutationRefreshMiddleware } from '@/features/Calendars/listeners/postMutationRefresh'
import searchResultReducer from '@/features/Search/SearchSlice'
import settingsReducer from '@/features/Settings/SettingsSlice'
import userReducer from '@/features/User/userSlice'
@@ -24,7 +25,9 @@ export const setupStore = (preloadedState?: Partial<RootState>) => {
reducer: rootReducer,
preloadedState,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(routerMiddleware)
getDefaultMiddleware()
.concat(routerMiddleware)
.prepend(postMutationRefreshMiddleware.middleware)
})
}
@@ -0,0 +1,109 @@
import type { AppDispatch, RootState } from '@/app/store'
import { findCalendarById, getDisplayedCalendarRange } from '@/utils'
import { createListenerMiddleware } from '@reduxjs/toolkit'
import {
deleteEventAsync,
deleteEventInstanceAsync,
moveEventAsync,
putEventAsync,
refreshCalendarWithSyncToken,
updateEventInstanceAsync,
updateSeriesAsync
} from '../services'
export const postMutationRefreshMiddleware = createListenerMiddleware()
const startListening = postMutationRefreshMiddleware.startListening.withTypes<
RootState,
AppDispatch
>()
function triggerRefresh(
dispatch: AppDispatch,
getState: () => RootState,
calId: string
): void {
const found = findCalendarById(getState(), calId)
if (!found) return
void dispatch(
refreshCalendarWithSyncToken({
calendar: found.calendar,
calType: found.type,
calendarRange: getDisplayedCalendarRange()
})
)
}
startListening({
actionCreator: putEventAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.payload.calId
)
}
})
startListening({
actionCreator: deleteEventAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.payload.calId
)
}
})
startListening({
actionCreator: deleteEventInstanceAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.payload.calId
)
}
})
startListening({
actionCreator: updateEventInstanceAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.payload.calId
)
}
})
startListening({
actionCreator: moveEventAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.payload.calId
)
const sourceCalId = action.meta.arg.newEvent.calId
if (sourceCalId && sourceCalId !== action.payload.calId) {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
sourceCalId
)
}
}
})
startListening({
actionCreator: updateSeriesAsync.fulfilled,
effect: (action, listenerApi) => {
triggerRefresh(
listenerApi.dispatch,
() => listenerApi.getState(),
action.meta.arg.cal.id
)
}
})
+49 -18
View File
@@ -13,6 +13,7 @@ import { parseMessage } from './parseMessage'
import { UpdateCalendarsAccumulators } from './type/UpdateCalendarsAccumulators'
const DEFAULT_DEBOUNCE_MS = 0
const DEFAULT_WS_SKIP_DELAY_MS = 2000
function createDebouncedUpdate(
debouncePeriodMs: number,
@@ -39,7 +40,7 @@ function createDebouncedUpdate(
resetShouldRefreshCalendarList()
try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess)
scheduleCalendarsRefresh(dispatch, currentRange, calendarsToProcess)
processCalendarsToHide(calendarsToHideSnapshot)
if (shouldRefresh) {
@@ -106,7 +107,7 @@ export function updateCalendars(
accumulators.shouldRefreshCalendarListRef.current = false
try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess)
scheduleCalendarsRefresh(dispatch, currentRange, calendarsToProcess)
processCalendarsToHide(calendarsToHideSnapshot)
if (shouldRefreshCalendarList) {
dispatch(getCalendarsListAsync())
@@ -117,6 +118,52 @@ export function updateCalendars(
}
// --- Helpers ---
function scheduleCalendarsRefresh(
dispatch: AppDispatch,
currentRange: { start: Date; end: Date },
calendarsMap: Map<string, { calendar: Calendar; type?: 'temp' }>
) {
const skipDelayMs = window.WS_SKIP_DELAY_MS ?? DEFAULT_WS_SKIP_DELAY_MS
if (skipDelayMs === 0) {
dispatchCalendarsRefresh(dispatch, currentRange, calendarsMap)
return
}
setTimeout(() => {
const stateAfterDelay = store.getState()
const rangeAfterDelay = getDisplayedCalendarRange()
calendarsMap.forEach(({ calendar, type }, calId) => {
const current = findCalendarById(stateAfterDelay, calId)
if (current && current.calendar.syncToken !== calendar.syncToken) return
dispatch(
refreshCalendarWithSyncToken({
calendar: current?.calendar ?? calendar,
calType: type,
calendarRange: rangeAfterDelay
})
)
})
}, skipDelayMs)
}
function dispatchCalendarsRefresh(
dispatch: AppDispatch,
currentRange: { start: Date; end: Date },
calendarsMap: Map<string, { calendar: Calendar; type?: 'temp' }>
) {
calendarsMap.forEach(({ calendar, type }) => {
dispatch(
refreshCalendarWithSyncToken({
calendar,
calType: type,
calendarRange: currentRange
})
)
})
}
function accumulateCalendarsToRefresh(
state: ReturnType<typeof store.getState>,
calendarPaths: Set<string>,
@@ -149,22 +196,6 @@ function accumulateCalendarsToHide(
})
}
function processCalendarsToRefresh(
dispatch: AppDispatch,
currentRange: { start: Date; end: Date },
calendarsMap: Map<string, { calendar: Calendar; type?: 'temp' }>
) {
calendarsMap.forEach(calendar => {
dispatch(
refreshCalendarWithSyncToken({
calendar: calendar.calendar,
calType: calendar.type,
calendarRange: currentRange
})
)
})
}
function processCalendarsToHide(calendarsToHideSnapshot: Set<string>) {
if (calendarsToHideSnapshot.size === 0) return
+1
View File
@@ -27,6 +27,7 @@ declare global {
WEBSOCKET_URL: string
WS_DEBOUNCE_PERIOD_MS: number
WS_SKIP_DELAY_MS: number
WS_PING_PERIOD_MS: number
WS_PING_TIMEOUT_PERIOD_MS: number