import Redis from "ioredis";
// ============================================
// Configuration
// ============================================
const redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB || "0"),
retryStrategy: (times) => Math.min(times * 50, 2000),
});
redis.on("connect", () => console.log("Redis connected"));
redis.on("error", (err) => console.error("Redis error:", err));
// ============================================
// Basic Operations
// ============================================
export async function set(
key: string,
value: unknown,
ttlSeconds?: number,
): Promise<void> {
const serialized = JSON.stringify(value);
if (ttlSeconds) {
await redis.setex(key, ttlSeconds, serialized);
} else {
await redis.set(key, serialized);
}
}
export async function get<T>(key: string): Promise<T | null> {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
}
export async function del(key: string): Promise<boolean> {
const result = await redis.del(key);
return result > 0;
}
export async function exists(key: string): Promise<boolean> {
const result = await redis.exists(key);
return result === 1;
}
// ============================================
// Cache Helper
// ============================================
export async function cached<T>(
key: string,
ttlSeconds: number,
fetchFn: () => Promise<T>,
): Promise<T> {
const cached = await get<T>(key);
if (cached !== null) {
return cached;
}
const fresh = await fetchFn();
await set(key, fresh, ttlSeconds);
return fresh;
}
// ============================================
// Rate Limiting
// ============================================
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetIn: number;
}
export async function rateLimit(
key: string,
maxRequests: number,
windowSeconds: number,
): Promise<RateLimitResult> {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSeconds);
}
const ttl = await redis.ttl(key);
return {
allowed: current <= maxRequests,
remaining: Math.max(0, maxRequests - current),
resetIn: ttl > 0 ? ttl : windowSeconds,
};
}
// ============================================
// Session Store
// ============================================
export interface Session {
userId: string;
data: Record<string, unknown>;
createdAt: number;
}
const SESSION_PREFIX = "session:";
const SESSION_TTL = 86400; // 24 hours
export const sessionStore = {
async create(
sessionId: string,
userId: string,
data: Record<string, unknown> = {},
): Promise<void> {
const session: Session = {
userId,
data,
createdAt: Date.now(),
};
await set(`${SESSION_PREFIX}${sessionId}`, session, SESSION_TTL);
},
async get(sessionId: string): Promise<Session | null> {
return get<Session>(`${SESSION_PREFIX}${sessionId}`);
},
async update(
sessionId: string,
data: Record<string, unknown>,
): Promise<void> {
const session = await this.get(sessionId);
if (session) {
session.data = { ...session.data, ...data };
await set(`${SESSION_PREFIX}${sessionId}`, session, SESSION_TTL);
}
},
async destroy(sessionId: string): Promise<boolean> {
return del(`${SESSION_PREFIX}${sessionId}`);
},
async refresh(sessionId: string): Promise<void> {
await redis.expire(`${SESSION_PREFIX}${sessionId}`, SESSION_TTL);
},
};
// ============================================
// Pub/Sub
// ============================================
const subscriber = redis.duplicate();
type MessageHandler = (message: string, channel: string) => void;
const handlers = new Map<string, MessageHandler[]>();
subscriber.on("message", (channel, message) => {
const channelHandlers = handlers.get(channel) || [];
channelHandlers.forEach((handler) => handler(message, channel));
});
export async function subscribe(
channel: string,
handler: MessageHandler,
): Promise<void> {
const existing = handlers.get(channel) || [];
handlers.set(channel, [...existing, handler]);
if (existing.length === 0) {
await subscriber.subscribe(channel);
}
}
export async function publish(
channel: string,
message: unknown,
): Promise<number> {
const serialized =
typeof message === "string" ? message : JSON.stringify(message);
return redis.publish(channel, serialized);
}
// ============================================
// Queue (Simple Job Queue)
// ============================================
export interface Job<T = unknown> {
id: string;
type: string;
data: T;
createdAt: number;
attempts: number;
}
const QUEUE_PREFIX = "queue:";
export const queue = {
async push<T>(name: string, type: string, data: T): Promise<string> {
const job: Job<T> = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type,
data,
createdAt: Date.now(),
attempts: 0,
};
await redis.lpush(`${QUEUE_PREFIX}${name}`, JSON.stringify(job));
return job.id;
},
async pop<T>(name: string, timeout = 0): Promise<Job<T> | null> {
const result = await redis.brpop(`${QUEUE_PREFIX}${name}`, timeout);
if (!result) return null;
const job = JSON.parse(result[1]) as Job<T>;
job.attempts++;
return job;
},
async size(name: string): Promise<number> {
return redis.llen(`${QUEUE_PREFIX}${name}`);
},
async clear(name: string): Promise<void> {
await redis.del(`${QUEUE_PREFIX}${name}`);
},
};
// ============================================
// Leaderboard
// ============================================
export const leaderboard = {
async addScore(name: string, memberId: string, score: number): Promise<void> {
await redis.zadd(name, score, memberId);
},
async incrementScore(
name: string,
memberId: string,
increment: number,
): Promise<number> {
return redis.zincrby(name, increment, memberId);
},
async getTop(
name: string,
count = 10,
): Promise<Array<{ memberId: string; score: number }>> {
const results = await redis.zrevrange(name, 0, count - 1, "WITHSCORES");
const entries: Array<{ memberId: string; score: number }> = [];
for (let i = 0; i < results.length; i += 2) {
entries.push({
memberId: results[i],
score: parseFloat(results[i + 1]),
});
}
return entries;
},
async getRank(name: string, memberId: string): Promise<number | null> {
const rank = await redis.zrevrank(name, memberId);
return rank !== null ? rank + 1 : null;
},
async getScore(name: string, memberId: string): Promise<number | null> {
const score = await redis.zscore(name, memberId);
return score !== null ? parseFloat(score) : null;
},
};
// ============================================
// Exports
// ============================================
export { redis };
export default redis;