@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-emitterRegister 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;
}| Property | Type | Description |
|---|---|---|
notifiable | NotifiableEntity | The entity receiving the notification. |
notification | Notification | The notification instance. |
channel | string | The 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;
}| Property | Type | Description |
|---|---|---|
notifiable | NotifiableEntity | The entity that received the notification. |
notification | Notification | The notification instance. |
channel | string | The 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;
}| Property | Type | Description |
|---|---|---|
notifiable | NotifiableEntity | The entity the notification was being sent to. |
notification | Notification | The notification instance. |
channel | string | The channel that failed. |
error | Error | The 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;
}| Property | Type | Description |
|---|---|---|
notification | NotificationEntity | The 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;
}| Property | Type | Description |
|---|---|---|
notifiableType | string | The entity type (e.g., "User"). |
notifiableId | string | The 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-thrownNote that channels are processed sequentially. If an earlier channel fails, subsequent channels in the via() list are not attempted.