Recipes

Cache

What is cache?

Cache is a temporary storage layer that keeps frequently accessed data readily available for quick access. In Seyfert, the cache system stores Discord data in memory by default, though it can be configured to use other storage solutions like Redis.

Resources

All entities supported by Seyfert's cache are resources, such as channels, users, members, etc. Each of these resources is managed in the same way, but they can be modified and handled differently depending on the Adapter.

Disabling

Seyfert allows you to disable these resources separately.

Global Data

In Seyfert, the cache is global, meaning everything is stored in the same resource, with no distinction between guilds, until they are retrieved, at which point you would need to specify the source.

ResourceElements
channelsTextChannel, DMChannel, VoiceChannel, ThreadChannel...
bansGuildBan
emojisEmoji
guildsGuild
messagesMessage
overwritesPermissionsOverwrites
presencePresence
membersGuildMember
rolesGuildRole
usersUser
stickersSticker
voiceStatesVoiceStates
stagesInstancesStageChannel
import {  } from 'seyfert';
 
const  = new ();
 
.({ : { : { : true } } })

The example above disables the bans cache, and that resource would not exist at runtime.

Disabling the Cache

You can completely remove the cache functionality:

.({ : { : true } })

Filtering

You can filter which data gets stored in a resource. For example, if your application doesn't need to cache DM channels, you can filter them out:

index.ts
import {  } from "seyfert";
import { type ,  } from "seyfert/lib/types";
const  = new ();
 
..!. = (
    ,
    ,
    ,
) => {
    return ![
        .,
        .
    ].(.);
};

Adapters

Seyfert allows you to provide your own adapter for the cache, which you can think of as a driver to let Seyfert use an unsupported tool. By default, Seyfert includes MemoryAdapter and LimitedMemoryAdapter, both of which operate in RAM. Additionally, Seyfert has official Redis support through the Redis Adapter.

Difference between MemoryAdapter and LimitedMemoryAdapter

The MemoryAdapter stores all data in memory, while the LimitedMemoryAdapter limits the amount of data stored in memory by providing expiration times and limits of entries.

Building Your Own Cache

Custom Resource

A custom resource is just a new cache entity, so integrating it is relatively simple. Let's take the example of the Cooldown resource from the cooldown package.

It's important to note that Seyfert provides a base for three types of resources:

  • BaseResource: a basic entity, which should be completely independent
  • GuildBaseResource: an entity linked to a guild (like bans)
  • GuildRelatedResource: an entity that may or may not be linked to a guild (like messages)
resource.ts
import { BaseResource } from 'seyfert/lib/cache';
 
export class CooldownResource extends BaseResource<CooldownData> {
    // The namespace is the base that separates each resource
    namespace = 'cooldowns';
 
    // We override set to apply the typing and format we want
    override set(id: string, data: MakePartial<CooldownData, 'lastDrip'>) {
        return super.set(id, { ...data, lastDrip: data.lastDrip ?? Date.now() });
    }
}

Note that a custom resource is for developer use; Seyfert will not interact with it unless specified in the application's code.

import { Client } from 'seyfert';
import { CooldownResource } from './resource'
 
const client = new Client();
 
client.cache.cooldown = new CooldownResource(client.cache);
 
declare module "seyfert" {
    interface Cache {
        cooldown: CooldownResource;
    }
    interface UsingClient extends ParseClient<Client> {}
}

Custom Adapter

Don't like storing the cache in memory or Redis? Or maybe you just want to do it your own way?

Here, you'll learn how to create your own cache adapter.

Before You Start

Consider whether your adapter might be asynchronous; if it is, you'll need to specify it:

import { Adapter } from 'seyfert';
 
class  implements Adapter {
     = true;
 
    async () {
        // This function will run before starting the bot
    }
}

This guide is for creating an asynchronous adapter. If you want a synchronous one, simply do not return a promise in any of the methods (the start method can be asynchronous).

Storing Data

In Seyfert's cache, there are relationships, so you can know who a resource belongs to.

