cache/ttl/TTLCache.ts

import { CacheValue } from './CacheValue';
import { Serializable } from './types';

/**
 * ttl 캐시
 * @export
 * @class TTLCache
 * @example
 *  const ttlCache = new TTLCache();
 * ttlCache.set("A", "myValue1", 0);
 * ttlCache.set("B", "myValue2", 100);
 * ttlCache.set("C", "myValue3", 200);
 * ttlCache.expired("unknown-k1"); // true;
 * ttlCache.expired("unknown-k2"); // true;
 * ttlCache.expired("A"); // true;
 * ttlCache.expired("B"); // false;
 * ttlCache.expired("C")); // false;
 * ttlCache.get("unknown-k1"); // undefined
 * ttlCache.get("unknown-k2"); // undefined
 * ttlCache.get("A"); // undefined
 * ttlCache.get("B"); // "myValue2"
 * ttlCache.get("C"); // "myValue3"
 * ttlCache.toJson(); // { B: "myValue2", C: "myValue3" }
 * // await delay(101);
 * ttlCache.expired("unknown-k1"); // true
 * ttlCache.expired("unknown-k2"); // true
 * ttlCache.expired("A"); // true
 * ttlCache.expired("B"); // true
 * ttlCache.expired("C"); // false
 * ttlCache.get("unknown-k1"); // undefined
 * ttlCache.get("unknown-k2"); // undefined
 * ttlCache.get("A"); // undefined
 * ttlCache.get("B"); // undefined
 * ttlCache.get("C"); // "myValue3"
 * ttlCache.toJson(); // { C: "myValue3" }
 */
export class TTLCache {
  protected cacheMap: Map<string, CacheValue> = new Map();
  protected now(): number {
    return Date.now();
  }

  /**
   * 보유 맵
   * @readonly
   * @type {Map<string, CacheValue>}
   */
  get map(): Map<string, CacheValue> {
    return this.cacheMap;
  }

  /**
   * 맵의 value
   * @protected
   * @param {string} key
   * @returns {CacheValue}
   */
  protected getCache(key: string): CacheValue {
    return this.cacheMap.get(key);
  }

  /**
   * key 가 존재하는 경우 값이 expire 되었는지 여부 확인.
   * key 가 존재하지 않는 경우 true.
   * @param {string} key
   */
  expired(key: string): boolean {
    if (this.cacheMap.has(key)) {
      const now = this.now();
      const cacheValue = this.getCache(key);
      const expireAt = cacheValue.expireAt;
      if (expireAt < now) {
        this.remove(key);
        return true;
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * CacheValue 에 등록된 expire 시간(정도) 후 CacheValue 로 부터 호출을 기대하는 콜백
   * @param {string} key
   */
  expireNotify(key: string) {
    if (this.expired(key)) {
      this.remove(key);
    }
  }

  /**
   * key 에 해당하는 값 반환
   * @template T
   * @param {string} key
   */
  get<T>(key: string): T {
    if (this.has(key)) {
      if (this.expired(key)) {
        return;
      } else {
        return this.getCache(key).value;
      }
    }
    return;
  }

  /**
   * key 에 value 를 expire(millisecond) 만큼 저장
   * @template T
   * @param {string} key
   * @param {T} value
   * @param {number} expire
   */
  set<T>(key: string, value: T, expire: number): void {
    if (!expire) return;
    const now = this.now();
    const cacheValue = new CacheValue<T>();
    const existCache = this.getCache(key);
    if (existCache) {
      existCache.destroy();
    }
    cacheValue.setKey(key);
    cacheValue.setValue(value);
    cacheValue.setExpireAt(now + +expire);
    cacheValue.setExpireNotify(expire, this.expireNotify.bind(this));
    this.cacheMap.set(key, cacheValue);
  }

  /**
   * key 가 존재하는지 여부
   * @param {string} key
   */
  has(key: string): boolean {
    return !this.expired(key) ? this.cacheMap.has(key) : false;
  }

  /**
   * key 삭제
   * @param {string} key
   */
  remove(key: string): void {
    const cacheValue = this.getCache(key);
    if (cacheValue) {
      cacheValue.destroy();
    }
    this.cacheMap.delete(key);
  }

  /**
   * key 배열 반환
   * @returns {string[]}
   */
  getKeys(): string[] {
    return Array.from(this.cacheMap.keys());
  }

  /**
   * expire 된것 삭제
   */
  flushExpired() {
    this.getKeys().forEach((key) => {
      if (this.expired(key)) {
        this.remove(key);
      }
    });
  }

  /**
   * 비우기
   */
  flushAll() {
    this.getKeys().forEach((key) => {
      this.remove(key);
    });
  }

  /**
   * 보유하고 있는 key&value 를 모두 반환
   */
  toJson() {
    const keys = Array.from(this.cacheMap.keys());
    const hash: { [key: string]: Serializable } = {};
    keys.map((key) => {
      if (!this.expired(key)) {
        hash[key.toString()] = this.get(key);
      }
    });
    return hash;
  }

  /**
   * 파기
   */
  destroy() {
    this.flushAll();
  }
}