NestboltNestbolt

@nestbolt/notifications

Events

Listen to notification lifecycle events -- sending, sent, failed, read, and all-read -- using @nestjs/event-emitter.

The notification system emits lifecycle events at key points during the notification process. These events allow you to hook into the notification pipeline for logging, analytics, side effects, or error handling.

Prerequisites

Events require the @nestjs/event-emitter package. Install it if you have not already:

npm install @nestjs/event-emitter

Register the EventEmitterModule in your root module:

import { Module } from "@nestjs/common";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { NotificationModule } from "@nestbolt/notifications";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    NotificationModule.forRoot({
      channels: { database: true },
    }),
  ],
})
export class AppModule {}

When @nestjs/event-emitter is not installed, the notification system works normally -- events are simply not emitted, and no errors occur.

Event Constants

All event names are available through the NOTIFICATION_EVENTS constant:

import { NOTIFICATION_EVENTS } from "@nestbolt/notifications";

NOTIFICATION_EVENTS.SENDING;  // "notification.sending"
NOTIFICATION_EVENTS.SENT;     // "notification.sent"
NOTIFICATION_EVENTS.FAILED;   // "notification.failed"
NOTIFICATION_EVENTS.READ;     // "notification.read"
NOTIFICATION_EVENTS.ALL_READ; // "notification.all-read"

Events Reference

notification.sending

Emitted before a notification is sent through a channel. This event fires once per channel -- if a notification uses both "database" and "mail", the event fires twice.

Payload: NotificationSendingEvent

interface NotificationSendingEvent {
  notifiable: NotifiableEntity;
  notification: Notification;
  channel: string;
}
PropertyTypeDescription
notifiableNotifiableEntityThe entity receiving the notification.
notificationNotificationThe notification instance.
channelstringThe channel name (e.g., "database", "mail").

Example listener:

import { Injectable } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import {
  NOTIFICATION_EVENTS,
  NotificationSendingEvent,
} from "@nestbolt/notifications";

@Injectable()
export class NotificationListener {
  @OnEvent(NOTIFICATION_EVENTS.SENDING)
  handleSending(event: NotificationSendingEvent) {
    console.log(
      `Sending "${event.notification.notificationType()}" to ${event.notifiable.getNotifiableType()}:${event.notifiable.getNotifiableId()} via ${event.channel}`,
    );
  }
}

notification.sent

Emitted after a notification has been successfully sent through a channel.

Payload: NotificationSentEvent

interface NotificationSentEvent {
  notifiable: NotifiableEntity;
  notification: Notification;
  channel: string;
}
PropertyTypeDescription
notifiableNotifiableEntityThe entity that received the notification.
notificationNotificationThe notification instance.
channelstringThe channel name.

Example listener:

@OnEvent(NOTIFICATION_EVENTS.SENT)
handleSent(event: NotificationSentEvent) {
  console.log(
    `Notification "${event.notification.notificationType()}" sent to ${event.notifiable.getNotifiableId()} via ${event.channel}`,
  );
}

notification.failed

Emitted when a notification fails to send through a channel. The error is captured and included in the event payload. After emitting this event, the error is re-thrown to the caller.

Payload: NotificationFailedEvent

interface NotificationFailedEvent {
  notifiable: NotifiableEntity;
  notification: Notification;
  channel: string;
  error: Error;
}
PropertyTypeDescription
notifiableNotifiableEntityThe entity the notification was being sent to.
notificationNotificationThe notification instance.
channelstringThe channel that failed.
errorErrorThe error that caused the failure.

Example listener:

@OnEvent(NOTIFICATION_EVENTS.FAILED)
handleFailed(event: NotificationFailedEvent) {
  console.error(
    `Notification "${event.notification.notificationType()}" failed on ${event.channel}: ${event.error.message}`,
  );

  // Report to error tracking service
  this.errorReporter.capture(event.error, {
    context: "notification",
    channel: event.channel,
    notificationType: event.notification.notificationType(),
    notifiableType: event.notifiable.getNotifiableType(),
    notifiableId: event.notifiable.getNotifiableId(),
  });
}

notification.read

Emitted when a single notification is marked as read via NotificationService.markAsRead().

Payload: NotificationReadEvent

interface NotificationReadEvent {
  notification: NotificationEntity;
}
PropertyTypeDescription
notificationNotificationEntityThe notification entity that was read.

Example listener:

@OnEvent(NOTIFICATION_EVENTS.READ)
handleRead(event: NotificationReadEvent) {
  console.log(
    `Notification ${event.notification.id} (type: ${event.notification.type}) marked as read`,
  );
}

notification.all-read

Emitted when all notifications for a notifiable entity are marked as read via NotificationService.markAllAsRead(). This event only fires if at least one notification was actually updated.

Payload: NotificationAllReadEvent

interface NotificationAllReadEvent {
  notifiableType: string;
  notifiableId: string;
}
PropertyTypeDescription
notifiableTypestringThe entity type (e.g., "User").
notifiableIdstringThe entity ID.

Example listener:

@OnEvent(NOTIFICATION_EVENTS.ALL_READ)
handleAllRead(event: NotificationAllReadEvent) {
  console.log(
    `All notifications marked as read for ${event.notifiableType}:${event.notifiableId}`,
  );
}

Complete Listener Example

Here is a complete listener class that handles all notification events:

import { Injectable, Logger } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import {
  NOTIFICATION_EVENTS,
  NotificationSendingEvent,
  NotificationSentEvent,
  NotificationFailedEvent,
  NotificationReadEvent,
  NotificationAllReadEvent,
} from "@nestbolt/notifications";

