Skip to content

Commit e16347d

Browse files
committed
feat: add redis cache provider and more
1 parent ee50393 commit e16347d

File tree

9 files changed

+750
-321
lines changed

9 files changed

+750
-321
lines changed

apps/test-bot/src/events/messageCreate/give-xp.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import { database } from '../../database/store';
55
export default async function (message: Message) {
66
if (message.author.bot || !message.inGuild()) return;
77

8-
const oldXp =
9-
(await database.get(`${message.guildId}:${message.author.id}`)) ?? 0;
8+
const key = `xp:${message.guildId}:${message.author.id}`;
9+
const oldXp = (await database.get(key)) ?? 0;
1010
const xp = Math.floor(Math.random() * 10) + 1;
1111

12-
const key = `xp:${message.guildId}:${message.author.id}`;
1312
await database.set(key, oldXp + xp);
1413

1514
// invalidate the cache

apps/website/docs/guide/11-caching.mdx

Lines changed: 210 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,76 +32,220 @@ new CommandKit({
3232

3333
The `MyCustomCacheProvider` class should extend `CacheProvider` from CommandKit and implement the required methods. You may use this to store the cache in redis, a database or a file system.
3434

35-
## Using the cache
35+
CommandKit officially provides a `RedisCache` provider as well with `@commandkit/redis` package. You can use it as follows:
3636

37-
### Using commandkit CLI
37+
```js
38+
import { CommandKit } from 'commandkit';
39+
import { RedisCache } from '@commandkit/redis';
40+
41+
new CommandKit({
42+
client,
43+
commandsPath,
44+
eventsPath,
45+
// uses default redis connection options (ioredis package)
46+
cacheProvider: new RedisCache(),
47+
});
48+
```
49+
50+
## Real World Example: XP System
51+
52+
Let's build a simple XP system using CommandKit's caching feature. We'll create:
3853

39-
If you are using the commandkit cli to run your bot, you can simply add `"use cache"` directive on a function that you want to cache the result of.
54+
- A message event to give XP
55+
- A command to check XP
56+
- Commands to manage the XP cache
57+
58+
### XP Command
4059

4160
```js
42-
async function fetchData() {
61+
import {
62+
SlashCommandProps,
63+
CommandData,
64+
unstable_cacheTag as cacheTag,
65+
} from 'commandkit';
66+
import { database } from '../database';
67+
68+
export const data = {
69+
name: 'xp',
70+
description: 'Check your XP',
71+
};
72+
73+
// This function will be cached
74+
async function getUserXP(guildId, userId) {
4375
'use cache';
4476

45-
// Fetch data from an external source
46-
const data = await fetch('https://my-example-api.com/data');
77+
const key = `xp:${guildId}:${userId}`;
78+
cacheTag(key); // Use the database key as cache tag
4779

48-
return data.json();
80+
const xp = (await database.get(key)) ?? 0;
81+
return xp;
4982
}
5083

5184
export async function run({ interaction }) {
5285
await interaction.deferReply();
5386

54-
// Fetch data
55-
const data = await fetchData();
87+
const xp = await getUserXP(interaction.guildId, interaction.user.id);
5688

57-
// Send the data to the user
58-
await interaction.editReply(data);
89+
return interaction.editReply(`You have ${xp} XP!`);
5990
}
6091
```
6192
62-
### Using the cache manually
93+
### XP Event Handler
94+
95+
```js
96+
import { unstable_invalidate as invalidate } from 'commandkit';
97+
import { database } from '../database';
98+
99+
// Give XP when user sends a message
100+
export default async function (message) {
101+
if (message.author.bot || !message.inGuild()) return;
102+
103+
const key = `xp:${message.guildId}:${message.author.id}`;
104+
const oldXp = (await database.get(key)) ?? 0;
105+
const xp = Math.floor(Math.random() * 10) + 1;
106+
107+
await database.set(key, oldXp + xp);
108+
await invalidate(key); // Invalidate cache so next xp check is fresh
109+
}
110+
```
63111
64-
To use the cache manually, you can import the `cache()` function from CommandKit and use it to cache the result of a function.
112+
In this example:
113+
114+
1. The XP command caches user XP data to avoid database queries
115+
2. When a user sends a message, they get random XP
116+
3. The cache is invalidated after XP update so next check shows new value
117+
118+
## Common Caching Patterns
119+
120+
### Pattern 1: Cache with Auto-Expiry
65121
66122
```js
67123
import { unstable_cache as cache } from 'commandkit';
68124

69-
const fetchData = cache(async () => {
70-
// Fetch data from an external source
71-
const data = await fetch('https://my-example-api.com/data');
125+
// Cache API results for 5 minutes
126+
const fetchPokemon = cache(
127+
async (name) => {
128+
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
129+
return response.json();
130+
},
131+
{
132+
tag: 'pokemon-data',
133+
ttl: '5m', // Expires after 5 minutes
134+
},
135+
);
136+
```
72137
73-
return data.json();
74-
});
138+
### Pattern 2: Manual Cache Control
139+
140+
```js
141+
// Using "use cache" directive with manual control
142+
async function fetchUserProfile(userId) {
143+
'use cache';
144+
145+
cacheTag({
146+
tag: `user-${userId}`,
147+
ttl: '1h',
148+
});
149+
150+
return database.getUserProfile(userId);
151+
}
152+
153+
// Invalidate cache when profile updates
154+
async function updateUserProfile(userId, data) {
155+
await database.updateUserProfile(userId, data);
156+
await invalidate(`user-${userId}`);
157+
}
158+
```
159+
160+
### Pattern 3: Forced Refresh with Revalidation
161+
162+
```js
163+
import { unstable_revalidate as revalidate } from 'commandkit';
75164

165+
// Command to force refresh server stats
76166
export async function run({ interaction }) {
77167
await interaction.deferReply();
78168

79-
// Fetch data
80-
const data = await fetchData();
169+
// Force refresh and get new data
170+
const newStats = await revalidate('server-stats');
81171

82-
// Send the data to the user
83-
await interaction.editReply(data);
172+
return interaction.editReply({
173+
content: `Stats refreshed! New member count: ${newStats.members}`,
174+
});
84175
}
85176
```
86177
87-
By default, the cached data will be stored forever until `revalidate()` or `expire()` is called on the cache object. You can also specify a custom TTL (time to live) for the cache by passing a second argument to the `cache` function.
178+
## Cache Duration Format
179+
180+
You can specify cache duration in multiple formats:
181+
182+
- As milliseconds: `ttl: 60000` (1 minute)
183+
- As string shortcuts:
184+
- `'5s'` - 5 seconds
185+
- `'1m'` - 1 minute
186+
- `'2h'` - 2 hours
187+
- `'1d'` - 1 day etc.
188+
189+
## Best Practices
190+
191+
1. **Choose Good Cache Keys:**
192+
193+
- Use meaningful, unique cache tags
194+
- Include relevant IDs (like `user-123` or `guild-456`)
195+
196+
2. **Set Appropriate TTL:**
197+
198+
- Short TTL for frequently changing data
199+
- Longer TTL for static content
200+
- Consider invalidating manually for important updates
201+
202+
3. **Handle Cache Misses:**
203+
204+
- Cache functions should handle cases when data isn't found
205+
- Provide default values when appropriate
206+
207+
4. **Invalidate Strategically:**
208+
- Invalidate cache when underlying data changes
209+
- Consider using `revalidate()` for controlled updates
210+
211+
## Using the cache
212+
213+
### Using commandkit CLI
88214
89215
```js
216+
async function fetchData() {
217+
'use cache';
218+
219+
// Fetch data from an external source
220+
const data = await fetch('https://my-example-api.com/data');
221+
222+
return data.json();
223+
}
224+
```
225+
226+
### Using the cache manually
227+
228+
```js
229+
import { unstable_cache as cache } from 'commandkit';
230+
90231
const fetchData = cache(
91232
async () => {
92233
// Fetch data from an external source
93234
const data = await fetch('https://my-example-api.com/data');
94-
95235
return data.json();
96236
},
97237
{
98-
name: 'fetchData', // name of the cache
99-
ttl: 60_000, // cache for 1 minute
238+
tag: 'user-data', // Optional cache tag name
239+
ttl: '1m', // TTL as string or number (in ms)
100240
},
101241
);
102242
```
103243
104-
You may want to specify the cache parameters when using `"use cache"` directive. When using this approach, you can use `cacheTag()` to tag the cache with custom parameters.
244+
By default, the cached data will be stored for 15 minutes unless `revalidate()` or `invalidate()` is called. You can specify a custom TTL (time to live) either as milliseconds or as a time string.
245+
246+
### Setting Cache Parameters
247+
248+
When using the `"use cache"` directive, you can use `cacheTag()` to set cache parameters:
105249
106250
```js
107251
import { unstable_cacheTag as cacheTag } from 'commandkit';
@@ -110,46 +254,66 @@ async function fetchData() {
110254
'use cache';
111255

112256
cacheTag({
113-
name: 'fetchData', // name of the cache
114-
ttl: 60_000, // cache for 1 minute
257+
tag: 'user-data', // cache tag name
258+
ttl: '1m', // TTL as string or number (in ms)
115259
});
260+
// Or just set the tag name
261+
cacheTag('user-data');
116262

117-
// Fetch data from an external source
118263
const data = await fetch('https://my-example-api.com/data');
264+
return data.json();
265+
}
266+
```
119267
268+
You can also set just the TTL using `cacheLife`:
269+
270+
```js
271+
import { unstable_cacheLife as cacheLife } from 'commandkit';
272+
273+
async function fetchData() {
274+
'use cache';
275+
276+
cacheLife('1m'); // TTL as string
277+
// or
278+
cacheLife(60_000); // TTL in milliseconds
279+
280+
const data = await fetch('https://my-example-api.com/data');
120281
return data.json();
121282
}
122283
```
123284
124285
:::tip
125-
`cacheTag()` will only tag the function when it first runs. Subsequent calls to the function will not tag the cache again.
126-
If not tagged manually, commandkit assigns random tag name with 15 minutes TTL.
127-
128-
`cacheTag()` does not work with the `cache` function. It must be used with the `"use cache"` directive only.
286+
`cacheTag()` will only tag the function when it first runs. If not tagged manually, CommandKit assigns a random tag name with 15 minutes TTL.
287+
`cacheTag()` must be used with the `cache()` function or a function with `"use cache"` directive only.
129288
:::
130289
131-
> You can alternatively use `cacheLife()` to set the TTL of the cache. Example: `cacheLife(10_000)` would set the TTL to 10 seconds.
290+
## Managing Cache Data
132291
133-
## Invalidating/Revalidating the cache
292+
### Invalidating Cache
134293
135-
Revalidating the cache is the process of updating the cached data with fresh data from the original source on demand. You can use the `unstable_revalidate()` function to revalidate the cache. CommandKit will not immediately revalidate the cache, but it will do so the next time the cached data is requested. Because of this, we can also term it as "lazy revalidation".
294+
To immediately remove cached data:
136295
137296
```js
138-
import { unstable_revalidate as revalidate } from 'commandkit';
297+
import { unstable_invalidate as invalidate } from 'commandkit';
139298

140-
// Revalidate the cache
141-
await revalidate('cache-tag-name');
299+
// Remove the cache entry immediately
300+
await invalidate('user-data');
142301
```
143302
144-
## Expire the cache
303+
### Revalidating Cache
145304
146-
Expiring the cache is the process of removing the cached data or resetting the TTL of the cache. Use the `unstable_expire()` function to expire the cache.
305+
To force refresh the cached data:
147306
148307
```js
149-
import { unstable_expire as expire } from 'commandkit';
308+
import { unstable_revalidate as revalidate } from 'commandkit';
150309

151-
// Expire the cache
152-
await expire('cache-tag-name', /* optional ttl */ 60_000);
310+
// Revalidate and get fresh data
311+
const freshData = await revalidate('user-data');
153312
```
154313
155-
If no TTL is given or TTL is in the past, commandkit deletes the cache immediately.
314+
The revalidation process will:
315+
316+
1. Remove the existing cache entry
317+
2. Re-execute the cached function to get fresh data
318+
3. Store the new data in cache
319+
4. Return the fresh data

packages/commandkit/bin/esbuild-plugins/use-cache.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const generate = _generate.default || _generate;
99
const IMPORT_PATH = 'commandkit';
1010
const DIRECTIVE = 'use cache';
1111
const CACHE_IDENTIFIER =
12-
'unstable_super_duper_secret_internal_for_use_cache_directive_of_commandkit_cli_do_not_use_it_directly_or_you_will_be_fired_kthxbai';
12+
'unstable_super_duper_secret_internal_for_use_cache_directive_of_commandkit_cli_do_not_use_it_directly_or_you_will_be_fired_from_your_job_kthxbai';
1313

1414
const generateRandomString = (length = 6) => {
1515
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

0 commit comments

Comments
 (0)