📩 Event Subscribers
EventSubscribers are objects that are notified by an EventBus once a given event happens, and certain conditions
specified by the subscriber happen.
Subscribers don't depend on a specific bot or bus, and you can reuse the same instance for many.
To get the bot that is calling an event, use the EventDispatchMeta#getBot() method on the meta passed for the event.
Note it can be null if the bus doesn't have a bot. You can pass true to this method to throw an error if it's not present.
In practice, all built-in event buses have a bot, it will only be null for your custom ones where you didn't pass a bot.
The subscriber is also notified once it's subscribed to a bus (which contains the bot), on
EventSubscriber#onSubscribe() , same when unsubscribed, on #onUnsubscribe().
Override these methods to perform setup or cleanup logic on the bus/bot.
👷 Creation
You can create an event subscriber by either:
- Extending
AbstractEventSubscriberfrom@framework(recommended). - Instantiating a
SubscriberCallbackWrapperfrom@framework, passing a callback function. - Implementing the
EventSubscriberinterface from@core.
- Extending AbstractEventSubscriber
- Instantiating SubscriberCallbackWrapper
- Implementing EventSubscriber
import { AbstractEventSubscriber } from '@nyx-discord/framework';
import type { EventDispatchMeta } from '@nyx-discord/core';
class MyEventSubscriber extends AbstractEventSubscriber<MyEventsArgs, 'someEvent'> {
protected readonly event = 'someEvent';
public handleEvent(meta: EventDispatchMeta, ...args: MyEventsArgs['someEvent']) {
// Null if you didn't pass a bot while creating your bus
const bot = meta.getBot();
if (!bot) {
console.log('Hello world');
return;
}
bot.getLogger().log('Hello world');
}
}
const frameworkSubscriber = new MyEventSubscriber();
await myBus.subscribe(frameworkSubscriber);
import { SubscriberCallbackWrapper } from '@nyx-discord/framework';
const callbackSubscriber = new SubscriberCallbackWrapper < MyEventsArgs, 'someEvent'>(
'someEvent',
(meta: EventDispatchMeta, ...args: MyEventsArgs['someEvent']) => {
// Null if you didn't pass a bot while creating your bus
const bot = meta.getBot();
if (!bot) {
console.log('Hello world');
return;
}
bot.getLogger().log('Hello world');
}
);
await myBus.subscribe(callbackSubscriber);
import { EventSubscriber } from '@nyx-discord/core';
class MyInterfaceEventSubscriber implements EventSubscriber {
protected readonly event = 'someEvent';
public handleEvent(meta: EventDispatchMeta, ...args: unknown[]): void {
// Null if you didn't pass a bot while creating your bus
const bot = meta.getBot();
if (!bot) {
console.log('Hello world');
return;
}
bot.getLogger().log('Hello world');
}
// ...
}
const interfaceSubscriber = new MyInterfaceEventSubscriber();
await myBus.subscribe(interfaceSubscriber);
👂 Subscription
To subscribe to a bus, you'll either need a reference to it or its ID.
// Using the EventManager
await bot.getEventManager().subscribe(subscriber, myBusId); // Can also pass the bus reference
// Using an existing reference to the bus
await myBus.subscribe(subscriber);
The difference with using the EventManager versus using a direct reference is that the EventManager will make sure
that the bus is currently registered on the bot. Otherwise, it will throw an ObjectNotFoundError.
const newBus = BasicEventBus.createSync(bot, Symbol('myBus'));
await bot.getEventManager().subscribe(subscriber, newBus); // Throws ObjectNotFoundError
🧱 Event Dispatch Meta
The first argument of the EventSubscriber#handleEvent() handler is an EventDispatchMeta object, which is a
Collection that stores metadata about the event call.
This metadata can be created by the caller on the EventBus#emit() method, specifying extra arguments to be read by the
subscribers (apart from the event arguments).
It's also passed to the EventMiddleware, and can be used as a way to share data from the middleware to subscribers.
Apart from the keys saved by the caller or the middleware, the dispatch meta contains:
- The bot that called the event, via
#getBot().
Note it can be null if the bus doesn't have a bot. You can pass true to this method to throw an error if it's not present.
In practice, all built-in event buses have a bot, it will only be null for your custom ones where you didn't pass a bot.
- The bus where the event was emitted, via
#getBus(). - Whether the event has been marked as handled by another subscriber, via
#isHandled().
✅ Event handling marking
By default, when a subscriber marks an event as "handled" with the EventDispatchMeta#setHandled() method, it won't be
received by the rest of the subscribers. This is useful for events that only need to be "executed once", for example,
replying to an Interaction.
However, you can make your subscriber to be always called, even when the event is handled, overriding
the ignoreHandled property on AbstractEventSubscriber.
Example of event handling marking functionality
import { AbstractEventSubscriber } from '@nyx-discord/framework';
// A subscriber with priority Highest marks the event as handled
class HighestEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly priority = PriorityEnum.Highest;
public handleEvent(meta: EventDispatchMeta): void {
// some logic
const bot = meta.getBot(true);
bot.getLogger().log('Called HighestEventSubscriber!');
meta.setHandled();
}
}
// A subscriber with priority Lowest doesn't receive the event
class LowestEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly priority = PriorityEnum.Lowest;
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('Called LowestEventSubscriber!');
}
}
// A subscriber with priority Lowest but `ignoreHandled = false` receives the event
class LowestNotIgnoredEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly priority = PriorityEnum.Lowest;
protected readonly ignoreHandled = false;
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('Called LowestNotIgnoredEventSubscriber!');
}
}
await myBus
.subscribe(new LowestEventSubscriber())
.subscribe(new HighestEventSubscriber());
await myBus.emit('someEvent', []);
/**
* Logger output:
* - Called HighestEventSubscriber!
* - Called LowestNotIgnoredEventSubscriber!
* Notice how LowestEventSubscriber wasn't called.
*/
The handling marking logic is done by the HandleCheckEventMiddleware on @framework. If you make your entirely
custom middleware, include it (or an equivalent). Otherwise, this feature won't work at all.
💟 Lifetime
Subscribers can specify its "lifetime", which determines how much it lasts registered on the bus. This is similar to
using Node's EventEmitter#on() vs EventEmitter#once(). This can be done overriding the lifetime property on
AbstractEventSubscriber.
Available lifetimes can be gotten from EventSubscriberLifetimeEnum on @core, which are:
Onto "permanently" subscribe to the bus until manually unsubscribed.Onceto subscribe for a single call (TheEventSubscriberMiddlewaremust allow the execution).
Subscriber lifetime example on AbstractEventSubscriber
import { EventSubscriberLifetimeEnum } from '@nyx-discord/core';
import { AbstractEventSubscriber } from '@nyx-discord/framework';
class OnEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly lifetime = EventSubscriberLifetimeEnum.On;
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('Hello from On subscriber!');
}
}
class OnceEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly lifetime = EventSubscriberLifetimeEnum.Once;
public override onUnsubscribe(bus: EventBus): void {
bus.bot.getLogger().log('Unsubscribed OnceEventSubscriber.');
}
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('Hello from On subscriber!');
}
}
await myBus
.subscribe(new OnEventSubscriber())
.subscribe(new OnceEventSubscriber());
await myBus.emit('someEvent', []);
/**
* Logger output:
* - Hello from On subscriber!
* - Hello from Once subscriber!
* - Unsubscribed OnceEventSubscriber.
*/
await myBus.emit('someEvent', []);
/**
* Logger output:
* - Hello from On subscriber!
*/
The lifetime logic is done by the LifetimeCheckEventMiddleware on @framework. If you make your entirely custom
middleware, include it (or an equivalent). Otherwise, this feature won't work at all.
🔃 Priority
Subscribers with higher Priority will be called early, and get the opportunity to mark the event as handled first.
This can be done overriding the priority property on AbstractEventSubscriber.
Available priorities can be gotten from PriorityEnum on @core, which are, sorted from executed first to last:
LowMonitor- Meant to be used for monitoring (like analytics) and not actual execution logic.LowestLowNormal- Default value.HighHighestHighMonitor- Meant to be used for monitoring (like analytics) and not actual execution logic.
Subscriber priority example on AbstractEventSubscriber
import { PriorityEnum } from '@nyx-discord/core';
import { AbstractEventSubscriber } from '@nyx-discord/framework';
class HighestEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly priority = PriorityEnum.Highest;
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('First!');
}
}
class LowestEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly priority = PriorityEnum.Lowest;
public handleEvent(meta: EventDispatchMeta): void {
const bot = meta.getBot(true);
bot.getLogger().log('Last!');
}
}
await myBus
.subscribe(new LowestEventSubscriber())
.subscribe(new HighestEventSubscriber());
await myBus.emit('someEvent', []);
/**
* Logger output:
* - First!
* - Last!
*/
The priority sorting logic is done by the BasicEventBus#DefaultSorter static property. If you override your sorter,
make sure to keep the priority in mind. Otherwise, this feature won't work at all.
🛑 Filtering
Subscribers can specify an EventSubscriberFilter, which is a custom object that specifies whether the subscriber
should be called or not. It receives the subscriber, the subscriber arguments, and returns a boolean.
Filters are particularly useful when reusing event ignoring logic. They also get access to the EventDispatchMeta,
where they can save objects to be used by the subscriber.
-
Filters are not subscriber aware, meaning that the same instance can be reused on many subscribers.
-
While subscribers cannot specify more than one filter, you can use a
FilterAggregatorfrom@frameworkto "merge" filters. For example, use theAndFilterto make a filter that returnstrueif all filters passed on its constructor returntrue. -
You can check more information about event interception on the 🛡️ Event Interception category guide. More specifically, you can check the 🚧 Filters section.
Filter example using AbstractSubscriberFilter
import type {
ClientEvents,
Snowflake
} from 'discord.js';
import type { EventDispatchArgs } from '@nyx-discord/core';
import {
AbstractEventSubscriber,
AbstractSubscriberFilter
} from '@nyx-discord/framework';
type InteractionCreateArgs = ClientEvents['interactionCreate'];
class UserBlacklistInteractionSubscriberFilter
extends AbstractSubscriberFilter<InteractionCreateArgs> {
protected readonly userIds: Snowflake[];
constructor(userIds: Snowflake[]) {
super();
this.userIds = userIds;
}
public check(
_subscriber: EventSubscriber,
...args: EventDispatchArgs<InteractionCreateArgs>,
) {
const [_meta, interaction] = args;
return this.userIds.includes(interaction.user.id);
}
}
class MyEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly filter = new UserBlacklistInteractionSubscriberFilter(
['235428738748121088'],
);
public handleEvent(): void {
// Only executed if user ID is not '235428738748121088'
}
}
The filter check logic is done by the SubscriberFilterCheckMiddleware on @framework. If you make your entirely
custom middleware, include it (or an equivalent). Otherwise, this feature won't work at all.
📝 Metadata
When extending nyx with plugins, you may want to specify extra data for them. You can do so with the MetaCollection
object, saved on the subscriber.
While subscribers internally save a MetaCollection, the return type of #getMeta() is a ReadonlyMetaCollection,
meaning that external objects can read the collection, but not modify it.
import { AbstractEventSubscriber } from '@nyx-discord/framework';
import { Collection } from '@discordjs/collection';
class MyEventSubscriber extends AbstractEventSubscriber {
protected readonly event = 'someEvent';
protected readonly meta = new Collection<Identifier, unknown>([
['someKey', 'someValue']
]);
}
// a plugin can now read it via:
const meta = myEventSubscriber.getMeta();
const value = meta.get('someKey'); // 'someValue'