|
1 | 1 | import { Client, User } from "discord.js"; |
| 2 | +import { redis } from "../data/redis.js"; |
| 3 | +import { incrementDebugCounter } from "../debugCounters.js"; |
2 | 4 |
|
3 | 5 | const getOrFetchUserPromises: Map<string, Promise<User | undefined>> = new Map(); |
4 | 6 |
|
| 7 | +const UNKNOWN_KEY = "__UNKNOWN__"; |
| 8 | + |
| 9 | +const baseCacheTimeSeconds = 60 * 60; // 1 hour |
| 10 | +const cacheTimeJitterSeconds = 5 * 60; // 5 minutes |
| 11 | + |
| 12 | +// Use jitter on cache time to avoid tons of keys expiring at the same time |
| 13 | +const generateCacheTime = () => { |
| 14 | + const jitter = Math.floor(Math.random() * cacheTimeJitterSeconds); |
| 15 | + return baseCacheTimeSeconds + jitter; |
| 16 | +}; |
| 17 | + |
5 | 18 | /** |
6 | 19 | * Gets a user from cache or fetches it from the API if not cached. |
7 | 20 | * Concurrent requests are merged. |
8 | 21 | */ |
9 | 22 | export async function getOrFetchUser(bot: Client, userId: string): Promise<User | undefined> { |
| 23 | + // 1. Check Discord.js cache |
10 | 24 | const cachedUser = bot.users.cache.get(userId); |
11 | 25 | if (cachedUser) { |
| 26 | + incrementDebugCounter("getOrFetchUser:djsCache"); |
12 | 27 | return cachedUser; |
13 | 28 | } |
14 | 29 |
|
| 30 | + // 2. Check Redis |
| 31 | + const redisCacheKey = `cache:user:${userId}`; |
| 32 | + const userData = await redis.get(redisCacheKey); |
| 33 | + if (userData) { |
| 34 | + incrementDebugCounter("getOrFetchUser:redisCache"); |
| 35 | + if (userData === UNKNOWN_KEY) { |
| 36 | + console.log("Found unknown user in Redis cache"); |
| 37 | + return undefined; |
| 38 | + } |
| 39 | + console.log("Found user in Redis cache"); |
| 40 | + // @ts-expect-error Replace with a proper solution once that exists |
| 41 | + return new User(bot, JSON.parse(userData)); |
| 42 | + } |
| 43 | + |
15 | 44 | if (!getOrFetchUserPromises.has(userId)) { |
| 45 | + incrementDebugCounter("getOrFetchUser:fresh"); |
16 | 46 | getOrFetchUserPromises.set( |
17 | 47 | userId, |
18 | 48 | bot.users |
19 | 49 | .fetch(userId) |
20 | | - .catch(() => undefined) |
| 50 | + .catch(async () => { |
| 51 | + return undefined; |
| 52 | + }) |
| 53 | + .then(async (user) => { |
| 54 | + const cacheValue = user ? JSON.stringify(user.toJSON()) : UNKNOWN_KEY; |
| 55 | + await redis.set(redisCacheKey, cacheValue, { |
| 56 | + expiration: { |
| 57 | + type: "EX", |
| 58 | + value: generateCacheTime(), |
| 59 | + }, |
| 60 | + }); |
| 61 | + return user; |
| 62 | + }) |
21 | 63 | .finally(() => { |
22 | 64 | getOrFetchUserPromises.delete(userId); |
23 | 65 | }), |
|
0 commit comments