clean-jsdoc-theme TypeDoc APIv0.0.0
docs-site/typedoc-src/services/cache/Cache.tstypescript
/**
 * Cache Service
 *
 * @remarks
 * Provides in-memory caching with TTL support, eviction policies,
 * and cache statistics.
 *
 * @packageDocumentation
 */

/**
 * Cache entry metadata
 *
 * @typeParam T - Cached value type
 * @public
 */
export interface CacheEntry<T> {
  /** Cached value */
  value: T
  /** Creation timestamp */
  createdAt: number
  /** Last access timestamp */
  lastAccessedAt: number
  /** Expiration timestamp (undefined = no expiry) */
  expiresAt?: number
  /** Number of times accessed */
  hits: number
  /** Size in bytes (approximate) */
  size: number
}

/**
 * Cache configuration options
 *
 * @public
 */
export interface CacheOptions {
  /**
   * Maximum number of entries
   * @defaultValue 1000
   */
  maxSize?: number

  /**
   * Default TTL in milliseconds
   * @defaultValue undefined (no expiry)
   */
  defaultTtl?: number

  /**
   * Eviction policy when cache is full
   * @defaultValue 'lru'
   */
  evictionPolicy?: 'lru' | 'lfu' | 'fifo'

  /**
   * Whether to clone values on get/set
   * @defaultValue false
   */
  cloneValues?: boolean

  /**
   * Interval for checking expired entries (ms)
   * @defaultValue 60000
   */
  cleanupInterval?: number
}

/**
 * Cache statistics
 *
 * @public
 */
export interface CacheStats {
  /** Total number of entries */
  entries: number
  /** Total cache hits */
  hits: number
  /** Total cache misses */
  misses: number
  /** Hit rate (0-1) */
  hitRate: number
  /** Total evictions */
  evictions: number
  /** Approximate memory usage in bytes */
  memoryUsage: number
  /** Cache uptime in milliseconds */
  uptime: number
}

/**
 * Generic cache implementation
 *
 * @remarks
 * A feature-rich caching solution with:
 * - Configurable TTL (Time To Live)
 * - Multiple eviction policies (LRU, LFU, FIFO)
 * - Automatic cleanup of expired entries
 * - Cache statistics and monitoring
 * - Namespace support for logical grouping
 *
 * @typeParam K - Key type
 * @typeParam V - Value type
 *
 * @example
 * Basic usage:
 * ```typescript
 * const cache = new Cache<string, User>({
 *   maxSize: 100,
 *   defaultTtl: 5 * 60 * 1000 // 5 minutes
 * })
 *
 * // Set a value
 * cache.set('user:123', { id: '123', name: 'John' })
 *
 * // Get a value
 * const user = cache.get('user:123')
 *
 * // Check if exists
 * if (cache.has('user:123')) {
 *   // ...
 * }
 * ```
 *
 * @example
 * With custom TTL:
 * ```typescript
 * // Cache for 1 hour
 * cache.set('session:abc', sessionData, { ttl: 60 * 60 * 1000 })
 *
 * // No expiry
 * cache.set('config', configData, { ttl: 0 })
 * ```
 *
 * @example
 * Using getOrSet pattern:
 * ```typescript
 * const user = await cache.getOrSet('user:123', async () => {
 *   return await fetchUserFromDatabase('123')
 * })
 * ```
 *
 * @public
 */
export class Cache<K = string, V = unknown> {
  /**
   * Internal cache storage
   * @internal
   */
  private storage: Map<K, CacheEntry<V>> = new Map()

  /**
   * Cache configuration
   */
  private readonly options: Required<CacheOptions>

  /**
   * Statistics counters
   */
  private stats = {
    hits: 0,
    misses: 0,
    evictions: 0,
  }

  /**
   * Cache creation timestamp
   */
  private readonly createdAt: number

  /**
   * Cleanup timer reference
   */
  private cleanupTimer?: ReturnType<typeof setInterval>

  /**
   * Creates a new Cache instance
   *
   * @param options - Cache configuration options
   *
   * @example
   * ```typescript
   * const cache = new Cache({
   *   maxSize: 500,
   *   defaultTtl: 300000,
   *   evictionPolicy: 'lru'
   * })
   * ```
   */
  constructor(options: CacheOptions = {}) {
    this.options = {
      maxSize: options.maxSize ?? 1000,
      defaultTtl: options.defaultTtl ?? 0,
      evictionPolicy: options.evictionPolicy ?? 'lru',
      cloneValues: options.cloneValues ?? false,
      cleanupInterval: options.cleanupInterval ?? 60000,
    }
    this.createdAt = Date.now()

    // Start cleanup timer if TTL is used
    if (this.options.defaultTtl > 0 || this.options.cleanupInterval > 0) {
      this.startCleanupTimer()
    }
  }

