Events
Intent offers a simple yet useful implementation of the observer pattern, allowing you to listen to various events that occur within your application. Event classes are typically stored in the app/events
directory, and listeners are stored in the app/events/listeners
directory. When you create your first event and listeners, these directories will get created automatically.
Events provide a great mechanism to decouple your business logic which do not depend on each other. Let's understand it better with the help of an example: Let's say you are building a book store, when a user orders a book, we want to send them a notification.
- Send "Order Successful" notification.
- Send a notification to Seller as well.
- Do some 3rd party API calls
See how the above mentioned points, do not depend on each other, they can run independently and complete their own specified task. Instead of coupling the logic, we can simply trigger an OrderPlaced
event which listeners can receive and use to dispatch notifications.
Generating Events and Listeners
Using intent command
To quickly generate events and listeners, you can make use of make:event
and make:listener
commands. The events and listeners by default are stored inside the app/events
and app/events/listeners
directories respectively.
node intent make:event order_placednode intent make:listener order_placed --event=order_placed
This would generate the following files and classes.
import { EmitsEvent, Event } from '@intentjs/core';@Event('order_placed')export class OrderPlacedEvent extends EmitsEvent { constructor() { super(); }}
import { Injectable, ListensTo } from '@intentjs/core';@Injectable()export class OrderPlacedListener { @ListensTo('order_placed') async handle(data: Record<string, any>): Promise<void> { // write your code here... }}
Also, the listener class automatically gets registered inside the module.ts
file as a provider.
Defining Events
To create an event class, you can create a class similar to below inside the app/events
directory. An event class is a data container which holds the information related to the event
import { EmitsEvent, Event } from '@intentjs/core';@Event('order_placed')export class OrderPlacedEvent extends EmitsEvent { constructor() { super(); }}
As you can see, the event class doesn't contain any logic, and whatever data we pass to this event shall automatically be passed to all listeners. Event class has a few configuration options available, which we will see in later sections.
Defining Listeners
To create a listener for a particular event, we can utilize the @ListensTo
decorator from the @intentjs/core
package.
All of the listeners only receive the normalised form of the all the data passed to the event.
import { Injectable, ListensTo } from '@intentjs/core';@Injectable()export class OrderPlacedListener { @ListensTo('order_placed') async handle(data: Record<string, any>): Promise<void> { // write your code here... }}
Since the listener classes needs to be discovered, you will have to register it inside the providers
array in the module.ts
file.
Intent will automatically scan the class and store it as a listener for the specific event. Also, it will look for handle
method inside the listener class at the time of booting up the application.
Dispatching Events
To dispatch an event, you can make either use of the emit()
method on the event class, or Emit()
helper method. You can access the emit
method after creating an instance of the event class.
Let's take a look on how to use the emit
method.
const order = { id: 123, product: "A book" };const event = new OrderPlacedEvent(order);event.emit();
If you would like to conditionally dispatch an event, you can make use of either emitIf
or emitUnless
method.
As the name suggest, emitIf
will only emit event if the condition
evaluates to true
.
const order = { id: 123, product: "A book" };const event = new OrderPlacedEvent(order);event.emitIf(condition);
Unlike emitIf
, if you would like to only emit an event if the condition
evalues to false
, then you can use emitUnless
method.
const order = { id: 123, product: "A book" };const event = new OrderPlacedEvent(order);event.emitUnless(condition);
For a better readability, Intent also ships and Emit
method which accepts multiple events which can emit simulataneously.
import { Emit } from '@intentjs/core';Emit( new OrderPlacedEvent(order); new SomeOtherEvent(order);)
Similar to the emitIf
and emitUnless
method, you can use also EmitIf
and EmitUnless
methods to conditionally dispatch your events. The only change in this is that you pass the condition as the first logic. This helper method helps you avoid the condition
multiple times.
import { Emit, EmitUnless } from '@intentjs/core';EmitIf( condition, new OrderPlacedEvent(order), new SomeOtherEvent(order));EmitUnless( condition, new OrderPlacedEvent(order), new SomeOtherEvent(order));
Queuable Events
If your listeners are going to perform slow tasks like sending notifications, emails or making an HTTP request, Queueable Events can be beneficial for you. Events internally make use of the Intent Queue to do so.
Making an events queueable means it will be processed automatically via the configured queue. To make an event to go through queue, you will need to implement QueueableEvent
interface and implement the shouldBeQueued
method.
Doing so, you may define connection
, queue
, or delay
properties.
import { Event, QueueableEvent } from "@intentjs/core";export class OrderPlacedEvent extends Event implements QueueableEvent { /** * The name of the connection on which the event should be sent to. * Leave it blank to use the configured queue for the connection. */ public connection = "sqs"; /** * The name of the queue on which the event should be sent to. * Leave it blank to use the configured queue for the connection. */ public queue = "order_notifications"; /** * The delay(in seconds) before the job should be processed. */ public delay = 50; constructor(public order: OrderModel) { // your logic here. } shouldBeQueued(): boolean | JobOptions { return true; }}
If you would like to define the event's queue connection, queue name, or delay at the runtime, you can return an object implementation of JobOptions
from the shouldBeQueued
method.
shouldBeQueued(): boolean | JobOptions { return { connection: 'redis', delay: 0, };}
Conditionally queueing events
Just like emitIf
method, you can make use of shouldQueue
method inside the event class to determine if the event should be queued and processed or not. Returning true
will mean the event will be queued and processed, and false
means it will neither be queued, nor be processed.
shouldBeQueued(): boolean { return this.order.isConfirmed;}