summaryrefslogtreecommitdiff
path: root/packages/backend/src/misc/cache.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/misc/cache.ts')
-rw-r--r--packages/backend/src/misc/cache.ts97
1 files changed, 91 insertions, 6 deletions
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index a805d18421..870dfd237c 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -1,9 +1,94 @@
+import Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
+// redis通すとDateのインスタンスはstringに変換されるので
+type Serialized<T> = {
+ [K in keyof T]:
+ T[K] extends Date
+ ? string
+ : T[K] extends (Date | null)
+ ? (string | null)
+ : T[K] extends Record<string, any>
+ ? Serialized<T[K]>
+ : T[K];
+};
+
+export class RedisKVCache<T> {
+ private redisClient: Redis.Redis;
+ private name: string;
+ private lifetime: number;
+ private memoryCache: MemoryKVCache<T>;
+
+ constructor(redisClient: RedisKVCache<never>['redisClient'], name: RedisKVCache<never>['name'], lifetime: RedisKVCache<never>['lifetime'], memoryCacheLifetime: number) {
+ this.redisClient = redisClient;
+ this.name = name;
+ this.lifetime = lifetime;
+ this.memoryCache = new MemoryKVCache(memoryCacheLifetime);
+ }
+
+ @bindThis
+ public async set(key: string, value: T): Promise<void> {
+ this.memoryCache.set(key, value);
+ if (this.lifetime === Infinity) {
+ await this.redisClient.set(
+ `kvcache:${this.name}:${key}`,
+ JSON.stringify(value),
+ );
+ } else {
+ await this.redisClient.set(
+ `kvcache:${this.name}:${key}`,
+ JSON.stringify(value),
+ 'ex', Math.round(this.lifetime / 1000),
+ );
+ }
+ }
+
+ @bindThis
+ public async get(key: string): Promise<Serialized<T> | T | undefined> {
+ const memoryCached = this.memoryCache.get(key);
+ if (memoryCached !== undefined) return memoryCached;
+
+ const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
+ if (cached == null) return undefined;
+ return JSON.parse(cached);
+ }
+
+ @bindThis
+ public async delete(key: string): Promise<void> {
+ this.memoryCache.delete(key);
+ await this.redisClient.del(`kvcache:${this.name}:${key}`);
+ }
+
+ /**
+ * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+ * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+ */
+ @bindThis
+ public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: Serialized<T> | T) => boolean): Promise<Serialized<T> | T> {
+ const cachedValue = await this.get(key);
+ if (cachedValue !== undefined) {
+ if (validator) {
+ if (validator(cachedValue)) {
+ // Cache HIT
+ return cachedValue;
+ }
+ } else {
+ // Cache HIT
+ return cachedValue;
+ }
+ }
+
+ // Cache MISS
+ const value = await fetcher();
+ this.set(key, value);
+ return value;
+ }
+}
+
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class MemoryKVCache<T> {
- public cache: Map<string | null, { date: number; value: T; }>;
+ public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
@@ -12,7 +97,7 @@ export class MemoryKVCache<T> {
}
@bindThis
- public set(key: string | null, value: T): void {
+ public set(key: string, value: T): void {
this.cache.set(key, {
date: Date.now(),
value,
@@ -20,7 +105,7 @@ export class MemoryKVCache<T> {
}
@bindThis
- public get(key: string | null): T | undefined {
+ public get(key: string): T | undefined {
const cached = this.cache.get(key);
if (cached == null) return undefined;
if ((Date.now() - cached.date) > this.lifetime) {
@@ -31,7 +116,7 @@ export class MemoryKVCache<T> {
}
@bindThis
- public delete(key: string | null) {
+ public delete(key: string) {
this.cache.delete(key);
}
@@ -40,7 +125,7 @@ export class MemoryKVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
- public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
+ public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
@@ -65,7 +150,7 @@ export class MemoryKVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
- public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
+ public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {