Digging deeper
Events
Events let one part of your app announce that something happened, and let other parts act on it, without the two knowing about each other.
Introduction#
An event is a plain class that carries what happened: an OrderShipped event carries the order. A listener is a
class with a handle() method that the event is passed to. One event can have many listeners: when an order ships,
one listener emails the customer and another tells the warehouse, and the code that ships the order dispatches the
event and knows about neither.
Events live in app/ and listeners in app/. Neither folder exists in a new app: the make:event and
make:listener commands create them.
Generating events and listeners#
node oriel make:event OrderShippednode oriel make:listener SendShipmentNotification --event=OrderShippedmake:listener with --event writes a listener that already names its event in static listensTo, so
discovery registers it. Add --queued for a queued listener.
Registering events and listeners#
Event discovery#
When your app boots, Oriel imports every file under app/, folders included, and registers each exported
class that has a handle() method and a static listensTo field. listensTo names the event the listener handles:
an event class, an event's name as a string, or an array of either.
import { OrderShipped } from '../Events/OrderShipped'export class SendShipmentNotification { static listensTo = OrderShipped async handle(event: OrderShipped): Promise<void> { // Email the customer about event.order... }}Any other export in a listener's file, such as a helper function, is skipped.
A new app has no config/. To turn discovery off, add one: export default { discover: false }.
Manually registering events#
Event.listen() registers a listener in a provider's boot() method. The listener can be a listener class, a
[class, method] pair, or a function:
import { Event, ServiceProvider } from '@orieljs/framework'import { OrderShipped } from '../Events/OrderShipped'import { SendShipmentNotification } from '../Listeners/SendShipmentNotification'export class AppServiceProvider extends ServiceProvider { override boot(): void { Event.listen(OrderShipped, SendShipmentNotification) Event.listen(OrderShipped, (event: OrderShipped) => { // ... }) }}A listener class registered here and also discovered runs twice, once for each registration.
config/ can list them too, under listen, as [event, listeners] pairs (a TypeScript object can't take a
class as a key):
import type { EventListenMap } from '@orieljs/framework'import { OrderShipped } from '../app/Events/OrderShipped'import { SendShipmentNotification } from '../app/Listeners/SendShipmentNotification'export default { discover: true, listen: [[OrderShipped, [SendShipmentNotification]]] as EventListenMap,}Wildcard listeners#
A listener for a string event can use * as a wildcard. It is called with the event's name and the array of its
payload:
import { Event } from '@orieljs/framework'Event.listen('order.*', (eventName: string, payload?: unknown[]) => { // ...})Viewing the events#
event:list lists every event with how many listeners it has:
node oriel event:listDefining events#
An event holds the data its listeners need, and nothing else:
import type { Order } from '../Models/Order'export class OrderShipped { constructor(readonly order: Order) {}}Defining listeners#
A listener's handle() method takes the event. It may be async; the dispatch waits for it before it calls the next
listener.
Your app builds a listener class with its container each time the event is dispatched. It is built by the application,
not by the request's scope, so an inject() of a request-scoped service in a listener's field throws during a request.
Call inject() inside handle() instead.
Stopping the propagation of an event#
A listener that returns false stops the listeners after it from running.
Queued event listeners#
A listener that sends email or calls another service can run on the queue instead, so the request doesn't wait for it.
Give it a readonly shouldQueue = true field, as make:listener --queued does:
import type { ShouldQueue } from '@orieljs/framework/contracts'import { OrderShipped } from '../Events/OrderShipped'export class SendShipmentNotification implements ShouldQueue { static listensTo = OrderShipped readonly shouldQueue = true async handle(event: OrderShipped): Promise<void> { // ... }}When the event is dispatched, the listener is pushed to the queue as a job, with the event, and a worker
(node oriel queue:work) runs it. The queue component must be in your app; read the Queues page for
workers and connections.
Customizing the queue connection, name and delay#
A queued listener takes the job options as fields: connection, queue, delay, tries, backoff and timeout.
Each can be a method that returns the value. A listener with any of these fields is queued even without
shouldQueue.
export class SendShipmentNotification { readonly shouldQueue = true readonly connection = 'redis' readonly queue = 'listeners' readonly delay = 60 async handle(): Promise<void> { // ... }}Unique listeners#
A queued listener with a uniqueId() method is unique (so is one with uniqueFor, uniqueUntilProcessing or
uniqueVia(), whose id is then empty): while one of its jobs is waiting or running, another dispatch with the same id
isn't queued. uniqueId() is called with the event, when the event is dispatched:
import { OrderShipped } from '../Events/OrderShipped'export class SendShipmentNotification { static listensTo = OrderShipped readonly shouldQueue = true readonly uniqueFor = 3600 uniqueId(event: OrderShipped): string { return String(event.order.getKey()) } async handle(event: OrderShipped): Promise<void> { // ... }}A duplicate is a second job for the same listener class with the same id. The lock is the listener's, by class: the
same class listening to two events, or through two methods of a subscriber, shares one lock
for each id. uniqueFor is how many seconds the lock lasts at most, and uniqueUntilProcessing = true releases it
when the job starts rather than when it ends. The lock is a cache lock, so your app's cache store must support locks
(read the Cache page's Atomic locks); uniqueVia() returns another store's name.
Dispatching events#
Event.dispatch() dispatches an event, and the event() helper does the same. Both wait for every listener that
isn't queued, and give back the listeners' return values:
import { Event, event } from '@orieljs/framework'import { OrderShipped } from './app/Events/OrderShipped'import { Order } from './app/Models/Order'const order = await Order.findOrFail(1)await Event.dispatch(new OrderShipped(order))await event(new OrderShipped(order))A string event takes its payload as an array, which its listeners receive as arguments:
Event.dispatch('order.shipped', [order]). Event.until() stops at the first listener that returns something other
than undefined, and gives back that value.
Event subscribers#
A subscriber is one class that listens to several events. Its subscribe() method returns a map of event names to the
method, or methods, that handle them:
export class UserEventSubscriber { handleUserLogin(): void { // ... } handleUserLogout(): void { // ... } subscribe(): Record<string, string> { return { 'user.login': 'handleUserLogin', 'user.logout': 'handleUserLogout', } }}The map's keys are strings, so they name string events. For class events, subscribe() takes the dispatcher and can
call its listen() with [UserEventSubscriber, 'handleUserLogin'] pairs. Register a subscriber in a provider's
boot(), with Event.subscribe(UserEventSubscriber); discovery doesn't find subscribers.
Testing#
Event.fake() swaps the dispatcher for a fake that records events instead of running their listeners. Give it the
events to fake, and the others still run:
import { Event } from '@orieljs/framework'import { OrderShipped } from './app/Events/OrderShipped'const events = Event.fake([OrderShipped])// Ship the order...events.assertDispatched(OrderShipped)events.assertDispatchedTimes(OrderShipped, 1)events.assertNotDispatched('order.cancelled')The fake also has assertNothingDispatched(), assertListening(event, listener) and dispatched(event).
EventFake.fakeExcept([OrderShipped]), from @orieljs/, fakes every event but the ones you list.
Fakes covers fakes in full.