const { getRedisClient } = require("./client") const { recordCacheMiss } = require("./cacheMetrics.service") const USER_PUBLIC_ID_KEY_PREFIX = "users:public:id:" const USER_PUBLIC_NAME_KEY_PREFIX = "users:public:name:" const DEFAULT_USER_TTL_SECONDS = 60 * 60 function createRedisClient() { return getRedisClient() } function normalizeUserId(userId) { const id = Number(userId) return Number.isInteger(id) && id > 0 ? id : null } function normalizeUserPayload(user) { const id = normalizeUserId(user?.id) if (!id) return null const badgesSource = Array.isArray(user?.badges) ? user.badges : Array.isArray(user?.userBadges) ? user.userBadges : [] const badgeIds = badgesSource .map((item) => { if (!item) return null const badge = item.badge || item const id = Number(badge?.id) return Number.isInteger(id) && id > 0 ? id : null }) .filter(Boolean) return { id, username: user?.username ?? null, avatarUrl: user?.avatarUrl ?? null, badgeIds: Array.from(new Set(badgeIds)), } } async function getUserPublicFromRedis(userId) { const id = normalizeUserId(userId) if (!id) return null const redis = createRedisClient() const key = `${USER_PUBLIC_ID_KEY_PREFIX}${id}` try { const raw = await redis.call("JSON.GET", key) if (!raw) { await recordCacheMiss({ key, label: "user-public" }) return null } return JSON.parse(raw) } catch { return null } finally {} } async function getUsersPublicByIds(userIds = []) { const ids = Array.from( new Set((Array.isArray(userIds) ? userIds : []).map(normalizeUserId).filter(Boolean)) ) if (!ids.length) return new Map() const redis = createRedisClient() try { const pipeline = redis.pipeline() ids.forEach((id) => pipeline.call("JSON.GET", `${USER_PUBLIC_ID_KEY_PREFIX}${id}`)) const results = await pipeline.exec() const map = new Map() results.forEach(([, raw], idx) => { if (!raw) return try { const user = JSON.parse(raw) if (user && user.id) map.set(ids[idx], user) } catch { return } }) return map } catch { return new Map() } finally {} } async function setUsersPublicInRedis(users = [], { ttlSecondsById = null } = {}) { const payloads = (Array.isArray(users) ? users : []) .map(normalizeUserPayload) .filter(Boolean) if (!payloads.length) return 0 const redis = createRedisClient() try { const pipeline = redis.pipeline() payloads.forEach((user) => { const key = `${USER_PUBLIC_ID_KEY_PREFIX}${user.id}` pipeline.call("JSON.SET", key, "$", JSON.stringify(user)) if (user.username) { pipeline.set(`${USER_PUBLIC_NAME_KEY_PREFIX}${String(user.username).toLowerCase()}`, String(user.id)) } const ttlSeconds = ttlSecondsById?.[user.id] if (ttlSeconds) { pipeline.expire(key, Number(ttlSeconds)) if (user.username) { pipeline.expire( `${USER_PUBLIC_NAME_KEY_PREFIX}${String(user.username).toLowerCase()}`, Number(ttlSeconds) ) } } }) await pipeline.exec() return payloads.length } catch { return 0 } finally {} } async function setUserPublicInRedis(user, { ttlSeconds = DEFAULT_USER_TTL_SECONDS } = {}) { const payload = normalizeUserPayload(user) if (!payload) return false const count = await setUsersPublicInRedis([payload], { ttlSecondsById: { [payload.id]: ttlSeconds } }) return count > 0 } async function ensureUserMinTtl(userId, { minSeconds = DEFAULT_USER_TTL_SECONDS } = {}) { const id = normalizeUserId(userId) if (!id) return { bumped: false } const redis = createRedisClient() const key = `${USER_PUBLIC_ID_KEY_PREFIX}${id}` const minTtl = Math.max(1, Number(minSeconds) || DEFAULT_USER_TTL_SECONDS) try { const ttl = await redis.ttl(key) if (ttl === -2) return { bumped: false } // no key if (ttl === -1 || ttl < minTtl) { const nextTtl = minTtl await redis.expire(key, nextTtl) return { bumped: true, ttl: nextTtl } } return { bumped: false, ttl } } catch { return { bumped: false } } finally {} } async function getUserIdByUsername(userName) { const normalized = String(userName || "").trim().toLowerCase() if (!normalized) return null const redis = createRedisClient() try { const raw = await redis.get(`${USER_PUBLIC_NAME_KEY_PREFIX}${normalized}`) if (!raw) { await recordCacheMiss({ key: `${USER_PUBLIC_NAME_KEY_PREFIX}${normalized}`, label: "user-name", }) } const id = raw ? Number(raw) : null return Number.isInteger(id) && id > 0 ? id : null } catch { return null } finally {} } module.exports = { getUserPublicFromRedis, getUsersPublicByIds, setUserPublicInRedis, setUsersPublicInRedis, ensureUserMinTtl, getUserIdByUsername, }