Saltearse al contenido

Caché

¿Qué es el caché?

El caché es una capa de almacenamiento temporal que mantiene los datos frecuentemente accedidos disponibles para un acceso rápido. En Seyfert, el sistema de caché almacena los datos de Discord en memoria por defecto, aunque puede configurarse para usar otras soluciones de almacenamiento como Redis.

Resources (Recursos)

Todas las entidades soportadas por el caché de Seyfert son recursos, como canales, usuarios, miembros, etc. Cada uno de estos recursos se gestiona de la misma manera, pero pueden ser modificados y manejados de forma diferente según el Adaptador.

Deshabilitando

Seyfert permite deshabilitar estos recursos por separado.

RecursoElementos
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 } } })

El ejemplo anterior deshabilita el caché de baneos, y ese recurso no existiría en tiempo de ejecución.

Filtrando

Puedes filtrar qué datos se almacenan en un recurso. Por ejemplo, si tu aplicación no necesita almacenar en caché los canales de MD, puedes filtrarlos:

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
);
};

Adaptadores

Seyfert te permite proporcionar tu propio adaptador para el caché, que puedes pensar como un controlador para permitir que Seyfert use una herramienta no soportada. Por defecto, Seyfert incluye MemoryAdapter y LimitedMemoryAdapter, ambos operan en RAM. Además, Seyfert tiene soporte oficial para Redis a través del Adaptador Redis.

Construyendo Tu Propio Caché

Recurso Personalizado

Un recurso personalizado es simplemente una nueva entidad de caché, por lo que integrarlo es relativamente simple. Tomemos como ejemplo el recurso Cooldown del paquete cooldown.

Es importante notar que Seyfert proporciona una base para tres tipos de recursos:

  • BaseResource: una entidad básica, que debe ser completamente independiente
  • GuildBaseResource: una entidad vinculada a un servidor (como los baneos)
  • GuildRelatedResource: una entidad que puede estar o no vinculada a un servidor (como los mensajes)
resource.ts
import { BaseResource } from 'seyfert/lib/cache';
export class CooldownResource extends BaseResource<CooldownData> {
// El namespace es la base que separa cada recurso
namespace = 'cooldowns';
// Sobrescribimos set para aplicar el tipado y formato que queremos
override set(id: string, data: MakePartial<CooldownData, 'lastDrip'>) {
return super.set(id, { ...data, lastDrip: data.lastDrip ?? Date.now() });
}
}

Ten en cuenta que un recurso personalizado es para uso del desarrollador; Seyfert no interactuará con él a menos que se especifique en el código de la aplicación.

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> {}
}

Adaptador Personalizado

¿No te gusta almacenar el caché en memoria o Redis? ¿O tal vez simplemente quieres hacerlo a tu manera?

Aquí aprenderás cómo crear tu propio adaptador de caché.

Antes de Empezar

Considera si tu adaptador podría ser asíncrono; si lo es, necesitarás especificarlo:

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
MiAdaptador.isAsync: boolean
isAsync
= true;
async
MiAdaptador.start(): Promise<void>
start
() {
// Esta función se ejecutará antes de iniciar el bot
}
}

Esta guía es para crear un adaptador asíncrono. Si quieres uno síncrono, simplemente no devuelvas una promesa en ninguno de los métodos (el método start puede ser asíncrono).

Almacenando Datos

En el caché de Seyfert, hay relaciones, para que puedas saber a quién pertenece un recurso.

Hay cuatro métodos que debes implementar en tu adaptador para almacenar valores: set, patch, bulkPatch, y bulkSet.

set y bulkSet

Empezando por lo más simple:

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.set(key: string, value: any | any[]): Promise<void>
set
(
key: string
key
: string,
value: any
value
: any | any[]) {
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.set(key: string, value: any): Promise<void>
set
(
key: string
key
, {
value: any
value
});
}
async
MiAdaptador.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.
MiAdaptador.set(key: string, value: any | any[]): Promise<void>
set
(
let key: string
key
,
let value: any
value
);
}
}
}

patch y bulkPatch

El método patch no debe sobrescribir todas las propiedades del valor anterior, solo las que se pasan.

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.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.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
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.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.set(key: string, value: any): Promise<void>
set
(
key: string
key
, {
value: any
value
:
const newValue: any
newValue
});
}
async
MiAdaptador.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.
MiAdaptador.patch(key: string, value: any | any[]): Promise<void>
patch
(
let key: string
key
,
let value: any
value
);
}
}
}

Almacenando Relaciones

Para almacenar relaciones, usas los métodos bulkAddToRelationShip y addToRelationship.

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.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
]) {
// Agregar a un "Set", los IDs deben ser únicos
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.setAdd(key: string, key: string): Promise<void>
setAdd
(
id: string
id
,
const key: string
key
);
}
}
async
MiAdaptador.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.
MiAdaptador.addToRelationship(id: string, keys: string | string[]): Promise<void>
addToRelationship
(
const i: string
i
,
data: Record<string, string[]>
data
[
const i: string
i
]);
}
}
}

