core_types.ts

import { InjectionToken, Signal } from "@angular/core";
import { NgxCachrService } from "../services/ngx-cachr.service";

/**
 * The caching strategy to use.
 */
export type CacheStrategy = 'cache-first' | 'network-first' | 'swr';

/**
 * The configuration for the cache.
 */
export interface CacheConfig {
  /**
   * A unique namespace for keys. 
   * Needed when sharing LocalStorage with other apps on the same domain.
   */
  prefix: string;

  /**
   * A global version number. 
   * If this changes (e.g., from 1 to 2), the cache will automatically 
   * flush all persistent state to prevent schema mismatching.
   */
  version: number;

  /**
   * Global default TTL (in milliseconds).
   * Can be overridden per resource.
   */
  defaultTtl: number;

  /**
   * The default caching strategy to use if none is specified in cachedResource.
   */
  defaultStrategy: CacheStrategy;

  /**
   * If true, logs cache hits, misses, and invalidations to the console.
   */
  debug?: boolean;

  /**
   * Configuration specific to the memory cache.
   */
  memory?: {
    /**
     * The maximum number of items to keep in memory.
     * Implements an LRU (Least Recently Used) eviction policy 
     * to prevent memory leaks in large Single Page Apps.
     */
    maxEntries?: number;
  };

  /**
   * Configuration specific to persistent storage (LocalStorage/IndexedDB).
   */
  storage?: {
    /**
     * If true, sensitive data marked as 'secure' won't be written to 
     * persistent storage, only memory.
     */
    excludeSecure?: boolean;
  };
}

/**
 * A provider for the cache config.
 * 
 * @example
 * {
 *   prefix: 'my-app-v1:',
 *   version: 1,
 *   defaultTtl: 300000,
 *   defaultStrategy: 'swr',
 *   debug: false,
 * }
 * 
 * @param {Partial<CacheConfig>} config - The cache config to provide.
 */
export type CacheConfigProvider = Partial<CacheConfig>;

/**
 * Cache key used to resolve or store data from the cache.
 * 
 * @example
 * 'user:123'
 * 'post:456'
 * ['user:123', 'post:456']
 * ['user:123', 'post:456', 'comment:789']
 * 
 * @param {string | number | Array<string | number>} key - The cache key.
 */
export type CacheKey = string | number | Array<string | number>;

/**
 * Metadata for a cached entry.
 * 
 * @example
 * {
 *   createdAt: 1716796800000,
 *   ttl: 300000,
 *   tags: ['user', 'post'],
 *   version: 1,
 * }
 */
export interface CacheMetadata {
  createdAt: number;
  ttl: number;
  tags: string[];
  version: number;
}

/**
 * A cached entry state.
 * 
 * @example
 * {
 *   data: { name: 'John Doe' },
 *   metadata: { createdAt: 1716796800000, ttl: 300000, tags: ['user'], version: 1 },
 * }
 * 
 * @param {T} data - The data to cache.
 * @param {CacheMetadata} metadata - The metadata for the cached entry.
 */
export interface CacheEntry<T> {
  /**
   * The data to cache.
   */
  data: T;

  /**
   * The metadata for the cached entry.
   */
  metadata: CacheMetadata;
}

/**
 * A cache driver.
 * 
 * @example
 * {
 *   get: async (key: string) => Promise<CacheEntry<T> | null>,
 *   set: async (key: string, entry: CacheEntry<T>) => Promise<void>,
 *   delete: async (key: string) => Promise<void>,
 *   clear: async () => Promise<void>,
 * }
 * 
 * @param {string} key - The cache key.
 * @param {CacheEntry<T>} entry - The cached entry.
 */
export interface CacheDriver {
  /**
   * Get a cached entry by key.
   * 
   * @param {string} key - The cache key.
   * @returns {Promise<CacheEntry<T> | null>} The cached entry or null if not found.
   */
  get<T>(key: string): Promise<CacheEntry<T> | null>;

  /**
   * Set a cached entry by key.
   * 
   * @param {string} key - The cache key.
   * @param {CacheEntry<T>} entry - The cached entry.
   */
  set<T>(key: string, entry: CacheEntry<T>): Promise<void>;

  /**
   * Delete a cached entry by key.
   * 
   * @param {string} key - The cache key.
   */
  delete(key: string): Promise<void>;

  /**
   * Clear all cached entries.
   */
  clear(): Promise<void>;
}

/**
 * Options for a cached resource.
 * 
 * @example
 * {
 *   key: 'user:123',
 *   loader: async () => Promise<User>,
 *   ttl: 300000,
 *   strategy: 'swr',
 *   tags: ['user'],
 *   storage: 'memory',
 * }
 */
export interface CachedResourceOptions<T> {
  /**
   * The cache key.
   */
  key: CacheKey;

  /**
   * The loader function to fetch the data.
   */
  loader: () => Promise<T>;

  /**
   * The time-to-live for the cached entry.
   */
  ttl?: number; // Default 5 mins

  /**
   * The caching strategy to use.
   */
  strategy?: 'cache-first' | 'network-first' | 'swr';

  /**
   * The tags for the cached entry.
   */
  tags?: string[];

  /**
   * The storage to use for the cached entry.
   */
  storage?: 'memory' | 'local' | 'session';
}

/**
 * A cached resource.
 * 
 * @example
 * {
 *   data: { name: 'John Doe' },
 *   status: 'idle',
 *   error: null,
 *   mutate: (newData: User) => void,
 *   invalidate: () => void,
 * }
 */
export interface CachedResource<T> {
  /**
   * The data for the cached resource.
   */
  data: Signal<T | undefined>;

  /**
   * The status of the cached resource.
   */
  status: Signal<'idle' | 'loading' | 'revalidating' | 'error' | 'success'>;

  /**
   * The error for the cached resource.
   */
  error: Signal<unknown>;

  /**
   * The function to mutate the cached resource.
   */
  mutate: (newData: T) => void;

  /**
   * The function to invalidate the cached resource.
   */
  invalidate: () => void;
}

/**
 * A snapshot of the cache.
 */
export interface CacheSnapshot {
  keys: string[];
  memory: Record<string, CacheEntry<any>>;
  pending: string[];
}

/**
 * The injection token for the cache config.
 */
export const CACHE_CONFIG = new InjectionToken<CacheConfig>('CACHE_CONFIG');