hippofish/packages/backend/src/misc/cache.ts

132 lines
3.1 KiB
TypeScript
Raw Normal View History

2023-07-03 02:37:46 +02:00
import { redisClient } from "@/db/redis.js";
import { encode, decode } from "@msgpack/msgpack";
import { ChainableCommander } from "ioredis";
2021-03-18 02:49:14 +01:00
export class Cache<T> {
2023-07-03 02:37:46 +02:00
private ttl: number;
2023-07-03 02:55:20 +02:00
private prefix: string;
2023-07-03 02:37:46 +02:00
2023-07-03 04:10:33 +02:00
constructor(prefix: string, ttlSeconds: number) {
this.ttl = ttlSeconds;
2023-07-03 02:55:20 +02:00
this.prefix = `cache:${prefix}`;
2023-07-03 02:37:46 +02:00
}
2021-03-18 02:49:14 +01:00
2023-07-03 02:37:46 +02:00
private prefixedKey(key: string | null): string {
2023-07-03 02:55:20 +02:00
return key ? `${this.prefix}:${key}` : this.prefix;
2023-07-03 02:37:46 +02:00
}
2023-07-03 04:10:33 +02:00
public async set(
key: string | null,
value: T,
transaction?: ChainableCommander,
): Promise<void> {
2023-07-03 02:37:46 +02:00
const _key = this.prefixedKey(key);
const _value = Buffer.from(encode(value));
const commander = transaction ?? redisClient;
2023-07-03 04:10:33 +02:00
await commander.set(_key, _value, "EX", this.ttl);
2021-03-18 02:49:14 +01:00
}
2023-07-03 04:10:33 +02:00
public async get(key: string | null, renew = false): Promise<T | undefined> {
2023-07-03 02:37:46 +02:00
const _key = this.prefixedKey(key);
const cached = await redisClient.getBuffer(_key);
if (cached === null) return undefined;
2023-07-03 04:10:33 +02:00
if (renew) await redisClient.expire(_key, this.ttl);
2023-07-03 02:37:46 +02:00
return decode(cached) as T;
2021-03-18 02:49:14 +01:00
}
2023-07-03 04:10:33 +02:00
public async getAll(renew = false): Promise<Map<string, T>> {
2023-07-03 02:55:20 +02:00
const keys = await redisClient.keys(`${this.prefix}*`);
2023-07-03 02:37:46 +02:00
const map = new Map<string, T>();
if (keys.length === 0) {
return map;
}
const values = await redisClient.mgetBuffer(keys);
for (const [i, key] of keys.entries()) {
const val = values[i];
if (val !== null) {
map.set(key, decode(val) as T);
}
2021-03-18 02:49:14 +01:00
}
2023-07-03 02:37:46 +02:00
2023-07-03 04:10:33 +02:00
if (renew) {
const trans = redisClient.multi();
for (const key of map.keys()) {
trans.expire(key, this.ttl);
}
await trans.exec();
}
2023-07-03 02:37:46 +02:00
return map;
2021-03-18 02:49:14 +01:00
}
2023-07-03 02:37:46 +02:00
public async delete(...keys: (string | null)[]): Promise<void> {
if (keys.length > 0) {
const _keys = keys.map(this.prefixedKey);
await redisClient.del(_keys);
}
}
2022-03-20 17:22:00 +01:00
/**
2023-07-03 02:37:46 +02:00
* Returns if cached value exists. Otherwise, calls fetcher and caches.
* Overwrites cached value if invalidated by the optional validator.
2022-03-20 17:22:00 +01:00
*/
2023-01-13 05:40:33 +01:00
public async fetch(
key: string | null,
fetcher: () => Promise<T>,
2023-07-03 04:10:33 +02:00
renew = false,
2023-01-13 05:40:33 +01:00
validator?: (cachedValue: T) => boolean,
): Promise<T> {
2023-07-03 04:10:33 +02:00
const cachedValue = await this.get(key, renew);
if (cachedValue !== undefined) {
2022-03-20 17:22:00 +01:00
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();
2023-07-03 02:37:46 +02:00
await this.set(key, value);
return value;
}
/**
2023-07-03 02:37:46 +02:00
* 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.
*/
2023-01-13 05:40:33 +01:00
public async fetchMaybe(
key: string | null,
fetcher: () => Promise<T | undefined>,
2023-07-03 04:10:33 +02:00
renew = false,
2023-01-13 05:40:33 +01:00
validator?: (cachedValue: T) => boolean,
): Promise<T | undefined> {
2023-07-03 04:10:33 +02:00
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) {
2023-07-03 02:37:46 +02:00
await this.set(key, value);
}
return value;
}
2021-03-18 02:49:14 +01:00
}