diff --git a/.prettierignore b/.prettierignore index fbf8015..eb4ffd6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,4 +24,5 @@ Dockerfile docker-compose.yml .agents -.claude \ No newline at end of file +.claude +CLAUDE.md \ No newline at end of file diff --git a/bun.lock b/bun.lock index af0b51b..74136e3 100644 --- a/bun.lock +++ b/bun.lock @@ -19,12 +19,12 @@ "hono-pino": "^0.10.3", "hono-rate-limiter": "^0.5.3", "hono-zod-openapi": "^1.1.1", - "ioredis": "^5.10.1", "nodemailer": "^8.0.7", "pg": "^8.21.0", "pino": "^10.3.1", }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/bcryptjs": "^3.0.0", "@types/bun": "^1.3.14", "@types/crypto-js": "^4.2.2", @@ -39,6 +39,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-simple-import-sort": "^13.0.0", + "globals": "^17.6.0", "husky": "^9.1.7", "lint-staged": "^17.0.5", "prettier": "^3.8.3", @@ -122,6 +123,8 @@ "@eslint/core": ["@eslint/core@1.2.1", "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], @@ -400,6 +403,8 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="], + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], diff --git a/package.json b/package.json index e5623ab..3e6c84c 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,12 @@ "hono-pino": "^0.10.3", "hono-rate-limiter": "^0.5.3", "hono-zod-openapi": "^1.1.1", - "ioredis": "^5.10.1", "nodemailer": "^8.0.7", "pg": "^8.21.0", "pino": "^10.3.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/bcryptjs": "^3.0.0", "@types/bun": "^1.3.14", "@types/crypto-js": "^4.2.2", @@ -63,6 +63,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-simple-import-sort": "^13.0.0", + "globals": "^17.6.0", "husky": "^9.1.7", "lint-staged": "^17.0.5", "prettier": "^3.8.3", diff --git a/src/bull/queue/send-email.queue.ts b/src/bull/queue/send-email.queue.ts index b067c3d..265822e 100644 --- a/src/bull/queue/send-email.queue.ts +++ b/src/bull/queue/send-email.queue.ts @@ -1,8 +1,6 @@ import { Queue } from "bullmq"; import { RedisClient } from "@database"; -const queueRedis = RedisClient.getQueueRedisClient(); - export const sendEmailQueue = new Queue("send-email", { - connection: queueRedis, + connection: RedisClient.getQueueConnection(), }); diff --git a/src/bull/worker/send-email.worker.ts b/src/bull/worker/send-email.worker.ts index 972a5aa..f4e0da9 100644 --- a/src/bull/worker/send-email.worker.ts +++ b/src/bull/worker/send-email.worker.ts @@ -3,8 +3,6 @@ import { RedisClient } from "@database"; import { logger } from "@utils"; import { EmailOptions, EmailService } from "@mail/mail.service"; -const queueRedis = RedisClient.getQueueRedisClient(); - const worker = new Worker( "send-email", async (job) => { @@ -17,7 +15,7 @@ const worker = new Worker( } }, { - connection: queueRedis, + connection: RedisClient.getQueueConnection(), }, ); diff --git a/src/libs/cache/cache.ts b/src/libs/cache/cache.ts index dc40261..73473da 100644 --- a/src/libs/cache/cache.ts +++ b/src/libs/cache/cache.ts @@ -1,22 +1,15 @@ -import Redis from "ioredis"; +import type { RedisClient as BunRedisClient } from "bun"; import { logger } from "@utils"; import { RedisClient } from "@database"; class Cache { - private static redis: Redis | null = null; - - private static getRedisClient(): Redis { - if (!this.redis) { - this.redis = RedisClient.getRedisClient(); - } - - return this.redis; + private static getClient(): BunRedisClient { + return RedisClient.getRedisClient(); } static async get(key: string): Promise { try { - const client = this.getRedisClient(); - const value = await client.get(key); + const value = await this.getClient().get(key); return value ? (JSON.parse(value) as T) : null; } catch (error) { logger.error(error, `Error getting cache for key ${key}:`); @@ -30,8 +23,12 @@ class Cache { ttl: number = 3600, ): Promise { try { - const client = this.getRedisClient(); - await client.set(key, JSON.stringify(value), "EX", ttl); + await this.getClient().send("SET", [ + key, + JSON.stringify(value), + "EX", + String(ttl), + ]); } catch (error) { logger.error(error, `Error setting cache for key ${key}:`); } @@ -39,8 +36,7 @@ class Cache { static async delete(key: string): Promise { try { - const client = this.getRedisClient(); - await client.del(key); + await this.getClient().del(key); } catch (error) { logger.error(error, `Error deleting cache for key ${key}:`); } @@ -48,8 +44,7 @@ class Cache { static async flush(): Promise { try { - const client = this.getRedisClient(); - await client.flushdb(); + await this.getClient().send("FLUSHDB", []); } catch (error) { logger.error(error, "Error flushing Redis cache:"); } @@ -57,9 +52,8 @@ class Cache { static async exists(key: string): Promise { try { - const client = this.getRedisClient(); - const exists = await client.exists(key); - return exists === 1; + const exists = await this.getClient().exists(key); + return exists === true; } catch (error) { logger.error(error, `Error checking existence of key ${key}:`); return false; @@ -87,8 +81,7 @@ class Cache { static async getKeys(pattern: string): Promise { try { - const client = this.getRedisClient(); - const keys = await client.keys(pattern); + const keys = await this.getClient().keys(pattern); return keys; } catch (error) { logger.error(error, `Error getting keys with pattern ${pattern}:`); @@ -96,12 +89,9 @@ class Cache { } } - static async disconnect(): Promise { + static disconnect(): void { try { - if (this.redis) { - await this.redis.quit(); - this.redis = null; - } + this.getClient().close(); } catch (error) { logger.error(error, "Error disconnecting from Redis:"); } diff --git a/src/libs/database/clickhouse/scripts/migrate.ts b/src/libs/database/clickhouse/scripts/migrate.ts index a871a41..d0a001a 100644 --- a/src/libs/database/clickhouse/scripts/migrate.ts +++ b/src/libs/database/clickhouse/scripts/migrate.ts @@ -126,9 +126,9 @@ async function main() { const migrator = new ClickHouseMigrator(); const command = process.argv[2]; - let executed: string[] = []; - let all: Array = []; - let pending: Array = []; + let executed: string[]; + let all: Array; + let pending: Array; switch (command) { case "migrate": diff --git a/src/libs/database/redis/redis-client.ts b/src/libs/database/redis/redis-client.ts index 0aecc0d..acb3145 100644 --- a/src/libs/database/redis/redis-client.ts +++ b/src/libs/database/redis/redis-client.ts @@ -1,32 +1,36 @@ +import { RedisClient as BunRedisClient } from "bun"; import { RedisConfig } from "@config"; -import Redis from "ioredis"; + +const buildUrl = (): string => { + const auth = RedisConfig.REDIS_PASSWORD + ? `:${encodeURIComponent(RedisConfig.REDIS_PASSWORD)}@` + : ""; + return `redis://${auth}${RedisConfig.REDIS_HOST}:${RedisConfig.REDIS_PORT}`; +}; + +export interface QueueConnectionOptions { + host: string; + port: number; + password?: string; + maxRetriesPerRequest: null; +} export class RedisClient { - private static redis: Redis | null = null; - private static queueRedis: Redis | null = null; + private static redis: BunRedisClient | null = null; - static getRedisClient(): Redis { + static getRedisClient(): BunRedisClient { if (!this.redis) { - this.redis = new Redis({ - host: RedisConfig.REDIS_HOST, - port: RedisConfig.REDIS_PORT, - password: RedisConfig.REDIS_PASSWORD || undefined, - }); + this.redis = new BunRedisClient(buildUrl()); } - return this.redis; } - static getQueueRedisClient(): Redis { - if (!this.queueRedis) { - this.queueRedis = new Redis({ - host: RedisConfig.REDIS_HOST, - port: RedisConfig.REDIS_PORT, - password: RedisConfig.REDIS_PASSWORD || undefined, - maxRetriesPerRequest: null, - }); - } - - return this.queueRedis; + static getQueueConnection(): QueueConnectionOptions { + return { + host: RedisConfig.REDIS_HOST, + port: RedisConfig.REDIS_PORT, + password: RedisConfig.REDIS_PASSWORD || undefined, + maxRetriesPerRequest: null, + }; } } diff --git a/src/modules/home/routes.ts b/src/modules/home/routes.ts index f1cc6e3..de22e9f 100644 --- a/src/modules/home/routes.ts +++ b/src/modules/home/routes.ts @@ -125,10 +125,10 @@ HomeRoutes.openapi(HealthRoute, async (c) => { // Redis remains unhealthy } - // Check Redis Queue + // Check Redis Queue (shares the same Redis instance) try { const start = Date.now(); - await RedisClient.getQueueRedisClient().ping(); + await RedisClient.getRedisClient().ping(); services.redisQueue = { status: "healthy", responseTime: Date.now() - start,