소스 검색

feat: Adds cache module

Owen Diffey 1 년 전
부모
커밋
1f1c54e1ad
3개의 변경된 파일162개의 추가작업 그리고 0개의 파일을 삭제
  1. 2 0
      backend/src/ModuleManager.ts
  2. 155 0
      backend/src/modules/CacheModule.ts
  3. 5 0
      backend/src/types/Modules.ts

+ 2 - 0
backend/src/ModuleManager.ts

@@ -35,6 +35,7 @@ export class ModuleManager {
 	private async _loadModule<T extends keyof Modules>(moduleName: T) {
 		const mapper = {
 			api: "APIModule",
+			cache: "CacheModule",
 			data: "DataModule",
 			events: "EventsModule",
 			stations: "StationsModule",
@@ -54,6 +55,7 @@ export class ModuleManager {
 	private async _loadModules() {
 		this._modules = {
 			api: await this._loadModule("api"),
+			cache: await this._loadModule("cache"),
 			data: await this._loadModule("data"),
 			events: await this._loadModule("events"),
 			stations: await this._loadModule("stations"),

+ 155 - 0
backend/src/modules/CacheModule.ts

@@ -0,0 +1,155 @@
+import config from "config";
+import { RedisClientType, createClient } from "redis";
+import BaseModule, { ModuleStatus } from "@/BaseModule";
+import { UniqueMethods } from "@/types/Modules";
+
+export class CacheModule extends BaseModule {
+	private _redisClient?: RedisClientType;
+
+	/**
+	 * Cache Module
+	 */
+	public constructor() {
+		super("cache");
+
+		this._jobConfigDefault = "disabled";
+	}
+
+	/**
+	 * startup - Startup cache module
+	 */
+	public override async startup() {
+		await super.startup();
+
+		// @ts-ignore
+		this._redisClient = createClient({
+			...config.get("redis"),
+			reconnectStrategy: (retries: number, error) => {
+				if (
+					retries >= 10 ||
+					![ModuleStatus.STARTING, ModuleStatus.STARTED].includes(
+						this.getStatus()
+					)
+				)
+					return false;
+
+				this.log({
+					type: "debug",
+					message: `Redis reconnect attempt ${retries}`,
+					data: error
+				});
+
+				return Math.min(retries * 50, 500);
+			}
+		});
+
+		this._redisClient.on("error", error => {
+			this.log({ type: "error", message: error.message, data: error });
+
+			this.setStatus(ModuleStatus.ERROR);
+		});
+
+		this._redisClient.on("ready", () => {
+			this.log({ type: "debug", message: "Redis connection ready" });
+
+			if (this.getStatus() === ModuleStatus.ERROR)
+				this.setStatus(ModuleStatus.STARTED);
+		});
+
+		await this._redisClient.connect();
+
+		const redisConfigResponse = await this._redisClient.sendCommand([
+			"CONFIG",
+			"GET",
+			"notify-keyspace-events"
+		]);
+
+		if (
+			!(
+				Array.isArray(redisConfigResponse) &&
+				redisConfigResponse[1] === "xE"
+			)
+		)
+			throw new Error(
+				`notify-keyspace-events is NOT configured correctly! It is set to: ${
+					(Array.isArray(redisConfigResponse) &&
+						redisConfigResponse[1]) ||
+					"unknown"
+				}`
+			);
+
+		await super._started();
+	}
+
+	/**
+	 * shutdown - Shutdown cache module
+	 */
+	public override async shutdown() {
+		await super.shutdown();
+		if (this._redisClient) await this._redisClient.quit();
+		await this._stopped();
+	}
+
+	public canRunJobs(): boolean {
+		return this._redisClient?.isReady === true && super.canRunJobs();
+	}
+
+	public async getKeys(pattern: string) {
+		return this._redisClient!.KEYS(pattern);
+	}
+
+	public async get(key: string) {
+		const value = await this._redisClient!.GET(key);
+
+		return value === null ? null : JSON.parse(value);
+	}
+
+	public async set(key: string, value: any, ttl?: number) {
+		await this._redisClient!.SET(key, JSON.stringify(value), { EX: ttl });
+	}
+
+	public async remove(key: string) {
+		await this._redisClient!.DEL(key);
+	}
+
+	public async removeMany(keys: string | string[]) {
+		await Promise.all(
+			(Array.isArray(keys) ? keys : [keys]).map(async pattern => {
+				for await (const key of this._redisClient!.scanIterator({
+					MATCH: pattern
+				})) {
+					await this.remove(key);
+				}
+			})
+		);
+	}
+
+	public async getTtl(key: string) {
+		return this._redisClient!.TTL(key);
+	}
+
+	public async getTable(key: string) {
+		return this._redisClient!.HGETALL(key);
+	}
+
+	public async getTableItem(table: string, key: string) {
+		return this._redisClient!.HGET(table, key);
+	}
+
+	public async setTableItem(table: string, key: string, value: any) {
+		return this._redisClient!.HSET(table, key, value);
+	}
+
+	public async removeTableItem(table: string, key: string) {
+		return this._redisClient!.HDEL(table, key);
+	}
+}
+
+export type CacheModuleJobs = {
+	[Property in keyof UniqueMethods<CacheModule>]: {
+		payload: Parameters<UniqueMethods<CacheModule>[Property]>[1];
+		returns: Awaited<ReturnType<UniqueMethods<CacheModule>[Property]>>;
+	};
+};
+
+export default new CacheModule();

+ 5 - 0
backend/src/types/Modules.ts

@@ -1,4 +1,5 @@
 import { APIModule, APIModuleJobs } from "@/modules/APIModule";
+import { CacheModule, CacheModuleJobs } from "@/modules/CacheModule";
 import { DataModule, DataModuleJobs } from "@/modules/DataModule";
 import { EventsModule, EventsModuleJobs } from "@/modules/EventsModule";
 import { StationsModule, StationsModuleJobs } from "@/modules/StationsModule";
@@ -18,6 +19,9 @@ export type Jobs = {
 	api: {
 		[Property in keyof APIModuleJobs]: APIModuleJobs[Property];
 	};
+	cache: {
+		[Property in keyof CacheModuleJobs]: CacheModuleJobs[Property];
+	};
 	data: {
 		[Property in keyof DataModuleJobs]: DataModuleJobs[Property];
 	};
@@ -34,6 +38,7 @@ export type Jobs = {
 
 export type Modules = {
 	api: APIModule & typeof BaseModule;
+	cache: CacheModule & typeof BaseModule;
 	data: DataModule & typeof BaseModule;
 	events: EventsModule & typeof BaseModule;
 	stations: StationsModule & typeof BaseModule;