Skip to content
GitHub

RedisCooldownStore

class in meocord/common Since 4.1.0

class RedisCooldownStore extends CooldownStore

A CooldownStore on Redis, or on any server that speaks its protocol and runs its Lua scripts.

Use it for counts that outlive a restart and are shared by every process and shard on one server, so 'user' and 'global' cooldowns stay exact across them. For process shards on one host, ShardedCooldownStore needs no database.

Examples

TypeScript
import { createClient } from 'redis'

const redis = await createClient({ url: process.env.REDIS_URL }).connect()

@MeoCord({
  controllers: [],
  clientOptions: { intents: [GatewayIntentBits.Guilds] },
  cooldownStore: RedisCooldownStore.using((script, keys, args) => redis.eval(script, { keys, arguments: args })),
})
export default class App {}

Members

constructor

new RedisCooldownStore(evaluate: RedisEval, options?: RedisCooldownStoreOptions)

A store that runs its script through evaluate. To bind one to an app, use RedisCooldownStore.using, which @MeoCord({ cooldownStore }) takes.

Parameters

NameTypeDescription
evaluateRedisEval

Runs a script, as the client's EVAL.

options?RedisCooldownStoreOptions

A key prefix, an EVALSHA runner, and hashTag for Redis Cluster.

options.prefix?string

Put before every key the store writes. Defaults to meocord:cooldown:.

options.evalsha?RedisEvalSha

Runs the script by its SHA1, sending it in full only when the server answers NOSCRIPT: once after each restart or SCRIPT FLUSH. Without it, every call sends the script with EVAL.

options.hashTag?'handler'

'handler' puts each handler's keys in one Redis Cluster slot, as {Controller.method}#…, so its stacked cooldowns stay one step on Cluster as they are on one server. Every call to a handler then lands on that one slot, so a busy handler's slot carries all of its traffic. Without it, on Cluster, a handler's cooldowns are each counted by a script of their own, in order. A single server needs neither.

consume

consume(key: string, limit: CooldownLimit): Promise<CooldownVerdict>

Records a call for key on the server if the limit allows it, as one script.

Parameters

NameTypeDescription
keystring

Identifies the handler, the cooldown and the caller, user or place it counts per.

limitCooldownLimit

The calls allowed, and the window they are counted over.

Returns

Promise<CooldownVerdict>

Whether this call was recorded, and if not, how long until one can be.

consumeMany

consumeMany(entries: readonly CooldownEntry[]): Promise<CooldownBatchVerdict>

Records a call against every entry if all allow it, as one script: one round trip, however many cooldowns a handler stacks. On Redis Cluster, where a handler's keys sit in different slots and one script cannot reach them all, each key is counted by a script of its own, in order, and a refusal gives back the uses counted before it, so a refused call counts against none unless a give-back fails; hashTag: 'handler' keeps the keys in one slot, in one round trip.

Parameters

NameTypeDescription
entriesreadonly CooldownEntry[]

The keys and limits the call counts against.

Returns

Promise<CooldownBatchVerdict>

Whether the call was recorded, and if not, how long until it can be and which entry refused it.

peekMany

peekMany(entries: readonly CooldownEntry[]): Promise<CooldownBatchVerdict>

Checks every entry as consumeMany would, recording nothing, as one read-only script: one round trip. On Redis Cluster, where a handler's keys sit in different slots, each key is checked by a script of its own, together; hashTag: 'handler' keeps them in one slot.

Parameters

NameTypeDescription
entriesreadonly CooldownEntry[]

The keys and limits to check.

Returns

Promise<CooldownBatchVerdict>

Whether every entry allows a call now, and if not, how long until it would and which refused.

using

static using(
  evaluate: RedisEval,
  options?: RedisCooldownStoreOptions,
): new () => RedisCooldownStore

A store class for @MeoCord({ cooldownStore }) that runs its script with your client.

Parameters

NameTypeDescription
evaluateRedisEval

Runs a script, as the client's EVAL.

options?RedisCooldownStoreOptions

A key prefix, an EVALSHA runner to send the script only when the server lacks it, and hashTag to keep a handler's keys in one Redis Cluster slot.

options.prefix?string

Put before every key the store writes. Defaults to meocord:cooldown:.

options.evalsha?RedisEvalSha

Runs the script by its SHA1, sending it in full only when the server answers NOSCRIPT: once after each restart or SCRIPT FLUSH. Without it, every call sends the script with EVAL.

options.hashTag?'handler'

'handler' puts each handler's keys in one Redis Cluster slot, as {Controller.method}#…, so its stacked cooldowns stay one step on Cluster as they are on one server. Every call to a handler then lands on that one slot, so a busy handler's slot carries all of its traffic. Without it, on Cluster, a handler's cooldowns are each counted by a script of their own, in order. A single server needs neither.

Returns

new () => RedisCooldownStore

A class the app resolves like a service, with nothing to inject.

Examples

TypeScript
// node-redis, sending the script by its SHA1 once the server has it
RedisCooldownStore.using((script, keys, args) => redis.eval(script, { keys, arguments: args }), {
  evalsha: (sha, keys, args) => redis.evalSha(sha, { keys, arguments: args }),
})

// ioredis
RedisCooldownStore.using((script, keys, args) => redis.eval(script, keys.length, ...keys, ...args), {
  prefix: 'mybot:cooldown:',
})