@Injectable()
export class NotificationEventListener {
  private readonly logger = new Logger(NotificationEventListener.name);

  @OnEvent(NOTIFICATION_EVENTS.SENDING)
  onSending(event: NotificationSendingEvent): void {
    this.logger.debug(
      `Sending "${event.notification.notificationType()}" to ${event.notifiable.getNotifiableType()}:${event.notifiable.getNotifiableId()} via ${event.channel}`,
    );
  }

  @OnEvent(NOTIFICATION_EVENTS.SENT)
  onSent(event: NotificationSentEvent): void {
    this.logger.log(
      `Sent "${event.notification.notificationType()}" to ${event.notifiable.getNotifiableType()}:${event.notifiable.getNotifiableId()} via ${event.channel}`,
    );
  }

  @OnEvent(NOTIFICATION_EVENTS.FAILED)
  onFailed(event: NotificationFailedEvent): void {
    this.logger.error(
      `Failed "${event.notification.notificationType()}" on ${event.channel}: ${event.error.message}`,
      event.error.stack,
    );
  }

  @OnEvent(NOTIFICATION_EVENTS.READ)
  onRead(event: NotificationReadEvent): void {
    this.logger.debug(
      `Notification ${event.notification.id} marked as read`,
    );
  }

  @OnEvent(NOTIFICATION_EVENTS.ALL_READ)
  onAllRead(event: NotificationAllReadEvent): void {
    this.logger.debug(
      `All notifications read for ${event.notifiableType}:${event.notifiableId}`,
    );
  }
}

Register the listener as a provider in your module:

import { Module } from "@nestjs/common";
import { NotificationEventListener } from "./listeners/notification-event.listener";

@Module({
  providers: [NotificationEventListener],
})
export class AppModule {}

Use Cases

Analytics and Metrics

Track notification delivery rates and performance:

@Injectable()
export class NotificationMetricsListener {
  constructor(private readonly metrics: MetricsService) {}

  @OnEvent(NOTIFICATION_EVENTS.SENT)
  onSent(event: NotificationSentEvent): void {
    this.metrics.increment("notifications.sent", {
      type: event.notification.notificationType(),
      channel: event.channel,
    });
  }

  @OnEvent(NOTIFICATION_EVENTS.FAILED)
  onFailed(event: NotificationFailedEvent): void {
    this.metrics.increment("notifications.failed", {
      type: event.notification.notificationType(),
      channel: event.channel,
    });
  }
}

Audit Logging

Log all notification activity for compliance:

@Injectable()
export class NotificationAuditListener {
  constructor(private readonly auditService: AuditService) {}

  @OnEvent(NOTIFICATION_EVENTS.SENT)
  async onSent(event: NotificationSentEvent): Promise<void> {
    await this.auditService.log({
      action: "notification.sent",
      entityType: event.notifiable.getNotifiableType(),
      entityId: event.notifiable.getNotifiableId(),
      metadata: {
        notificationType: event.notification.notificationType(),
        channel: event.channel,
      },
    });
  }
}

Real-Time Updates via WebSocket

Push real-time notification updates to connected clients:

@Injectable()
export class NotificationWebSocketListener {
  constructor(private readonly gateway: NotificationsGateway) {}

  @OnEvent(NOTIFICATION_EVENTS.SENT)
  onSent(event: NotificationSentEvent): void {
    if (event.channel === "database") {
      this.gateway.sendToUser(event.notifiable.getNotifiableId(), {
        type: "notification:new",
        notification: event.notification.notificationType(),
      });
    }
  }

  @OnEvent(NOTIFICATION_EVENTS.READ)
  onRead(event: NotificationReadEvent): void {
    this.gateway.sendToUser(event.notification.notifiableId, {
      type: "notification:read",
      notificationId: event.notification.id,
    });
  }

  @OnEvent(NOTIFICATION_EVENTS.ALL_READ)
  onAllRead(event: NotificationAllReadEvent): void {
    this.gateway.sendToUser(event.notifiableId, {
      type: "notification:all-read",
    });
  }
}

Retry on Failure

Implement retry logic for failed notifications:

@Injectable()
export class NotificationRetryListener {
  private readonly logger = new Logger(NotificationRetryListener.name);

  constructor(private readonly notificationService: NotificationService) {}

  @OnEvent(NOTIFICATION_EVENTS.FAILED)
  async onFailed(event: NotificationFailedEvent): Promise<void> {
    this.logger.warn(
      `Notification failed on ${event.channel}, scheduling retry...`,
    );

    // Simple retry with delay
    setTimeout(async () => {
      try {
        await this.notificationService.send(
          event.notifiable,
          event.notification,
        );
      } catch (error) {
        this.logger.error("Retry also failed", error);
      }
    }, 5000);
  }
}

Event Flow

Here is the sequence of events for a typical notification sent through two channels:

notificationService.send(user, notification)
  |
  |-- [channel: "database"]
  |     |-- emit("notification.sending", { notifiable, notification, channel: "database" })
  |     |-- DatabaseChannel.send()
  |     |-- emit("notification.sent", { notifiable, notification, channel: "database" })
  |
  |-- [channel: "mail"]
        |-- emit("notification.sending", { notifiable, notification, channel: "mail" })
        |-- MailChannel.send()
        |-- emit("notification.sent", { notifiable, notification, channel: "mail" })

// If MailChannel.send() throws:
        |-- emit("notification.failed", { notifiable, notification, channel: "mail", error })
        |-- Error is re-thrown

Note that channels are processed sequentially. If an earlier channel fails, subsequent channels in the via() list are not attempted.