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, }