  /**
   * Gets the current size of the cache
   */
  public get size(): number {
    return this.storage.size
  }

  /**
   * Gets a value from the cache
   *
   * @param key - Cache key
   * @returns Cached value or undefined if not found/expired
   *
   * @example
   * ```typescript
   * const value = cache.get('myKey')
   * if (value !== undefined) {
   *   console.log('Found:', value)
   * }
   * ```
   */
  public get(key: K): V | undefined {
    const entry = this.storage.get(key)

    if (!entry) {
      this.stats.misses++
      return undefined
    }

    // Check expiration
    if (entry.expiresAt && Date.now() > entry.expiresAt) {
      this.storage.delete(key)
      this.stats.misses++
      return undefined
    }

    // Update access metadata
    entry.lastAccessedAt = Date.now()
    entry.hits++
    this.stats.hits++

    return this.options.cloneValues ? this.clone(entry.value) : entry.value
  }

  /**
   * Sets a value in the cache
   *
   * @param key - Cache key
   * @param value - Value to cache
   * @param options - Optional settings for this entry
   * @returns This cache instance for chaining
   *
   * @example
   * ```typescript
   * cache
   *   .set('key1', 'value1')
   *   .set('key2', 'value2', { ttl: 60000 })
   * ```
   */
  public set(key: K, value: V, options?: { ttl?: number }): this {
    // Evict if at capacity
    if (this.storage.size >= this.options.maxSize && !this.storage.has(key)) {
      this.evict()
    }

    const now = Date.now()
    const ttl = options?.ttl ?? this.options.defaultTtl

    const entry: CacheEntry<V> = {
      value: this.options.cloneValues ? this.clone(value) : value,
      createdAt: now,
      lastAccessedAt: now,
      expiresAt: ttl > 0 ? now + ttl : undefined,
      hits: 0,
      size: this.estimateSize(value),
    }

    this.storage.set(key, entry)
    return this
  }

  /**
   * Gets a value, setting it if not present
   *
   * @typeParam T - Value type (extends V)
   * @param key - Cache key
   * @param factory - Function to create value if not cached
   * @param options - Optional cache settings
   * @returns The cached or newly created value
   *
   * @example
   * ```typescript
   * const user = await cache.getOrSet(
   *   'user:123',
   *   async () => await db.users.find('123'),
   *   { ttl: 300000 }
   * )
   * ```
   */
  public async getOrSet<T extends V>(
    key: K,
    factory: () => T | Promise<T>,
    options?: { ttl?: number }
  ): Promise<T> {
    const existing = this.get(key)
    if (existing !== undefined) {
      return existing as T
    }

    const value = await factory()
    this.set(key, value, options)
    return value
  }

  /**
   * Checks if a key exists and is not expired
   *
   * @param key - Cache key
   * @returns True if key exists and is valid
   */
  public has(key: K): boolean {
    const entry = this.storage.get(key)
    if (!entry) return false

    if (entry.expiresAt && Date.now() > entry.expiresAt) {
      this.storage.delete(key)
      return false
    }

    return true
  }

  /**
   * Deletes a value from the cache
   *
   * @param key - Cache key
   * @returns True if the key was deleted
   */
  public delete(key: K): boolean {
    return this.storage.delete(key)
  }

  /**
   * Clears all entries from the cache
   *
   * @returns This cache instance for chaining
   */
  public clear(): this {
    this.storage.clear()
    return this
  }

  /**
   * Gets all keys in the cache
   *
   * @returns Array of keys
   */
  public keys(): K[] {
    return Array.from(this.storage.keys())
  }

  /**
   * Gets all values in the cache
   *
   * @returns Array of values
   */
  public values(): V[] {
    return Array.from(this.storage.values())
      .filter((entry) => !entry.expiresAt || Date.now() <= entry.expiresAt)
      .map((entry) => entry.value)
  }

  /**
   * Gets all entries as key-value pairs
   *
   * @returns Array of [key, value] tuples
   */
  public entries(): [K, V][] {
    const result: [K, V][] = []
    for (const [key, entry] of this.storage) {
      if (!entry.expiresAt || Date.now() <= entry.expiresAt) {
        result.push([key, entry.value])
      }
    }
    return result
  }

  /**
   * Gets cache statistics
   *
   * @returns Current cache statistics
   *
   * @example
   * ```typescript
   * const stats = cache.getStats()
   * console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(2)}%`)
   * console.log(`Memory: ${stats.memoryUsage} bytes`)
   * ```
   */
  public getStats(): CacheStats {
    const totalRequests = this.stats.hits + this.stats.misses
    let memoryUsage = 0

    for (const entry of this.storage.values()) {
      memoryUsage += entry.size
    }

    return {
      entries: this.storage.size,
      hits: this.stats.hits,
      misses: this.stats.misses,
      hitRate: totalRequests > 0 ? this.stats.hits / totalRequests : 0,
      evictions: this.stats.evictions,
      memoryUsage,
      uptime: Date.now() - this.createdAt,
    }
  }

