[#449] added debounce for websocket updates (#476)

* [#449] added debounce for websocket updates

* [#449] changed behavior to have disableable debounce + changed debounced messages data storage to be handled by websocket Gate
This commit is contained in:
Camille Moussu
2026-01-26 11:15:37 +01:00
committed by GitHub
parent 1fc85e141e
commit dafac94aec
8 changed files with 341 additions and 41 deletions
+19 -2
View File
@@ -1,10 +1,11 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { AppDispatch } from "@/app/store";
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { WebSocketWithCleanup } from "./connection";
import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection";
import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection";
import { updateCalendars } from "./messaging";
import { updateCalendars } from "./messaging/updateCalendars";
import { syncCalendarRegistrations } from "./operations";
export function WebSocketGate() {
@@ -24,9 +25,25 @@ export function WebSocketGate() {
useAppSelector((state) => state?.calendars?.templist) ?? {}
);
const calendarsToRefreshRef = useRef<Map<string, any>>(new Map());
const calendarsToHideRef = useRef<Set<string>>(new Set());
const debouncedUpdateFnRef = useRef<
((dispatch: AppDispatch) => void) | undefined
>();
const currentDebouncePeriodRef = useRef<number | undefined>();
const onMessage = useCallback(
(message: unknown) => {
updateCalendars(message, dispatch);
const accumulators = {
calendarsToRefresh: calendarsToRefreshRef.current,
calendarsToHide: calendarsToHideRef.current,
debouncedUpdateFn: debouncedUpdateFnRef.current,
currentDebouncePeriod: currentDebouncePeriodRef.current,
};
updateCalendars(message, dispatch, accumulators);
// Persist any mutations back to refs
debouncedUpdateFnRef.current = accumulators.debouncedUpdateFn;
currentDebouncePeriodRef.current = accumulators.currentDebouncePeriod;
},
[dispatch]
);
@@ -0,0 +1,8 @@
import { AppDispatch } from "@/app/store";
export interface UpdateCalendarsAccumulators {
calendarsToRefresh: Map<string, any>;
calendarsToHide: Set<string>;
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
currentDebouncePeriod?: number;
}
+134 -19
View File
@@ -3,42 +3,157 @@ import { store } from "@/app/store";
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services";
import { findCalendarById, getDisplayedCalendarRange } from "@/utils";
import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars";
import { debounce } from "lodash";
import { parseCalendarPath } from "./parseCalendarPath";
import { parseMessage } from "./parseMessage";
import { UpdateCalendarsAccumulators } from "./type/UpdateCalendarsAccumulators";
export function updateCalendars(message: unknown, dispatch: AppDispatch) {
const currentRange = getDisplayedCalendarRange();
const DEFAULT_DEBOUNCE_MS = 0;
function createDebouncedUpdate(
debouncePeriodMs: number,
getCalendarsToRefresh: () => Map<string, any>,
getCalendarsToHide: () => Set<string>
) {
return debounce(
(dispatch: AppDispatch) => {
const currentRange = getDisplayedCalendarRange();
// Snapshot state
const calendarsToProcess = new Map(getCalendarsToRefresh());
const calendarsToHideSnapshot = new Set(getCalendarsToHide());
// Clear accumulators
getCalendarsToRefresh().clear();
getCalendarsToHide().clear();
try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
processCalendarsToHide(calendarsToHideSnapshot);
} catch (error) {
console.warn("Error processing accumulated calendar updates:", error);
}
},
debouncePeriodMs,
{ leading: true, trailing: true }
);
}
export function updateCalendars(
message: unknown,
dispatch: AppDispatch,
accumulators: UpdateCalendarsAccumulators
) {
const state = store.getState();
const { calendarsToRefresh, calendarsToHide } = parseMessage(message);
calendarsToRefresh.forEach((calendarPath) => {
// Accumulate
accumulateCalendarsToRefresh(
state,
calendarsToRefresh,
accumulators.calendarsToRefresh
);
accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide);
const debouncePeriod =
(window as any).WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS;
if (debouncePeriod > 0) {
if (
!accumulators.debouncedUpdateFn ||
accumulators.currentDebouncePeriod !== debouncePeriod
) {
accumulators.debouncedUpdateFn = createDebouncedUpdate(
debouncePeriod,
() => accumulators.calendarsToRefresh,
() => accumulators.calendarsToHide
);
accumulators.currentDebouncePeriod = debouncePeriod;
}
accumulators.debouncedUpdateFn(dispatch);
return;
}
// Immediate processing if debounce disabled
const currentRange = getDisplayedCalendarRange();
const calendarsToProcess = new Map(accumulators.calendarsToRefresh);
const calendarsToHideSnapshot = new Set(accumulators.calendarsToHide);
accumulators.calendarsToRefresh.clear();
accumulators.calendarsToHide.clear();
try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
processCalendarsToHide(calendarsToHideSnapshot);
} catch (error) {
console.warn("Error processing calendar updates:", error);
}
}
// --- Helpers ---
function accumulateCalendarsToRefresh(
state: ReturnType<typeof store.getState>,
calendarPaths: Set<string>,
calendarsToRefreshMap: Map<string, any>
) {
calendarPaths.forEach((calendarPath) => {
const calendarId = parseCalendarPath(calendarPath);
if (!calendarId) {
console.warn("Invalid calendar path received:", calendarPath);
return;
}
const calendar = findCalendarById(state, calendarId);
if (calendar) {
dispatch(
refreshCalendarWithSyncToken({
calendar: calendar.calendar,
calType: calendar.type,
calendarRange: currentRange,
})
);
} else {
if (!calendar) {
console.warn("Calendar not found for id:", calendarId);
return;
}
calendarsToRefreshMap.set(calendarId, calendar);
});
}
function accumulateCalendarsToHide(
calendarPaths: Set<string>,
calendarsToHideSet: Set<string>
) {
calendarPaths.forEach((calendarPath) => {
const calendarId = parseCalendarPath(calendarPath);
if (calendarId) {
calendarsToHideSet.add(calendarId);
}
});
const currentSelectedCalendars = JSON.parse(
localStorage.getItem("selectedCalendars") ?? "[]"
) as string[];
}
const calendarIdsToHide = [...calendarsToHide]
.map(parseCalendarPath)
.filter((id): id is string => Boolean(id));
function processCalendarsToRefresh(
dispatch: AppDispatch,
currentRange: { start: Date; end: Date },
calendarsMap: Map<string, any>
) {
calendarsMap.forEach((calendar) => {
dispatch(
refreshCalendarWithSyncToken({
calendar: calendar.calendar,
calType: calendar.type,
calendarRange: currentRange,
})
);
});
}
function processCalendarsToHide(calendarsToHideSnapshot: Set<string>) {
if (calendarsToHideSnapshot.size === 0) return;
let currentSelectedCalendars: string[];
try {
const stored = localStorage.getItem("selectedCalendars") ?? "[]";
currentSelectedCalendars = JSON.parse(stored);
} catch (error) {
console.warn("Failed to parse selectedCalendars from localStorage:", error);
return;
}
const updatedSelectedCalendars = currentSelectedCalendars.filter(
(id) => !calendarIdsToHide.includes(id)
(id) => !calendarsToHideSnapshot.has(id)
);
setSelectedCalendars(updatedSelectedCalendars);