Skip to content

Commit 7e476f7

Browse files
committed
feat: rework on cache
1 parent b45ec83 commit 7e476f7

File tree

9 files changed

+291
-114
lines changed

9 files changed

+291
-114
lines changed

apps/test-bot/src/commands/misc/help.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
1-
import {
2-
SlashCommandProps,
3-
CommandData,
4-
unstable_cacheTag as cacheTag,
5-
} from 'commandkit';
6-
import { setTimeout } from 'node:timers/promises';
1+
import { SlashCommandProps, CommandData } from 'commandkit';
72

83
export const data: CommandData = {
94
name: 'help',
105
description: 'This is a help command.',
116
};
127

13-
async function someExpensiveDatabaseCall() {
14-
'use cache';
15-
16-
await setTimeout(5000);
17-
18-
return Date.now();
19-
}
20-
21-
cacheTag(15000, someExpensiveDatabaseCall);
22-
238
export async function run({ interaction }: SlashCommandProps) {
249
await interaction.deferReply();
2510

26-
const dataRetrievalStart = Date.now();
27-
const time = await someExpensiveDatabaseCall();
28-
const dataRetrievalEnd = Date.now() - dataRetrievalStart;
29-
3011
return interaction.editReply({
3112
embeds: [
3213
{
3314
title: 'Help',
34-
description: `This is a help command. The current time is \`${time}\`. Fetched in ${dataRetrievalEnd}ms.`,
15+
description: `This is a help command.`,
3516
color: 0x7289da,
3617
timestamp: new Date().toISOString(),
3718
},
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
SlashCommandProps,
3+
CommandData,
4+
unstable_cacheTag as cacheTag,
5+
} from 'commandkit';
6+
import { setTimeout } from 'node:timers/promises';
7+
import { database } from '../../database/store';
8+
9+
export const data: CommandData = {
10+
name: 'xp',
11+
description: 'This is an xp command.',
12+
};
13+
14+
async function getUserXP(guildId: string, userId: string) {
15+
'use cache';
16+
17+
cacheTag(`xp:${guildId}:${userId}`);
18+
19+
const xp: number = (await database.get(`${guildId}:${userId}`)) ?? 0;
20+
21+
return xp;
22+
}
23+
24+
export async function run({ interaction }: SlashCommandProps) {
25+
await interaction.deferReply();
26+
27+
const dataRetrievalStart = Date.now();
28+
const xp = await getUserXP(interaction.guildId!, interaction.user.id);
29+
const dataRetrievalEnd = Date.now() - dataRetrievalStart;
30+
31+
return interaction.editReply({
32+
embeds: [
33+
{
34+
title: 'XP',
35+
description: `Hello ${interaction.user}, your xp is ${xp}.`,
36+
color: 0x7289da,
37+
timestamp: new Date().toISOString(),
38+
footer: {
39+
text: `Data retrieval took ${dataRetrievalEnd}ms`,
40+
},
41+
},
42+
],
43+
});
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
3+
// Simulate a random latency between 30ms to 1.5s
4+
const randomLatency = () => setTimeout(Math.floor(Math.random() * 1500) + 30);
5+
6+
class DataStore {
7+
private store = new Map<string, any>();
8+
9+
async get(key: string) {
10+
await randomLatency();
11+
return this.store.get(key);
12+
}
13+
14+
async set(key: string, value: any) {
15+
await randomLatency();
16+
this.store.set(key, value);
17+
}
18+
19+
async delete(key: string) {
20+
await randomLatency();
21+
this.store.delete(key);
22+
}
23+
24+
async clear() {
25+
await randomLatency();
26+
this.store.clear();
27+
}
28+
}
29+
30+
export const database = new DataStore();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Message } from 'discord.js';
2+
import { unstable_revalidate as revalidate } from 'commandkit';
3+
import { database } from '../../database/store';
4+
5+
export default async function (message: Message) {
6+
if (message.author.bot || !message.inGuild()) return;
7+
8+
const oldXp =
9+
(await database.get(`${message.guildId}:${message.author.id}`)) ?? 0;
10+
const xp = Math.floor(Math.random() * 10) + 1;
11+
12+
await database.set(`${message.guildId}:${message.author.id}`, oldXp + xp);
13+
14+
// revalidate the cache
15+
await revalidate(`xp:${message.guildId}:${message.author.id}`);
16+
}

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

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ description: A guide on how to implement caching in your bot using CommandKit.
66
# Caching
77

88
:::warning
9-
This feature is currently available in development version of CommandKit only.
9+
This feature is currently available in development version of CommandKit only. Since it is an unstable feature, it may change in the future.
10+
You need to prefix the function with `unstable_` to use this feature until it is stable.
1011
:::
1112

1213
Caching is a technique used to store data in a temporary storage to reduce the time it takes to fetch the data from the original source. This can be useful in Discord bots to reduce the number of database queries or external API calls.
@@ -60,7 +61,7 @@ export async function run({ interaction }) {
6061

6162
### Using the cache manually
6263

63-
To use the cache manually, you can import the `unstable_cache()` function from CommandKit and use it to cache the result of a function.
64+
To use the cache manually, you can import the `cache()` function from CommandKit and use it to cache the result of a function.
6465

6566
```js
6667
import { unstable_cache as cache } from 'commandkit';
@@ -83,7 +84,7 @@ export async function run({ interaction }) {
8384
}
8485
```
8586

86-
By default, the cached data will be stored forever until `unstable_revalidate()` or `unstable_invalidate()` 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.
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.
8788

8889
```js
8990
const fetchData = cache(
@@ -100,25 +101,55 @@ const fetchData = cache(
100101
);
101102
```
102103

103-
You may want to specify the cache parameters when using `"use cache"` directive. When using this approach, you can use `unstable_cacheTag()` to tag the cache with custom parameters.
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.
104105

105106
```js
106107
import { unstable_cacheTag as cacheTag } from 'commandkit';
107108

108109
async function fetchData() {
109110
'use cache';
110111

112+
cacheTag({
113+
name: 'fetchData', // name of the cache
114+
ttl: 60_000, // cache for 1 minute
115+
});
116+
111117
// Fetch data from an external source
112118
const data = await fetch('https://my-example-api.com/data');
113119

114120
return data.json();
115121
}
122+
```
116123

117-
cacheTag(
118-
{
119-
name: 'fetchData', // name of the cache
120-
ttl: 60_000, // cache for 1 minute
121-
},
122-
fetchData,
123-
);
124+
:::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.
129+
:::
130+
131+
> You can alternatively use `cacheLife()` to set the TTL of the cache. Example: `cacheLife(10_000)` would set the TTL to 10 seconds.
132+
133+
## Invalidating/Revalidating the cache
134+
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".
136+
137+
```js
138+
import { unstable_revalidate as revalidate } from 'commandkit';
139+
140+
// Revalidate the cache
141+
await revalidate('cache-tag-name');
142+
```
143+
144+
## Expire the cache
145+
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.
147+
148+
```js
149+
import { unstable_expire as expire } from 'commandkit';
150+
151+
// Expire the cache
152+
await expire('cache-tag-name', /* optional ttl */ 60_000);
124153
```
154+
155+
If no TTL is given or TTL is in the past, commandkit deletes the cache immediately.

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const generate = _generate.default || _generate;
88

99
const IMPORT_PATH = 'commandkit';
1010
const DIRECTIVE = 'use cache';
11-
const CACHE_IDENTIFIER = 'unstable_cache';
11+
const CACHE_IDENTIFIER = 'unstable_super_duper_secret_internal_for_use_cache_directive_of_commandkit_cli_do_not_use_it_directly_or_you_will_be_fired_kthxbai';
1212

1313
const generateRandomString = (length = 6) => {
1414
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -46,7 +46,7 @@ export const cacheDirectivePlugin = () => {
4646
enter(path) {
4747
const binding = path.scope.getBinding(CACHE_IDENTIFIER);
4848
if (binding) {
49-
state.cacheIdentifierName = `cache_${generateRandomString()}`;
49+
state.cacheIdentifierName = `${CACHE_IDENTIFIER}_${generateRandomString()}`;
5050
}
5151
},
5252
},
@@ -115,11 +115,11 @@ export const cacheDirectivePlugin = () => {
115115
// Create a new body without the 'use cache' directive
116116
const newBody = t.isBlockStatement(path.node.body)
117117
? t.blockStatement(
118-
path.node.body.body,
119-
path.node.body.directives.filter(
120-
(d) => d.value.value !== DIRECTIVE,
121-
),
122-
)
118+
path.node.body.body,
119+
path.node.body.directives.filter(
120+
(d) => d.value.value !== DIRECTIVE,
121+
),
122+
)
123123
: path.node.body;
124124

125125
const wrapped = t.callExpression(

packages/commandkit/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@
4848
"@babel/types": "^7.26.5",
4949
"commander": "^12.1.0",
5050
"dotenv": "^16.4.7",
51+
"ms": "^2.1.3",
5152
"ora": "^8.0.1",
5253
"rfdc": "^1.3.1",
5354
"rimraf": "^5.0.5",
5455
"tsup": "^8.3.5"
5556
},
5657
"devDependencies": {
58+
"@types/ms": "^0.7.34",
5759
"@types/node": "^22.10.2",
5860
"@types/yargs": "^17.0.32",
5961
"discord.js": "^14.16.3",

0 commit comments

Comments
 (0)