{"version":3,"file":"lru-cache.mjs","names":[],"sources":["../src/lru-cache.ts"],"sourcesContent":["/* -------------------------------------------------------------------\n\n                       🗲 Storm Software - Stryke\n\n This code was released as part of the Stryke project. Stryke\n is maintained by Storm Software under the Apache-2.0 license, and is\n free for commercial and private use. For more information, please visit\n our licensing page at https://stormsoftware.com/licenses/projects/stryke.\n\n Website:                  https://stormsoftware.com\n Repository:               https://github.com/storm-software/stryke\n Documentation:            https://docs.stormsoftware.com/projects/stryke\n Contact:                  https://stormsoftware.com/contact\n\n SPDX-License-Identifier:  Apache-2.0\n\n ------------------------------------------------------------------- */\n\nclass LRUNode<T> {\n  public readonly key: string;\n\n  public data: T;\n\n  public size: number;\n\n  public prev: LRUNode<T> | SentinelNode<T> | null = null;\n\n  public next: LRUNode<T> | SentinelNode<T> | null = null;\n\n  constructor(key: string, data: T, size: number) {\n    this.key = key;\n    this.data = data;\n    this.size = size;\n  }\n}\n\n/**\n * Sentinel node used for head/tail boundaries.\n * These nodes don't contain actual cache data but simplify list operations.\n */\nclass SentinelNode<T> {\n  public prev: LRUNode<T> | SentinelNode<T> | null = null;\n\n  public next: LRUNode<T> | SentinelNode<T> | null = null;\n}\n\n/**\n * LRU (Least Recently Used) Cache implementation using a doubly-linked list\n * and hash map for O(1) operations.\n *\n * Algorithm:\n * - Uses a doubly-linked list to maintain access order (most recent at head)\n * - Hash map provides O(1) key-to-node lookup\n * - Sentinel head/tail nodes simplify edge case handling\n * - Size-based eviction supports custom size calculation functions\n *\n * Data Structure Layout:\n * HEAD \\<-\\> [most recent] \\<-\\> ... \\<-\\> [least recent] \\<-\\> TAIL\n *\n * Operations:\n * - get(): Move accessed node to head (mark as most recent)\n * - set(): Add new node at head, evict from tail if over capacity\n * - Eviction: Remove least recent node (tail.prev) when size exceeds limit\n */\nexport class LRUCache<T> {\n  private readonly cache: Map<string, LRUNode<T>> = new Map();\n\n  private readonly head: SentinelNode<T>;\n\n  private readonly tail: SentinelNode<T>;\n\n  private totalSize: number = 0;\n\n  private readonly maxSize: number;\n\n  private readonly calculateSize: ((value: T) => number) | undefined;\n\n  constructor(maxSize: number, calculateSize?: (value: T) => number) {\n    this.maxSize = maxSize;\n    this.calculateSize = calculateSize;\n\n    // Create sentinel nodes to simplify doubly-linked list operations\n    // HEAD <-> TAIL (empty list)\n    this.head = new SentinelNode<T>();\n    this.tail = new SentinelNode<T>();\n    this.head.next = this.tail;\n    this.tail.prev = this.head;\n  }\n\n  /**\n   * Adds a node immediately after the head (marks as most recently used). Used when inserting new items or when an item is accessed. **PRECONDITION:** node must be disconnected (prev/next should be null)\n   *\n   * @param node - The node to add after the head.\n   */\n  private addToHead(node: LRUNode<T>): void {\n    node.prev = this.head;\n    node.next = this.head.next;\n    // head.next is always non-null (points to tail or another node)\n    this.head.next!.prev = node;\n    this.head.next = node;\n  }\n\n  /**\n   * Removes a node from its current position in the doubly-linked list. Updates the prev/next pointers of adjacent nodes to maintain list integrity. **PRECONDITION:** node must be connected (prev/next are non-null)\n   *\n   * @param node - The node to remove from the list.\n   */\n  private removeNode(node: LRUNode<T>): void {\n    // Connected nodes always have non-null prev/next\n    node.prev!.next = node.next;\n    node.next!.prev = node.prev;\n  }\n\n  /**\n   * Moves an existing node to the head position (marks as most recently used). This is the core LRU operation - accessed items become most recent.\n   *\n   * @param node - The node to move to the head.\n   */\n  private moveToHead(node: LRUNode<T>): void {\n    this.removeNode(node);\n    this.addToHead(node);\n  }\n\n  /**\n   * Removes and returns the least recently used node (the one before tail). This is called during eviction when the cache exceeds capacity. **PRECONDITION:** cache is not empty (ensured by caller)\n   *\n   * @returns The removed least recently used node.\n   */\n  private removeTail(): LRUNode<T> {\n    const lastNode = this.tail.prev as LRUNode<T>;\n    // tail.prev is always non-null and always LRUNode when cache is not empty\n    this.removeNode(lastNode);\n    return lastNode;\n  }\n\n  /**\n   * Sets a key-value pair in the cache.\n   * If the key exists, updates the value and moves to head.\n   * If new, adds at head and evicts from tail if necessary.\n   *\n   * Time Complexity:\n   * - O(1) for uniform item sizes\n   * - O(k) where k is the number of items evicted (can be O(N) for variable sizes)\n   *\n   * @param key - The key to set.\n   * @param value - The value to set.\n   */\n  public set(key: string, value: T): void {\n    const size = this.calculateSize?.(value) ?? 1;\n    if (size > this.maxSize) {\n      // console.warn(\"Single item size exceeds maxSize\");\n      return;\n    }\n\n    const existing = this.cache.get(key);\n    if (existing) {\n      // Update existing node: adjust size and move to head (most recent)\n      existing.data = value;\n      this.totalSize = this.totalSize - existing.size + size;\n      existing.size = size;\n      this.moveToHead(existing);\n    } else {\n      // Add new node at head (most recent position)\n      const newNode = new LRUNode(key, value, size);\n      this.cache.set(key, newNode);\n      this.addToHead(newNode);\n      this.totalSize += size;\n    }\n\n    // Evict least recently used items until under capacity\n    while (this.totalSize > this.maxSize && this.cache.size > 0) {\n      const tail = this.removeTail();\n      this.cache.delete(tail.key);\n      this.totalSize -= tail.size;\n    }\n  }\n\n  /**\n   * Checks if a key exists in the cache.\n   * This is a pure query operation - does NOT update LRU order.\n   *\n   * Time Complexity: O(1)\n   */\n  public has(key: string): boolean {\n    return this.cache.has(key);\n  }\n\n  /**\n   * Retrieves a value by key and marks it as most recently used.\n   * Moving to head maintains the LRU property for future evictions.\n   *\n   * Time Complexity: O(1)\n   */\n  public get(key: string): T | undefined {\n    const node = this.cache.get(key);\n    if (!node) return undefined;\n\n    // Mark as most recently used by moving to head\n    this.moveToHead(node);\n\n    return node.data;\n  }\n\n  /**\n   * Returns an iterator over the cache entries. The order is outputted in the\n   * order of most recently used to least recently used.\n   */\n  public *[Symbol.iterator](): IterableIterator<[string, T]> {\n    let current = this.head.next;\n    while (current && current !== this.tail) {\n      // Between head and tail, current is always LRUNode\n      const node = current as LRUNode<T>;\n      yield [node.key, node.data];\n      current = current.next;\n    }\n  }\n\n  /**\n   * Removes a specific key from the cache.\n   * Updates both the hash map and doubly-linked list.\n   *\n   * Time Complexity: O(1)\n   */\n  public remove(key: string): void {\n    const node = this.cache.get(key);\n    if (!node) return;\n\n    this.removeNode(node);\n    this.cache.delete(key);\n    this.totalSize -= node.size;\n  }\n\n  /**\n   * Returns the number of items in the cache.\n   */\n  public get size(): number {\n    return this.cache.size;\n  }\n\n  /**\n   * Returns the current total size of all cached items.\n   * This uses the custom size calculation if provided.\n   */\n  public get currentSize(): number {\n    return this.totalSize;\n  }\n}\n"],"mappings":";AAkBA,IAAM,UAAN,MAAiB;CACf,AAAgB;CAEhB,AAAO;CAEP,AAAO;CAEP,AAAO,OAA4C;CAEnD,AAAO,OAA4C;CAEnD,YAAY,KAAa,MAAS,MAAc;AAC9C,OAAK,MAAM;AACX,OAAK,OAAO;AACZ,OAAK,OAAO;;;;;;;AAQhB,IAAM,eAAN,MAAsB;CACpB,AAAO,OAA4C;CAEnD,AAAO,OAA4C;;;;;;;;;;;;;;;;;;;;AAqBrD,IAAa,WAAb,MAAyB;CACvB,AAAiB,wBAAiC,IAAI,KAAK;CAE3D,AAAiB;CAEjB,AAAiB;CAEjB,AAAQ,YAAoB;CAE5B,AAAiB;CAEjB,AAAiB;CAEjB,YAAY,SAAiB,eAAsC;AACjE,OAAK,UAAU;AACf,OAAK,gBAAgB;AAIrB,OAAK,OAAO,IAAI,cAAiB;AACjC,OAAK,OAAO,IAAI,cAAiB;AACjC,OAAK,KAAK,OAAO,KAAK;AACtB,OAAK,KAAK,OAAO,KAAK;;;;;;;CAQxB,AAAQ,UAAU,MAAwB;AACxC,OAAK,OAAO,KAAK;AACjB,OAAK,OAAO,KAAK,KAAK;AAEtB,OAAK,KAAK,KAAM,OAAO;AACvB,OAAK,KAAK,OAAO;;;;;;;CAQnB,AAAQ,WAAW,MAAwB;AAEzC,OAAK,KAAM,OAAO,KAAK;AACvB,OAAK,KAAM,OAAO,KAAK;;;;;;;CAQzB,AAAQ,WAAW,MAAwB;AACzC,OAAK,WAAW,KAAK;AACrB,OAAK,UAAU,KAAK;;;;;;;CAQtB,AAAQ,aAAyB;EAC/B,MAAM,WAAW,KAAK,KAAK;AAE3B,OAAK,WAAW,SAAS;AACzB,SAAO;;;;;;;;;;;;;;CAeT,AAAO,IAAI,KAAa,OAAgB;EACtC,MAAM,OAAO,KAAK,gBAAgB,MAAM,IAAI;AAC5C,MAAI,OAAO,KAAK,QAEd;EAGF,MAAM,WAAW,KAAK,MAAM,IAAI,IAAI;AACpC,MAAI,UAAU;AAEZ,YAAS,OAAO;AAChB,QAAK,YAAY,KAAK,YAAY,SAAS,OAAO;AAClD,YAAS,OAAO;AAChB,QAAK,WAAW,SAAS;SACpB;GAEL,MAAM,UAAU,IAAI,QAAQ,KAAK,OAAO,KAAK;AAC7C,QAAK,MAAM,IAAI,KAAK,QAAQ;AAC5B,QAAK,UAAU,QAAQ;AACvB,QAAK,aAAa;;AAIpB,SAAO,KAAK,YAAY,KAAK,WAAW,KAAK,MAAM,OAAO,GAAG;GAC3D,MAAM,OAAO,KAAK,YAAY;AAC9B,QAAK,MAAM,OAAO,KAAK,IAAI;AAC3B,QAAK,aAAa,KAAK;;;;;;;;;CAU3B,AAAO,IAAI,KAAsB;AAC/B,SAAO,KAAK,MAAM,IAAI,IAAI;;;;;;;;CAS5B,AAAO,IAAI,KAA4B;EACrC,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI;AAChC,MAAI,CAAC,KAAM,QAAO;AAGlB,OAAK,WAAW,KAAK;AAErB,SAAO,KAAK;;;;;;CAOd,EAAS,OAAO,YAA2C;EACzD,IAAI,UAAU,KAAK,KAAK;AACxB,SAAO,WAAW,YAAY,KAAK,MAAM;GAEvC,MAAM,OAAO;AACb,SAAM,CAAC,KAAK,KAAK,KAAK,KAAK;AAC3B,aAAU,QAAQ;;;;;;;;;CAUtB,AAAO,OAAO,KAAmB;EAC/B,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI;AAChC,MAAI,CAAC,KAAM;AAEX,OAAK,WAAW,KAAK;AACrB,OAAK,MAAM,OAAO,IAAI;AACtB,OAAK,aAAa,KAAK;;;;;CAMzB,IAAW,OAAe;AACxB,SAAO,KAAK,MAAM;;;;;;CAOpB,IAAW,cAAsB;AAC/B,SAAO,KAAK"}