There are four methods you must implement in your adapter to store values: set, patch, bulkPatch, and bulkSet.

set and bulkSet

Starting with the simplest:

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string, : any | any[]) {
        await this..(, {  });
    }
 
    async (: [string, any][]) {
        for (let [, ] of ) {
            await this.(, );
        }
    }
}

patch and bulkPatch

The patch method should not overwrite the entire properties of the old value, just the ones you pass.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string, : any | any[]) {
        const  = await this..() ?? {};
        const  = .()
            ? 
            : ({ ..., ... });
 
        await this..(, { :  });
    }
 
    async (: [string, any][]) {
        for (let [, ] of ) {
            await this.(, );
        }
    }
}

Storing Relationships

To store relationships, you use the bulkAddToRelationShip and addToRelationship methods.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string, : string | string[]) {
        for (const  of .() ?  : []) {
            // Add to a "Set", IDs must be unique
            await this..(, );
        }
    }
 
    async (: <string, string[]>) {
        for (const  in ) {
            await this.(, []);
        }
    }
}

Retrieving Data

You must implement three methods in your adapter to retrieve values: get, bulkGet, and scan.

get and bulkGet

Starting with the simplest:

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string) {
        return this..();
    }
 
    async (: string[]) {
        const : <any>[] = [];
        for (let  of ) {
            .(this.());
        }
 
        return (await .())
            // Do not return null values
            .( => )
    }
}

The scan method

Currently, we are storing data in this format:

<resource>.<id2>.<id1> // member.1003825077969764412.1095572785482444860
<resource>.<id1> // user.863313703072170014

The scan method takes a string with this format:

<resource>.<*>.<*> // member.*.*
<resource>.<*>.<id> // member.*.1095572785482444860
<resource>.<id>.<*> // member.1003825077969764412.*
<resource>.<*> // user.*

The * indicates that any ID may be present.

You should return all matches.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string, ?: false): any[]; 
    async (: string, : true): string[]; 
    async (: string,  = false) {
        const : (string | unknown)[] = [];
        const  = .('.');
        // Your client will likely have a more optimized way to do this.
        // Like our Redis adapter.
        for (const [, ] of await this..()) {
            const  = .('.')
                .((, ) => ([] === '*' ? !! : [] === ));
            if () {
                .( ?  : );
            }
        }
 
        return ;
    }
}

Example of Redis Adapter

Retrieving Relationships

To get the IDs of a relationship, we have the getToRelationship method.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string) {
        return await this..() ?? []
    }
}

keys, values, count, and contains

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string) {
        return await this..() ?? []
    }
 
    async (: string) {
        const  = await this..() ?? [];
        return .( => `${}.${}`);
    }
 
    async (: string) {
        const : any[] = [];
        const  = await this.();
 
        for (const  of ) {
            const  = await this.();
 
            if () {
                .();
            }
        }
 
        return ;
    }
 
    async (: string) {
        return (await this.()).;
    }
 
    async (: string, : string) {
        return (await this.()).();
    }
}

Deleting Data

remove, bulkRemove and flush

There are three methods you must implement in your adapter to delete values: remove, bulkRemove, and flush.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string) {
        await this..();
    }
 
    async (: string[]) {
        for (const  of ) {
            await this.();
        }
    }
 
    async () {
        await this..(); // Delete values
        await this..(); // Delete relationships
    }
}

Deleting Relationships

To remove IDs from a relationship, we have the removeToRelationship and removeRelationship methods.

import { Adapter } from 'seyfert';
 
class  implements Adapter {
    async (: string) {
        // Remove the "Set" completely
        await this..();
    }
 
    async (: string, : string | string[]) {
        // Remove the ID(s) from the "Set"
        const  = .() ?  : [];
        await this..(, );
    }
}

Testing

To ensure your adapter works, run the testAdapter method from Cache.

import {  } from 'seyfert';
 
const  = new ();
 
.({
    : {
        : new MyAdapter()
    }
})
 
await ..();