From bc7657f55209fd713cc3fc74f4d242c7457da2d6 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 18 Feb 2026 10:18:09 +0100 Subject: [PATCH] fix: memory leak and O(n^2) performance in last calls section - Limit missedCalls array to 50 elements to prevent unbounded growth - Replace lodash differenceWith and array.map().includes() with Set lookups for O(1) uniqueid comparison - Add cleanup for IPC event listeners in NethLinkPage and store - Remove unnecessary array spreads on every update --- .../NethVoice/LastCalls/LastCallsBox.tsx | 17 +++---- .../src/hooks/usePhoneIslandEventHandler.ts | 46 +++++++++-------- src/renderer/src/pages/NethLinkPage.tsx | 49 +++++++++++-------- src/renderer/src/store.ts | 6 ++- 4 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx index 70e0e11a..b8cf2c99 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx @@ -48,21 +48,18 @@ export function LastCallsBox({ showContactForm }): JSX.Element { const prepareCalls = () => { if (lastCalls) { + const missedCallIds = new Set(missedCalls?.map(c => c.uniqueid)) const preparedCalls: LastCallData[] = lastCalls - .map((c) => { - const elem: LastCallData = { - ...c, - username: getCallName(c), - hasNotification: - missedCalls?.map((c) => c.uniqueid).includes(c.uniqueid) || false, - } - return elem - }) + .map((c) => ({ + ...c, + username: getCallName(c), + hasNotification: missedCallIds.has(c.uniqueid), + })) .filter((call) => { const numberToCheck = call.direction === 'in' ? call.src : call.dst return !numberToCheck?.includes(audioTestCode) }) - setPreparedCalls((p) => preparedCalls) + setPreparedCalls(preparedCalls) } } diff --git a/src/renderer/src/hooks/usePhoneIslandEventHandler.ts b/src/renderer/src/hooks/usePhoneIslandEventHandler.ts index 234c0490..3059761a 100644 --- a/src/renderer/src/hooks/usePhoneIslandEventHandler.ts +++ b/src/renderer/src/hooks/usePhoneIslandEventHandler.ts @@ -8,7 +8,6 @@ import { IPC_EVENTS, PERMISSION } from "@shared/constants" import { getTimeDifference } from "@renderer/lib/dateTime" import { format, utcToZonedTime } from "date-fns-tz" import { useLoggedNethVoiceAPI } from "./useLoggedNethVoiceAPI" -import { differenceWith } from "lodash" import { t } from "i18next" import { useRefState } from "./useRefState" @@ -112,33 +111,32 @@ export const usePhoneIslandEventHandler = () => { return `${sign}${hours}${minutes}` } - const diff = differenceWith(newLastCalls.rows, lastCalls.current || [], (a, b) => a.uniqueid === b.uniqueid) - const _missedCalls: CallData[] = [ - ...(missedCalls.current || []) - ] - let missed: CallData[] = [] + const existingIds = new Set((lastCalls.current || []).map(c => c.uniqueid)) + const diff = newLastCalls.rows.filter(c => !existingIds.has(c.uniqueid)) + if (diff.length > 0) { - diff.forEach((c) => { - if (c.direction === 'in' && c.disposition === 'NO ANSWER') { - _missedCalls.push(c) - const differenceBetweenTimezone = diffValueConversation(getTimeDifference(account.current!, false)) - const timeDiff = format(utcToZonedTime(c.time! * 1000, differenceBetweenTimezone), 'HH:mm') - sendNotification(t('Notification.lost_call_title', { user: c.cnam || c.ccompany || c.src || t('Common.Unknown') }), t('Notification.lost_call_body', { number: c.src, datetime: timeDiff })) - } - }) + const newMissed = diff.filter(c => c.direction === 'in' && c.disposition === 'NO ANSWER') - setMissedCalls((p) => { - const pmap = p?.map((c) => c.uniqueid) || [] - missed = [ - ...(p || []), - ..._missedCalls.filter((c) => !pmap.includes(c.uniqueid)) - ] - return missed + newMissed.forEach((c) => { + const differenceBetweenTimezone = diffValueConversation(getTimeDifference(account.current!, false)) + const timeDiff = format(utcToZonedTime(c.time! * 1000, differenceBetweenTimezone), 'HH:mm') + sendNotification(t('Notification.lost_call_title', { user: c.cnam || c.ccompany || c.src || t('Common.Unknown') }), t('Notification.lost_call_body', { number: c.src, datetime: timeDiff })) }) + + if (newMissed.length > 0) { + const MAX_MISSED_CALLS = 50 + setMissedCalls((p) => { + const existingMissedIds = new Set((p || []).map(c => c.uniqueid)) + const uniqueNewMissed = newMissed.filter(c => !existingMissedIds.has(c.uniqueid)) + const combined = [...(p || []), ...uniqueNewMissed] + return combined.length > MAX_MISSED_CALLS + ? combined.slice(combined.length - MAX_MISSED_CALLS) + : combined + }) + } } - setLastCalls(() => [ - ...newLastCalls.rows - ]) + + setLastCalls(() => newLastCalls.rows) } const updateLastCalls = async () => { diff --git a/src/renderer/src/pages/NethLinkPage.tsx b/src/renderer/src/pages/NethLinkPage.tsx index 4c510e9b..48ae2aaa 100644 --- a/src/renderer/src/pages/NethLinkPage.tsx +++ b/src/renderer/src/pages/NethLinkPage.tsx @@ -33,16 +33,44 @@ export function NethLinkPage({ handleRefreshConnection }: NethLinkPageProps) { const accountMeInterval = useRef() useInitialize(() => { - initialize() + Log.info('INITIALIZE NETHLINK FRONTEND') + Notification.requestPermission() + .then(() => { + Log.info('requested notification permission') + }) + .catch((e) => { + Log.warning('notification permission error or unsuccessfully acquired', e) + }) }) useEffect(() => { if (account) { + window.electron.receive(IPC_EVENTS.UPDATE_APP_NOTIFICATION, showUpdateAppNotification) + window.electron.receive(IPC_EVENTS.EMIT_CALL_END, updateLastCalls) + window.electron.receive(IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE, onMainPresence) + window.electron.receive(IPC_EVENTS.EMIT_PARKING_UPDATE, updateParkings) + window.electron.receive(IPC_EVENTS.EMIT_QUEUE_UPDATE, onQueueUpdate) + window.electron.receive(IPC_EVENTS.UPDATE_ACCOUNT, updateAccountData) + window.electron.receive(IPC_EVENTS.RESPONSE_START_CALL_BY_URL, handleStartCallByUrlResponse) + window.electron.receive(IPC_EVENTS.RECONNECT_SOCKET, handleSocketReconnect) + if (!accountMeInterval.current) { accountMeInterval.current = setInterval(loadData, 1000 * 60 * 5 ) } + + return () => { + window.electron.removeAllListeners(IPC_EVENTS.UPDATE_APP_NOTIFICATION) + window.electron.removeAllListeners(IPC_EVENTS.EMIT_CALL_END) + window.electron.removeAllListeners(IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE) + window.electron.removeAllListeners(IPC_EVENTS.EMIT_PARKING_UPDATE) + window.electron.removeAllListeners(IPC_EVENTS.EMIT_QUEUE_UPDATE) + window.electron.removeAllListeners(IPC_EVENTS.UPDATE_ACCOUNT) + window.electron.removeAllListeners(IPC_EVENTS.RESPONSE_START_CALL_BY_URL) + window.electron.removeAllListeners(IPC_EVENTS.RECONNECT_SOCKET) + stopInterval(accountMeInterval) + } } else { Log.info('Account logout') stopInterval(accountMeInterval) @@ -63,25 +91,6 @@ export function NethLinkPage({ handleRefreshConnection }: NethLinkPageProps) { } } - function initialize() { - Log.info('INITIALIZE NETHLINK FRONTEND') - Notification.requestPermission() - .then(() => { - Log.info('requested notification permission') - }) - .catch((e) => { - Log.warning('notification permission error or unsuccessfully acquired', e) - }) - window.electron.receive(IPC_EVENTS.UPDATE_APP_NOTIFICATION, showUpdateAppNotification) - window.electron.receive(IPC_EVENTS.EMIT_CALL_END, updateLastCalls) - window.electron.receive(IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE, onMainPresence) - window.electron.receive(IPC_EVENTS.EMIT_PARKING_UPDATE, updateParkings) - window.electron.receive(IPC_EVENTS.EMIT_QUEUE_UPDATE, onQueueUpdate) - window.electron.receive(IPC_EVENTS.UPDATE_ACCOUNT, updateAccountData) - window.electron.receive(IPC_EVENTS.RESPONSE_START_CALL_BY_URL, handleStartCallByUrlResponse) - window.electron.receive(IPC_EVENTS.RECONNECT_SOCKET, handleSocketReconnect) - } - const handleSocketReconnect = () => { window.location.reload() } diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 25f9241b..ca086699 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -94,12 +94,16 @@ export function createGlobalStateHook(globalStateDefaultObject: T, sharedWith Log.debug('shared state received from', fromPage) Object.keys(newStore as object).forEach((k: any) => { setData(k, newStore[k]) - //global[k] = newStore[k] }) } }) Log.debug('shared state requested for the first time') window.electron.send(IPC_EVENTS.REQUEST_SHARED_STATE); + + return () => { + window.electron.removeAllListeners(IPC_EVENTS.SHARED_STATE_UPDATED) + isRegistered.current = false + } } }, [pageData]); }