Skip to content

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.

ResourceElements
channelsTextChannel, DMChannel, VoiceChannel, ThreadChannel…
bansGuildBan
emojisEmoji
guildsGuild
messagesMessage
overwritesPermissionsOverwrites
presencePresence
membersGuildMember
rolesGuildRole
usersUser
stickersSticker
voiceStatesVoiceStates
stagesInstancesStageChannel
import {
class Client<Ready extends boolean = boolean>
Client
} from 'seyfert';
const
const client: Client<boolean>
client
= new
new Client<boolean>(options?: ClientOptions): Client<boolean>
Client
();
const client: Client<boolean>
client
.
Client<boolean>.setServices({ gateway, ...rest }: ServicesOptions & {
gateway?: ShardManager;
}): void
setServices
({
ServicesOptions.cache?: {
adapter?: Adapter;
disabledCache?: boolean | DisabledCache | ((cacheType: keyof DisabledCache) => boolean);
}
cache
: {
disabledCache?: boolean | DisabledCache | ((cacheType: keyof DisabledCache) => boolean)
disabledCache
: {
bans?: boolean
bans
: true } } })

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

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 {
class Client<Ready extends boolean = boolean>
Client
} from "seyfert";
import { type
type APIChannel = APIDMChannel | APIGroupDMChannel | APIGuildCategoryChannel | APIGuildForumChannel | APIGuildMediaChannel | ... 4 more ... | APIThreadChannel
APIChannel
, ChannelType } from "seyfert/lib/types";
const
const client: Client<boolean>
client
= new
new Client<boolean>(options?: ClientOptions): Client<boolean>
Client
();
const client: Client<boolean>
client
.
BaseClient.cache: Cache
cache
.
Cache.channels?: Channels | undefined
channels
!.
Channels.filter(data: APIChannel, id: string, guild_id: string, from: CacheFrom): boolean
filter
= (
channel: APIChannel
channel
,
id: string
id
,
guildId: string
guildId
,
) => {
return ![
ChannelType.
function (enum member) ChannelType.DM = 1

A direct message between users

DM
,
ChannelType.
function (enum member) ChannelType.GroupDM = 3

A direct message between multiple users

GroupDM
].
Array<ChannelType>.includes(searchElement: ChannelType, fromIndex?: number): boolean

Determines whether an array includes a certain element, returning true or false as appropriate.

@paramsearchElement The element to search for.

@paramfromIndex The position in this array at which to begin searching for searchElement.

includes
(
channel: APIChannel
channel
.
type: ChannelType.GuildText | ChannelType.DM | ChannelType.GuildVoice | ChannelType.GroupDM | ChannelType.GuildCategory | ChannelType.GuildAnnouncement | ChannelType.GuildStageVoice | ChannelType.GuildForum | ChannelType.GuildMedia | ThreadChannelType
type
);
};

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.

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 {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
MyAdapter.isAsync: boolean
isAsync
= true;
async
MyAdapter.start(): Promise<void>
start
() {
// 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 {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.set(key: string, value: any | any[]): Promise<void>
set
(
key: string
key
: string,
value: any
value
: any | any[]) {
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.set(key: string, value: any): Promise<void>
set
(
key: string
key
, {
value: any
value
});
}
async
MyAdapter.bulkSet(keys: [string, any][]): Promise<void>
bulkSet
(
keys: [string, any][]
keys
: [string, any][]) {
for (let [
let key: string
key
,
let value: any
value
] of
keys: [string, any][]
keys
) {
await this.
MyAdapter.set(key: string, value: any | any[]): Promise<void>
set
(
let key: string
key
,
let value: any
value
);
}
}
}

patch and bulkPatch

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

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.patch(key: string, value: any | any[]): Promise<void>
patch
(
key: string
key
: string,
value: any
value
: any | any[]) {
const
const oldData: any
oldData
= await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.get(key: string): Promise<any>
get
(
key: string
key
) ?? {};
const
const newValue: any
newValue
=
var Array: ArrayConstructor
Array
.
ArrayConstructor.isArray(arg: any): arg is any[]
isArray
(
value: any
value
)
?
value: any[]
value
: ({ ...
const oldData: any
oldData
, ...
value: any
value
});
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.set(key: string, value: any): Promise<void>
set
(
key: string
key
, {
value: any
value
:
const newValue: any
newValue
});
}
async
MyAdapter.bulkPatch(keys: [string, any][]): Promise<void>
bulkPatch
(
keys: [string, any][]
keys
: [string, any][]) {
for (let [
let key: string
key
,
let value: any
value
] of
keys: [string, any][]
keys
) {
await this.
MyAdapter.patch(key: string, value: any | any[]): Promise<void>
patch
(
let key: string
key
,
let value: any
value
);
}
}
}

