Statefulness and data sharing for Pieces

Hi, let's imagine the following: you want to keep track of some data about the people who join and leave a certain voice channel. It would be convenient to store this data alongside the listener and maybe provide an interface to it directly from the listener. However, it's not fully clear how and when Pieces are instantiated. There are no examples of holding (let alone sharing) any state. How to do this right? 1. is it okay to hold state in a Listener instance? 2. if so, how to access the very same instance somewhere else? 3. if not, what to do instead? Exposing all that data via the container sounds quite painful because that's very far from encapsulated. What would you do to expose some data managed by a certain event handler?
Solution:
use a database like sqlite, postgres, mongo, etc. Especially if you want to retain it between reboots. Otherwise, remember how DJS and sapphire do caching internally: ```ts // store.ts import { Collection } from 'discord.js'; import { container } from '@sapphire/framework';...
Jump to solution
33 Replies
Solution
Favna
Favna3mo ago
use a database like sqlite, postgres, mongo, etc. Especially if you want to retain it between reboots. Otherwise, remember how DJS and sapphire do caching internally:
// store.ts
import { Collection } from 'discord.js';
import { container } from '@sapphire/framework';

export const myCache = new Collection<KeyType, ValueType>();
container.myCache = myCache;

declare module '@sapphire/framework' {
interface Container {
myCache: typeof myCache;
}
}

// listener
this.container.myCache.get('id');
this.container.myCache.set('id', value);
// etc
// store.ts
import { Collection } from 'discord.js';
import { container } from '@sapphire/framework';

export const myCache = new Collection<KeyType, ValueType>();
container.myCache = myCache;

declare module '@sapphire/framework' {
interface Container {
myCache: typeof myCache;
}
}

// listener
this.container.myCache.get('id');
this.container.myCache.set('id', value);
// etc
Favna
Favna3mo ago
as for when listeners are instantiated, once, before client login.
Nemokosch
Nemokosch3mo ago
Whether to use a database or not should also rather be an implementation detail; allowing anyone to just grab the database would be a lot like shared global variables "Worst case" that should still be a container service but it really would be best to keep this the business of those 2 or 3 classes inside the same "module" (registered path)
Sean
Sean3mo ago
Most people just attach properties to container (accessible in pieces with this.container and importable elsewhere For example, I have this code in my main function
container.redis = redisClient;
container.cardCache = new Map<string, any>();
container.redis = redisClient;
container.cardCache = new Map<string, any>();
And then I can access redis from calling this.container.redis.whatever, same for cardCache
Nemokosch
Nemokosch3mo ago
Yeah well, this is the point: this is better than nothing but in a sufficiently complex bot - which this will become over time - you wouldn't want to globally expose things that are only related to one functionality In discord.py, this is a no-brainer because the whole functionality could be one cog with its shared state I started thinking that listeners of the same name might even override each other, regardless that they have different paths. I wouldn't know the implementation but it's too suspicious
Favna
Favna3mo ago
that is correct because we use a Map to store entries which are keyed properties, ergo keys have to be unique
MDN Web Docs
Map - JavaScript | MDN
The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.
Favna
Favna3mo ago
Sapphire Framework
Implementing a discord.py like Cog system | Sapphire
Developer who come from the Python ecosystem may be familiar with the "Cog ↗️" system that discord.py has
Favna
Favna3mo ago
also you say your bot will be "sufficiently complex" but I actually no joke dare you to hold up a lens against @Skyra which still does exactly as I described https://github.com/skyra-project/skyra
GitHub
GitHub - skyra-project/skyra: A multipurpose Discord Bot designed t...
A multipurpose Discord Bot designed to carry out most of your server's needs with great performance and stability. - skyra-project/skyra
Nemokosch
Nemokosch3mo ago
yes, and it is really close to a trap. You wouldn't want name collisions for things you intend to be completely unrelated components
Favna
Favna3mo ago
just name the files better :meguFace: ? sounds like a skill issue tbh lol
Nemokosch
Nemokosch3mo ago
Okay, this is cheap now. If anything, it is a skill issue to not distinguish same file names coming from unrelated components
Favna
Favna3mo ago
it aint hard to name a file playMessageCreate for audio and pointMessageCreate for level up system
Nemokosch
Nemokosch3mo ago
why even have directories if you still gonna hardcode the feature into the file name
Favna
Favna3mo ago
aside from the fact that those are horrible examples because audiobots are DOA and level up systems are a dime a dozen categorization
Nemokosch
Nemokosch3mo ago
if you hardcode the category into all names, there isn't much to categorize and this is not even hard to implement better, I actually created a custom class builder that encodes the folder structure into the name... talking about skill issue
Favna
Favna3mo ago
categories arent hardcoded also it should be noted that Sapphire supports fs-less pieces where piece names are defined through config only
Nemokosch
Nemokosch3mo ago
but to silently do the thing that you would basically never want...
Favna
Favna3mo ago
You're the only person among many to complain about this tiny detail so I wouldn't say never
Nemokosch
Nemokosch3mo ago
then perhaps it was about time
Favna
Favna3mo ago
🤷‍♂️ very unlikely to change plus if we would change it, it would be a breaking change (semver major)
Nemokosch
Nemokosch3mo ago
it's the first step to improvements
Favna
Favna3mo ago
but you're welcome to make an RFC issue 🤷‍♂️
Nemokosch
Nemokosch3mo ago
anyway, now that I caught this gotcha and have a fix is there a way to keep the data of a certain feature more local than the container but less local than the same class?
Favna
Favna3mo ago
Basically the same as before but export the constant and import it elsewhere? I have one other idea but I doubt you'll like it. Extend sapphire's Piece class and add a property to it. Call it SpecificPiece for example. Then create SpecificAliasPiece, SpecificCommand and SpecificListener so you can access the custom property down the line. But that's a LOT of structural overhead
Nemokosch
Nemokosch3mo ago
(by the way, this codebase still doesn't seem to organize separate features into any sort of unit, it is a big chunk of DJS handlers dressed up into OO syntax in this regard)
Favna
Favna3mo ago
Correct on the first part. We don't use cogging for skyra. We never had a use case for it. Long ago only audio was split off before we removed it entirely but that was moreso to localise the code and not to localise something like you want As for the second part, that's what any bot is really Well any OO bot
Nemokosch
Nemokosch3mo ago
to be frank, I don't like this confusion between hard singletons (like a cached package) and object instances that happen to be the only one of their kind
Favna
Favna3mo ago
That's basic OO with dependency injection ¯\_(ツ)_/¯. You'll also see it in other big frameworks like Spring Framework for Java and I think .net core does something like it too Fwiw spring and .net are like the poster children of OO
Nemokosch
Nemokosch3mo ago
In Java, this dichotomy doesn't really exist. One file is one class. The static parts are plain singletons
Favna
Favna3mo ago
Have you used spring though. Raw java is not comparable at all.
Nemokosch
Nemokosch3mo ago
you would never import or export a variable
Favna
Favna3mo ago
Anyway Im driving now
Nemokosch
Nemokosch3mo ago
I haven't used Spring but I have used some ASP.NET Core and I'm pretty sure it's not like this. Everything is instantiated and passed around.
Want results from more Discord servers?
Add your server