import { redisClient } from "../db/redis.js"; import { encode, decode } from "msgpackr"; import config from "../config/index.js"; export class Cache { ttl; prefix; constructor(name, ttlSeconds){ this.ttl = ttlSeconds; this.prefix = `cache:${name}`; } prefixedKey(key) { return key ? `${this.prefix}:${key}` : this.prefix; } async set(key, value, transaction) { const _key = this.prefixedKey(key); const _value = Buffer.from(encode(value)); const commander = transaction ?? redisClient; await commander.set(_key, _value, "EX", this.ttl); } async get(key, renew = false) { const _key = this.prefixedKey(key); const cached = await redisClient.getBuffer(_key); if (cached === null) return undefined; if (renew) await redisClient.expire(_key, this.ttl); return decode(cached); } async getAll(renew = false) { const finalPrefix = `${config.redis.prefix}:${this.prefix}:`; const keys = (await redisClient.keys(`${finalPrefix}*`)).map((p)=>p.substring(finalPrefix.length)); const prefixedKeys = keys.map((p)=>this.prefixedKey(p)); const map = new Map(); if (keys.length === 0) { return map; } const values = await redisClient.mgetBuffer(prefixedKeys); for (const [i, key] of keys.entries()){ const val = values[i]; if (val !== null) { map.set(key, decode(val)); } } if (renew) { const trans = redisClient.multi(); for (const key of map.keys()){ trans.expire(this.prefixedKey(key), this.ttl); } await trans.exec(); } return map; } async delete(...keys) { if (keys.length > 0) { const _keys = keys.map((p)=>this.prefixedKey(p)); await redisClient.del(_keys); } } /** * Returns if cached value exists. Otherwise, calls fetcher and caches. * Overwrites cached value if invalidated by the optional validator. */ async fetch(key, fetcher, renew = false, validator) { const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { // Cache HIT return cachedValue; } } else { // Cache HIT return cachedValue; } } // Cache MISS const value = await fetcher(); await this.set(key, value); return value; } /** * Returns if cached value exists. Otherwise, calls fetcher and caches if the fetcher returns a value. * Overwrites cached value if invalidated by the optional validator. */ async fetchMaybe(key, fetcher, renew = false, validator) { const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { // Cache HIT return cachedValue; } } else { // Cache HIT return cachedValue; } } // Cache MISS const value = await fetcher(); if (value !== undefined) { await this.set(key, value); } return value; } }