perf(renderer): cache team message filter data
This commit is contained in:
parent
5d129ce822
commit
5f5b3dc62c
2 changed files with 120 additions and 32 deletions
|
|
@ -19,6 +19,30 @@ export interface TeamMessagesFilter {
|
||||||
showNoise: boolean;
|
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 {
|
function normalizeMessageText(value: string | undefined): string {
|
||||||
return (value ?? '')
|
return (value ?? '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -41,6 +65,47 @@ function normalizeLeadNames(values: Iterable<string> | undefined): Set<string> {
|
||||||
return normalized;
|
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 {
|
function isLeadAlias(value: string | undefined): boolean {
|
||||||
const normalized = normalizeParticipant(value).replace(/[\s_]+/g, '-');
|
const normalized = normalizeParticipant(value).replace(/[\s_]+/g, '-');
|
||||||
return (
|
return (
|
||||||
|
|
@ -65,25 +130,27 @@ function isRelayDuplicateOfVisibleMessage(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInboxNoiseMessage(message.text)) {
|
const messageData = getMessageFilterData(message);
|
||||||
|
if (messageData.isNoiseMessage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const originalData = getMessageFilterData(original);
|
||||||
|
|
||||||
const isInternalLeadRelayDelivery =
|
const isInternalLeadRelayDelivery =
|
||||||
(message.source === 'runtime_delivery' || message.source === 'lead_process') &&
|
(message.source === 'runtime_delivery' || message.source === 'lead_process') &&
|
||||||
original.source === 'user_sent' &&
|
original.source === 'user_sent' &&
|
||||||
normalizeParticipant(original.from) === 'user' &&
|
originalData.normalizedFrom === 'user' &&
|
||||||
isLeadParticipant(original.to, leadNames) &&
|
isLeadParticipant(original.to, leadNames) &&
|
||||||
isLeadParticipant(message.from, leadNames) &&
|
isLeadParticipant(message.from, leadNames) &&
|
||||||
normalizeParticipant(message.to) !== 'user';
|
messageData.normalizedTo !== 'user';
|
||||||
|
|
||||||
if (isInternalLeadRelayDelivery) {
|
if (isInternalLeadRelayDelivery) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameDirection =
|
const sameDirection =
|
||||||
normalizeParticipant(message.from) === normalizeParticipant(original.from) &&
|
messageData.normalizedFrom === originalData.normalizedFrom &&
|
||||||
normalizeParticipant(message.to) === normalizeParticipant(original.to);
|
messageData.normalizedTo === originalData.normalizedTo;
|
||||||
|
|
||||||
if (!sameDirection) {
|
if (!sameDirection) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -93,7 +160,7 @@ function isRelayDuplicateOfVisibleMessage(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeMessageText(message.text) === normalizeMessageText(original.text);
|
return messageData.normalizedText === originalData.normalizedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuntimeDeliveryRelayDuplicateKey(
|
function getRuntimeDeliveryRelayDuplicateKey(
|
||||||
|
|
@ -103,9 +170,10 @@ function getRuntimeDeliveryRelayDuplicateKey(
|
||||||
if (message.source !== 'runtime_delivery') {
|
if (message.source !== 'runtime_delivery') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const from = normalizeParticipant(message.from);
|
const data = getMessageFilterData(message);
|
||||||
const to = normalizeParticipant(message.to);
|
const from = data.normalizedFrom;
|
||||||
const text = normalizeMessageText(message.text);
|
const to = data.normalizedTo;
|
||||||
|
const text = data.normalizedText;
|
||||||
if (!from || !to || !text) {
|
if (!from || !to || !text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -135,14 +203,16 @@ export function filterTeamMessages(
|
||||||
} = options;
|
} = options;
|
||||||
const leadNames = normalizeLeadNames(rawLeadNames);
|
const leadNames = normalizeLeadNames(rawLeadNames);
|
||||||
|
|
||||||
let list = messages.filter(
|
let list = messages.filter((m) => {
|
||||||
(m) =>
|
const data = getMessageFilterData(m);
|
||||||
m.messageKind !== 'task_comment_notification' &&
|
return (
|
||||||
(includeAutomationEvents || !isTaskStallRemediationMessage(m)) &&
|
!data.isTaskCommentNotification &&
|
||||||
(includeMemberWorkSyncNudges || !isMemberWorkSyncNudgeMessage(m)) &&
|
(includeAutomationEvents || !data.isTaskStallRemediation) &&
|
||||||
!isReviewPickupEscalationMessage(m) &&
|
(includeMemberWorkSyncNudges || !data.isMemberWorkSyncNudge) &&
|
||||||
!isTeamInternalControlMessageEnvelope(m)
|
!data.isReviewPickupEscalation &&
|
||||||
);
|
!data.isInternalControlEnvelope
|
||||||
|
);
|
||||||
|
});
|
||||||
if (timeWindow) {
|
if (timeWindow) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
const ts = new Date(m.timestamp).getTime();
|
const ts = new Date(m.timestamp).getTime();
|
||||||
|
|
@ -151,12 +221,9 @@ export function filterTeamMessages(
|
||||||
}
|
}
|
||||||
if (!filter.showNoise) {
|
if (!filter.showNoise) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
const text = typeof m.text === 'string' ? m.text : '';
|
const data = getMessageFilterData(m);
|
||||||
if (!isInboxNoiseMessage(text)) return true;
|
if (!data.isNoiseMessage) return true;
|
||||||
return (
|
return includePassiveIdlePeerSummariesWhenNoiseHidden && data.keepIdleWhenNoiseHidden;
|
||||||
includePassiveIdlePeerSummariesWhenNoiseHidden &&
|
|
||||||
shouldKeepIdleMessageInActivityWhenNoiseHidden(text)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,14 +231,16 @@ export function filterTeamMessages(
|
||||||
const hasTo = filter.to.size > 0;
|
const hasTo = filter.to.size > 0;
|
||||||
if (hasFrom && hasTo) {
|
if (hasFrom && hasTo) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
const fromMatch = Boolean(m.from?.trim() && filter.from.has(m.from.trim()));
|
const data = getMessageFilterData(m);
|
||||||
const toMatch = Boolean(m.to?.trim() && filter.to.has(m.to.trim()));
|
const fromMatch = Boolean(data.trimmedFrom && filter.from.has(data.trimmedFrom));
|
||||||
|
const toMatch = Boolean(data.trimmedTo && filter.to.has(data.trimmedTo));
|
||||||
return fromMatch && toMatch;
|
return fromMatch && toMatch;
|
||||||
});
|
});
|
||||||
} else if (hasFrom || hasTo) {
|
} else if (hasFrom || hasTo) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
if (hasFrom) return Boolean(m.from?.trim() && filter.from.has(m.from.trim()));
|
const data = getMessageFilterData(m);
|
||||||
if (hasTo) return Boolean(m.to?.trim() && filter.to.has(m.to.trim()));
|
if (hasFrom) return Boolean(data.trimmedFrom && filter.from.has(data.trimmedFrom));
|
||||||
|
if (hasTo) return Boolean(data.trimmedTo && filter.to.has(data.trimmedTo));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +259,7 @@ export function filterTeamMessages(
|
||||||
const visibleMessagesById = new Map(
|
const visibleMessagesById = new Map(
|
||||||
list
|
list
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
const id = typeof m.messageId === 'string' ? m.messageId.trim() : '';
|
const id = getMessageFilterData(m).trimmedMessageId;
|
||||||
return id ? ([id, m] as const) : null;
|
return id ? ([id, m] as const) : null;
|
||||||
})
|
})
|
||||||
.filter((entry): entry is readonly [string, InboxMessage] => entry !== null)
|
.filter((entry): entry is readonly [string, InboxMessage] => entry !== null)
|
||||||
|
|
@ -204,8 +273,8 @@ export function filterTeamMessages(
|
||||||
if (!relayOfMessageId) {
|
if (!relayOfMessageId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const ownMessageId = typeof m.messageId === 'string' ? m.messageId.trim() : '';
|
const data = getMessageFilterData(m);
|
||||||
if (relayOfMessageId === ownMessageId) {
|
if (relayOfMessageId === data.trimmedMessageId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const runtimeDuplicateKey = getRuntimeDeliveryRelayDuplicateKey(m, relayOfMessageId);
|
const runtimeDuplicateKey = getRuntimeDeliveryRelayDuplicateKey(m, relayOfMessageId);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import type { InboxMessage } from '@shared/types';
|
import type { InboxMessage } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -500,6 +499,26 @@ Messages:
|
||||||
expect(result[0].messageId).toBe('msg-2');
|
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', () => {
|
it('can preserve passive peer-summary idle rows in the activity sink while keeping pure heartbeat hidden even after read', () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
makeMessage({
|
makeMessage({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue