@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 || {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user