Skip to main content

👤 Sessions

Hey there 👋! This guide is here to give you a fast understanding of how nyx's session system works, so you can use it right away. For in-depth details, see the respective guides of each session related object.

📚 Description

Sessions are objects that represent an open interaction session with a user on Discord. They can be used as an easy way to maintain a "conversation" with a user, and respond to interactions that the user makes to it.

tip

Some examples of sessions are:

  • A paginated embed that shows a user's moderations, allowing advanced options like filtering by type.
  • An interactive embed that guides a user step by step to accomplish something, like creating a ticket category.
  • A detailed settings embed that allows a user to change their server's settings with buttons, modals or select menus.
  • In essence, any kind of situation where continuous interaction with a user is needed.

Sessions can contain a "result", which is an internal object transmitted to objects that are waiting for the session to finish via a Promise. This result can be used for even further interaction with the user once the session stops.

The session system is made up of several objects that work together to track user interactions on sessions, sessions lifetime and serialization, all coordinated by a SessionManager.

Specifically, the session related objects are:

  • SessionManager: The entry point for the session system, holding all the session-related objects and methods that use all of these objects. All objects below are contained here.
  • SessionCustomIdCodec: De/serializes session ids to/from customId strings, to create session components. This component allows you to create and manipulate customIds programatically, with no "magic values".
  • SessionRepository: Temporarily stores sessions until their TTL is over, and notifies the manager when that happens.
  • SessionPromiseRepository: Stores session promises that will be resolved once a given session ends.
  • SessionExecutor: Executes sessions, checking its SessionMiddlewareList and passing any errors to its ErrorHandler.
  • SessionUpdateSubscriber: An 📩 Event Subscriber that listens for interactions that can refer to sessions.
  • EventBus: An 📣 Event Bus that emits session related events.

There are three utility session types that derive from the main Session interface:

  • PaginationSession: A session with "pages", like ListPaginationSession or StagePaginationSession.
  • ListPaginationSession Paginates a list of items, showing a given amount of them per page.
  • StagePaginationSession: Paginates through "stages", which act like nested sessions.

🔢 Session sequences

Session process overview

Session creation:

  1. A user triggers a session creation, typically via a command interaction.
  2. The command creates the session and registers it to the SessionManager. It can then await for Session#getEndPromise() to wait for the session to end.
  3. The SessionManager saves the session on the repository, which will expire it later if needed.
  4. The SessionManager uses the SessionExecutor to start the session.
  5. The SessionExecutor checks its SessionStartMiddlewareList which will allow or deny the start. This includes checking the filter on Session#getStartFilter().
  6. If the execution is allowed, the session is started, passing any errors to its SessionStartErrorHandler.
  7. The SessionManager uses the EventBus to emit a SessionStart event asynchronously, passing the session.

Session update:

  1. When the user uses a session component, the SessionUpdateSubscriber receives and passes it to the SessionManager.
  2. The SessionManager uses the SessionCustomIdCodec to check if the interaction refers to a session. If it does, it continues.
  3. The SessionManager gets the session from the SessionRepository. If it doesn't exist, it calls SessionExecutor#handleMissing(), which will handle the interaction referring to a missing (probably expired) session. If it does exist, it continues.
  4. The SessionManager calls SessionExecutor#update() to handle the update, passing the interaction and session.
  5. The SessionExecutor checks its SessionMiddlewareList which will allow or deny the execution. This includes checking the filter on Session#getUpdateFilter().
  6. If the execution is allowed, the session is updated via Session#update(), passing any errors to its SessionUpdateErrorHandler, and returning what Session#update() returns.
  7. If the executor returns true, the SessionManager calls SessionRepository#setTTL() to refresh the TTL of the session.
  8. The SessionManager uses the EventBus to emit a SessionUpdate event asynchronously, passing the session, interaction and meta.

Session expiration:

  1. Once a given session expires, the SessionRepository removes it from its cache, and notifies the manager about it.
  2. The SessionManager creates a SessionEndData with an Expired origin and no result, which is passed to SessionExecutor#end(), alongside the session.
  3. The SessionManager calls SessionPromiseRepository#resolve(), to resolve promises waiting for the session to end.
  4. The SessionManager uses the EventBus to emit a SessionExpire event asynchronously, passing the session and meta.
tip
  • A dashed step means it's executed asynchronously, so the next one is inmediately executed.
  • You can hover over steps with (?) to see extra details.

Start sequence

Update sequence

Expire sequence

✨ Quick Examples

A simple session just contains components that the user can interact with, for example, a select menu or a button. The session will then reply to these interactions on Session#update().

For utility, the default Session#update() routes to other methods depending on the interaction type (button, select menu, or modal). You can of course override this method to your liking.

// A session with no result (void).
class MySession extends AbstractSession<void> {
public async start() {
const customIdBuilder = this.customId.clone();

/**
* The builder allows you to create customIds that will route to this session.
* The 'select' and 'button' parts are not needed for routing,
* but they're there to avoid duplicated customIds, which aren't allowed by Discord.
*/

// Creating a select menu
const selectMenuId = customIdBuilder.push('select').build();
const mySelectMenu = new StringSelectMenuBuilder()
.setCustomId(selectMenuId)
.setPlaceholder('A select menu!')
.setMaxValues(1)
.addOptions([
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
]);

// Creating a button
const buttonId = customIdBuilder.push('button').build();
const myButton = new ButtonBuilder()
.setCustomId(buttonId)
.setLabel('Click to open a modal!')
.setStyle(ButtonStyle.Primary);

// Wrapping and replying
const row = new ActionRowBuilder().addComponents(myButton, mySelectMenu);

await interaction.reply({
content: 'Select an option!',
components: [row],
});
}

/**
* Called when updating the session, and `Session#update()`
* has determined that it's a button interaction.
*/
protected async handleButton(interaction: ButtonInteraction) {
const modalId = customIdBuilder.push('modal').build();
const myModal = new ModalBuilder()
.setCustomId(modalId)
.setTitle('My modal!')
.addComponents([
new ActionRowBuilder()
.addComponents([
new TextInputBuilder()
.setCustomId('name')
.setLabel('Your Name')
.setRequired(true)
.setStyle(TextInputStyle.Short)
]),
]);

await interaction.showModal(myModal);

// You can also get the extra 'button' token (or any added token) here.
const codec = this.bot.getSessionManager().getCustomIdCodec();
const iterator = codec.createIteratorFromCustomId(interaction.customId);
this.bot.getLogger().log(iterator.getTokens()); // ['button']
}

/**
* Called when updating the session, and `Session#update()`
* has determined that it's a select menu interaction.
*/
protected async handleSelectMenu(interaction: AnySelectMenuInteraction) {
await interaction.reply('You selected: ' + interaction.values[0]);
}

/**
* Called when updating the session, and `Session#update()`
* has determined that it's a modal interaction.
*/
protected async handleModal(interaction: ModalMessageModalSubmitInteraction) {
const name = interaction.fields.getTextInputValue('name');

await interaction.reply(`Your name is ${name}`);
}
}

// Somewhere in your code, like inside a command...

const sessionId = 'mySessionId'; // Ideally randomly generated
const session = new MySession(bot, sessionId, interaction);
await bot.getSessionManager().start(session);

// `await` for the session to end
await session.getEndPromise();

await interaction.editReply('Session ended!');