{"version":3,"sources":["../src/lru.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\n// LRU Cache with TTL, size limit, auto GC, and peek.\n//\n// Interface design notes:\n// 1. Event listeners - Not included as forseeable usage is very limited.\n// 2. Cache hit/miss stats - Not included as these do not seem useful enough.\n// 3. Peek - Included as there is no performance impact.\n// 4. Max size - Seems like a common config needed for LRU caches, hence\n//    included.\n// 5. TTL - Seems useful because data could get stale, and having this avoids\n//    requiring the user to implement this by themselves.\n// 6. GC - Included because the GC is super efficient using timers. Users can\n//    just ignore GC entirely and let it do its thing.\n// 7. Map implementation - Uses the default Map implementation in ES, no need\n//    to reinvent our own map here to save on a few nanoseconds (at best).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** All options are optional. */\nexport type LRUMapOptions = {\n  /**\n   * Maximum number of entries. Oldest entry is evicted when exceeded.\n   * Defaults to no limit. 0 means \"no max\", not \"no entries allowed\".\n   */\n  maxSize?: number;\n\n  /**\n   * Time-to-live in milliseconds. Entries expire after this duration.\n   * Defaults to no expiration. 0 means \"no expiration\", not\n   * \"expires immediately\".\n   */\n  ttlMs?: number;\n};\n\ntype Entry<K, V> = {\n  key: K;\n  value: V;\n  /** Absolute expiry timestamp (ms), or undefined if no TTL. */\n  expiryMs?: number;\n  expiryTimeout?: ReturnType<typeof setTimeout>;\n  prev?: Entry<K, V>;\n  next?: Entry<K, V>;\n};\n\n/**\n * Strongly-typed LRU cache that implements the Map interface. You can also use\n * this as a size-limited map by setting maxSize.\n *\n * Values can be null, but cannot be undefined.\n */\nexport class LRUMap<K, V> implements Map<K, V> {\n  private readonly options: LRUMapOptions;\n  private readonly map = new Map<K, Entry<K, V>>();\n\n  // Doubly-linked list — head.next = MRU, tail.prev = LRU\n  // Sentinel nodes simplify edge cases.\n  private readonly head = {} as Entry<K, V>;\n  private readonly tail = {} as Entry<K, V>;\n\n  public constructor(options?: LRUMapOptions) {\n    this.options = { ...options }; // clone the argument\n    this.head.next = this.tail;\n    this.tail.prev = this.head;\n  }\n\n  // ── Core API ───────────────────────────────────────────────────────────────\n\n  /**\n   * Store a value. Overwrites any existing entry for the key.\n   * Note: Disallow setting a custom TTL because that will require us to do a\n   * sorted insertion instead of an insert-at-front.\n   */\n  public set(key: K, value: V): this {\n    const existingEntry = this.map.get(key);\n\n    if (existingEntry) {\n      existingEntry.value = value;\n      this.setupExpiryTimeout(existingEntry);\n      this.moveToFront(existingEntry);\n    } else {\n      // Evict LRU entry if over capacity.\n      if (this.options.maxSize && this.map.size >= this.options.maxSize) {\n        this.evictOldestEntry();\n      }\n\n      const entry: Entry<K, V> = { key, value };\n      this.setupExpiryTimeout(entry);\n      this.map.set(key, entry);\n      this.insertAtFront(entry);\n    }\n\n    return this;\n  }\n\n  /**\n   * Retrieve a value and mark it as recently used.\n   * Returns `undefined` on miss or if the entry has expired.\n   */\n  public get(key: K): V | undefined {\n    const entry = this.map.get(key);\n    if (!entry || this.isExpired(entry)) {\n      if (entry) this.deleteEntry(entry);\n      return undefined;\n    }\n\n    // Don't renew expiration time on gets. If we do, stale entries might never\n    // expire if we keep on reading them.\n    this.moveToFront(entry);\n    return entry.value;\n  }\n\n  /**\n   * Read a value WITHOUT updating recency or hit/miss stats.\n   * Useful for inspection or monitoring without polluting cache order.\n   */\n  public peek(key: K): V | undefined {\n    return this.peekEntry(key)?.value;\n  }\n\n  /**\n   * Get the existing value, or return a default value. Will not set the value\n   * in the map.\n   */\n  public getOrDefault(key: K, defaultValue: V): V {\n    const existingValue = this.get(key);\n    if (existingValue !== undefined) {\n      return existingValue;\n    }\n\n    return defaultValue;\n  }\n\n  /** Required by Map interface. */\n  public getOrInsert(key: K, value: V): V {\n    const existingValue = this.get(key);\n    if (existingValue !== undefined) {\n      return existingValue;\n    }\n\n    this.set(key, value);\n    return value;\n  }\n\n  /** Required by Map interface. */\n  public getOrInsertComputed(key: K, callback: (key: K) => V): V {\n    const existingValue = this.get(key);\n    if (existingValue !== undefined) {\n      return existingValue;\n    }\n\n    const value = callback(key);\n    this.set(key, value);\n    return value;\n  }\n\n  /** Same as getOrInsertComputed, but async. */\n  public async getOrInsertLoaded(\n    key: K,\n    loader: (key: K) => Promise<V>,\n  ): Promise<V> {\n    const existingValue = this.get(key);\n    if (existingValue !== undefined) {\n      return existingValue;\n    }\n\n    const loadedValue = await loader(key);\n    this.set(key, loadedValue);\n    return loadedValue;\n  }\n\n  /** Returns true if the key exists and has not expired. */\n  public has(key: K): boolean {\n    const entry = this.map.get(key);\n    if (!entry) return false;\n    if (this.isExpired(entry)) {\n      this.deleteEntry(entry);\n      return false;\n    }\n    return true;\n  }\n\n  /** Remove a single entry. Returns true if the key existed. */\n  public delete(key: K): boolean {\n    const entry = this.map.get(key);\n    if (!entry) return false;\n    this.deleteEntry(entry);\n    return true;\n  }\n\n  /** Remove all entries. */\n  public clear(): void {\n    if (this.options.ttlMs) {\n      for (const entry of this.map.values()) {\n        if (entry.expiryTimeout) {\n          clearTimeout(entry.expiryTimeout);\n          delete entry.expiryTimeout;\n        }\n      }\n    }\n\n    this.map.clear();\n    this.head.next = this.tail;\n    this.tail.prev = this.head;\n  }\n\n  /** Number of entries currently in the cache (including expired ones). */\n  public get size(): number {\n    return this.map.size;\n  }\n\n  public get [Symbol.toStringTag](): string {\n    return `LRUMap(${this.size})`;\n  }\n\n  // ── Iteration ──────────────────────────────────────────────────────────────\n\n  /** Filter out expired entries. */\n  public keys(): MapIterator<K> {\n    return this.entries().map(([k]) => k);\n  }\n\n  /** Filter out expired entries. */\n  public values(): MapIterator<V> {\n    return this.entries().map(([, v]) => v);\n  }\n\n  /**\n   * Iterate over [key, value] pairs (in insertion order), skipping expired\n   * entries.\n   * NOTE: Do not delete any entries, otherwise it will break the LRU data.\n   */\n  public entries(): MapIterator<[K, V]> {\n    return this.map\n      .entries()\n      .filter(([, e]) => !this.isExpired(e))\n      .map(([k, e]) => [k, e.value]);\n  }\n\n  /** NOTE: Do not delete any entries, otherwise it will break the LRU data. */\n  public [Symbol.iterator](): MapIterator<[K, V]> {\n    return this.entries();\n  }\n\n  /**\n   * NOTE: Do not use the `map` argument as it will always be an empty Map.\n   * The actual underlying map has a different value type.\n   */\n  public forEach(\n    callbackFn: (value: V, key: K, map: Map<K, V>) => void,\n    thisArg?: unknown,\n  ): void {\n    const tempMap = new Map<K, V>();\n\n    this.map.forEach((entry, key) => {\n      if (!this.isExpired(entry)) {\n        callbackFn(entry.value, key, tempMap);\n      }\n    }, thisArg);\n  }\n\n  // ── Private Helpers ────────────────────────────────────────────────────────\n\n  private calcExpiry(): number | undefined {\n    // When ttlMs=0, it means \"no expiration\", not \"always expire\".\n    const ms = this.options.ttlMs;\n    return ms ? Date.now() + ms : undefined;\n  }\n\n  private isExpired(entry: Entry<K, V>): boolean {\n    // When expiryMs=0, it means \"no expiration\", not \"already expired\".\n    return !!entry.expiryMs && Date.now() >= entry.expiryMs;\n  }\n\n  private peekEntry(key: K): Entry<K, V> | undefined {\n    const entry = this.map.get(key);\n    if (!entry || this.isExpired(entry)) {\n      return undefined;\n    }\n    return entry;\n  }\n\n  private unrefTimer(t: ReturnType<typeof setTimeout>): void {\n    const timer = t as unknown;\n\n    if (\n      timer &&\n      typeof timer === \"object\" &&\n      \"unref\" in timer &&\n      typeof timer.unref === \"function\"\n    ) {\n      timer.unref();\n    }\n  }\n\n  private setupExpiryTimeout(entry: Entry<K, V>) {\n    if (!this.options.ttlMs) {\n      return;\n    }\n\n    const expiryMs = (entry.expiryMs = this.calcExpiry());\n\n    if (entry.expiryTimeout) {\n      clearTimeout(entry.expiryTimeout);\n    }\n\n    entry.expiryTimeout = setTimeout(() => {\n      if (entry.expiryMs === expiryMs) {\n        this.deleteEntry(entry);\n      }\n    }, this.options.ttlMs);\n\n    // NodeJS only: Allow process to exit before this timeout runs.\n    this.unrefTimer(entry.expiryTimeout);\n  }\n\n  private insertAtFront(entry: Entry<K, V>): void {\n    entry.prev = this.head;\n    entry.next = this.head.next;\n    this.head.next!.prev = entry;\n    this.head.next = entry;\n  }\n\n  private removeFromList(entry: Entry<K, V>): void {\n    entry.prev!.next = entry.next;\n    entry.next!.prev = entry.prev;\n  }\n\n  private moveToFront(entry: Entry<K, V>): void {\n    if (this.head.next === entry) return; // already MRU\n    this.removeFromList(entry);\n    this.insertAtFront(entry);\n  }\n\n  private evictOldestEntry(): void {\n    const oldestEntry = this.tail.prev!;\n    if (oldestEntry === this.head) return; // empty\n    this.deleteEntry(oldestEntry);\n  }\n\n  private deleteEntry(entry: Entry<K, V>): void {\n    if (entry.expiryTimeout) {\n      clearTimeout(entry.expiryTimeout);\n      delete entry.expiryTimeout;\n    }\n\n    this.removeFromList(entry);\n    this.map.delete(entry.key);\n  }\n}\n"],"mappings":"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","names":["lru_exports","__export","LRUMap","__toCommonJS","options","key","value","existingEntry","entry","defaultValue","existingValue","callback","loader","loadedValue","k","v","e","callbackFn","thisArg","tempMap","ms","timer","expiryMs","oldestEntry"]}