import { Redis } from '@upstash/redis' import type { CacheInterface, CacheOptions } from './interfaces' import { keys } from './keys' /** * Upstash Redis cache implementation with namespace support */ export class UpstashCache implements CacheInterface { private client: Redis | null = null private defaultNamespace?: string constructor(defaultNamespace?: string) { this.defaultNamespace = defaultNamespace } private getClient(): Redis { if (!this.client) { const config = keys() if (!config.KV_REST_API_URL || !config.KV_REST_API_TOKEN) { throw new Error( 'Upstash Redis configuration is missing. Please set KV_REST_API_URL and KV_REST_API_TOKEN environment variables.' ) } this.client = new Redis({ url: config.KV_REST_API_URL, token: config.KV_REST_API_TOKEN }) } return this.client } private getNamespacedKey(key: string, namespace?: string): string { const ns = namespace || this.defaultNamespace return ns ? `${ns}:${key}` : key } async get(key: string): Promise { const client = this.getClient() const value = await client.get(this.getNamespacedKey(key)) return value } async set(key: string, value: T, options?: CacheOptions): Promise { const client = this.getClient() const namespacedKey = this.getNamespacedKey(key, options?.namespace) if (options?.ttl) { await client.set(namespacedKey, value, { ex: options.ttl }) } else { await client.set(namespacedKey, value) } } async delete(key: string): Promise { const client = this.getClient() await client.del(this.getNamespacedKey(key)) } async has(key: string): Promise { const client = this.getClient() const exists = await client.exists(this.getNamespacedKey(key)) return exists === 1 } async clear(): Promise { const client = this.getClient() if (this.defaultNamespace) { const keys = await client.keys(`${this.defaultNamespace}:*`) if (keys.length > 0) { await client.del(...keys) } } else { await client.flushall() } } async mget(keys: string[]): Promise<(T | null)[]> { const client = this.getClient() const namespacedKeys = keys.map(key => this.getNamespacedKey(key)) const values = await client.mget(...namespacedKeys) return values } async mset(entries: [string, T][], options?: CacheOptions): Promise { const client = this.getClient() const namespacedEntries = entries.map(([key, value]) => [ this.getNamespacedKey(key, options?.namespace), value ]) // Convert array of tuples to Record const record = Object.fromEntries(namespacedEntries) await client.mset(record) if (options?.ttl) { await Promise.all( namespacedEntries.map(([key]) => client.expire(key as string, options.ttl as number)) ) } } }