# Tiny LRU

[![npm version](https://img.shields.io/npm/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru)
[![npm downloads](https://img.shields.io/npm/dm/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru)
[![License](https://img.shields.io/npm/l/tiny-lru.svg)](https://github.com/avoidwork/tiny-lru/blob/master/LICENSE)
[![Node.js version](https://img.shields.io/node/v/tiny-lru.svg)](https://www.npmjs.com/package/tiny-lru)
[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions?query=workflow%3Aci)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](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

```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");
```

## With TTL (Time-to-Live)

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
```

## When to Use Tiny LRU

**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

### Class: `new LRU(max?, ttl?, resetTtl?)`

Creates an LRU cache instance without parameter validation.

```javascript
import { LRU } from "tiny-lru";

const cache = new LRU(100, 5000);
```

**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()`                |

### Properties

| 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}`. |

## Common Patterns

### Memoization

```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
```

### Cache-Aside Pattern

```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;
}
```

### Finding What Was Evicted

```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']
```

## Advanced Usage

### Batch Operations with Keys

```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
```

### Interop with Lodash

```javascript
import _ from "lodash";
import { lru } from "tiny-lru";

_.memoize.Cache = lru().constructor;

const slowFunc = _.memoize(expensiveOperation);
slowFunc.cache.max = 100; // Configure cache size
```

### Session and Authentication Caching

```javascript
import { LRU } from "tiny-lru";

class AuthCache {
  constructor() {
    // Session cache: 30 minutes with TTL reset on 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);
  }
}
```

### LLM Response Caching

```javascript
import { LRU } from "tiny-lru";

class LLMCache {
  constructor() {
    // Cache up to 1000 responses for 1 hour
    this.cache = new LRU(1000, 3600000); // 1 hour TTL
  }

  async getResponse(model, prompt, params = {}) {
    const key = this.generateKey(model, prompt, params);

    // Check cache first
    const cached = this.cache.get(key);
    if (cached) {
      return { ...cached, fromCache: true };
    }

    // Make expensive API call
    const response = await this.callLLMAPI(model, prompt, params);

    // Cache the response
    this.cache.set(key, {
      response: response.text,
      tokens: response.tokens,
      timestamp: Date.now(),
    });

    return { ...response, fromCache: false };
  }

  generateKey(model, prompt, params = {}) {
    const paramsHash = this.hashObject(params);
    const promptHash = this.hashString(prompt);
    return `llm:${model}:${promptHash}:${paramsHash}`;
  }

  hashString(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = (hash << 5) - hash + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(36);
  }

  hashObject(obj) {
    return this.hashString(JSON.stringify(obj, Object.keys(obj).sort()));
  }
}
```

## Why Tiny LRU?

| Feature          | tiny-lru     | lru-cache   | quick-lru   |
| ---------------- | ------------ | ----------- | ----------- |
| Bundle size      | ~2.2 KB      | ~12 KB      | ~1.5 KB     |
| O(1) operations  | ✅           | ✅          | ✅          |
| TTL support      | ✅           | ✅          | ✅          |
| TypeScript       | ✅           | ✅          | ✅          |
| Zero dependencies| ✅           | ❌          | ✅          |
| Pure LRU         | ✅           | ❌*         | ✅          |

\* lru-cache uses a hybrid design that can hold 2× the specified size for performance

## Performance

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`

## Documentation

- [API Reference](https://github.com/avoidwork/tiny-lru/blob/master/docs/API.md) - Complete API documentation
- [Technical Documentation](https://github.com/avoidwork/tiny-lru/blob/master/docs/TECHNICAL_DOCUMENTATION.md) - Architecture, performance, and security
- [Code Style Guide](https://github.com/avoidwork/tiny-lru/blob/master/docs/CODE_STYLE_GUIDE.md) - Contributing guidelines

## License

Copyright (c) 2026, Jason Mulligan

BSD-3-Clause
