From 138cfd7f4eae0155d44b4c14b710c0176e72d1e9 Mon Sep 17 00:00:00 2001 From: Benoit TELLIER Date: Fri, 24 Apr 2026 10:05:42 +0200 Subject: [PATCH] ISSUE-825 Improve refresh without WS (#826) --- .../utils/notificationStorm.test.tsx | 1 + .../websocket/utils/updateCalendars.test.tsx | 1 + src/app/store.ts | 5 +- .../listeners/postMutationRefresh.ts | 109 ++++++++++++++++++ src/websocket/messaging/updateCalendars.ts | 67 ++++++++--- src/window.d.ts | 1 + 6 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 src/features/Calendars/listeners/postMutationRefresh.ts diff --git a/__test__/features/websocket/utils/notificationStorm.test.tsx b/__test__/features/websocket/utils/notificationStorm.test.tsx index a3634b3..e7771b5 100644 --- a/__test__/features/websocket/utils/notificationStorm.test.tsx +++ b/__test__/features/websocket/utils/notificationStorm.test.tsx @@ -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() mockAccumulators.calendarsToHide = new Set() mockAccumulators.shouldRefreshCalendarListRef.current = false diff --git a/__test__/features/websocket/utils/updateCalendars.test.tsx b/__test__/features/websocket/utils/updateCalendars.test.tsx index 2bfebd9..9ac5363 100644 --- a/__test__/features/websocket/utils/updateCalendars.test.tsx +++ b/__test__/features/websocket/utils/updateCalendars.test.tsx @@ -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', () => { diff --git a/src/app/store.ts b/src/app/store.ts index c831016..a1db006 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -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) => { reducer: rootReducer, preloadedState, middleware: getDefaultMiddleware => - getDefaultMiddleware().concat(routerMiddleware) + getDefaultMiddleware() + .concat(routerMiddleware) + .prepend(postMutationRefreshMiddleware.middleware) }) } diff --git a/src/features/Calendars/listeners/postMutationRefresh.ts b/src/features/Calendars/listeners/postMutationRefresh.ts new file mode 100644 index 0000000..404cfbc --- /dev/null +++ b/src/features/Calendars/listeners/postMutationRefresh.ts @@ -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 + ) + } +}) diff --git a/src/websocket/messaging/updateCalendars.ts b/src/websocket/messaging/updateCalendars.ts index 5931ce5..b0d66ad 100644 --- a/src/websocket/messaging/updateCalendars.ts +++ b/src/websocket/messaging/updateCalendars.ts @@ -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 +) { + 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 +) { + calendarsMap.forEach(({ calendar, type }) => { + dispatch( + refreshCalendarWithSyncToken({ + calendar, + calType: type, + calendarRange: currentRange + }) + ) + }) +} + function accumulateCalendarsToRefresh( state: ReturnType, calendarPaths: Set, @@ -149,22 +196,6 @@ function accumulateCalendarsToHide( }) } -function processCalendarsToRefresh( - dispatch: AppDispatch, - currentRange: { start: Date; end: Date }, - calendarsMap: Map -) { - calendarsMap.forEach(calendar => { - dispatch( - refreshCalendarWithSyncToken({ - calendar: calendar.calendar, - calType: calendar.type, - calendarRange: currentRange - }) - ) - }) -} - function processCalendarsToHide(calendarsToHideSnapshot: Set) { if (calendarsToHideSnapshot.size === 0) return diff --git a/src/window.d.ts b/src/window.d.ts index 4e2afbd..c142a2a 100644 --- a/src/window.d.ts +++ b/src/window.d.ts @@ -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