All files Cache.ts

51.35% Statements 38/74
40% Branches 12/30
38.46% Functions 5/13
51.35% Lines 38/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125  1x 1x 1x     6x 6x 6x 6x 6x 6x 6x 6x                     6x         8x       8x 1x     7x 7x   8x                     8x 8x 8x     15x 15x 9x 9x 7x   2x 1x 1x     1x 1x 1x       6x                                                               1x 1x   1x 1x                 1x            
export interface CacheEntry<T> {
  t: number; // Time created.
  value: T;
}
 
const noop = () => {};
 
export class Cache<T> {
  ttl = 10000; // Time how long item is kept in cache without refreshing.
  evictionTime = 20000; // After this time item is evicted from cache.
  gcPeriod = 30000; // How often to run GC.
  maxEntries = 100000;
 
  private entries = 0; // Number of values in cache.
  public map = new Map<string, CacheEntry<T>>();
  private timer: any; // GC setTimeout Timer.

  constructor(public method: (key: string) => Promise<T> = noop as any) {}

  put(key: string, value: T) {
    const entry = {
      t: Date.now(),
      value,
    };
    if (this.map.get(key)) {
      this.map.set(key, entry);
    } else {
      this.map.set(key, entry);
      this.entries++;
    }
    if (this.entries > this.maxEntries) {
      for (const iterationKey of this.map.keys()) {
        if (key !== iterationKey) {
          this.map.delete(iterationKey);
          this.entries--;
          break;
        }
      }
    }
  }
I
  async getFromSource(key: string): Promise<T> {
    const value = await this.method(key);
    this.put(key, value);
    return value;
  }
 
  async get(key: string): Promise<T> {
    const entry = this.map.get(key);
    if (entry) {
      const now = Date.now();
      if (now - entry.t <= this.ttl) {
        return entry.value;
      } else if (now - entry.t <= this.evictionTime) {
        this.getFromSource(key).catch(noop);
        return entry.value;
      } else {
        this.map.delete(key);
        this.entries--;
        return await this.getFromSource(key);
      }
    } else {
      return await this.getFromSource(key);
    }
  }
 
  getSync(key: string): T | null {
    const entry = this.map.get(key);
    if (!entry) return null;
    const now = Date.now();
    if (now - entry.t <= this.ttl) {
      return entry.value;
    } else if (now - entry.t <= this.evictionTime) {
      this.getFromSource(key).catch(noop);
      return entry.value;
    }
    return null;
  }

  exists(key: string): boolean {
    const entry = this.map.get(key);
    if (!entry) return false;
    const now = Date.now();
    return now - entry.t <= this.evictionTime;
  }

  scheduleGC() {
    this.timer = setTimeout(this.runGC, this.gcPeriod);
    this.timer.unref();
  }

  startGC() {
    this.scheduleGC();
  }

  runGC = () => {
    const now = Date.now();
    for (const key of this.map.keys()) {
      const entry = this.map.get(key);
      if (entry && now - entry.t >= this.evictionTime) {
        this.map.delete(key);
        this.entries--;
      }
    }
    this.scheduleGC();
  };
I
  stopGC = () => {
    clearTimeout(this.timer);
  };
 
  retire(key: string, newTime: number = 0): boolean {
    const entry = this.map.get(key);
    if (!entry) return false;
    entry.t = newTime;
    return true;
  }
 
  remove(key: string): boolean {
    const success = this.map.delete(key);
    if (success) this.entries--;
    return success;
  }
}