import type { NotificationClassification, NotificationProcessingResult, NotificationUiConfig, PushNotificationPayload, } from "../../types"; import type { Logger } from "../../utils/logger"; import { NotificationClassifier } from "./NotificationClassifier"; import type { SirrusReactNativeSDK } from "../../SirrusReactNativeSDK"; export class NotificationEngine { private readonly classifier: NotificationClassifier; private readonly recentlyReceivedMessageIds = new Map(); private readonly receiveDedupeWindowMs = 5000; constructor( private readonly sdk: SirrusReactNativeSDK, private readonly logger: Logger, private readonly config: NotificationUiConfig = {} ) { this.classifier = new NotificationClassifier( this.config.handlingMode ?? "sirrus-only", this.config.sourceMatcher ); } classify(payload: PushNotificationPayload): NotificationClassification { return this.classifier.classify(payload); } async processReceived( payload: PushNotificationPayload, options: { displayForegroundNotification?: boolean; displaySuppressionReason?: string; } = {} ): Promise { const classification = this.classify(payload); if (!classification.matched) { this.logger.info("Ignoring non-Sirrus notification receive", classification); return { handled: false, displayed: false, opened: false, classification, }; } const dedupeKey = payload.messageId; if (dedupeKey) { const now = Date.now(); this.pruneRecentlyReceived(now); const previousReceivedAt = this.recentlyReceivedMessageIds.get(dedupeKey); if ( previousReceivedAt && now - previousReceivedAt < this.receiveDedupeWindowMs ) { this.logger.info("Skipping duplicate Sirrus notification receive", { messageId: payload.messageId, campaignId: payload.campaignId, }); return { handled: true, displayed: false, opened: false, classification, }; } this.recentlyReceivedMessageIds.set(dedupeKey, now); } this.logger.info("Sirrus notification receive matched", { campaignId: payload.campaignId, messageId: payload.messageId, ownership: classification.ownership, }); let displayed = false; const shouldDisplay = this.config.showForegroundNotifications && options.displayForegroundNotification !== false; if (shouldDisplay) { displayed = await this.displayForegroundNotification(payload); } else if (this.config.showForegroundNotifications) { this.logger.info("Sirrus foreground notification display suppressed", { campaignId: payload.campaignId, messageId: payload.messageId, reason: options.displaySuppressionReason ?? "display_disabled", }); } await this.sdk.handlePushReceivedInternally(payload).catch((error) => { this.logger.warn("Sirrus push received tracking failed", error); }); this.logger.info("Sirrus notification receive processed", { campaignId: payload.campaignId, messageId: payload.messageId, displayed, }); return { handled: true, displayed, opened: false, classification, }; } async processOpened( payload: PushNotificationPayload ): Promise { const classification = this.classify(payload); if (!classification.matched) { this.logger.info("Ignoring non-Sirrus notification open", classification); return { handled: false, displayed: false, opened: false, classification, }; } this.logger.info("Sirrus notification open matched", { campaignId: payload.campaignId, messageId: payload.messageId, ownership: classification.ownership, }); await this.sdk.handlePushOpenedInternally(payload); this.logger.info("Sirrus notification open processed", { campaignId: payload.campaignId, messageId: payload.messageId, }); return { handled: true, displayed: false, opened: true, classification, }; } private async displayForegroundNotification( payload: PushNotificationPayload ): Promise { try { await this.sdk.showNotificationInternally(payload); return true; } catch (error) { this.logger.warn("Sirrus foreground native notification display failed", error); return false; } } private pruneRecentlyReceived(now: number): void { for (const [messageId, receivedAt] of this.recentlyReceivedMessageIds) { if (now - receivedAt > this.receiveDedupeWindowMs) { this.recentlyReceivedMessageIds.delete(messageId); } } } }