diff options
| author | Alex Pooley (@zuedev) <zuedev@gmail.com> | 2026-07-22 15:48:31 +0100 |
|---|---|---|
| committer | Alex Pooley (@zuedev) <zuedev@gmail.com> | 2026-07-22 15:48:31 +0100 |
| commit | c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae (patch) | |
| tree | c3aba3cd5c4f4708ca0bac39a2504a9309f67156 /174bg/discord-bot/source | |
| parent | 72487019b3f255cece40a8073bbad8bc9968a3b7 (diff) | |
| download | unnamed-group-c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae.tar unnamed-group-c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae.tar.gz unnamed-group-c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae.tar.bz2 unnamed-group-c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae.tar.xz unnamed-group-c6c9ad5d948a38c05e6a7d75097ca8a54c5a33ae.zip | |
implement caching for uex
Diffstat (limited to '174bg/discord-bot/source')
| -rw-r--r-- | 174bg/discord-bot/source/commands/_index.js | 5 | ||||
| -rw-r--r-- | 174bg/discord-bot/source/commands/clear-cache.js | 27 | ||||
| -rw-r--r-- | 174bg/discord-bot/source/controllers/uex.js | 20 | ||||
| -rw-r--r-- | 174bg/discord-bot/source/utilities/cache.js | 66 |
4 files changed, 111 insertions, 7 deletions
diff --git a/174bg/discord-bot/source/commands/_index.js b/174bg/discord-bot/source/commands/_index.js index 5fea200..25154e4 100644 --- a/174bg/discord-bot/source/commands/_index.js +++ b/174bg/discord-bot/source/commands/_index.js @@ -2,7 +2,8 @@ import requisition from "./requisition.js"; import closeTicket from "./close-ticket.js"; import oncallCheck from "./oncall-check.js"; import uex from "./uex.js"; +import clearCache from "./clear-cache.js"; -export { requisition, closeTicket, oncallCheck, uex }; +export { requisition, closeTicket, oncallCheck, uex, clearCache }; -export default [requisition, closeTicket, oncallCheck, uex]; +export default [requisition, closeTicket, oncallCheck, uex, clearCache]; diff --git a/174bg/discord-bot/source/commands/clear-cache.js b/174bg/discord-bot/source/commands/clear-cache.js new file mode 100644 index 0000000..ad99723 --- /dev/null +++ b/174bg/discord-bot/source/commands/clear-cache.js @@ -0,0 +1,27 @@ +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import fs from "fs"; + +export default { + data: new SlashCommandBuilder() + .setName("clear-cache") + .setDescription( + "Clears the bot's cache.", + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + const cacheDir = ".cache"; + + if (fs.existsSync(cacheDir)) { + fs.rmSync(cacheDir, { recursive: true, force: true }); + await interaction.reply({ + content: "Cache cleared successfully.", + flags: MessageFlags.Ephemeral, + }); + } else { + await interaction.reply({ + content: "Cache directory does not exist.", + flags: MessageFlags.Ephemeral, + }); + } + }, +}; diff --git a/174bg/discord-bot/source/controllers/uex.js b/174bg/discord-bot/source/controllers/uex.js index 615b517..827013a 100644 --- a/174bg/discord-bot/source/controllers/uex.js +++ b/174bg/discord-bot/source/controllers/uex.js @@ -1,24 +1,34 @@ import { get } from "./http.js"; +import { cached } from "../utilities/cache.js"; export async function categories() { - return await get("https://api.uexcorp.uk/2.0/categories"); + return await cached("uex", "categories", 60 * 60 * 1000 * 24, () => + get("https://api.uexcorp.uk/2.0/categories"), + ); } export async function items(id_category) { let items = []; if (!id_category) { - const { data: categories } = await get("https://api.uexcorp.uk/2.0/categories"); + const { data: allCategories } = await categories(); - for (const category of categories) { - const { data: categoryItems } = await get(`https://api.uexcorp.uk/2.0/items?id_category=${category.id}`); + for (const category of allCategories) { + const { data: categoryItems } = await cached( + "uex", + `items:${category.id}`, + 60 * 60 * 1000 * 24, + () => get(`https://api.uexcorp.uk/2.0/items?id_category=${category.id}`), + ); if (!categoryItems) continue; items = items.concat(categoryItems); } } else { - const { data } = await get(`https://api.uexcorp.uk/2.0/items?category=${id_category}`); + const { data } = await cached("uex", `items:${id_category}`, 60 * 60 * 1000 * 24, () => + get(`https://api.uexcorp.uk/2.0/items?category=${id_category}`), + ); items = data; } diff --git a/174bg/discord-bot/source/utilities/cache.js b/174bg/discord-bot/source/utilities/cache.js new file mode 100644 index 0000000..229a3d0 --- /dev/null +++ b/174bg/discord-bot/source/utilities/cache.js @@ -0,0 +1,66 @@ +/** + * Utility module for caching arbitrary JSON-serializable values to disk with an expiry, + * to avoid re-fetching data (e.g. from external APIs) more often than necessary. + * + * Cache entries are stored under `.cache/<namespace>/<hashed-key>.json` at the project root. + */ + +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const CACHE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".cache"); + +function keyToFileName(key) { + return `${createHash("sha256").update(key).digest("hex")}.json`; +} + +async function readEntry(namespace, key) { + const filePath = path.join(CACHE_ROOT, namespace, keyToFileName(key)); + + try { + const { expiresAt, data } = JSON.parse(await readFile(filePath, "utf-8")); + + if (Date.now() >= expiresAt) return undefined; + + return data; + } catch { + // missing, unreadable, or corrupt cache entry - treat as a cache miss + return undefined; + } +} + +async function writeEntry(namespace, key, data, ttlMs) { + const dir = path.join(CACHE_ROOT, namespace); + + await mkdir(dir, { recursive: true }); + await writeFile( + path.join(dir, keyToFileName(key)), + JSON.stringify({ expiresAt: Date.now() + ttlMs, data }), + ); +} + +/** + * Returns the cached value for `key` within `namespace` if it exists and hasn't expired. + * Otherwise, calls `fetcher`, caches its result for `ttlMs` milliseconds, and returns it. + * @param {string} namespace - Subdirectory under `.cache` to group related entries (e.g. "uex"). + * @param {string} key - Unique identifier for this cached value within the namespace. + * @param {number} ttlMs - How long the cached value should remain valid, in milliseconds. + * @param {() => Promise<any>} fetcher - Called to produce a fresh value on a cache miss. + */ +export async function cached(namespace, key, ttlMs, fetcher) { + const existing = await readEntry(namespace, key); + if (existing !== undefined) return existing; + + const data = await fetcher(); + + // don't cache failed/empty lookups so they get retried on the next call + if (data !== null && data !== undefined) { + await writeEntry(namespace, key, data, ttlMs); + } + + return data; +} + +export default { cached }; |
