[#522] added push for calendars updates (#541)

This commit is contained in:
Camille Moussu
2026-02-17 17:52:23 +01:00
committed by GitHub
parent c7843f8d69
commit 5d9eb8e19d
10 changed files with 50 additions and 72 deletions
@@ -336,45 +336,6 @@ describe("CalendarSlice", () => {
expect(result.payload.errors).toBeTruthy(); expect(result.payload.errors).toBeTruthy();
}); });
it("patchCalendarAsync.fulfilled updates calendar fields", () => {
const calId = "c1";
const prev = {
...initialState,
list: { c1: { id: calId, events: { e1: { uid: "e1" } } } as any },
};
const patch = { name: "N", desc: "D", color: { "apple:color": "#00f" } };
const state = reducer(
prev,
patchCalendarAsync.fulfilled(
{ calId, calLink: "link", patch },
"req1",
{
calId,
calLink: "link",
patch,
}
)
);
expect(state.list[calId].name).toBe("N");
expect(state.list[calId].description).toBe("D");
expect(state.list[calId].color?.["apple:color"]).toBe("#00f");
});
it("removeCalendarAsync.fulfilled deletes calendar", () => {
const prev = {
...initialState,
list: { c1: { id: "c1" } as any },
};
const state = reducer(
prev,
removeCalendarAsync.fulfilled({ calId: "c1" }, "req2", {
calId: "c1",
calLink: "l",
})
);
expect(state.list).toEqual({});
});
it("patchACLCalendarAsync.fulfilled sets visibility", () => { it("patchACLCalendarAsync.fulfilled sets visibility", () => {
const prev = { const prev = {
...initialState, ...initialState,
@@ -41,10 +41,12 @@ const mockAccumulators: {
calendarsToRefresh: Map<string, any>; calendarsToRefresh: Map<string, any>;
calendarsToHide: Set<string>; calendarsToHide: Set<string>;
debouncedUpdateFn?: (dispatch: AppDispatch) => void; debouncedUpdateFn?: (dispatch: AppDispatch) => void;
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
currentDebouncePeriod?: number; currentDebouncePeriod?: number;
} = { } = {
calendarsToRefresh: new Map<string, any>(), calendarsToRefresh: new Map<string, any>(),
calendarsToHide: new Set(), calendarsToHide: new Set(),
shouldRefreshCalendarListRef: { current: false },
currentDebouncePeriod: 0, currentDebouncePeriod: 0,
debouncedUpdateFn: undefined, debouncedUpdateFn: undefined,
}; };
@@ -60,7 +62,9 @@ describe("websocket messages storm", () => {
window.WS_DEBOUNCE_PERIOD_MS = 500; window.WS_DEBOUNCE_PERIOD_MS = 500;
mockAccumulators.calendarsToRefresh = new Map<string, any>(); mockAccumulators.calendarsToRefresh = new Map<string, any>();
mockAccumulators.calendarsToHide = new Set(); mockAccumulators.calendarsToHide = new Set();
mockAccumulators.shouldRefreshCalendarListRef.current = false;
mockAccumulators.currentDebouncePeriod = 0; mockAccumulators.currentDebouncePeriod = 0;
mockAccumulators.debouncedUpdateFn = undefined; mockAccumulators.debouncedUpdateFn = undefined;
}); });
it("debounces calendar updates during message storm", () => { it("debounces calendar updates during message storm", () => {
@@ -33,10 +33,12 @@ describe("updateCalendars", () => {
calendarsToRefresh: Map<string, any>; calendarsToRefresh: Map<string, any>;
calendarsToHide: Set<string>; calendarsToHide: Set<string>;
debouncedUpdateFn?: (dispatch: AppDispatch) => void; debouncedUpdateFn?: (dispatch: AppDispatch) => void;
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
currentDebouncePeriod?: number; currentDebouncePeriod?: number;
} = { } = {
calendarsToRefresh: new Map<string, any>(), calendarsToRefresh: new Map<string, any>(),
calendarsToHide: new Set(), calendarsToHide: new Set(),
shouldRefreshCalendarListRef: { current: false },
currentDebouncePeriod: 0, currentDebouncePeriod: 0,
debouncedUpdateFn: jest.fn(), debouncedUpdateFn: jest.fn(),
}; };
@@ -49,6 +51,7 @@ describe("updateCalendars", () => {
mockAccumulators.calendarsToRefresh = new Map<string, any>(); mockAccumulators.calendarsToRefresh = new Map<string, any>();
mockAccumulators.calendarsToHide = new Set(); mockAccumulators.calendarsToHide = new Set();
mockAccumulators.currentDebouncePeriod = 0; mockAccumulators.currentDebouncePeriod = 0;
mockAccumulators.shouldRefreshCalendarListRef.current = false;
mockAccumulators.debouncedUpdateFn = jest.fn(); mockAccumulators.debouncedUpdateFn = jest.fn();
}); });
+2 -26
View File
@@ -256,31 +256,8 @@ const CalendarSlice = createSlice({
} as Calendar; } as Calendar;
state.error = null; state.error = null;
}) })
.addCase(patchCalendarAsync.fulfilled, (state, action) => { .addCase(patchCalendarAsync.fulfilled, (state) => {
state.pending = false; state.pending = false;
if (action.payload.patch.color) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
color: action.payload.patch.color,
};
Object.keys(state.list[action.payload.calId].events).forEach(
(evId) =>
(state.list[action.payload.calId].events[evId].color =
action.payload.patch.color)
);
}
if (action.payload.patch.desc) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
description: action.payload.patch.desc,
};
}
if (action.payload.patch.name) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
name: action.payload.patch.name,
};
}
state.error = null; state.error = null;
}) })
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => { .addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
@@ -297,9 +274,8 @@ const CalendarSlice = createSlice({
} as Calendar; } as Calendar;
state.error = null; state.error = null;
}) })
.addCase(removeCalendarAsync.fulfilled, (state, action) => { .addCase(removeCalendarAsync.fulfilled, (state) => {
state.pending = false; state.pending = false;
delete state.list[action.payload.calId];
state.error = null; state.error = null;
}) })
.addCase(patchACLCalendarAsync.fulfilled, (state, action) => { .addCase(patchACLCalendarAsync.fulfilled, (state, action) => {
@@ -105,7 +105,10 @@ export const getCalendarsListAsync = createAsyncThunk<
if (fetchedCal) { if (fetchedCal) {
importedCalendars[id] = { importedCalendars[id] = {
...fetchedCal, ...fetchedCal,
color: existingCal.color, color: {
...existingCal.color,
light: fetchedCal.color?.light,
},
events: existingCal.events || {}, events: existingCal.events || {},
}; };
} }
+2
View File
@@ -57,6 +57,7 @@ export function WebSocketGate() {
Map<string, { calendar: Calendar; type?: "temp" }> Map<string, { calendar: Calendar; type?: "temp" }>
>(new Map()); >(new Map());
const calendarsToHideRef = useRef<Set<string>>(new Set()); const calendarsToHideRef = useRef<Set<string>>(new Set());
const shouldRefreshCalendarListRef = useRef<boolean>(false);
const debouncedUpdateFnRef = useRef< const debouncedUpdateFnRef = useRef<
((dispatch: AppDispatch) => void) | undefined ((dispatch: AppDispatch) => void) | undefined
>(); >();
@@ -67,6 +68,7 @@ export function WebSocketGate() {
const accumulators = { const accumulators = {
calendarsToRefresh: calendarsToRefreshRef.current, calendarsToRefresh: calendarsToRefreshRef.current,
calendarsToHide: calendarsToHideRef.current, calendarsToHide: calendarsToHideRef.current,
shouldRefreshCalendarListRef,
debouncedUpdateFn: debouncedUpdateFnRef.current, debouncedUpdateFn: debouncedUpdateFnRef.current,
currentDebouncePeriod: currentDebouncePeriodRef.current, currentDebouncePeriod: currentDebouncePeriodRef.current,
}; };
+6 -2
View File
@@ -3,8 +3,9 @@ import { WS_INBOUND_EVENTS } from "../protocols";
export function parseMessage(message: unknown) { export function parseMessage(message: unknown) {
const calendarsToRefresh = new Set<string>(); const calendarsToRefresh = new Set<string>();
const calendarsToHide = new Set<string>(); const calendarsToHide = new Set<string>();
let shouldRefreshCalendarList = false;
if (typeof message !== "object" || message === null) { if (typeof message !== "object" || message === null) {
return { calendarsToRefresh, calendarsToHide }; return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList };
} }
for (const [key, value] of Object.entries(message)) { for (const [key, value] of Object.entries(message)) {
@@ -21,11 +22,14 @@ export function parseMessage(message: unknown) {
break; break;
case WS_INBOUND_EVENTS.CALENDAR_CLIENT_REGISTERED: case WS_INBOUND_EVENTS.CALENDAR_CLIENT_REGISTERED:
break; break;
case WS_INBOUND_EVENTS.CALENDAR_LIST:
shouldRefreshCalendarList = true;
break;
default: { default: {
calendarsToRefresh.add(key); calendarsToRefresh.add(key);
} }
} }
} }
return { calendarsToRefresh, calendarsToHide }; return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList };
} }
@@ -4,6 +4,7 @@ import { Calendar } from "@/features/Calendars/CalendarTypes";
export interface UpdateCalendarsAccumulators { export interface UpdateCalendarsAccumulators {
calendarsToRefresh: Map<string, { calendar: Calendar; type?: "temp" }>; calendarsToRefresh: Map<string, { calendar: Calendar; type?: "temp" }>;
calendarsToHide: Set<string>; calendarsToHide: Set<string>;
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
debouncedUpdateFn?: (dispatch: AppDispatch) => void; debouncedUpdateFn?: (dispatch: AppDispatch) => void;
currentDebouncePeriod?: number; currentDebouncePeriod?: number;
} }
+27 -4
View File
@@ -1,7 +1,10 @@
import type { AppDispatch } from "@/app/store"; import type { AppDispatch } from "@/app/store";
import { store } from "@/app/store"; import { store } from "@/app/store";
import { Calendar } from "@/features/Calendars/CalendarTypes"; import { Calendar } from "@/features/Calendars/CalendarTypes";
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services"; import {
getCalendarsListAsync,
refreshCalendarWithSyncToken,
} from "@/features/Calendars/services";
import { findCalendarById, getDisplayedCalendarRange } from "@/utils"; import { findCalendarById, getDisplayedCalendarRange } from "@/utils";
import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars"; import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars";
import { debounce } from "lodash"; import { debounce } from "lodash";
@@ -17,7 +20,9 @@ function createDebouncedUpdate(
string, string,
{ calendar: Calendar; type?: "temp" } { calendar: Calendar; type?: "temp" }
>, >,
getCalendarsToHide: () => Set<string> getCalendarsToHide: () => Set<string>,
getShouldRefreshCalendarList: () => boolean,
resetShouldRefreshCalendarList: () => void
) { ) {
return debounce( return debounce(
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
@@ -26,14 +31,20 @@ function createDebouncedUpdate(
// Snapshot state // Snapshot state
const calendarsToProcess = new Map(getCalendarsToRefresh()); const calendarsToProcess = new Map(getCalendarsToRefresh());
const calendarsToHideSnapshot = new Set(getCalendarsToHide()); const calendarsToHideSnapshot = new Set(getCalendarsToHide());
const shouldRefresh = getShouldRefreshCalendarList();
// Clear accumulators // Clear accumulators
getCalendarsToRefresh().clear(); getCalendarsToRefresh().clear();
getCalendarsToHide().clear(); getCalendarsToHide().clear();
resetShouldRefreshCalendarList();
try { try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess); processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
processCalendarsToHide(calendarsToHideSnapshot); processCalendarsToHide(calendarsToHideSnapshot);
if (shouldRefresh) {
dispatch(getCalendarsListAsync());
}
} catch (error) { } catch (error) {
console.warn("Error processing accumulated calendar updates:", error); console.warn("Error processing accumulated calendar updates:", error);
} }
@@ -49,7 +60,8 @@ export function updateCalendars(
accumulators: UpdateCalendarsAccumulators accumulators: UpdateCalendarsAccumulators
) { ) {
const state = store.getState(); const state = store.getState();
const { calendarsToRefresh, calendarsToHide } = parseMessage(message); const { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList } =
parseMessage(message);
// Accumulate // Accumulate
accumulateCalendarsToRefresh( accumulateCalendarsToRefresh(
@@ -59,6 +71,9 @@ export function updateCalendars(
); );
accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide); accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide);
accumulators.shouldRefreshCalendarListRef.current ||=
shouldRefreshCalendarList;
const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS; const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS;
if (debouncePeriod > 0) { if (debouncePeriod > 0) {
@@ -69,7 +84,11 @@ export function updateCalendars(
accumulators.debouncedUpdateFn = createDebouncedUpdate( accumulators.debouncedUpdateFn = createDebouncedUpdate(
debouncePeriod, debouncePeriod,
() => accumulators.calendarsToRefresh, () => accumulators.calendarsToRefresh,
() => accumulators.calendarsToHide () => accumulators.calendarsToHide,
() => accumulators.shouldRefreshCalendarListRef.current,
() => {
accumulators.shouldRefreshCalendarListRef.current = false;
}
); );
accumulators.currentDebouncePeriod = debouncePeriod; accumulators.currentDebouncePeriod = debouncePeriod;
} }
@@ -84,10 +103,14 @@ export function updateCalendars(
accumulators.calendarsToRefresh.clear(); accumulators.calendarsToRefresh.clear();
accumulators.calendarsToHide.clear(); accumulators.calendarsToHide.clear();
accumulators.shouldRefreshCalendarListRef.current = false;
try { try {
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess); processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
processCalendarsToHide(calendarsToHideSnapshot); processCalendarsToHide(calendarsToHideSnapshot);
if (shouldRefreshCalendarList) {
dispatch(getCalendarsListAsync());
}
} catch (error) { } catch (error) {
console.warn("Error processing calendar updates:", error); console.warn("Error processing calendar updates:", error);
} }
+1
View File
@@ -6,4 +6,5 @@ export const WS_INBOUND_EVENTS = {
CLIENT_REGISTERED: "registered", CLIENT_REGISTERED: "registered",
CLIENT_UNREGISTERED: "unregistered", CLIENT_UNREGISTERED: "unregistered",
CALENDAR_CLIENT_REGISTERED: "calendarListRegistered", CALENDAR_CLIENT_REGISTERED: "calendarListRegistered",
CALENDAR_LIST: "calendarList",
} as const; } as const;