0.1.7Updated 17 days ago
interface MemCacheEntry<T = unknown> {
  expires_at: number
  data: T
}

export class MemCache<T = unknown> {
  private static __all: MemCache[] = []

  readonly expiration_period_seconds: number
  readonly interval_ms: number
  private interval_id!: number

  /**
   * Creates a new non-persistent cache.
   * 
   * @param expiration_period_seconds How long an entry should be kept in this cache.
   * @param clean_interval_seconds How frequently this cache should be checked for stale entries and cleaned up.
   */
  constructor(expiration_period_seconds: number = 60, clean_interval_seconds: number = 10) {
    this.expiration_period_seconds = expiration_period_seconds;
    this.interval_ms = clean_interval_seconds * 1000;

    MemCache.__all.push(this);

    if(!Deno.args.includes('build')) this.queue();
  }

  private _entries: Map<string, MemCacheEntry<T>> = new Map<string, MemCacheEntry<T>>()

  get size() {
    return this._entries.size;
  }

  get snapshot() {
    return this._entries.keys().filter((_, i) => i < 20).map(key => ({
      key,
      value: this.get(key) as T
    }))
  }

  /**
   * Get the value of a cached item
   */
  get(key: string): T | null {
    if(!this._entries.has(key)) return null;

    const entry = this._entries.get(key)!;

    entry.expires_at = Date.now();
    return entry.data as T;
  }

  /**
   * Checks whether a cached item exists
   */
  has(key: string) {
    return this._entries.has(key);
  }

  /**
   * If a value exists, return it. Otherwise, set it and return it.
   */
  async passthrough(key: string, value: PassthroughValue<T> | PassthroughFunction<T>) {
    if(this.has(key)) return this.get(key);

    let result;
    if(typeof value === 'function') {
      result = await (value as PassthroughFunction<T>)();
    } else {
      result = value as T | undefined | null;
    }

    if(result !== null && result !== undefined) {
      return this.set(key, result);
    }

    return null;
  }

  /**
   * Sets the cached item and returns it
   */
  set(key: string, value: T) {
    this._entries.set(key, { data: value, expires_at: Date.now() + this.expiration_period_seconds * 1000 });
    return value;
  }

  /**
   * Removes the cached item and returns it (if it exists)
   */
  delete(key: string) {
    if(!this._entries.has(key)) return null;
    const value = this.get(key);
    this._entries.delete(key);
    return value;
  }

  /**
   * Clears all entries from the cache
   */
  clear() {
    this._entries.clear();
  }

  /**
   * Returns an iterable list of key, value pairs
   */
  get entries() {
    return this._entries.entries.bind(this._entries);
  }

  /**
   * Returns an iterable list of keys
   */
  get keys() {
    return this._entries.keys.bind(this._entries);
  }

  /**
   * Returns an iterable list of keys
   */
  get values() {
    return this._entries.values.bind(this._entries);
  }

  private queue() {
    this.interval_id = setTimeout(this.clean.bind(this), this.interval_ms);
  }

  private clean() {
    clearTimeout(this.interval_id);

    const expiration_timestamp = Date.now();

    for(const [key, { expires_at }] of Object.entries(this._entries)) {
      if(expires_at < expiration_timestamp) this._entries.delete(key);
    }

    this.queue();
  }
}

type PassthroughValueBasic<T> = T | null | undefined
export type PassthroughValue<T> = PassthroughValueBasic<T> | Promise<PassthroughValueBasic<T>>
export type PassthroughFunction<T> = (...params: unknown[]) => PassthroughValue<T>;