import Redis from "ioredis"; import { config } from "./config.ts"; import type { EnrichedProduct, KeepaData, SpApiData } from "./types.ts"; let redis: Redis | null = null; let disabled = false; export type ApiCacheEntry = { title: string; keepa: KeepaData | null; spApi: SpApiData; fetchedAt: string; }; function getApiCacheKey(asin: string): string { return `api:asin:${asin}`; } export async function connectCache(): Promise { if (disabled) return; try { redis = new Redis(config.redisUrl, { maxRetriesPerRequest: 1, connectTimeout: 3000, lazyConnect: true, retryStrategy: () => null, reconnectOnError: () => false, }); // Swallow connection-level errors after we intentionally disable cache. redis.on("error", () => { // no-op }); await redis.connect(); console.log("Redis connected"); } catch (err) { console.warn(`Redis unavailable, running without cache: ${err}`); if (redis) { redis.disconnect(); } redis = null; disabled = true; } } export async function getCache(asin: string): Promise { if (!redis) return null; try { const data = await redis.get(`asin:${asin}`); return data ? JSON.parse(data) : null; } catch { return null; } } export async function setCache( asin: string, data: EnrichedProduct, ): Promise { if (!redis) return; try { await redis.set( `asin:${asin}`, JSON.stringify(data), "EX", config.cacheTtl, ); } catch { // Non-critical, continue without caching } } export async function getApiCache(asin: string): Promise { if (!redis) return null; try { const raw = await redis.get(getApiCacheKey(asin)); if (!raw) return null; return JSON.parse(raw) as ApiCacheEntry; } catch { return null; } } export async function setApiCache( asin: string, data: ApiCacheEntry, ttlSeconds: number, ): Promise { if (!redis) return; try { await redis.set( getApiCacheKey(asin), JSON.stringify(data), "EX", ttlSeconds, ); } catch { // Non-critical, continue without caching } } export async function disconnectCache(): Promise { if (redis) { await redis.quit(); redis = null; } }