174 lines
4.8 KiB
JavaScript
174 lines
4.8 KiB
JavaScript
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,
|
|
}
|