@plinthjs/notifications
v0.1.0
Published
Mason notifications: a multi-channel notification system (the Illuminate\Notifications equivalent).
Maintainers
Readme
@plinthjs/notifications
Mason's multi-channel notification system, the Illuminate\Notifications equivalent. Subclass
Notification, declare its channels in via(), implement the matching payload builders
(toArray, toMail, toDatabase, toBroadcast), and let a NotificationSender fan it out across
the registered channels. Every backend (database store, mail transport, broadcaster, queue) is
injected, so the package has no runtime dependencies.
Install
npm install @plinthjs/notificationsUsage
import {
DatabaseChannel,
InMemoryNotificationStore,
MailChannel,
MailMessage,
Notification,
NotificationSender,
onDemand,
type Notifiable,
} from '@plinthjs/notifications'
class InvoicePaid extends Notification {
constructor(private readonly invoiceId: number) {
super()
}
via(_notifiable: Notifiable): string[] {
return ['mail', 'database']
}
toMail(_notifiable: Notifiable): MailMessage {
return new MailMessage()
.subject('Invoice paid')
.greeting('Hello!')
.line(`Invoice #${this.invoiceId} has been paid.`)
.action('View invoice', `https://example.com/invoices/${this.invoiceId}`)
.salutation('Regards, Mason')
.success()
}
toArray(_notifiable: Notifiable): Record<string, unknown> {
return { invoiceId: this.invoiceId }
}
}
const user: Notifiable = {
routeNotificationFor: (channel) => (channel === 'mail' ? '[email protected]' : undefined),
getKey: () => 1,
}
const store = new InMemoryNotificationStore()
const sender = new NotificationSender({
mail: new MailChannel({ send: async (to, message) => smtp.deliver(to, message.render()) }),
database: new DatabaseChannel(store),
})
await sender.send(user, new InvoicePaid(42)) // one recipient
await sender.send([user, otherUser], new InvoicePaid(43)) // or many
// Notify a raw address with no model behind it.
await sender.send(onDemand({ mail: '[email protected]' }), new InvoicePaid(44))A via() channel with no registered entry throws, naming the available channels. Override
shouldSend(notifiable, channel) to skip a single channel for a single recipient.
Channels
MailChannel(transport)renderstoMail()and callstransport.send(route, message).message.render()returns the structured fields plustextand inline-styledhtml.DatabaseChannel(store)inserts{ notifiableKey, type, data }fromtoDatabase()(which defaults totoArray()).BroadcastChannel(broadcaster)pushestoBroadcast()torouteNotificationFor('broadcast'), or tonotifiable.{key}by default.ArrayChannelrecords every send in itssentarray.
Reading stored notifications
import { DatabaseNotificationCollection } from '@plinthjs/notifications'
const inbox = new DatabaseNotificationCollection(store, user.getKey())
const [latest] = await inbox.unread() // newest first
await inbox.markAsRead() // all, or markAsRead(id) for one
await inbox.markAsUnread(latest.id)
await inbox.read() // rows whose readAt is setQueueing and events
import { queueable } from '@plinthjs/notifications'
const queued = new NotificationSender(channels, { push: (job) => jobs.push(job) }, events)
await queued.send(user, queueable(new InvoicePaid(42), 5000)) // one job per recipient, 5s delay
await queued.sendNow(user, new InvoicePaid(42)) // bypass the queueThe default queue, runInlineQueue, runs jobs immediately. The optional third argument receives
NotificationSending, NotificationSent and NotificationFailed events via dispatch(event).
Testing
import { NotificationSender } from '@plinthjs/notifications'
const fake = NotificationSender.fake()
await fake.send(user, new InvoicePaid(42))
fake.assertSentTo(user, InvoicePaid, (n) => n instanceof InvoicePaid)
fake.assertSentOnChannel('mail', InvoicePaid)
fake.assertSentTimes(InvoicePaid, 1)
fake.assertNotSentTo(otherUser)