150 lines
4.3 KiB
JavaScript
150 lines
4.3 KiB
JavaScript
const { getRedisClient } = require("./client")
|
|
|
|
const CATEGORY_DEAL_INDEX_KEY_PREFIX = "index:category:"
|
|
const CATEGORY_DEAL_INDEX_KEY_SUFFIX = ":deals"
|
|
|
|
function normalizePositiveInt(value) {
|
|
const num = Number(value)
|
|
if (!Number.isInteger(num) || num <= 0) return null
|
|
return num
|
|
}
|
|
|
|
function normalizeEpochMs(value) {
|
|
const num = Number(value)
|
|
if (!Number.isFinite(num) || num <= 0) return null
|
|
return Math.floor(num)
|
|
}
|
|
|
|
function toEpochMs(value) {
|
|
if (value instanceof Date) return value.getTime()
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return null
|
|
return date.getTime()
|
|
}
|
|
|
|
function isActiveStatus(status) {
|
|
return String(status || "").toUpperCase() === "ACTIVE"
|
|
}
|
|
|
|
function getCategoryDealIndexKey(categoryId) {
|
|
const cid = normalizePositiveInt(categoryId)
|
|
if (!cid) return null
|
|
return `${CATEGORY_DEAL_INDEX_KEY_PREFIX}${cid}${CATEGORY_DEAL_INDEX_KEY_SUFFIX}`
|
|
}
|
|
|
|
function normalizeDealIndexPayload(payload = {}) {
|
|
const dealId = normalizePositiveInt(payload.dealId ?? payload.id)
|
|
const categoryId = normalizePositiveInt(payload.categoryId)
|
|
const createdAtTs =
|
|
normalizeEpochMs(payload.createdAtTs) ?? normalizeEpochMs(toEpochMs(payload.createdAt))
|
|
const status = String(payload.status || "").toUpperCase()
|
|
return { dealId, categoryId, createdAtTs, status }
|
|
}
|
|
|
|
function isIndexableDeal(payload = {}) {
|
|
const normalized = normalizeDealIndexPayload(payload)
|
|
return Boolean(
|
|
normalized.dealId &&
|
|
normalized.categoryId &&
|
|
normalized.createdAtTs &&
|
|
isActiveStatus(normalized.status)
|
|
)
|
|
}
|
|
|
|
function addDealToCategoryIndexInPipeline(pipeline, payload = {}) {
|
|
const normalized = normalizeDealIndexPayload(payload)
|
|
if (!normalized.dealId || !normalized.categoryId || !normalized.createdAtTs) return false
|
|
if (!isActiveStatus(normalized.status)) return false
|
|
const key = getCategoryDealIndexKey(normalized.categoryId)
|
|
if (!key) return false
|
|
pipeline.zadd(key, String(normalized.createdAtTs), String(normalized.dealId))
|
|
return true
|
|
}
|
|
|
|
function removeDealFromCategoryIndexInPipeline(pipeline, payload = {}) {
|
|
const normalized = normalizeDealIndexPayload(payload)
|
|
if (!normalized.dealId || !normalized.categoryId) return false
|
|
const key = getCategoryDealIndexKey(normalized.categoryId)
|
|
if (!key) return false
|
|
pipeline.zrem(key, String(normalized.dealId))
|
|
return true
|
|
}
|
|
|
|
async function reconcileDealCategoryIndex({ before = null, after = null } = {}) {
|
|
const prev = normalizeDealIndexPayload(before || {})
|
|
const next = normalizeDealIndexPayload(after || {})
|
|
|
|
const prevIndexable = isIndexableDeal(prev)
|
|
const nextIndexable = isIndexableDeal(next)
|
|
|
|
const redis = getRedisClient()
|
|
const pipeline = redis.pipeline()
|
|
let commandCount = 0
|
|
|
|
if (prevIndexable) {
|
|
const removedForStatus = !nextIndexable
|
|
const movedCategory = nextIndexable && prev.categoryId !== next.categoryId
|
|
if (removedForStatus || movedCategory) {
|
|
if (removeDealFromCategoryIndexInPipeline(pipeline, prev)) commandCount += 1
|
|
}
|
|
}
|
|
|
|
if (nextIndexable) {
|
|
const isNew = !prevIndexable
|
|
const movedCategory = prevIndexable && prev.categoryId !== next.categoryId
|
|
const scoreChanged =
|
|
prevIndexable &&
|
|
prev.categoryId === next.categoryId &&
|
|
Number(prev.createdAtTs) !== Number(next.createdAtTs)
|
|
if (isNew || movedCategory || scoreChanged) {
|
|
if (addDealToCategoryIndexInPipeline(pipeline, next)) commandCount += 1
|
|
}
|
|
}
|
|
|
|
if (!commandCount) return 0
|
|
try {
|
|
await pipeline.exec()
|
|
return commandCount
|
|
} catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
async function getRecentDealIdsByCategory({
|
|
categoryId,
|
|
sinceTs,
|
|
limit = 30,
|
|
} = {}) {
|
|
const cid = normalizePositiveInt(categoryId)
|
|
if (!cid) return []
|
|
const key = getCategoryDealIndexKey(cid)
|
|
if (!key) return []
|
|
const minTs = normalizeEpochMs(sinceTs) || 0
|
|
const safeLimit = Math.max(1, Math.min(Number(limit) || 30, 300))
|
|
const redis = getRedisClient()
|
|
|
|
try {
|
|
const ids = await redis.zrevrangebyscore(
|
|
key,
|
|
"+inf",
|
|
String(minTs),
|
|
"LIMIT",
|
|
0,
|
|
safeLimit
|
|
)
|
|
return Array.isArray(ids)
|
|
? ids.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0)
|
|
: []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getCategoryDealIndexKey,
|
|
addDealToCategoryIndexInPipeline,
|
|
removeDealFromCategoryIndexInPipeline,
|
|
reconcileDealCategoryIndex,
|
|
getRecentDealIdsByCategory,
|
|
}
|