import Redis from "ioredis"; const globalForRedis = globalThis as unknown as { redis: Redis | undefined; redisSub: Redis | undefined; }; function createRedisClient(): Redis { return new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: 3, retryStrategy(times) { const delay = Math.min(times * 50, 2000); return delay; }, }); } export const redis = globalForRedis.redis ?? createRedisClient(); export const redisSub = globalForRedis.redisSub ?? createRedisClient(); if (process.env.NODE_ENV !== "production") { globalForRedis.redis = redis; globalForRedis.redisSub = redisSub; } const CHANNEL_REALTIME = "channel:realtime"; const ACTIVE_CLAWS_KEY = "active:claws"; const STATS_GLOBAL_KEY = "stats:global"; const STATS_REGION_KEY = "stats:region"; const HEATMAP_CACHE_KEY = "cache:heatmap"; const HOURLY_ACTIVITY_KEY = "stats:hourly"; export async function setClawOnline( clawId: string, ip: string ): Promise { await redis.set(`claw:online:${clawId}`, ip, "EX", 300); } export async function isClawOnline(clawId: string): Promise { const result = await redis.exists(`claw:online:${clawId}`); return result === 1; } export async function updateActiveClaws(clawId: string): Promise { const now = Date.now(); await redis.zadd(ACTIVE_CLAWS_KEY, now, clawId); } export async function getActiveClawIds( limit: number = 100 ): Promise { return redis.zrevrange(ACTIVE_CLAWS_KEY, 0, limit - 1); } export async function incrementRegionCount(region: string): Promise { await redis.hincrby(STATS_REGION_KEY, region, 1); } export async function incrementGlobalStat( field: string, amount: number = 1 ): Promise { await redis.hincrby(STATS_GLOBAL_KEY, field, amount); } export async function getGlobalStats(): Promise> { return redis.hgetall(STATS_GLOBAL_KEY); } export async function getRegionStats(): Promise> { return redis.hgetall(STATS_REGION_KEY); } export async function incrementHourlyActivity(): Promise { const now = new Date(); const hourKey = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")}T${String(now.getUTCHours()).padStart(2, "0")}`; await redis.hincrby(HOURLY_ACTIVITY_KEY, hourKey, 1); await redis.expire(HOURLY_ACTIVITY_KEY, 48 * 60 * 60); } export async function getHourlyActivity(): Promise> { const allData = await redis.hgetall(HOURLY_ACTIVITY_KEY); const now = new Date(); const result: Record = {}; for (let i = 23; i >= 0; i--) { const date = new Date(now.getTime() - i * 60 * 60 * 1000); const hourKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}T${String(date.getUTCHours()).padStart(2, "0")}`; result[hourKey] = parseInt(allData[hourKey] || "0", 10); } return result; } export async function setCacheHeatmap(data: string): Promise { await redis.set(HEATMAP_CACHE_KEY, data, "EX", 30); } export async function getCacheHeatmap(): Promise { return redis.get(HEATMAP_CACHE_KEY); } export async function publishEvent(event: object): Promise { await redis.publish(CHANNEL_REALTIME, JSON.stringify(event)); }