perf(renderer): cache team message filter data

This commit is contained in:
777genius 2026-05-31 07:01:27 +03:00
parent 5d129ce822
commit 5f5b3dc62c
2 changed files with 120 additions and 32 deletions

View file

@ -19,6 +19,30 @@ export interface TeamMessagesFilter {
showNoise: boolean;
}
interface CachedMessageFilterData {
readonly messageKind: InboxMessage['messageKind'];
readonly source: InboxMessage['source'];
readonly from: InboxMessage['from'];
readonly to: InboxMessage['to'];
readonly messageId: InboxMessage['messageId'];
readonly text: InboxMessage['text'];
readonly trimmedMessageId: string;
readonly trimmedFrom: string;
readonly trimmedTo: string;
readonly normalizedFrom: string;
readonly normalizedTo: string;
readonly normalizedText: string;
readonly isTaskCommentNotification: boolean;
readonly isTaskStallRemediation: boolean;
readonly isMemberWorkSyncNudge: boolean;
readonly isReviewPickupEscalation: boolean;
readonly isInternalControlEnvelope: boolean;
readonly isNoiseMessage: boolean;
readonly keepIdleWhenNoiseHidden: boolean;
}
const messageFilterDataCache = new WeakMap<InboxMessage, CachedMessageFilterData>();
function normalizeMessageText(value: string | undefined): string {
return (value ?? '')
.trim()
@ -41,6 +65,47 @@ function normalizeLeadNames(values: Iterable<string> | undefined): Set<string> {
return normalized;
}
function getMessageFilterData(message: InboxMessage): CachedMessageFilterData {
const cached = messageFilterDataCache.get(message);
if (
cached &&
cached.messageKind === message.messageKind &&
cached.source === message.source &&
cached.from === message.from &&
cached.to === message.to &&
cached.messageId === message.messageId &&
cached.text === message.text
) {
return cached;
}
const text = typeof message.text === 'string' ? message.text : '';
const isNoiseMessage = isInboxNoiseMessage(text);
const next: CachedMessageFilterData = {
messageKind: message.messageKind,
source: message.source,
from: message.from,
to: message.to,
messageId: message.messageId,
text: message.text,
trimmedMessageId: typeof message.messageId === 'string' ? message.messageId.trim() : '',
trimmedFrom: typeof message.from === 'string' ? message.from.trim() : '',
trimmedTo: typeof message.to === 'string' ? message.to.trim() : '',
normalizedFrom: normalizeParticipant(message.from),
normalizedTo: normalizeParticipant(message.to),
normalizedText: normalizeMessageText(message.text),
isTaskCommentNotification: message.messageKind === 'task_comment_notification',
isTaskStallRemediation: isTaskStallRemediationMessage(message),
isMemberWorkSyncNudge: isMemberWorkSyncNudgeMessage(message),
isReviewPickupEscalation: isReviewPickupEscalationMessage(message),
isInternalControlEnvelope: isTeamInternalControlMessageEnvelope(message),
isNoiseMessage,
keepIdleWhenNoiseHidden: isNoiseMessage && shouldKeepIdleMessageInActivityWhenNoiseHidden(text),
};
messageFilterDataCache.set(message, next);
return next;
}
function isLeadAlias(value: string | undefined): boolean {
const normalized = normalizeParticipant(value).replace(/[\s_]+/g, '-');
return (
@ -65,25 +130,27 @@ function isRelayDuplicateOfVisibleMessage(
return false;
}
if (isInboxNoiseMessage(message.text)) {
const messageData = getMessageFilterData(message);
if (messageData.isNoiseMessage) {
return true;
}
const originalData = getMessageFilterData(original);
const isInternalLeadRelayDelivery =
(message.source === 'runtime_delivery' || message.source === 'lead_process') &&
original.source === 'user_sent' &&
normalizeParticipant(original.from) === 'user' &&
originalData.normalizedFrom === 'user' &&
isLeadParticipant(original.to, leadNames) &&
isLeadParticipant(message.from, leadNames) &&
normalizeParticipant(message.to) !== 'user';
messageData.normalizedTo !== 'user';
if (isInternalLeadRelayDelivery) {
return true;
}
const sameDirection =
normalizeParticipant(message.from) === normalizeParticipant(original.from) &&
normalizeParticipant(message.to) === normalizeParticipant(original.to);
messageData.normalizedFrom === originalData.normalizedFrom &&
messageData.normalizedTo === originalData.normalizedTo;
if (!sameDirection) {
return false;
@ -93,7 +160,7 @@ function isRelayDuplicateOfVisibleMessage(
return true;
}
return normalizeMessageText(message.text) === normalizeMessageText(original.text);
return messageData.normalizedText === originalData.normalizedText;
}
function getRuntimeDeliveryRelayDuplicateKey(
@ -103,9 +170,10 @@ function getRuntimeDeliveryRelayDuplicateKey(
if (message.source !== 'runtime_delivery') {
return null;
}
const from = normalizeParticipant(message.from);
const to = normalizeParticipant(message.to);
const text = normalizeMessageText(message.text);
const data = getMessageFilterData(message);
const from = data.normalizedFrom;
const to = data.normalizedTo;
const text = data.normalizedText;
if (!from || !to || !text) {
return null;
}
@ -135,14 +203,16 @@ export function filterTeamMessages(
} = options;
const leadNames = normalizeLeadNames(rawLeadNames);
let list = messages.filter(
(m) =>
m.messageKind !== 'task_comment_notification' &&
(includeAutomationEvents || !isTaskStallRemediationMessage(m)) &&
(includeMemberWorkSyncNudges || !isMemberWorkSyncNudgeMessage(m)) &&
!isReviewPickupEscalationMessage(m) &&
!isTeamInternalControlMessageEnvelope(m)
);
let list = messages.filter((m) => {
const data = getMessageFilterData(m);
return (
!data.isTaskCommentNotification &&
(includeAutomationEvents || !data.isTaskStallRemediation) &&
(includeMemberWorkSyncNudges || !data.isMemberWorkSyncNudge) &&
!data.isReviewPickupEscalation &&
!data.isInternalControlEnvelope
);
});
if (timeWindow) {
list = list.filter((m) => {
const ts = new Date(m.timestamp).getTime();
@ -151,12 +221,9 @@ export function filterTeamMessages(
}
if (!filter.showNoise) {
list = list.filter((m) => {
const text = typeof m.text === 'string' ? m.text : '';
if (!isInboxNoiseMessage(text)) return true;
return (
includePassiveIdlePeerSummariesWhenNoiseHidden &&
shouldKeepIdleMessageInActivityWhenNoiseHidden(text)
);
const data = getMessageFilterData(m);
if (!data.isNoiseMessage) return true;
return includePassiveIdlePeerSummariesWhenNoiseHidden && data.keepIdleWhenNoiseHidden;
});
}
@ -164,14 +231,16 @@ export function filterTeamMessages(
const hasTo = filter.to.size > 0;
if (hasFrom && hasTo) {
list = list.filter((m) => {
const fromMatch = Boolean(m.from?.trim() && filter.from.has(m.from.trim()));
const toMatch = Boolean(m.to?.trim() && filter.to.has(m.to.trim()));
const data = getMessageFilterData(m);
const fromMatch = Boolean(data.trimmedFrom && filter.from.has(data.trimmedFrom));
const toMatch = Boolean(data.trimmedTo && filter.to.has(data.trimmedTo));
return fromMatch && toMatch;
});
} else if (hasFrom || hasTo) {
list = list.filter((m) => {
if (hasFrom) return Boolean(m.from?.trim() && filter.from.has(m.from.trim()));
if (hasTo) return Boolean(m.to?.trim() && filter.to.has(m.to.trim()));
const data = getMessageFilterData(m);
if (hasFrom) return Boolean(data.trimmedFrom && filter.from.has(data.trimmedFrom));
if (hasTo) return Boolean(data.trimmedTo && filter.to.has(data.trimmedTo));
return true;
});
}
@ -190,7 +259,7 @@ export function filterTeamMessages(
const visibleMessagesById = new Map(
list
.map((m) => {
const id = typeof m.messageId === 'string' ? m.messageId.trim() : '';
const id = getMessageFilterData(m).trimmedMessageId;
return id ? ([id, m] as const) : null;
})
.filter((entry): entry is readonly [string, InboxMessage] => entry !== null)
@ -204,8 +273,8 @@ export function filterTeamMessages(
if (!relayOfMessageId) {
return true;
}
const ownMessageId = typeof m.messageId === 'string' ? m.messageId.trim() : '';
if (relayOfMessageId === ownMessageId) {
const data = getMessageFilterData(m);
if (relayOfMessageId === data.trimmedMessageId) {
return true;
}
const runtimeDuplicateKey = getRuntimeDeliveryRelayDuplicateKey(m, relayOfMessageId);

View file

@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { describe, expect, it } from 'vitest';
import type { InboxMessage } from '@shared/types';
@ -500,6 +499,26 @@ Messages:
expect(result[0].messageId).toBe('msg-2');
});
it('recomputes cached message classification when mutable message fields change', () => {
const message = makeMessage({
messageId: 'mutable-message',
text: '{"type":"idle_notification","idleReason":"available"}',
});
const options = {
timeWindow: null,
filter: { from: new Set<string>(), to: new Set<string>(), showNoise: false },
searchQuery: '',
};
expect(filterTeamMessages([message], options)).toEqual([]);
message.text = 'Real visible message';
expect(filterTeamMessages([message], options).map((item) => item.messageId)).toEqual([
'mutable-message',
]);
});
it('can preserve passive peer-summary idle rows in the activity sink while keeping pure heartbeat hidden even after read', () => {
const messages = [
makeMessage({