Skip to content

Commit

Permalink
feat: rework on cache
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Jan 13, 2025
1 parent b45ec83 commit 7e476f7
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 114 deletions.
23 changes: 2 additions & 21 deletions apps/test-bot/src/commands/misc/help.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
import {
SlashCommandProps,
CommandData,
unstable_cacheTag as cacheTag,
} from 'commandkit';
import { setTimeout } from 'node:timers/promises';
import { SlashCommandProps, CommandData } from 'commandkit';

export const data: CommandData = {
name: 'help',
description: 'This is a help command.',
};

async function someExpensiveDatabaseCall() {
'use cache';

await setTimeout(5000);

return Date.now();
}

cacheTag(15000, someExpensiveDatabaseCall);

export async function run({ interaction }: SlashCommandProps) {
await interaction.deferReply();

const dataRetrievalStart = Date.now();
const time = await someExpensiveDatabaseCall();
const dataRetrievalEnd = Date.now() - dataRetrievalStart;

return interaction.editReply({
embeds: [
{
title: 'Help',
description: `This is a help command. The current time is \`${time}\`. Fetched in ${dataRetrievalEnd}ms.`,
description: `This is a help command.`,
color: 0x7289da,
timestamp: new Date().toISOString(),
},
Expand Down
44 changes: 44 additions & 0 deletions apps/test-bot/src/commands/misc/xp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
SlashCommandProps,
CommandData,
unstable_cacheTag as cacheTag,
} from 'commandkit';
import { setTimeout } from 'node:timers/promises';
import { database } from '../../database/store';

export const data: CommandData = {
name: 'xp',
description: 'This is an xp command.',
};

async function getUserXP(guildId: string, userId: string) {
'use cache';

cacheTag(`xp:${guildId}:${userId}`);

const xp: number = (await database.get(`${guildId}:${userId}`)) ?? 0;

return xp;
}

export async function run({ interaction }: SlashCommandProps) {
await interaction.deferReply();

const dataRetrievalStart = Date.now();
const xp = await getUserXP(interaction.guildId!, interaction.user.id);
const dataRetrievalEnd = Date.now() - dataRetrievalStart;

return interaction.editReply({
embeds: [
{
title: 'XP',
description: `Hello ${interaction.user}, your xp is ${xp}.`,
color: 0x7289da,
timestamp: new Date().toISOString(),
footer: {
text: `Data retrieval took ${dataRetrievalEnd}ms`,
},
},
],
});
}
30 changes: 30 additions & 0 deletions apps/test-bot/src/database/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { setTimeout } from 'node:timers/promises';

// Simulate a random latency between 30ms to 1.5s
const randomLatency = () => setTimeout(Math.floor(Math.random() * 1500) + 30);

class DataStore {
private store = new Map<string, any>();

async get(key: string) {
await randomLatency();
return this.store.get(key);
}

async set(key: string, value: any) {
await randomLatency();
this.store.set(key, value);
}

async delete(key: string) {
await randomLatency();
this.store.delete(key);
}

async clear() {
await randomLatency();
this.store.clear();
}
}

export const database = new DataStore();
16 changes: 16 additions & 0 deletions apps/test-bot/src/events/messageCreate/give-xp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Message } from 'discord.js';
import { unstable_revalidate as revalidate } from 'commandkit';
import { database } from '../../database/store';

export default async function (message: Message) {
if (message.author.bot || !message.inGuild()) return;

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

await database.set(`${message.guildId}:${message.author.id}`, oldXp + xp);

// revalidate the cache
await revalidate(`xp:${message.guildId}:${message.author.id}`);
}
53 changes: 42 additions & 11 deletions apps/website/docs/guide/11-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ description: A guide on how to implement caching in your bot using CommandKit.
# Caching

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

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.
Expand Down Expand Up @@ -60,7 +61,7 @@ export async function run({ interaction }) {

### Using the cache manually

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

```js
import { unstable_cache as cache } from 'commandkit';
Expand All @@ -83,7 +84,7 @@ export async function run({ interaction }) {
}
```

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.
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.

```js
const fetchData = cache(
Expand All @@ -100,25 +101,55 @@ const fetchData = cache(
);
```

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.
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.

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

async function fetchData() {
'use cache';

cacheTag({
name: 'fetchData', // name of the cache
ttl: 60_000, // cache for 1 minute
});

// Fetch data from an external source
const data = await fetch('https://my-example-api.com/data');

return data.json();
}
```

cacheTag(
{
name: 'fetchData', // name of the cache
ttl: 60_000, // cache for 1 minute
},
fetchData,
);
:::tip
`cacheTag()` will only tag the function when it first runs. Subsequent calls to the function will not tag the cache again.
If not tagged manually, commandkit assigns random tag name with 15 minutes TTL.

`cacheTag()` does not work with the `cache` function. It must be used with the `"use cache"` directive only.
:::

> You can alternatively use `cacheLife()` to set the TTL of the cache. Example: `cacheLife(10_000)` would set the TTL to 10 seconds.
## Invalidating/Revalidating the cache

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".

```js
import { unstable_revalidate as revalidate } from 'commandkit';

// Revalidate the cache
await revalidate('cache-tag-name');
```

## Expire the cache

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.

```js
import { unstable_expire as expire } from 'commandkit';

// Expire the cache
await expire('cache-tag-name', /* optional ttl */ 60_000);
```

If no TTL is given or TTL is in the past, commandkit deletes the cache immediately.
14 changes: 7 additions & 7 deletions packages/commandkit/bin/esbuild-plugins/use-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const generate = _generate.default || _generate;

const IMPORT_PATH = 'commandkit';
const DIRECTIVE = 'use cache';
const CACHE_IDENTIFIER = 'unstable_cache';
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';

const generateRandomString = (length = 6) => {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
Expand Down Expand Up @@ -46,7 +46,7 @@ export const cacheDirectivePlugin = () => {
enter(path) {
const binding = path.scope.getBinding(CACHE_IDENTIFIER);
if (binding) {
state.cacheIdentifierName = `cache_${generateRandomString()}`;
state.cacheIdentifierName = `${CACHE_IDENTIFIER}_${generateRandomString()}`;
}
},
},
Expand Down Expand Up @@ -115,11 +115,11 @@ export const cacheDirectivePlugin = () => {
// Create a new body without the 'use cache' directive
const newBody = t.isBlockStatement(path.node.body)
? t.blockStatement(
path.node.body.body,
path.node.body.directives.filter(
(d) => d.value.value !== DIRECTIVE,
),
)
path.node.body.body,
path.node.body.directives.filter(
(d) => d.value.value !== DIRECTIVE,
),
)
: path.node.body;

const wrapped = t.callExpression(
Expand Down
2 changes: 2 additions & 0 deletions packages/commandkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@
"@babel/types": "^7.26.5",
"commander": "^12.1.0",
"dotenv": "^16.4.7",
"ms": "^2.1.3",
"ora": "^8.0.1",
"rfdc": "^1.3.1",
"rimraf": "^5.0.5",
"tsup": "^8.3.5"
},
"devDependencies": {
"@types/ms": "^0.7.34",
"@types/node": "^22.10.2",
"@types/yargs": "^17.0.32",
"discord.js": "^14.16.3",
Expand Down
Loading

0 comments on commit 7e476f7

Please sign in to comment.