From 30fd43b0cf931483c20391b9ca7d90cde01eab4a Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 8 May 2026 15:24:35 +0200 Subject: [PATCH 1/2] fix: notifications are deduped in memory, not just in persistant storage --- src/renderer/src/moss-store.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/moss-store.ts b/src/renderer/src/moss-store.ts index 60835f62..fe540865 100644 --- a/src/renderer/src/moss-store.ts +++ b/src/renderer/src/moss-store.ts @@ -70,6 +70,7 @@ import { } from './electron-api.js'; import { devModeToolLibraryFromDevConfig, + encodeAndStringify, findAppForDnaHash, getNotificationState, sortVersionsDescending, @@ -167,6 +168,13 @@ export type DevModeToolLibrary = { devCollective: DeveloperCollective; }; +function getNotificationFeedEntryId({ source, notification }: MossNotification): string { + const sourceKey = + source.type === 'applet' ? `applet:${source.appletId}` : `group:${source.groupDnaHash}`; + + return `${sourceKey}:${encodeAndStringify(notification)}`; +} + export class MossStore { constructor( public adminWebsocket: AdminWebsocket, @@ -698,12 +706,12 @@ export class MossStore { ): Promise { const mainWindowFocused = await window.electronAPI.isMainWindowFocused(); - for (const notification of notifications) { - // 1. Persist if requested (tools do this, foyer doesn't) - if (options.persist && source.type === 'applet') { - storeAppletNotifications([notification], source.appletId, true, this.persistedStore); - } + // 1. Persist if requested (tools do this, foyer doesn't) + if (options.persist && source.type === 'applet') { + const unreadNotifications = storeAppletNotifications(notifications, source.appletId, true, this.persistedStore); + } + for (const notification of notifications) { // 2. Update unread count for sidebar dot if (options.updateUnreadCount) { if (source.type === 'applet') { @@ -731,7 +739,15 @@ export class MossStore { if (options.showInFeed) { this._notificationFeed.update((feed) => { const newNotification: MossNotification = { source, notification, sourceName: options.sourceName }; - return [newNotification, ...feed].sort( + const deduplicatedFeed = [newNotification, ...feed].filter( + (entry, index, allEntries) => + index === + allEntries.findIndex( + (candidate) => getNotificationFeedEntryId(candidate) === getNotificationFeedEntryId(entry), + ), + ); + + return deduplicatedFeed.sort( (a, b) => b.notification.timestamp - a.notification.timestamp, ); }); From 5440cdca502334e2128b4cafd1345078f9922b44 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 18 May 2026 15:55:25 +0200 Subject: [PATCH 2/2] fix: prevent n2 issue sorting notifications --- src/renderer/src/moss-store.ts | 49 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/renderer/src/moss-store.ts b/src/renderer/src/moss-store.ts index fe540865..b259675e 100644 --- a/src/renderer/src/moss-store.ts +++ b/src/renderer/src/moss-store.ts @@ -708,10 +708,23 @@ export class MossStore { // 1. Persist if requested (tools do this, foyer doesn't) if (options.persist && source.type === 'applet') { - const unreadNotifications = storeAppletNotifications(notifications, source.appletId, true, this.persistedStore); + storeAppletNotifications(notifications, source.appletId, true, this.persistedStore); } - for (const notification of notifications) { + // Deduplicate incoming notifications against what's already in the feed so that + // OS notifications, sounds, and unread counts are not triggered for duplicates. + const existingFeedIds = new Set( + get(this._notificationFeed).map((entry) => getNotificationFeedEntryId(entry)), + ); + const seenInBatch = new Set(); + const newNotifications = notifications.filter((notification) => { + const id = getNotificationFeedEntryId({ source, notification, sourceName: options.sourceName }); + if (existingFeedIds.has(id) || seenInBatch.has(id)) return false; + seenInBatch.add(id); + return true; + }); + + for (const notification of newNotifications) { // 2. Update unread count for sidebar dot if (options.updateUnreadCount) { if (source.type === 'applet') { @@ -735,24 +748,6 @@ export class MossStore { } } - // 3. Add to activity feed - if (options.showInFeed) { - this._notificationFeed.update((feed) => { - const newNotification: MossNotification = { source, notification, sourceName: options.sourceName }; - const deduplicatedFeed = [newNotification, ...feed].filter( - (entry, index, allEntries) => - index === - allEntries.findIndex( - (candidate) => getNotificationFeedEntryId(candidate) === getNotificationFeedEntryId(entry), - ), - ); - - return deduplicatedFeed.sort( - (a, b) => b.notification.timestamp - a.notification.timestamp, - ); - }); - } - // 4. Send OS notification if conditions are met if ( options.sendOSNotification && @@ -780,6 +775,20 @@ export class MossStore { } } + // 3. Add to activity feed (done once after the loop to avoid O(k*n²)) + if (options.showInFeed && newNotifications.length > 0) { + this._notificationFeed.update((feed) => { + const newEntries: MossNotification[] = newNotifications.map((notification) => ({ + source, + notification, + sourceName: options.sourceName, + })); + return [...newEntries, ...feed].sort( + (a, b) => b.notification.timestamp - a.notification.timestamp, + ); + }); + } + } /**