* [#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:
@@ -0,0 +1,131 @@
|
|||||||
|
import type { AppDispatch, RootState } from "@/app/store";
|
||||||
|
import { store } from "@/app/store";
|
||||||
|
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services";
|
||||||
|
import { getDisplayedCalendarRange } from "@/utils";
|
||||||
|
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
|
||||||
|
|
||||||
|
jest.mock("@/features/Calendars/services", () => ({
|
||||||
|
refreshCalendarWithSyncToken: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/utils", () => ({
|
||||||
|
getDisplayedCalendarRange: jest.fn(),
|
||||||
|
findCalendarById: jest.requireActual("@/utils").findCalendarById,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/app/store", () => ({
|
||||||
|
store: {
|
||||||
|
getState: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const mockRange = {
|
||||||
|
start: new Date("2025-01-15T10:00:00Z"),
|
||||||
|
end: new Date("2025-01-16T10:00:00Z"),
|
||||||
|
};
|
||||||
|
const mockState = {
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
"cal1/entry1": { id: "cal1/entry1", name: "Calendar 1", syncToken: 1 },
|
||||||
|
"cal2/entry2": { id: "cal2/entry2", name: "Calendar 2", syncToken: 1 },
|
||||||
|
"cal/A": { id: "cal/A", name: "Cal A", syncToken: 1 },
|
||||||
|
"cal/B": { id: "cal/B", name: "Cal B", syncToken: 1 },
|
||||||
|
"cal/C": { id: "cal/C", name: "Cal C", syncToken: 1 },
|
||||||
|
},
|
||||||
|
templist: {},
|
||||||
|
},
|
||||||
|
} as unknown as RootState;
|
||||||
|
const mockAccumulators: {
|
||||||
|
calendarsToRefresh: Map<string, any>;
|
||||||
|
calendarsToHide: Set<string>;
|
||||||
|
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||||
|
currentDebouncePeriod?: number;
|
||||||
|
} = {
|
||||||
|
calendarsToRefresh: new Map<string, any>(),
|
||||||
|
calendarsToHide: new Set(),
|
||||||
|
currentDebouncePeriod: 0,
|
||||||
|
debouncedUpdateFn: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("websocket messages storm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(refreshCalendarWithSyncToken as unknown as jest.Mock).mockClear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.clearAllTimers();
|
||||||
|
jest.resetModules();
|
||||||
|
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
|
||||||
|
(store.getState as jest.Mock).mockReturnValue(mockState);
|
||||||
|
(window as any).WS_DEBOUNCE_PERIOD_MS = 500;
|
||||||
|
mockAccumulators.calendarsToRefresh = new Map<string, any>();
|
||||||
|
mockAccumulators.calendarsToHide = new Set();
|
||||||
|
mockAccumulators.currentDebouncePeriod = 0;
|
||||||
|
mockAccumulators.debouncedUpdateFn = undefined;
|
||||||
|
});
|
||||||
|
it("debounces calendar updates during message storm", () => {
|
||||||
|
const mockMessage = {
|
||||||
|
"/calendars/cal1/entry1": {
|
||||||
|
syncToken: "ldsk",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
updateCalendars(mockMessage, mockDispatch, mockAccumulators);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch called once because of leading edge
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Trailing edge
|
||||||
|
jest.advanceTimersByTime(500);
|
||||||
|
|
||||||
|
// only one call for the last message + leading edge message
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("debounces calendar updates during message storm with multiple updates", () => {
|
||||||
|
// Send a storm with mixed messages
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
if (i % 3 === 0)
|
||||||
|
updateCalendars(
|
||||||
|
{ "/calendars/cal/A": { syncToken: "ldskfjsld" + i } },
|
||||||
|
mockDispatch,
|
||||||
|
mockAccumulators
|
||||||
|
);
|
||||||
|
else if (i % 3 === 1)
|
||||||
|
updateCalendars(
|
||||||
|
{ "/calendars/cal/B": { syncToken: "ldskfjsld" + i } },
|
||||||
|
mockDispatch,
|
||||||
|
mockAccumulators
|
||||||
|
);
|
||||||
|
else
|
||||||
|
updateCalendars(
|
||||||
|
{ "/calendars/cal/C": { syncToken: "ldskfjsld" + i } },
|
||||||
|
mockDispatch,
|
||||||
|
mockAccumulators
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch called once because of leading edge
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Trailing edge
|
||||||
|
jest.advanceTimersByTime(500);
|
||||||
|
|
||||||
|
// Trailing edge updates once per calendar + the original leading edge
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("executes immediately when debounce is disabled", () => {
|
||||||
|
(window as any).WS_DEBOUNCE_PERIOD_MS = 0;
|
||||||
|
|
||||||
|
updateCalendars(
|
||||||
|
{ "/calendars/cal1/entry1": { syncToken: "abc" } },
|
||||||
|
mockDispatch,
|
||||||
|
mockAccumulators
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RootState, store } from "@/app/store";
|
import { AppDispatch, RootState, store } from "@/app/store";
|
||||||
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services/refreshCalendar";
|
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services/refreshCalendar";
|
||||||
import { getDisplayedCalendarRange } from "@/utils/CalendarRangeManager";
|
import { getDisplayedCalendarRange } from "@/utils/CalendarRangeManager";
|
||||||
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
|
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
|
||||||
@@ -29,23 +29,38 @@ describe("updateCalendars", () => {
|
|||||||
templist: {},
|
templist: {},
|
||||||
},
|
},
|
||||||
} as unknown as RootState;
|
} as unknown as RootState;
|
||||||
|
const mockAccumulators: {
|
||||||
|
calendarsToRefresh: Map<string, any>;
|
||||||
|
calendarsToHide: Set<string>;
|
||||||
|
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||||
|
currentDebouncePeriod?: number;
|
||||||
|
} = {
|
||||||
|
calendarsToRefresh: new Map<string, any>(),
|
||||||
|
calendarsToHide: new Set(),
|
||||||
|
currentDebouncePeriod: 0,
|
||||||
|
debouncedUpdateFn: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockDispatch = jest.fn();
|
mockDispatch = jest.fn();
|
||||||
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
|
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
|
||||||
(store.getState as jest.Mock).mockReturnValue(mockState);
|
(store.getState as jest.Mock).mockReturnValue(mockState);
|
||||||
|
mockAccumulators.calendarsToRefresh = new Map<string, any>();
|
||||||
|
mockAccumulators.calendarsToHide = new Set();
|
||||||
|
mockAccumulators.currentDebouncePeriod = 0;
|
||||||
|
mockAccumulators.debouncedUpdateFn = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not dispatch for non-object messages", () => {
|
it("should not dispatch for non-object messages", () => {
|
||||||
updateCalendars(null, mockDispatch);
|
updateCalendars(null, mockDispatch, mockAccumulators);
|
||||||
updateCalendars("string", mockDispatch);
|
updateCalendars("string", mockDispatch, mockAccumulators);
|
||||||
updateCalendars(123, mockDispatch);
|
updateCalendars(123, mockDispatch, mockAccumulators);
|
||||||
|
|
||||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should dispatch for registered calendars", () => {
|
it("should dispatch for registered calendars", async () => {
|
||||||
const message = {
|
const message = {
|
||||||
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
|
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
|
||||||
"/calendars/cal1/entry1",
|
"/calendars/cal1/entry1",
|
||||||
@@ -53,19 +68,22 @@ describe("updateCalendars", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCalendars(message, mockDispatch);
|
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||||
|
|
||||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2);
|
await waitFor(() =>
|
||||||
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should dispatch for calendar path updates", () => {
|
it("should dispatch for calendar path updates", async () => {
|
||||||
const message = {
|
const message = {
|
||||||
"/calendars/cal1/entry1": { updated: true },
|
"/calendars/cal1/entry1": { updated: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCalendars(message, mockDispatch);
|
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||||
|
await waitFor(() =>
|
||||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalled();
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalled()
|
||||||
|
);
|
||||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
||||||
calendar: mockState.calendars.list["cal1/entry1"],
|
calendar: mockState.calendars.list["cal1/entry1"],
|
||||||
calType: undefined,
|
calType: undefined,
|
||||||
@@ -73,14 +91,13 @@ describe("updateCalendars", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use current displayed calendar range", () => {
|
it("should use displayed calendar range", async () => {
|
||||||
const message = {
|
const message = {
|
||||||
"/calendars/cal1/entry1": {},
|
"/calendars/cal1/entry1": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCalendars(message, mockDispatch);
|
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||||
|
await waitFor(() => expect(getDisplayedCalendarRange).toHaveBeenCalled());
|
||||||
expect(getDisplayedCalendarRange).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle temp calendars", async () => {
|
it("should handle temp calendars", async () => {
|
||||||
@@ -103,7 +120,7 @@ describe("updateCalendars", () => {
|
|||||||
"/calendars/temp1/entry1": {},
|
"/calendars/temp1/entry1": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCalendars(message, mockDispatch);
|
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
||||||
@@ -120,7 +137,7 @@ describe("updateCalendars", () => {
|
|||||||
"not-a-path": {},
|
"not-a-path": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCalendars(message, mockDispatch);
|
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||||
|
|
||||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+12
-3
@@ -27,6 +27,7 @@
|
|||||||
"i18next": "^23.7.9",
|
"i18next": "^23.7.9",
|
||||||
"ical.js": "^2.2.0",
|
"ical.js": "^2.2.0",
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
"@types/node": "^20.19.24",
|
"@types/node": "^20.19.24",
|
||||||
"@types/react": "^18.3.26",
|
"@types/react": "^18.3.26",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
@@ -4619,6 +4621,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.27",
|
"version": "20.19.27",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
@@ -11103,9 +11112,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"i18next": "^23.7.9",
|
"i18next": "^23.7.9",
|
||||||
"ical.js": "^2.2.0",
|
"ical.js": "^2.2.0",
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
"@types/node": "^20.19.24",
|
"@types/node": "^20.19.24",
|
||||||
"@types/react": "^18.3.26",
|
"@types/react": "^18.3.26",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ var VIDEO_CONFERENCE_BASE_URL = "https://meet.linagora.com";
|
|||||||
var DEBUG = false;
|
var DEBUG = false;
|
||||||
var LANG = "en";
|
var LANG = "en";
|
||||||
var WEBSOCKET_URL = "wss://calendar.example.com";
|
var WEBSOCKET_URL = "wss://calendar.example.com";
|
||||||
|
var WS_DEBOUNCE_PERIOD_MS = 100; // milliseconds, remove or set to 0 to disable debounce
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||||
|
import { AppDispatch } from "@/app/store";
|
||||||
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { WebSocketWithCleanup } from "./connection";
|
import type { WebSocketWithCleanup } from "./connection";
|
||||||
import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection";
|
import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection";
|
||||||
import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection";
|
import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection";
|
||||||
import { updateCalendars } from "./messaging";
|
import { updateCalendars } from "./messaging/updateCalendars";
|
||||||
import { syncCalendarRegistrations } from "./operations";
|
import { syncCalendarRegistrations } from "./operations";
|
||||||
|
|
||||||
export function WebSocketGate() {
|
export function WebSocketGate() {
|
||||||
@@ -24,9 +25,25 @@ export function WebSocketGate() {
|
|||||||
useAppSelector((state) => state?.calendars?.templist) ?? {}
|
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(
|
const onMessage = useCallback(
|
||||||
(message: unknown) => {
|
(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]
|
[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;
|
||||||
|
}
|
||||||
@@ -3,42 +3,157 @@ import { store } from "@/app/store";
|
|||||||
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services";
|
import { 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 { parseCalendarPath } from "./parseCalendarPath";
|
import { parseCalendarPath } from "./parseCalendarPath";
|
||||||
import { parseMessage } from "./parseMessage";
|
import { parseMessage } from "./parseMessage";
|
||||||
|
import { UpdateCalendarsAccumulators } from "./type/UpdateCalendarsAccumulators";
|
||||||
|
|
||||||
export function updateCalendars(message: unknown, dispatch: AppDispatch) {
|
const DEFAULT_DEBOUNCE_MS = 0;
|
||||||
const currentRange = getDisplayedCalendarRange();
|
|
||||||
|
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 state = store.getState();
|
||||||
const { calendarsToRefresh, calendarsToHide } = parseMessage(message);
|
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);
|
const calendarId = parseCalendarPath(calendarPath);
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
console.warn("Invalid calendar path received:", calendarPath);
|
console.warn("Invalid calendar path received:", calendarPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const calendar = findCalendarById(state, calendarId);
|
const calendar = findCalendarById(state, calendarId);
|
||||||
if (calendar) {
|
if (!calendar) {
|
||||||
dispatch(
|
|
||||||
refreshCalendarWithSyncToken({
|
|
||||||
calendar: calendar.calendar,
|
|
||||||
calType: calendar.type,
|
|
||||||
calendarRange: currentRange,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn("Calendar not found for id:", calendarId);
|
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]
|
function processCalendarsToRefresh(
|
||||||
.map(parseCalendarPath)
|
dispatch: AppDispatch,
|
||||||
.filter((id): id is string => Boolean(id));
|
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(
|
const updatedSelectedCalendars = currentSelectedCalendars.filter(
|
||||||
(id) => !calendarIdsToHide.includes(id)
|
(id) => !calendarsToHideSnapshot.has(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedCalendars(updatedSelectedCalendars);
|
setSelectedCalendars(updatedSelectedCalendars);
|
||||||
|
|||||||
Reference in New Issue
Block a user