All files / src/lib/integrations redis.ts

94.97% Statements 189/199
83.6% Branches 51/61
100% Functions 14/14
94.97% Lines 189/199

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 2001x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 107x 107x 57x 57x 50x 50x 50x 50x 107x 8x 8x 8x 8x 42x 42x 42x 42x 42x 42x 42x 42x 42x 55x     42x 55x         42x 42x 42x 42x 42x 42x 42x 16x 16x 16x 1x 1x 1x 42x 42x 13x 13x 3x 13x 10x 9x 13x 1x 1x 42x 42x 6x 6x 5x 4x 6x 1x 1x 42x 42x 7x 7x 7x 1x 1x 1x 42x 42x 4x 4x 4x 1x 1x 1x 42x 42x 5x 5x 5x 1x 1x 1x 42x 42x 3x 3x 3x 1x 1x 42x 42x 42x 42x 42x 55x         107x 1x 1x 1x 1x 1x 13x 13x 1x 1x 1x 1x 1x 48x 48x 48x 1x 1x 1x 1x 1x 17x 17x 16x 16x 1x 1x 1x 1x 1x 14x 14x 14x 13x 1x 1x 1x 1x 1x 7x 7x 6x 6x 1x 1x 1x 1x 1x 8x 8x 7x 7x  
/**
 * Redis Configuration
 *
 * Uses Upstash Redis for distributed caching.
 * Falls back to in-memory cache when Redis is not configured.
 */
 
import { logger } from "@/lib/logging";
 
// Cache TTL configuration (in seconds)
export const CACHE_TTL = {
  categories: 3600,      // 1 hour
  products: 300,         // 5 minutes
  productDetail: 600,    // 10 minutes
  filters: 3600,         // 1 hour
  search: 180,           // 3 minutes
};
 
// Redis client singleton
let redisClient: RedisClient | null = null;
let redisAvailable: boolean | null = null;
 
// Interface for Redis client operations
interface RedisClient {
  get: <T>(key: string) => Promise<T | null>;
  set: (key: string, value: unknown, options?: { ex?: number }) => Promise<void>;
  del: (...keys: string[]) => Promise<void>;
  keys: (pattern: string) => Promise<string[]>;
  exists: (...keys: string[]) => Promise<number>;
  incr: (key: string) => Promise<number>;
  expire: (key: string, seconds: number) => Promise<void>;
}
 
// Interface for the dynamically imported Redis module
interface UpstashRedisModule {
  Redis: new (config: { url: string; token: string }) => RedisClient;
}
 
/**
 * Initialize Redis client
 * Returns null if Redis is not configured
 */
async function initRedis(): Promise<RedisClient | null> {
  if (redisAvailable !== null) {
    return redisAvailable ? redisClient : null;
  }
 
  const url = process.env.UPSTASH_REDIS_REST_URL;
  const token = process.env.UPSTASH_REDIS_REST_TOKEN;
 
  if (!url || !token) {
    logger.info("Redis not configured, using in-memory cache fallback", { category: "REDIS" });
    redisAvailable = false;
    return null;
  }
 
  try {
    // Dynamic import to avoid errors if package isn't installed
    let redisModule: UpstashRedisModule | null = null;
 
    try {
      // Hide dynamic import from Turbopack static analysis
      const dynamicImport = eval("import('@upstash/redis')");
      redisModule = await dynamicImport;
    } catch {
      // fallback to in-memory cache
    }
 
    if (!redisModule) {
      logger.info("@upstash/redis not installed, using in-memory cache", { category: "REDIS" });
      redisAvailable = false;
      return null;
    }
 
    const { Redis } = redisModule;
    const client = new Redis({ url, token });
 
    // Wrap client with our interface
    redisClient = {
      get: async <T>(key: string): Promise<T | null> => {
        try {
          return await client.get(key) as T | null;
        } catch (error) {
          logger.error(`Error getting key ${key}`, error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
          return null;
        }
      },
      set: async (key: string, value: unknown, options?: { ex?: number }): Promise<void> => {
        try {
          if (options?.ex) {
            await client.set(key, value, { ex: options.ex });
          } else {
            await client.set(key, value);
          }
        } catch (error) {
          logger.error(`Error setting key ${key}`, error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
        }
      },
      del: async (...keys: string[]): Promise<void> => {
        try {
          if (keys.length > 0) {
            await client.del(...keys);
          }
        } catch (error) {
          logger.error("Error deleting keys", error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
        }
      },
      keys: async (pattern: string): Promise<string[]> => {
        try {
          return await client.keys(pattern) as string[];
        } catch (error) {
          logger.error(`Error getting keys for pattern ${pattern}`, error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
          return [];
        }
      },
      exists: async (...keys: string[]): Promise<number> => {
        try {
          return await client.exists(...keys) as number;
        } catch (error) {
          logger.error("Error checking exists", error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
          return 0;
        }
      },
      incr: async (key: string): Promise<number> => {
        try {
          return await client.incr(key) as number;
        } catch (error) {
          logger.error(`Error incrementing ${key}`, error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
          return 0;
        }
      },
      expire: async (key: string, seconds: number): Promise<void> => {
        try {
          await client.expire(key, seconds);
        } catch (error) {
          logger.error(`Error setting expiry for ${key}`, error instanceof Error ? error : new Error(String(error)), { category: "REDIS" });
        }
      }};
 
    logger.info("Redis client initialized successfully", { category: "REDIS" });
    redisAvailable = true;
    return redisClient;
  } catch (error) {
    logger.warn("Failed to initialize Redis, falling back to in-memory", { category: "REDIS", error: error instanceof Error ? error : new Error(String(error)) });
    redisAvailable = false;
    return null;
  }
}
 
/**
 * Get Redis client (initializes if needed)
 */
export async function getRedisClient(): Promise<RedisClient | null> {
  return initRedis();
}
 
/**
 * Check if Redis is available
 */
export async function isRedisAvailable(): Promise<boolean> {
  const client = await initRedis();
  return client !== null;
}
 
/**
 * Get value from Redis
 */
export async function redisGet<T>(key: string): Promise<T | null> {
  const client = await initRedis();
  if (!client) return null;
  return client.get<T>(key);
}
 
/**
 * Set value in Redis with optional TTL
 */
export async function redisSet(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
  const client = await initRedis();
  if (!client) return;
  await client.set(key, value, ttlSeconds ? { ex: ttlSeconds } : undefined);
}
 
/**
 * Delete keys from Redis
 */
export async function redisDel(...keys: string[]): Promise<void> {
  const client = await initRedis();
  if (!client) return;
  await client.del(...keys);
}
 
/**
 * Get keys matching pattern
 */
export async function redisKeys(pattern: string): Promise<string[]> {
  const client = await initRedis();
  if (!client) return [];
  return client.keys(pattern);
}