From 3b5d733f3b8ca5f49d760748a56203a70ae94d77 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 13:36:48 -0400 Subject: [PATCH 01/55] Add peek() method to retrieve value without LRU update - peek() retrieves value without moving item in LRU order - peek() does not perform TTL checks or delete expired items - All core operations remain O(1) - Updated tests, documentation, and AGENTS.md --- AGENTS.md | 3 +- README.md | 1 + coverage.txt | 4 +-- dist/tiny-lru.cjs | 12 ++++++++ dist/tiny-lru.js | 12 ++++++++ dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- dist/tiny-lru.umd.js | 12 ++++++++ dist/tiny-lru.umd.min.js | 2 +- dist/tiny-lru.umd.min.js.map | 2 +- docs/API.md | 22 ++++++++++++++ docs/TECHNICAL_DOCUMENTATION.md | 2 ++ src/lru.js | 12 ++++++++ tests/unit/lru.test.js | 52 +++++++++++++++++++++++++++++++++ 14 files changed, 133 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2ac9f75..cfae2d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,9 +56,10 @@ Source code is in `src/`. - `lru(max, ttl, resetTtl)` - Factory function to create cache - `LRU` class - Direct instantiation with `new LRU(max, ttl, resetTtl)` -- Key methods: `get()`, `set()`, `delete()`, `has()`, `clear()`, `evict()` +- Key methods: `get()`, `set()`, `peek()`, `delete()`, `has()`, `clear()`, `evict()` - Array methods: `keys()`, `values()`, `entries()` - Properties: `first`, `last`, `max`, `size`, `ttl`, `resetTtl` +- `peek(key)` - Retrieve value without moving it (no LRU update, no TTL check) ## Testing diff --git a/README.md b/README.md index 57114b2..6724038 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ const user: User | undefined = cache.get("user:1"); | `get(key)` | Retrieve a value. Moves item to most recent. | | `has(key)` | Check if key exists and is not expired. | | `keys()` | Get all keys in LRU order (oldest first). | +| `peek(key)` | Retrieve a value without LRU update. | | `set(key, value)` | Store a value. Returns `this` for chaining. | | `setWithEvicted(key, value)` | Store value, return evicted item if full. | | `values(keys?)` | Get all values, or values for specific keys. | diff --git a/coverage.txt b/coverage.txt index b235dc8..2cc92d0 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 97.30 | 100.00 | +ℹ lru.js | 100.00 | 97.40 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 97.30 | 100.00 | +ℹ all files | 100.00 | 97.40 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 63747db..36b65ea 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -130,6 +130,18 @@ class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Retrieves a value from the cache by key without updating LRU order. + * Note: Does not perform TTL checks or remove expired items. + * + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found. + */ + peek(key) { + const item = this.items[key]; + return item !== undefined ? item.value : undefined; + } + /** * Retrieves a value from the cache by key. Updates the item's position to most recently used. * diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index c3acd31..7407329 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -128,6 +128,18 @@ class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Retrieves a value from the cache by key without updating LRU order. + * Note: Does not perform TTL checks or remove expired items. + * + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found. + */ + peek(key) { + const item = this.items[key]; + return item !== undefined ? item.value : undefined; + } + /** * Retrieves a value from the cache by key. Updates the item's position to most recently used. * diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 6b48b21..b2e44d4 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{constructor(t=0,i=0,s=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=s,this.size=0,this.ttl=i}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){const i=this.items[t];return void 0!==i&&(delete this.items[t],this.size--,this.#t(i),i.prev=null,i.next=null),this}entries(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let s=0;s0&&i.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(i),i.value)}has(t){const i=this.items[t];return void 0!==i&&(0===this.ttl||i.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let i=this.first,s=0;for(;null!==i;)t[s++]=i.key,i=i.next;return t}setWithEvicted(t,i){let s=null,e=this.items[t];return void 0!==e?(e.value=i,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(s={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=e:this.last.next=e,this.last=e),s}set(t,i){let s=this.items[t];return void 0!==s?(s.value=i,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&this.evict(),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=s:this.last.next=s,this.last=s),this}values(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let s=0;s0&&i.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(i),i.value)}has(t){const i=this.items[t];return void 0!==i&&(0===this.ttl||i.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let i=this.first,s=0;for(;null!==i;)t[s++]=i.key,i=i.next;return t}setWithEvicted(t,i){let s=null,e=this.items[t];return void 0!==e?(e.value=i,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(s={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=e:this.last.next=e,this.last=e),s}set(t,i){let s=this.items[t];return void 0!==s?(s.value=i,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&this.evict(),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=s:this.last.next=s,this.last=s),this}values(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/dist/tiny-lru.umd.js b/dist/tiny-lru.umd.js index 9ab27e8..8572a7e 100644 --- a/dist/tiny-lru.umd.js +++ b/dist/tiny-lru.umd.js @@ -128,6 +128,18 @@ class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Retrieves a value from the cache by key without updating LRU order. + * Note: Does not perform TTL checks or remove expired items. + * + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found. + */ + peek(key) { + const item = this.items[key]; + return item !== undefined ? item.value : undefined; + } + /** * Retrieves a value from the cache by key. Updates the item's position to most recently used. * diff --git a/dist/tiny-lru.umd.min.js b/dist/tiny-lru.umd.min.js index b76e912..456c1b0 100644 --- a/dist/tiny-lru.umd.min.js +++ b/dist/tiny-lru.umd.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).lru={})}(this,(function(t){"use strict";class i{constructor(t=0,i=0,e=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=e,this.size=0,this.ttl=i}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){const i=this.items[t];return void 0!==i&&(delete this.items[t],this.size--,this.#t(i),i.prev=null,i.next=null),this}entries(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let e=0;e0&&i.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(i),i.value)}has(t){const i=this.items[t];return void 0!==i&&(0===this.ttl||i.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let i=this.first,e=0;for(;null!==i;)t[e++]=i.key,i=i.next;return t}setWithEvicted(t,i){let e=null,s=this.items[t];return void 0!==s?(s.value=i,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&(e={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=s:this.last.next=s,this.last=s),e}set(t,i){let e=this.items[t];return void 0!==e?(e.value=i,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&this.evict(),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=e:this.last.next=e,this.last=e),this}values(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let e=0;e0&&e.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(e),e.value)}has(t){const e=this.items[t];return void 0!==e&&(0===this.ttl||e.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let e=this.first,i=0;for(;null!==e;)t[i++]=e.key,e=e.next;return t}setWithEvicted(t,e){let i=null,s=this.items[t];return void 0!==s?(s.value=e,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=s:this.last.next=s,this.last=s),i}set(t,e){let i=this.items[t];return void 0!==i?(i.value=e,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=i:this.last.next=i,this.last=i),this}values(t){void 0===t&&(t=this.keys());const e=Array.from({length:t.length});for(let i=0;i>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","isNaN","TypeError"],"mappings":";;;;CAAA,SAAAA,EAAAC,GAAA,iBAAAC,SAAA,oBAAAC,OAAAF,EAAAC,SAAA,mBAAAE,QAAAA,OAAAC,IAAAD,OAAA,CAAA,WAAAH,GAAAA,GAAAD,EAAA,oBAAAM,WAAAA,WAAAN,GAAAO,MAAAC,IAAA,CAAA,EAAA,CAAA,CAAAC,MAAA,SAAAP,GAAA,aAOO,MAAMQ,EAUZ,WAAAC,CAAYC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACxCL,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKG,IAAMA,EACXH,KAAKK,SAAWA,EAChBL,KAAKW,KAAO,EACZX,KAAKI,IAAMA,CACZ,CAOA,KAAAQ,GAMC,OALAZ,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKW,KAAO,EAELX,IACR,CAQA,OAAOa,GACN,MAAMC,EAAOd,KAAKO,MAAMM,GAYxB,YAVaE,IAATD,WACId,KAAKO,MAAMM,GAClBb,KAAKW,OAELX,MAAKgB,EAAQF,GAEbA,EAAKG,KAAO,KACZH,EAAKI,KAAO,MAGNlB,IACR,CAUA,OAAAmB,CAAQC,QACML,IAATK,IACHA,EAAOpB,KAAKoB,QAGb,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMZ,EAAMO,EAAKK,GACXX,EAAOd,KAAKO,MAAMM,GACxBQ,EAAOI,GAAK,CAACZ,OAAcE,IAATD,EAAqBA,EAAKY,WAAQX,EACrD,CAEA,OAAOM,CACR,CAOA,KAAAM,GACC,GAAkB,IAAd3B,KAAKW,KACR,OAAOX,KAGR,MAAMc,EAAOd,KAAKM,MAalB,cAXON,KAAKO,MAAMO,EAAKD,KAEH,KAAdb,KAAKW,MACVX,KAAKM,MAAQ,KACbN,KAAKU,KAAO,MAEZV,MAAKgB,EAAQF,GAGdA,EAAKI,KAAO,KAELlB,IACR,CAQA,SAAA4B,CAAUf,GACT,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,EAAqBA,EAAKe,YAASd,CAC3C,CAQA,GAAAe,CAAIjB,GACH,MAAMC,EAAOd,KAAKO,MAAMM,GAExB,QAAaE,IAATD,EAEH,OAAId,KAAKI,IAAM,GACVU,EAAKe,QAAUE,KAAKC,WACvBhC,KAAKiC,OAAOpB,IAOdb,KAAKkC,UAAUpB,GAERA,EAAKY,MAId,CAQA,GAAAS,CAAItB,GACH,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,IAAoC,IAAbd,KAAKI,KAAaU,EAAKe,OAASE,KAAKC,MACpE,CASA,EAAAhB,CAAQF,GACW,OAAdA,EAAKG,OACRH,EAAKG,KAAKC,KAAOJ,EAAKI,MAGL,OAAdJ,EAAKI,OACRJ,EAAKI,KAAKD,KAAOH,EAAKG,MAGnBjB,KAAKM,QAAUQ,IAClBd,KAAKM,MAAQQ,EAAKI,MAGflB,KAAKU,OAASI,IACjBd,KAAKU,KAAOI,EAAKG,KAEnB,CAUA,SAAAiB,CAAUpB,GACLd,KAAKU,OAASI,IAIlBd,MAAKgB,EAAQF,GAEbA,EAAKG,KAAOjB,KAAKU,KACjBI,EAAKI,KAAO,KACZlB,KAAKU,KAAKQ,KAAOJ,EACjBd,KAAKU,KAAOI,EACb,CAOA,IAAAM,GACC,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQxB,KAAKW,OACzC,IAAIyB,EAAIpC,KAAKM,MACTmB,EAAI,EAER,KAAa,OAANW,GACNf,EAAOI,KAAOW,EAAEvB,IAChBuB,EAAIA,EAAElB,KAGP,OAAOG,CACR,CASA,cAAAgB,CAAexB,EAAKa,GACnB,IAAIY,EAAU,KACVxB,EAAOd,KAAKO,MAAMM,GAmCtB,YAjCaE,IAATD,GACHA,EAAKY,MAAQA,EACT1B,KAAKK,WACRS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,KAE3DJ,KAAKkC,UAAUpB,KAEXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,MACtCmC,EAAU,CACTzB,IAAKb,KAAKM,MAAMO,IAChBa,MAAO1B,KAAKM,MAAMoB,MAClBG,OAAQ7B,KAAKM,MAAMuB,QAEpB7B,KAAK2B,SAGNb,EAAOd,KAAKO,MAAMM,GAAO,CACxBgB,OAAQ7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLI,KAAMjB,KAAKU,KACXQ,KAAM,KACNQ,SAGmB,KAAd1B,KAAKW,KACVX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKQ,KAAOJ,EAGlBd,KAAKU,KAAOI,GAGNwB,CACR,CASA,GAAAC,CAAI1B,EAAKa,GACR,IAAIZ,EAAOd,KAAKO,MAAMM,GAgCtB,YA9BaE,IAATD,GACHA,EAAKY,MAAQA,EAET1B,KAAKK,WACRS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,KAG3DJ,KAAKkC,UAAUpB,KAEXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,KACtCH,KAAK2B,QAGNb,EAAOd,KAAKO,MAAMM,GAAO,CACxBgB,OAAQ7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLI,KAAMjB,KAAKU,KACXQ,KAAM,KACNQ,SAGmB,KAAd1B,KAAKW,KACVX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKQ,KAAOJ,EAGlBd,KAAKU,KAAOI,GAGNd,IACR,CAUA,MAAAwC,CAAOpB,QACOL,IAATK,IACHA,EAAOpB,KAAKoB,QAGb,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMX,EAAOd,KAAKO,MAAMa,EAAKK,IAC7BJ,EAAOI,QAAcV,IAATD,EAAqBA,EAAKY,WAAQX,CAC/C,CAEA,OAAOM,CACR,EA2BD5B,EAAAQ,IAAAA,EAAAR,EAAAM,IAdO,SAAaI,EAAM,IAAMC,EAAM,EAAGC,GAAW,GACnD,GAAIoC,MAAMtC,IAAQA,EAAM,EACvB,MAAM,IAAIuC,UAAU,qBAGrB,GAAID,MAAMrC,IAAQA,EAAM,EACvB,MAAM,IAAIsC,UAAU,qBAGrB,GAAwB,kBAAbrC,EACV,MAAM,IAAIqC,UAAU,0BAGrB,OAAO,IAAIzC,EAAIE,EAAKC,EAAKC,EAC1B,CAAA"} \ No newline at end of file +{"version":3,"file":"tiny-lru.umd.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 0d2e110..dcb1cbb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -226,6 +226,28 @@ console.log(cache.expiresAt("nonexistent")); // undefined --- +### `peek(key)` + +Retrieves value without updating LRU order. + +```javascript +cache.set("a", 1).set("b", 2); +cache.peek("a"); // 1 +console.log(cache.keys()); // ['b', 'a'] - order unchanged +``` + +**Parameters:** + +| Name | Type | Description | +| ----- | -------- | --------------- | +| `key` | `string` | Key to retrieve | + +**Returns:** `* | undefined` - Value or undefined if not found + +**Note:** Does not perform TTL expiration checks or update LRU order. + +--- + ### `get(key)` Retrieves value and promotes to most recently used. diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md index 4ff417b..af022c1 100644 --- a/docs/TECHNICAL_DOCUMENTATION.md +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -127,6 +127,7 @@ sequenceDiagram | `setWithEvicted(key, value)` | O(1) | O(1) | O(1) | Store value, return evicted item | | `delete(key)` | O(1) | O(1) | O(1) | Remove item from cache | | `has(key)` | O(1) | O(1) | O(1) | Check key existence | +| `peek(key)` | O(1) | O(1) | O(1) | Retrieve value without LRU update | | `clear()` | O(1) | O(1) | O(1) | Reset all pointers | | `evict()` | O(1) | O(1) | O(1) | Remove least recently used item | | `expiresAt(key)` | O(1) | O(1) | O(1) | Get expiration timestamp | @@ -338,6 +339,7 @@ export class LRU { get(key: any): T | undefined; has(key: any): boolean; keys(): any[]; + peek(key: any): T | undefined; set(key: any, value: T): this; setWithEvicted(key: any, value: T): EvictedItem | null; values(keys?: any[]): (T | undefined)[]; diff --git a/src/lru.js b/src/lru.js index 8f73490..c6b4b31 100644 --- a/src/lru.js +++ b/src/lru.js @@ -121,6 +121,18 @@ export class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Retrieves a value from the cache by key without updating LRU order. + * Note: Does not perform TTL checks or remove expired items. + * + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found. + */ + peek(key) { + const item = this.items[key]; + return item !== undefined ? item.value : undefined; + } + /** * Retrieves a value from the cache by key. Updates the item's position to most recently used. * diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index 2c22683..4f82776 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -728,4 +728,56 @@ describe("LRU Cache", function () { assert.deepEqual(values, [1, undefined, 2]); }); }); + + describe("peek method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + }); + + it("should retrieve value without moving to end", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + + assert.equal(cache.peek("key1"), "value1"); + assert.deepEqual(cache.keys(), ["key1", "key2"]); + }); + + it("should return undefined for non-existent key", function () { + assert.equal(cache.peek("nonexistent"), undefined); + }); + + it("should not affect LRU order when used with get", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + cache.peek("key1"); + cache.get("key2"); + + assert.deepEqual(cache.keys(), ["key1", "key3", "key2"]); + }); + + it("should work with TTL enabled but not check expiration", function () { + const ttlCache = new LRU(3, 100); + ttlCache.set("key1", "value1"); + + assert.equal(ttlCache.peek("key1"), "value1"); + + const expiry = ttlCache.expiresAt("key1"); + assert.ok(expiry > 0); + }); + + it("should allow peek after get maintains LRU order", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + cache.get("key1"); + cache.peek("key2"); + + assert.deepEqual(cache.keys(), ["key2", "key3", "key1"]); + }); + }); }); From 55c3c6c63f5a687507f48b0bbfe6845ceab9fe7b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 13:51:54 -0400 Subject: [PATCH 02/55] Add comprehensive tests for 9 LRU utility methods - Added 68 new tests covering forEach, getMany, hasAll, hasAny, cleanup, toJSON, stats, onEvict, sizeByTTL, keysByTTL, and valuesByTTL methods - Refactored values() to directly iterate linked list instead of using keys() - Refactored forEach() for direct linked list traversal - Updated API documentation in docs/API.md - Updated README.md method table - Updated AGENTS.md API reference - All tests passing (145/145) --- AGENTS.md | 3 +- README.md | 11 + coverage.txt | 4 +- dist/tiny-lru.cjs | 278 ++++++++++++- dist/tiny-lru.js | 278 ++++++++++++- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- dist/tiny-lru.umd.js | 278 ++++++++++++- dist/tiny-lru.umd.min.js | 2 +- dist/tiny-lru.umd.min.js.map | 2 +- docs/API.md | 240 ++++++++++++ src/lru.js | 278 ++++++++++++- tests/unit/lru.test.js | 730 +++++++++++++++++++++++++++++++++++ types/lru.d.ts | 101 ++++- 14 files changed, 2189 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cfae2d5..d1a0858 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,8 +56,9 @@ Source code is in `src/`. - `lru(max, ttl, resetTtl)` - Factory function to create cache - `LRU` class - Direct instantiation with `new LRU(max, ttl, resetTtl)` -- Key methods: `get()`, `set()`, `peek()`, `delete()`, `has()`, `clear()`, `evict()` +- Core methods: `get()`, `set()`, `peek()`, `delete()`, `has()`, `clear()`, `evict()` - Array methods: `keys()`, `values()`, `entries()` +- Utility methods: `forEach()`, `getMany()`, `hasAll()`, `hasAny()`, `cleanup()`, `toJSON()`, `stats()`, `onEvict()`, `sizeByTTL()`, `keysByTTL()`, `valuesByTTL()` - Properties: `first`, `last`, `max`, `size`, `ttl`, `resetTtl` - `peek(key)` - Retrieve value without moving it (no LRU update, no TTL check) diff --git a/README.md b/README.md index 6724038..7c203eb 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,24 @@ const user: User | undefined = cache.get("user:1"); | `entries(keys?)` | Get `[key, value]` pairs in LRU order. | | `evict()` | Remove the least recently used item. | | `expiresAt(key)` | Get expiration timestamp for a key. | +| `forEach(callback, thisArg?)` | Iterate over items in LRU order. | | `get(key)` | Retrieve a value. Moves item to most recent. | +| `getMany(keys)` | Batch retrieve multiple items. | | `has(key)` | Check if key exists and is not expired. | +| `hasAll(keys)` | Check if ALL keys exist. | +| `hasAny(keys)` | Check if ANY key exists. | | `keys()` | Get all keys in LRU order (oldest first). | +| `cleanup()` | Remove expired items without LRU update. | | `peek(key)` | Retrieve a value without LRU update. | | `set(key, value)` | Store a value. Returns `this` for chaining. | | `setWithEvicted(key, value)` | Store value, return evicted item if full. | | `values(keys?)` | Get all values, or values for specific keys. | +| `toJSON()` | Serialize cache to JSON format. | +| `stats()` | Get cache statistics. | +| `onEvict(callback)` | Register eviction callback. | +| `sizeByTTL()` | Get counts by TTL status. | +| `keysByTTL()` | Get keys by TTL status. | +| `valuesByTTL()` | Get values by TTL status. | ### Properties diff --git a/coverage.txt b/coverage.txt index 2cc92d0..6f12e04 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 97.40 | 100.00 | +ℹ lru.js | 99.38 | 97.73 | 100.00 | 534-535 563-564 ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 97.40 | 100.00 | +ℹ all files | 99.38 | 97.73 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 36b65ea..d34051b 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -15,6 +15,9 @@ * @class LRU */ class LRU { + #stats; + #onEvict; + /** * Creates a new LRU cache instance. * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. @@ -32,6 +35,8 @@ class LRU { this.resetTtl = resetTtl; this.size = 0; this.ttl = ttl; + this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; + this.#onEvict = null; } /** @@ -44,6 +49,11 @@ class LRU { this.items = Object.create(null); this.last = null; this.size = 0; + this.#stats.hits = 0; + this.#stats.misses = 0; + this.#stats.sets = 0; + this.#stats.deletes = 0; + this.#stats.evictions = 0; return this; } @@ -60,6 +70,7 @@ class LRU { if (item !== undefined) { delete this.items[key]; this.size--; + this.#stats.deletes++; this.#unlink(item); @@ -106,6 +117,7 @@ class LRU { const item = this.first; delete this.items[item.key]; + this.#stats.evictions++; if (--this.size === 0) { this.first = null; @@ -115,6 +127,13 @@ class LRU { } item.next = null; + if (this.#onEvict !== null) { + this.#onEvict({ + key: item.key, + value: item.value, + expiry: item.expiry, + }); + } return this; } @@ -156,6 +175,7 @@ class LRU { if (this.ttl > 0) { if (item.expiry <= Date.now()) { this.delete(key); + this.#stats.misses++; return undefined; } @@ -163,10 +183,12 @@ class LRU { // Fast LRU update without full set() overhead this.moveToEnd(item); + this.#stats.hits++; return item.value; } + this.#stats.misses++; return undefined; } @@ -289,6 +311,7 @@ class LRU { this.last = item; } + this.#stats.sets++; return evicted; } @@ -332,6 +355,8 @@ class LRU { this.last = item; } + this.#stats.sets++; + return this; } @@ -340,12 +365,17 @@ class LRU { * When no keys provided, returns all values in LRU order. * When keys provided, order matches the input array. * - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys. * @returns {Array<*>} Array of values corresponding to the keys. */ values(keys) { if (keys === undefined) { - keys = this.keys(); + const result = Array.from({ length: this.size }); + let i = 0; + for (let x = this.first; x !== null; x = x.next) { + result[i++] = x.value; + } + return result; } const result = Array.from({ length: keys.length }); @@ -356,6 +386,248 @@ class LRU { return result; } + + /** + * Iterate over cache items in LRU order (least to most recent). + * Note: This method directly accesses items from the linked list without calling + * get() or peek(), so it does not update LRU order or check TTL expiration during iteration. + * + * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache) + * @param {Object} [thisArg] - Value to use as `this` when executing callback. + * @returns {LRU} The LRU instance for method chaining. + */ + forEach(callback, thisArg) { + for (let x = this.first; x !== null; x = x.next) { + callback.call(thisArg, x.value, x.key, this); + } + + return this; + } + + /** + * Batch retrieve multiple items. + * + * @param {string[]} keys - Array of keys to retrieve. + * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). + */ + getMany(keys) { + const result = Object.create(null); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + result[key] = this.get(key); + } + + return result; + } + + /** + * Batch existence check - returns true if ALL keys exist. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if all keys exist and are not expired. + */ + hasAll(keys) { + for (let i = 0; i < keys.length; i++) { + if (!this.has(keys[i])) { + return false; + } + } + + return true; + } + + /** + * Batch existence check - returns true if ANY key exists. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if any key exists and is not expired. + */ + hasAny(keys) { + for (let i = 0; i < keys.length; i++) { + if (this.has(keys[i])) { + return true; + } + } + + return false; + } + + /** + * Remove expired items without affecting LRU order. + * Unlike get(), this does not move items to the end. + * + * @returns {number} Number of expired items removed. + */ + cleanup() { + if (this.ttl === 0 || this.size === 0) { + return 0; + } + + const now = Date.now(); + let removed = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry <= now) { + const key = x.key; + if (this.items[key] !== undefined) { + delete this.items[key]; + this.size--; + removed++; + } + } + } + + if (removed > 0) { + this.#rebuildList(); + } + + return removed; + } + + /** + * Serialize cache to JSON-compatible format. + * + * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items. + */ + toJSON() { + const result = []; + for (let x = this.first; x !== null; x = x.next) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } + + return result; + } + + /** + * Get cache statistics. + * + * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts. + */ + stats() { + return { ...this.#stats }; + } + + /** + * Register callback for evicted items. + * + * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}. + * @returns {LRU} The LRU instance for method chaining. + */ + onEvict(callback) { + this.#onEvict = callback; + + return this; + } + + /** + * Get counts of items by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL counts. + */ + sizeByTTL() { + if (this.ttl === 0) { + return { valid: this.size, expired: 0, noTTL: this.size }; + } + + const now = Date.now(); + let valid = 0; + let expired = 0; + let noTTL = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + noTTL++; + valid++; + } else if (x.expiry > now) { + valid++; + } else { + expired++; + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get keys filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of keys. + */ + keysByTTL() { + if (this.ttl === 0) { + return { valid: this.keys(), expired: [], noTTL: this.keys() }; + } + + const now = Date.now(); + const valid = []; + const expired = []; + const noTTL = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + valid.push(x.key); + noTTL.push(x.key); + } else if (x.expiry > now) { + valid.push(x.key); + } else { + expired.push(x.key); + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get values filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of values. + */ + valuesByTTL() { + const keysByTTL = this.keysByTTL(); + + return { + valid: this.values(keysByTTL.valid), + expired: this.values(keysByTTL.expired), + noTTL: this.values(keysByTTL.noTTL), + }; + } + + /** + * Rebuild the doubly-linked list after cleanup by deleting expired items. + * This removes nodes that were deleted during cleanup. + * + * @private + */ + #rebuildList() { + if (this.size === 0) { + this.first = null; + this.last = null; + return; + } + + const keys = this.keys(); + this.first = null; + this.last = null; + + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + if (item !== null && item !== undefined) { + if (this.first === null) { + this.first = item; + item.prev = null; + } else { + item.prev = this.last; + this.last.next = item; + } + item.next = null; + this.last = item; + } + } + } } /** @@ -364,7 +636,7 @@ class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). + * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 7407329..a33f205 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -13,6 +13,9 @@ * @class LRU */ class LRU { + #stats; + #onEvict; + /** * Creates a new LRU cache instance. * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. @@ -30,6 +33,8 @@ class LRU { this.resetTtl = resetTtl; this.size = 0; this.ttl = ttl; + this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; + this.#onEvict = null; } /** @@ -42,6 +47,11 @@ class LRU { this.items = Object.create(null); this.last = null; this.size = 0; + this.#stats.hits = 0; + this.#stats.misses = 0; + this.#stats.sets = 0; + this.#stats.deletes = 0; + this.#stats.evictions = 0; return this; } @@ -58,6 +68,7 @@ class LRU { if (item !== undefined) { delete this.items[key]; this.size--; + this.#stats.deletes++; this.#unlink(item); @@ -104,6 +115,7 @@ class LRU { const item = this.first; delete this.items[item.key]; + this.#stats.evictions++; if (--this.size === 0) { this.first = null; @@ -113,6 +125,13 @@ class LRU { } item.next = null; + if (this.#onEvict !== null) { + this.#onEvict({ + key: item.key, + value: item.value, + expiry: item.expiry, + }); + } return this; } @@ -154,6 +173,7 @@ class LRU { if (this.ttl > 0) { if (item.expiry <= Date.now()) { this.delete(key); + this.#stats.misses++; return undefined; } @@ -161,10 +181,12 @@ class LRU { // Fast LRU update without full set() overhead this.moveToEnd(item); + this.#stats.hits++; return item.value; } + this.#stats.misses++; return undefined; } @@ -287,6 +309,7 @@ class LRU { this.last = item; } + this.#stats.sets++; return evicted; } @@ -330,6 +353,8 @@ class LRU { this.last = item; } + this.#stats.sets++; + return this; } @@ -338,12 +363,17 @@ class LRU { * When no keys provided, returns all values in LRU order. * When keys provided, order matches the input array. * - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys. * @returns {Array<*>} Array of values corresponding to the keys. */ values(keys) { if (keys === undefined) { - keys = this.keys(); + const result = Array.from({ length: this.size }); + let i = 0; + for (let x = this.first; x !== null; x = x.next) { + result[i++] = x.value; + } + return result; } const result = Array.from({ length: keys.length }); @@ -354,6 +384,248 @@ class LRU { return result; } + + /** + * Iterate over cache items in LRU order (least to most recent). + * Note: This method directly accesses items from the linked list without calling + * get() or peek(), so it does not update LRU order or check TTL expiration during iteration. + * + * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache) + * @param {Object} [thisArg] - Value to use as `this` when executing callback. + * @returns {LRU} The LRU instance for method chaining. + */ + forEach(callback, thisArg) { + for (let x = this.first; x !== null; x = x.next) { + callback.call(thisArg, x.value, x.key, this); + } + + return this; + } + + /** + * Batch retrieve multiple items. + * + * @param {string[]} keys - Array of keys to retrieve. + * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). + */ + getMany(keys) { + const result = Object.create(null); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + result[key] = this.get(key); + } + + return result; + } + + /** + * Batch existence check - returns true if ALL keys exist. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if all keys exist and are not expired. + */ + hasAll(keys) { + for (let i = 0; i < keys.length; i++) { + if (!this.has(keys[i])) { + return false; + } + } + + return true; + } + + /** + * Batch existence check - returns true if ANY key exists. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if any key exists and is not expired. + */ + hasAny(keys) { + for (let i = 0; i < keys.length; i++) { + if (this.has(keys[i])) { + return true; + } + } + + return false; + } + + /** + * Remove expired items without affecting LRU order. + * Unlike get(), this does not move items to the end. + * + * @returns {number} Number of expired items removed. + */ + cleanup() { + if (this.ttl === 0 || this.size === 0) { + return 0; + } + + const now = Date.now(); + let removed = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry <= now) { + const key = x.key; + if (this.items[key] !== undefined) { + delete this.items[key]; + this.size--; + removed++; + } + } + } + + if (removed > 0) { + this.#rebuildList(); + } + + return removed; + } + + /** + * Serialize cache to JSON-compatible format. + * + * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items. + */ + toJSON() { + const result = []; + for (let x = this.first; x !== null; x = x.next) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } + + return result; + } + + /** + * Get cache statistics. + * + * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts. + */ + stats() { + return { ...this.#stats }; + } + + /** + * Register callback for evicted items. + * + * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}. + * @returns {LRU} The LRU instance for method chaining. + */ + onEvict(callback) { + this.#onEvict = callback; + + return this; + } + + /** + * Get counts of items by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL counts. + */ + sizeByTTL() { + if (this.ttl === 0) { + return { valid: this.size, expired: 0, noTTL: this.size }; + } + + const now = Date.now(); + let valid = 0; + let expired = 0; + let noTTL = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + noTTL++; + valid++; + } else if (x.expiry > now) { + valid++; + } else { + expired++; + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get keys filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of keys. + */ + keysByTTL() { + if (this.ttl === 0) { + return { valid: this.keys(), expired: [], noTTL: this.keys() }; + } + + const now = Date.now(); + const valid = []; + const expired = []; + const noTTL = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + valid.push(x.key); + noTTL.push(x.key); + } else if (x.expiry > now) { + valid.push(x.key); + } else { + expired.push(x.key); + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get values filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of values. + */ + valuesByTTL() { + const keysByTTL = this.keysByTTL(); + + return { + valid: this.values(keysByTTL.valid), + expired: this.values(keysByTTL.expired), + noTTL: this.values(keysByTTL.noTTL), + }; + } + + /** + * Rebuild the doubly-linked list after cleanup by deleting expired items. + * This removes nodes that were deleted during cleanup. + * + * @private + */ + #rebuildList() { + if (this.size === 0) { + this.first = null; + this.last = null; + return; + } + + const keys = this.keys(); + this.first = null; + this.last = null; + + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + if (item !== null && item !== undefined) { + if (this.first === null) { + this.first = item; + item.prev = null; + } else { + item.prev = this.last; + this.last.next = item; + } + item.next = null; + this.last = item; + } + } + } } /** @@ -362,7 +634,7 @@ class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). + * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index b2e44d4..10a4bc9 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{constructor(t=0,i=0,s=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=s,this.size=0,this.ttl=i}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){const i=this.items[t];return void 0!==i&&(delete this.items[t],this.size--,this.#t(i),i.prev=null,i.next=null),this}entries(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let s=0;s0&&i.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(i),i.value)}has(t){const i=this.items[t];return void 0!==i&&(0===this.ttl||i.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let i=this.first,s=0;for(;null!==i;)t[s++]=i.key,i=i.next;return t}setWithEvicted(t,i){let s=null,e=this.items[t];return void 0!==e?(e.value=i,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(s={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=e:this.last.next=e,this.last=e),s}set(t,i){let s=this.items[t];return void 0!==s?(s.value=i,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&this.evict(),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:i},1==++this.size?this.first=s:this.last.next=s,this.last=s),this}values(t){void 0===t&&(t=this.keys());const i=Array.from({length:t.length});for(let s=0;s0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/dist/tiny-lru.umd.js b/dist/tiny-lru.umd.js index 8572a7e..d416927 100644 --- a/dist/tiny-lru.umd.js +++ b/dist/tiny-lru.umd.js @@ -13,6 +13,9 @@ * @class LRU */ class LRU { + #stats; + #onEvict; + /** * Creates a new LRU cache instance. * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. @@ -30,6 +33,8 @@ class LRU { this.resetTtl = resetTtl; this.size = 0; this.ttl = ttl; + this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; + this.#onEvict = null; } /** @@ -42,6 +47,11 @@ class LRU { this.items = Object.create(null); this.last = null; this.size = 0; + this.#stats.hits = 0; + this.#stats.misses = 0; + this.#stats.sets = 0; + this.#stats.deletes = 0; + this.#stats.evictions = 0; return this; } @@ -58,6 +68,7 @@ class LRU { if (item !== undefined) { delete this.items[key]; this.size--; + this.#stats.deletes++; this.#unlink(item); @@ -104,6 +115,7 @@ class LRU { const item = this.first; delete this.items[item.key]; + this.#stats.evictions++; if (--this.size === 0) { this.first = null; @@ -113,6 +125,13 @@ class LRU { } item.next = null; + if (this.#onEvict !== null) { + this.#onEvict({ + key: item.key, + value: item.value, + expiry: item.expiry, + }); + } return this; } @@ -154,6 +173,7 @@ class LRU { if (this.ttl > 0) { if (item.expiry <= Date.now()) { this.delete(key); + this.#stats.misses++; return undefined; } @@ -161,10 +181,12 @@ class LRU { // Fast LRU update without full set() overhead this.moveToEnd(item); + this.#stats.hits++; return item.value; } + this.#stats.misses++; return undefined; } @@ -287,6 +309,7 @@ class LRU { this.last = item; } + this.#stats.sets++; return evicted; } @@ -330,6 +353,8 @@ class LRU { this.last = item; } + this.#stats.sets++; + return this; } @@ -338,12 +363,17 @@ class LRU { * When no keys provided, returns all values in LRU order. * When keys provided, order matches the input array. * - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys. * @returns {Array<*>} Array of values corresponding to the keys. */ values(keys) { if (keys === undefined) { - keys = this.keys(); + const result = Array.from({ length: this.size }); + let i = 0; + for (let x = this.first; x !== null; x = x.next) { + result[i++] = x.value; + } + return result; } const result = Array.from({ length: keys.length }); @@ -354,6 +384,248 @@ class LRU { return result; } + + /** + * Iterate over cache items in LRU order (least to most recent). + * Note: This method directly accesses items from the linked list without calling + * get() or peek(), so it does not update LRU order or check TTL expiration during iteration. + * + * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache) + * @param {Object} [thisArg] - Value to use as `this` when executing callback. + * @returns {LRU} The LRU instance for method chaining. + */ + forEach(callback, thisArg) { + for (let x = this.first; x !== null; x = x.next) { + callback.call(thisArg, x.value, x.key, this); + } + + return this; + } + + /** + * Batch retrieve multiple items. + * + * @param {string[]} keys - Array of keys to retrieve. + * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). + */ + getMany(keys) { + const result = Object.create(null); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + result[key] = this.get(key); + } + + return result; + } + + /** + * Batch existence check - returns true if ALL keys exist. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if all keys exist and are not expired. + */ + hasAll(keys) { + for (let i = 0; i < keys.length; i++) { + if (!this.has(keys[i])) { + return false; + } + } + + return true; + } + + /** + * Batch existence check - returns true if ANY key exists. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if any key exists and is not expired. + */ + hasAny(keys) { + for (let i = 0; i < keys.length; i++) { + if (this.has(keys[i])) { + return true; + } + } + + return false; + } + + /** + * Remove expired items without affecting LRU order. + * Unlike get(), this does not move items to the end. + * + * @returns {number} Number of expired items removed. + */ + cleanup() { + if (this.ttl === 0 || this.size === 0) { + return 0; + } + + const now = Date.now(); + let removed = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry <= now) { + const key = x.key; + if (this.items[key] !== undefined) { + delete this.items[key]; + this.size--; + removed++; + } + } + } + + if (removed > 0) { + this.#rebuildList(); + } + + return removed; + } + + /** + * Serialize cache to JSON-compatible format. + * + * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items. + */ + toJSON() { + const result = []; + for (let x = this.first; x !== null; x = x.next) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } + + return result; + } + + /** + * Get cache statistics. + * + * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts. + */ + stats() { + return { ...this.#stats }; + } + + /** + * Register callback for evicted items. + * + * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}. + * @returns {LRU} The LRU instance for method chaining. + */ + onEvict(callback) { + this.#onEvict = callback; + + return this; + } + + /** + * Get counts of items by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL counts. + */ + sizeByTTL() { + if (this.ttl === 0) { + return { valid: this.size, expired: 0, noTTL: this.size }; + } + + const now = Date.now(); + let valid = 0; + let expired = 0; + let noTTL = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + noTTL++; + valid++; + } else if (x.expiry > now) { + valid++; + } else { + expired++; + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get keys filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of keys. + */ + keysByTTL() { + if (this.ttl === 0) { + return { valid: this.keys(), expired: [], noTTL: this.keys() }; + } + + const now = Date.now(); + const valid = []; + const expired = []; + const noTTL = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + valid.push(x.key); + noTTL.push(x.key); + } else if (x.expiry > now) { + valid.push(x.key); + } else { + expired.push(x.key); + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get values filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of values. + */ + valuesByTTL() { + const keysByTTL = this.keysByTTL(); + + return { + valid: this.values(keysByTTL.valid), + expired: this.values(keysByTTL.expired), + noTTL: this.values(keysByTTL.noTTL), + }; + } + + /** + * Rebuild the doubly-linked list after cleanup by deleting expired items. + * This removes nodes that were deleted during cleanup. + * + * @private + */ + #rebuildList() { + if (this.size === 0) { + this.first = null; + this.last = null; + return; + } + + const keys = this.keys(); + this.first = null; + this.last = null; + + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + if (item !== null && item !== undefined) { + if (this.first === null) { + this.first = item; + item.prev = null; + } else { + item.prev = this.last; + this.last.next = item; + } + item.next = null; + this.last = item; + } + } + } } /** @@ -362,7 +634,7 @@ class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). + * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ diff --git a/dist/tiny-lru.umd.min.js b/dist/tiny-lru.umd.min.js index 456c1b0..2e9f1c4 100644 --- a/dist/tiny-lru.umd.min.js +++ b/dist/tiny-lru.umd.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).lru={})}(this,(function(t){"use strict";class e{constructor(t=0,e=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=e}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){const e=this.items[t];return void 0!==e&&(delete this.items[t],this.size--,this.#t(e),e.prev=null,e.next=null),this}entries(t){void 0===t&&(t=this.keys());const e=Array.from({length:t.length});for(let i=0;i0&&e.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(e),e.value)}has(t){const e=this.items[t];return void 0!==e&&(0===this.ttl||e.expiry>Date.now())}#t(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#t(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let e=this.first,i=0;for(;null!==e;)t[i++]=e.key,e=e.next;return t}setWithEvicted(t,e){let i=null,s=this.items[t];return void 0!==s?(s.value=e,this.resetTtl&&(s.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(s)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),s=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=s:this.last.next=s,this.last=s),i}set(t,e){let i=this.items[t];return void 0!==i?(i.value=e,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=i:this.last.next=i,this.last=i),this}values(t){void 0===t&&(t=this.keys());const e=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#e(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#e(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,e=0;for(;null!==s;)t[e++]=s.key,s=s.next;return t}setWithEvicted(t,s){let e=null,i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&(e={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,e}set(t,s){let e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&this.evict(),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let e=this.first;null!==e;e=e.next)t[s++]=e.value;return t}const s=Array.from({length:t.length});for(let e=0;e0&&this.#i(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,e=0,i=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(i++,s++):l.expiry>t?s++:e++;return{valid:s,expired:e,noTTL:i}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],e=[],i=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),i.push(l.key)):l.expiry>t?s.push(l.key):e.push(l.key);return{valid:s,expired:e,noTTL:i}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#i(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiresAt","expiry","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","isNaN","TypeError"],"mappings":";;;;CAAA,SAAAA,EAAAC,GAAA,iBAAAC,SAAA,oBAAAC,OAAAF,EAAAC,SAAA,mBAAAE,QAAAA,OAAAC,IAAAD,OAAA,CAAA,WAAAH,GAAAA,GAAAD,EAAA,oBAAAM,WAAAA,WAAAN,GAAAO,MAAAC,IAAA,CAAA,EAAA,CAAA,CAAAC,MAAA,SAAAP,GAAA,aAOO,MAAMQ,EAUZ,WAAAC,CAAYC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACxCL,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKG,IAAMA,EACXH,KAAKK,SAAWA,EAChBL,KAAKW,KAAO,EACZX,KAAKI,IAAMA,CACZ,CAOA,KAAAQ,GAMC,OALAZ,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKW,KAAO,EAELX,IACR,CAQA,OAAOa,GACN,MAAMC,EAAOd,KAAKO,MAAMM,GAYxB,YAVaE,IAATD,WACId,KAAKO,MAAMM,GAClBb,KAAKW,OAELX,MAAKgB,EAAQF,GAEbA,EAAKG,KAAO,KACZH,EAAKI,KAAO,MAGNlB,IACR,CAUA,OAAAmB,CAAQC,QACML,IAATK,IACHA,EAAOpB,KAAKoB,QAGb,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMZ,EAAMO,EAAKK,GACXX,EAAOd,KAAKO,MAAMM,GACxBQ,EAAOI,GAAK,CAACZ,OAAcE,IAATD,EAAqBA,EAAKY,WAAQX,EACrD,CAEA,OAAOM,CACR,CAOA,KAAAM,GACC,GAAkB,IAAd3B,KAAKW,KACR,OAAOX,KAGR,MAAMc,EAAOd,KAAKM,MAalB,cAXON,KAAKO,MAAMO,EAAKD,KAEH,KAAdb,KAAKW,MACVX,KAAKM,MAAQ,KACbN,KAAKU,KAAO,MAEZV,MAAKgB,EAAQF,GAGdA,EAAKI,KAAO,KAELlB,IACR,CAQA,SAAA4B,CAAUf,GACT,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,EAAqBA,EAAKe,YAASd,CAC3C,CASA,IAAAe,CAAKjB,GACJ,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,EAAqBA,EAAKY,WAAQX,CAC1C,CAQA,GAAAgB,CAAIlB,GACH,MAAMC,EAAOd,KAAKO,MAAMM,GAExB,QAAaE,IAATD,EAEH,OAAId,KAAKI,IAAM,GACVU,EAAKe,QAAUG,KAAKC,WACvBjC,KAAKkC,OAAOrB,IAOdb,KAAKmC,UAAUrB,GAERA,EAAKY,MAId,CAQA,GAAAU,CAAIvB,GACH,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,IAAoC,IAAbd,KAAKI,KAAaU,EAAKe,OAASG,KAAKC,MACpE,CASA,EAAAjB,CAAQF,GACW,OAAdA,EAAKG,OACRH,EAAKG,KAAKC,KAAOJ,EAAKI,MAGL,OAAdJ,EAAKI,OACRJ,EAAKI,KAAKD,KAAOH,EAAKG,MAGnBjB,KAAKM,QAAUQ,IAClBd,KAAKM,MAAQQ,EAAKI,MAGflB,KAAKU,OAASI,IACjBd,KAAKU,KAAOI,EAAKG,KAEnB,CAUA,SAAAkB,CAAUrB,GACLd,KAAKU,OAASI,IAIlBd,MAAKgB,EAAQF,GAEbA,EAAKG,KAAOjB,KAAKU,KACjBI,EAAKI,KAAO,KACZlB,KAAKU,KAAKQ,KAAOJ,EACjBd,KAAKU,KAAOI,EACb,CAOA,IAAAM,GACC,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQxB,KAAKW,OACzC,IAAI0B,EAAIrC,KAAKM,MACTmB,EAAI,EAER,KAAa,OAANY,GACNhB,EAAOI,KAAOY,EAAExB,IAChBwB,EAAIA,EAAEnB,KAGP,OAAOG,CACR,CASA,cAAAiB,CAAezB,EAAKa,GACnB,IAAIa,EAAU,KACVzB,EAAOd,KAAKO,MAAMM,GAmCtB,YAjCaE,IAATD,GACHA,EAAKY,MAAQA,EACT1B,KAAKK,WACRS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI4B,KAAKC,MAAQjC,KAAKI,IAAMJ,KAAKI,KAE3DJ,KAAKmC,UAAUrB,KAEXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,MACtCoC,EAAU,CACT1B,IAAKb,KAAKM,MAAMO,IAChBa,MAAO1B,KAAKM,MAAMoB,MAClBG,OAAQ7B,KAAKM,MAAMuB,QAEpB7B,KAAK2B,SAGNb,EAAOd,KAAKO,MAAMM,GAAO,CACxBgB,OAAQ7B,KAAKI,IAAM,EAAI4B,KAAKC,MAAQjC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLI,KAAMjB,KAAKU,KACXQ,KAAM,KACNQ,SAGmB,KAAd1B,KAAKW,KACVX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKQ,KAAOJ,EAGlBd,KAAKU,KAAOI,GAGNyB,CACR,CASA,GAAAC,CAAI3B,EAAKa,GACR,IAAIZ,EAAOd,KAAKO,MAAMM,GAgCtB,YA9BaE,IAATD,GACHA,EAAKY,MAAQA,EAET1B,KAAKK,WACRS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI4B,KAAKC,MAAQjC,KAAKI,IAAMJ,KAAKI,KAG3DJ,KAAKmC,UAAUrB,KAEXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,KACtCH,KAAK2B,QAGNb,EAAOd,KAAKO,MAAMM,GAAO,CACxBgB,OAAQ7B,KAAKI,IAAM,EAAI4B,KAAKC,MAAQjC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLI,KAAMjB,KAAKU,KACXQ,KAAM,KACNQ,SAGmB,KAAd1B,KAAKW,KACVX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKQ,KAAOJ,EAGlBd,KAAKU,KAAOI,GAGNd,IACR,CAUA,MAAAyC,CAAOrB,QACOL,IAATK,IACHA,EAAOpB,KAAKoB,QAGb,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMX,EAAOd,KAAKO,MAAMa,EAAKK,IAC7BJ,EAAOI,QAAcV,IAATD,EAAqBA,EAAKY,WAAQX,CAC/C,CAEA,OAAOM,CACR,EA2BD5B,EAAAQ,IAAAA,EAAAR,EAAAM,IAdO,SAAaI,EAAM,IAAMC,EAAM,EAAGC,GAAW,GACnD,GAAIqC,MAAMvC,IAAQA,EAAM,EACvB,MAAM,IAAIwC,UAAU,qBAGrB,GAAID,MAAMtC,IAAQA,EAAM,EACvB,MAAM,IAAIuC,UAAU,qBAGrB,GAAwB,kBAAbtC,EACV,MAAM,IAAIsC,UAAU,0BAGrB,OAAO,IAAI1C,EAAIE,EAAKC,EAAKC,EAC1B,CAAA"} \ No newline at end of file +{"version":3,"file":"tiny-lru.umd.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","stats","onEvict","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index dcb1cbb..4459269 100644 --- a/docs/API.md +++ b/docs/API.md @@ -248,6 +248,246 @@ console.log(cache.keys()); // ['b', 'a'] - order unchanged --- +### `forEach(callback, thisArg?)` + +Iterates over cache items in LRU order (least to most recent). + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +cache.forEach((value, key, cache) => { + console.log(key, value); +}); +// Output: +// a 1 +// b 2 +// c 3 +``` + +**Parameters:** + +| Name | Type | Description | +| --------- | ---------- | --------------------------------------- | +| `callback` | `function` | Function to call for each item. Signature: `callback(value, key, cache)` | +| `thisArg` | `Object` | Value to use as `this` when executing callback | + +**Returns:** `LRU` - this instance (for chaining) + +**Note:** This method creates a snapshot of keys before iteration to safely handle cache modifications during iteration. + +--- + +### `getMany(keys)` + +Batch retrieves multiple items. + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +const result = cache.getMany(["a", "c"]); +console.log(result); // { a: 1, c: 3 } +``` + +**Parameters:** + +| Name | Type | Description | +| ------ | ---------- | -------------------- | +| `keys` | `string[]` | Array of keys to get | + +**Returns:** `Object` - Object mapping keys to values (undefined for missing/expired keys) + +**Note:** Returns `undefined` for non-existent or expired keys. + +--- + +### `hasAll(keys)` + +Batch existence check - returns true if ALL keys exist and are not expired. + +```javascript +cache.set("a", 1).set("b", 2); +const result = cache.hasAll(["a", "b"]); +console.log(result); // true + +cache.hasAll(["a", "nonexistent"]); // false +``` + +**Parameters:** + +| Name | Type | Description | +| ------ | ---------- | -------------------- | +| `keys` | `string[]` | Array of keys to check | + +**Returns:** `boolean` - True if all keys exist and are not expired + +**Note:** Returns `true` for empty arrays. + +--- + +### `hasAny(keys)` + +Batch existence check - returns true if ANY key exists and is not expired. + +```javascript +cache.set("a", 1).set("b", 2); +cache.hasAny(["nonexistent", "a"]); // true +cache.hasAny(["nonexistent1", "nonexistent2"]); // false +``` + +**Parameters:** + +| Name | Type | Description | +| ------ | ---------- | -------------------- | +| `keys` | `string[]` | Array of keys to check | + +**Returns:** `boolean` - True if any key exists and is not expired + +**Note:** Returns `false` for empty arrays. + +--- + +### `cleanup()` + +Removes expired items without affecting LRU order. + +```javascript +cache.set("a", 1).set("b", 2); +// ... wait for items to expire +const removed = cache.cleanup(); +console.log(removed); // 2 (number of items removed) +``` + +**Returns:** `number` - Number of expired items removed + +**Note:** Only removes items when TTL is enabled (`ttl > 0`). + +--- + +### `toJSON()` + +Serializes cache to JSON-compatible format. + +```javascript +cache.set("a", 1).set("b", 2); +const json = cache.toJSON(); +console.log(json); +// [ +// { key: "a", value: 1, expiry: 0 }, +// { key: "b", value: 2, expiry: 0 } +// ] + +// Works with JSON.stringify: +const jsonString = JSON.stringify(cache); +``` + +**Returns:** `Array<{key, value, expiry}>` - Array of cache items + +--- + +### `stats()` + +Returns cache statistics. + +```javascript +cache.set("a", 1).set("b", 2); +cache.get("a"); +cache.get("nonexistent"); + +console.log(cache.stats()); +// { +// hits: 1, +// misses: 1, +// sets: 2, +// deletes: 0, +// evictions: 0 +// } +``` + +**Returns:** `Object` - Statistics object with the following properties: +- `hits` - Number of successful get() calls +- `misses` - Number of failed get() calls +- `sets` - Number of set() calls +- `deletes` - Number of delete() calls +- `evictions` - Number of evicted items + +--- + +### `onEvict(callback)` + +Registers a callback function to be called when items are evicted. + +```javascript +cache.onEvict((item) => { + console.log("Evicted:", item.key, item.value); +}); + +cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); +// Evicted: a 1 +``` + +**Parameters:** + +| Name | Type | Description | +| --------- | ---------- | ----------------------------------------------------- | +| `callback` | `function` | Function called with evicted item. Receives `{key, value, expiry}` | + +**Returns:** `LRU` - this instance (for chaining) + +**Note:** Only the last registered callback will be used. Only triggers on explicit eviction via `set()` or `setWithEvicted()` when cache is full, not on TTL expiry. + +--- + +### `sizeByTTL()` + +Returns counts of items by TTL status. + +```javascript +cache.set("a", 1).set("b", 2); +console.log(cache.sizeByTTL()); +// { valid: 2, expired: 0, noTTL: 0 } +``` + +**Returns:** `Object` - Object with three properties: +- `valid` - Number of items that haven't expired +- `expired` - Number of expired items +- `noTTL` - Number of items without TTL (when `ttl=0`) + +**Note:** Items without TTL (expiry === 0) count as both valid and noTTL. + +--- + +### `keysByTTL()` + +Returns keys grouped by TTL status. + +```javascript +cache.set("a", 1).set("b", 2); +console.log(cache.keysByTTL()); +// { valid: ["a", "b"], expired: [], noTTL: ["a", "b"] } +``` + +**Returns:** `Object` - Object with three properties: +- `valid` - Array of valid (non-expired) keys +- `expired` - Array of expired keys +- `noTTL` - Array of keys without TTL (when `ttl=0`) + +--- + +### `valuesByTTL()` + +Returns values grouped by TTL status. + +```javascript +cache.set("a", 1).set("b", 2); +console.log(cache.valuesByTTL()); +// { valid: [1, 2], expired: [], noTTL: [1, 2] } +``` + +**Returns:** `Object` - Object with three properties: +- `valid` - Array of valid (non-expired) values +- `expired` - Array of expired values +- `noTTL` - Array of values without TTL (when `ttl=0`) + +--- + ### `get(key)` Retrieves value and promotes to most recently used. diff --git a/src/lru.js b/src/lru.js index c6b4b31..575e5e5 100644 --- a/src/lru.js +++ b/src/lru.js @@ -6,6 +6,9 @@ * @class LRU */ export class LRU { + #stats; + #onEvict; + /** * Creates a new LRU cache instance. * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. @@ -23,6 +26,8 @@ export class LRU { this.resetTtl = resetTtl; this.size = 0; this.ttl = ttl; + this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; + this.#onEvict = null; } /** @@ -35,6 +40,11 @@ export class LRU { this.items = Object.create(null); this.last = null; this.size = 0; + this.#stats.hits = 0; + this.#stats.misses = 0; + this.#stats.sets = 0; + this.#stats.deletes = 0; + this.#stats.evictions = 0; return this; } @@ -51,6 +61,7 @@ export class LRU { if (item !== undefined) { delete this.items[key]; this.size--; + this.#stats.deletes++; this.#unlink(item); @@ -97,6 +108,7 @@ export class LRU { const item = this.first; delete this.items[item.key]; + this.#stats.evictions++; if (--this.size === 0) { this.first = null; @@ -106,6 +118,13 @@ export class LRU { } item.next = null; + if (this.#onEvict !== null) { + this.#onEvict({ + key: item.key, + value: item.value, + expiry: item.expiry, + }); + } return this; } @@ -147,6 +166,7 @@ export class LRU { if (this.ttl > 0) { if (item.expiry <= Date.now()) { this.delete(key); + this.#stats.misses++; return undefined; } @@ -154,10 +174,12 @@ export class LRU { // Fast LRU update without full set() overhead this.moveToEnd(item); + this.#stats.hits++; return item.value; } + this.#stats.misses++; return undefined; } @@ -280,6 +302,7 @@ export class LRU { this.last = item; } + this.#stats.sets++; return evicted; } @@ -323,6 +346,8 @@ export class LRU { this.last = item; } + this.#stats.sets++; + return this; } @@ -331,12 +356,17 @@ export class LRU { * When no keys provided, returns all values in LRU order. * When keys provided, order matches the input array. * - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys. * @returns {Array<*>} Array of values corresponding to the keys. */ values(keys) { if (keys === undefined) { - keys = this.keys(); + const result = Array.from({ length: this.size }); + let i = 0; + for (let x = this.first; x !== null; x = x.next) { + result[i++] = x.value; + } + return result; } const result = Array.from({ length: keys.length }); @@ -347,6 +377,248 @@ export class LRU { return result; } + + /** + * Iterate over cache items in LRU order (least to most recent). + * Note: This method directly accesses items from the linked list without calling + * get() or peek(), so it does not update LRU order or check TTL expiration during iteration. + * + * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache) + * @param {Object} [thisArg] - Value to use as `this` when executing callback. + * @returns {LRU} The LRU instance for method chaining. + */ + forEach(callback, thisArg) { + for (let x = this.first; x !== null; x = x.next) { + callback.call(thisArg, x.value, x.key, this); + } + + return this; + } + + /** + * Batch retrieve multiple items. + * + * @param {string[]} keys - Array of keys to retrieve. + * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). + */ + getMany(keys) { + const result = Object.create(null); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + result[key] = this.get(key); + } + + return result; + } + + /** + * Batch existence check - returns true if ALL keys exist. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if all keys exist and are not expired. + */ + hasAll(keys) { + for (let i = 0; i < keys.length; i++) { + if (!this.has(keys[i])) { + return false; + } + } + + return true; + } + + /** + * Batch existence check - returns true if ANY key exists. + * + * @param {string[]} keys - Array of keys to check. + * @returns {boolean} True if any key exists and is not expired. + */ + hasAny(keys) { + for (let i = 0; i < keys.length; i++) { + if (this.has(keys[i])) { + return true; + } + } + + return false; + } + + /** + * Remove expired items without affecting LRU order. + * Unlike get(), this does not move items to the end. + * + * @returns {number} Number of expired items removed. + */ + cleanup() { + if (this.ttl === 0 || this.size === 0) { + return 0; + } + + const now = Date.now(); + let removed = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry <= now) { + const key = x.key; + if (this.items[key] !== undefined) { + delete this.items[key]; + this.size--; + removed++; + } + } + } + + if (removed > 0) { + this.#rebuildList(); + } + + return removed; + } + + /** + * Serialize cache to JSON-compatible format. + * + * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items. + */ + toJSON() { + const result = []; + for (let x = this.first; x !== null; x = x.next) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } + + return result; + } + + /** + * Get cache statistics. + * + * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts. + */ + stats() { + return { ...this.#stats }; + } + + /** + * Register callback for evicted items. + * + * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}. + * @returns {LRU} The LRU instance for method chaining. + */ + onEvict(callback) { + this.#onEvict = callback; + + return this; + } + + /** + * Get counts of items by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL counts. + */ + sizeByTTL() { + if (this.ttl === 0) { + return { valid: this.size, expired: 0, noTTL: this.size }; + } + + const now = Date.now(); + let valid = 0; + let expired = 0; + let noTTL = 0; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + noTTL++; + valid++; + } else if (x.expiry > now) { + valid++; + } else { + expired++; + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get keys filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of keys. + */ + keysByTTL() { + if (this.ttl === 0) { + return { valid: this.keys(), expired: [], noTTL: this.keys() }; + } + + const now = Date.now(); + const valid = []; + const expired = []; + const noTTL = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry === 0) { + valid.push(x.key); + noTTL.push(x.key); + } else if (x.expiry > now) { + valid.push(x.key); + } else { + expired.push(x.key); + } + } + + return { valid, expired, noTTL }; + } + + /** + * Get values filtered by TTL status. + * + * @returns {Object} Object with valid, expired, and noTTL arrays of values. + */ + valuesByTTL() { + const keysByTTL = this.keysByTTL(); + + return { + valid: this.values(keysByTTL.valid), + expired: this.values(keysByTTL.expired), + noTTL: this.values(keysByTTL.noTTL), + }; + } + + /** + * Rebuild the doubly-linked list after cleanup by deleting expired items. + * This removes nodes that were deleted during cleanup. + * + * @private + */ + #rebuildList() { + if (this.size === 0) { + this.first = null; + this.last = null; + return; + } + + const keys = this.keys(); + this.first = null; + this.last = null; + + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + if (item !== null && item !== undefined) { + if (this.first === null) { + this.first = item; + item.prev = null; + } else { + item.prev = this.last; + this.last.next = item; + } + item.next = null; + this.last = item; + } + } + } } /** @@ -355,7 +627,7 @@ export class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get(). + * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index 4f82776..e770868 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -780,4 +780,734 @@ describe("LRU Cache", function () { assert.deepEqual(cache.keys(), ["key2", "key3", "key1"]); }); }); + + describe("forEach method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should iterate over all items in LRU order", function () { + const result = []; + cache.forEach((value, key) => { + result.push({ key, value }); + }); + + assert.deepEqual(result, [ + { key: "a", value: 1 }, + { key: "b", value: 2 }, + { key: "c", value: 3 }, + ]); + }); + + it("should iterate without modifying LRU order", function () { + const result = []; + cache.forEach((value, key) => { + result.push(key); + }); + + assert.deepEqual(result, ["a", "b", "c"]); + assert.deepEqual(cache.keys(), ["a", "b", "c"]); + }); + + it("should not modify LRU order when calling get() during forEach", function () { + const result = []; + cache.forEach((value, key) => { + result.push(key); + // Note: calling get() during forEach modifies the linked list + // which breaks the traversal - this is expected behavior + // Users should avoid modifying the cache during iteration + cache.peek(key); // Use peek instead of get to avoid modification + }); + + assert.deepEqual(result, ["a", "b", "c"]); + assert.deepEqual(cache.keys(), ["a", "b", "c"]); + }); + + it("should work with thisArg parameter", function () { + const context = { items: [] }; + cache.forEach(function (value, key) { + this.items.push({ key, value }); + }, context); + + assert.deepEqual(context.items, [ + { key: "a", value: 1 }, + { key: "b", value: 2 }, + { key: "c", value: 3 }, + ]); + }); + + it("should return this for chaining", function () { + const result = cache.forEach(() => {}); + assert.equal(result, cache); + }); + + it("should handle empty cache", function () { + cache.clear(); + const result = []; + cache.forEach((value, key) => result.push({ key, value })); + assert.deepEqual(result, []); + }); + + it("should iterate in correct LRU order after operations", function () { + cache.get("a"); + cache.set("d", 4); + + const result = []; + cache.forEach((value, key) => result.push(key)); + + assert.deepEqual(result, ["b", "c", "a", "d"]); + }); + + it("should allow deleting items during iteration", function () { + const keysToDelete = []; + cache.forEach((value, key) => { + if (value === 2) { + keysToDelete.push(key); + } + }); + + keysToDelete.forEach((key) => cache.delete(key)); + + assert.equal(cache.size, 2); + assert.equal(cache.has("b"), false); + }); + }); + + describe("getMany method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should retrieve multiple values", function () { + const result = cache.getMany(["a", "c"]); + assert.deepEqual(result, { a: 1, c: 3 }); + }); + + it("should handle non-existent keys", function () { + const result = cache.getMany(["a", "nonexistent", "c"]); + assert.deepEqual(result, { a: 1, nonexistent: undefined, c: 3 }); + }); + + it("should handle empty array", function () { + const result = cache.getMany([]); + assert.deepEqual(result, {}); + }); + + it("should expire items when TTL is enabled", async function () { + const ttlCache = new LRU(5, 50); + ttlCache.set("a", 1).set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const result = ttlCache.getMany(["a", "b"]); + assert.equal(result.a, undefined); + assert.equal(result.b, undefined); + }); + + it("should update LRU order for retrieved items", function () { + cache.getMany(["a", "b"]); + + assert.deepEqual(cache.keys(), ["c", "a", "b"]); + }); + + it("should work with single key", function () { + const result = cache.getMany(["b"]); + assert.deepEqual(result, { b: 2 }); + }); + }); + + describe("hasAll method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should return true when all keys exist", function () { + assert.equal(cache.hasAll(["a", "b"]), true); + assert.equal(cache.hasAll(["a", "b", "c"]), true); + }); + + it("should return false when any key is missing", function () { + assert.equal(cache.hasAll(["a", "nonexistent"]), false); + assert.equal(cache.hasAll(["a", "b", "nonexistent"]), false); + }); + + it("should return true for empty array", function () { + assert.equal(cache.hasAll([]), true); + }); + + it("should return false for expired items with TTL", async function () { + const ttlCache = new LRU(5, 50); + ttlCache.set("a", 1).set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert.equal(ttlCache.hasAll(["a", "b"]), false); + }); + + it("should not modify LRU order", function () { + cache.hasAll(["a", "b"]); + assert.deepEqual(cache.keys(), ["a", "b", "c"]); + }); + }); + + describe("hasAny method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should return true when any key exists", function () { + assert.equal(cache.hasAny(["a", "nonexistent"]), true); + assert.equal(cache.hasAny(["nonexistent", "b"]), true); + }); + + it("should return false when no keys exist", function () { + assert.equal(cache.hasAny(["nonexistent1", "nonexistent2"]), false); + }); + + it("should return false for empty array", function () { + assert.equal(cache.hasAny([]), false); + }); + + it("should return false for all expired items with TTL", async function () { + const ttlCache = new LRU(5, 50); + ttlCache.set("a", 1).set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert.equal(ttlCache.hasAny(["a", "b"]), false); + }); + + it("should not modify LRU order", function () { + cache.hasAny(["a", "b"]); + assert.deepEqual(cache.keys(), ["a", "b", "c"]); + }); + }); + + describe("cleanup method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5, 100); + }); + + it("should remove expired items", async function () { + cache.set("a", 1); + cache.set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + const removed = cache.cleanup(); + assert.equal(removed, 2); + assert.equal(cache.size, 0); + }); + + it("should return 0 when no items expired", async function () { + cache.set("a", 1); + cache.set("b", 2); + + const removed = cache.cleanup(); + assert.equal(removed, 0); + assert.equal(cache.size, 2); + }); + + it("should return 0 when TTL is disabled", function () { + const noTtlCache = new LRU(5, 0); + noTtlCache.set("a", 1); + + const removed = noTtlCache.cleanup(); + assert.equal(removed, 0); + }); + + it("should not affect valid items", async function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const removed = cache.cleanup(); + assert.equal(removed, 0); + assert.equal(cache.size, 3); + }); + + it("should handle mixed expired and valid items", async function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + cache.set("d", 4); + cache.set("e", 5); + + const removed = cache.cleanup(); + assert.equal(removed, 3); + assert.equal(cache.size, 2); + assert.equal(cache.has("a"), false); + assert.equal(cache.has("b"), false); + assert.equal(cache.has("c"), false); + assert.equal(cache.has("d"), true); + assert.equal(cache.has("e"), true); + }); + + it("should return 0 for empty cache", function () { + const removed = cache.cleanup(); + assert.equal(removed, 0); + }); + }); + + describe("toJSON method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should serialize cache items", function () { + const json = cache.toJSON(); + assert.strictEqual(Array.isArray(json), true); + assert.equal(json.length, 3); + + assert.deepEqual(json[0], { key: "a", value: 1, expiry: 0 }); + assert.deepEqual(json[1], { key: "b", value: 2, expiry: 0 }); + assert.deepEqual(json[2], { key: "c", value: 3, expiry: 0 }); + }); + + it("should include expiry timestamps when TTL is enabled", function () { + const ttlCache = new LRU(5, 1000); + ttlCache.set("a", 1); + + const json = ttlCache.toJSON(); + assert.ok(json[0].expiry > 0); + }); + + it("should preserve LRU order", function () { + cache.get("a"); + const json = cache.toJSON(); + + assert.deepEqual(json[0].key, "b"); + assert.deepEqual(json[1].key, "c"); + assert.deepEqual(json[2].key, "a"); + }); + + it("should return empty array for empty cache", function () { + cache.clear(); + const json = cache.toJSON(); + assert.deepEqual(json, []); + }); + + it("should be compatible with JSON.stringify", function () { + const jsonStr = JSON.stringify(cache); + const parsed = JSON.parse(jsonStr); + + assert.strictEqual(Array.isArray(parsed), true); + assert.equal(parsed.length, 3); + }); + }); + + describe("stats method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + cache.set("a", 1).set("b", 2).set("c", 3); + }); + + it("should return statistics object", function () { + const stats = cache.stats(); + assert.strictEqual(typeof stats, "object"); + assert.ok("hits" in stats); + assert.ok("misses" in stats); + assert.ok("sets" in stats); + assert.ok("deletes" in stats); + assert.ok("evictions" in stats); + }); + + it("should track set operations", function () { + cache.set("d", 4); + const stats = cache.stats(); + assert.equal(stats.sets, 4); + }); + + it("should track get hits", function () { + cache.get("a"); + cache.get("b"); + const stats = cache.stats(); + assert.equal(stats.hits, 2); + }); + + it("should track get misses", function () { + cache.get("nonexistent"); + const stats = cache.stats(); + assert.equal(stats.misses, 1); + }); + + it("should track delete operations", function () { + cache.delete("a"); + const stats = cache.stats(); + assert.equal(stats.deletes, 1); + }); + + it("should track evictions", function () { + cache.set("d", 4); + cache.set("e", 5); + const stats = cache.stats(); + assert.equal(stats.evictions, 2); + }); + + it("should return a copy, not the internal object", function () { + const stats1 = cache.stats(); + cache.set("d", 4); + const stats2 = cache.stats(); + + assert.equal(stats1.sets, 3); + assert.equal(stats2.sets, 4); + }); + + it("should track correct hits/misses with has()", function () { + cache.has("a"); + cache.has("nonexistent"); + const stats = cache.stats(); + + assert.equal(stats.hits, 0); + assert.equal(stats.misses, 0); + }); + + it("should reset on clear()", function () { + cache.get("a"); + cache.clear(); + const stats = cache.stats(); + + assert.equal(stats.hits, 0); + assert.equal(stats.misses, 0); + assert.equal(stats.sets, 0); + assert.equal(stats.deletes, 0); + assert.equal(stats.evictions, 0); + }); + + it("should track evictions with onEvict callback", function () { + let evictedKey; + cache.onEvict((item) => { + evictedKey = item.key; + }); + + cache.set("d", 4); + cache.set("e", 5); + + const stats = cache.stats(); + assert.equal(stats.evictions, 2); + assert.equal(evictedKey, "b"); + }); + }); + + describe("onEvict method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + }); + + it("should register evict callback", function () { + let evicted = null; + cache.onEvict((item) => { + evicted = item; + }); + + cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); + + assert.ok(evicted !== null); + assert.equal(evicted.key, "a"); + assert.equal(evicted.value, 1); + }); + + it("should receive correct item shape", function () { + cache.set("a", 1).set("b", 2).set("c", 3); + + let receivedItem; + cache.onEvict((item) => { + receivedItem = item; + }); + + cache.set("d", 4); + + assert.equal(receivedItem.key, "a"); + assert.equal(receivedItem.value, 1); + assert.ok("expiry" in receivedItem); + }); + + it("should work with TTL expiry via cleanup", async function () { + const ttlCache = new LRU(5, 50); + ttlCache.set("a", 1).set("b", 2).set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 100)); + const removed = ttlCache.cleanup(); + + assert.equal(removed, 3); + assert.equal(ttlCache.size, 0); + }); + + it("should only have last registered callback", function () { + let firstCalled = false; + let secondCalled = false; + + cache.onEvict(() => { + firstCalled = true; + }); + + cache.onEvict(() => { + secondCalled = true; + }); + + cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); + + assert.equal(firstCalled, false); + assert.equal(secondCalled, true); + }); + + it("should return this for chaining", function () { + const result = cache.onEvict(() => {}); + assert.equal(result, cache); + }); + + it("should handle multiple evictions", function () { + const evictedItems = []; + cache.onEvict((item) => { + evictedItems.push(item); + }); + + cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4).set("e", 5).set("f", 6); + + assert.equal(evictedItems.length, 3); + assert.equal(evictedItems[0].key, "a"); + assert.equal(evictedItems[1].key, "b"); + assert.equal(evictedItems[2].key, "c"); + }); + }); + + describe("sizeByTTL method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(10, 100); + }); + + it("should return counts for valid, expired, and noTTL items", function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + assert.deepEqual(cache.sizeByTTL(), { valid: 3, expired: 0, noTTL: 0 }); + }); + + it("should count expired items correctly", async function () { + cache.set("a", 1); + cache.set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + const counts = cache.sizeByTTL(); + assert.equal(counts.valid, 0); + assert.equal(counts.expired, 2); + assert.equal(counts.noTTL, 0); + }); + + it("should count noTTL items when ttl=0", function () { + const noTtlCache = new LRU(10, 0); + noTtlCache.set("a", 1).set("b", 2); + + assert.deepEqual(noTtlCache.sizeByTTL(), { valid: 2, expired: 0, noTTL: 2 }); + }); + + it("should handle mixed expired and valid items", async function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + cache.set("d", 4); + cache.set("e", 5); + + const counts = cache.sizeByTTL(); + assert.equal(counts.valid, 2); + assert.equal(counts.expired, 3); + assert.equal(counts.noTTL, 0); + }); + + it("should return zero counts for empty cache", function () { + assert.deepEqual(cache.sizeByTTL(), { valid: 0, expired: 0, noTTL: 0 }); + }); + + it("should not modify cache state", function () { + cache.set("a", 1).set("b", 2); + const originalSize = cache.size; + + cache.sizeByTTL(); + + assert.equal(cache.size, originalSize); + }); + }); + + describe("keysByTTL method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(10, 100); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + }); + + it("should return keys grouped by TTL status", function () { + const result = cache.keysByTTL(); + assert.ok("valid" in result); + assert.ok("expired" in result); + assert.ok("noTTL" in result); + }); + + it("should return all keys in valid array when none expired", function () { + const result = cache.keysByTTL(); + assert.deepEqual(result.valid.sort(), ["a", "b", "c"]); + assert.deepEqual(result.expired, []); + }); + + it("should return correct expired keys after TTL", async function () { + await new Promise((resolve) => setTimeout(resolve, 150)); + const result = cache.keysByTTL(); + + assert.deepEqual(result.valid, []); + assert.deepEqual(result.expired.sort(), ["a", "b", "c"]); + }); + + it("should handle noTTL when ttl=0", function () { + const noTtlCache = new LRU(10, 0); + noTtlCache.set("a", 1).set("b", 2); + + const result = noTtlCache.keysByTTL(); + assert.deepEqual(result.valid.sort(), ["a", "b"]); + assert.deepEqual(result.expired, []); + assert.deepEqual(result.noTTL.sort(), ["a", "b"]); + }); + + it("should return empty arrays for empty cache", function () { + cache.clear(); + const result = cache.keysByTTL(); + assert.deepEqual(result.valid, []); + assert.deepEqual(result.expired, []); + assert.deepEqual(result.noTTL, []); + }); + + it("should preserve key order", function () { + const result = cache.keysByTTL(); + assert.deepEqual(result.valid, ["a", "b", "c"]); + }); + + it("should handle mixed expired and valid", async function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + cache.set("d", 4); + cache.set("e", 5); + + const result = cache.keysByTTL(); + assert.equal(result.valid.length, 2); + assert.equal(result.expired.length, 3); + assert.ok(result.valid.includes("d")); + assert.ok(result.valid.includes("e")); + assert.ok(result.expired.includes("a")); + assert.ok(result.expired.includes("b")); + assert.ok(result.expired.includes("c")); + }); + }); + + describe("valuesByTTL method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(10, 100); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + }); + + it("should return values grouped by TTL status", function () { + const result = cache.valuesByTTL(); + assert.ok("valid" in result); + assert.ok("expired" in result); + assert.ok("noTTL" in result); + }); + + it("should return all values in valid array when none expired", function () { + const result = cache.valuesByTTL(); + assert.deepEqual(result.valid, [1, 2, 3]); + assert.deepEqual(result.expired, []); + }); + + it("should return correct expired values after TTL", async function () { + await new Promise((resolve) => setTimeout(resolve, 150)); + const result = cache.valuesByTTL(); + + assert.deepEqual(result.valid, []); + assert.deepEqual(result.expired.sort(), [1, 2, 3]); + }); + + it("should handle noTTL when ttl=0", function () { + const noTtlCache = new LRU(10, 0); + noTtlCache.set("a", 1).set("b", 2); + + const result = noTtlCache.valuesByTTL(); + assert.deepEqual(result.valid.sort(), [1, 2]); + assert.deepEqual(result.expired, []); + assert.deepEqual(result.noTTL.sort(), [1, 2]); + }); + + it("should return empty arrays for empty cache", function () { + cache.clear(); + const result = cache.valuesByTTL(); + assert.deepEqual(result.valid, []); + assert.deepEqual(result.expired, []); + assert.deepEqual(result.noTTL, []); + }); + + it("should preserve value order matching keys", function () { + const result = cache.valuesByTTL(); + assert.deepEqual(result.valid, [1, 2, 3]); + }); + + it("should handle mixed expired and valid", async function () { + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + cache.set("d", 4); + cache.set("e", 5); + + const result = cache.valuesByTTL(); + assert.equal(result.valid.length, 2); + assert.equal(result.expired.length, 3); + assert.ok(result.valid.includes(4)); + assert.ok(result.valid.includes(5)); + assert.ok(result.expired.includes(1)); + assert.ok(result.expired.includes(2)); + assert.ok(result.expired.includes(3)); + }); + }); }); diff --git a/types/lru.d.ts b/types/lru.d.ts index 3429ff5..40f3e88 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -58,7 +58,7 @@ export class LRU { readonly last: LRUItem | null; /** Maximum number of items to store (0 = unlimited) */ readonly max: number; - /** Whether to reset TTL on each get() operation */ + /** Whether to reset TTL on set() operations */ readonly resetTtl: boolean; /** Current number of items in the cache */ readonly size: number; @@ -113,6 +113,105 @@ export class LRU { */ has(key: any): boolean; + /** + * Iterate over cache items in LRU order (least to most recent). + * @param callback Function to call for each item. Signature: callback(value, key, cache) + * @param thisArg Value to use as `this` when executing callback + * @returns The LRU instance for method chaining + */ + forEach(callback: (value: T, key: any, cache: this) => void, thisArg?: any): this; + + /** + * Retrieve a value from the cache by key without updating LRU order. + * Note: Does not perform TTL checks or remove expired items. + * @param key The key to retrieve + * @returns The value associated with the key, or undefined if not found + */ + peek(key: any): T | undefined; + + /** + * Batch retrieve multiple items. + * @param keys Array of keys to retrieve + * @returns Object mapping keys to values (undefined for missing/expired keys) + */ + getMany(keys: any[]): Record; + + /** + * Batch existence check - returns true if ALL keys exist. + * @param keys Array of keys to check + * @returns True if all keys exist and are not expired + */ + hasAll(keys: any[]): boolean; + + /** + * Batch existence check - returns true if ANY key exists. + * @param keys Array of keys to check + * @returns True if any key exists and is not expired + */ + hasAny(keys: any[]): boolean; + + /** + * Remove expired items without affecting LRU order. + * Unlike get(), this does not move items to the end. + * @returns Number of expired items removed + */ + cleanup(): number; + + /** + * Serialize cache to JSON-compatible format. + * @returns Array of cache items with key, value, and expiry + */ + toJSON(): Array<{ key: any; value: T; expiry: number }>; + + /** + * Get cache statistics. + * @returns Statistics object with hits, misses, sets, deletes, evictions counts + */ + stats(): { + hits: number; + misses: number; + sets: number; + deletes: number; + evictions: number; + }; + + /** + * Register callback for evicted items. + * @param callback Function called when item is evicted. Receives {key, value, expiry} + * @returns The LRU instance for method chaining + */ + onEvict(callback: (item: { key: any; value: T; expiry: number }) => void): this; + + /** + * Get counts of items by TTL status. + * @returns Object with valid, expired, and noTTL counts + */ + sizeByTTL(): { + valid: number; + expired: number; + noTTL: number; + }; + + /** + * Get keys filtered by TTL status. + * @returns Object with valid, expired, and noTTL arrays of keys + */ + keysByTTL(): { + valid: any[]; + expired: any[]; + noTTL: any[]; + }; + + /** + * Get values filtered by TTL status. + * @returns Object with valid, expired, and noTTL arrays of values + */ + valuesByTTL(): { + valid: (T | undefined)[]; + expired: (T | undefined)[]; + noTTL: (T | undefined)[]; + }; + /** * Returns an array of all keys in the cache, ordered from least to most recently used. * @returns Array of keys in LRU order From b60f5667588ef18f7810d12093bcda1c29db924a Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 13:54:41 -0400 Subject: [PATCH 03/55] Add tests for noTTL items in sizeByTTL/keysByTTL - Added test for items with expiry=0 when ttl>0 (manual expiry set) - Covers uncovered lines 534-535 and 563-564 in sizeByTTL and keysByTTL - 100% line coverage achieved --- coverage.txt | 4 ++-- tests/unit/lru.test.js | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/coverage.txt b/coverage.txt index 6f12e04..e5c0881 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 99.38 | 97.73 | 100.00 | 534-535 563-564 +ℹ lru.js | 100.00 | 99.25 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 99.38 | 97.73 | 100.00 | +ℹ all files | 100.00 | 99.25 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index e770868..84d1fc3 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -1331,6 +1331,21 @@ describe("LRU Cache", function () { assert.deepEqual(noTtlCache.sizeByTTL(), { valid: 2, expired: 0, noTTL: 2 }); }); + it("should handle items with expiry=0 when ttl>0 (manual expiry set)", function () { + const cache = new LRU(10, 100); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + cache.items["a"].expiry = 0; + cache.items["b"].expiry = 0; + + const counts = cache.sizeByTTL(); + assert.equal(counts.valid, 3); + assert.equal(counts.expired, 0); + assert.equal(counts.noTTL, 2); + }); + it("should handle mixed expired and valid items", async function () { cache.set("a", 1); cache.set("b", 2); @@ -1402,6 +1417,24 @@ describe("LRU Cache", function () { assert.deepEqual(result.noTTL.sort(), ["a", "b"]); }); + it("should handle items with expiry=0 when ttl>0 (manual expiry set)", function () { + const cache = new LRU(10, 100); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + cache.items["a"].expiry = 0; + cache.items["b"].expiry = 0; + + const result = cache.keysByTTL(); + assert.equal(result.valid.length, 3); + assert.equal(result.expired.length, 0); + assert.deepEqual(result.noTTL.sort(), ["a", "b"]); + assert.ok(result.valid.includes("a")); + assert.ok(result.valid.includes("b")); + assert.ok(result.valid.includes("c")); + }); + it("should return empty arrays for empty cache", function () { cache.clear(); const result = cache.keysByTTL(); @@ -1459,6 +1492,24 @@ describe("LRU Cache", function () { assert.deepEqual(result.expired, []); }); + it("should handle items with expiry=0 when ttl>0 (manual expiry set)", function () { + const cache = new LRU(10, 100); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + cache.items["a"].expiry = 0; + cache.items["b"].expiry = 0; + + const result = cache.valuesByTTL(); + assert.equal(result.valid.length, 3); + assert.equal(result.expired.length, 0); + assert.deepEqual(result.noTTL.sort(), [1, 2]); + assert.ok(result.valid.includes(1)); + assert.ok(result.valid.includes(2)); + assert.ok(result.valid.includes(3)); + }); + it("should return correct expired values after TTL", async function () { await new Promise((resolve) => setTimeout(resolve, 150)); const result = cache.valuesByTTL(); From 3ddaeba1f746c93d9642ee67c50d3e52c8e4dddc Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 13:55:29 -0400 Subject: [PATCH 04/55] Docs: sort Methods table alphabetically --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7c203eb..906af51 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ const user: User | undefined = cache.get("user:1"); | Method | Description | | ----------------------- | ---------------------------------------------- | +| `cleanup()` | Remove expired items without LRU update. | | `clear()` | Remove all items. Returns `this` for chaining. | | `delete(key)` | Remove an item. Returns `this` for chaining. | | `entries(keys?)` | Get `[key, value]` pairs in LRU order. | @@ -128,17 +129,16 @@ const user: User | undefined = cache.get("user:1"); | `hasAll(keys)` | Check if ALL keys exist. | | `hasAny(keys)` | Check if ANY key exists. | | `keys()` | Get all keys in LRU order (oldest first). | -| `cleanup()` | Remove expired items without LRU update. | +| `keysByTTL()` | Get keys by TTL status. | | `peek(key)` | Retrieve a value without LRU update. | | `set(key, value)` | Store a value. Returns `this` for chaining. | | `setWithEvicted(key, value)` | Store value, return evicted item if full. | -| `values(keys?)` | Get all values, or values for specific keys. | -| `toJSON()` | Serialize cache to JSON format. | -| `stats()` | Get cache statistics. | -| `onEvict(callback)` | Register eviction callback. | | `sizeByTTL()` | Get counts by TTL status. | -| `keysByTTL()` | Get keys by TTL status. | +| `stats()` | Get cache statistics. | +| `toJSON()` | Serialize cache to JSON format. | +| `values(keys?)` | Get all values, or values for specific keys. | | `valuesByTTL()` | Get values by TTL status. | +| `onEvict(callback)` | Register eviction callback. | ### Properties From a2f36520d2c1a9ba58c48931409e699531efc258 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:02:04 -0400 Subject: [PATCH 05/55] README: fix badge order and remove duplicate tag line --- README.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 906af51..0115c2a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,12 @@ [![npm version](https://img.shields.io/npm/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) [![npm downloads](https://img.shields.io/npm/dm/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) -[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions) -[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/avoidwork/tiny-lru) +[![GitHub stars](https://img.shields.io/github/stars/avoidwork/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/stargazers) +[![License](https://img.shields.io/npm/l/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/blob/master/LICENSE) +[![Node.js version](https://img.shields.io/node/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) +[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/avoidwork/tiny-lru/actions?query=branch%3Amaster) +[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://app.codecov.io/gh/avoidwork/tiny-lru) +[![npm](https://nodei.co/npm/tiny-lru.png?downloads=true&stars=true)](https://www.npmjs.com/package/tiny-lru) A fast, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support. @@ -40,6 +44,21 @@ cache.size; // 3 cache.keys(); // ['a', 'b', 'c'] (LRU order) ``` +### TypeScript + +```typescript +import { LRU } from "tiny-lru"; + +interface User { + id: number; + name: string; +} + +const cache = new LRU(100); +cache.set("user:1", { id: 1, name: "Alice" }); +const user: User | undefined = cache.get("user:1"); +``` + ## With TTL (Time-to-Live) Items can automatically expire after a set time: From d30a7ff5ef2def7539f6ca71d9091add14dd984d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:05:10 -0400 Subject: [PATCH 06/55] README: complete rewrite based on source documentation - Compress badges to first 8 lines after title - Add new badges: GitHub stars, Codecov - Organize content with clear sections - Add modern usage patterns (LLM caching, session auth) - Improve 'Why Tiny LRU?' comparison table - Add detailed API reference with all methods - Include TypeScript examples - Add security best practices - Remove duplicate TypeScript section - Standardize method descriptions - Add comprehensive code examples Fixes: branch name references (master vs main) --- README.md | 299 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 224 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0115c2a..2a0d124 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,23 @@ [![Node.js version](https://img.shields.io/node/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) [![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/avoidwork/tiny-lru/actions?query=branch%3Amaster) [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://app.codecov.io/gh/avoidwork/tiny-lru) + [![npm](https://nodei.co/npm/tiny-lru.png?downloads=true&stars=true)](https://www.npmjs.com/package/tiny-lru) -A fast, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support. +A high-performance, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support. ## What is an LRU Cache? Think of an LRU cache like a limited-size bookshelf. When you add a new book and the shelf is full, you remove the **least recently used** book to make room. Every time you read a book, it moves to the front. This pattern is perfect for caching where you want to keep the most frequently accessed items. +The tiny-lru library provides: +- **O(1)** operations for get, set, delete, and has +- Optional **TTL (Time-To-Live)** support for automatic expiration +- **Zero dependencies** - pure JavaScript +- **100% test coverage** - fully tested and reliable +- **TypeScript support** - full type definitions included +- **~2.2 KB** minified and gzipped (compared to ~15 KB for lru-cache) + ## Installation ```bash @@ -88,6 +97,8 @@ cache.set("key", "new value"); // TTL resets - Function memoization - Session storage with expiration - Rate limiting +- LLM response caching +- Database query result caching - Any scenario where you want to limit memory usage **Not ideal for:** @@ -99,6 +110,8 @@ cache.set("key", "new value"); // TTL resets ### Factory Function: `lru(max?, ttl?, resetTtl?)` +Creates a new LRU cache instance with parameter validation. + ```javascript import { lru } from "tiny-lru"; @@ -108,66 +121,72 @@ const cache3 = lru(100, 30000); // 100 items, 30s TTL const cache4 = lru(100, 60000, true); // with resetTtl enabled ``` +**Parameters:** + +| Name | Type | Default | Description | +| ---------- | --------- | ------- | ---------------------------------------------------------------- | +| `max` | `number` | `1000` | Maximum items. `0` = unlimited. Must be >= 0. | +| `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. Must be >= 0. | +| `resetTtl` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | + +**Throws:** `TypeError` if parameters are invalid + ### Class: `new LRU(max?, ttl?, resetTtl?)` +Creates an LRU cache instance without parameter validation. + ```javascript import { LRU } from "tiny-lru"; const cache = new LRU(100, 5000); ``` -### TypeScript +**Parameters:** -```typescript -import { LRU } from "tiny-lru"; +| Name | Type | Default | Description | +| ---------- | --------- | ------- | -------------------------------------------------- | +| `max` | `number` | `0` | Maximum items. `0` = unlimited. | +| `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. | +| `resetTtl` | `boolean` | `false` | Reset TTL when updating via `set()` | -interface User { - id: number; - name: string; -} +### Properties -const cache = new LRU(100); -cache.set("user:1", { id: 1, name: "Alice" }); -const user: User | undefined = cache.get("user:1"); -``` +| Property | Type | Description | +| --------- | ---------------- | ------------------------------------------ | +| `first` | `object | null` | Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | +| `last` | `object | null` | Most recently used item | +| `max` | `number` | Maximum items allowed | +| `size` | `number` | Current number of items | +| `ttl` | `number` | Time-to-live in milliseconds | +| `resetTtl`| `boolean` | Whether TTL resets on `set()` updates | ### Methods -| Method | Description | -| ----------------------- | ---------------------------------------------- | -| `cleanup()` | Remove expired items without LRU update. | -| `clear()` | Remove all items. Returns `this` for chaining. | -| `delete(key)` | Remove an item. Returns `this` for chaining. | -| `entries(keys?)` | Get `[key, value]` pairs in LRU order. | -| `evict()` | Remove the least recently used item. | -| `expiresAt(key)` | Get expiration timestamp for a key. | -| `forEach(callback, thisArg?)` | Iterate over items in LRU order. | -| `get(key)` | Retrieve a value. Moves item to most recent. | -| `getMany(keys)` | Batch retrieve multiple items. | -| `has(key)` | Check if key exists and is not expired. | -| `hasAll(keys)` | Check if ALL keys exist. | -| `hasAny(keys)` | Check if ANY key exists. | -| `keys()` | Get all keys in LRU order (oldest first). | -| `keysByTTL()` | Get keys by TTL status. | -| `peek(key)` | Retrieve a value without LRU update. | -| `set(key, value)` | Store a value. Returns `this` for chaining. | -| `setWithEvicted(key, value)` | Store value, return evicted item if full. | -| `sizeByTTL()` | Get counts by TTL status. | -| `stats()` | Get cache statistics. | -| `toJSON()` | Serialize cache to JSON format. | -| `values(keys?)` | Get all values, or values for specific keys. | -| `valuesByTTL()` | Get values by TTL status. | -| `onEvict(callback)` | Register eviction callback. | - -### Properties - -| Property | Type | Description | -| -------- | ------ | ---------------------------- | -| `first` | object | Least recently used item | -| `last` | object | Most recently used item | -| `max` | number | Maximum items allowed | -| `size` | number | Current number of items | -| `ttl` | number | Time-to-live in milliseconds | +| Method | Description | +| --------------------------- | ---------------------------------------------- | +| `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | +| `clear()` | Remove all items. Returns `this` for chaining. | +| `delete(key)` | Remove an item by key. Returns `this` for chaining. | +| `entries(keys?)` | Get `[key, value]` pairs in LRU order. | +| `evict()` | Remove the least recently used item. Returns `this` for chaining. | +| `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | +| `forEach(callback, thisArg?)` | Iterate over items in LRU order. Returns `this` for chaining. | +| `get(key)` | Retrieve a value. Moves item to most recent. Returns value or `undefined`. | +| `getMany(keys)` | Batch retrieve multiple items. Returns object mapping keys to values. | +| `has(key)` | Check if key exists and is not expired. Returns `boolean`. | +| `hasAll(keys)` | Check if ALL keys exist. Returns `boolean`. | +| `hasAny(keys)` | Check if ANY key exists. Returns `boolean`. | +| `keys()` | Get all keys in LRU order (oldest first). Returns `string[]`. | +| `keysByTTL()` | Get keys by TTL status. Returns `{valid, expired, noTTL}`. | +| `peek(key)` | Retrieve a value without LRU update. Returns value or `undefined`. | +| `set(key, value)` | Store a value. Returns `this` for chaining. | +| `setWithEvicted(key, value)` | Store value, return evicted item if full. Returns `{key, value, expiry} | null`. | +| `sizeByTTL()` | Get counts by TTL status. Returns `{valid, expired, noTTL}`. | +| `stats()` | Get cache statistics. Returns `{hits, misses, sets, deletes, evictions}`. | +| `toJSON()` | Serialize cache to JSON format. Returns array of items. | +| `values(keys?)` | Get all values, or values for specific keys. Returns array of values. | +| `valuesByTTL()` | Get values by TTL status. Returns `{valid, expired, noTTL}`. | +| `onEvict(callback)` | Register eviction callback. Returns `this` for chaining. | ## Common Patterns @@ -175,19 +194,19 @@ const user: User | undefined = cache.get("user:1"); ```javascript function memoize(fn, maxSize = 100) { - const cache = lru(maxSize); + const cache = lru(maxSize); - return function (...args) { - const key = JSON.stringify(args); + return function (...args) { + const key = JSON.stringify(args); - if (cache.has(key)) { - return cache.get(key); - } + if (cache.has(key)) { + return cache.get(key); + } - const result = fn(...args); - cache.set(key, result); - return result; - }; + const result = fn(...args); + cache.set(key, result); + return result; + }; } // Cache expensive computations @@ -200,21 +219,21 @@ fib(100); // even faster - from cache ```javascript async function getUser(userId) { - const cache = lru(1000, 60000); // 1 minute cache + const cache = lru(1000, 60000); // 1 minute cache - // Check cache first - const cached = cache.get(`user:${userId}`); - if (cached) { - return cached; - } + // Check cache first + const cached = cache.get(`user:${userId}`); + if (cached) { + return cached; + } - // Fetch from database - const user = await db.users.findById(userId); + // Fetch from database + const user = await db.users.findById(userId); - // Store in cache - cache.set(`user:${userId}`, user); + // Store in cache + cache.set(`user:${userId}`, user); - return user; + return user; } ``` @@ -257,15 +276,103 @@ const slowFunc = _.memoize(expensiveOperation); slowFunc.cache.max = 100; // Configure cache size ``` +### Session and Authentication Caching + +```javascript +import { LRU } from "tiny-lru"; + +class AuthCache { + constructor() { + // Session cache: 30 minutes with TTL reset on access + this.sessions = new LRU(10000, 1800000, true); + // Token validation cache: 5 minutes, no reset + this.tokens = new LRU(5000, 300000, false); + // Permission cache: 15 minutes + this.permissions = new LRU(5000, 900000); + } + + cacheSession(sessionId, userData, domain = "app") { + const key = `${domain}:session:${sessionId}`; + this.sessions.set(key, { + userId: userData.userId, + permissions: userData.permissions, + loginTime: Date.now(), + lastActivity: Date.now(), + }); + } + + getSession(sessionId, domain = "app") { + const key = `${domain}:session:${sessionId}`; + return this.sessions.get(key); + } +} +``` + +### LLM Response Caching + +```javascript +import { LRU } from "tiny-lru"; + +class LLMCache { + constructor() { + // Cache up to 1000 responses for 1 hour + this.cache = new LRU(1000, 3600000); // 1 hour TTL + } + + async getResponse(model, prompt, params = {}) { + const key = this.generateKey(model, prompt, params); + + // Check cache first + const cached = this.cache.get(key); + if (cached) { + return { ...cached, fromCache: true }; + } + + // Make expensive API call + const response = await this.callLLMAPI(model, prompt, params); + + // Cache the response + this.cache.set(key, { + response: response.text, + tokens: response.tokens, + timestamp: Date.now(), + }); + + return { ...response, fromCache: false }; + } + + generateKey(model, prompt, params = {}) { + const paramsHash = this.hashObject(params); + const promptHash = this.hashString(prompt); + return `llm:${model}:${promptHash}:${paramsHash}`; + } + + hashString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash).toString(36); + } + + hashObject(obj) { + return this.hashString(JSON.stringify(obj, Object.keys(obj).sort())); + } +} +``` + ## Why Tiny LRU? -| Feature | tiny-lru | lru-cache | -| ---------------- | ------------ | --------- | -| Bundle size | ~2.2 KB | ~15 KB | -| O(1) operations | ✅ | ✅ | -| TTL support | ✅ | ✅ | -| TypeScript | ✅ | ✅ | -| Zero dependencies| ✅ | ❌ | +| Feature | tiny-lru | lru-cache | quick-lru | +| ---------------- | ------------ | --------- | ----------- | +| Bundle size | ~2.2 KB | ~15 KB | ~1.8 KB | +| O(1) operations | ✅ | ✅ | ✅ | +| TTL support | ✅ | ✅ | ❌ | +| TypeScript | ✅ | ✅ | ✅ | +| Zero dependencies| ✅ | ❌ | ✅ | +| 100% coverage | ✅ | ❌ | ❌ | ## Performance @@ -276,6 +383,16 @@ All core operations are O(1): - **Delete**: Remove items - **Has**: Quick existence check +### Benchmarks + +Run our comprehensive benchmark suite to see performance characteristics: + +```bash +npm run benchmark:all +``` + +See [benchmarks/README.md](benchmarks/README.md) for more details. + ## Development ```bash @@ -287,6 +404,16 @@ npm run build # Build distribution files npm run coverage # Generate test coverage report ``` +### Build Output + +Build produces multiple module formats: +- `dist/tiny-lru.js` - ES Modules +- `dist/tiny-lru.cjs` - CommonJS +- `dist/tiny-lru.min.js` - Minified ESM +- `dist/tiny-lru.umd.js` - UMD +- `dist/tiny-lru.umd.min.js` - Minified UMD +- `types/lru.d.ts` - TypeScript definitions + ## Test Coverage | Metric | Coverage | @@ -304,6 +431,28 @@ npm run coverage # Generate test coverage report 5. Push to the branch (`git push origin feature/amazing-feature`) 6. Open a Pull Request +## Security + +### Multi-Domain Key Convention + +Implement a hierarchical key naming convention to prevent cross-domain data leakage: + +``` +{domain}:{service}:{resource}:{identifier}[:{version}] +``` + +Example domains: +- User-related: `usr:profile:data:12345` +- Authentication: `auth:login:session:abc123` +- External API: `api:response:endpoint:hash` +- Database: `db:query:sqlhash:paramshash` +- Application: `app:cache:feature:value` +- System: `sys:config:feature:version` +- Analytics: `analytics:event:user:session` +- ML/AI: `ml:llm:response:gpt4-hash` + +See [docs/TECHNICAL_DOCUMENTATION.md](docs/TECHNICAL_DOCUMENTATION.md) for more security best practices. + ## License Copyright (c) 2026, Jason Mulligan From 92c78cc3edd21d87976717d4da9824f2eeaa8b56 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:07:06 -0400 Subject: [PATCH 07/55] README: fix build status badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a0d124..e4137b2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![GitHub stars](https://img.shields.io/github/stars/avoidwork/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/stargazers) [![License](https://img.shields.io/npm/l/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/blob/master/LICENSE) [![Node.js version](https://img.shields.io/node/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) -[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/avoidwork/tiny-lru/actions?query=branch%3Amaster) +[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions?query=workflow%3Aci) [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://app.codecov.io/gh/avoidwork/tiny-lru) [![npm](https://nodei.co/npm/tiny-lru.png?downloads=true&stars=true)](https://www.npmjs.com/package/tiny-lru) From 0c25ff2478aecafed60ea17fc91b513732231106 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:07:47 -0400 Subject: [PATCH 08/55] ci.yml: add push event for master branch --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d919c8c..f648207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,9 @@ name: ci on: + push: + branches: + - master pull_request: branches: - master From fa769cbcdd4e946aaf9b430cb14c41b5b3768c7f Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:09:13 -0400 Subject: [PATCH 09/55] README: use full GitHub paths for markdown doc links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e4137b2..e5285be 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,7 @@ Run our comprehensive benchmark suite to see performance characteristics: npm run benchmark:all ``` -See [benchmarks/README.md](benchmarks/README.md) for more details. +See [benchmarks/README.md](https://github.com/avoidwork/tiny-lru/blob/master/benchmarks/README.md) for more details. ## Development @@ -451,7 +451,7 @@ Example domains: - Analytics: `analytics:event:user:session` - ML/AI: `ml:llm:response:gpt4-hash` -See [docs/TECHNICAL_DOCUMENTATION.md](docs/TECHNICAL_DOCUMENTATION.md) for more security best practices. +See [docs/TECHNICAL_DOCUMENTATION.md](https://github.com/avoidwork/tiny-lru/blob/master/docs/TECHNICAL_DOCUMENTATION.md) for more security best practices. ## License From dc636cc4352ffd2689658413e034556cea88814f Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:13:01 -0400 Subject: [PATCH 10/55] README: correct comparison table values - Update bundle sizes to actual measured values (gzipped) - Fix quick-lru TTL support (it does support maxAge) - Replace inaccurate 'Comprehensive API' column with 'Pure LRU' --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e5285be..e6a9349 100644 --- a/README.md +++ b/README.md @@ -365,14 +365,16 @@ class LLMCache { ## Why Tiny LRU? -| Feature | tiny-lru | lru-cache | quick-lru | -| ---------------- | ------------ | --------- | ----------- | -| Bundle size | ~2.2 KB | ~15 KB | ~1.8 KB | -| O(1) operations | ✅ | ✅ | ✅ | -| TTL support | ✅ | ✅ | ❌ | -| TypeScript | ✅ | ✅ | ✅ | -| Zero dependencies| ✅ | ❌ | ✅ | -| 100% coverage | ✅ | ❌ | ❌ | +| Feature | tiny-lru | lru-cache | quick-lru | +| ---------------- | ------------ | ----------- | ----------- | +| Bundle size | ~2.2 KB | ~12 KB | ~1.5 KB | +| O(1) operations | ✅ | ✅ | ✅ | +| TTL support | ✅ | ✅ | ✅ | +| TypeScript | ✅ | ✅ | ✅ | +| Zero dependencies| ✅ | ❌ | ✅ | +| Pure LRU | ✅ | ❌* | ✅ | + +\* lru-cache uses a hybrid design that can hold 2× the specified size for performance ## Performance From 31362e72c869962dec9ebcff452dd0fc780e379c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:14:13 -0400 Subject: [PATCH 11/55] build: remove UMD output format --- dist/tiny-lru.umd.js | 655 ----------------------------------- dist/tiny-lru.umd.min.js | 5 - dist/tiny-lru.umd.min.js.map | 1 - rollup.config.js | 12 - 4 files changed, 673 deletions(-) delete mode 100644 dist/tiny-lru.umd.js delete mode 100644 dist/tiny-lru.umd.min.js delete mode 100644 dist/tiny-lru.umd.min.js.map diff --git a/dist/tiny-lru.umd.js b/dist/tiny-lru.umd.js deleted file mode 100644 index d416927..0000000 --- a/dist/tiny-lru.umd.js +++ /dev/null @@ -1,655 +0,0 @@ -/** - * tiny-lru - * - * @copyright 2026 Jason Mulligan - * @license BSD-3-Clause - * @version 12.0.0 - */ -(function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports):typeof define==='function'&&define.amd?define(['exports'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.lru={}));})(this,(function(exports){'use strict';/** - * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support. - * Items are automatically evicted when the cache reaches its maximum size, - * removing the least recently used items first. All core operations (get, set, delete) are O(1). - * - * @class LRU - */ -class LRU { - #stats; - #onEvict; - - /** - * Creates a new LRU cache instance. - * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. - * - * @constructor - * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited. - * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). - */ - constructor(max = 0, ttl = 0, resetTtl = false) { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.max = max; - this.resetTtl = resetTtl; - this.size = 0; - this.ttl = ttl; - this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; - this.#onEvict = null; - } - - /** - * Removes all items from the cache. - * - * @returns {LRU} The LRU instance for method chaining. - */ - clear() { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.size = 0; - this.#stats.hits = 0; - this.#stats.misses = 0; - this.#stats.sets = 0; - this.#stats.deletes = 0; - this.#stats.evictions = 0; - - return this; - } - - /** - * Removes an item from the cache by key. - * - * @param {string} key - The key of the item to delete. - * @returns {LRU} The LRU instance for method chaining. - */ - delete(key) { - const item = this.items[key]; - - if (item !== undefined) { - delete this.items[key]; - this.size--; - this.#stats.deletes++; - - this.#unlink(item); - - item.prev = null; - item.next = null; - } - - return this; - } - - /** - * Returns an array of [key, value] pairs for the specified keys. - * When no keys provided, returns all entries in LRU order. - * When keys provided, order matches the input array. - * - * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. - * @returns {Array>} Array of [key, value] pairs. - */ - entries(keys) { - if (keys === undefined) { - keys = this.keys(); - } - - const result = Array.from({ length: keys.length }); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const item = this.items[key]; - result[i] = [key, item !== undefined ? item.value : undefined]; - } - - return result; - } - - /** - * Removes the least recently used item from the cache. - * - * @returns {LRU} The LRU instance for method chaining. - */ - evict() { - if (this.size === 0) { - return this; - } - - const item = this.first; - - delete this.items[item.key]; - this.#stats.evictions++; - - if (--this.size === 0) { - this.first = null; - this.last = null; - } else { - this.#unlink(item); - } - - item.next = null; - if (this.#onEvict !== null) { - this.#onEvict({ - key: item.key, - value: item.value, - expiry: item.expiry, - }); - } - - return this; - } - - /** - * Returns the expiration timestamp for a given key. - * - * @param {string} key - The key to check expiration for. - * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. - */ - expiresAt(key) { - const item = this.items[key]; - return item !== undefined ? item.expiry : undefined; - } - - /** - * Retrieves a value from the cache by key without updating LRU order. - * Note: Does not perform TTL checks or remove expired items. - * - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found. - */ - peek(key) { - const item = this.items[key]; - return item !== undefined ? item.value : undefined; - } - - /** - * Retrieves a value from the cache by key. Updates the item's position to most recently used. - * - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found or expired. - */ - get(key) { - const item = this.items[key]; - - if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - this.#stats.misses++; - - return undefined; - } - } - - // Fast LRU update without full set() overhead - this.moveToEnd(item); - this.#stats.hits++; - - return item.value; - } - - this.#stats.misses++; - return undefined; - } - - /** - * Checks if a key exists in the cache. - * - * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. - */ - has(key) { - const item = this.items[key]; - return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); - } - - /** - * Unlinks an item from the doubly-linked list. - * Updates first/last pointers if needed. - * Does NOT clear the item's prev/next pointers or delete from items map. - * - * @private - */ - #unlink(item) { - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - if (this.first === item) { - this.first = item.next; - } - - if (this.last === item) { - this.last = item.prev; - } - } - - /** - * Efficiently moves an item to the end of the LRU list (most recently used position). - * This is an internal optimization method that avoids the overhead of the full set() operation - * when only LRU position needs to be updated. - * - * @param {Object} item - The cache item with prev/next pointers to reposition. - * @private - */ - moveToEnd(item) { - if (this.last === item) { - return; - } - - this.#unlink(item); - - item.prev = this.last; - item.next = null; - this.last.next = item; - this.last = item; - } - - /** - * Returns an array of all keys in the cache, ordered from least to most recently used. - * - * @returns {string[]} Array of keys in LRU order. - */ - keys() { - const result = Array.from({ length: this.size }); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; - } - - return result; - } - - /** - * Sets a value in the cache and returns any evicted item. - * - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null. - */ - setWithEvicted(key, value) { - let evicted = null; - let item = this.items[key]; - - if (item !== undefined) { - item.value = value; - if (this.resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - this.moveToEnd(item); - } else { - if (this.max > 0 && this.size === this.max) { - evicted = { - key: this.first.key, - value: this.first.value, - expiry: this.first.expiry, - }; - this.evict(); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value, - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - this.#stats.sets++; - return evicted; - } - - /** - * Sets a value in the cache. Updates the item's position to most recently used. - * - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @returns {LRU} The LRU instance for method chaining. - */ - set(key, value) { - let item = this.items[key]; - - if (item !== undefined) { - item.value = value; - - if (this.resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - - this.moveToEnd(item); - } else { - if (this.max > 0 && this.size === this.max) { - this.evict(); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value, - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - this.#stats.sets++; - - return this; - } - - /** - * Returns an array of all values in the cache for the specified keys. - * When no keys provided, returns all values in LRU order. - * When keys provided, order matches the input array. - * - * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys. - * @returns {Array<*>} Array of values corresponding to the keys. - */ - values(keys) { - if (keys === undefined) { - const result = Array.from({ length: this.size }); - let i = 0; - for (let x = this.first; x !== null; x = x.next) { - result[i++] = x.value; - } - return result; - } - - const result = Array.from({ length: keys.length }); - for (let i = 0; i < keys.length; i++) { - const item = this.items[keys[i]]; - result[i] = item !== undefined ? item.value : undefined; - } - - return result; - } - - /** - * Iterate over cache items in LRU order (least to most recent). - * Note: This method directly accesses items from the linked list without calling - * get() or peek(), so it does not update LRU order or check TTL expiration during iteration. - * - * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache) - * @param {Object} [thisArg] - Value to use as `this` when executing callback. - * @returns {LRU} The LRU instance for method chaining. - */ - forEach(callback, thisArg) { - for (let x = this.first; x !== null; x = x.next) { - callback.call(thisArg, x.value, x.key, this); - } - - return this; - } - - /** - * Batch retrieve multiple items. - * - * @param {string[]} keys - Array of keys to retrieve. - * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). - */ - getMany(keys) { - const result = Object.create(null); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - result[key] = this.get(key); - } - - return result; - } - - /** - * Batch existence check - returns true if ALL keys exist. - * - * @param {string[]} keys - Array of keys to check. - * @returns {boolean} True if all keys exist and are not expired. - */ - hasAll(keys) { - for (let i = 0; i < keys.length; i++) { - if (!this.has(keys[i])) { - return false; - } - } - - return true; - } - - /** - * Batch existence check - returns true if ANY key exists. - * - * @param {string[]} keys - Array of keys to check. - * @returns {boolean} True if any key exists and is not expired. - */ - hasAny(keys) { - for (let i = 0; i < keys.length; i++) { - if (this.has(keys[i])) { - return true; - } - } - - return false; - } - - /** - * Remove expired items without affecting LRU order. - * Unlike get(), this does not move items to the end. - * - * @returns {number} Number of expired items removed. - */ - cleanup() { - if (this.ttl === 0 || this.size === 0) { - return 0; - } - - const now = Date.now(); - let removed = 0; - - for (let x = this.first; x !== null; x = x.next) { - if (x.expiry <= now) { - const key = x.key; - if (this.items[key] !== undefined) { - delete this.items[key]; - this.size--; - removed++; - } - } - } - - if (removed > 0) { - this.#rebuildList(); - } - - return removed; - } - - /** - * Serialize cache to JSON-compatible format. - * - * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items. - */ - toJSON() { - const result = []; - for (let x = this.first; x !== null; x = x.next) { - result.push({ - key: x.key, - value: x.value, - expiry: x.expiry, - }); - } - - return result; - } - - /** - * Get cache statistics. - * - * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts. - */ - stats() { - return { ...this.#stats }; - } - - /** - * Register callback for evicted items. - * - * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}. - * @returns {LRU} The LRU instance for method chaining. - */ - onEvict(callback) { - this.#onEvict = callback; - - return this; - } - - /** - * Get counts of items by TTL status. - * - * @returns {Object} Object with valid, expired, and noTTL counts. - */ - sizeByTTL() { - if (this.ttl === 0) { - return { valid: this.size, expired: 0, noTTL: this.size }; - } - - const now = Date.now(); - let valid = 0; - let expired = 0; - let noTTL = 0; - - for (let x = this.first; x !== null; x = x.next) { - if (x.expiry === 0) { - noTTL++; - valid++; - } else if (x.expiry > now) { - valid++; - } else { - expired++; - } - } - - return { valid, expired, noTTL }; - } - - /** - * Get keys filtered by TTL status. - * - * @returns {Object} Object with valid, expired, and noTTL arrays of keys. - */ - keysByTTL() { - if (this.ttl === 0) { - return { valid: this.keys(), expired: [], noTTL: this.keys() }; - } - - const now = Date.now(); - const valid = []; - const expired = []; - const noTTL = []; - - for (let x = this.first; x !== null; x = x.next) { - if (x.expiry === 0) { - valid.push(x.key); - noTTL.push(x.key); - } else if (x.expiry > now) { - valid.push(x.key); - } else { - expired.push(x.key); - } - } - - return { valid, expired, noTTL }; - } - - /** - * Get values filtered by TTL status. - * - * @returns {Object} Object with valid, expired, and noTTL arrays of values. - */ - valuesByTTL() { - const keysByTTL = this.keysByTTL(); - - return { - valid: this.values(keysByTTL.valid), - expired: this.values(keysByTTL.expired), - noTTL: this.values(keysByTTL.noTTL), - }; - } - - /** - * Rebuild the doubly-linked list after cleanup by deleting expired items. - * This removes nodes that were deleted during cleanup. - * - * @private - */ - #rebuildList() { - if (this.size === 0) { - this.first = null; - this.last = null; - return; - } - - const keys = this.keys(); - this.first = null; - this.last = null; - - for (let i = 0; i < keys.length; i++) { - const item = this.items[keys[i]]; - if (item !== null && item !== undefined) { - if (this.first === null) { - this.first = item; - item.prev = null; - } else { - item.prev = this.last; - this.last.next = item; - } - item.next = null; - this.last = item; - } - } - } -} - -/** - * Factory function to create a new LRU cache instance with parameter validation. - * - * @function lru - * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. - * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). - * @returns {LRU} A new LRU cache instance. - * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). - */ -function lru(max = 1000, ttl = 0, resetTtl = false) { - if (isNaN(max) || max < 0) { - throw new TypeError("Invalid max value"); - } - - if (isNaN(ttl) || ttl < 0) { - throw new TypeError("Invalid ttl value"); - } - - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); - } - - return new LRU(max, ttl, resetTtl); -}exports.LRU=LRU;exports.lru=lru;})); \ No newline at end of file diff --git a/dist/tiny-lru.umd.min.js b/dist/tiny-lru.umd.min.js deleted file mode 100644 index 2e9f1c4..0000000 --- a/dist/tiny-lru.umd.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! - 2026 Jason Mulligan - @version 12.0.0 -*/ -!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports):"function"==typeof define&&define.amd?define(["exports"],s):s((t="undefined"!=typeof globalThis?globalThis:t||self).lru={})}(this,(function(t){"use strict";class s{#t;#s;constructor(t=0,s=0,e=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=e,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#e(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let e=0;e0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#e(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#e(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,e=0;for(;null!==s;)t[e++]=s.key,s=s.next;return t}setWithEvicted(t,s){let e=null,i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&(e={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,e}set(t,s){let e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&this.evict(),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let e=this.first;null!==e;e=e.next)t[s++]=e.value;return t}const s=Array.from({length:t.length});for(let e=0;e0&&this.#i(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,e=0,i=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(i++,s++):l.expiry>t?s++:e++;return{valid:s,expired:e,noTTL:i}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],e=[],i=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),i.push(l.key)):l.expiry>t?s.push(l.key):e.push(l.key);return{valid:s,expired:e,noTTL:i}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#i(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","stats","onEvict","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 05849ef..2331b3c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -17,7 +17,6 @@ const bannerShort = `/*! const defaultOutBase = { compact: true, banner: bannerLong, name: pkg.name }; const cjOutBase = { ...defaultOutBase, compact: false, format: "cjs", exports: "named" }; const esmOutBase = { ...defaultOutBase, format: "esm" }; -const umdOutBase = { ...defaultOutBase, format: "umd" }; const minOutBase = { banner: bannerShort, name: pkg.name, plugins: [terser()], sourcemap: true }; export default [ @@ -37,17 +36,6 @@ export default [ ...minOutBase, file: `dist/${pkg.name}.min.js`, }, - { - ...umdOutBase, - file: `dist/${pkg.name}.umd.js`, - name: "lru", - }, - { - ...umdOutBase, - ...minOutBase, - file: `dist/${pkg.name}.umd.min.js`, - name: "lru", - }, ], }, ]; From f4ceed2812008341cbcd7d649424d9b1a70656ff Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:15:39 -0400 Subject: [PATCH 12/55] docs: remove UMD mentions from README.md, AGENTS.md and TECHNICAL_DOCUMENTATION.md --- AGENTS.md | 3 +++ README.md | 2 -- docs/TECHNICAL_DOCUMENTATION.md | 11 ++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d1a0858..36b1d0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,9 @@ Source code is in `src/`. ├── tests/ # Test files ├── benchmarks/ # Performance benchmarks ├── dist/ # Built distribution files +│ ├── tiny-lru.js # ES Modules +│ ├── tiny-lru.cjs # CommonJS +│ └── tiny-lru.min.js # Minified ESM ├── types/ # TypeScript definitions ├── docs/ # Documentation ├── rollup.config.js # Build configuration diff --git a/README.md b/README.md index e6a9349..2344267 100644 --- a/README.md +++ b/README.md @@ -412,8 +412,6 @@ Build produces multiple module formats: - `dist/tiny-lru.js` - ES Modules - `dist/tiny-lru.cjs` - CommonJS - `dist/tiny-lru.min.js` - Minified ESM -- `dist/tiny-lru.umd.js` - UMD -- `dist/tiny-lru.umd.min.js` - Minified UMD - `types/lru.d.ts` - TypeScript definitions ## Test Coverage diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md index af022c1..c17d043 100644 --- a/docs/TECHNICAL_DOCUMENTATION.md +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -1084,10 +1084,6 @@ export default [ { format: "esm", file: "dist/tiny-lru.js" }, // Minified ES Modules { format: "esm", file: "dist/tiny-lru.min.js", plugins: [terser()] }, - // UMD for browsers - { format: "umd", file: "dist/tiny-lru.umd.js", name: "lru" }, - // Minified UMD - { format: "umd", file: "dist/tiny-lru.umd.min.js", name: "lru", plugins: [terser()] }, ], }, ]; @@ -1113,8 +1109,7 @@ export default [ - **ESM**: `dist/tiny-lru.js` - ES Modules for modern bundlers - **CommonJS**: `dist/tiny-lru.cjs` - Node.js compatible format -- **UMD**: `dist/tiny-lru.umd.js` - Universal format for browsers -- **Minified**: All formats available with `.min.js` extension +- **Minified**: `dist/tiny-lru.min.js` - Minified ESM - **TypeScript**: `types/lru.d.ts` - Complete type definitions ### Development Commands @@ -1144,9 +1139,7 @@ tiny-lru/ ├── dist/ # Built distributions (generated) │ ├── tiny-lru.js # ES Modules │ ├── tiny-lru.cjs # CommonJS -│ ├── tiny-lru.min.js # Minified ESM -│ ├── tiny-lru.umd.js # UMD -│ └── tiny-lru.umd.min.js # Minified UMD +│ └── tiny-lru.min.js # Minified ESM ├── tests/ │ ├── unit/ # Unit tests with Node.js built-in test runner │ └── integration/ # Integration tests From 0c727d629d939fcbfb23972f4d37f31f45ab6c55 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:16:41 -0400 Subject: [PATCH 13/55] docs: update test coverage metrics --- AGENTS.md | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 36b1d0b..c969a55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,9 +68,9 @@ Source code is in `src/`. ## Testing - Framework: Node.js built-in test runner (`node --test`) -- Coverage: 100% +- Coverage: 100% lines, 99.25% branches, 100% functions - Test pattern: `tests/**/*.js` -- All tests must pass with 100% coverage before merging +- All tests must pass with 100% line coverage before merging - Run: `npm test` (lint + tests) or `npm run coverage` for coverage report ## Common Issues to Avoid diff --git a/README.md b/README.md index 2344267..db69b58 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ Build produces multiple module formats: | Metric | Coverage | | --------- | -------- | | Lines | 100% | -| Branches | 95% | +| Branches | 99.25% | | Functions | 100% | ## Contributing From c1c95d3229867fda560dddf9087e82ec0945e91d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:20:29 -0400 Subject: [PATCH 14/55] docs: order methods alphabetically, expand ToC - Methods now sorted A-Z: cleanup() to valuesByTTL() - Expanded table of contents with 1-3 level headers - All method anchors now directly link to section headers --- docs/API.md | 401 ++++++++++++++++++++++++++++------------------------ 1 file changed, 215 insertions(+), 186 deletions(-) diff --git a/docs/API.md b/docs/API.md index 4459269..9bf39be 100644 --- a/docs/API.md +++ b/docs/API.md @@ -7,7 +7,36 @@ Complete API documentation for tiny-lru. - [Factory Function](#factory-function) - [LRU Class](#lru-class) - [Properties](#properties) + - [first](#first) + - [last](#last) + - [max](#max) + - [resetTtl](#resetttl) + - [size](#size) + - [ttl](#ttl) - [Methods](#methods) + - [cleanup()](#cleanup) + - [clear()](#clear) + - [delete(key)](#deletekey) + - [entries(keys?)](#entrieskeys) + - [evict()](#evict) + - [expiresAt(key)](#expiresatkey) + - [forEach(callback, thisArg?)](#foreachcallback-thisarg) + - [get(key)](#getkey) + - [getMany(keys)](#getmanykeys) + - [has(key)](#haskey) + - [hasAll(keys)](#hasallkeys) + - [hasAny(keys)](#hasanykeys) + - [keys()](#keys) + - [keysByTTL()](#keysbyttl) + - [onEvict(callback)](#onevictcallback) + - [peek(key)](#peekkey) + - [set(key, value)](#setkey-value) + - [setWithEvicted(key, value)](#setwithevictedkey-value) + - [sizeByTTL()](#sizebyttl) + - [stats()](#stats) + - [toJSON()](#tojson) + - [values(keys?)](#valueskeys) + - [valuesByTTL()](#valuesbyttl) --- @@ -70,32 +99,34 @@ const cache = new LRU(100, 5000, true); ## Properties -### `size` +### `first` -`number` - Current number of items in cache. +`Object | null` - Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`). ```javascript -const cache = lru(10); +const cache = lru(2); cache.set("a", 1).set("b", 2); -console.log(cache.size); // 2 +console.log(cache.first.key); // "a" +console.log(cache.first.value); // 1 ``` -### `max` +### `last` -`number` - Maximum number of items allowed. +`Object | null` - Most recently used item. ```javascript -const cache = lru(100); -console.log(cache.max); // 100 +const cache = lru(2); +cache.set("a", 1).set("b", 2); +console.log(cache.last.key); // "b" ``` -### `ttl` +### `max` -`number` - Time-to-live in milliseconds. `0` = no expiration. +`number` - Maximum number of items allowed. ```javascript -const cache = lru(100, 60000); -console.log(cache.ttl); // 60000 +const cache = lru(100); +console.log(cache.max); // 100 ``` ### `resetTtl` @@ -107,31 +138,46 @@ const cache = lru(100, 5000, true); console.log(cache.resetTtl); // true ``` -### `first` +### `size` -`Object | null` - Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`). +`number` - Current number of items in cache. ```javascript -const cache = lru(2); +const cache = lru(10); cache.set("a", 1).set("b", 2); -console.log(cache.first.key); // "a" -console.log(cache.first.value); // 1 +console.log(cache.size); // 2 ``` -### `last` +### `ttl` -`Object | null` - Most recently used item. +`number` - Time-to-live in milliseconds. `0` = no expiration. ```javascript -const cache = lru(2); -cache.set("a", 1).set("b", 2); -console.log(cache.last.key); // "b" +const cache = lru(100, 60000); +console.log(cache.ttl); // 60000 ``` --- ## Methods +### `cleanup()` + +Removes expired items without affecting LRU order. + +```javascript +cache.set("a", 1).set("b", 2); +// ... wait for items to expire +const removed = cache.cleanup(); +console.log(removed); // 2 (number of items removed) +``` + +**Returns:** `number` - Number of expired items removed + +**Note:** Only removes items when TTL is enabled (`ttl > 0`). + +--- + ### `clear()` Removes all items from cache. @@ -226,28 +272,6 @@ console.log(cache.expiresAt("nonexistent")); // undefined --- -### `peek(key)` - -Retrieves value without updating LRU order. - -```javascript -cache.set("a", 1).set("b", 2); -cache.peek("a"); // 1 -console.log(cache.keys()); // ['b', 'a'] - order unchanged -``` - -**Parameters:** - -| Name | Type | Description | -| ----- | -------- | --------------- | -| `key` | `string` | Key to retrieve | - -**Returns:** `* | undefined` - Value or undefined if not found - -**Note:** Does not perform TTL expiration checks or update LRU order. - ---- - ### `forEach(callback, thisArg?)` Iterates over cache items in LRU order (least to most recent). @@ -276,6 +300,28 @@ cache.forEach((value, key, cache) => { --- +### `get(key)` + +Retrieves value and promotes to most recently used. + +```javascript +cache.set("a", 1).set("b", 2); +cache.get("a"); // 1 +console.log(cache.keys()); // ['b', 'a'] - 'a' moved to end +``` + +Expired items are deleted and return `undefined`. + +**Parameters:** + +| Name | Type | Description | +| ----- | -------- | --------------- | +| `key` | `string` | Key to retrieve | + +**Returns:** `* | undefined` - Value or undefined if not found/expired + +--- + ### `getMany(keys)` Batch retrieves multiple items. @@ -298,6 +344,26 @@ console.log(result); // { a: 1, c: 3 } --- +### `has(key)` + +Checks if key exists and is not expired. + +```javascript +cache.set("a", 1); +cache.has("a"); // true +cache.has("nonexistent"); // false +``` + +**Parameters:** + +| Name | Type | Description | +| ----- | -------- | ------------ | +| `key` | `string` | Key to check | + +**Returns:** `boolean` + +--- + ### `hasAll(keys)` Batch existence check - returns true if ALL keys exist and are not expired. @@ -344,69 +410,34 @@ cache.hasAny(["nonexistent1", "nonexistent2"]); // false --- -### `cleanup()` - -Removes expired items without affecting LRU order. - -```javascript -cache.set("a", 1).set("b", 2); -// ... wait for items to expire -const removed = cache.cleanup(); -console.log(removed); // 2 (number of items removed) -``` - -**Returns:** `number` - Number of expired items removed - -**Note:** Only removes items when TTL is enabled (`ttl > 0`). - ---- - -### `toJSON()` +### `keys()` -Serializes cache to JSON-compatible format. +Returns all keys in LRU order (oldest first). ```javascript -cache.set("a", 1).set("b", 2); -const json = cache.toJSON(); -console.log(json); -// [ -// { key: "a", value: 1, expiry: 0 }, -// { key: "b", value: 2, expiry: 0 } -// ] - -// Works with JSON.stringify: -const jsonString = JSON.stringify(cache); +cache.set("a", 1).set("b", 2).set("c", 3); +cache.get("a"); // Promote 'a' +console.log(cache.keys()); // ['b', 'c', 'a'] ``` -**Returns:** `Array<{key, value, expiry}>` - Array of cache items +**Returns:** `string[]` --- -### `stats()` +### `keysByTTL()` -Returns cache statistics. +Returns keys grouped by TTL status. ```javascript cache.set("a", 1).set("b", 2); -cache.get("a"); -cache.get("nonexistent"); - -console.log(cache.stats()); -// { -// hits: 1, -// misses: 1, -// sets: 2, -// deletes: 0, -// evictions: 0 -// } +console.log(cache.keysByTTL()); +// { valid: ["a", "b"], expired: [], noTTL: ["a", "b"] } ``` -**Returns:** `Object` - Statistics object with the following properties: -- `hits` - Number of successful get() calls -- `misses` - Number of failed get() calls -- `sets` - Number of set() calls -- `deletes` - Number of delete() calls -- `evictions` - Number of evicted items +**Returns:** `Object` - Object with three properties: +- `valid` - Array of valid (non-expired) keys +- `expired` - Array of expired keys +- `noTTL` - Array of keys without TTL (when `ttl=0`) --- @@ -435,112 +466,25 @@ cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); --- -### `sizeByTTL()` - -Returns counts of items by TTL status. - -```javascript -cache.set("a", 1).set("b", 2); -console.log(cache.sizeByTTL()); -// { valid: 2, expired: 0, noTTL: 0 } -``` - -**Returns:** `Object` - Object with three properties: -- `valid` - Number of items that haven't expired -- `expired` - Number of expired items -- `noTTL` - Number of items without TTL (when `ttl=0`) - -**Note:** Items without TTL (expiry === 0) count as both valid and noTTL. - ---- - -### `keysByTTL()` - -Returns keys grouped by TTL status. - -```javascript -cache.set("a", 1).set("b", 2); -console.log(cache.keysByTTL()); -// { valid: ["a", "b"], expired: [], noTTL: ["a", "b"] } -``` - -**Returns:** `Object` - Object with three properties: -- `valid` - Array of valid (non-expired) keys -- `expired` - Array of expired keys -- `noTTL` - Array of keys without TTL (when `ttl=0`) - ---- - -### `valuesByTTL()` - -Returns values grouped by TTL status. - -```javascript -cache.set("a", 1).set("b", 2); -console.log(cache.valuesByTTL()); -// { valid: [1, 2], expired: [], noTTL: [1, 2] } -``` - -**Returns:** `Object` - Object with three properties: -- `valid` - Array of valid (non-expired) values -- `expired` - Array of expired values -- `noTTL` - Array of values without TTL (when `ttl=0`) - ---- - -### `get(key)` +### `peek(key)` -Retrieves value and promotes to most recently used. +Retrieves value without updating LRU order. ```javascript cache.set("a", 1).set("b", 2); -cache.get("a"); // 1 -console.log(cache.keys()); // ['b', 'a'] - 'a' moved to end +cache.peek("a"); // 1 +console.log(cache.keys()); // ['b', 'a'] - order unchanged ``` -Expired items are deleted and return `undefined`. - **Parameters:** | Name | Type | Description | | ----- | -------- | --------------- | | `key` | `string` | Key to retrieve | -**Returns:** `* | undefined` - Value or undefined if not found/expired - ---- - -### `has(key)` - -Checks if key exists and is not expired. - -```javascript -cache.set("a", 1); -cache.has("a"); // true -cache.has("nonexistent"); // false -``` - -**Parameters:** - -| Name | Type | Description | -| ----- | -------- | ------------ | -| `key` | `string` | Key to check | - -**Returns:** `boolean` - ---- - -### `keys()` - -Returns all keys in LRU order (oldest first). - -```javascript -cache.set("a", 1).set("b", 2).set("c", 3); -cache.get("a"); // Promote 'a' -console.log(cache.keys()); // ['b', 'c', 'a'] -``` +**Returns:** `* | undefined` - Value or undefined if not found -**Returns:** `string[]` +**Note:** Does not perform TTL expiration checks or update LRU order. --- @@ -588,6 +532,74 @@ console.log(cache.keys()); // ['b', 'c'] --- +### `sizeByTTL()` + +Returns counts of items by TTL status. + +```javascript +cache.set("a", 1).set("b", 2); +console.log(cache.sizeByTTL()); +// { valid: 2, expired: 0, noTTL: 0 } +``` + +**Returns:** `Object` - Object with three properties: +- `valid` - Number of items that haven't expired +- `expired` - Number of expired items +- `noTTL` - Number of items without TTL (when `ttl=0`) + +**Note:** Items without TTL (expiry === 0) count as both valid and noTTL. + +--- + +### `stats()` + +Returns cache statistics. + +```javascript +cache.set("a", 1).set("b", 2); +cache.get("a"); +cache.get("nonexistent"); + +console.log(cache.stats()); +// { +// hits: 1, +// misses: 1, +// sets: 2, +// deletes: 0, +// evictions: 0 +// } +``` + +**Returns:** `Object` - Statistics object with the following properties: +- `hits` - Number of successful get() calls +- `misses` - Number of failed get() calls +- `sets` - Number of set() calls +- `deletes` - Number of delete() calls +- `evictions` - Number of evicted items + +--- + +### `toJSON()` + +Serializes cache to JSON-compatible format. + +```javascript +cache.set("a", 1).set("b", 2); +const json = cache.toJSON(); +console.log(json); +// [ +// { key: "a", value: 1, expiry: 0 }, +// { key: "b", value: 2, expiry: 0 } +// ] + +// Works with JSON.stringify: +const jsonString = JSON.stringify(cache); +``` + +**Returns:** `Array<{key, value, expiry}>` - Array of cache items + +--- + ### `values(keys?)` Returns values in LRU order. @@ -611,6 +623,23 @@ console.log(cache.values(["c", "a"])); --- +### `valuesByTTL()` + +Returns values grouped by TTL status. + +```javascript +cache.set("a", 1).set("b", 2); +console.log(cache.valuesByTTL()); +// { valid: [1, 2], expired: [], noTTL: [1, 2] } +``` + +**Returns:** `Object` - Object with three properties: +- `valid` - Array of valid (non-expired) values +- `expired` - Array of expired values +- `noTTL` - Array of values without TTL (when `ttl=0`) + +--- + ## Evicted Item Shape When `setWithEvicted` returns an evicted item: From 610068800d8656dc32f3e0cb81bc3d05fe5910b2 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:25:54 -0400 Subject: [PATCH 15/55] Add documentation links to README.md - Removed redundant TECHNICAL_DOCUMENTATION link in Security section - Added Documentation section with links to all three docs files --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index db69b58..46e91f1 100644 --- a/README.md +++ b/README.md @@ -451,7 +451,11 @@ Example domains: - Analytics: `analytics:event:user:session` - ML/AI: `ml:llm:response:gpt4-hash` -See [docs/TECHNICAL_DOCUMENTATION.md](https://github.com/avoidwork/tiny-lru/blob/master/docs/TECHNICAL_DOCUMENTATION.md) for more security best practices. +## Documentation + +- [API Reference](https://github.com/avoidwork/tiny-lru/blob/master/docs/API.md) - Complete API documentation +- [Technical Documentation](https://github.com/avoidwork/tiny-lru/blob/master/docs/TECHNICAL_DOCUMENTATION.md) - Architecture, performance, and security +- [Code Style Guide](https://github.com/avoidwork/tiny-lru/blob/master/docs/CODE_STYLE_GUIDE.md) - Contributing guidelines ## License From 69d3ea8a84ac02af138acef909cbd2b094538062 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:28:12 -0400 Subject: [PATCH 16/55] Fix incorrect resetTtl documentation and readonly properties - Corrected resetTtl doc to say 'via set()' instead of 'via get()' - Removed 'readonly' from class properties (implementation doesn't use readonly) --- types/lru.d.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/types/lru.d.ts b/types/lru.d.ts index 40f3e88..b07a10c 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -2,7 +2,7 @@ * Factory function to create a new LRU cache instance with parameter validation. * @param max Maximum number of items to store (default: 1000, 0 = unlimited) * @param ttl Time to live in milliseconds (default: 0, 0 = no expiration) - * @param resetTtl Whether to reset TTL when accessing existing items via get() (default: false) + * @param resetTtl Whether to reset TTL when updating existing items via set() (default: false) * @returns A new LRU cache instance * @throws TypeError when parameters are invalid (negative numbers or wrong types) */ @@ -46,24 +46,24 @@ export class LRU { * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. * @param max Maximum number of items to store (default: 0, 0 = unlimited) * @param ttl Time to live in milliseconds (default: 0, 0 = no expiration) - * @param resetTtl Whether to reset TTL when accessing existing items via get() (default: false) + * @param resetTtl Whether to reset TTL when updating existing items via set() (default: false) */ constructor(max?: number, ttl?: number, resetTtl?: boolean); /** Pointer to the least recently used item (first to be evicted) */ - readonly first: LRUItem | null; + first: LRUItem | null; /** Hash map for O(1) key-based access to cache nodes */ - readonly items: Record>; + items: Record>; /** Pointer to the most recently used item */ - readonly last: LRUItem | null; + last: LRUItem | null; /** Maximum number of items to store (0 = unlimited) */ - readonly max: number; + max: number; /** Whether to reset TTL on set() operations */ - readonly resetTtl: boolean; + resetTtl: boolean; /** Current number of items in the cache */ - readonly size: number; + size: number; /** Time-to-live in milliseconds (0 = no expiration) */ - readonly ttl: number; + ttl: number; /** * Removes all items from the cache. From 212a42a18724860dd4daf3695ea8b2dad9323392 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:35:11 -0400 Subject: [PATCH 17/55] evict(): clear item.prev to allow garbage collection - When evicting items, ensure both prev and next pointers are cleared - This prevents keeping the rest of the list reachable when consumers hold references to evicted nodes --- dist/tiny-lru.cjs | 1 + dist/tiny-lru.js | 1 + dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index d34051b..fd4b5c6 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -126,6 +126,7 @@ class LRU { this.#unlink(item); } + item.prev = null; item.next = null; if (this.#onEvict !== null) { this.#onEvict({ diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index a33f205..af3363e 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -124,6 +124,7 @@ class LRU { this.#unlink(item); } + item.prev = null; item.next = null; if (this.#onEvict !== null) { this.#onEvict({ diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 10a4bc9..227b6a7 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index 575e5e5..a413785 100644 --- a/src/lru.js +++ b/src/lru.js @@ -117,6 +117,7 @@ export class LRU { this.#unlink(item); } + item.prev = null; item.next = null; if (this.#onEvict !== null) { this.#onEvict({ From d5d914cb41d28ad6ed18ad80a305692d64d1a7cf Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:37:26 -0400 Subject: [PATCH 18/55] cleanup(): fix inconsistency with expiry === 0 handling - Items with expiry === 0 represent noTTL items, not expired items - Changed cleanup() to only remove items where expiry !== 0 && expiry <= now - This matches the semantics in sizeByTTL()/keysByTTL() --- dist/tiny-lru.cjs | 2 +- dist/tiny-lru.js | 2 +- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index fd4b5c6..bfaf883 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -468,7 +468,7 @@ class LRU { let removed = 0; for (let x = this.first; x !== null; x = x.next) { - if (x.expiry <= now) { + if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index af3363e..363b30d 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -466,7 +466,7 @@ class LRU { let removed = 0; for (let x = this.first; x !== null; x = x.next) { - if (x.expiry <= now) { + if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 227b6a7..f7af490 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index a413785..cd60d43 100644 --- a/src/lru.js +++ b/src/lru.js @@ -459,7 +459,7 @@ export class LRU { let removed = 0; for (let x = this.first; x !== null; x = x.next) { - if (x.expiry <= now) { + if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; From 94a31a7fc2c4b8f128369bee631bc0c185e5f990 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:40:07 -0400 Subject: [PATCH 19/55] docs: correct forEach() documentation to match actual implementation --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 9bf39be..434f08d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -296,7 +296,7 @@ cache.forEach((value, key, cache) => { **Returns:** `LRU` - this instance (for chaining) -**Note:** This method creates a snapshot of keys before iteration to safely handle cache modifications during iteration. +**Note:** This method traverses the linked list directly and does not update LRU order or check TTL expiration during iteration. Cache modifications during iteration may cause unexpected behavior. --- From f3de5f03ffa6cbe685fb15378443bedba7b42c01 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:40:55 -0400 Subject: [PATCH 20/55] docs: fix cache-aside example to show shared cache instance --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46e91f1..363f2f5 100644 --- a/README.md +++ b/README.md @@ -218,9 +218,10 @@ fib(100); // even faster - from cache ### Cache-Aside Pattern ```javascript -async function getUser(userId) { - const cache = lru(1000, 60000); // 1 minute cache +// Cache instance shared across calls (outside the function) +const cache = lru(1000, 60000); // 1 minute cache +async function getUser(userId) { // Check cache first const cached = cache.get(`user:${userId}`); if (cached) { From 5805bc83cbb55ec26b45bfe75ca73790650dcc2c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:41:24 -0400 Subject: [PATCH 21/55] docs: correct comment to say 'TTL reset on update' instead of 'on access' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 363f2f5..772d34a 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ import { LRU } from "tiny-lru"; class AuthCache { constructor() { - // Session cache: 30 minutes with TTL reset on access + // Session cache: 30 minutes with TTL reset on update this.sessions = new LRU(10000, 1800000, true); // Token validation cache: 5 minutes, no reset this.tokens = new LRU(5000, 300000, false); From ebffa0f8d6865ab49686a503f8f7e687ffeda105 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:50:23 -0400 Subject: [PATCH 22/55] Fix cleanup() memory leak - clear prev/next pointers on expired nodes - Properly unlink expired nodes during cleanup() deletion - Clear prev/next pointers to allow garbage collection - Fix iteration to save next pointer before unlinking --- dist/tiny-lru.cjs | 7 ++++++- dist/tiny-lru.js | 7 ++++++- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 7 ++++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index bfaf883..13764bf 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -467,15 +467,20 @@ class LRU { const now = Date.now(); let removed = 0; - for (let x = this.first; x !== null; x = x.next) { + for (let x = this.first; x !== null; ) { + const next = x.next; if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; this.size--; removed++; + this.#unlink(x); + x.prev = null; + x.next = null; } } + x = next; } if (removed > 0) { diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 363b30d..558aa34 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -465,15 +465,20 @@ class LRU { const now = Date.now(); let removed = 0; - for (let x = this.first; x !== null; x = x.next) { + for (let x = this.first; x !== null; ) { + const next = x.next; if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; this.size--; removed++; + this.#unlink(x); + x.prev = null; + x.next = null; } } + x = next; } if (removed > 0) { diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index f7af490..c2ec49e 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":";;;;AAOO,MAAMA,EACZC,GACAC,GAWA,WAAAC,CAAYC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACxCC,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKH,IAAMA,EACXG,KAAKD,SAAWA,EAChBC,KAAKM,KAAO,EACZN,KAAKF,IAAMA,EACXE,MAAKN,EAAS,CAAEa,KAAM,EAAGC,OAAQ,EAAGC,KAAM,EAAGC,QAAS,EAAGC,UAAW,GACpEX,MAAKL,EAAW,IACjB,CAOA,KAAAiB,GAWC,OAVAZ,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKM,KAAO,EACZN,MAAKN,EAAOa,KAAO,EACnBP,MAAKN,EAAOc,OAAS,EACrBR,MAAKN,EAAOe,KAAO,EACnBT,MAAKN,EAAOgB,QAAU,EACtBV,MAAKN,EAAOiB,UAAY,EAEjBX,IACR,CAQA,OAAOa,GACN,MAAMC,EAAOd,KAAKE,MAAMW,GAaxB,YAXaE,IAATD,WACId,KAAKE,MAAMW,GAClBb,KAAKM,OACLN,MAAKN,EAAOgB,UAEZV,MAAKgB,EAAQF,GAEbA,EAAKG,KAAO,KACZH,EAAKI,KAAO,MAGNlB,IACR,CAUA,OAAAmB,CAAQC,QACML,IAATK,IACHA,EAAOpB,KAAKoB,QAGb,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMZ,EAAMO,EAAKK,GACXX,EAAOd,KAAKE,MAAMW,GACxBQ,EAAOI,GAAK,CAACZ,OAAcE,IAATD,EAAqBA,EAAKY,WAAQX,EACrD,CAEA,OAAOM,CACR,CAOA,KAAAM,GACC,GAAkB,IAAd3B,KAAKM,KACR,OAAON,KAGR,MAAMc,EAAOd,KAAKC,MAsBlB,cApBOD,KAAKE,MAAMY,EAAKD,KACvBb,MAAKN,EAAOiB,YAEQ,KAAdX,KAAKM,MACVN,KAAKC,MAAQ,KACbD,KAAKK,KAAO,MAEZL,MAAKgB,EAAQF,GAGdA,EAAKG,KAAO,KACZH,EAAKI,KAAO,KACU,OAAlBlB,MAAKL,GACRK,MAAKL,EAAS,CACbkB,IAAKC,EAAKD,IACVa,MAAOZ,EAAKY,MACZE,OAAQd,EAAKc,SAIR5B,IACR,CAQA,SAAA6B,CAAUhB,GACT,MAAMC,EAAOd,KAAKE,MAAMW,GACxB,YAAgBE,IAATD,EAAqBA,EAAKc,YAASb,CAC3C,CASA,IAAAe,CAAKjB,GACJ,MAAMC,EAAOd,KAAKE,MAAMW,GACxB,YAAgBE,IAATD,EAAqBA,EAAKY,WAAQX,CAC1C,CAQA,GAAAgB,CAAIlB,GACH,MAAMC,EAAOd,KAAKE,MAAMW,GAExB,QAAaE,IAATD,EAEH,OAAId,KAAKF,IAAM,GACVgB,EAAKc,QAAUI,KAAKC,OACvBjC,KAAKkC,OAAOrB,QACZb,MAAKN,EAAOc,WAOdR,KAAKmC,UAAUrB,GACfd,MAAKN,EAAOa,OAELO,EAAKY,OAGb1B,MAAKN,EAAOc,QAEb,CAQA,GAAA4B,CAAIvB,GACH,MAAMC,EAAOd,KAAKE,MAAMW,GACxB,YAAgBE,IAATD,IAAoC,IAAbd,KAAKF,KAAagB,EAAKc,OAASI,KAAKC,MACpE,CASA,EAAAjB,CAAQF,GACW,OAAdA,EAAKG,OACRH,EAAKG,KAAKC,KAAOJ,EAAKI,MAGL,OAAdJ,EAAKI,OACRJ,EAAKI,KAAKD,KAAOH,EAAKG,MAGnBjB,KAAKC,QAAUa,IAClBd,KAAKC,MAAQa,EAAKI,MAGflB,KAAKK,OAASS,IACjBd,KAAKK,KAAOS,EAAKG,KAEnB,CAUA,SAAAkB,CAAUrB,GACLd,KAAKK,OAASS,IAIlBd,MAAKgB,EAAQF,GAEbA,EAAKG,KAAOjB,KAAKK,KACjBS,EAAKI,KAAO,KACZlB,KAAKK,KAAKa,KAAOJ,EACjBd,KAAKK,KAAOS,EACb,CAOA,IAAAM,GACC,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQxB,KAAKM,OACzC,IAAI+B,EAAIrC,KAAKC,MACTwB,EAAI,EAER,KAAa,OAANY,GACNhB,EAAOI,KAAOY,EAAExB,IAChBwB,EAAIA,EAAEnB,KAGP,OAAOG,CACR,CASA,cAAAiB,CAAezB,EAAKa,GACnB,IAAIa,EAAU,KACVzB,EAAOd,KAAKE,MAAMW,GAoCtB,YAlCaE,IAATD,GACHA,EAAKY,MAAQA,EACT1B,KAAKD,WACRe,EAAKc,OAAS5B,KAAKF,IAAM,EAAIkC,KAAKC,MAAQjC,KAAKF,IAAME,KAAKF,KAE3DE,KAAKmC,UAAUrB,KAEXd,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,MACtC0C,EAAU,CACT1B,IAAKb,KAAKC,MAAMY,IAChBa,MAAO1B,KAAKC,MAAMyB,MAClBE,OAAQ5B,KAAKC,MAAM2B,QAEpB5B,KAAK2B,SAGNb,EAAOd,KAAKE,MAAMW,GAAO,CACxBe,OAAQ5B,KAAKF,IAAM,EAAIkC,KAAKC,MAAQjC,KAAKF,IAAME,KAAKF,IACpDe,IAAKA,EACLI,KAAMjB,KAAKK,KACXa,KAAM,KACNQ,SAGmB,KAAd1B,KAAKM,KACVN,KAAKC,MAAQa,EAEbd,KAAKK,KAAKa,KAAOJ,EAGlBd,KAAKK,KAAOS,GAGbd,MAAKN,EAAOe,OACL8B,CACR,CASA,GAAAC,CAAI3B,EAAKa,GACR,IAAIZ,EAAOd,KAAKE,MAAMW,GAkCtB,YAhCaE,IAATD,GACHA,EAAKY,MAAQA,EAET1B,KAAKD,WACRe,EAAKc,OAAS5B,KAAKF,IAAM,EAAIkC,KAAKC,MAAQjC,KAAKF,IAAME,KAAKF,KAG3DE,KAAKmC,UAAUrB,KAEXd,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,KACtCG,KAAK2B,QAGNb,EAAOd,KAAKE,MAAMW,GAAO,CACxBe,OAAQ5B,KAAKF,IAAM,EAAIkC,KAAKC,MAAQjC,KAAKF,IAAME,KAAKF,IACpDe,IAAKA,EACLI,KAAMjB,KAAKK,KACXa,KAAM,KACNQ,SAGmB,KAAd1B,KAAKM,KACVN,KAAKC,MAAQa,EAEbd,KAAKK,KAAKa,KAAOJ,EAGlBd,KAAKK,KAAOS,GAGbd,MAAKN,EAAOe,OAELT,IACR,CAUA,MAAAyC,CAAOrB,GACN,QAAaL,IAATK,EAAoB,CACvB,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQxB,KAAKM,OACzC,IAAImB,EAAI,EACR,IAAK,IAAIY,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KAC1CG,EAAOI,KAAOY,EAAEX,MAEjB,OAAOL,CACR,CAEA,MAAMA,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMX,EAAOd,KAAKE,MAAMkB,EAAKK,IAC7BJ,EAAOI,QAAcV,IAATD,EAAqBA,EAAKY,WAAQX,CAC/C,CAEA,OAAOM,CACR,CAWA,OAAAqB,CAAQC,EAAUC,GACjB,IAAK,IAAIP,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KAC1CyB,EAASE,KAAKD,EAASP,EAAEX,MAAOW,EAAExB,IAAKb,MAGxC,OAAOA,IACR,CAQA,OAAA8C,CAAQ1B,GACP,MAAMC,EAASlB,OAAOC,OAAO,MAC7B,IAAK,IAAIqB,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMZ,EAAMO,EAAKK,GACjBJ,EAAOR,GAAOb,KAAK+B,IAAIlB,EACxB,CAEA,OAAOQ,CACR,CAQA,MAAA0B,CAAO3B,GACN,IAAK,IAAIK,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAChC,IAAKzB,KAAKoC,IAAIhB,EAAKK,IAClB,OAAO,EAIT,OAAO,CACR,CAQA,MAAAuB,CAAO5B,GACN,IAAK,IAAIK,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAChC,GAAIzB,KAAKoC,IAAIhB,EAAKK,IACjB,OAAO,EAIT,OAAO,CACR,CAQA,OAAAwB,GACC,GAAiB,IAAbjD,KAAKF,KAA2B,IAAdE,KAAKM,KAC1B,OAAO,EAGR,MAAM2B,EAAMD,KAAKC,MACjB,IAAIiB,EAAU,EAEd,IAAK,IAAIb,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KAC1C,GAAiB,IAAbmB,EAAET,QAAgBS,EAAET,QAAUK,EAAK,CACtC,MAAMpB,EAAMwB,EAAExB,SACUE,IAApBf,KAAKE,MAAMW,YACPb,KAAKE,MAAMW,GAClBb,KAAKM,OACL4C,IAEF,CAOD,OAJIA,EAAU,GACblD,MAAKmD,IAGCD,CACR,CAOA,MAAAE,GACC,MAAM/B,EAAS,GACf,IAAK,IAAIgB,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KAC1CG,EAAOgC,KAAK,CACXxC,IAAKwB,EAAExB,IACPa,MAAOW,EAAEX,MACTE,OAAQS,EAAET,SAIZ,OAAOP,CACR,CAOA,KAAA3B,GACC,MAAO,IAAKM,MAAKN,EAClB,CAQA,OAAAC,CAAQgD,GAGP,OAFA3C,MAAKL,EAAWgD,EAET3C,IACR,CAOA,SAAAsD,GACC,GAAiB,IAAbtD,KAAKF,IACR,MAAO,CAAEyD,MAAOvD,KAAKM,KAAMkD,QAAS,EAAGC,MAAOzD,KAAKM,MAGpD,MAAM2B,EAAMD,KAAKC,MACjB,IAAIsB,EAAQ,EACRC,EAAU,EACVC,EAAQ,EAEZ,IAAK,IAAIpB,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KACzB,IAAbmB,EAAET,QACL6B,IACAF,KACUlB,EAAET,OAASK,EACrBsB,IAEAC,IAIF,MAAO,CAAED,QAAOC,UAASC,QAC1B,CAOA,SAAAC,GACC,GAAiB,IAAb1D,KAAKF,IACR,MAAO,CAAEyD,MAAOvD,KAAKoB,OAAQoC,QAAS,GAAIC,MAAOzD,KAAKoB,QAGvD,MAAMa,EAAMD,KAAKC,MACXsB,EAAQ,GACRC,EAAU,GACVC,EAAQ,GAEd,IAAK,IAAIpB,EAAIrC,KAAKC,MAAa,OAANoC,EAAYA,EAAIA,EAAEnB,KACzB,IAAbmB,EAAET,QACL2B,EAAMF,KAAKhB,EAAExB,KACb4C,EAAMJ,KAAKhB,EAAExB,MACHwB,EAAET,OAASK,EACrBsB,EAAMF,KAAKhB,EAAExB,KAEb2C,EAAQH,KAAKhB,EAAExB,KAIjB,MAAO,CAAE0C,QAAOC,UAASC,QAC1B,CAOA,WAAAE,GACC,MAAMD,EAAY1D,KAAK0D,YAEvB,MAAO,CACNH,MAAOvD,KAAKyC,OAAOiB,EAAUH,OAC7BC,QAASxD,KAAKyC,OAAOiB,EAAUF,SAC/BC,MAAOzD,KAAKyC,OAAOiB,EAAUD,OAE/B,CAQA,EAAAN,GACC,GAAkB,IAAdnD,KAAKM,KAGR,OAFAN,KAAKC,MAAQ,UACbD,KAAKK,KAAO,MAIb,MAAMe,EAAOpB,KAAKoB,OAClBpB,KAAKC,MAAQ,KACbD,KAAKK,KAAO,KAEZ,IAAK,IAAIoB,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACrC,MAAMX,EAAOd,KAAKE,MAAMkB,EAAKK,IACzBX,UACgB,OAAfd,KAAKC,OACRD,KAAKC,MAAQa,EACbA,EAAKG,KAAO,OAEZH,EAAKG,KAAOjB,KAAKK,KACjBL,KAAKK,KAAKa,KAAOJ,GAElBA,EAAKI,KAAO,KACZlB,KAAKK,KAAOS,EAEd,CACD,EAaM,SAAS8C,EAAI/D,EAAM,IAAMC,EAAM,EAAGC,GAAW,GACnD,GAAI8D,MAAMhE,IAAQA,EAAM,EACvB,MAAM,IAAIiE,UAAU,qBAGrB,GAAID,MAAM/D,IAAQA,EAAM,EACvB,MAAM,IAAIgE,UAAU,qBAGrB,GAAwB,kBAAb/D,EACV,MAAM,IAAI+D,UAAU,0BAGrB,OAAO,IAAIrE,EAAII,EAAKC,EAAKC,EAC1B,QAAAN,SAAAmE"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index cd60d43..a6de570 100644 --- a/src/lru.js +++ b/src/lru.js @@ -458,15 +458,20 @@ export class LRU { const now = Date.now(); let removed = 0; - for (let x = this.first; x !== null; x = x.next) { + for (let x = this.first; x !== null; ) { + const next = x.next; if (x.expiry !== 0 && x.expiry <= now) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; this.size--; removed++; + this.#unlink(x); + x.prev = null; + x.next = null; } } + x = next; } if (removed > 0) { From 52965ada1c2d35f7a3a5fcac9f0235b9c896bdd7 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:51:08 -0400 Subject: [PATCH 23/55] Fix onEvict() documentation - callback also triggers on direct evict() calls --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 434f08d..8847c30 100644 --- a/docs/API.md +++ b/docs/API.md @@ -462,7 +462,7 @@ cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); **Returns:** `LRU` - this instance (for chaining) -**Note:** Only the last registered callback will be used. Only triggers on explicit eviction via `set()` or `setWithEvicted()` when cache is full, not on TTL expiry. +**Note:** Only the last registered callback will be used. Triggers on explicit eviction via `evict()` or when `set()`/`setWithEvicted()` evicts items due to cache being full. Does not trigger on TTL expiry (items are silently removed). --- From 7df06c3471447e7eb2ecaa270d325521745090fc Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:52:01 -0400 Subject: [PATCH 24/55] Fix test name - should test peek() during forEach, not get() --- tests/unit/lru.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index 84d1fc3..c2452ba 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -812,14 +812,11 @@ describe("LRU Cache", function () { assert.deepEqual(cache.keys(), ["a", "b", "c"]); }); - it("should not modify LRU order when calling get() during forEach", function () { + it("should not modify LRU order when calling peek() during forEach", function () { const result = []; cache.forEach((value, key) => { result.push(key); - // Note: calling get() during forEach modifies the linked list - // which breaks the traversal - this is expected behavior - // Users should avoid modifying the cache during iteration - cache.peek(key); // Use peek instead of get to avoid modification + cache.peek(key); }); assert.deepEqual(result, ["a", "b", "c"]); From 75f8b40ded8c663f8ad7a1c3da5eebd2c98d72ee Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:54:33 -0400 Subject: [PATCH 25/55] Fix documentation inaccuracies - README.md: Add missing methods (expiresAt, sizeByTTL, keysByTTL, valuesByTTL) - README.md: Fix entries() description - with keys parameter, order matches input - README.md: Clarify onEvict() triggers on evict() and set()/setWithEvicted() eviction - API.md: Fix entries() - input array order, not LRU order, when keys provided - API.md: Clarify onEvict() triggering conditions explicitly - API.md: Add note about cleanup() silently removing items - API.md: Clarify setWithEvicted() can return null when no eviction needed --- README.md | 6 +++++- docs/API.md | 21 ++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 772d34a..6ad9a13 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ const cache4 = lru(100, 60000, true); // with resetTtl enabled | `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. Must be >= 0. | | `resetTtl` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | +**Returns:** `LRU` - New cache instance + **Throws:** `TypeError` if parameters are invalid ### Class: `new LRU(max?, ttl?, resetTtl?)` @@ -168,6 +170,7 @@ const cache = new LRU(100, 5000); | `clear()` | Remove all items. Returns `this` for chaining. | | `delete(key)` | Remove an item by key. Returns `this` for chaining. | | `entries(keys?)` | Get `[key, value]` pairs in LRU order. | +| `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `evict()` | Remove the least recently used item. Returns `this` for chaining. | | `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | | `forEach(callback, thisArg?)` | Iterate over items in LRU order. Returns `this` for chaining. | @@ -177,6 +180,7 @@ const cache = new LRU(100, 5000); | `hasAll(keys)` | Check if ALL keys exist. Returns `boolean`. | | `hasAny(keys)` | Check if ANY key exists. Returns `boolean`. | | `keys()` | Get all keys in LRU order (oldest first). Returns `string[]`. | +| `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `keysByTTL()` | Get keys by TTL status. Returns `{valid, expired, noTTL}`. | | `peek(key)` | Retrieve a value without LRU update. Returns value or `undefined`. | | `set(key, value)` | Store a value. Returns `this` for chaining. | @@ -186,7 +190,7 @@ const cache = new LRU(100, 5000); | `toJSON()` | Serialize cache to JSON format. Returns array of items. | | `values(keys?)` | Get all values, or values for specific keys. Returns array of values. | | `valuesByTTL()` | Get values by TTL status. Returns `{valid, expired, noTTL}`. | -| `onEvict(callback)` | Register eviction callback. Returns `this` for chaining. | +| `onEvict(callback)` | Register eviction callback (triggers on `evict()` or when `set()`/`setWithEvicted()` evicts). Returns `this` for chaining. | ## Common Patterns diff --git a/docs/API.md b/docs/API.md index 8847c30..4ae0d9a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -163,7 +163,7 @@ console.log(cache.ttl); // 60000 ### `cleanup()` -Removes expired items without affecting LRU order. +Removes expired items without affecting LRU order. Silently removes expired items without triggering the `onEvict()` callback. ```javascript cache.set("a", 1).set("b", 2); @@ -215,7 +215,7 @@ console.log(cache.size); // 1 ### `entries(keys?)` -Returns `[key, value]` pairs in LRU order. +Returns `[key, value]` pairs. Without `keys`: returns all entries in LRU order. With `keys`: order matches the input array. ```javascript cache.set("a", 1).set("b", 2).set("c", 3); @@ -223,7 +223,7 @@ console.log(cache.entries()); // [['a', 1], ['b', 2], ['c', 3]] console.log(cache.entries(["c", "a"])); -// [['c', 3], ['a', 1]] - respects LRU order +// [['c', 3], ['a', 1]] - order matches input array ``` **Parameters:** @@ -382,9 +382,7 @@ cache.hasAll(["a", "nonexistent"]); // false | ------ | ---------- | -------------------- | | `keys` | `string[]` | Array of keys to check | -**Returns:** `boolean` - True if all keys exist and are not expired - -**Note:** Returns `true` for empty arrays. +**Returns:** `boolean` - True if all keys exist and are not expired. Returns `true` for empty arrays (vacuous truth). --- @@ -404,9 +402,7 @@ cache.hasAny(["nonexistent1", "nonexistent2"]); // false | ------ | ---------- | -------------------- | | `keys` | `string[]` | Array of keys to check | -**Returns:** `boolean` - True if any key exists and is not expired - -**Note:** Returns `false` for empty arrays. +**Returns:** `boolean` - True if any key exists and is not expired. Returns `false` for empty arrays. --- @@ -462,7 +458,10 @@ cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); **Returns:** `LRU` - this instance (for chaining) -**Note:** Only the last registered callback will be used. Triggers on explicit eviction via `evict()` or when `set()`/`setWithEvicted()` evicts items due to cache being full. Does not trigger on TTL expiry (items are silently removed). +**Note:** Only the last registered callback will be used. Triggers on: +- Explicit eviction via `evict()` +- Implicit eviction via `set()`/`setWithEvicted()` when cache is at max capacity +Does NOT trigger on TTL expiry (items are silently removed). --- @@ -528,7 +527,7 @@ console.log(cache.keys()); // ['b', 'c'] | `key` | `string` | Item key | | `value` | `*` | Item value | -**Returns:** `{ key, value, expiry } | null` - Evicted item or null +**Returns:** `{ key, value, expiry } | null` - Evicted item (if any) or `null` when no eviction occurs --- From ae5f8cb46b917973921b11d4c35c3a54ffa06d6d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 14:59:28 -0400 Subject: [PATCH 26/55] Fix clear() memory leak - clear prev/next pointers on all nodes When clear() was called, linked list nodes retained their prev/next pointers. If external references to nodes existed, the entire list remained reachable and couldn't be garbage collected. This fix iterates through all nodes before clearing references, ensuring proper cleanup and allowing GC even with external node refs. --- dist/tiny-lru.cjs | 7 +++++++ dist/tiny-lru.js | 7 +++++++ dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 7 +++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 13764bf..783b616 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -45,6 +45,13 @@ class LRU { * @returns {LRU} The LRU instance for method chaining. */ clear() { + for (let x = this.first; x !== null; ) { + const next = x.next; + x.prev = null; + x.next = null; + x = next; + } + this.first = null; this.items = Object.create(null); this.last = null; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 558aa34..be52ad0 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -43,6 +43,13 @@ class LRU { * @returns {LRU} The LRU instance for method chaining. */ clear() { + for (let x = this.first; x !== null; ) { + const next = x.next; + x.prev = null; + x.next = null; + x = next; + } + this.first = null; this.items = Object.create(null); this.last = null; diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index c2ec49e..e49e9af 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","key","item","undefined","unlink","prev","next","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index a6de570..36a1910 100644 --- a/src/lru.js +++ b/src/lru.js @@ -36,6 +36,13 @@ export class LRU { * @returns {LRU} The LRU instance for method chaining. */ clear() { + for (let x = this.first; x !== null; ) { + const next = x.next; + x.prev = null; + x.next = null; + x = next; + } + this.first = null; this.items = Object.create(null); this.last = null; From d1157db83dbf05ef8daa6c4dbfd113a20eaf7486 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:00:35 -0400 Subject: [PATCH 27/55] Fix TTL semantics inconsistency - expiry === 0 should be noTTL, not expired get() was treating expiry === 0 as expired when ttl > 0, but cleanup(), sizeByTTL(), keysByTTL(), and valuesByTTL() all treat expiry === 0 as noTTL/valid items. The fix adds 'item.expiry !== 0' check to get() so noTTL items are never incorrectly treated as expired. --- coverage.txt | 4 ++-- dist/tiny-lru.cjs | 4 ++-- dist/tiny-lru.js | 4 ++-- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coverage.txt b/coverage.txt index e5c0881..8c2a060 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.25 | 100.00 | +ℹ lru.js | 100.00 | 99.26 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.25 | 100.00 | +ℹ all files | 100.00 | 99.26 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 783b616..9de0671 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -179,8 +179,8 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { + // Check TTL only if item has expiration set + if (this.ttl > 0 && item.expiry !== 0) { if (item.expiry <= Date.now()) { this.delete(key); this.#stats.misses++; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index be52ad0..946b025 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -177,8 +177,8 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { + // Check TTL only if item has expiration set + if (this.ttl > 0 && item.expiry !== 0) { if (item.expiry <= Date.now()) { this.delete(key); this.#stats.misses++; diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index e49e9af..cf9ac11 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){for(let t=this.first;null!==t;){const s=t.next;t.prev=null,t.next=null,t=s}return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0&&0!==s.expiry&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if item has expiration set\n\t\t\tif (this.ttl > 0 && item.expiry !== 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index 36a1910..fb4e857 100644 --- a/src/lru.js +++ b/src/lru.js @@ -170,8 +170,8 @@ export class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { + // Check TTL only if item has expiration set + if (this.ttl > 0 && item.expiry !== 0) { if (item.expiry <= Date.now()) { this.delete(key); this.#stats.misses++; From 01cfb31f8fbb3fbc70771b88c2e0ebd8e30e19fd Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:01:10 -0400 Subject: [PATCH 28/55] Add #isExpired() private function for DRY TTL expiration check Extracts TTL expiration logic into a single private function that: - Returns false if ttl === 0 or item.expiry === 0 (noTTL items) - Returns true if item.expiry <= Date.now() for items with TTL set Updates get() and cleanup() to use #isExpired() instead of duplicating logic. Size reductions from DRY: get() reduced by 4 lines, cleanup() by 5 lines. --- coverage.txt | 4 ++-- dist/tiny-lru.cjs | 38 +++++++++++++++++++++++--------------- dist/tiny-lru.js | 38 +++++++++++++++++++++++--------------- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 38 +++++++++++++++++++++++--------------- 6 files changed, 73 insertions(+), 49 deletions(-) diff --git a/coverage.txt b/coverage.txt index 8c2a060..3800a75 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.26 | 100.00 | +ℹ lru.js | 100.00 | 99.27 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.26 | 100.00 | +ℹ all files | 100.00 | 99.27 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 9de0671..a3a7422 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -157,6 +157,21 @@ class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Checks if an item has expired. + * + * @param {Object} item - The cache item to check. + * @returns {boolean} True if the item has expired, false otherwise. + * @private + */ + #isExpired(item) { + if (this.ttl === 0 || item.expiry === 0) { + return false; + } + + return item.expiry <= Date.now(); + } + /** * Retrieves a value from the cache by key without updating LRU order. * Note: Does not perform TTL checks or remove expired items. @@ -179,21 +194,15 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if item has expiration set - if (this.ttl > 0 && item.expiry !== 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - this.#stats.misses++; - - return undefined; - } + if (!this.#isExpired(item)) { + this.moveToEnd(item); + this.#stats.hits++; + return item.value; } - // Fast LRU update without full set() overhead - this.moveToEnd(item); - this.#stats.hits++; - - return item.value; + this.delete(key); + this.#stats.misses++; + return undefined; } this.#stats.misses++; @@ -471,12 +480,11 @@ class LRU { return 0; } - const now = Date.now(); let removed = 0; for (let x = this.first; x !== null; ) { const next = x.next; - if (x.expiry !== 0 && x.expiry <= now) { + if (this.#isExpired(x)) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 946b025..6b0497c 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -155,6 +155,21 @@ class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Checks if an item has expired. + * + * @param {Object} item - The cache item to check. + * @returns {boolean} True if the item has expired, false otherwise. + * @private + */ + #isExpired(item) { + if (this.ttl === 0 || item.expiry === 0) { + return false; + } + + return item.expiry <= Date.now(); + } + /** * Retrieves a value from the cache by key without updating LRU order. * Note: Does not perform TTL checks or remove expired items. @@ -177,21 +192,15 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if item has expiration set - if (this.ttl > 0 && item.expiry !== 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - this.#stats.misses++; - - return undefined; - } + if (!this.#isExpired(item)) { + this.moveToEnd(item); + this.#stats.hits++; + return item.value; } - // Fast LRU update without full set() overhead - this.moveToEnd(item); - this.#stats.hits++; - - return item.value; + this.delete(key); + this.#stats.misses++; + return undefined; } this.#stats.misses++; @@ -469,12 +478,11 @@ class LRU { return 0; } - const now = Date.now(); let removed = 0; for (let x = this.first; x !== null; ) { const next = x.next; - if (x.expiry !== 0 && x.expiry <= now) { + if (this.#isExpired(x)) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index cf9ac11..dc786a9 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){for(let t=this.first;null!==t;){const s=t.next;t.prev=null,t.next=null,t=s}return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0&&0!==s.expiry&&s.expiry<=Date.now()?(this.delete(t),void this.#t.misses++):(this.moveToEnd(s),this.#t.hits++,s.value);this.#t.misses++}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#e(),s}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#e(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;sDate.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if item has expiration set\n\t\t\tif (this.ttl > 0 && item.expiry !== 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\t\t\t\t\tthis.#stats.misses++;\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\t\t\tthis.#stats.hits++;\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (x.expiry !== 0 && x.expiry <= now) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","peek","get","Date","now","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index fb4e857..94e6a7f 100644 --- a/src/lru.js +++ b/src/lru.js @@ -148,6 +148,21 @@ export class LRU { return item !== undefined ? item.expiry : undefined; } + /** + * Checks if an item has expired. + * + * @param {Object} item - The cache item to check. + * @returns {boolean} True if the item has expired, false otherwise. + * @private + */ + #isExpired(item) { + if (this.ttl === 0 || item.expiry === 0) { + return false; + } + + return item.expiry <= Date.now(); + } + /** * Retrieves a value from the cache by key without updating LRU order. * Note: Does not perform TTL checks or remove expired items. @@ -170,21 +185,15 @@ export class LRU { const item = this.items[key]; if (item !== undefined) { - // Check TTL only if item has expiration set - if (this.ttl > 0 && item.expiry !== 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - this.#stats.misses++; - - return undefined; - } + if (!this.#isExpired(item)) { + this.moveToEnd(item); + this.#stats.hits++; + return item.value; } - // Fast LRU update without full set() overhead - this.moveToEnd(item); - this.#stats.hits++; - - return item.value; + this.delete(key); + this.#stats.misses++; + return undefined; } this.#stats.misses++; @@ -462,12 +471,11 @@ export class LRU { return 0; } - const now = Date.now(); let removed = 0; for (let x = this.first; x !== null; ) { const next = x.next; - if (x.expiry !== 0 && x.expiry <= now) { + if (this.#isExpired(x)) { const key = x.key; if (this.items[key] !== undefined) { delete this.items[key]; From eadb29448afae8071730fc10b1a3363d677152b2 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:07:18 -0400 Subject: [PATCH 29/55] Fix has() TTL semantics - use #isExpired() consistently has() was incorrectly treating expiry === 0 as expired when ttl > 0, while #isExpired() and cleanup() correctly treat expiry === 0 as noTTL. Aligns has() with the rest of the TTL semantics by using #isExpired(). --- coverage.txt | 4 ++-- dist/tiny-lru.cjs | 4 ++-- dist/tiny-lru.js | 4 ++-- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- src/lru.js | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coverage.txt b/coverage.txt index 3800a75..8c2a060 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.27 | 100.00 | +ℹ lru.js | 100.00 | 99.26 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.27 | 100.00 | +ℹ all files | 100.00 | 99.26 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index a3a7422..793e0de 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -213,11 +213,11 @@ class LRU { * Checks if a key exists in the cache. * * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. + * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + return item !== undefined && !this.#isExpired(item); } /** diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 6b0497c..36a0179 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -211,11 +211,11 @@ class LRU { * Checks if a key exists in the cache. * * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. + * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + return item !== undefined && !this.#isExpired(item); } /** diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index dc786a9..c9ad017 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){for(let t=this.first;null!==t;){const s=t.next;t.prev=null,t.next=null,t=s}return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;iDate.now())}#i(t){null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),this.last===t&&(this.last=t.prev)}moveToEnd(t){this.last!==t&&(this.#i(t),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,i=0;for(;null!==s;)t[i++]=s.key,s=s.next;return t}setWithEvicted(t,s){let i=null,e=this.items[t];return void 0!==e?(e.value=s,this.resetTtl&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists and is not expired, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && !this.#isExpired(item);\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index 94e6a7f..671144d 100644 --- a/src/lru.js +++ b/src/lru.js @@ -204,11 +204,11 @@ export class LRU { * Checks if a key exists in the cache. * * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. + * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + return item !== undefined && !this.#isExpired(item); } /** From 470cbdf788a5b72a18d628be1d1a3f2ae6eecf94 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:08:19 -0400 Subject: [PATCH 30/55] docs: consolidate duplicate entries() method descriptions Removed duplicate entries() method entries (rows 172-173 and 183) and combined into single clear description: 'Get [key, value] pairs. Without keys: LRU order. With keys: input array order.' --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 6ad9a13..dcaa1ce 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,6 @@ const cache = new LRU(100, 5000); | `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | | `clear()` | Remove all items. Returns `this` for chaining. | | `delete(key)` | Remove an item by key. Returns `this` for chaining. | -| `entries(keys?)` | Get `[key, value]` pairs in LRU order. | | `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `evict()` | Remove the least recently used item. Returns `this` for chaining. | | `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | @@ -180,7 +179,6 @@ const cache = new LRU(100, 5000); | `hasAll(keys)` | Check if ALL keys exist. Returns `boolean`. | | `hasAny(keys)` | Check if ANY key exists. Returns `boolean`. | | `keys()` | Get all keys in LRU order (oldest first). Returns `string[]`. | -| `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `keysByTTL()` | Get keys by TTL status. Returns `{valid, expired, noTTL}`. | | `peek(key)` | Retrieve a value without LRU update. Returns value or `undefined`. | | `set(key, value)` | Store a value. Returns `this` for chaining. | From 1ea54ab1fda21c7cc6494fcbfeed6423f64955e1 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:13:51 -0400 Subject: [PATCH 31/55] docs: fix backticks and add full description to Properties table Changed 'object | null' to 'object' | 'null' in Type column for 'first' and 'last'. Also added the missing node details to 'last' property description to match 'first'. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dcaa1ce..1278c12 100644 --- a/README.md +++ b/README.md @@ -155,8 +155,8 @@ const cache = new LRU(100, 5000); | Property | Type | Description | | --------- | ---------------- | ------------------------------------------ | -| `first` | `object | null` | Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | -| `last` | `object | null` | Most recently used item | +| `first` | `object` \| `null` | Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | +| `last` | `object` \| `null` | Most recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | | `max` | `number` | Maximum items allowed | | `size` | `number` | Current number of items | | `ttl` | `number` | Time-to-live in milliseconds | From 15b484fa03bdc52b589733ab0446487bdb2a1cb2 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:22:08 -0400 Subject: [PATCH 32/55] test: fix forEach delete test title to reflect actual behavior The test collects keys during forEach but deletes them afterward, so it doesn't actually test mutation during iteration. Updated title to clarify: 'should allow deleting items after collecting keys during iteration'. --- tests/unit/lru.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index c2452ba..80cb18b 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -858,7 +858,7 @@ describe("LRU Cache", function () { assert.deepEqual(result, ["b", "c", "a", "d"]); }); - it("should allow deleting items during iteration", function () { + it("should allow deleting items after collecting keys during iteration", function () { const keysToDelete = []; cache.forEach((value, key) => { if (value === 2) { From be91f2ce3e3d88dba5779452d8b364cc84e4a14c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:25:48 -0400 Subject: [PATCH 33/55] docs: fix noTTL description in keysByTTL() and sizeByTTL() The noTTL property correctly identifies items where item.expiry === 0, not where cache ttl === 0. Updated documentation to reflect this. --- docs/API.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 4ae0d9a..25fc788 100644 --- a/docs/API.md +++ b/docs/API.md @@ -433,7 +433,7 @@ console.log(cache.keysByTTL()); **Returns:** `Object` - Object with three properties: - `valid` - Array of valid (non-expired) keys - `expired` - Array of expired keys -- `noTTL` - Array of keys without TTL (when `ttl=0`) +- `noTTL` - Array of keys without TTL (`expiry === 0`) --- @@ -544,7 +544,7 @@ console.log(cache.sizeByTTL()); **Returns:** `Object` - Object with three properties: - `valid` - Number of items that haven't expired - `expired` - Number of expired items -- `noTTL` - Number of items without TTL (when `ttl=0`) +- `noTTL` - Number of items without TTL (`expiry === 0`) **Note:** Items without TTL (expiry === 0) count as both valid and noTTL. From 0f5ff1e4a713a801b0862490a12cf82db0a8e3ef Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:28:57 -0400 Subject: [PATCH 34/55] fix(types): make items property return Record | undefined> This correctly reflects that items[key] returns undefined for missing keys, --- types/lru.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/lru.d.ts b/types/lru.d.ts index b07a10c..f46e69f 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -53,7 +53,7 @@ export class LRU { /** Pointer to the least recently used item (first to be evicted) */ first: LRUItem | null; /** Hash map for O(1) key-based access to cache nodes */ - items: Record>; + items: Record | undefined>; /** Pointer to the most recently used item */ last: LRUItem | null; /** Maximum number of items to store (0 = unlimited) */ From af1d817fdb55a16fa00c22fe4f66a6a544787edf Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:30:28 -0400 Subject: [PATCH 35/55] fix: validate onEvict callback is a function before storing Prevents runtime error if onEvict() is called with undefined or non-function value. The evict() method already checked #onEvict !== null, but that allows non-functions. --- coverage.txt | 4 ++-- src/lru.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage.txt b/coverage.txt index 8c2a060..bd5ff4a 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.26 | 100.00 | +ℹ lru.js | 100.00 | 98.54 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.26 | 100.00 | +ℹ all files | 100.00 | 98.54 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/src/lru.js b/src/lru.js index 671144d..5218e42 100644 --- a/src/lru.js +++ b/src/lru.js @@ -126,7 +126,7 @@ export class LRU { item.prev = null; item.next = null; - if (this.#onEvict !== null) { + if (typeof this.#onEvict === "function") { this.#onEvict({ key: item.key, value: item.value, @@ -530,7 +530,7 @@ export class LRU { * @returns {LRU} The LRU instance for method chaining. */ onEvict(callback) { - this.#onEvict = callback; + this.#onEvict = typeof callback === "function" ? callback : null; return this; } From 5aa9087674f23e209b0e73fd15313df88e324c97 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:31:06 -0400 Subject: [PATCH 36/55] fix: throw TypeError if onEvict callback is not a function --- coverage.txt | 4 ++-- src/lru.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/coverage.txt b/coverage.txt index bd5ff4a..5dd5bf1 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 98.54 | 100.00 | +ℹ lru.js | 99.70 | 98.54 | 100.00 | 534-535 ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 98.54 | 100.00 | +ℹ all files | 99.70 | 98.54 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/src/lru.js b/src/lru.js index 5218e42..2338294 100644 --- a/src/lru.js +++ b/src/lru.js @@ -126,7 +126,7 @@ export class LRU { item.prev = null; item.next = null; - if (typeof this.#onEvict === "function") { + if (this.#onEvict !== null) { this.#onEvict({ key: item.key, value: item.value, @@ -530,7 +530,11 @@ export class LRU { * @returns {LRU} The LRU instance for method chaining. */ onEvict(callback) { - this.#onEvict = typeof callback === "function" ? callback : null; + if (typeof callback !== "function") { + throw new TypeError("onEvict callback must be a function"); + } + + this.#onEvict = callback; return this; } From 49b49407eff7619c23bd1432abd678603dae43e8 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:38:22 -0400 Subject: [PATCH 37/55] test: add onEvict validation tests for non-function callbacks --- coverage.txt | 4 ++-- tests/unit/lru.test.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/coverage.txt b/coverage.txt index 5dd5bf1..355f23e 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 99.70 | 98.54 | 100.00 | 534-535 +ℹ lru.js | 100.00 | 99.28 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 99.70 | 98.54 | 100.00 | +ℹ all files | 100.00 | 99.28 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index 80cb18b..ee1682a 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -1292,6 +1292,17 @@ describe("LRU Cache", function () { assert.equal(evictedItems[1].key, "b"); assert.equal(evictedItems[2].key, "c"); }); + + it("should throw TypeError if callback is not a function", function () { + assert.throws(() => cache.onEvict(undefined), { + message: "onEvict callback must be a function", + }); + assert.throws(() => cache.onEvict(null), { message: "onEvict callback must be a function" }); + assert.throws(() => cache.onEvict("not a function"), { + message: "onEvict callback must be a function", + }); + assert.throws(() => cache.onEvict({}), { message: "onEvict callback must be a function" }); + }); }); describe("sizeByTTL method", function () { From 522ff6b9c6256191fe79f9859b4706f8127fe42c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:39:51 -0400 Subject: [PATCH 38/55] docs: update coverage metrics to 99.28% branches --- AGENTS.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c969a55..5f1b56a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,7 @@ Source code is in `src/`. ## Testing - Framework: Node.js built-in test runner (`node --test`) -- Coverage: 100% lines, 99.25% branches, 100% functions +- Coverage: 100% lines, 99.28% branches, 100% functions - Test pattern: `tests/**/*.js` - All tests must pass with 100% line coverage before merging - Run: `npm test` (lint + tests) or `npm run coverage` for coverage report diff --git a/README.md b/README.md index 1278c12..e9ec553 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ Build produces multiple module formats: | Metric | Coverage | | --------- | -------- | | Lines | 100% | -| Branches | 99.25% | +| Branches | 99.28% | | Functions | 100% | ## Contributing From 8b440cf07b38a4a99da377cf5e0394ac2ac2e468 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:39:59 -0400 Subject: [PATCH 39/55] docs: add testing and coverage requirements to CODE_STYLE_GUIDE --- docs/CODE_STYLE_GUIDE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/CODE_STYLE_GUIDE.md b/docs/CODE_STYLE_GUIDE.md index c129134..85530c4 100644 --- a/docs/CODE_STYLE_GUIDE.md +++ b/docs/CODE_STYLE_GUIDE.md @@ -185,6 +185,13 @@ if (isNaN(max) || max < 0) { } ``` +## Testing and Coverage + +- All tests must pass before merging +- 100% line coverage required +- Run tests with `npm test` (includes linting) +- Generate coverage report with `npm run coverage` + ## Lint Configuration The project uses oxlint. Run `npm run lint` to check code style. From 9fbe54cc488f88da8cb615b0184f2f7a53b737c4 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:45:14 -0400 Subject: [PATCH 40/55] chore: build and update coverage metrics to 99.28% branches --- dist/tiny-lru.cjs | 4 ++++ dist/tiny-lru.js | 4 ++++ dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 793e0de..d38a8ec 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -539,6 +539,10 @@ class LRU { * @returns {LRU} The LRU instance for method chaining. */ onEvict(callback) { + if (typeof callback !== "function") { + throw new TypeError("onEvict callback must be a function"); + } + this.#onEvict = callback; return this; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index 36a0179..e4a7d73 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -537,6 +537,10 @@ class LRU { * @returns {LRU} The LRU instance for method chaining. */ onEvict(callback) { + if (typeof callback !== "function") { + throw new TypeError("onEvict callback must be a function"); + } + this.#onEvict = callback; return this; diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index c9ad017..718874c 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){for(let t=this.first;null!==t;){const s=t.next;t.prev=null,t.next=null,t=s}return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){if("function"!=typeof t)throw new TypeError("onEvict callback must be a function");return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists and is not expired, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && !this.#isExpired(item);\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN","TypeError"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTtl = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTtl = resetTtl;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists and is not expired, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && !this.#isExpired(item);\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"onEvict callback must be a function\");\n\t\t}\n\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","TypeError","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN"],"mappings":"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"} \ No newline at end of file From 762df14c1d0734cee11b453fde51ce34395c918b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:45:52 -0400 Subject: [PATCH 41/55] docs: fix clear() time complexity from O(1) to O(n) clear() now iterates through linked list to nullify prev/next pointers, allowing proper garbage collection of nodes. --- docs/TECHNICAL_DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md index c17d043..3be4445 100644 --- a/docs/TECHNICAL_DOCUMENTATION.md +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -128,7 +128,7 @@ sequenceDiagram | `delete(key)` | O(1) | O(1) | O(1) | Remove item from cache | | `has(key)` | O(1) | O(1) | O(1) | Check key existence | | `peek(key)` | O(1) | O(1) | O(1) | Retrieve value without LRU update | -| `clear()` | O(1) | O(1) | O(1) | Reset all pointers | +| `clear()` | O(n) | O(n) | O(1) | Reset all pointers, nullify node links | | `evict()` | O(1) | O(1) | O(1) | Remove least recently used item | | `expiresAt(key)` | O(1) | O(1) | O(1) | Get expiration timestamp | | `moveToEnd(item)` | O(1) | O(1) | O(1) | Internal: optimize LRU positioning | From f31dadbd5dbdddb4dcd0a4e0905e2ac3789a9aa1 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:46:31 -0400 Subject: [PATCH 42/55] docs: fix stats() sets description to include setWithEvicted() The sets counter increments in both set() and setWithEvicted() methods. Updated documentation to reflect actual behavior. --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 25fc788..1c8853d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -572,7 +572,7 @@ console.log(cache.stats()); **Returns:** `Object` - Statistics object with the following properties: - `hits` - Number of successful get() calls - `misses` - Number of failed get() calls -- `sets` - Number of set() calls +- `sets` - Number of set() and setWithEvicted() calls - `deletes` - Number of delete() calls - `evictions` - Number of evicted items From b151f13ac2e4047cc6e77b78f1aaa0060ea842ee Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 15:55:55 -0400 Subject: [PATCH 43/55] docs: Fix stats().deletes description The delete count includes both explicit delete() calls AND internal removal of expired items by get(). Update the documentation to reflect this behavior accurately. --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 1c8853d..ec363d1 100644 --- a/docs/API.md +++ b/docs/API.md @@ -573,7 +573,7 @@ console.log(cache.stats()); - `hits` - Number of successful get() calls - `misses` - Number of failed get() calls - `sets` - Number of set() and setWithEvicted() calls -- `deletes` - Number of delete() calls +- `deletes` - Number of delete() calls plus internal removal of expired items by get() - `evictions` - Number of evicted items --- From be0cfaf949770513af1c63e21feadc1bd99a0ac8 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:00:15 -0400 Subject: [PATCH 44/55] docs: Remove nodei.co npm badge Remove the redundant nodei.co npm badge which duplicated npm version and downloads information already shown in the badges above. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e9ec553..34b9faf 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ [![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions?query=workflow%3Aci) [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://app.codecov.io/gh/avoidwork/tiny-lru) -[![npm](https://nodei.co/npm/tiny-lru.png?downloads=true&stars=true)](https://www.npmjs.com/package/tiny-lru) - A high-performance, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support. ## What is an LRU Cache? From 3995ab99181658496002934325890e958b8faee6 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:01:40 -0400 Subject: [PATCH 45/55] docs: Sort Properties and Methods alphabetically - Properties: first, last, max, resetTtl, size, ttl - Methods: clear, cleanup, delete, entries, evict, forEach, get, getMany, has, hasAll, hasAny, keys, keysByTTL, onEvict, peek, set, setWithEvicted, sizeByTTL, stats, toJSON, values, valuesByTTL, expiresAt --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 34b9faf..0a26790 100644 --- a/README.md +++ b/README.md @@ -156,20 +156,19 @@ const cache = new LRU(100, 5000); | `first` | `object` \| `null` | Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | | `last` | `object` \| `null` | Most recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | | `max` | `number` | Maximum items allowed | +| `resetTtl`| `boolean` | Whether TTL resets on `set()` updates | | `size` | `number` | Current number of items | | `ttl` | `number` | Time-to-live in milliseconds | -| `resetTtl`| `boolean` | Whether TTL resets on `set()` updates | ### Methods | Method | Description | | --------------------------- | ---------------------------------------------- | -| `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | | `clear()` | Remove all items. Returns `this` for chaining. | +| `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | | `delete(key)` | Remove an item by key. Returns `this` for chaining. | | `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `evict()` | Remove the least recently used item. Returns `this` for chaining. | -| `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | | `forEach(callback, thisArg?)` | Iterate over items in LRU order. Returns `this` for chaining. | | `get(key)` | Retrieve a value. Moves item to most recent. Returns value or `undefined`. | | `getMany(keys)` | Batch retrieve multiple items. Returns object mapping keys to values. | @@ -178,6 +177,7 @@ const cache = new LRU(100, 5000); | `hasAny(keys)` | Check if ANY key exists. Returns `boolean`. | | `keys()` | Get all keys in LRU order (oldest first). Returns `string[]`. | | `keysByTTL()` | Get keys by TTL status. Returns `{valid, expired, noTTL}`. | +| `onEvict(callback)` | Register eviction callback (triggers on `evict()` or when `set()`/`setWithEvicted()` evicts). Returns `this` for chaining. | | `peek(key)` | Retrieve a value without LRU update. Returns value or `undefined`. | | `set(key, value)` | Store a value. Returns `this` for chaining. | | `setWithEvicted(key, value)` | Store value, return evicted item if full. Returns `{key, value, expiry} | null`. | @@ -186,7 +186,7 @@ const cache = new LRU(100, 5000); | `toJSON()` | Serialize cache to JSON format. Returns array of items. | | `values(keys?)` | Get all values, or values for specific keys. Returns array of values. | | `valuesByTTL()` | Get values by TTL status. Returns `{valid, expired, noTTL}`. | -| `onEvict(callback)` | Register eviction callback (triggers on `evict()` or when `set()`/`setWithEvicted()` evicts). Returns `this` for chaining. | +| `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | ## Common Patterns From 6440f1e68b4c6cf1c7dd2b29416b8b04b18ed37e Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:02:22 -0400 Subject: [PATCH 46/55] docs: Sort Methods section alphabetically Updated Methods table to be alphabetically sorted: - cleanup, clear, delete, entries, evict, expiresAt, forEach, get, getMany, has, hasAll, hasAny, keys, keysByTTL, onEvict, peek, set, setWithEvicted, sizeByTTL, stats, toJSON, values, valuesByTTL --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a26790..754fcc3 100644 --- a/README.md +++ b/README.md @@ -164,11 +164,12 @@ const cache = new LRU(100, 5000); | Method | Description | | --------------------------- | ---------------------------------------------- | -| `clear()` | Remove all items. Returns `this` for chaining. | | `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | +| `clear()` | Remove all items. Returns `this` for chaining. | | `delete(key)` | Remove an item by key. Returns `this` for chaining. | | `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | | `evict()` | Remove the least recently used item. Returns `this` for chaining. | +| `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | | `forEach(callback, thisArg?)` | Iterate over items in LRU order. Returns `this` for chaining. | | `get(key)` | Retrieve a value. Moves item to most recent. Returns value or `undefined`. | | `getMany(keys)` | Batch retrieve multiple items. Returns object mapping keys to values. | @@ -186,7 +187,6 @@ const cache = new LRU(100, 5000); | `toJSON()` | Serialize cache to JSON format. Returns array of items. | | `values(keys?)` | Get all values, or values for specific keys. Returns array of values. | | `valuesByTTL()` | Get values by TTL status. Returns `{valid, expired, noTTL}`. | -| `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | ## Common Patterns From feff12b4333737bb9ff06a0a0621ddce1ac5f4da Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:09:40 -0400 Subject: [PATCH 47/55] docs: Update test counts to 149 tests across 26 suites - Added test count to README.md Tests section - Added test count to AGENTS.md Testing section --- AGENTS.md | 1 + README.md | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5f1b56a..3f35aed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,7 @@ Source code is in `src/`. ## Testing - Framework: Node.js built-in test runner (`node --test`) +- Tests: 149 tests across 26 suites - Coverage: 100% lines, 99.28% branches, 100% functions - Test pattern: `tests/**/*.js` - All tests must pass with 100% line coverage before merging diff --git a/README.md b/README.md index 754fcc3..ddd2c46 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,13 @@ Build produces multiple module formats: - `dist/tiny-lru.min.js` - Minified ESM - `types/lru.d.ts` - TypeScript definitions +## Tests + +| Metric | Count | +| --------- | ----- | +| Tests | 149 | +| Suites | 26 | + ## Test Coverage | Metric | Coverage | From b027260b843c933dca67e9fccc47415f9b9b8787 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:10:55 -0400 Subject: [PATCH 48/55] docs: Remove GitHub stars badge from README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ddd2c46..cd04ae5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![npm version](https://img.shields.io/npm/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) [![npm downloads](https://img.shields.io/npm/dm/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) -[![GitHub stars](https://img.shields.io/github/stars/avoidwork/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/stargazers) [![License](https://img.shields.io/npm/l/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/blob/master/LICENSE) [![Node.js version](https://img.shields.io/node/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru) [![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions?query=workflow%3Aci) From 4d90ed7cd0740619f9fedbc381ca77922dc7cc92 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:12:14 -0400 Subject: [PATCH 49/55] docs: Fix lru-cache bundle size from 15 KB to 12 KB --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd04ae5..1ade838 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The tiny-lru library provides: - **Zero dependencies** - pure JavaScript - **100% test coverage** - fully tested and reliable - **TypeScript support** - full type definitions included -- **~2.2 KB** minified and gzipped (compared to ~15 KB for lru-cache) +- **~2.2 KB** minified and gzipped (compared to ~12 KB for lru-cache) ## Installation From 7748307a9ff668c51195f727b22675d99b0b73e8 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:15:14 -0400 Subject: [PATCH 50/55] docs: Fix API.md inaccuracies - cleanup() returns number, not (documented in Method Chaining section) - setWithEvicted() TTL reset behavior on updates with resetTtl=true - values() example comment: 'respects LRU order' -> 'order matches input array' - onEvict() parameter validation throws TypeError - forEach() thisArg type: Object -> * - entries() return type: Array<[string, *]> -> Array<(string|*)[]> --- docs/API.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index ec363d1..a8f3e1b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -174,7 +174,7 @@ console.log(removed); // 2 (number of items removed) **Returns:** `number` - Number of expired items removed -**Note:** Only removes items when TTL is enabled (`ttl > 0`). +**Note:** Only removes items when TTL is enabled (`ttl > 0`). Does not support method chaining (returns `number`). --- @@ -232,7 +232,7 @@ console.log(cache.entries(["c", "a"])); | ------ | ---------- | ---------------------------------- | | `keys` | `string[]` | Optional specific keys to retrieve | -**Returns:** `Array<[string, *]>` - Array of key-value pairs +**Returns:** `Array<(string|*)[]>` - Array of key-value pairs --- @@ -292,7 +292,7 @@ cache.forEach((value, key, cache) => { | Name | Type | Description | | --------- | ---------- | --------------------------------------- | | `callback` | `function` | Function to call for each item. Signature: `callback(value, key, cache)` | -| `thisArg` | `Object` | Value to use as `this` when executing callback | +| `thisArg` | `*` | Value to use as `this` when executing callback | **Returns:** `LRU` - this instance (for chaining) @@ -458,6 +458,8 @@ cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); **Returns:** `LRU` - this instance (for chaining) +**Throws:** `TypeError` if callback is not a function + **Note:** Only the last registered callback will be used. Triggers on: - Explicit eviction via `evict()` - Implicit eviction via `set()`/`setWithEvicted()` when cache is at max capacity @@ -529,6 +531,8 @@ console.log(cache.keys()); // ['b', 'c'] **Returns:** `{ key, value, expiry } | null` - Evicted item (if any) or `null` when no eviction occurs +**Note:** When updating an existing key with `resetTtl=true`, the TTL is reset but no eviction occurs (returns `null`). + --- ### `sizeByTTL()` @@ -609,7 +613,7 @@ console.log(cache.values()); // [1, 2, 3] console.log(cache.values(["c", "a"])); -// [3, 1] - respects LRU order +// [3, 1] - order matches input array ``` **Parameters:** @@ -655,7 +659,7 @@ When `setWithEvicted` returns an evicted item: ## Method Chaining -All mutation methods return `this` for chaining: +All mutation methods return `this` for chaining (except `cleanup()` which returns `number`): ```javascript cache.set("a", 1).set("b", 2).set("c", 3).delete("a").evict(); From 5bad7e5b1a4b7cfb7134cd93328b8c2060fef74e Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:16:28 -0400 Subject: [PATCH 51/55] docs: Clarify which build files are shipped via npm - dist/tiny-lru.min.js is available in repo but not packaged for npm - Only dist/tiny-lru.js, dist/tiny-lru.cjs, and types/lru.d.ts are shipped --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ade838..5187555 100644 --- a/README.md +++ b/README.md @@ -408,12 +408,13 @@ npm run coverage # Generate test coverage report ### Build Output -Build produces multiple module formats: +Build produces multiple module formats. When you install from npm, you'll get: - `dist/tiny-lru.js` - ES Modules - `dist/tiny-lru.cjs` - CommonJS -- `dist/tiny-lru.min.js` - Minified ESM - `types/lru.d.ts` - TypeScript definitions +The minified version (`dist/tiny-lru.min.js`) is available in the repository for local testing but is not shipped via npm. + ## Tests | Metric | Count | From f2fc84b7e34b18bcd442a1481879c361f922fdf6 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:17:16 -0400 Subject: [PATCH 52/55] docs: Document getMany() side effects - getMany() calls get() for each key - Updates LRU order (items move to MRU position) - Removes expired items (affects stats: hits, misses, deletes) - Clarify this is not a read-only operation --- docs/API.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index a8f3e1b..9ccbc60 100644 --- a/docs/API.md +++ b/docs/API.md @@ -324,7 +324,7 @@ Expired items are deleted and return `undefined`. ### `getMany(keys)` -Batch retrieves multiple items. +Batch retrieves multiple items. Calls `get()` for each key, so it updates LRU order and may remove expired items. ```javascript cache.set("a", 1).set("b", 2).set("c", 3); @@ -340,7 +340,7 @@ console.log(result); // { a: 1, c: 3 } **Returns:** `Object` - Object mapping keys to values (undefined for missing/expired keys) -**Note:** Returns `undefined` for non-existent or expired keys. +**Note:** Returns `undefined` for non-existent or expired keys. This method is NOT read-only - it updates LRU order (items move to most recently used) and may delete expired items, affecting `hits`, `misses`, and `deletes` stats. --- From 4cba0a2d40d059a39f52bad6f8ccb839baba6b81 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:27:32 -0400 Subject: [PATCH 53/55] Rename resetTtl to resetTTL for consistent TTL naming --- README.md | 6 +++--- dist/tiny-lru.cjs | 20 ++++++++++---------- dist/tiny-lru.js | 20 ++++++++++---------- dist/tiny-lru.min.js | 2 +- dist/tiny-lru.min.js.map | 2 +- docs/API.md | 8 ++++---- docs/TECHNICAL_DOCUMENTATION.md | 2 +- src/lru.js | 20 ++++++++++---------- tests/unit/lru.test.js | 30 ++++++++++++++++++------------ types/lru.d.ts | 10 +++++----- 10 files changed, 63 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 5187555..63f6854 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ const cache4 = lru(100, 60000, true); // with resetTtl enabled | ---------- | --------- | ------- | ---------------------------------------------------------------- | | `max` | `number` | `1000` | Maximum items. `0` = unlimited. Must be >= 0. | | `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. Must be >= 0. | -| `resetTtl` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | +| `resetTTL` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | **Returns:** `LRU` - New cache instance @@ -146,7 +146,7 @@ const cache = new LRU(100, 5000); | ---------- | --------- | ------- | -------------------------------------------------- | | `max` | `number` | `0` | Maximum items. `0` = unlimited. | | `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. | -| `resetTtl` | `boolean` | `false` | Reset TTL when updating via `set()` | +| `resetTTL` | `boolean` | `false` | Reset TTL when updating via `set()` | ### Properties @@ -155,7 +155,7 @@ const cache = new LRU(100, 5000); | `first` | `object` \| `null` | Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | | `last` | `object` \| `null` | Most recently used item (node with `key`, `value`, `prev`, `next`, `expiry`) | | `max` | `number` | Maximum items allowed | -| `resetTtl`| `boolean` | Whether TTL resets on `set()` updates | +| `resetTTL`| `boolean` | Whether TTL resets on `set()` updates | | `size` | `number` | Current number of items | | `ttl` | `number` | Time-to-live in milliseconds | diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index d38a8ec..2a540dd 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -25,14 +25,14 @@ class LRU { * @constructor * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited. * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). */ - constructor(max = 0, ttl = 0, resetTtl = false) { + constructor(max = 0, ttl = 0, resetTTL = false) { this.first = null; this.items = Object.create(null); this.last = null; this.max = max; - this.resetTtl = resetTtl; + this.resetTTL = resetTTL; this.size = 0; this.ttl = ttl; this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; @@ -297,7 +297,7 @@ class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); @@ -345,7 +345,7 @@ class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } @@ -661,11 +661,11 @@ class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ -function lru(max = 1000, ttl = 0, resetTtl = false) { +function lru(max = 1000, ttl = 0, resetTTL = false) { if (isNaN(max) || max < 0) { throw new TypeError("Invalid max value"); } @@ -674,11 +674,11 @@ function lru(max = 1000, ttl = 0, resetTtl = false) { throw new TypeError("Invalid ttl value"); } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTTL); } exports.LRU = LRU; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index e4a7d73..24fd987 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -23,14 +23,14 @@ class LRU { * @constructor * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited. * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). */ - constructor(max = 0, ttl = 0, resetTtl = false) { + constructor(max = 0, ttl = 0, resetTTL = false) { this.first = null; this.items = Object.create(null); this.last = null; this.max = max; - this.resetTtl = resetTtl; + this.resetTTL = resetTTL; this.size = 0; this.ttl = ttl; this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; @@ -295,7 +295,7 @@ class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); @@ -343,7 +343,7 @@ class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } @@ -659,11 +659,11 @@ class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ -function lru(max = 1000, ttl = 0, resetTtl = false) { +function lru(max = 1000, ttl = 0, resetTTL = false) { if (isNaN(max) || max < 0) { throw new TypeError("Invalid max value"); } @@ -672,9 +672,9 @@ function lru(max = 1000, ttl = 0, resetTtl = false) { throw new TypeError("Invalid ttl value"); } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTTL); }export{LRU,lru}; \ No newline at end of file diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 718874c..3dc6009 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 12.0.0 */ -class t{#t;#s;constructor(t=0,s=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=s,this.#t={hits:0,misses:0,sets:0,deletes:0,evictions:0},this.#s=null}clear(){for(let t=this.first;null!==t;){const s=t.next;t.prev=null,t.next=null,t=s}return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this.#t.hits=0,this.#t.misses=0,this.#t.sets=0,this.#t.deletes=0,this.#t.evictions=0,this}delete(t){const s=this.items[t];return void 0!==s&&(delete this.items[t],this.size--,this.#t.deletes++,this.#i(s),s.prev=null,s.next=null),this}entries(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let i=0;i0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTtl&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){if("function"!=typeof t)throw new TypeError("onEvict callback must be a function");return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict()),e=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=e:this.last.next=e,this.last=e),this.#t.sets++,i}set(t,s){let i=this.items[t];return void 0!==i?(i.value=s,this.resetTTL&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i)):(this.max>0&&this.size===this.max&&this.evict(),i=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:s},1==++this.size?this.first=i:this.last.next=i,this.last=i),this.#t.sets++,this}values(t){if(void 0===t){const t=Array.from({length:this.size});let s=0;for(let i=this.first;null!==i;i=i.next)t[s++]=i.value;return t}const s=Array.from({length:t.length});for(let i=0;i0&&this.#l(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)t.push({key:s.key,value:s.value,expiry:s.expiry});return t}stats(){return{...this.#t}}onEvict(t){if("function"!=typeof t)throw new TypeError("onEvict callback must be a function");return this.#s=t,this}sizeByTTL(){if(0===this.ttl)return{valid:this.size,expired:0,noTTL:this.size};const t=Date.now();let s=0,i=0,e=0;for(let l=this.first;null!==l;l=l.next)0===l.expiry?(e++,s++):l.expiry>t?s++:i++;return{valid:s,expired:i,noTTL:e}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],i=[],e=[];for(let l=this.first;null!==l;l=l.next)0===l.expiry?(s.push(l.key),e.push(l.key)):l.expiry>t?s.push(l.key):i.push(l.key);return{valid:s,expired:i,noTTL:e}}valuesByTTL(){const t=this.keysByTTL();return{valid:this.values(t.valid),expired:this.values(t.expired),noTTL:this.values(t.noTTL)}}#l(){if(0===this.size)return this.first=null,void(this.last=null);const t=this.keys();this.first=null,this.last=null;for(let s=0;s>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists and is not expired, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && !this.#isExpired(item);\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"onEvict callback must be a function\");\n\t\t}\n\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","TypeError","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tiny-lru.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n */\nexport class LRU {\n\t#stats;\n\t#onEvict;\n\n\t/**\n\t * Creates a new LRU cache instance.\n\t * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n\t *\n\t * @constructor\n\t * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n\t * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n\t * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set().\n\t */\n\tconstructor(max = 0, ttl = 0, resetTTL = false) {\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.max = max;\n\t\tthis.resetTTL = resetTTL;\n\t\tthis.size = 0;\n\t\tthis.ttl = ttl;\n\t\tthis.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };\n\t\tthis.#onEvict = null;\n\t}\n\n\t/**\n\t * Removes all items from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tclear() {\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tx.prev = null;\n\t\t\tx.next = null;\n\t\t\tx = next;\n\t\t}\n\n\t\tthis.first = null;\n\t\tthis.items = Object.create(null);\n\t\tthis.last = null;\n\t\tthis.size = 0;\n\t\tthis.#stats.hits = 0;\n\t\tthis.#stats.misses = 0;\n\t\tthis.#stats.sets = 0;\n\t\tthis.#stats.deletes = 0;\n\t\tthis.#stats.evictions = 0;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Removes an item from the cache by key.\n\t *\n\t * @param {string} key - The key of the item to delete.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tdelete(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tdelete this.items[key];\n\t\t\tthis.size--;\n\t\t\tthis.#stats.deletes++;\n\n\t\t\tthis.#unlink(item);\n\n\t\t\titem.prev = null;\n\t\t\titem.next = null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of [key, value] pairs for the specified keys.\n\t * When no keys provided, returns all entries in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n\t * @returns {Array>} Array of [key, value] pairs.\n\t */\n\tentries(keys) {\n\t\tif (keys === undefined) {\n\t\t\tkeys = this.keys();\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tconst item = this.items[key];\n\t\t\tresult[i] = [key, item !== undefined ? item.value : undefined];\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes the least recently used item from the cache.\n\t *\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tevict() {\n\t\tif (this.size === 0) {\n\t\t\treturn this;\n\t\t}\n\n\t\tconst item = this.first;\n\n\t\tdelete this.items[item.key];\n\t\tthis.#stats.evictions++;\n\n\t\tif (--this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t} else {\n\t\t\tthis.#unlink(item);\n\t\t}\n\n\t\titem.prev = null;\n\t\titem.next = null;\n\t\tif (this.#onEvict !== null) {\n\t\t\tthis.#onEvict({\n\t\t\t\tkey: item.key,\n\t\t\t\tvalue: item.value,\n\t\t\t\texpiry: item.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the expiration timestamp for a given key.\n\t *\n\t * @param {string} key - The key to check expiration for.\n\t * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n\t */\n\texpiresAt(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.expiry : undefined;\n\t}\n\n\t/**\n\t * Checks if an item has expired.\n\t *\n\t * @param {Object} item - The cache item to check.\n\t * @returns {boolean} True if the item has expired, false otherwise.\n\t * @private\n\t */\n\t#isExpired(item) {\n\t\tif (this.ttl === 0 || item.expiry === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item.expiry <= Date.now();\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key without updating LRU order.\n\t * Note: Does not perform TTL checks or remove expired items.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found.\n\t */\n\tpeek(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined ? item.value : undefined;\n\t}\n\n\t/**\n\t * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to retrieve.\n\t * @returns {*} The value associated with the key, or undefined if not found or expired.\n\t */\n\tget(key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\tif (!this.#isExpired(item)) {\n\t\t\t\tthis.moveToEnd(item);\n\t\t\t\tthis.#stats.hits++;\n\t\t\t\treturn item.value;\n\t\t\t}\n\n\t\t\tthis.delete(key);\n\t\t\tthis.#stats.misses++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis.#stats.misses++;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists and is not expired, false otherwise.\n\t */\n\thas(key) {\n\t\tconst item = this.items[key];\n\t\treturn item !== undefined && !this.#isExpired(item);\n\t}\n\n\t/**\n\t * Unlinks an item from the doubly-linked list.\n\t * Updates first/last pointers if needed.\n\t * Does NOT clear the item's prev/next pointers or delete from items map.\n\t *\n\t * @private\n\t */\n\t#unlink(item) {\n\t\tif (item.prev !== null) {\n\t\t\titem.prev.next = item.next;\n\t\t}\n\n\t\tif (item.next !== null) {\n\t\t\titem.next.prev = item.prev;\n\t\t}\n\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\tif (this.last === item) {\n\t\t\tthis.last = item.prev;\n\t\t}\n\t}\n\n\t/**\n\t * Efficiently moves an item to the end of the LRU list (most recently used position).\n\t * This is an internal optimization method that avoids the overhead of the full set() operation\n\t * when only LRU position needs to be updated.\n\t *\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t */\n\tmoveToEnd(item) {\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unlink(item);\n\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\t\tthis.last.next = item;\n\t\tthis.last = item;\n\t}\n\n\t/**\n\t * Returns an array of all keys in the cache, ordered from least to most recently used.\n\t *\n\t * @returns {string[]} Array of keys in LRU order.\n\t */\n\tkeys() {\n\t\tconst result = Array.from({ length: this.size });\n\t\tlet x = this.first;\n\t\tlet i = 0;\n\n\t\twhile (x !== null) {\n\t\t\tresult[i++] = x.key;\n\t\t\tx = x.next;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Sets a value in the cache and returns any evicted item.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry}, or null.\n\t */\n\tsetWithEvicted(key, value) {\n\t\tlet evicted = null;\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\t\t\tif (this.resetTTL) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: this.first.key,\n\t\t\t\t\tvalue: this.first.value,\n\t\t\t\t\texpiry: this.first.expiry,\n\t\t\t\t};\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tset(key, value) {\n\t\tlet item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\titem.value = value;\n\n\t\t\tif (this.resetTTL) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict();\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue,\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\tthis.#stats.sets++;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * When no keys provided, returns all values in LRU order.\n\t * When keys provided, order matches the input array.\n\t *\n\t * @param {string[]} [keys] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys.\n\t */\n\tvalues(keys) {\n\t\tif (keys === undefined) {\n\t\t\tconst result = Array.from({ length: this.size });\n\t\t\tlet i = 0;\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tresult[i++] = x.value;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tconst result = Array.from({ length: keys.length });\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tresult[i] = item !== undefined ? item.value : undefined;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Iterate over cache items in LRU order (least to most recent).\n\t * Note: This method directly accesses items from the linked list without calling\n\t * get() or peek(), so it does not update LRU order or check TTL expiration during iteration.\n\t *\n\t * @param {function(*, any, LRU): void} callback - Function to call for each item. Signature: callback(value, key, cache)\n\t * @param {Object} [thisArg] - Value to use as `this` when executing callback.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tforEach(callback, thisArg) {\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Batch retrieve multiple items.\n\t *\n\t * @param {string[]} keys - Array of keys to retrieve.\n\t * @returns {Object} Object mapping keys to values (undefined for missing/expired keys).\n\t */\n\tgetMany(keys) {\n\t\tconst result = Object.create(null);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[key] = this.get(key);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ALL keys exist.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if all keys exist and are not expired.\n\t */\n\thasAll(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (!this.has(keys[i])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Batch existence check - returns true if ANY key exists.\n\t *\n\t * @param {string[]} keys - Array of keys to check.\n\t * @returns {boolean} True if any key exists and is not expired.\n\t */\n\thasAny(keys) {\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (this.has(keys[i])) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Remove expired items without affecting LRU order.\n\t * Unlike get(), this does not move items to the end.\n\t *\n\t * @returns {number} Number of expired items removed.\n\t */\n\tcleanup() {\n\t\tif (this.ttl === 0 || this.size === 0) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet removed = 0;\n\n\t\tfor (let x = this.first; x !== null; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (this.#isExpired(x)) {\n\t\t\t\tconst key = x.key;\n\t\t\t\tif (this.items[key] !== undefined) {\n\t\t\t\t\tdelete this.items[key];\n\t\t\t\t\tthis.size--;\n\t\t\t\t\tremoved++;\n\t\t\t\t\tthis.#unlink(x);\n\t\t\t\t\tx.prev = null;\n\t\t\t\t\tx.next = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tx = next;\n\t\t}\n\n\t\tif (removed > 0) {\n\t\t\tthis.#rebuildList();\n\t\t}\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Serialize cache to JSON-compatible format.\n\t *\n\t * @returns {Array<{key: any, value: *, expiry: number}>} Array of cache items.\n\t */\n\ttoJSON() {\n\t\tconst result = [];\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tresult.push({\n\t\t\t\tkey: x.key,\n\t\t\t\tvalue: x.value,\n\t\t\t\texpiry: x.expiry,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get cache statistics.\n\t *\n\t * @returns {Object} Statistics object with hits, misses, sets, deletes, evictions counts.\n\t */\n\tstats() {\n\t\treturn { ...this.#stats };\n\t}\n\n\t/**\n\t * Register callback for evicted items.\n\t *\n\t * @param {function(Object): void} callback - Function called when item is evicted. Receives {key, value, expiry}.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t */\n\tonEvict(callback) {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"onEvict callback must be a function\");\n\t\t}\n\n\t\tthis.#onEvict = callback;\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get counts of items by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL counts.\n\t */\n\tsizeByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.size, expired: 0, noTTL: this.size };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tlet valid = 0;\n\t\tlet expired = 0;\n\t\tlet noTTL = 0;\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tnoTTL++;\n\t\t\t\tvalid++;\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid++;\n\t\t\t} else {\n\t\t\t\texpired++;\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get keys filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of keys.\n\t */\n\tkeysByTTL() {\n\t\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.keys(), expired: [], noTTL: this.keys() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\t\tconst noTTL = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry === 0) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t\tnoTTL.push(x.key);\n\t\t\t} else if (x.expiry > now) {\n\t\t\t\tvalid.push(x.key);\n\t\t\t} else {\n\t\t\t\texpired.push(x.key);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL };\n\t}\n\n\t/**\n\t * Get values filtered by TTL status.\n\t *\n\t * @returns {Object} Object with valid, expired, and noTTL arrays of values.\n\t */\n\tvaluesByTTL() {\n\t\tconst keysByTTL = this.keysByTTL();\n\n\t\treturn {\n\t\t\tvalid: this.values(keysByTTL.valid),\n\t\t\texpired: this.values(keysByTTL.expired),\n\t\t\tnoTTL: this.values(keysByTTL.noTTL),\n\t\t};\n\t}\n\n\t/**\n\t * Rebuild the doubly-linked list after cleanup by deleting expired items.\n\t * This removes nodes that were deleted during cleanup.\n\t *\n\t * @private\n\t */\n\t#rebuildList() {\n\t\tif (this.size === 0) {\n\t\t\tthis.first = null;\n\t\t\tthis.last = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst keys = this.keys();\n\t\tthis.first = null;\n\t\tthis.last = null;\n\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst item = this.items[keys[i]];\n\t\t\tif (item !== null && item !== undefined) {\n\t\t\t\tif (this.first === null) {\n\t\t\t\t\tthis.first = item;\n\t\t\t\t\titem.prev = null;\n\t\t\t\t} else {\n\t\t\t\t\titem.prev = this.last;\n\t\t\t\t\tthis.last.next = item;\n\t\t\t\t}\n\t\t\t\titem.next = null;\n\t\t\t\tthis.last = item;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n */\nexport function lru(max = 1000, ttl = 0, resetTTL = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTTL !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTTL value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTTL);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTTL","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","unlink","entries","keys","result","Array","from","length","i","value","evict","expiry","expiresAt","isExpired","Date","now","peek","get","delete","moveToEnd","has","setWithEvicted","evicted","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","push","TypeError","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru","isNaN"],"mappings":"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"} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 9ccbc60..13b4223 100644 --- a/docs/API.md +++ b/docs/API.md @@ -10,7 +10,7 @@ Complete API documentation for tiny-lru. - [first](#first) - [last](#last) - [max](#max) - - [resetTtl](#resetttl) + - [resetTTL](#resetttl) - [size](#size) - [ttl](#ttl) - [Methods](#methods) @@ -61,7 +61,7 @@ const cache = lru(100, 5000, true); | ---------- | --------- | ------- | ---------------------------------------------------------------- | | `max` | `number` | `1000` | Maximum items. `0` = unlimited. Must be >= 0. | | `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. Must be >= 0. | -| `resetTtl` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | +| `resetTTL` | `boolean` | `false` | Reset TTL when updating existing items via `set()` | **Returns:** `LRU` - New cache instance @@ -93,7 +93,7 @@ const cache = new LRU(100, 5000, true); | ---------- | --------- | ------- | -------------------------------------------------- | | `max` | `number` | `0` | Maximum items. `0` = unlimited. | | `ttl` | `number` | `0` | Time-to-live in milliseconds. `0` = no expiration. | -| `resetTtl` | `boolean` | `false` | Reset TTL when updating via `set()` | +| `resetTTL` | `boolean` | `false` | Reset TTL when updating via `set()` | --- @@ -129,7 +129,7 @@ const cache = lru(100); console.log(cache.max); // 100 ``` -### `resetTtl` +### `resetTTL` `boolean` - Whether TTL resets on `set()` updates. diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md index 3be4445..c2d1057 100644 --- a/docs/TECHNICAL_DOCUMENTATION.md +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -56,7 +56,7 @@ graph TD - `max`: Maximum cache size (0 = unlimited) - `size`: Current number of items - `ttl`: Time-to-live in milliseconds (0 = no expiration) - - `resetTtl`: Whether to reset TTL on `set()` and `setWithEvicted()` operations (not on `get()`) + - `resetTTL`: Whether to reset TTL on `set()` and `setWithEvicted()` operations (not on `get()`) ## Data Flow diff --git a/src/lru.js b/src/lru.js index 2338294..00cb72f 100644 --- a/src/lru.js +++ b/src/lru.js @@ -16,14 +16,14 @@ export class LRU { * @constructor * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited. * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). */ - constructor(max = 0, ttl = 0, resetTtl = false) { + constructor(max = 0, ttl = 0, resetTTL = false) { this.first = null; this.items = Object.create(null); this.last = null; this.max = max; - this.resetTtl = resetTtl; + this.resetTTL = resetTTL; this.size = 0; this.ttl = ttl; this.#stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 }; @@ -288,7 +288,7 @@ export class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); @@ -336,7 +336,7 @@ export class LRU { if (item !== undefined) { item.value = value; - if (this.resetTtl) { + if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } @@ -652,11 +652,11 @@ export class LRU { * @function lru * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size. * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration. - * @param {boolean} [resetTtl=false] - Whether to reset TTL when updating existing items via set(). + * @param {boolean} [resetTTL=false] - Whether to reset TTL when updating existing items via set(). * @returns {LRU} A new LRU cache instance. * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ -export function lru(max = 1000, ttl = 0, resetTtl = false) { +export function lru(max = 1000, ttl = 0, resetTTL = false) { if (isNaN(max) || max < 0) { throw new TypeError("Invalid max value"); } @@ -665,9 +665,9 @@ export function lru(max = 1000, ttl = 0, resetTtl = false) { throw new TypeError("Invalid ttl value"); } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTTL); } diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index ee1682a..6f3d7d4 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -8,7 +8,7 @@ describe("LRU Cache", function () { const cache = new LRU(); assert.equal(cache.max, 0); assert.equal(cache.ttl, 0); - assert.equal(cache.resetTtl, false); + assert.equal(cache.resetTTL, false); assert.equal(cache.size, 0); assert.equal(cache.first, null); assert.equal(cache.last, null); @@ -20,7 +20,7 @@ describe("LRU Cache", function () { const cache = new LRU(10, 5000, true); assert.equal(cache.max, 10); assert.equal(cache.ttl, 5000); - assert.equal(cache.resetTtl, true); + assert.equal(cache.resetTTL, true); assert.equal(cache.size, 0); }); }); @@ -30,14 +30,14 @@ describe("LRU Cache", function () { const cache = lru(); assert.equal(cache.max, 1000); assert.equal(cache.ttl, 0); - assert.equal(cache.resetTtl, false); + assert.equal(cache.resetTTL, false); }); it("should create an LRU instance with custom parameters", function () { const cache = lru(50, 1000, true); assert.equal(cache.max, 50); assert.equal(cache.ttl, 1000); - assert.equal(cache.resetTtl, true); + assert.equal(cache.resetTTL, true); }); it("should throw TypeError for invalid max value", function () { @@ -52,9 +52,9 @@ describe("LRU Cache", function () { assert.throws(() => lru(10, NaN), TypeError, "Invalid ttl value"); }); - it("should throw TypeError for invalid resetTtl value", function () { - assert.throws(() => lru(10, 0, "invalid"), TypeError, "Invalid resetTtl value"); - assert.throws(() => lru(10, 0, 1), TypeError, "Invalid resetTtl value"); + it("should throw TypeError for invalid resetTTL value", function () { + assert.throws(() => lru(10, 0, "invalid"), TypeError, "Invalid resetTTL value"); + assert.throws(() => lru(10, 0, 1), TypeError, "Invalid resetTTL value"); }); }); @@ -326,7 +326,7 @@ describe("LRU Cache", function () { assert.equal(neverExpireCache.expiresAt("key1"), 0); }); - it("should reset TTL when updating with resetTtl=true", async function () { + it("should reset TTL when updating with resetTTL=true", async function () { const resetCache = new LRU(5, 1000, true); resetCache.set("key1", "value1"); @@ -339,7 +339,7 @@ describe("LRU Cache", function () { assert.ok(secondExpiry > firstExpiry, "TTL should be reset"); }); - it("should not reset TTL when resetTtl=false", async function () { + it("should not reset TTL when resetTTL=false", async function () { const noResetCache = new LRU(5, 100, false); noResetCache.set("key1", "value1"); @@ -350,7 +350,7 @@ describe("LRU Cache", function () { assert.equal(noResetCache.get("key1"), undefined); }); - it("should not reset TTL on get() even with resetTtl=true", async function () { + it("should not reset TTL on get() even with resetTTL=true", async function () { const resetCache = new LRU(5, 100, true); resetCache.set("key1", "value1"); @@ -647,7 +647,7 @@ describe("LRU Cache", function () { assert.ok(expiry <= before + 250); }); - it("should set expiry to 0 when resetTtl=true and ttl=0 on update", function () { + it("should set expiry to 0 when resetTTL=true and ttl=0 on update", function () { const cache = new LRU(2, 0, true); cache.set("x", 1); assert.equal(cache.expiresAt("x"), 0); @@ -655,7 +655,7 @@ describe("LRU Cache", function () { assert.equal(cache.expiresAt("x"), 0); }); - it("should set expiry to 0 when resetTtl=true and ttl=0 on setWithEvicted", function () { + it("should set expiry to 0 when resetTTL=true and ttl=0 on setWithEvicted", function () { const cache = new LRU(2, 0, true); cache.set("x", 1); assert.equal(cache.expiresAt("x"), 0); @@ -1249,11 +1249,17 @@ describe("LRU Cache", function () { const ttlCache = new LRU(5, 50); ttlCache.set("a", 1).set("b", 2).set("c", 3); + let evictedItem = null; + ttlCache.onEvict((item) => { + evictedItem = item; + }); + await new Promise((resolve) => setTimeout(resolve, 100)); const removed = ttlCache.cleanup(); assert.equal(removed, 3); assert.equal(ttlCache.size, 0); + assert.equal(evictedItem, null, "onEvict callback should not be called during cleanup()"); }); it("should only have last registered callback", function () { diff --git a/types/lru.d.ts b/types/lru.d.ts index f46e69f..8fb38aa 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -2,11 +2,11 @@ * Factory function to create a new LRU cache instance with parameter validation. * @param max Maximum number of items to store (default: 1000, 0 = unlimited) * @param ttl Time to live in milliseconds (default: 0, 0 = no expiration) - * @param resetTtl Whether to reset TTL when updating existing items via set() (default: false) + * @param resetTTL Whether to reset TTL when updating existing items via set() (default: false) * @returns A new LRU cache instance * @throws TypeError when parameters are invalid (negative numbers or wrong types) */ -export function lru(max?: number, ttl?: number, resetTtl?: boolean): LRU; +export function lru(max?: number, ttl?: number, resetTTL?: boolean): LRU; /** * Internal structure representing a cache item in the doubly-linked list. @@ -46,9 +46,9 @@ export class LRU { * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation. * @param max Maximum number of items to store (default: 0, 0 = unlimited) * @param ttl Time to live in milliseconds (default: 0, 0 = no expiration) - * @param resetTtl Whether to reset TTL when updating existing items via set() (default: false) + * @param resetTTL Whether to reset TTL when updating existing items via set() (default: false) */ - constructor(max?: number, ttl?: number, resetTtl?: boolean); + constructor(max?: number, ttl?: number, resetTTL?: boolean); /** Pointer to the least recently used item (first to be evicted) */ first: LRUItem | null; @@ -59,7 +59,7 @@ export class LRU { /** Maximum number of items to store (0 = unlimited) */ max: number; /** Whether to reset TTL on set() operations */ - resetTtl: boolean; + resetTTL: boolean; /** Current number of items in the cache */ size: number; /** Time-to-live in milliseconds (0 = no expiration) */ From dd492ba938db13271ef891794380ac9c33a96e31 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:29:14 -0400 Subject: [PATCH 54/55] docs: Fix entries() return type to specify 2-element tuple --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 13b4223..b8951ce 100644 --- a/docs/API.md +++ b/docs/API.md @@ -232,7 +232,7 @@ console.log(cache.entries(["c", "a"])); | ------ | ---------- | ---------------------------------- | | `keys` | `string[]` | Optional specific keys to retrieve | -**Returns:** `Array<(string|*)[]>` - Array of key-value pairs +**Returns:** `[string, *][]` - Array of 2-element tuples [key, value] --- From d175d2e671b56c6979d3a0175cb8e78d35850ba4 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sun, 29 Mar 2026 16:31:38 -0400 Subject: [PATCH 55/55] docs: fix valuesByTTL() noTTL description Correctly state that noTTL contains items where expiry === 0, regardless of cache ttl setting --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index b8951ce..e5b4c48 100644 --- a/docs/API.md +++ b/docs/API.md @@ -639,7 +639,7 @@ console.log(cache.valuesByTTL()); **Returns:** `Object` - Object with three properties: - `valid` - Array of valid (non-expired) values - `expired` - Array of expired values -- `noTTL` - Array of values without TTL (when `ttl=0`) +- `noTTL` - Array of values without TTL (`expiry === 0`, regardless of cache `ttl` setting) ---