Recuperando Datos

Debes implementar tres métodos en tu adaptador para recuperar valores: get, bulkGet, y scan.

get y bulkGet

Empezando por lo más simple:

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.get(key: string): Promise<any>
get
(
key: string
key
: string) {
return this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.get(key: string): Promise<any>
get
(
key: string
key
);
}
async
MiAdaptador.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.
MiAdaptador.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
))
// No devolver valores nulos
.
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
)
}
}

El método scan

Actualmente, estamos almacenando datos en este formato:

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

El método scan toma una cadena con este formato:

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

El * indica que puede haber cualquier ID.

Debes devolver todas las coincidencias.

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.scan(query: string, keys?: false): any[] (+1 overload)
scan
(
query: string
query
: string,
keys: false | undefined
keys
?: false): any[];
async
MiAdaptador.scan(query: string, keys: true): string[] (+1 overload)
scan
(
query: string
query
: string,
keys: true
keys
: true): string[];
async
MiAdaptador.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
('.');
// Tu cliente probablemente tendrá una forma más optimizada de hacer esto.
// Como nuestro adaptador de Redis.
for (const [
const key: string
key
,
const value: unknown
value
] of await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
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
;
}
}

Ejemplo del adaptador de Redis

Recuperando Relaciones

Para obtener los IDs de una relación, tenemos el método getToRelationship.

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

keys, values, count y contains

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.getToRelationship(to: string): Promise<string[]>
getToRelationship
(
to: string
to
: string) {
return await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.setGet(key: string): Promise<string[] | undefined>
setGet
(
to: string
to
) ?? []
}
async
MiAdaptador.keys(to: string): Promise<string[]>
keys
(
to: string
to
: string) {
const
const keys: string[]
keys
= await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
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
MiAdaptador.values(to: string): Promise<any[]>
values
(
to: string
to
: string) {
const
const array: any[]
array
: any[] = [];
const
const keys: string[]
keys
= await this.
MiAdaptador.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.
MiAdaptador.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
MiAdaptador.count(to: string): Promise<number>
count
(
to: string
to
: string) {
return (await this.
MiAdaptador.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
MiAdaptador.contains(to: string, key: string): Promise<boolean>
contains
(
to: string
to
: string,
key: string
key
: string) {
return (await this.
MiAdaptador.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
);
}
}

Eliminando Datos

remove, bulkRemove y flush

Hay tres métodos que debes implementar en tu adaptador para eliminar valores: remove, bulkRemove, y flush.

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.remove(key: string): Promise<void>
remove
(
key: string
key
: string) {
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.remove(key: string): Promise<void>
remove
(
key: string
key
);
}
async
MiAdaptador.bulkRemove(keys: string[]): Promise<void>
bulkRemove
(
keys: string[]
keys
: string[]) {
for (const
const key: string
key
of
keys: string[]
keys
) {
await this.
MiAdaptador.remove(key: string): Promise<void>
remove
(
const key: string
key
);
}
}
async
MiAdaptador.flush(): Promise<void>
flush
() {
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.flush(): Promise<void>
flush
(); // Eliminar valores
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.setFlush(): Promise<void>
setFlush
(); // Eliminar relaciones
}
}

Eliminando Relaciones

Para eliminar IDs de una relación, tenemos los métodos removeToRelationship y removeRelationship.

import {
(alias) interface Adapter
import Adapter
Adapter
} from 'seyfert';
class
class MiAdaptador
MiAdaptador
implements
(alias) interface Adapter
import Adapter
Adapter
{
async
MiAdaptador.removeToRelationship(to: string): Promise<void>
removeToRelationship
(
to: string
to
: string) {
// Eliminar el "Set" completamente
await this.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.setRemove(key: string): Promise<void>
setRemove
(
to: string
to
);
}
async
MiAdaptador.removeRelationship(to: string, key: string | string[]): Promise<void>
removeRelationship
(
to: string
to
: string,
key: string | string[]
key
: string | string[]) {
// Eliminar el/los ID(s) del "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.
MiAdaptador.almacenamiento: SeyfertDotDev
almacenamiento
.
SeyfertDotDev.setPull(to: string, key: string[]): Promise<void>
setPull
(
to: string
to
,
const keys: string[]
keys
);
}
}

Probando

Para asegurarte de que tu adaptador funciona, ejecuta el método testAdapter de 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
MiAdaptador
()
}
})
await
const client: Client<boolean>
client
.
BaseClient.cache: Cache
cache
.
Cache.testAdapter(): Promise<void>
testAdapter
();