Storing Relationships

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

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.addToRelationship(id: string, keys: string | string[]): Promise<void>
addToRelationship
(
id: string
id
: string,
keys: string | string[]
keys
: string | string[]) {
for (const
const key: string
key
of
var Array: ArrayConstructor
Array
.
ArrayConstructor.isArray(arg: any): arg is any[]
isArray
(
keys: string | string[]
keys
) ?
keys: string[]
keys
: [
keys: string
keys
]) {
// Add to a "Set", IDs must be unique
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setAdd(key: string, key: string): Promise<void>
setAdd
(
id: string
id
,
const key: string
key
);
}
}
async
MyAdapter.bulkAddToRelationShip(data: Record<string, string[]>): Promise<void>
bulkAddToRelationShip
(
data: Record<string, string[]>
data
:
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<string, string[]>) {
for (const
const i: string
i
in
data: Record<string, string[]>
data
) {
await this.
MyAdapter.addToRelationship(id: string, keys: string | string[]): Promise<void>
addToRelationship
(
const i: string
i
,
data: Record<string, string[]>
data
[
const i: string
i
]);
}
}
}

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 {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.get(key: string): Promise<any>
get
(
key: string
key
: string) {
return this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.get(key: string): Promise<any>
get
(
key: string
key
);
}
async
MyAdapter.bulkGet(keys: string[]): Promise<any[]>
bulkGet
(
keys: string[]
keys
: string[]) {
const
const values: Promise<any>[]
values
:
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<any>[] = [];
for (let
let key: string
key
of
keys: string[]
keys
) {
const values: Promise<any>[]
values
.
Array<Promise<any>>.push(...items: Promise<any>[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.

push
(this.
MyAdapter.get(key: string): Promise<any>
get
(
let key: string
key
));
}
return (await
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.all<Promise<any>[]>(values: Promise<any>[]): Promise<any[]> (+1 overload)

Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

@paramvalues An array of Promises.

@returnsA new Promise.

all
(
const values: Promise<any>[]
values
))
// Do not return null values
.
Array<any>.filter(predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any): any[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

@parampredicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.

@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.

filter
(
value: any
value
=>
value: any
value
)
}
}

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 {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.scan(query: string, keys?: false): any[] (+1 overload)
scan
(
query: string
query
: string,
keys: false | undefined
keys
?: false): any[];
async
MyAdapter.scan(query: string, keys: true): string[] (+1 overload)
scan
(
query: string
query
: string,
keys: true
keys
: true): string[];
async
MyAdapter.scan(query: string, keys?: false): any[] (+1 overload)
scan
(
query: string
query
: string,
keys: boolean
keys
= false) {
const
const values: unknown[]
values
: (string | unknown)[] = [];
const
const sq: string[]
sq
=
query: string
query
.
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.

@paramlimit A value used to limit the number of elements returned in the array.

split
('.');
// Your client will likely have a more optimized way to do this.
// Like our Redis adapter.
for (const [
const key: string
key
,
const value: unknown
value
] of await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.entries(): Promise<[string, unknown][]>
entries
()) {
const
const match: boolean
match
=
const key: string
key
.
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.

@paramlimit A value used to limit the number of elements returned in the array.

split
('.')
.
Array<string>.every(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean (+1 overload)

Determines whether all the members of an array satisfy the specified test.

@parampredicate A function that accepts up to three arguments. The every method calls the predicate function for each element in the array until the predicate returns a value which is coercible to the Boolean value false, or until the end of the array.

@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.

every
((
value: string
value
,
i: number
i
) => (
const sq: string[]
sq
[
i: number
i
] === '*' ? !!
value: string
value
:
const sq: string[]
sq
[
i: number
i
] ===
value: string
value
));
if (
const match: boolean
match
) {
const values: unknown[]
values
.
Array<unknown>.push(...items: unknown[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.

push
(
keys: boolean
keys
?
const key: string
key
:
const value: unknown
value
);
}
}
return
const values: unknown[]
values
;
}
}

Example of Redis Adapter

Retrieving Relationships

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

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.getToRelationship(to: string): Promise<string[]>
getToRelationship
(
to: string
to
: string) {
return await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setGet(key: string): Promise<string[] | undefined>
setGet
(
to: string
to
) ?? []
}
}

keys, values, count, and contains

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.getToRelationship(to: string): Promise<string[]>
getToRelationship
(
to: string
to
: string) {
return await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setGet(key: string): Promise<string[] | undefined>
setGet
(
to: string
to
) ?? []
}
async
MyAdapter.keys(to: string): Promise<string[]>
keys
(
to: string
to
: string) {
const
const keys: string[]
keys
= await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setGet(key: string): Promise<string[] | undefined>
setGet
(
to: string
to
) ?? [];
return
const keys: string[]
keys
.
Array<string>.map<string>(callbackfn: (value: string, index: number, array: string[]) => string, thisArg?: any): string[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
(
key: string
key
=> `${
to: string
to
}.${
key: string
key
}`);
}
async
MyAdapter.values(to: string): Promise<any[]>
values
(
to: string
to
: string) {
const
const array: any[]
array
: any[] = [];
const
const keys: string[]
keys
= await this.
MyAdapter.keys(to: string): Promise<string[]>
keys
(
to: string
to
);
for (const
const key: string
key
of
const keys: string[]
keys
) {
const
const content: any
content
= await this.
MyAdapter.get(key: string): Promise<any>
get
(
const key: string
key
);
if (
const content: any
content
) {
const array: any[]
array
.
Array<any>.push(...items: any[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.

push
(
const content: any
content
);
}
}
return
const array: any[]
array
;
}
async
MyAdapter.count(to: string): Promise<number>
count
(
to: string
to
: string) {
return (await this.
MyAdapter.getToRelationship(to: string): Promise<string[]>
getToRelationship
(
to: string
to
)).
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
;
}
async
MyAdapter.contains(to: string, key: string): Promise<boolean>
contains
(
to: string
to
: string,
key: string
key
: string) {
return (await this.
MyAdapter.getToRelationship(to: string): Promise<string[]>
getToRelationship
(
to: string
to
)).
Array<string>.includes(searchElement: string, fromIndex?: number): boolean

Determines whether an array includes a certain element, returning true or false as appropriate.

@paramsearchElement The element to search for.

@paramfromIndex The position in this array at which to begin searching for searchElement.

includes
(
key: string
key
);
}
}

Deleting Data

remove, bulkRemove and flush

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

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.remove(key: string): Promise<void>
remove
(
key: string
key
: string) {
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.remove(key: string): Promise<void>
remove
(
key: string
key
);
}
async
MyAdapter.bulkRemove(keys: string[]): Promise<void>
bulkRemove
(
keys: string[]
keys
: string[]) {
for (const
const key: string
key
of
keys: string[]
keys
) {
await this.
MyAdapter.remove(key: string): Promise<void>
remove
(
const key: string
key
);
}
}
async
MyAdapter.flush(): Promise<void>
flush
() {
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.flush(): Promise<void>
flush
(); // Delete values
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setFlush(): Promise<void>
setFlush
(); // Delete relationships
}
}

Deleting Relationships

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

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MyAdapter
MyAdapter
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MyAdapter.removeToRelationship(to: string): Promise<void>
removeToRelationship
(
to: string
to
: string) {
// Remove the "Set" completely
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setRemove(key: string): Promise<void>
setRemove
(
to: string
to
);
}
async
MyAdapter.removeRelationship(to: string, key: string | string[]): Promise<void>
removeRelationship
(
to: string
to
: string,
key: string | string[]
key
: string | string[]) {
// Remove the ID(s) from the "Set"
const
const keys: string[]
keys
=
var Array: ArrayConstructor
Array
.
ArrayConstructor.isArray(arg: any): arg is any[]
isArray
(
key: string | string[]
key
) ?
key: string[]
key
: [
key: string
key
];
await this.
MyAdapter.storage: SeyfertDotDev
storage
.
SeyfertDotDev.setPull(to: string, key: string[]): Promise<void>
setPull
(
to: string
to
,
const keys: string[]
keys
);
}
}

Testing

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

import {
class Client<Ready extends boolean = boolean>
Client
} from 'seyfert';
const
const client: Client<boolean>
client
= new
new Client<boolean>(options?: ClientOptions): Client<boolean>
Client
();
const client: Client<boolean>
client
.
Client<boolean>.setServices({ gateway, ...rest }: ServicesOptions & {
gateway?: ShardManager;
}): void
setServices
({
ServicesOptions.cache?: {
adapter?: Adapter;
disabledCache?: boolean | DisabledCache | ((cacheType: keyof DisabledCache) => boolean);
}
cache
: {
adapter?: Adapter
adapter
: new
any
MyAdapter
()
}
})
await
const client: Client<boolean>
client
.
BaseClient.cache: Cache
cache
.
Cache.testAdapter(): Promise<void>
testAdapter
();