tiny-lru
Version:
A fast, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support.
473 lines (352 loc) • 15.4 kB
Markdown
[](https://www.npmjs.com/package/tiny-lru)
[](https://www.npmjs.com/package/tiny-lru)
[](https://github.com/avoidwork/tiny-lru/blob/master/LICENSE)
[](https://www.npmjs.com/package/tiny-lru)
[](https://github.com/avoidwork/tiny-lru/actions?query=workflow%3Aci)
[](https://app.codecov.io/gh/avoidwork/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?
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 ~12 KB for lru-cache)
## Installation
```bash
npm install tiny-lru
```
Requires Node.js ≥14 or modern browsers with ES Module support.
## Quick Start
```javascript
import { lru } from "tiny-lru";
// Create a cache that holds up to 100 items
const cache = lru(100);
// Store and retrieve data
cache.set("user:42", { name: "Alice", score: 1500 });
const user = cache.get("user:42"); // { name: "Alice", score: 1500 }
// Chain operations
cache.set("a", 1).set("b", 2).set("c", 3);
// Check what's in the cache
cache.has("a"); // true
cache.size; // 3
cache.keys(); // ['a', 'b', 'c'] (LRU order)
```
```typescript
import { LRU } from "tiny-lru";
interface User {
id: number;
name: string;
}
const cache = new LRU<User>(100);
cache.set("user:1", { id: 1, name: "Alice" });
const user: User | undefined = cache.get("user:1");
```
Items can automatically expire after a set time:
```javascript
// Cache that expires after 5 seconds
const sessionCache = lru(100, 5000);
sessionCache.set("session:id", { userId: 123 });
// After 5 seconds, this returns undefined
sessionCache.get("session:id");
```
Want TTL to reset when you update an item? Enable `resetTtl`:
```javascript
const cache = lru(100, 60000, true); // 1 minute TTL, resets on update
cache.set("key", "value");
cache.set("key", "new value"); // TTL resets
```
**Great for:**
- API response caching
- 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:**
- Non-string keys (works best with strings)
- Very large caches (consider a database)
## API Reference
### Factory Function: `lru(max?, ttl?, resetTtl?)`
Creates a new LRU cache instance with parameter validation.
```javascript
import { lru } from "tiny-lru";
const cache1 = lru(); // 1000 items, no TTL
const cache2 = lru(500); // 500 items, no TTL
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()` |
**Returns:** `LRU` - New cache instance
**Throws:** `TypeError` if parameters are invalid
Creates an LRU cache instance without parameter validation.
```javascript
import { LRU } from "tiny-lru";
const cache = new LRU(100, 5000);
```
**Parameters:**
| 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()` |
| Property | Type | Description |
| --------- | ---------------- | ------------------------------------------ |
| `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 |
### Methods
| 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. 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. |
| `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}`. |
| `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`. |
| `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}`. |
```javascript
function memoize(fn, maxSize = 100) {
const cache = lru(maxSize);
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Cache expensive computations
const fib = memoize((n) => (n <= 1 ? n : fib(n - 1) + fib(n - 2)), 50);
fib(100); // fast - cached
fib(100); // even faster - from cache
```
```javascript
// 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) {
return cached;
}
// Fetch from database
const user = await db.users.findById(userId);
// Store in cache
cache.set(`user:${userId}`, user);
return user;
}
```
```javascript
const cache = lru(3);
cache.set("a", 1).set("b", 2).set("c", 3);
const evicted = cache.setWithEvicted("d", 4);
console.log(evicted); // { key: 'a', value: 1, expiry: 0 }
cache.keys(); // ['b', 'c', 'd']
```
```javascript
const cache = lru(100);
cache.set("users:1", { name: "Alice" });
cache.set("users:2", { name: "Bob" });
cache.set("users:3", { name: "Carol" });
// Get values for specific keys (order matches input array)
const values = cache.values(["users:3", "users:1"]);
// ['Carol', 'Alice'] - matches input key order
```
```javascript
import _ from "lodash";
import { lru } from "tiny-lru";
_.memoize.Cache = lru().constructor;
const slowFunc = _.memoize(expensiveOperation);
slowFunc.cache.max = 100; // Configure cache size
```
```javascript
import { LRU } from "tiny-lru";
class AuthCache {
constructor() {
// 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);
// 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);
}
}
```
```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()));
}
}
```
| 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
All core operations are O(1):
- **Set**: Add or update items
- **Get**: Retrieve and promote to most recent
- **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](https://github.com/avoidwork/tiny-lru/blob/master/benchmarks/README.md) for more details.
## Development
```bash
npm install # Install dependencies
npm test # Run lint and tests
npm run lint # Lint and check formatting
npm run fix # Fix lint and formatting issues
npm run build # Build distribution files
npm run coverage # Generate test coverage report
```
### Build Output
Build produces multiple module formats. When you install from npm, you'll get:
- `dist/tiny-lru.js` - ES Modules
- `dist/tiny-lru.cjs` - CommonJS
- `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 |
| --------- | ----- |
| Tests | 149 |
| Suites | 26 |
## Test Coverage
| Metric | Coverage |
| --------- | -------- |
| Lines | 100% |
| Branches | 99.28% |
| Functions | 100% |
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Run `npm test` to ensure all tests pass
4. Commit your changes (`git commit -m 'Add amazing feature'`)
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`
- [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
Copyright (c) 2026, Jason Mulligan
BSD-3-Clause