  /**
   * Updates the TTL for an existing entry
   *
   * @param key - Cache key
   * @param ttl - New TTL in milliseconds
   * @returns True if entry was updated
   */
  public touch(key: K, ttl?: number): boolean {
    const entry = this.storage.get(key)
    if (!entry) return false

    const newTtl = ttl ?? this.options.defaultTtl
    entry.expiresAt = newTtl > 0 ? Date.now() + newTtl : undefined
    return true
  }

  /**
   * Iterates over cache entries
   *
   * @param callback - Function called for each entry
   *
   * @example
   * ```typescript
   * cache.forEach((value, key, entry) => {
   *   console.log(`${key}: ${value} (hits: ${entry.hits})`)
   * })
   * ```
   */
  public forEach(callback: (value: V, key: K, entry: CacheEntry<V>) => void): void {
    for (const [key, entry] of this.storage) {
      if (!entry.expiresAt || Date.now() <= entry.expiresAt) {
        callback(entry.value, key, entry)
      }
    }
  }

  /**
   * Destroys the cache and cleans up resources
   */
  public destroy(): void {
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer)
    }
    this.storage.clear()
  }

  /**
   * Evicts entries based on the configured policy
   *
   * @internal
   */
  private evict(): void {
    if (this.storage.size === 0) return

    let keyToEvict: K | undefined
    let targetEntry: CacheEntry<V> | undefined

    switch (this.options.evictionPolicy) {
      case 'lru': // Least Recently Used
        for (const [key, entry] of this.storage) {
          if (!targetEntry || entry.lastAccessedAt < targetEntry.lastAccessedAt) {
            keyToEvict = key
            targetEntry = entry
          }
        }
        break

      case 'lfu': // Least Frequently Used
        for (const [key, entry] of this.storage) {
          if (!targetEntry || entry.hits < targetEntry.hits) {
            keyToEvict = key
            targetEntry = entry
          }
        }
        break

      case 'fifo': // First In First Out
        for (const [key, entry] of this.storage) {
          if (!targetEntry || entry.createdAt < targetEntry.createdAt) {
            keyToEvict = key
            targetEntry = entry
          }
        }
        break
    }

    if (keyToEvict !== undefined) {
      this.storage.delete(keyToEvict)
      this.stats.evictions++
    }
  }

  /**
   * Starts the cleanup timer
   *
   * @internal
   */
  private startCleanupTimer(): void {
    this.cleanupTimer = setInterval(() => {
      this.cleanup()
    }, this.options.cleanupInterval)
  }

  /**
   * Removes expired entries
   *
   * @internal
   */
  private cleanup(): void {
    const now = Date.now()
    for (const [key, entry] of this.storage) {
      if (entry.expiresAt && now > entry.expiresAt) {
        this.storage.delete(key)
      }
    }
  }

  /**
   * Estimates the size of a value in bytes
   *
   * @internal
   */
  private estimateSize(value: V): number {
    try {
      return JSON.stringify(value).length * 2 // Rough estimate
    } catch {
      return 0
    }
  }

  /**
   * Clones a value
   *
   * @internal
   */
  private clone(value: V): V {
    return JSON.parse(JSON.stringify(value))
  }
}

/**
 * Creates a namespace-prefixed cache wrapper
 *
 * @typeParam V - Value type
 * @param cache - Base cache instance
 * @param namespace - Namespace prefix
 * @returns Namespaced cache wrapper
 *
 * @example
 * ```typescript
 * const baseCache = new Cache<string, unknown>()
 * const userCache = createNamespacedCache(baseCache, 'users')
 *
 * userCache.set('123', userData) // Actually stores as 'users:123'
 * userCache.get('123') // Retrieves from 'users:123'
 * ```
 *
 * @public
 */
export function createNamespacedCache<V>(
  cache: Cache<string, V>,
  namespace: string
): {
  get: (key: string) => V | undefined
  set: (key: string, value: V, options?: { ttl?: number }) => void
  delete: (key: string) => boolean
  has: (key: string) => boolean
} {
  const prefix = `${namespace}:`

  return {
    get: (key: string) => cache.get(`${prefix}${key}`),
    set: (key: string, value: V, options?: { ttl?: number }) => {
      cache.set(`${prefix}${key}`, value, options)
    },
    delete: (key: string) => cache.delete(`${prefix}${key}`),
    has: (key: string) => cache.has(`${prefix}${key}`),
  }
}