lru_map
lru_map is a small bounded JavaScript map backed by a native Map and a doubly linked list. Reading with get marks an entry as most recently used, and adding beyond the numeric entry limit removes the least recently used pair. Its API resembles Map and adds find for a read that does not change recency, shift for explicit eviction, oldest and newest entry pointers, iterable ordering, JSON-friendly output, and bundled TypeScript declarations.
Still understandable and useful for a bare entry-count cache in older module systems. New projects should usually install lru-cache or quick-lru for maintained ESM packaging, expiration, disposal, and fewer API surprises.
Use it if
- You need a tiny dependency-free cache limited only by entry count
- Both recency-changing get and recency-neutral find are useful to your eviction policy
- Iteration from least recently used to most recently used is useful for inspection or persistence
- CommonJS, AMD, or a browser-global UMD build fits an older JavaScript codebase
- You need expiration, stale values, size-based limits, async fetch deduplication, abort signals, or disposal callbacks: none are first-class features
- You expect exact Map semantics: delete returns the removed value rather than a boolean, set silently discards the evicted pair, and iteration is recency order
- You want a current ESM package: 0.4.1 ships one UMD main file, was published in October 2020, and has no exports or module field
- You need a stable post-1.0 contract: the current version is 0.4.1, and the README's example still implies set returns an evicted entry even though current source and types return the cache
- You prefer normal dependency maintenance: the README actually recommends copying lru.js and lru.d.ts into your source tree, which makes upstream fixes and provenance harder to track
Setup reality
`npm install lru_map` brings no runtime dependencies and loads `dist/lru.js`, a UMD bundle that supports CommonJS, AMD, and a browser global. There is no modern conditional exports map or native ESM entry. Node can use `const {LRUMap} = require('lru_map')`; ESM named-import interop depends on the runtime or bundler's CommonJS handling, so test it rather than assuming. Type declarations are included, but they were authored against TypeScript 3.9-era tooling. Capacity is an entry count only: a thousand one-byte values and a thousand hundred-megabyte buffers are equivalent to the cache. There is no TTL, max byte size, stale policy, automatic loader, promise coalescing, metrics, or built-in disposal event. `get` changes recency, while `find` and `has` do not. `set` returns the map for chaining and internally calls `shift()` when over capacity, discarding the evicted pair. The README suggests overriding shift for cleanup, which works because internal eviction calls it, but monkeypatching an instance method is a brittle lifecycle hook. Entry objects exposed by oldest and newest contain linked-list pointers behind symbols and are invalidated by mutation; the README says never store or modify them. The mutable public `limit` property is not a safe resize API because lowering it does not immediately trim all excess entries. The build requires modern JavaScript features such as Map, Symbol.iterator, const, and let; the README points ES5 users to an older v2 branch.
Patterns
Create an entry-count-limited cachecreate-bounded-cache
const {LRUMap} = require('lru_map');
const cache = new LRUMap(3);
cache.set('a', 1).set('b', 2).set('c', 3);
console.log(cache.size); // 3The limit counts entries, not bytes. Values of radically different memory sizes count the same.
Read while refreshing recencyread-and-refresh
const cache = new LRUMap(2, [['a', 1], ['b', 2]]);
cache.get('a'); // a becomes newest
cache.set('c', 3); // b is evicted
console.log(cache.has('a'), cache.has('b')); // true falseget changes cache state even though it looks like a read. Use find when inspection must not affect eviction.
Read without changing recencypeek-without-refresh
const cache = new LRUMap(2, [['a', 1], ['b', 2]]);
const value = cache.find('a');
cache.set('c', 3);
console.log(value, cache.has('a')); // 1 falsefind leaves a as the oldest entry, so the next insertion evicts it.
Check membership without a cache hitcheck-without-refresh
if (cache.has(key)) {
console.log('present but recency unchanged');
}
const value = cache.get(key); // this call refreshes recencyhas does not mark an entry used. Calling has and then get performs two lookups; use get alone when undefined cannot be a stored value.
Remove the least recently used pairevict-oldest-manually
const removed = cache.shift();
if (removed) {
const [key, value] = removed;
dispose(key, value);
}shift returns a [key, value] tuple or undefined. Automatic eviction from set calls shift but does not return that tuple to the caller.
Delete a key and receive its valuedelete-and-read-value
const removedValue = cache.delete('session-42');
if (removedValue !== undefined) {
closeSession(removedValue);
}Unlike native Map.delete, this returns the removed value rather than true or false. A stored undefined value cannot be distinguished from absence.
Seed the cache from entriesinitialize-from-entries
const cache = new LRUMap(3, [
['oldest', 1],
['middle', 2],
['newest', 3],
]);Input order becomes oldest to newest. More seed entries than the numeric limit throws an overflow error during assignment.
Use the entry count as capacityinfer-limit-from-entries
const cache = new LRUMap([
['one', 1],
['two', 2],
]);
console.log(cache.limit); // 2The iterable-only constructor fixes the limit to the initial size. Additions immediately evict unless limit is changed, and changing it is not a complete resize operation.
Iterate from oldest to newestiterate-by-recency
for (const [key, value] of cache) {
console.log(key, value);
}
console.log([...cache.keys()]);
console.log([...cache.values()]);Iteration order is eviction order, not insertion order after reads. Avoid mutating the cache while consuming an iterator.
Inspect oldest and newest entriesinspect-recency-ends
const oldestKey = cache.oldest?.key;
const newestKey = cache.newest?.key;
console.log({oldestKey, newestKey});Entry objects are invalidated when the map changes. Read key and value immediately and never retain or modify the entry object.
Create JSON-friendly cache dataserialize-cache-order
const payload = JSON.stringify(cache.toJSON());
// [{"key":...,"value":...}] from oldest to newesttoJSON returns objects with key and value fields, not Map-style pairs. Complex keys may not round-trip through JSON.
Run cleanup whenever shift evictsdispose-on-eviction
const originalShift = cache.shift;
cache.shift = function () {
const removed = originalShift.call(this);
if (removed) dispose(removed[0], removed[1]);
return removed;
};The README documents this override because automatic eviction calls shift. It is instance monkeypatching, so lru-cache's native disposal hooks are safer for larger systems.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lru-cache | npm | You need actively maintained TTL, max-size calculation, disposal hooks, stale policies, or async fetch support |
| quick-lru | npm | You want a modern ESM cache with maxAge and a compact API for ordinary applications |
| mnemonist | npm | You need LRU caches alongside a broader collection of tested data structures |