Cooldowns
Put a time window around a command without burning failed attempts.
Cooldown is a built-in precondition. Add it to the preconditions array with a duration in milliseconds:
import { Cooldown, CooldownScope } from "swagcommands";
preconditions: [
Cooldown({ duration: 5_000 }),
]The default scope is one bucket per command and user. Other scopes are CooldownScope.Channel, CooldownScope.Guild, and CooldownScope.Global. A guild scope cannot be used in a DM because there's no guild ID there.
Cooldown({
duration: 10_000,
scope: CooldownScope.User,
id: "moderation-actions",
})Use id to deliberately share a bucket between commands. Otherwise generated IDs include the full command identity, so two subcommand leaves don't collide.
Why failed checks don't eat the cooldown
Preconditions have a read-only check phase followed by a commit phase. Only after all selected checks pass does Cooldown atomically claim its bucket. A malformed invocation or a failed permission check won't use it up. If two requests race, both may pass the read, but only one can win the claim. The other gets COOLDOWN_ACTIVE before its callback runs.
The failure context includes cooldownId, scope, expiresAt, and remaining milliseconds. Handle it through the failure hook if you want a friendlier message.
Storage
MemoryCooldownStore is the default. It lasts only as long as the process and doesn't coordinate multiple bot instances. For persistence or multiple processes, inject your own CooldownStore in SWAG.create({ cooldownStore }). Its claimCooldown(id, expiresAt, now) must be atomic across your workers. A database transaction, compare-and-set, or Redis script can do that; a separate read followed by a write cannot.
See Stores for the full interface, including getCooldown, setCooldown, and deleteCooldown.