diff --git a/coverage.txt b/coverage.txt index 355f23e..e37deb0 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.28 | 100.00 | +ℹ lru.js | 100.00 | 99.39 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.28 | 100.00 | +ℹ all files | 100.00 | 99.39 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index b61ab79..bbca10d 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -20,14 +20,26 @@ class LRU { /** * 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(). + * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ constructor(max = 0, ttl = 0, resetTTL = false) { + if (!Number.isInteger(max) || max < 0) { + throw new TypeError("Invalid max value"); + } + + if (!Number.isInteger(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } + + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); + } + this.first = null; this.items = Object.create(null); this.last = null; @@ -75,14 +87,8 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - delete this.items[key]; - this.size--; + this.#removeItem(item); this.#stats.deletes++; - - this.#unlink(item); - - item.prev = null; - item.next = null; } return this; @@ -98,14 +104,25 @@ class LRU { */ entries(keys) { if (keys === undefined) { - keys = this.keys(); + const result = []; + for (let x = this.first; x !== null; x = x.next) { + if (!this.#isExpired(x)) { + result.push([x.key, x.value]); + } + } + + return result; + } + + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); } 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]; + result[i] = [key, item !== undefined && !this.#isExpired(item) ? item.value : undefined]; } return result; @@ -121,20 +138,8 @@ class LRU { 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); - } + const item = this.#evictItem(); - item.prev = null; - item.next = null; if (this.#onEvict !== null) { this.#onEvict({ key: item.key, @@ -165,7 +170,7 @@ class LRU { * @private */ #isExpired(item) { - if (this.ttl === 0 || item.expiry === 0) { + if (this.ttl === 0) { return false; } @@ -200,7 +205,7 @@ class LRU { return item.value; } - this.delete(key); + this.#removeItem(item); this.#stats.misses++; return undefined; } @@ -211,13 +216,20 @@ class LRU { /** * Checks if a key exists in the cache. + * Expired items are removed before returning false. * * @param {string} key - The key to check for. * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && !this.#isExpired(item); + + if (item !== undefined && this.#isExpired(item)) { + this.#removeItem(item); + return false; + } + + return item !== undefined; } /** @@ -245,6 +257,47 @@ class LRU { } } + /** + * Removes an item from the cache without incrementing the deletes stat. + * Used internally by get()/has() when removing expired items. + * + * @param {Object} item - The cache item to remove. + * @private + */ + #removeItem(item) { + delete this.items[item.key]; + this.size--; + this.#unlink(item); + item.prev = null; + item.next = null; + } + + /** + * Evicts the least recently used item from the cache without firing onEvict. + * Used internally by setWithEvicted() to avoid double-notification. + * + * @returns {Object} The evicted item. + * @private + */ + #evictItem() { + 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.prev = null; + item.next = null; + + return item; + } + /** * 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 @@ -286,6 +339,7 @@ class LRU { /** * Sets a value in the cache and returns any evicted item. + * Eviction is silent — onEvict is not fired for the returned item. * * @param {string} key - The key to set. * @param {*} value - The value to store. @@ -295,20 +349,24 @@ class LRU { let evicted = null; let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { + const evictedItem = this.#evictItem(); evicted = { - key: this.first.key, - value: this.first.value, - expiry: this.first.expiry, + key: evictedItem.key, + value: evictedItem.value, + expiry: evictedItem.expiry, }; - this.evict(); } item = this.items[key] = { @@ -342,7 +400,7 @@ class LRU { set(key, value) { let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { @@ -351,6 +409,10 @@ class LRU { this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { this.evict(); } @@ -387,18 +449,24 @@ class LRU { */ values(keys) { if (keys === undefined) { - const result = Array.from({ length: this.size }); - let i = 0; + const result = []; for (let x = this.first; x !== null; x = x.next) { - result[i++] = x.value; + if (!this.#isExpired(x)) { + result.push(x.value); + } } + return result; } + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + 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; + result[i] = item !== undefined && !this.#isExpired(item) ? item.value : undefined; } return result; @@ -407,15 +475,19 @@ class LRU { /** * 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. + * get() or peek(), so it does not update LRU order. Expired items are skipped. * * @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); + for (let x = this.first; x !== null; ) { + const next = x.next; + if (!this.#isExpired(x)) { + callback.call(thisArg, x.value, x.key, this); + } + x = next; } return this; @@ -428,6 +500,10 @@ class LRU { * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). */ getMany(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + const result = Object.create(null); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -444,6 +520,10 @@ class LRU { * @returns {boolean} True if all keys exist and are not expired. */ hasAll(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (!this.has(keys[i])) { return false; @@ -460,6 +540,10 @@ class LRU { * @returns {boolean} True if any key exists and is not expired. */ hasAny(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (this.has(keys[i])) { return true; @@ -513,11 +597,13 @@ class LRU { toJSON() { const result = []; for (let x = this.first; x !== null; x = x.next) { - result.push({ - key: x.key, - value: x.value, - expiry: x.expiry, - }); + if (!this.#isExpired(x)) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } } return result; @@ -561,20 +647,16 @@ class LRU { 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) { + if (x.expiry > now) { valid++; } else { expired++; } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: 0 }; } /** @@ -590,20 +672,16 @@ class LRU { 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) { + if (x.expiry > now) { valid.push(x.key); } else { expired.push(x.key); } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: [] }; } /** @@ -612,13 +690,23 @@ class LRU { * @returns {Object} Object with valid, expired, and noTTL arrays of values. */ valuesByTTL() { - const keysByTTL = this.keysByTTL(); + if (this.ttl === 0) { + return { valid: this.values(), expired: [], noTTL: this.values() }; + } - return { - valid: this.values(keysByTTL.valid), - expired: this.values(keysByTTL.expired), - noTTL: this.values(keysByTTL.noTTL), - }; + const now = Date.now(); + const valid = []; + const expired = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry > now) { + valid.push(x.value); + } else { + expired.push(x.value); + } + } + + return { valid, expired, noTTL: [] }; } /** @@ -666,18 +754,6 @@ class LRU { * @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); } diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index f8b50ed..58b667f 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -18,14 +18,26 @@ class LRU { /** * 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(). + * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ constructor(max = 0, ttl = 0, resetTTL = false) { + if (!Number.isInteger(max) || max < 0) { + throw new TypeError("Invalid max value"); + } + + if (!Number.isInteger(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } + + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); + } + this.first = null; this.items = Object.create(null); this.last = null; @@ -73,14 +85,8 @@ class LRU { const item = this.items[key]; if (item !== undefined) { - delete this.items[key]; - this.size--; + this.#removeItem(item); this.#stats.deletes++; - - this.#unlink(item); - - item.prev = null; - item.next = null; } return this; @@ -96,14 +102,25 @@ class LRU { */ entries(keys) { if (keys === undefined) { - keys = this.keys(); + const result = []; + for (let x = this.first; x !== null; x = x.next) { + if (!this.#isExpired(x)) { + result.push([x.key, x.value]); + } + } + + return result; + } + + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); } 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]; + result[i] = [key, item !== undefined && !this.#isExpired(item) ? item.value : undefined]; } return result; @@ -119,20 +136,8 @@ class LRU { 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); - } + const item = this.#evictItem(); - item.prev = null; - item.next = null; if (this.#onEvict !== null) { this.#onEvict({ key: item.key, @@ -163,7 +168,7 @@ class LRU { * @private */ #isExpired(item) { - if (this.ttl === 0 || item.expiry === 0) { + if (this.ttl === 0) { return false; } @@ -198,7 +203,7 @@ class LRU { return item.value; } - this.delete(key); + this.#removeItem(item); this.#stats.misses++; return undefined; } @@ -209,13 +214,20 @@ class LRU { /** * Checks if a key exists in the cache. + * Expired items are removed before returning false. * * @param {string} key - The key to check for. * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && !this.#isExpired(item); + + if (item !== undefined && this.#isExpired(item)) { + this.#removeItem(item); + return false; + } + + return item !== undefined; } /** @@ -243,6 +255,47 @@ class LRU { } } + /** + * Removes an item from the cache without incrementing the deletes stat. + * Used internally by get()/has() when removing expired items. + * + * @param {Object} item - The cache item to remove. + * @private + */ + #removeItem(item) { + delete this.items[item.key]; + this.size--; + this.#unlink(item); + item.prev = null; + item.next = null; + } + + /** + * Evicts the least recently used item from the cache without firing onEvict. + * Used internally by setWithEvicted() to avoid double-notification. + * + * @returns {Object} The evicted item. + * @private + */ + #evictItem() { + 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.prev = null; + item.next = null; + + return item; + } + /** * 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 @@ -284,6 +337,7 @@ class LRU { /** * Sets a value in the cache and returns any evicted item. + * Eviction is silent — onEvict is not fired for the returned item. * * @param {string} key - The key to set. * @param {*} value - The value to store. @@ -293,20 +347,24 @@ class LRU { let evicted = null; let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { + const evictedItem = this.#evictItem(); evicted = { - key: this.first.key, - value: this.first.value, - expiry: this.first.expiry, + key: evictedItem.key, + value: evictedItem.value, + expiry: evictedItem.expiry, }; - this.evict(); } item = this.items[key] = { @@ -340,7 +398,7 @@ class LRU { set(key, value) { let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { @@ -349,6 +407,10 @@ class LRU { this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { this.evict(); } @@ -385,18 +447,24 @@ class LRU { */ values(keys) { if (keys === undefined) { - const result = Array.from({ length: this.size }); - let i = 0; + const result = []; for (let x = this.first; x !== null; x = x.next) { - result[i++] = x.value; + if (!this.#isExpired(x)) { + result.push(x.value); + } } + return result; } + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + 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; + result[i] = item !== undefined && !this.#isExpired(item) ? item.value : undefined; } return result; @@ -405,15 +473,19 @@ class LRU { /** * 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. + * get() or peek(), so it does not update LRU order. Expired items are skipped. * * @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); + for (let x = this.first; x !== null; ) { + const next = x.next; + if (!this.#isExpired(x)) { + callback.call(thisArg, x.value, x.key, this); + } + x = next; } return this; @@ -426,6 +498,10 @@ class LRU { * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). */ getMany(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + const result = Object.create(null); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -442,6 +518,10 @@ class LRU { * @returns {boolean} True if all keys exist and are not expired. */ hasAll(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (!this.has(keys[i])) { return false; @@ -458,6 +538,10 @@ class LRU { * @returns {boolean} True if any key exists and is not expired. */ hasAny(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (this.has(keys[i])) { return true; @@ -511,11 +595,13 @@ class LRU { toJSON() { const result = []; for (let x = this.first; x !== null; x = x.next) { - result.push({ - key: x.key, - value: x.value, - expiry: x.expiry, - }); + if (!this.#isExpired(x)) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } } return result; @@ -559,20 +645,16 @@ class LRU { 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) { + if (x.expiry > now) { valid++; } else { expired++; } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: 0 }; } /** @@ -588,20 +670,16 @@ class LRU { 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) { + if (x.expiry > now) { valid.push(x.key); } else { expired.push(x.key); } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: [] }; } /** @@ -610,13 +688,23 @@ class LRU { * @returns {Object} Object with valid, expired, and noTTL arrays of values. */ valuesByTTL() { - const keysByTTL = this.keysByTTL(); + if (this.ttl === 0) { + return { valid: this.values(), expired: [], noTTL: this.values() }; + } - return { - valid: this.values(keysByTTL.valid), - expired: this.values(keysByTTL.expired), - noTTL: this.values(keysByTTL.noTTL), - }; + const now = Date.now(); + const valid = []; + const expired = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry > now) { + valid.push(x.value); + } else { + expired.push(x.value); + } + } + + return { valid, expired, noTTL: [] }; } /** @@ -664,17 +752,5 @@ class LRU { * @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); }export{LRU,lru}; \ No newline at end of file diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 37eb50a..28d0d11 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 13.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&&this.size===this.max){const t=this.#r();e={key:t.key,value:t.value,expiry:t.expiry}}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}else i.value=s,this.resetTTL&&(i.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(i);return this.#t.sets++,e}set(t,s){let e=this.items[t];return void 0===e||this.#i(e)?(void 0!==e&&this.#e(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):(e.value=s,this.resetTTL&&(e.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(e)),this.#t.sets++,this}values(t){if(void 0===t){const t=[];for(let s=this.first;null!==s;s=s.next)this.#i(s)||t.push(s.value);return t}if(!Array.isArray(t))throw new TypeError("keys must be an array");const s=Array.from({length:t.length});for(let e=0;e0&&this.#n(),t}toJSON(){const t=[];for(let s=this.first;null!==s;s=s.next)this.#i(s)||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,e=0;for(let i=this.first;null!==i;i=i.next)i.expiry>t?s++:e++;return{valid:s,expired:e,noTTL:0}}keysByTTL(){if(0===this.ttl)return{valid:this.keys(),expired:[],noTTL:this.keys()};const t=Date.now(),s=[],e=[];for(let i=this.first;null!==i;i=i.next)i.expiry>t?s.push(i.key):e.push(i.key);return{valid:s,expired:e,noTTL:[]}}valuesByTTL(){if(0===this.ttl)return{valid:this.values(),expired:[],noTTL:this.values()};const t=Date.now(),s=[],e=[];for(let i=this.first;null!==i;i=i.next)i.expiry>t?s.push(i.value):e.push(i.value);return{valid:s,expired:e,noTTL:[]}}#n(){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 *\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 * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n\t */\n\tconstructor(max = 0, ttl = 0, resetTTL = false) {\n\t\tif (!Number.isInteger(max) || max < 0) {\n\t\t\tthrow new TypeError(\"Invalid max value\");\n\t\t}\n\n\t\tif (!Number.isInteger(ttl) || ttl < 0) {\n\t\t\tthrow new TypeError(\"Invalid ttl value\");\n\t\t}\n\n\t\tif (typeof resetTTL !== \"boolean\") {\n\t\t\tthrow new TypeError(\"Invalid resetTTL value\");\n\t\t}\n\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\tthis.#removeItem(item);\n\t\t\tthis.#stats.deletes++;\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\tconst result = [];\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tif (!this.#isExpired(x)) {\n\t\t\t\t\tresult.push([x.key, x.value]);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t}\n\n\t\tif (!Array.isArray(keys)) {\n\t\t\tthrow new TypeError(\"keys must be an array\");\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 && !this.#isExpired(item) ? 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.#evictItem();\n\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) {\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.#removeItem(item);\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 * Expired items are removed before returning false.\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\n\t\tif (item !== undefined && this.#isExpired(item)) {\n\t\t\tthis.#removeItem(item);\n\t\t\treturn false;\n\t\t}\n\n\t\treturn item !== undefined;\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 * Removes an item from the cache without incrementing the deletes stat.\n\t * Used internally by get()/has() when removing expired items.\n\t *\n\t * @param {Object} item - The cache item to remove.\n\t * @private\n\t */\n\t#removeItem(item) {\n\t\tdelete this.items[item.key];\n\t\tthis.size--;\n\t\tthis.#unlink(item);\n\t\titem.prev = null;\n\t\titem.next = null;\n\t}\n\n\t/**\n\t * Evicts the least recently used item from the cache without firing onEvict.\n\t * Used internally by setWithEvicted() to avoid double-notification.\n\t *\n\t * @returns {Object} The evicted item.\n\t * @private\n\t */\n\t#evictItem() {\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\n\t\treturn item;\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 * Eviction is silent — onEvict is not fired for the returned 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 && !this.#isExpired(item)) {\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 (item !== undefined) {\n\t\t\t\tthis.#removeItem(item);\n\t\t\t}\n\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tconst evictedItem = this.#evictItem();\n\t\t\t\tevicted = {\n\t\t\t\t\tkey: evictedItem.key,\n\t\t\t\t\tvalue: evictedItem.value,\n\t\t\t\t\texpiry: evictedItem.expiry,\n\t\t\t\t};\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 && !this.#isExpired(item)) {\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 (item !== undefined) {\n\t\t\t\tthis.#removeItem(item);\n\t\t\t}\n\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 = [];\n\t\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\t\tif (!this.#isExpired(x)) {\n\t\t\t\t\tresult.push(x.value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t}\n\n\t\tif (!Array.isArray(keys)) {\n\t\t\tthrow new TypeError(\"keys must be an array\");\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 && !this.#isExpired(item) ? 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. Expired items are skipped.\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; ) {\n\t\t\tconst next = x.next;\n\t\t\tif (!this.#isExpired(x)) {\n\t\t\t\tcallback.call(thisArg, x.value, x.key, this);\n\t\t\t}\n\t\t\tx = next;\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\tif (!Array.isArray(keys)) {\n\t\t\tthrow new TypeError(\"keys must be an array\");\n\t\t}\n\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\tif (!Array.isArray(keys)) {\n\t\t\tthrow new TypeError(\"keys must be an array\");\n\t\t}\n\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\tif (!Array.isArray(keys)) {\n\t\t\tthrow new TypeError(\"keys must be an array\");\n\t\t}\n\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\tif (!this.#isExpired(x)) {\n\t\t\t\tresult.push({\n\t\t\t\t\tkey: x.key,\n\t\t\t\t\tvalue: x.value,\n\t\t\t\t\texpiry: x.expiry,\n\t\t\t\t});\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\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (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: 0 };\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\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (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\tif (this.ttl === 0) {\n\t\t\treturn { valid: this.values(), expired: [], noTTL: this.values() };\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst valid = [];\n\t\tconst expired = [];\n\n\t\tfor (let x = this.first; x !== null; x = x.next) {\n\t\t\tif (x.expiry > now) {\n\t\t\t\tvalid.push(x.value);\n\t\t\t} else {\n\t\t\t\texpired.push(x.value);\n\t\t\t}\n\t\t}\n\n\t\treturn { valid, expired, noTTL: [] };\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\treturn new LRU(max, ttl, resetTTL);\n}\n"],"names":["LRU","stats","onEvict","constructor","max","ttl","resetTTL","Number","isInteger","TypeError","this","first","items","Object","create","last","size","hits","misses","sets","deletes","evictions","clear","x","next","prev","key","item","undefined","removeItem","entries","keys","result","isExpired","push","value","Array","isArray","from","length","i","evict","evictItem","expiry","expiresAt","Date","now","peek","get","moveToEnd","has","unlink","setWithEvicted","evicted","evictedItem","set","values","forEach","callback","thisArg","call","getMany","hasAll","hasAny","cleanup","removed","rebuildList","toJSON","sizeByTTL","valid","expired","noTTL","keysByTTL","valuesByTTL","lru"],"mappings":"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"} \ No newline at end of file diff --git a/src/lru.js b/src/lru.js index 00cb72f..8b03036 100644 --- a/src/lru.js +++ b/src/lru.js @@ -11,14 +11,26 @@ export class LRU { /** * 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(). + * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ constructor(max = 0, ttl = 0, resetTTL = false) { + if (!Number.isInteger(max) || max < 0) { + throw new TypeError("Invalid max value"); + } + + if (!Number.isInteger(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } + + if (typeof resetTTL !== "boolean") { + throw new TypeError("Invalid resetTTL value"); + } + this.first = null; this.items = Object.create(null); this.last = null; @@ -66,14 +78,8 @@ export class LRU { const item = this.items[key]; if (item !== undefined) { - delete this.items[key]; - this.size--; + this.#removeItem(item); this.#stats.deletes++; - - this.#unlink(item); - - item.prev = null; - item.next = null; } return this; @@ -89,14 +95,25 @@ export class LRU { */ entries(keys) { if (keys === undefined) { - keys = this.keys(); + const result = []; + for (let x = this.first; x !== null; x = x.next) { + if (!this.#isExpired(x)) { + result.push([x.key, x.value]); + } + } + + return result; + } + + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); } 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]; + result[i] = [key, item !== undefined && !this.#isExpired(item) ? item.value : undefined]; } return result; @@ -112,20 +129,8 @@ export class LRU { 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); - } + const item = this.#evictItem(); - item.prev = null; - item.next = null; if (this.#onEvict !== null) { this.#onEvict({ key: item.key, @@ -156,7 +161,7 @@ export class LRU { * @private */ #isExpired(item) { - if (this.ttl === 0 || item.expiry === 0) { + if (this.ttl === 0) { return false; } @@ -191,7 +196,7 @@ export class LRU { return item.value; } - this.delete(key); + this.#removeItem(item); this.#stats.misses++; return undefined; } @@ -202,13 +207,20 @@ export class LRU { /** * Checks if a key exists in the cache. + * Expired items are removed before returning false. * * @param {string} key - The key to check for. * @returns {boolean} True if the key exists and is not expired, false otherwise. */ has(key) { const item = this.items[key]; - return item !== undefined && !this.#isExpired(item); + + if (item !== undefined && this.#isExpired(item)) { + this.#removeItem(item); + return false; + } + + return item !== undefined; } /** @@ -236,6 +248,47 @@ export class LRU { } } + /** + * Removes an item from the cache without incrementing the deletes stat. + * Used internally by get()/has() when removing expired items. + * + * @param {Object} item - The cache item to remove. + * @private + */ + #removeItem(item) { + delete this.items[item.key]; + this.size--; + this.#unlink(item); + item.prev = null; + item.next = null; + } + + /** + * Evicts the least recently used item from the cache without firing onEvict. + * Used internally by setWithEvicted() to avoid double-notification. + * + * @returns {Object} The evicted item. + * @private + */ + #evictItem() { + 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.prev = null; + item.next = null; + + return item; + } + /** * 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 @@ -277,6 +330,7 @@ export class LRU { /** * Sets a value in the cache and returns any evicted item. + * Eviction is silent — onEvict is not fired for the returned item. * * @param {string} key - The key to set. * @param {*} value - The value to store. @@ -286,20 +340,24 @@ export class LRU { let evicted = null; let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; } this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { + const evictedItem = this.#evictItem(); evicted = { - key: this.first.key, - value: this.first.value, - expiry: this.first.expiry, + key: evictedItem.key, + value: evictedItem.value, + expiry: evictedItem.expiry, }; - this.evict(); } item = this.items[key] = { @@ -333,7 +391,7 @@ export class LRU { set(key, value) { let item = this.items[key]; - if (item !== undefined) { + if (item !== undefined && !this.#isExpired(item)) { item.value = value; if (this.resetTTL) { @@ -342,6 +400,10 @@ export class LRU { this.moveToEnd(item); } else { + if (item !== undefined) { + this.#removeItem(item); + } + if (this.max > 0 && this.size === this.max) { this.evict(); } @@ -378,18 +440,24 @@ export class LRU { */ values(keys) { if (keys === undefined) { - const result = Array.from({ length: this.size }); - let i = 0; + const result = []; for (let x = this.first; x !== null; x = x.next) { - result[i++] = x.value; + if (!this.#isExpired(x)) { + result.push(x.value); + } } + return result; } + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + 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; + result[i] = item !== undefined && !this.#isExpired(item) ? item.value : undefined; } return result; @@ -398,15 +466,19 @@ export class LRU { /** * 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. + * get() or peek(), so it does not update LRU order. Expired items are skipped. * * @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); + for (let x = this.first; x !== null; ) { + const next = x.next; + if (!this.#isExpired(x)) { + callback.call(thisArg, x.value, x.key, this); + } + x = next; } return this; @@ -419,6 +491,10 @@ export class LRU { * @returns {Object} Object mapping keys to values (undefined for missing/expired keys). */ getMany(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + const result = Object.create(null); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -435,6 +511,10 @@ export class LRU { * @returns {boolean} True if all keys exist and are not expired. */ hasAll(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (!this.has(keys[i])) { return false; @@ -451,6 +531,10 @@ export class LRU { * @returns {boolean} True if any key exists and is not expired. */ hasAny(keys) { + if (!Array.isArray(keys)) { + throw new TypeError("keys must be an array"); + } + for (let i = 0; i < keys.length; i++) { if (this.has(keys[i])) { return true; @@ -504,11 +588,13 @@ export class LRU { toJSON() { const result = []; for (let x = this.first; x !== null; x = x.next) { - result.push({ - key: x.key, - value: x.value, - expiry: x.expiry, - }); + if (!this.#isExpired(x)) { + result.push({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } } return result; @@ -552,20 +638,16 @@ export class LRU { 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) { + if (x.expiry > now) { valid++; } else { expired++; } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: 0 }; } /** @@ -581,20 +663,16 @@ export class LRU { 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) { + if (x.expiry > now) { valid.push(x.key); } else { expired.push(x.key); } } - return { valid, expired, noTTL }; + return { valid, expired, noTTL: [] }; } /** @@ -603,13 +681,23 @@ export class LRU { * @returns {Object} Object with valid, expired, and noTTL arrays of values. */ valuesByTTL() { - const keysByTTL = this.keysByTTL(); + if (this.ttl === 0) { + return { valid: this.values(), expired: [], noTTL: this.values() }; + } - return { - valid: this.values(keysByTTL.valid), - expired: this.values(keysByTTL.expired), - noTTL: this.values(keysByTTL.noTTL), - }; + const now = Date.now(); + const valid = []; + const expired = []; + + for (let x = this.first; x !== null; x = x.next) { + if (x.expiry > now) { + valid.push(x.value); + } else { + expired.push(x.value); + } + } + + return { valid, expired, noTTL: [] }; } /** @@ -657,17 +745,5 @@ export class LRU { * @throws {TypeError} When parameters are invalid (negative numbers or wrong types). */ export 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); } diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index 6f3d7d4..a15cb67 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -1355,9 +1355,9 @@ describe("LRU Cache", function () { cache.items["b"].expiry = 0; const counts = cache.sizeByTTL(); - assert.equal(counts.valid, 3); - assert.equal(counts.expired, 0); - assert.equal(counts.noTTL, 2); + assert.equal(counts.valid, 1); + assert.equal(counts.expired, 2); + assert.equal(counts.noTTL, 0); }); it("should handle mixed expired and valid items", async function () { @@ -1441,12 +1441,12 @@ describe("LRU Cache", function () { 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.equal(result.valid.length, 1); + assert.equal(result.expired.length, 2); + assert.deepEqual(result.noTTL, []); assert.ok(result.valid.includes("c")); + assert.ok(result.expired.includes("a")); + assert.ok(result.expired.includes("b")); }); it("should return empty arrays for empty cache", function () { @@ -1516,12 +1516,12 @@ describe("LRU Cache", function () { 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.equal(result.valid.length, 1); + assert.equal(result.expired.length, 2); + assert.deepEqual(result.noTTL, []); assert.ok(result.valid.includes(3)); + assert.ok(result.expired.includes(1)); + assert.ok(result.expired.includes(2)); }); it("should return correct expired values after TTL", async function () { @@ -1575,4 +1575,179 @@ describe("LRU Cache", function () { assert.ok(result.expired.includes(3)); }); }); + + describe("Edge case fixes (issue #487)", function () { + it("should throw for non-array keys in entries()", function () { + const cache = new LRU(3); + cache.set("a", 1); + assert.throws(() => cache.entries(null), TypeError, "keys must be an array"); + assert.throws(() => cache.entries("abc"), TypeError, "keys must be an array"); + }); + + it("should throw for non-array keys in values()", function () { + const cache = new LRU(3); + cache.set("a", 1); + assert.throws(() => cache.values(null), TypeError, "keys must be an array"); + assert.throws(() => cache.values(5), TypeError, "keys must be an array"); + }); + + it("should throw for non-array keys in getMany()", function () { + const cache = new LRU(3); + cache.set("a", 1); + assert.throws(() => cache.getMany(null), TypeError, "keys must be an array"); + assert.throws(() => cache.getMany(5), TypeError, "keys must be an array"); + }); + + it("should throw for non-array keys in hasAll()", function () { + const cache = new LRU(3); + cache.set("a", 1); + assert.throws(() => cache.hasAll(null), TypeError, "keys must be an array"); + assert.throws(() => cache.hasAll("abc"), TypeError, "keys must be an array"); + }); + + it("should throw for non-array keys in hasAny()", function () { + const cache = new LRU(3); + cache.set("a", 1); + assert.throws(() => cache.hasAny(undefined), TypeError, "keys must be an array"); + assert.throws(() => cache.hasAny(5), TypeError, "keys must be an array"); + }); + + it("should validate max in constructor", function () { + assert.throws(() => new LRU(-1), TypeError, "Invalid max value"); + assert.throws(() => new LRU("10"), TypeError, "Invalid max value"); + assert.throws(() => new LRU(2.5), TypeError, "Invalid max value"); + assert.throws(() => new LRU(Infinity), TypeError, "Invalid max value"); + assert.throws(() => new LRU(null), TypeError, "Invalid max value"); + assert.throws(() => new LRU(""), TypeError, "Invalid max value"); + }); + + it("should validate ttl in constructor", function () { + assert.throws(() => new LRU(10, -1), TypeError, "Invalid ttl value"); + assert.throws(() => new LRU(10, "100"), TypeError, "Invalid ttl value"); + assert.throws(() => new LRU(10, 2.5), TypeError, "Invalid ttl value"); + assert.throws(() => new LRU(10, Infinity), TypeError, "Invalid ttl value"); + }); + + it("should validate resetTTL in constructor", function () { + assert.throws(() => new LRU(10, 0, "true"), TypeError, "Invalid resetTTL value"); + assert.throws(() => new LRU(10, 0, 1), TypeError, "Invalid resetTTL value"); + }); + + it("should reclaim expired key on set() with resetTTL=false", async function () { + const cache = new LRU(5, 50, false); + cache.set("k", "v"); + await new Promise((resolve) => setTimeout(resolve, 80)); + cache.set("k", "v2"); + assert.equal(cache.has("k"), true); + assert.equal(cache.get("k"), "v2"); + assert.equal(cache.size, 1); + }); + + it("should reclaim expired key on setWithEvicted()", async function () { + const cache = new LRU(1, 50, false); + cache.set("a", 1); + await new Promise((resolve) => setTimeout(resolve, 80)); + const evicted = cache.setWithEvicted("a", 2); + assert.equal(evicted, null); + assert.equal(cache.size, 1); + assert.equal(cache.get("a"), 2); + }); + + it("should skip expired items in values()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1); + cache.set("b", 2); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.deepEqual(cache.values(), []); + }); + + it("should skip expired items in entries()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.deepEqual(cache.entries(), []); + }); + + it("should skip expired items in toJSON()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.deepEqual(cache.toJSON(), []); + }); + + it("should skip expired items in forEach()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1); + cache.set("b", 2); + await new Promise((resolve) => setTimeout(resolve, 80)); + const seen = []; + cache.forEach((value, key) => seen.push(key)); + assert.deepEqual(seen, []); + }); + + it("should be mutation-safe in forEach() when deleting current item", function () { + const cache = new LRU(10); + cache.set("a", 1).set("b", 2).set("c", 3).set("d", 4); + const seen = []; + cache.forEach((value, key) => { + seen.push(key); + cache.delete(key); + }); + assert.deepEqual(seen, ["a", "b", "c", "d"]); + }); + + it("should not increment deletes when get() removes expired item", async function () { + const cache = new LRU(5, 50, false); + cache.set("k", "v"); + await new Promise((resolve) => setTimeout(resolve, 80)); + cache.get("k"); + const stats = cache.stats(); + assert.equal(stats.deletes, 0); + assert.equal(stats.misses, 1); + }); + + it("should delete expired items in getMany()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1).set("b", 2); + await new Promise((resolve) => setTimeout(resolve, 80)); + const result = cache.getMany(["a", "b"]); + assert.equal(result.a, undefined); + assert.equal(result.b, undefined); + assert.equal(cache.size, 0); + }); + + it("should treat expiry=0 with ttl>0 as expired in sizeByTTL()", function () { + const cache = new LRU(10, 100); + cache.set("a", 1).set("b", 2).set("c", 3); + cache.items["a"].expiry = 0; + cache.items["b"].expiry = 0; + const counts = cache.sizeByTTL(); + assert.equal(counts.valid, 1); + assert.equal(counts.expired, 2); + assert.equal(counts.noTTL, 0); + }); + + it("should treat items as expired when ttl is raised after insertion", function () { + const cache = new LRU(10, 0); + cache.set("a", 1); + assert.equal(cache.ttl, 0); + assert.equal(cache.get("a"), 1); + + cache.ttl = 5000; + assert.equal(cache.has("a"), false); + assert.equal(cache.get("a"), undefined); + assert.equal(cache.size, 0); + }); + + it("should not fire onEvict for setWithEvicted() silent eviction", function () { + const cache = new LRU(2); + let cbCount = 0; + cache.onEvict(() => cbCount++); + cache.set("a", 1).set("b", 2); + const evicted = cache.setWithEvicted("c", 3); + assert.notEqual(evicted, null); + assert.equal(evicted.key, "a"); + assert.equal(cbCount, 0); + }); + }); });