HotTRDealsBackend/services/redis/userPublicCache.service.js
2026-02-04 06:39:10 +00:00

164 lines
4.6 KiB
JavaScript

const { getRedisClient } = require("./client")
const { recordCacheMiss } = require("./cacheMetrics.service")
const USER_PUBLIC_ID_KEY_PREFIX = "user:id:"
const USER_PUBLIC_NAME_KEY_PREFIX = "user: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)
} 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
} 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
} finally {}
}
async function setUserPublicInRedis(user, { ttlSeconds = DEFAULT_USER_TTL_SECONDS } = {}) {
const payload = normalizeUserPayload(user)
if (!payload) return false
await setUsersPublicInRedis([payload], { ttlSecondsById: { [payload.id]: ttlSeconds } })
return true
}
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 }
} 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
} finally {}
}
module.exports = {
getUserPublicFromRedis,
getUsersPublicByIds,
setUserPublicInRedis,
setUsersPublicInRedis,
ensureUserMinTtl,
getUserIdByUsername,
}