Skip to main content
A store is declared with store(), in any file of src/, and exported. It keeps data from one restart to the next, typed by what it holds, and per server unless you say otherwise.
src/commands/warn.ts
That is all. The name of the export is the name of the store (warnings), and its data lives in data/warnings.json, at the root of your project. The command runs in a server, so the store reads and writes the warnings of that server: the key is options.who.id, nothing more.
Everything a store declares and does is in the reference.

Per server, without saying so

A store is per server by default. In a function that runs in a server (a command, a button, a form, an event that has a guild), the framework knows which server, and the store works on its data: warnings.get(member.id) in server A and in server B read two different things, with the same line. Outside a server (a task, which runs for every server; an event of a private message; code that looks at another server), say which one with in():
src/tasks/digest.ts
A store per server used where no server is known stops with a message saying to use in(). For data that is the same everywhere, declare the store with scope: 'global': it then takes no server at all.
src/settings.ts

What a store does

A key is a text, or a list of texts for a key made of several parts: [channel.id, message.id]. entries(channel.id) then lists what that channel has. Two update() at once on the same key never lose one another, whether they run in one process, in several, or on several machines sharing a Redis: change may then run a second time, on the value the other one kept, so keep it a plain computation (no message sent from it). For a counter, increment() says it in one word:
src/events/count-messages.ts
A value is JSON: texts, numbers, booleans, lists and plain objects. A Date, a Map or a class instance would not come back the same, so set() refuses them and says what to keep instead (an id, a text, a number). Every call returns a promise.
To keep a score per member in order (xp, levels, best times) and show who is first, use a ranking.

Expiration

expires says how long a value is kept: on the store, for every value, or on one set() or update(). A duration is a number and a unit, s, m, h or d, like '10m' or '30d'.
src/commands/verify.ts
An expired value is gone: get() answers undefined, entries() leaves it out, and the file forgets it. { expires: null } keeps one value forever in a store that expires; a Date sets the moment itself. update() keeps the expiration the value had, unless you give another.

Where the data is

One file per store, data/<name>.json, in the folder of your project. It is written shortly after each change, in one piece, so a crash never leaves a half-written file; what is pending when the bot stops is written then. The folder is in the .gitignore of a new project: it is your data, not your code. The bot keeps the data in memory and the file is its copy, so a store is for what fits in memory: settings, scores, warnings, who opened what. For millions of rows, use a database. In production on several processes, the data has one home, the process you started: the others ask it, so a command on any shard reads what the others wrote. Nothing to set up.
chapterjs dev and chapterjs start in the same folder share the same data/. With two bots in one folder, what you try in dev is what production reads.

In Redis

A store can keep its data in Redis instead of a file: say storage: 'redis' when you declare it, and give the address of your Redis in .env. Nothing else changes: same calls, same keys, same scope, same expiration.
src/sessions.ts
.env
REDIS_URL is the address, redis://host:port (rediss:// for a TLS connection, /1 at the end for a database other than the first); a user and a password can be in it (redis://user:password@host:6379), or the password in REDIS_PASSWORD, which wins. Only the stores that say storage: 'redis' go there: with REDIS_URL set and no such store, nothing is even connected. The bot connects when it starts and says which stores it keeps there:
Each value is one key, sessions:<server id>:<key> (sessions:<key> for a global store), with the JSON of the value and its expiration, so you can look at it with redis-cli. In chapterjs dev, every key starts with dev:, so a development bot and a production bot on one Redis never read each other’s data. Still, give each its own Redis, as you give each its own token: a shared one is a shared outage, and the same memory. Redis is for what expires, first: sessions, codes, cooldowns, drafts, what several processes or several machines must share, and what must survive a host without a persistent disk. Durable data works there too, with one thing to know: the data does not move with the store. A store that goes from a file to Redis starts from what Redis has, and data/<name>.json stays on disk, unread. A store kept in Redis without REDIS_URL is reported when the files load, and refuses to serve until it is set. A Redis that does not answer stops chapterjs dev and chapterjs start before they reach Discord, with the reason. Once the bot runs, a Redis that goes away is said once with ⚠, the bot keeps running (the calls of the stores kept there fail, the person is told something went wrong), the connection is tried again by itself with a growing wait, and ↻ says when it is back. chapterjs test needs no Redis: a store kept there runs in memory, like the others.

In development

Saving a file keeps the data: a store is tied to its name, not to the file. Two stores with the same name in two files are reported, and the later one is left out: they would share a file. Delete data/<name>.json to start a store empty.

store() reference

Every field of a store, and what each call returns.

Tasks

Something to do with what the bot keeps, every so often.