Caching in CommandKit
Caching is a technique that stores frequently accessed data in a temporary storage to improve performance and reduce load on your primary data sources. In Discord bots, this can significantly reduce database queries and external API calls.
CommandKit provides a powerful yet simple caching system through @commandkit/cache
plugin. This guide will show you how to effectively use caching in your bot.
CommandKit makes the use of a custom "use cache"
directive to simplify the caching boilerplate. This enables you to focus on your problem domain rather than unnecessary boilerplate logic.
Initializing the Cache Plugin
To get started with caching in CommandKit, you need to install the @commandkit/cache
package:
npm install @commandkit/cache
Then, import the cache plugin and add it to your CommandKit configuration:
import { defineConfig } from 'commandkit';
import { cache } from '@commandkit/cache';
export default defineConfig({
plugins: [cache()],
});
This will set up the default in-memory cache provider. You can also configure a custom cache provider if needed, which we will discuss later in this guide.
Now any function that uses the "use cache"
directive will automatically be cached.
async function fetchData() {
'use cache';
// This result will be cached
return database.getData();
}
Understanding CommandKit's Cache System
CommandKit's caching system is built on a provider-based architecture, allowing you to choose where your cache data is stored. The system includes:
- In-memory caching (default)
- Redis caching (via
@commandkit/redis
) - Custom cache providers (extendable)
Default In-Memory Cache
By default, CommandKit uses an in-memory cache provider. This means:
- Cache data is stored in your bot's memory
- Data is lost when the bot restarts
- Perfect for development and small bots
- No additional setup required
Setting Up Redis Cache
For production bots, Redis caching is recommended as it:
- Persists across bot restarts
- Can be shared across multiple bot instances
- Provides better performance for large datasets
To use Redis caching:
import { defineConfig } from 'commandkit';
import { cache } from '@commandkit/cache';
import { redis } from '@commandkit/redis';
export default defineConfig({
plugins: [cache(), redis()],
});
For custom Redis configuration:
import { setCacheProvider } from '@commandkit/cache';
import { RedisCache } from '@commandkit/redis';
import { Redis } from 'ioredis';
const redis = new Redis({
// Your Redis configuration
host: 'localhost',
port: 6379,
});
setCacheProvider(new RedisCache(redis));
Using the Cache
CommandKit provides a simple and powerful way to implement caching using the "use cache"
directive:
async function getUserProfile(userId: string) {
'use cache';
// This result will be cached
return await database.getUser(userId);
}
You can customize the cache behavior using helper functions:
import { cacheTag, cacheLife } from '@commandkit/cache';
async function getUserProfile(userId: string) {
'use cache';
// Set a custom cache key
cacheTag(`user:${userId}`);
// Set cache duration to 1 hour
cacheLife('1h');
return await database.getUser(userId);
}
Cache Duration Format
You can specify cache duration in multiple formats:
- Milliseconds:
ttl: 60000
(1 minute) - String shortcuts:
'5s'
- 5 seconds'1m'
- 1 minute'2h'
- 2 hours'1d'
- 1 day
Managing Cache Data
Invalidating Cache
To remove cached data:
import { invalidate } from '@commandkit/cache';
// Remove specific cache entry
await invalidate('user:123');
Revalidating Cache
To force refresh cached data:
import { revalidate } from '@commandkit/cache';
// Revalidate and get fresh data
const freshData = await revalidate('user:123');
Real World Example: XP System
Let's build a simple XP system using CommandKit's caching:
import { ChatInputCommand, CommandData } from 'commandkit';
import { cacheTag } from '@commandkit/cache';
import { database } from '../database';
export const command: CommandData = {
name: 'xp',
description: 'Check your XP',
};
// Cached function to get user XP
async function getUserXP(guildId: string, userId: string) {
'use cache';
const key = `xp:${guildId}:${userId}`;
cacheTag(key);
const xp = (await database.get(key)) ?? 0;
return xp;
}
export const chatInput: ChatInputCommand = async ({ interaction }) => {
await interaction.deferReply();
const xp = await getUserXP(interaction.guildId!, interaction.user.id);
return interaction.editReply(`You have ${xp} XP!`);
};
XP Event Handler
import { invalidate } from '@commandkit/cache';
import { database } from '../database';
export default async function (message) {
if (message.author.bot || !message.inGuild()) return;
const key = `xp:${message.guildId}:${message.author.id}`;
const oldXp = (await database.get(key)) ?? 0;
const xp = Math.floor(Math.random() * 10) + 1;
await database.set(key, oldXp + xp);
await invalidate(key); // Invalidate cache after update
}
Best Practices
-
Choose Appropriate Cache Duration
- Short TTL for frequently changing data
- Longer TTL for static content
- Consider your data update patterns
-
Use Meaningful Cache Keys
- Include relevant IDs (e.g.,
user:123
) - Use consistent naming patterns
- Consider data relationships
- Include relevant IDs (e.g.,
-
Handle Cache Misses
- Always provide fallback values
- Consider error cases
- Implement proper error handling
-
Invalidate Strategically
- Invalidate when data changes
- Use
revalidate()
for controlled updates - Consider cache dependencies
-
Monitor Cache Performance
- Watch memory usage with in-memory cache
- Monitor Redis memory if using Redis
- Adjust TTL based on usage patterns
Advanced Usage
Custom Cache Provider
You can create a custom cache provider by extending the CacheProvider
class:
import { CacheProvider } from '@commandkit/cache';
class MyCustomCache extends CacheProvider {
async get<T>(key: string) {
// Implement get logic
}
async set<T>(key: string, value: T, ttl?: number) {
// Implement set logic
}
async exists(key: string) {
// Implement exists logic
}
async delete(key: string) {
// Implement delete logic
}
async clear() {
// Implement clear logic
}
async expire(key: string, ttl: number) {
// Implement expire logic
}
}
Then use it in your bot:
import { setCacheProvider } from '@commandkit/cache';
setCacheProvider(new MyCustomCache());
How It Works Under the Hood
The "use cache"
directive is a powerful feature that transforms your functions into cached versions. Here's how it works:
-
Compilation Process
- The
"use cache"
directive is transformed into a higher-order function during compilation - This transformation wraps your original function with caching logic
- The compiler handles all the boilerplate code for you
- The
-
Runtime Behavior
- When a cached function is called, it first checks the cache provider for existing data
- If cached data exists and is valid, it's returned immediately
- If no cache exists or it's expired:
- The original function is executed
- The result is stored in the cache
- The result is returned to the caller
-
Cache Context
- Each cached function call runs in a cache context
- This context manages:
- Cache keys and tags
- TTL (Time To Live) settings
- Cache invalidation rules
-
Cache Provider Integration
- The cache provider (in-memory or Redis) handles the actual storage
- It manages:
- Data serialization/deserialization
- TTL enforcement
- Memory management
- Cache eviction policies
Here's a simplified view of what happens when you use the directive:
// Your code
async function getUserProfile(userId: string) {
'use cache';
cacheTag(`user:${userId}`);
return database.getUser(userId);
}
// What happens under the hood (simplified)
const cachedGetUserProfile = async (userId: string) => {
const cacheKey = `user:${userId}`;
const cached = await cacheProvider.get(cacheKey);
if (cached) return cached.value;
const result = await database.getUser(userId);
await cacheProvider.set(cacheKey, result, DEFAULT_TTL);
return result;
};
This architecture ensures:
- Efficient caching with minimal boilerplate
- Consistent cache behavior across your application
- Easy integration with different cache providers
- Type safety and proper error handling