diff --git a/.cursor/rules/javascript.mdc b/.cursor/rules/javascript.mdc deleted file mode 100644 index 78c6287..0000000 --- a/.cursor/rules/javascript.mdc +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: JavaScript module -globs: -alwaysApply: true ---- - -- Use JSDoc standard for creating docblocks of functions and classes. -- Always use camelCase for function names. -- Always use upper-case snake_case for constants. -- Create integration tests in 'tests/integration' that use node-assert, which run with mocha. -- Create unit tests in 'tests/unit' that use node-assert, which run with mocha. -- Use node.js community "Best Practices". -- Adhere to DRY, KISS, YAGNI, & SOLID principles -- Adhere to OWASP security guidance diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5a6cd4b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,77 @@ +# AGENTS.md + +## Project Overview + +`tiny-lru` is a high-performance, lightweight LRU (Least Recently Used) cache library for JavaScript with O(1) operations and optional TTL support. + +## Setup Commands + +```bash +npm install # Install dependencies +npm run build # Lint and build (runs lint then rollup) +npm run rollup # Build with rollup +npm run test # Run lint and tests +npm run mocha # Run tests with coverage (c8) +npm run fmt # Format code with oxfmt +npm run lint # Lint code with oxlint +``` + +## Development Workflow + +Source code is in `src/`. + +- **lint**: Check and fix linting issues with oxlint +- **fmt**: Format code with oxfmt +- **build**: Lint + rollup build +- **mocha**: Run test suite with coverage reporting + +## Project Structure + +``` +├── src/lru.js # Main LRU cache implementation +├── tests/ # Test files +├── benchmarks/ # Performance benchmarks +├── dist/ # Built distribution files +├── types/ # TypeScript definitions +├── docs/ # Documentation +├── rollup.config.js # Build configuration +└── package.json # Project configuration +``` + +## Code Style + +- Indentation: Tabs +- Quotes: Double quotes +- Semicolons: Required +- Array constructor: Avoid `new Array()` (oxlint will warn) + +## API Reference + +- `lru(max, ttl, resetTtl)` - Factory function to create cache +- `LRU` class - Direct instantiation with `new LRU(max, ttl, resetTtl)` +- Key methods: `get()`, `set()`, `delete()`, `has()`, `clear()`, `evict()` +- Array methods: `keys()`, `values()`, `entries()` +- Properties: `first`, `last`, `max`, `size`, `ttl`, `resetTtl` + +## Testing + +- Framework: Node.js built-in test runner (`node --test`) +- Coverage: 100% (c8) +- Test pattern: `tests/**/*.js` +- All tests must pass with 100% coverage before merging +- Run: `npm test` (lint + tests) + +## Common Issues to Avoid + +- **Memory leaks**: When removing items from the linked list, always clear `prev`/`next` pointers to allow garbage collection +- **LRU order pollution**: Methods like `entries()` and `values()` should access items directly rather than calling `get()`, which moves items and can delete expired items mid-iteration +- **TTL edge cases**: Direct property access (`this.items[key]`) should be used instead of `has()` when you need to inspect expired-but-not-yet-deleted items +- **Dead code**: Always verify edge case code is actually reachable before adding special handling +- **Constructor assignment**: Use `let` not `const` for variables that may be reassigned (e.g., in `setWithEvicted`) + +## Implementation Notes + +- The LRU uses a doubly-linked list with `first` and `last` pointers for O(1) operations +- TTL is stored per-item as an `expiry` timestamp; `0` means no expiration +- `moveToEnd()` is the core method for maintaining LRU order - item is always moved to the `last` position +- `setWithEvicted()` optimizes updates by avoiding the full `set()` path for existing keys diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index 75d00f8..e3a1539 100644 --- a/README.md +++ b/README.md @@ -1,264 +1,125 @@ -# 🚀 Tiny LRU +# Tiny LRU -[![npm version](https://badge.fury.io/js/tiny-lru.svg)](https://badge.fury.io/js/tiny-lru) -[![Node.js Version](https://img.shields.io/node/v/tiny-lru.svg)](https://nodejs.org/) -[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![Build Status](https://github.com/avoidwork/tiny-lru/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/tiny-lru/actions) -[![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/avoidwork/tiny-lru) +A fast, lightweight LRU (Least Recently Used) cache for JavaScript with O(1) operations and optional TTL support. -A **high-performance, lightweight** LRU cache for JavaScript with **strong UPDATE performance and competitive SET/GET/DELETE**, and a **compact bundle size**. Built for developers who need fast caching without compromising on features. +## What is an LRU Cache? -## 📦 Installation +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. + +## Installation ```bash npm install tiny-lru -# or -yarn add tiny-lru -# or -pnpm add tiny-lru ``` -**Requirements:** Node.js ≥12 +Requires Node.js ≥12. -## ⚡ Quick Start +## Quick Start ```javascript -import {lru} from "tiny-lru"; - -// Create cache and start using immediately -const cache = lru(100); // Max 100 items -cache.set('user:123', {name: 'John', age: 30}); -const user = cache.get('user:123'); // {name: 'John', age: 30} - -// With TTL (5 second expiration) -const tempCache = lru(50, 5000); -tempCache.set('session', 'abc123'); // Automatically expires after 5 seconds -``` - -## 📑 Table of Contents - -- [✨ Features & Benefits](#-features--benefits) -- [📊 Performance Deep Dive](#-performance-deep-dive) -- [📖 API Reference](#-api-reference) -- [🚀 Getting Started](#-getting-started) -- [💡 Real-World Examples](#-real-world-examples) -- [🔗 Interoperability](#-interoperability) -- [🛠️ Development](#️-development) -- [📄 License](#-license) - -## ✨ Features & Benefits - -### Why Choose Tiny LRU? - -- **🔄 Strong Cache Updates** - Excellent performance in update-heavy workloads -- **📦 Compact Bundle** - Just ~2.2 KiB minified for a full-featured LRU library -- **⚖️ Balanced Performance** - Competitive across all operations with O(1) complexity -- **⏱️ TTL Support** - Optional time-to-live with automatic expiration -- **🔄 Method Chaining** - Fluent API for better developer experience -- **🎯 TypeScript Ready** - Full TypeScript support with complete type definitions -- **🌐 Universal Compatibility** - Works seamlessly in Node.js and browsers -- **🛡️ Production Ready** - Battle-tested and reliable - -### Benchmark Comparison (Mean of 5 runs) - -| Library | SET ops/sec | GET ops/sec | UPDATE ops/sec | DELETE ops/sec | -|---------|-------------|-------------|----------------|----------------| -| **tiny-lru** | 404,753 | 1,768,449 | 1,703,716 | 298,770 | -| lru-cache | 326,221 | 1,069,061 | 878,858 | 277,734 | -| quick-lru | 591,683 | 1,298,487 | 935,481 | 359,600 | -| mnemonist | 412,467 | 2,478,778 | 2,156,690 | 0 | - -Notes: -- Mean values computed from the Performance Summary across 5 consecutive runs of `npm run benchmark:comparison`. -- mnemonist lacks a compatible delete method in this harness, so DELETE ops/sec is 0. -- Performance varies by hardware, Node.js version, and workload patterns; run the provided benchmarks locally to assess your specific use case. -- Environment: Node.js v24.5.0, macOS arm64. +import { lru } from "tiny-lru"; -## 📊 Performance Deep Dive - -### When to Choose Tiny LRU - -**✅ Perfect for:** -- **Frequent cache updates** - Leading UPDATE performance -- **Mixed read/write workloads** - Balanced across all operations -- **Bundle size constraints** - Compact library with full features -- **Production applications** - Battle-tested with comprehensive testing +// Create a cache that holds up to 100 items +const cache = lru(100); -### Running Your Own Benchmarks +// Store and retrieve data +cache.set("user:42", { name: "Alice", score: 1500 }); +const user = cache.get("user:42"); // { name: "Alice", score: 1500 } -```bash -# Run all performance benchmarks -npm run benchmark:all +// Chain operations +cache.set("a", 1).set("b", 2).set("c", 3); -# Individual benchmark suites -npm run benchmark:modern # Comprehensive Tinybench suite -npm run benchmark:perf # Performance measurements -npm run benchmark:comparison # Compare against other LRU libraries +// Check what's in the cache +cache.has("a"); // true +cache.size; // 3 +cache.keys(); // ['a', 'b', 'c'] (LRU order) ``` -## 🚀 Getting Started - -### Installation - -```bash -npm install tiny-lru -# or -yarn add tiny-lru -# or -pnpm add tiny-lru -``` +## With TTL (Time-to-Live) -### Quick Examples +Items can automatically expire after a set time: ```javascript -import {lru} from "tiny-lru"; - -// Basic cache -const cache = lru(100); -cache.set('key1', 'value1') - .set('key2', 'value2') - .set('key3', 'value3'); - -console.log(cache.get('key1')); // 'value1' -console.log(cache.size); // 3 +// Cache that expires after 5 seconds +const sessionCache = lru(100, 5000); -// With TTL (time-to-live) -const cacheWithTtl = lru(50, 30000); // 30 second TTL -cacheWithTtl.set('temp-data', {important: true}); -// Automatically expires after 30 seconds - -const resetCache = lru(25, 10000, true); -resetCache.set('session', 'user123'); -// Because resetTtl is true, TTL resets when you set() the same key again +sessionCache.set("session:id", { userId: 123 }); +// After 5 seconds, this returns undefined +sessionCache.get("session:id"); ``` -### CDN Usage (Browser) - -```html - - - - - - +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 ``` -### TypeScript Usage +## When to Use Tiny LRU -```typescript -import {lru, LRU} from "tiny-lru"; +**Great for:** +- API response caching +- Function memoization +- Session storage with expiration +- Rate limiting +- Any scenario where you want to limit memory usage -// Type-safe cache -const cache = lru(100); -// or: const cache: LRU = lru(100); -cache.set('user:123', 'John Doe'); -const user: string | undefined = cache.get('user:123'); +**Not ideal for:** +- Non-string keys (works best with strings) +- Very large caches (consider a database) -// Class inheritance -class MyCache extends LRU { - constructor() { - super(1000, 60000, true); // 1000 items, 1 min TTL, reset TTL on set - } -} -``` +## API Reference -### Configuration Options +### Factory Function: `lru(max?, ttl?, resetTtl?)` -#### Factory Function ```javascript -import {lru} from "tiny-lru"; +import { lru } from "tiny-lru"; -const cache = lru(max, ttl = 0, resetTtl = false); +const cache = lru(); // 1000 items, no TTL +const cache = lru(500); // 500 items, no TTL +const cache = lru(100, 30000); // 100 items, 30s TTL +const cache = lru(100, 60000, true); // with resetTtl enabled ``` -**Parameters:** -- `max` `{Number}` - Maximum number of items (0 = unlimited, default: 1000) -- `ttl` `{Number}` - Time-to-live in milliseconds (0 = no expiration, default: 0) -- `resetTtl` `{Boolean}` - Reset TTL when updating existing items via `set()` (default: false) +### Class: `new LRU(max?, ttl?, resetTtl?)` -#### Class Constructor ```javascript -import {LRU} from "tiny-lru"; +import { LRU } from "tiny-lru"; -const cache = new LRU(1000, 60000, true); // 1000 items, 1 min TTL, reset TTL on set +const cache = new LRU(100, 5000); ``` -#### Best Practices -```javascript -// 1. Size your cache appropriately -const cache = lru(1000); // Not too small, not too large - -// 2. Use meaningful keys -cache.set(`user:${userId}:profile`, userProfile); -cache.set(`product:${productId}:details`, productDetails); +### Methods -// 3. Handle cache misses gracefully -function getData(key) { - const cached = cache.get(key); - if (cached !== undefined) { - return cached; - } - - // Fallback to slower data source - const data = expensiveOperation(key); - cache.set(key, data); - return data; -} +| Method | Description | +|--------|-------------| +| `set(key, value)` | Store a value. Returns `this` for chaining. | +| `get(key)` | Retrieve a value. Moves item to most recent. | +| `has(key)` | Check if key exists and is not expired. | +| `delete(key)` | Remove an item. Returns `this` for chaining. | +| `clear()` | Remove all items. Returns `this` for chaining. | +| `evict()` | Remove the least recently used item. | +| `keys()` | Get all keys in LRU order (oldest first). | +| `values(keys?)` | Get all values, or values for specific keys. | +| `entries(keys?)` | Get `[key, value]` pairs in LRU order. | +| `expiresAt(key)` | Get expiration timestamp for a key. | -// 4. Clean up when needed -process.on('exit', () => { - cache.clear(); // Help garbage collection -}); -``` +### Properties -#### Optimization Tips -- **Cache Size**: Keep cache size reasonable (1000-10000 items for most use cases) -- **TTL Usage**: Only use TTL when necessary; it adds overhead -- **Key Types**: String keys perform better than object keys -- **Memory**: Call `clear()` when done to help garbage collection +| Property | Type | Description | +|----------|------|-------------| +| `size` | number | Current number of items | +| `max` | number | Maximum items allowed | +| `ttl` | number | Time-to-live in milliseconds | +| `first` | object | Least recently used item | +| `last` | object | Most recently used item | -## 💡 Real-World Examples +## Common Patterns -### API Response Caching +### Memoization ```javascript -import {lru} from "tiny-lru"; - -class ApiClient { - constructor() { - this.cache = lru(100, 300000); // 5 minute cache - } - - async fetchUser(userId) { - const cacheKey = `user:${userId}`; - - // Return cached result if available - if (this.cache.has(cacheKey)) { - return this.cache.get(cacheKey); - } - - // Fetch from API and cache - const response = await fetch(`/api/users/${userId}`); - const user = await response.json(); - - this.cache.set(cacheKey, user); - return user; - } -} -``` - -### Function Memoization - -```javascript -import {lru} from "tiny-lru"; - function memoize(fn, maxSize = 100) { const cache = lru(maxSize); @@ -269,386 +130,106 @@ function memoize(fn, maxSize = 100) { return cache.get(key); } - const result = fn.apply(this, args); + const result = fn(...args); cache.set(key, result); return result; }; } -// Usage -const expensiveCalculation = memoize((n) => { - console.log(`Computing for ${n}`); - return n * n * n; -}, 50); - -console.log(expensiveCalculation(5)); // Computing for 5 -> 125 -console.log(expensiveCalculation(5)); // 125 (cached) +// 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 ``` -### Session Management +### Cache-Aside Pattern ```javascript -import {lru} from "tiny-lru"; - -class SessionManager { - constructor() { - // 30 minute TTL, with resetTtl enabled for set() - this.sessions = lru(1000, 1800000, true); - } - - createSession(userId, data) { - const sessionId = this.generateId(); - const session = { - userId, - data, - createdAt: Date.now() - }; - - this.sessions.set(sessionId, session); - return sessionId; - } - - getSession(sessionId) { - // get() does not extend TTL; to extend, set the session again when resetTtl is true - return this.sessions.get(sessionId); - } - - endSession(sessionId) { - this.sessions.delete(sessionId); +async function getUser(userId) { + const cache = lru(1000, 60000); // 1 minute cache + + // 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; } ``` - - -## 🔗 Interoperability - -Compatible with Lodash's `memoize` function cache interface: +### Finding What Was Evicted ```javascript -import _ from "lodash"; -import {lru} from "tiny-lru"; +const cache = lru(3); +cache.set("a", 1).set("b", 2).set("c", 3); -_.memoize.Cache = lru().constructor; -const memoized = _.memoize(myFunc); -memoized.cache.max = 10; -``` - -## 🛠️ Development - -### Testing - -Tiny LRU maintains 100% test coverage with comprehensive unit and integration tests. - -```bash -# Run all tests with coverage -npm test +const evicted = cache.setWithEvicted("d", 4); +console.log(evicted); // { key: 'a', value: 1, expiry: 0 } -# Run tests with verbose output -npm run mocha - -# Lint code -npm run lint - -# Full build (lint + build) -npm run build -``` - -**Test Coverage:** 100% coverage across all modules - -```console -----------|---------|----------|---------|---------|------------------- -File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------|---------|----------|---------|---------|------------------- -All files | 100 | 100 | 100 | 100 | - lru.js | 100 | 100 | 100 | 100 | -----------|---------|----------|---------|---------|------------------- +cache.keys(); // ['b', 'c', 'd'] ``` -### Contributing - -#### Quick Start for Contributors - -```bash -# Clone and setup -git clone https://github.com/avoidwork/tiny-lru.git -cd tiny-lru -npm install - -# Run tests -npm test - -# Run linting -npm run lint - -# Run benchmarks -npm run benchmark:all - -# Build distribution files -npm run build -``` - -#### Development Workflow - -1. **Fork** the repository on GitHub -2. **Clone** your fork locally -3. **Create** a feature branch: `git checkout -b feature/amazing-feature` -4. **Develop** your changes with tests -5. **Test** thoroughly: `npm test && npm run lint` -6. **Commit** using conventional commits: `git commit -m "feat: add amazing feature"` -7. **Push** to your fork: `git push origin feature/amazing-feature` -8. **Submit** a Pull Request - -#### Contribution Guidelines - -- **Code Quality**: Follow ESLint rules and existing code style -- **Testing**: Maintain 100% test coverage for all changes -- **Documentation**: Update README.md and JSDoc for API changes -- **Performance**: Benchmark changes that could impact performance -- **Compatibility**: Ensure Node.js ≥12 compatibility -- **Commit Messages**: Use [Conventional Commits](https://conventionalcommits.org/) format +## Advanced Usage ---- - -## 📖 API Reference - -### Factory Function - -#### lru(max, ttl, resetTtl) - -Creates a new LRU cache instance using the factory function. - -**Parameters:** -- `max` `{Number}` - Maximum number of items to store (default: 1000; 0 = unlimited) -- `ttl` `{Number}` - Time-to-live in milliseconds (default: 0; 0 = no expiration) -- `resetTtl` `{Boolean}` - Reset TTL when updating existing items via `set()` (default: false) - -**Returns:** `{LRU}` New LRU cache instance - -**Throws:** `{TypeError}` When parameters are invalid +### Batch Operations with Keys ```javascript -import {lru} from "tiny-lru"; - -// Basic cache const cache = lru(100); +cache.set("users:1", { name: "Alice" }); +cache.set("users:2", { name: "Bob" }); +cache.set("users:3", { name: "Carol" }); -// With TTL -const cacheWithTtl = lru(50, 30000); // 30 second TTL - -// With resetTtl enabled for set() -const resetCache = lru(25, 10000, true); - -// Validation errors -lru(-1); // TypeError: Invalid max value -lru(100, -1); // TypeError: Invalid ttl value -lru(100, 0, "no"); // TypeError: Invalid resetTtl value -``` - -### Properties - -#### first -`{Object|null}` - Item in first (least recently used) position - -```javascript -const cache = lru(); -cache.first; // null - empty cache -``` - -#### last -`{Object|null}` - Item in last (most recently used) position - -```javascript -const cache = lru(); -cache.last; // null - empty cache -``` - -#### max -`{Number}` - Maximum number of items to hold in cache - -```javascript -const cache = lru(500); -cache.max; // 500 -``` - -#### resetTtl -`{Boolean}` - Whether to reset TTL when updating existing items via `set()` - -```javascript -const cache = lru(500, 5*6e4, true); -cache.resetTtl; // true -``` - -#### size -`{Number}` - Current number of items in cache - -```javascript -const cache = lru(); -cache.size; // 0 - empty cache -``` - -#### ttl -`{Number}` - TTL in milliseconds (0 = no expiration) - -```javascript -const cache = lru(100, 3e4); -cache.ttl; // 30000 -``` - -### Methods - -#### clear() -Removes all items from cache. - -**Returns:** `{Object}` LRU instance - -```javascript -cache.clear(); -``` - -#### delete(key) -Removes specified item from cache. - -**Parameters:** -- `key` `{String}` - Item key - -**Returns:** `{Object}` LRU instance - -```javascript -cache.set('key1', 'value1'); -cache.delete('key1'); -console.log(cache.has('key1')); // false -``` - -#### entries([keys]) -Returns array of cache items as `[key, value]` pairs. - -**Parameters:** -- `keys` `{Array}` - Optional array of specific keys to retrieve (defaults to all keys) - -**Returns:** `{Array}` Array of `[key, value]` pairs - -```javascript -cache.set('a', 1).set('b', 2); -console.log(cache.entries()); // [['a', 1], ['b', 2]] -console.log(cache.entries(['a'])); // [['a', 1]] -``` - -#### evict() -Removes the least recently used item from cache. - -**Returns:** `{Object}` LRU instance - -```javascript -cache.set('old', 'value').set('new', 'value'); -cache.evict(); // Removes 'old' item -``` - -#### expiresAt(key) -Gets expiration timestamp for cached item. - -**Parameters:** -- `key` `{String}` - Item key - -**Returns:** `{Number|undefined}` Expiration time (epoch milliseconds) or undefined if key doesn't exist - -```javascript -const cache = new LRU(100, 5000); // 5 second TTL -cache.set('key1', 'value1'); -console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now +// Get values for specific keys +const values = cache.values(["users:3", "users:1"]); +// ['Carol', 'Alice'] - maintains LRU order ``` -#### get(key) -Retrieves cached item and promotes it to most recently used position. - -**Parameters:** -- `key` `{String}` - Item key - -**Returns:** `{*}` Item value or undefined if not found/expired - -Note: `get()` does not reset or extend TTL. TTL is only reset on `set()` when `resetTtl` is `true`. +### Interop with Lodash ```javascript -cache.set('key1', 'value1'); -console.log(cache.get('key1')); // 'value1' -console.log(cache.get('nonexistent')); // undefined -``` - -#### has(key) -Checks if key exists in cache (without promoting it). - -**Parameters:** -- `key` `{String}` - Item key - -**Returns:** `{Boolean}` True if key exists and is not expired - -```javascript -cache.set('key1', 'value1'); -console.log(cache.has('key1')); // true -console.log(cache.has('nonexistent')); // false -``` - -#### keys() -Returns array of all cache keys in LRU order (first = least recent). - -**Returns:** `{Array}` Array of keys - -```javascript -cache.set('a', 1).set('b', 2); -cache.get('a'); // Move 'a' to most recent -console.log(cache.keys()); // ['b', 'a'] -``` - -#### set(key, value) -Stores item in cache as most recently used. - -**Parameters:** -- `key` `{String}` - Item key -- `value` `{*}` - Item value +import _ from "lodash"; +import { lru } from "tiny-lru"; -**Returns:** `{Object}` LRU instance +_.memoize.Cache = lru().constructor; -```javascript -cache.set('key1', 'value1') - .set('key2', 'value2') - .set('key3', 'value3'); +const slowFunc = _.memoize(expensiveOperation); +slowFunc.cache.max = 100; // Configure cache size ``` -#### setWithEvicted(key, value) -Stores item and returns evicted item if cache was full. +## Performance -**Parameters:** -- `key` `{String}` - Item key -- `value` `{*}` - Item value +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 -**Returns:** `{Object|null}` Evicted item `{key, value, expiry, prev, next}` or null +## Development -```javascript -const cache = new LRU(2); -cache.set('a', 1).set('b', 2); -const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} -if (evicted) { - console.log(`Evicted: ${evicted.key}`, evicted.value); -} +```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 ``` -#### values([keys]) -Returns array of cache values. - -**Parameters:** -- `keys` `{Array}` - Optional array of specific keys to retrieve (defaults to all keys) - -**Returns:** `{Array}` Array of values - -```javascript -cache.set('a', 1).set('b', 2); -console.log(cache.values()); // [1, 2] -console.log(cache.values(['a'])); // [1] -``` +## Test Coverage ---- +| Metric | Coverage | +|--------|----------| +| Lines | 100% | +| Branches | 95% | +| Functions | 100% | -## 📄 License +## License -Copyright (c) 2026 Jason Mulligan -Licensed under the BSD-3 license. +BSD-3-Clause diff --git a/benchmarks/README.md b/benchmarks/README.md index c6d16b6..dc9247e 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -16,6 +16,7 @@ This directory contains modern benchmark implementations for the tiny-lru librar - Realistic workload scenarios without measuring setup/teardown **Test Categories**: + - SET operations (empty cache, full cache, eviction scenarios) - GET operations (hit/miss patterns, access patterns) - Mixed operations (real-world 80/20 read-write scenarios) @@ -26,7 +27,7 @@ This directory contains modern benchmark implementations for the tiny-lru librar **Comprehensive comparison against other popular LRU cache libraries** -- **Libraries Tested**: +- **Libraries Tested**: - `tiny-lru` (this library) - `lru-cache` (most popular npm LRU implementation) - `quick-lru` (fast, lightweight alternative) @@ -39,6 +40,7 @@ This directory contains modern benchmark implementations for the tiny-lru librar - Multiple operations per measured callback to reduce harness overhead **Test Categories**: + - SET operations across all libraries - GET operations with pre-populated caches - DELETE operations comparison @@ -59,6 +61,7 @@ This directory contains modern benchmark implementations for the tiny-lru librar - Deterministic mixed workloads (no `Math.random()` in measured loops) **Test Categories**: + - Performance Observer based function timing - Custom timer with statistical analysis - Scalability tests (100 to 10,000 cache sizes) @@ -111,6 +114,7 @@ node benchmarks/comparison-benchmark.js ## Understanding the Results ### Tinybench Output + ``` ┌─────────┬─────────────────────────────┬─────────────────┬────────────────────┬──────────┬─────────┐ │ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ @@ -124,6 +128,7 @@ node benchmarks/comparison-benchmark.js - **Samples**: Number of samples collected for statistical significance ### Performance Observer Output + ``` ┌─────────────┬─────────┬────────────┬────────────┬────────────┬───────────────┬─────────┬────────┐ │ Function │ Calls │ Avg (ms) │ Min (ms) │ Max (ms) │ Median (ms) │ Std Dev │Ops/sec │ @@ -132,6 +137,7 @@ node benchmarks/comparison-benchmark.js ``` ### Comparison Benchmark Output + ``` 📊 SET Operations Benchmark ┌─────────┬─────────────────────────────┬─────────────────┬────────────────────┬──────────┬─────────┐ @@ -155,71 +161,88 @@ Memory Usage Results: ## Benchmark Categories Explained ### SET Operations + Tests cache write performance under various conditions: + - **Empty cache**: Setting items in a fresh cache - **Full cache**: Setting items when cache is at capacity (triggers eviction) - **Random vs Sequential**: Different access patterns - + Implementation details: + - Deterministic keys/values are pre-generated once per run - Access indices are precomputed via a fast PRNG (xorshift) to avoid runtime randomness - Multiple operations are executed per benchmark callback to minimize harness overhead -### GET Operations +### GET Operations + Tests cache read performance: + - **Cache hits**: Reading existing items - **Cache misses**: Reading non-existent items - **Mixed patterns**: Realistic 80% hit / 20% miss scenarios - + Implementation details: + - Caches are pre-populated outside the measured section - Access indices are precomputed; no `Math.random()` inside measured loops ### Mixed Operations + Real-world usage simulation: + - **80/20 read-write**: Typical web application pattern - **Cache warming**: Sequential population scenarios - **High churn**: Frequent eviction scenarios - **LRU access patterns**: Testing LRU algorithm efficiency - + Implementation details: + - Choice and index streams are precomputed - No wall-clock calls (`Date.now`) inside hot paths ### Special Operations + Edge cases and additional functionality: + - **Delete operations**: Individual item removal - **Clear operations**: Complete cache clearing - **Different data types**: Numbers, objects, strings - **Memory usage**: Heap consumption analysis - + Implementation details: + - Delete benchmarks maintain a steady state by re-adding deleted keys to keep cardinality stable ## Best Practices Implemented ### 1. Statistical Significance + - Minimum execution time (1 second) for reliable results - Multiple iterations for statistical validity - Standard deviation and margin of error reporting ### 2. Realistic Test Data + - Variable key/value sizes mimicking real applications - Deterministic pseudo-random and sequential access patterns (precomputed) - Pre-population scenarios for realistic cache states ### 3. Multiple Measurement Approaches + - **Tinybench**: Modern, accurate micro-benchmarking - **Performance Observer**: Native Node.js function timing - **Custom timers**: High-resolution manual timing ### 4. Comprehensive Coverage + - Different cache sizes (100, 1K, 5K, 10K) - Various workload patterns - Memory consumption analysis - Edge case testing ### 5. Methodology Improvements (Current) + - Setup/teardown moved outside measured sections to avoid skewing results - Deterministic data and access patterns (no randomness in hot paths) - Batched operations per invocation reduce harness overhead reliably across tasks @@ -228,6 +251,7 @@ Implementation details: ## Performance Tips ### For accurate results: + 1. **Close other applications** to reduce system noise 2. **Run multiple times** and compare results 3. **Use consistent hardware** for comparisons @@ -235,13 +259,15 @@ Implementation details: 5. **Consider CPU frequency scaling** on laptops ### Environment information included: + - Node.js version -- Platform and architecture +- Platform and architecture - Timestamp for result tracking ## Interpreting Results ### Good Performance Indicators: + - ✅ **Consistent ops/sec** across runs - ✅ **Low margin of error** (< 5%) - ✅ **Reasonable standard deviation** @@ -249,6 +275,7 @@ Implementation details: - ✅ **Cache hits faster than misses** ### Warning Signs: + - ⚠️ **High margin of error** (> 10%) - ⚠️ **Widely varying results** between runs - ⚠️ **Memory usage growing unexpectedly** @@ -260,16 +287,17 @@ To add new benchmark scenarios: ```javascript // In modern-benchmark.js -bench.add('your-test-name', () => { +bench.add("your-test-name", () => { // Your test code here const cache = lru(1000); - cache.set('key', 'value'); + cache.set("key", "value"); }); ``` ## Contributing When adding new benchmarks: + 1. Follow the existing naming conventions 2. Include proper setup/teardown 3. Add statistical significance checks @@ -279,6 +307,7 @@ When adding new benchmarks: ## Benchmark Results Archive Consider saving benchmark results with: + ```bash # Save all benchmark results npm run benchmark:all > results/benchmark-$(date +%Y%m%d-%H%M%S).txt @@ -296,19 +325,22 @@ This helps track performance improvements/regressions over time. Choose the right benchmark for your needs: ### Use `modern-benchmark.js` when: + - ✅ You want comprehensive analysis of tiny-lru performance - ✅ You need statistical significance and margin of error data - ✅ You're testing different cache sizes and workload patterns - ✅ You want realistic scenario testing ### Use `comparison-benchmark.js` when: + - ✅ You're evaluating tiny-lru against other LRU libraries - ✅ You need bundle size and memory usage comparisons - ✅ You want to see competitive performance analysis - ✅ You're making library selection decisions ### Use `performance-observer-benchmark.js` when: + - ✅ You need native Node.js performance measurement - ✅ You want function-level timing analysis - ✅ You're testing scalability across different cache sizes -- ✅ You prefer Performance API over external libraries \ No newline at end of file +- ✅ You prefer Performance API over external libraries diff --git a/benchmarks/comparison-benchmark.js b/benchmarks/comparison-benchmark.js index 69cb2cd..2cde8be 100644 --- a/benchmarks/comparison-benchmark.js +++ b/benchmarks/comparison-benchmark.js @@ -12,29 +12,29 @@ import { lru as tinyLru } from "../src/lru.js"; let LRUCache, QuickLRU, MnemonistLRU; try { - const lruCacheModule = await import("lru-cache"); - LRUCache = lruCacheModule.LRUCache || lruCacheModule.default; + const lruCacheModule = await import("lru-cache"); + LRUCache = lruCacheModule.LRUCache || lruCacheModule.default; } catch { - console.error("lru-cache not found. Run: npm install --no-save lru-cache"); - process.exit(1); + console.error("lru-cache not found. Run: npm install --no-save lru-cache"); + process.exit(1); } try { - const quickLruModule = await import("quick-lru"); - QuickLRU = quickLruModule.default; + const quickLruModule = await import("quick-lru"); + QuickLRU = quickLruModule.default; } catch { - console.error("quick-lru not found. Run: npm install --no-save quick-lru"); - process.exit(1); + console.error("quick-lru not found. Run: npm install --no-save quick-lru"); + process.exit(1); } try { - // Import from mnemonist using the correct export pattern - const mnemonistModule = await import("mnemonist"); - MnemonistLRU = mnemonistModule.LRUCacheWithDelete; + // Import from mnemonist using the correct export pattern + const mnemonistModule = await import("mnemonist"); + MnemonistLRU = mnemonistModule.LRUCacheWithDelete; } catch (error) { - console.error("mnemonist not found. Run: npm install --no-save mnemonist"); - console.error("Error:", error.message); - process.exit(1); + console.error("mnemonist not found. Run: npm install --no-save mnemonist"); + console.error("Error:", error.message); + process.exit(1); } // Configuration @@ -49,20 +49,20 @@ const OPS_PER_INVOCATION = 50; // Do multiple ops per call to reduce harness ove * @param {number} count - Number of items to generate * @returns {{keys: string[], values: Array<{id:number,data:string,nested:{foo:string,baz:number}}>} } */ -function generateTestData (count) { - const keys = new Array(count); - const values = new Array(count); - - for (let i = 0; i < count; i++) { - keys[i] = `key_${i}`; - values[i] = { - id: i, - data: `value_${i}`, - nested: { foo: "bar", baz: i } - }; - } - - return { keys, values }; +function generateTestData(count) { + const keys = Array.from({ length: count }); + const values = Array.from({ length: count }); + + for (let i = 0; i < count; i++) { + keys[i] = `key_${i}`; + values[i] = { + id: i, + data: `value_${i}`, + nested: { foo: "bar", baz: i }, + }; + } + + return { keys, values }; } /** @@ -72,31 +72,33 @@ function generateTestData (count) { * @param {number} modulo - Upper bound for indices * @returns {Uint32Array} */ -function generateAccessPattern (length, modulo) { - const pattern = new Uint32Array(length); - let x = 123456789; - let y = 362436069; - // Xorshift-based fast PRNG to avoid using Math.random() - for (let i = 0; i < length; i++) { - x ^= x << 13; x ^= x >>> 17; x ^= x << 5; - y = y + 1 >>> 0; - const n = x + y >>> 0; - pattern[i] = n % modulo; - } - - return pattern; +function generateAccessPattern(length, modulo) { + const pattern = new Uint32Array(length); + let x = 123456789; + let y = 362436069; + // Xorshift-based fast PRNG to avoid using Math.random() + for (let i = 0; i < length; i++) { + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + y = (y + 1) >>> 0; + const n = (x + y) >>> 0; + pattern[i] = n % modulo; + } + + return pattern; } // Initialize caches -function createCaches () { - return { - "tiny-lru": tinyLru(CACHE_SIZE), - "tiny-lru-ttl": tinyLru(CACHE_SIZE, TTL_MS), - "lru-cache": new LRUCache({ max: CACHE_SIZE }), - "lru-cache-ttl": new LRUCache({ max: CACHE_SIZE, ttl: TTL_MS }), - "quick-lru": new QuickLRU({ maxSize: CACHE_SIZE }), - "mnemonist": new MnemonistLRU(CACHE_SIZE) - }; +function createCaches() { + return { + "tiny-lru": tinyLru(CACHE_SIZE), + "tiny-lru-ttl": tinyLru(CACHE_SIZE, TTL_MS), + "lru-cache": new LRUCache({ max: CACHE_SIZE }), + "lru-cache-ttl": new LRUCache({ max: CACHE_SIZE, ttl: TTL_MS }), + "quick-lru": new QuickLRU({ maxSize: CACHE_SIZE }), + mnemonist: new MnemonistLRU(CACHE_SIZE), + }; } // Memory usage helper @@ -106,420 +108,446 @@ function createCaches () { * @param {boolean} force - Whether to call global.gc() if available * @returns {NodeJS.MemoryUsage} */ -function getMemoryUsage (force = false) { - if (force && global.gc) { - global.gc(); - } +function getMemoryUsage(force = false) { + if (force && global.gc) { + global.gc(); + } - return process.memoryUsage(); + return process.memoryUsage(); } // Calculate memory per item -function calculateMemoryPerItem (beforeMem, afterMem, itemCount) { - const heapDiff = afterMem.heapUsed - beforeMem.heapUsed; +function calculateMemoryPerItem(beforeMem, afterMem, itemCount) { + const heapDiff = afterMem.heapUsed - beforeMem.heapUsed; - return Math.round(heapDiff / itemCount); + return Math.round(heapDiff / itemCount); } // Bundle size estimation (approximate) const bundleSizes = { - "tiny-lru": "2.1KB", - "lru-cache": "~15KB", - "quick-lru": "~1.8KB", - "mnemonist": "~45KB" + "tiny-lru": "2.1KB", + "lru-cache": "~15KB", + "quick-lru": "~1.8KB", + mnemonist: "~45KB", }; -async function runBenchmarks () { - console.log("🚀 LRU Cache Library Comparison Benchmark\n"); - console.log(`Cache Size: ${CACHE_SIZE} items`); - console.log(`Iterations: ${ITERATIONS.toLocaleString()}`); - console.log(`Node.js: ${process.version}`); - console.log(`Platform: ${process.platform} ${process.arch}\n`); - - const testData = generateTestData(ITERATIONS); - const setPattern = generateAccessPattern(ITERATIONS, testData.keys.length); - const getPattern = generateAccessPattern(ITERATIONS, Math.min(CACHE_SIZE, 500)); - const updatePattern = generateAccessPattern(ITERATIONS, 100); - const deletePattern = generateAccessPattern(ITERATIONS, 50); - - // SET operations benchmark - console.log("📊 SET Operations Benchmark"); - console.log("=" .repeat(50)); - - const setBench = new Bench({ time: 2000 }); - - // Dedicated caches and state for SET to avoid measuring setup per-iteration - const setCaches = createCaches(); - const setState = { - "tiny-lru": 0, - "tiny-lru-ttl": 0, - "lru-cache": 0, - "lru-cache-ttl": 0, - "quick-lru": 0, - "mnemonist": 0 - }; - - setBench - .add("tiny-lru set", () => { - const cache = setCaches["tiny-lru"]; - let i = setState["tiny-lru"]; // cursor - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState["tiny-lru"] = i; - }) - .add("tiny-lru-ttl set", () => { - const cache = setCaches["tiny-lru-ttl"]; - let i = setState["tiny-lru-ttl"]; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState["tiny-lru-ttl"] = i; - }) - .add("lru-cache set", () => { - const cache = setCaches["lru-cache"]; - let i = setState["lru-cache"]; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState["lru-cache"] = i; - }) - .add("lru-cache-ttl set", () => { - const cache = setCaches["lru-cache-ttl"]; - let i = setState["lru-cache-ttl"]; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState["lru-cache-ttl"] = i; - }) - .add("quick-lru set", () => { - const cache = setCaches["quick-lru"]; - let i = setState["quick-lru"]; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState["quick-lru"] = i; - }) - .add("mnemonist set", () => { - const cache = setCaches.mnemonist; - let i = setState.mnemonist; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = setPattern[i++ % setPattern.length]; - cache.set(testData.keys[idx], testData.values[idx]); - } - setState.mnemonist = i; - }); - - await setBench.run(); - console.table(setBench.table()); - - // GET operations benchmark (with pre-populated caches) - console.log("\n📊 GET Operations Benchmark"); - console.log("=" .repeat(50)); - - const caches = createCaches(); - - // Pre-populate all caches deterministically - const prepopulated = Math.min(CACHE_SIZE, 500); - Object.values(caches).forEach(cache => { - for (let i = 0; i < prepopulated; i++) { - cache.set(testData.keys[i], testData.values[i]); - } - }); - - const getBench = new Bench({ time: 2000 }); - const getState = { idx: 0 }; - - getBench - .add("tiny-lru get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches["tiny-lru"].get(testData.keys[idx]); - } - getState.idx = i; - }) - .add("tiny-lru-ttl get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches["tiny-lru-ttl"].get(testData.keys[idx]); - } - getState.idx = i; - }) - .add("lru-cache get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches["lru-cache"].get(testData.keys[idx]); - } - getState.idx = i; - }) - .add("lru-cache-ttl get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches["lru-cache-ttl"].get(testData.keys[idx]); - } - getState.idx = i; - }) - .add("quick-lru get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches["quick-lru"].get(testData.keys[idx]); - } - getState.idx = i; - }) - .add("mnemonist get", () => { - let i = getState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = getPattern[i++ % getPattern.length]; - caches.mnemonist.get(testData.keys[idx]); - } - getState.idx = i; - }); - - await getBench.run(); - console.table(getBench.table()); - - // DELETE operations benchmark - console.log("\n📊 DELETE Operations Benchmark"); - console.log("=" .repeat(50)); - - const deleteBench = new Bench({ time: 2000 }); - - // Dedicated caches and state for DELETE - const deleteCaches = { - "tiny-lru": tinyLru(CACHE_SIZE), - "lru-cache": new LRUCache({ max: CACHE_SIZE }), - "quick-lru": new QuickLRU({ maxSize: CACHE_SIZE }), - "mnemonist": new MnemonistLRU(CACHE_SIZE) - }; - const deleteState = { idx: 0 }; - - // Pre-populate - Object.values(deleteCaches).forEach(cache => { - for (let i = 0; i < 100; i++) { - cache.set(testData.keys[i], testData.values[i]); - } - }); - - deleteBench - .add("tiny-lru delete", () => { - let i = deleteState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = deletePattern[i++ % deletePattern.length]; - deleteCaches["tiny-lru"].delete(testData.keys[idx]); - // Re-add to keep steady state for future deletes - deleteCaches["tiny-lru"].set(testData.keys[idx], testData.values[idx]); - } - deleteState.idx = i; - }) - .add("lru-cache delete", () => { - let i = deleteState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = deletePattern[i++ % deletePattern.length]; - deleteCaches["lru-cache"].delete(testData.keys[idx]); - deleteCaches["lru-cache"].set(testData.keys[idx], testData.values[idx]); - } - deleteState.idx = i; - }) - .add("quick-lru delete", () => { - let i = deleteState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = deletePattern[i++ % deletePattern.length]; - deleteCaches["quick-lru"].delete(testData.keys[idx]); - deleteCaches["quick-lru"].set(testData.keys[idx], testData.values[idx]); - } - deleteState.idx = i; - }) - .add("mnemonist delete", () => { - let i = deleteState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = deletePattern[i++ % deletePattern.length]; - deleteCaches.mnemonist.remove(testData.keys[idx]); - deleteCaches.mnemonist.set(testData.keys[idx], testData.values[idx]); - } - deleteState.idx = i; - }); - - await deleteBench.run(); - console.table(deleteBench.table()); - - // UPDATE operations benchmark - console.log("\n📊 UPDATE Operations Benchmark"); - console.log("=" .repeat(50)); - - const updateBench = new Bench({ time: 2000 }); - - // Dedicated caches for UPDATE - const updateCaches = createCaches(); - // Pre-populate with initial values - Object.values(updateCaches).forEach(cache => { - for (let i = 0; i < 100; i++) { - cache.set(testData.keys[i], testData.values[i]); - } - }); - - const updateState = { idx: 0 }; - - updateBench - .add("tiny-lru update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches["tiny-lru"].set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }) - .add("tiny-lru-ttl update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches["tiny-lru-ttl"].set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }) - .add("lru-cache update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches["lru-cache"].set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }) - .add("lru-cache-ttl update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches["lru-cache-ttl"].set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }) - .add("quick-lru update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches["quick-lru"].set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }) - .add("mnemonist update", () => { - let i = updateState.idx; - for (let j = 0; j < OPS_PER_INVOCATION; j++) { - const idx = updatePattern[i++ % updatePattern.length]; - updateCaches.mnemonist.set(testData.keys[idx], testData.values[(idx + 50) % testData.values.length]); - } - updateState.idx = i; - }); - - await updateBench.run(); - console.table(updateBench.table()); - - // Memory usage analysis - console.log("\n📊 Memory Usage Analysis"); - console.log("=" .repeat(50)); - - const memoryResults = {}; - const testSize = 1000; - - for (const [name, cache] of Object.entries(createCaches())) { - const beforeMem = getMemoryUsage(true); - - // Fill cache - for (let i = 0; i < testSize; i++) { - cache.set(testData.keys[i], testData.values[i]); - } - - const afterMem = getMemoryUsage(true); - const memoryPerItem = calculateMemoryPerItem(beforeMem, afterMem, testSize); - - memoryResults[name] = { - totalMemory: afterMem.heapUsed - beforeMem.heapUsed, - memoryPerItem: memoryPerItem, - bundleSize: bundleSizes[name.split("-")[0]] || "N/A" - }; - } - - console.log("\nMemory Usage Results:"); - console.log("┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐"); - console.log("│ Library │ Bundle Size │ Memory/Item │ Total Memory │"); - console.log("├─────────────────┼─────────────────┼─────────────────┼─────────────────┤"); - - Object.entries(memoryResults).forEach(([name, data]) => { - const nameCol = name.padEnd(15); - const bundleCol = data.bundleSize.padEnd(15); - const memoryCol = `${data.memoryPerItem} bytes`.padEnd(15); - const totalCol = `${Math.round(data.totalMemory / 1024)} KB`.padEnd(15); - console.log(`│ ${nameCol} │ ${bundleCol} │ ${memoryCol} │ ${totalCol} │`); - }); - - console.log("└─────────────────┴─────────────────┴─────────────────┴─────────────────┘"); - - // Performance summary - console.log("\n📊 Performance Summary"); - console.log("=" .repeat(50)); - - const setResults = setBench.tasks.map(task => ({ - name: task.name, - opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0 - })); - - const getResults = getBench.tasks.map(task => ({ - name: task.name, - opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0 - })); - - const updateResults = updateBench.tasks.map(task => ({ - name: task.name, - opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0 - })); - - const deleteResults = deleteBench.tasks.map(task => ({ - name: task.name, - opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0 - })); - - console.log("\nOperations per second (higher is better):"); - console.log("┌─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┐"); - console.log("│ Library │ SET ops/sec │ GET ops/sec │ UPDATE ops/sec │ DELETE ops/sec │"); - console.log("├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤"); - - // Group results by library - const libraries = ["tiny-lru", "lru-cache", "quick-lru", "mnemonist"]; - - libraries.forEach(lib => { - const setResult = setResults.find(r => r.name.includes(lib)); - const getResult = getResults.find(r => r.name.includes(lib)); - const updateResult = updateResults.find(r => r.name.includes(lib)); - const deleteResult = deleteResults.find(r => r.name.includes(lib)); - - if (setResult && getResult && updateResult && deleteResult) { - const nameCol = lib.padEnd(15); - const setCol = setResult.opsPerSec.toLocaleString().padEnd(15); - const getCol = getResult.opsPerSec.toLocaleString().padEnd(15); - const updateCol = updateResult.opsPerSec.toLocaleString().padEnd(15); - const deleteCol = deleteResult.opsPerSec.toLocaleString().padEnd(15); - console.log(`│ ${nameCol} │ ${setCol} │ ${getCol} │ ${updateCol} │ ${deleteCol} │`); - } - }); - - console.log("└─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┘"); - - console.log("\n✅ Benchmark completed!"); - console.log("\nTo regenerate this data, run: npm run benchmark:comparison"); +async function runBenchmarks() { + console.log("🚀 LRU Cache Library Comparison Benchmark\n"); + console.log(`Cache Size: ${CACHE_SIZE} items`); + console.log(`Iterations: ${ITERATIONS.toLocaleString()}`); + console.log(`Node.js: ${process.version}`); + console.log(`Platform: ${process.platform} ${process.arch}\n`); + + const testData = generateTestData(ITERATIONS); + const setPattern = generateAccessPattern(ITERATIONS, testData.keys.length); + const getPattern = generateAccessPattern(ITERATIONS, Math.min(CACHE_SIZE, 500)); + const updatePattern = generateAccessPattern(ITERATIONS, 100); + const deletePattern = generateAccessPattern(ITERATIONS, 50); + + // SET operations benchmark + console.log("📊 SET Operations Benchmark"); + console.log("=".repeat(50)); + + const setBench = new Bench({ time: 2000 }); + + // Dedicated caches and state for SET to avoid measuring setup per-iteration + const setCaches = createCaches(); + const setState = { + "tiny-lru": 0, + "tiny-lru-ttl": 0, + "lru-cache": 0, + "lru-cache-ttl": 0, + "quick-lru": 0, + mnemonist: 0, + }; + + setBench + .add("tiny-lru set", () => { + const cache = setCaches["tiny-lru"]; + let i = setState["tiny-lru"]; // cursor + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState["tiny-lru"] = i; + }) + .add("tiny-lru-ttl set", () => { + const cache = setCaches["tiny-lru-ttl"]; + let i = setState["tiny-lru-ttl"]; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState["tiny-lru-ttl"] = i; + }) + .add("lru-cache set", () => { + const cache = setCaches["lru-cache"]; + let i = setState["lru-cache"]; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState["lru-cache"] = i; + }) + .add("lru-cache-ttl set", () => { + const cache = setCaches["lru-cache-ttl"]; + let i = setState["lru-cache-ttl"]; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState["lru-cache-ttl"] = i; + }) + .add("quick-lru set", () => { + const cache = setCaches["quick-lru"]; + let i = setState["quick-lru"]; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState["quick-lru"] = i; + }) + .add("mnemonist set", () => { + const cache = setCaches.mnemonist; + let i = setState.mnemonist; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = setPattern[i++ % setPattern.length]; + cache.set(testData.keys[idx], testData.values[idx]); + } + setState.mnemonist = i; + }); + + await setBench.run(); + console.table(setBench.table()); + + // GET operations benchmark (with pre-populated caches) + console.log("\n📊 GET Operations Benchmark"); + console.log("=".repeat(50)); + + const caches = createCaches(); + + // Pre-populate all caches deterministically + const prepopulated = Math.min(CACHE_SIZE, 500); + Object.values(caches).forEach((cache) => { + for (let i = 0; i < prepopulated; i++) { + cache.set(testData.keys[i], testData.values[i]); + } + }); + + const getBench = new Bench({ time: 2000 }); + const getState = { idx: 0 }; + + getBench + .add("tiny-lru get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches["tiny-lru"].get(testData.keys[idx]); + } + getState.idx = i; + }) + .add("tiny-lru-ttl get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches["tiny-lru-ttl"].get(testData.keys[idx]); + } + getState.idx = i; + }) + .add("lru-cache get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches["lru-cache"].get(testData.keys[idx]); + } + getState.idx = i; + }) + .add("lru-cache-ttl get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches["lru-cache-ttl"].get(testData.keys[idx]); + } + getState.idx = i; + }) + .add("quick-lru get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches["quick-lru"].get(testData.keys[idx]); + } + getState.idx = i; + }) + .add("mnemonist get", () => { + let i = getState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = getPattern[i++ % getPattern.length]; + caches.mnemonist.get(testData.keys[idx]); + } + getState.idx = i; + }); + + await getBench.run(); + console.table(getBench.table()); + + // DELETE operations benchmark + console.log("\n📊 DELETE Operations Benchmark"); + console.log("=".repeat(50)); + + const deleteBench = new Bench({ time: 2000 }); + + // Dedicated caches and state for DELETE + const deleteCaches = { + "tiny-lru": tinyLru(CACHE_SIZE), + "lru-cache": new LRUCache({ max: CACHE_SIZE }), + "quick-lru": new QuickLRU({ maxSize: CACHE_SIZE }), + mnemonist: new MnemonistLRU(CACHE_SIZE), + }; + const deleteState = { idx: 0 }; + + // Pre-populate + Object.values(deleteCaches).forEach((cache) => { + for (let i = 0; i < 100; i++) { + cache.set(testData.keys[i], testData.values[i]); + } + }); + + deleteBench + .add("tiny-lru delete", () => { + let i = deleteState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = deletePattern[i++ % deletePattern.length]; + deleteCaches["tiny-lru"].delete(testData.keys[idx]); + // Re-add to keep steady state for future deletes + deleteCaches["tiny-lru"].set(testData.keys[idx], testData.values[idx]); + } + deleteState.idx = i; + }) + .add("lru-cache delete", () => { + let i = deleteState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = deletePattern[i++ % deletePattern.length]; + deleteCaches["lru-cache"].delete(testData.keys[idx]); + deleteCaches["lru-cache"].set(testData.keys[idx], testData.values[idx]); + } + deleteState.idx = i; + }) + .add("quick-lru delete", () => { + let i = deleteState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = deletePattern[i++ % deletePattern.length]; + deleteCaches["quick-lru"].delete(testData.keys[idx]); + deleteCaches["quick-lru"].set(testData.keys[idx], testData.values[idx]); + } + deleteState.idx = i; + }) + .add("mnemonist delete", () => { + let i = deleteState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = deletePattern[i++ % deletePattern.length]; + deleteCaches.mnemonist.remove(testData.keys[idx]); + deleteCaches.mnemonist.set(testData.keys[idx], testData.values[idx]); + } + deleteState.idx = i; + }); + + await deleteBench.run(); + console.table(deleteBench.table()); + + // UPDATE operations benchmark + console.log("\n📊 UPDATE Operations Benchmark"); + console.log("=".repeat(50)); + + const updateBench = new Bench({ time: 2000 }); + + // Dedicated caches for UPDATE + const updateCaches = createCaches(); + // Pre-populate with initial values + Object.values(updateCaches).forEach((cache) => { + for (let i = 0; i < 100; i++) { + cache.set(testData.keys[i], testData.values[i]); + } + }); + + const updateState = { idx: 0 }; + + updateBench + .add("tiny-lru update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches["tiny-lru"].set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }) + .add("tiny-lru-ttl update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches["tiny-lru-ttl"].set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }) + .add("lru-cache update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches["lru-cache"].set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }) + .add("lru-cache-ttl update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches["lru-cache-ttl"].set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }) + .add("quick-lru update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches["quick-lru"].set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }) + .add("mnemonist update", () => { + let i = updateState.idx; + for (let j = 0; j < OPS_PER_INVOCATION; j++) { + const idx = updatePattern[i++ % updatePattern.length]; + updateCaches.mnemonist.set( + testData.keys[idx], + testData.values[(idx + 50) % testData.values.length], + ); + } + updateState.idx = i; + }); + + await updateBench.run(); + console.table(updateBench.table()); + + // Memory usage analysis + console.log("\n📊 Memory Usage Analysis"); + console.log("=".repeat(50)); + + const memoryResults = {}; + const testSize = 1000; + + for (const [name, cache] of Object.entries(createCaches())) { + const beforeMem = getMemoryUsage(true); + + // Fill cache + for (let i = 0; i < testSize; i++) { + cache.set(testData.keys[i], testData.values[i]); + } + + const afterMem = getMemoryUsage(true); + const memoryPerItem = calculateMemoryPerItem(beforeMem, afterMem, testSize); + + memoryResults[name] = { + totalMemory: afterMem.heapUsed - beforeMem.heapUsed, + memoryPerItem: memoryPerItem, + bundleSize: bundleSizes[name.split("-")[0]] || "N/A", + }; + } + + console.log("\nMemory Usage Results:"); + console.log("┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐"); + console.log("│ Library │ Bundle Size │ Memory/Item │ Total Memory │"); + console.log("├─────────────────┼─────────────────┼─────────────────┼─────────────────┤"); + + Object.entries(memoryResults).forEach(([name, data]) => { + const nameCol = name.padEnd(15); + const bundleCol = data.bundleSize.padEnd(15); + const memoryCol = `${data.memoryPerItem} bytes`.padEnd(15); + const totalCol = `${Math.round(data.totalMemory / 1024)} KB`.padEnd(15); + console.log(`│ ${nameCol} │ ${bundleCol} │ ${memoryCol} │ ${totalCol} │`); + }); + + console.log("└─────────────────┴─────────────────┴─────────────────┴─────────────────┘"); + + // Performance summary + console.log("\n📊 Performance Summary"); + console.log("=".repeat(50)); + + const setResults = setBench.tasks.map((task) => ({ + name: task.name, + opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0, + })); + + const getResults = getBench.tasks.map((task) => ({ + name: task.name, + opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0, + })); + + const updateResults = updateBench.tasks.map((task) => ({ + name: task.name, + opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0, + })); + + const deleteResults = deleteBench.tasks.map((task) => ({ + name: task.name, + opsPerSec: task.result?.hz ? Math.round(task.result.hz) : 0, + })); + + console.log("\nOperations per second (higher is better):"); + console.log( + "┌─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┐", + ); + console.log( + "│ Library │ SET ops/sec │ GET ops/sec │ UPDATE ops/sec │ DELETE ops/sec │", + ); + console.log( + "├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤", + ); + + // Group results by library + const libraries = ["tiny-lru", "lru-cache", "quick-lru", "mnemonist"]; + + libraries.forEach((lib) => { + const setResult = setResults.find((r) => r.name.includes(lib)); + const getResult = getResults.find((r) => r.name.includes(lib)); + const updateResult = updateResults.find((r) => r.name.includes(lib)); + const deleteResult = deleteResults.find((r) => r.name.includes(lib)); + + if (setResult && getResult && updateResult && deleteResult) { + const nameCol = lib.padEnd(15); + const setCol = setResult.opsPerSec.toLocaleString().padEnd(15); + const getCol = getResult.opsPerSec.toLocaleString().padEnd(15); + const updateCol = updateResult.opsPerSec.toLocaleString().padEnd(15); + const deleteCol = deleteResult.opsPerSec.toLocaleString().padEnd(15); + console.log(`│ ${nameCol} │ ${setCol} │ ${getCol} │ ${updateCol} │ ${deleteCol} │`); + } + }); + + console.log( + "└─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┘", + ); + + console.log("\n✅ Benchmark completed!"); + console.log("\nTo regenerate this data, run: npm run benchmark:comparison"); } // Handle unhandled promise rejections -process.on("unhandledRejection", error => { - console.error("Unhandled promise rejection:", error); - process.exit(1); +process.on("unhandledRejection", (error) => { + console.error("Unhandled promise rejection:", error); + process.exit(1); }); // Run benchmarks diff --git a/benchmarks/modern-benchmark.js b/benchmarks/modern-benchmark.js index ed06560..b3282cd 100644 --- a/benchmarks/modern-benchmark.js +++ b/benchmarks/modern-benchmark.js @@ -5,8 +5,8 @@ import { lru } from "../dist/tiny-lru.js"; const CACHE_SIZES = [100, 1000, 5000]; const WORKLOAD_SIZES = [50, 500, 2500]; // Half of cache size for realistic workloads const ITERATIONS = { - time: 1000, // Run for 1 second minimum - iterations: 100 // Minimum iterations for statistical significance + time: 1000, // Run for 1 second minimum + iterations: 100, // Minimum iterations for statistical significance }; // Utility functions for generating test data @@ -16,32 +16,34 @@ const ITERATIONS = { * @param {number} size - Number of items * @returns {Array<{key:string,value:string}>} */ -function generateRandomData (size) { - const data = new Array(size); - let x = 2463534242; - for (let i = 0; i < size; i++) { - // xorshift32 - x ^= x << 13; x ^= x >>> 17; x ^= x << 5; - const n = (x >>> 0).toString(36); - data[i] = { - key: `key_${i}_${n}`, - value: `value_${i}_${n}${n}` - }; - } - - return data; +function generateRandomData(size) { + const data = Array.from({ length: size }); + let x = 2463534242; + for (let i = 0; i < size; i++) { + // xorshift32 + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + const n = (x >>> 0).toString(36); + data[i] = { + key: `key_${i}_${n}`, + value: `value_${i}_${n}${n}`, + }; + } + + return data; } -function generateSequentialData (size) { - const data = new Array(size); - for (let i = 0; i < size; i++) { - data[i] = { - key: `seq_key_${i}`, - value: `seq_value_${i}` - }; - } +function generateSequentialData(size) { + const data = Array.from({ length: size }); + for (let i = 0; i < size; i++) { + data[i] = { + key: `seq_key_${i}`, + value: `seq_value_${i}`, + }; + } - return data; + return data; } /** @@ -51,343 +53,345 @@ function generateSequentialData (size) { * @param {number} modulo - Upper bound (exclusive) * @returns {Uint32Array} */ -function generateAccessPattern (length, modulo) { - const pattern = new Uint32Array(length); - let x = 123456789; - let y = 362436069; - for (let i = 0; i < length; i++) { - x ^= x << 13; x ^= x >>> 17; x ^= x << 5; - y = y + 1 >>> 0; - pattern[i] = (x + y >>> 0) % modulo; - } - - return pattern; +function generateAccessPattern(length, modulo) { + const pattern = new Uint32Array(length); + let x = 123456789; + let y = 362436069; + for (let i = 0; i < length; i++) { + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + y = (y + 1) >>> 0; + pattern[i] = ((x + y) >>> 0) % modulo; + } + + return pattern; } // Pre-populate cache with data -function prepopulateCache (cache, data, fillRatio = 0.8) { - const fillCount = Math.floor(data.length * fillRatio); - for (let i = 0; i < fillCount; i++) { - cache.set(data[i].key, data[i].value); - } +function prepopulateCache(cache, data, fillRatio = 0.8) { + const fillCount = Math.floor(data.length * fillRatio); + for (let i = 0; i < fillCount; i++) { + cache.set(data[i].key, data[i].value); + } } // Benchmark suites -async function runSetOperationsBenchmarks () { - console.log("\n📝 SET Operations Benchmarks"); - console.log("=" .repeat(50)); - - for (const cacheSize of CACHE_SIZES) { - const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; - const bench = new Bench(ITERATIONS); - - console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); - - // Prepare test data & patterns - const randomData = generateRandomData(workloadSize); - const sequentialData = generateSequentialData(workloadSize); - const randomPattern = generateAccessPattern(10000, workloadSize); - let randomCursor = 0; - - // Test scenarios - bench - .add(`set-random-empty-cache-${cacheSize}`, () => { - const cache = lru(cacheSize); - const idx = randomPattern[randomCursor++ % randomPattern.length]; - const item = randomData[idx]; - cache.set(item.key, item.value); - }) - .add(`set-sequential-empty-cache-${cacheSize}`, () => { - const cache = lru(cacheSize); - const idx = randomPattern[randomCursor++ % randomPattern.length]; - const item = sequentialData[idx]; - cache.set(item.key, item.value); - }) - .add(`set-random-full-cache-${cacheSize}`, () => { - const cache = lru(cacheSize); - prepopulateCache(cache, randomData); - const idx = randomPattern[randomCursor++ % randomPattern.length]; - const item = randomData[idx]; - cache.set(item.key, item.value); - }) - .add(`set-new-items-full-cache-${cacheSize}`, () => { - const cache = lru(cacheSize); - prepopulateCache(cache, randomData); - // Force eviction by adding new items - const idx = randomPattern[randomCursor++ % randomPattern.length]; - cache.set(`new_key_${cacheSize}_${idx}`, `new_value_${idx}`); - }); - - await bench.run(); - console.table(bench.table()); - } +async function runSetOperationsBenchmarks() { + console.log("\n📝 SET Operations Benchmarks"); + console.log("=".repeat(50)); + + for (const cacheSize of CACHE_SIZES) { + const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; + const bench = new Bench(ITERATIONS); + + console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); + + // Prepare test data & patterns + const randomData = generateRandomData(workloadSize); + const sequentialData = generateSequentialData(workloadSize); + const randomPattern = generateAccessPattern(10000, workloadSize); + let randomCursor = 0; + + // Test scenarios + bench + .add(`set-random-empty-cache-${cacheSize}`, () => { + const cache = lru(cacheSize); + const idx = randomPattern[randomCursor++ % randomPattern.length]; + const item = randomData[idx]; + cache.set(item.key, item.value); + }) + .add(`set-sequential-empty-cache-${cacheSize}`, () => { + const cache = lru(cacheSize); + const idx = randomPattern[randomCursor++ % randomPattern.length]; + const item = sequentialData[idx]; + cache.set(item.key, item.value); + }) + .add(`set-random-full-cache-${cacheSize}`, () => { + const cache = lru(cacheSize); + prepopulateCache(cache, randomData); + const idx = randomPattern[randomCursor++ % randomPattern.length]; + const item = randomData[idx]; + cache.set(item.key, item.value); + }) + .add(`set-new-items-full-cache-${cacheSize}`, () => { + const cache = lru(cacheSize); + prepopulateCache(cache, randomData); + // Force eviction by adding new items + const idx = randomPattern[randomCursor++ % randomPattern.length]; + cache.set(`new_key_${cacheSize}_${idx}`, `new_value_${idx}`); + }); + + await bench.run(); + console.table(bench.table()); + } } -async function runGetOperationsBenchmarks () { - console.log("\n🔍 GET Operations Benchmarks"); - console.log("=" .repeat(50)); - - for (const cacheSize of CACHE_SIZES) { - const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; - const bench = new Bench(ITERATIONS); - - console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); - - // Prepare test data and caches - const randomData = generateRandomData(workloadSize); - const sequentialData = generateSequentialData(workloadSize); - - const randomCache = lru(cacheSize); - const sequentialCache = lru(cacheSize); - const mixedCache = lru(cacheSize); - - prepopulateCache(randomCache, randomData); - prepopulateCache(sequentialCache, sequentialData); - prepopulateCache(mixedCache, [...randomData.slice(0, Math.floor(workloadSize / 2)), - ...sequentialData.slice(0, Math.floor(workloadSize / 2))]); - - const hitPattern = generateAccessPattern(20000, Math.floor(workloadSize * 0.8)); - const missPattern = generateAccessPattern(20000, 1 << 30); - let getCursor = 0; - - bench - .add(`get-hit-random-${cacheSize}`, () => { - const idx = hitPattern[getCursor++ % hitPattern.length]; - const item = randomData[idx]; - randomCache.get(item.key); - }) - .add(`get-hit-sequential-${cacheSize}`, () => { - const idx = hitPattern[getCursor++ % hitPattern.length]; - const item = sequentialData[idx]; - sequentialCache.get(item.key); - }) - .add(`get-miss-${cacheSize}`, () => { - const idx = missPattern[getCursor++ % missPattern.length]; - randomCache.get(`nonexistent_key_${idx}`); - }) - .add(`get-mixed-pattern-${cacheSize}`, () => { - const choose = hitPattern[getCursor++ % hitPattern.length] % 10; // 0..9 - if (choose < 8) { - const idx = hitPattern[getCursor++ % hitPattern.length]; - const item = randomData[idx]; - mixedCache.get(item.key); - } else { - const idx = missPattern[getCursor++ % missPattern.length]; - mixedCache.get(`miss_key_${idx}`); - } - }); - - await bench.run(); - console.table(bench.table()); - } +async function runGetOperationsBenchmarks() { + console.log("\n🔍 GET Operations Benchmarks"); + console.log("=".repeat(50)); + + for (const cacheSize of CACHE_SIZES) { + const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; + const bench = new Bench(ITERATIONS); + + console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); + + // Prepare test data and caches + const randomData = generateRandomData(workloadSize); + const sequentialData = generateSequentialData(workloadSize); + + const randomCache = lru(cacheSize); + const sequentialCache = lru(cacheSize); + const mixedCache = lru(cacheSize); + + prepopulateCache(randomCache, randomData); + prepopulateCache(sequentialCache, sequentialData); + prepopulateCache(mixedCache, [ + ...randomData.slice(0, Math.floor(workloadSize / 2)), + ...sequentialData.slice(0, Math.floor(workloadSize / 2)), + ]); + + const hitPattern = generateAccessPattern(20000, Math.floor(workloadSize * 0.8)); + const missPattern = generateAccessPattern(20000, 1 << 30); + let getCursor = 0; + + bench + .add(`get-hit-random-${cacheSize}`, () => { + const idx = hitPattern[getCursor++ % hitPattern.length]; + const item = randomData[idx]; + randomCache.get(item.key); + }) + .add(`get-hit-sequential-${cacheSize}`, () => { + const idx = hitPattern[getCursor++ % hitPattern.length]; + const item = sequentialData[idx]; + sequentialCache.get(item.key); + }) + .add(`get-miss-${cacheSize}`, () => { + const idx = missPattern[getCursor++ % missPattern.length]; + randomCache.get(`nonexistent_key_${idx}`); + }) + .add(`get-mixed-pattern-${cacheSize}`, () => { + const choose = hitPattern[getCursor++ % hitPattern.length] % 10; // 0..9 + if (choose < 8) { + const idx = hitPattern[getCursor++ % hitPattern.length]; + const item = randomData[idx]; + mixedCache.get(item.key); + } else { + const idx = missPattern[getCursor++ % missPattern.length]; + mixedCache.get(`miss_key_${idx}`); + } + }); + + await bench.run(); + console.table(bench.table()); + } } -async function runMixedOperationsBenchmarks () { - console.log("\n🔄 Mixed Operations Benchmarks (Real-world scenarios)"); - console.log("=" .repeat(60)); - - for (const cacheSize of CACHE_SIZES) { - const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; - const bench = new Bench(ITERATIONS); - - console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); - - const testData = generateRandomData(workloadSize * 2); // More data than cache - const choosePattern = generateAccessPattern(50000, 10); - const idxPattern = generateAccessPattern(50000, testData.length); - let mixedCursor = 0; - - bench - .add(`real-world-80-20-read-write-${cacheSize}`, () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData, 0.5); - // Simulate 80% reads, 20% writes - for (let i = 0; i < 10; i++) { - const choose = choosePattern[mixedCursor++ % choosePattern.length]; - if (choose < 8) { - const item = testData[idxPattern[mixedCursor++ % idxPattern.length] % workloadSize]; - cache.get(item.key); - } else { - const item = testData[idxPattern[mixedCursor++ % idxPattern.length]]; - cache.set(item.key, item.value); - } - } - }) - .add(`cache-warming-${cacheSize}`, () => { - const cache = lru(cacheSize); - - // Simulate cache warming - sequential fills - for (let i = 0; i < Math.min(cacheSize, workloadSize); i++) { - cache.set(testData[i].key, testData[i].value); - } - }) - .add(`high-churn-${cacheSize}`, () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData, 1.0); // Fill cache completely - - // High churn - constantly adding new items - for (let i = 0; i < 5; i++) { - const idx = idxPattern[mixedCursor++ % idxPattern.length]; - cache.set(`churn_${cacheSize}_${i}_${idx}`, `value_${i}`); - } - }) - .add(`lru-access-pattern-${cacheSize}`, () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData, 1.0); - - // Access patterns that test LRU behavior - const hotKeys = testData.slice(0, 3); - cache.get(hotKeys[0].key); - cache.get(hotKeys[1].key); - cache.get(hotKeys[2].key); - cache.get(hotKeys[0].key); - cache.get(hotKeys[1].key); - cache.get(hotKeys[2].key); - }); - - await bench.run(); - console.table(bench.table()); - } +async function runMixedOperationsBenchmarks() { + console.log("\n🔄 Mixed Operations Benchmarks (Real-world scenarios)"); + console.log("=".repeat(60)); + + for (const cacheSize of CACHE_SIZES) { + const workloadSize = WORKLOAD_SIZES[CACHE_SIZES.indexOf(cacheSize)]; + const bench = new Bench(ITERATIONS); + + console.log(`\nCache Size: ${cacheSize}, Workload: ${workloadSize}`); + + const testData = generateRandomData(workloadSize * 2); // More data than cache + const choosePattern = generateAccessPattern(50000, 10); + const idxPattern = generateAccessPattern(50000, testData.length); + let mixedCursor = 0; + + bench + .add(`real-world-80-20-read-write-${cacheSize}`, () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData, 0.5); + // Simulate 80% reads, 20% writes + for (let i = 0; i < 10; i++) { + const choose = choosePattern[mixedCursor++ % choosePattern.length]; + if (choose < 8) { + const item = testData[idxPattern[mixedCursor++ % idxPattern.length] % workloadSize]; + cache.get(item.key); + } else { + const item = testData[idxPattern[mixedCursor++ % idxPattern.length]]; + cache.set(item.key, item.value); + } + } + }) + .add(`cache-warming-${cacheSize}`, () => { + const cache = lru(cacheSize); + + // Simulate cache warming - sequential fills + for (let i = 0; i < Math.min(cacheSize, workloadSize); i++) { + cache.set(testData[i].key, testData[i].value); + } + }) + .add(`high-churn-${cacheSize}`, () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData, 1.0); // Fill cache completely + + // High churn - constantly adding new items + for (let i = 0; i < 5; i++) { + const idx = idxPattern[mixedCursor++ % idxPattern.length]; + cache.set(`churn_${cacheSize}_${i}_${idx}`, `value_${i}`); + } + }) + .add(`lru-access-pattern-${cacheSize}`, () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData, 1.0); + + // Access patterns that test LRU behavior + const hotKeys = testData.slice(0, 3); + cache.get(hotKeys[0].key); + cache.get(hotKeys[1].key); + cache.get(hotKeys[2].key); + cache.get(hotKeys[0].key); + cache.get(hotKeys[1].key); + cache.get(hotKeys[2].key); + }); + + await bench.run(); + console.table(bench.table()); + } } -async function runSpecialOperationsBenchmarks () { - console.log("\n⚙️ Special Operations Benchmarks"); - console.log("=" .repeat(50)); - - const cacheSize = 1000; - const workloadSize = 500; - const bench = new Bench(ITERATIONS); - - const testData = generateRandomData(workloadSize); - const hitPattern = generateAccessPattern(20000, Math.floor(workloadSize * 0.8)); - let cursor = 0; - - // Test cache with different data types - const numberData = Array.from({length: workloadSize}, (_, i) => ({key: i, value: i * 2})); - const objectData = Array.from({length: workloadSize}, (_, i) => ({ - key: `obj_${i}`, - value: {id: i, data: `object_data_${i}`, nested: {prop: i}} - })); - - bench - .add("cache-clear", () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData); - cache.clear(); - }) - .add("cache-delete", () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData); - const item = testData[hitPattern[cursor++ % hitPattern.length]]; - cache.delete(item.key); - }) - .add("number-keys-values", () => { - const cache = lru(cacheSize); - const item = numberData[Math.floor(Math.random() * numberData.length)]; - cache.set(item.key, item.value); - cache.get(item.key); - }) - .add("object-values", () => { - const cache = lru(cacheSize); - const item = objectData[Math.floor(Math.random() * objectData.length)]; - cache.set(item.key, item.value); - cache.get(item.key); - }) - .add("has-operation", () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData); - const item = testData[hitPattern[cursor++ % hitPattern.length]]; - cache.has(item.key); - }) - .add("size-property", () => { - const cache = lru(cacheSize); - prepopulateCache(cache, testData); - // Access size property - - return cache.size; - }); - - await bench.run(); - console.table(bench.table()); +async function runSpecialOperationsBenchmarks() { + console.log("\n⚙️ Special Operations Benchmarks"); + console.log("=".repeat(50)); + + const cacheSize = 1000; + const workloadSize = 500; + const bench = new Bench(ITERATIONS); + + const testData = generateRandomData(workloadSize); + const hitPattern = generateAccessPattern(20000, Math.floor(workloadSize * 0.8)); + let cursor = 0; + + // Test cache with different data types + const numberData = Array.from({ length: workloadSize }, (_, i) => ({ key: i, value: i * 2 })); + const objectData = Array.from({ length: workloadSize }, (_, i) => ({ + key: `obj_${i}`, + value: { id: i, data: `object_data_${i}`, nested: { prop: i } }, + })); + + bench + .add("cache-clear", () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData); + cache.clear(); + }) + .add("cache-delete", () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData); + const item = testData[hitPattern[cursor++ % hitPattern.length]]; + cache.delete(item.key); + }) + .add("number-keys-values", () => { + const cache = lru(cacheSize); + const item = numberData[Math.floor(Math.random() * numberData.length)]; + cache.set(item.key, item.value); + cache.get(item.key); + }) + .add("object-values", () => { + const cache = lru(cacheSize); + const item = objectData[Math.floor(Math.random() * objectData.length)]; + cache.set(item.key, item.value); + cache.get(item.key); + }) + .add("has-operation", () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData); + const item = testData[hitPattern[cursor++ % hitPattern.length]]; + cache.has(item.key); + }) + .add("size-property", () => { + const cache = lru(cacheSize); + prepopulateCache(cache, testData); + // Access size property + + return cache.size; + }); + + await bench.run(); + console.table(bench.table()); } // Memory usage benchmarks -async function runMemoryBenchmarks () { - console.log("\n🧠 Memory Usage Analysis"); - console.log("=" .repeat(40)); +async function runMemoryBenchmarks() { + console.log("\n🧠 Memory Usage Analysis"); + console.log("=".repeat(40)); - const testSizes = [100, 1000, 10000]; + const testSizes = [100, 1000, 10000]; - for (const size of testSizes) { - console.log(`\nAnalyzing memory usage for cache size: ${size}`); + for (const size of testSizes) { + console.log(`\nAnalyzing memory usage for cache size: ${size}`); - const cache = lru(size); - const testData = generateRandomData(size); + const cache = lru(size); + const testData = generateRandomData(size); - // Memory before - if (global.gc) { - global.gc(); - } - const memBefore = process.memoryUsage(); + // Memory before + if (global.gc) { + global.gc(); + } + const memBefore = process.memoryUsage(); - // Fill cache - testData.forEach(item => cache.set(item.key, item.value)); + // Fill cache + testData.forEach((item) => cache.set(item.key, item.value)); - // Memory after - if (global.gc) { - global.gc(); - } - const memAfter = process.memoryUsage(); + // Memory after + if (global.gc) { + global.gc(); + } + const memAfter = process.memoryUsage(); - const heapUsed = memAfter.heapUsed - memBefore.heapUsed; - const perItem = heapUsed / size; + const heapUsed = memAfter.heapUsed - memBefore.heapUsed; + const perItem = heapUsed / size; - console.log(`Heap used: ${(heapUsed / 1024 / 1024).toFixed(2)} MB`); - console.log(`Per item: ${perItem.toFixed(2)} bytes`); - console.log(`Cache size: ${cache.size}`); - } + console.log(`Heap used: ${(heapUsed / 1024 / 1024).toFixed(2)} MB`); + console.log(`Per item: ${perItem.toFixed(2)} bytes`); + console.log(`Cache size: ${cache.size}`); + } } // Main execution -async function runAllBenchmarks () { - console.log("🚀 Tiny-LRU Modern Benchmark Suite"); - console.log("=================================="); - console.log(`Node.js version: ${process.version}`); - console.log(`Platform: ${process.platform} ${process.arch}`); - console.log(`Date: ${new Date().toISOString()}`); - - try { - await runSetOperationsBenchmarks(); - await runGetOperationsBenchmarks(); - await runMixedOperationsBenchmarks(); - await runSpecialOperationsBenchmarks(); - await runMemoryBenchmarks(); - - console.log("\n✅ All benchmarks completed successfully!"); - console.log("\n📊 Summary:"); - console.log("- SET operations: Tests cache population under various conditions"); - console.log("- GET operations: Tests cache retrieval with different hit/miss patterns"); - console.log("- Mixed operations: Simulates real-world usage scenarios"); - console.log("- Special operations: Tests additional cache methods and edge cases"); - console.log("- Memory analysis: Shows memory consumption patterns"); - - } catch (error) { - console.error("❌ Benchmark failed:", error); - process.exit(1); - } +async function runAllBenchmarks() { + console.log("🚀 Tiny-LRU Modern Benchmark Suite"); + console.log("=================================="); + console.log(`Node.js version: ${process.version}`); + console.log(`Platform: ${process.platform} ${process.arch}`); + console.log(`Date: ${new Date().toISOString()}`); + + try { + await runSetOperationsBenchmarks(); + await runGetOperationsBenchmarks(); + await runMixedOperationsBenchmarks(); + await runSpecialOperationsBenchmarks(); + await runMemoryBenchmarks(); + + console.log("\n✅ All benchmarks completed successfully!"); + console.log("\n📊 Summary:"); + console.log("- SET operations: Tests cache population under various conditions"); + console.log("- GET operations: Tests cache retrieval with different hit/miss patterns"); + console.log("- Mixed operations: Simulates real-world usage scenarios"); + console.log("- Special operations: Tests additional cache methods and edge cases"); + console.log("- Memory analysis: Shows memory consumption patterns"); + } catch (error) { + console.error("❌ Benchmark failed:", error); + process.exit(1); + } } // Allow running this file directly if (import.meta.url === `file://${process.argv[1]}`) { - runAllBenchmarks(); + runAllBenchmarks(); } export { - runAllBenchmarks, - runSetOperationsBenchmarks, - runGetOperationsBenchmarks, - runMixedOperationsBenchmarks, - runSpecialOperationsBenchmarks, - runMemoryBenchmarks + runAllBenchmarks, + runSetOperationsBenchmarks, + runGetOperationsBenchmarks, + runMixedOperationsBenchmarks, + runSpecialOperationsBenchmarks, + runMemoryBenchmarks, }; - diff --git a/benchmarks/performance-observer-benchmark.js b/benchmarks/performance-observer-benchmark.js index 9929990..a0a1402 100644 --- a/benchmarks/performance-observer-benchmark.js +++ b/benchmarks/performance-observer-benchmark.js @@ -3,282 +3,328 @@ import { lru } from "../dist/tiny-lru.js"; // Custom high-resolution timer benchmark (alternative approach) class CustomTimer { - constructor () { - this.results = new Map(); - } - - async timeFunction (name, fn, iterations = 1000) { - const times = []; - - // Warmup - for (let i = 0; i < Math.min(100, iterations / 10); i++) { - await fn(); - } - - // Actual measurement - for (let i = 0; i < iterations; i++) { - const start = performance.now(); - await fn(); - const end = performance.now(); - times.push(end - start); - } - - // Calculate statistics - const totalTime = times.reduce((a, b) => a + b, 0); - const avgTime = totalTime / iterations; - const minTime = Math.min(...times); - const maxTime = Math.max(...times); - - const sorted = [...times].sort((a, b) => a - b); - const median = sorted[Math.floor(sorted.length / 2)]; - - const variance = times.reduce((acc, time) => acc + Math.pow(time - avgTime, 2), 0) / iterations; - const stdDev = Math.sqrt(variance); - - this.results.set(name, { - name, - iterations, - avgTime, - minTime, - maxTime, - median, - stdDev, - opsPerSec: 1000 / avgTime // Convert ms to ops/sec - }); - } - - printResults () { - console.log("\n⏱️ Performance Results"); - console.log("========================"); - - const results = Array.from(this.results.values()); - console.table(results.map(r => ({ - "Operation": r.name, - "Iterations": r.iterations, - "Avg (ms)": r.avgTime.toFixed(6), - "Min (ms)": r.minTime.toFixed(6), - "Max (ms)": r.maxTime.toFixed(6), - "Median (ms)": r.median.toFixed(6), - "Std Dev": r.stdDev.toFixed(6), - "Ops/sec": Math.round(r.opsPerSec) - }))); - } + constructor() { + this.results = new Map(); + } + + async timeFunction(name, fn, iterations = 1000) { + const times = []; + + // Warmup + for (let i = 0; i < Math.min(100, iterations / 10); i++) { + await fn(); + } + + // Actual measurement + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + await fn(); + const end = performance.now(); + times.push(end - start); + } + + // Calculate statistics + const totalTime = times.reduce((a, b) => a + b, 0); + const avgTime = totalTime / iterations; + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + + const sorted = [...times].sort((a, b) => a - b); + const median = sorted[Math.floor(sorted.length / 2)]; + + const variance = times.reduce((acc, time) => acc + Math.pow(time - avgTime, 2), 0) / iterations; + const stdDev = Math.sqrt(variance); + + this.results.set(name, { + name, + iterations, + avgTime, + minTime, + maxTime, + median, + stdDev, + opsPerSec: 1000 / avgTime, // Convert ms to ops/sec + }); + } + + printResults() { + console.log("\n⏱️ Performance Results"); + console.log("========================"); + + const results = Array.from(this.results.values()); + console.table( + results.map((r) => ({ + Operation: r.name, + Iterations: r.iterations, + "Avg (ms)": r.avgTime.toFixed(6), + "Min (ms)": r.minTime.toFixed(6), + "Max (ms)": r.maxTime.toFixed(6), + "Median (ms)": r.median.toFixed(6), + "Std Dev": r.stdDev.toFixed(6), + "Ops/sec": Math.round(r.opsPerSec), + })), + ); + } } // Test data generation -function generateTestData (size) { - const out = new Array(size); - for (let i = 0; i < size; i++) { - out[i] = { - key: `key_${i}`, - value: `value_${i}_${"x".repeat(50)}` - }; - } - - return out; +function generateTestData(size) { + const out = Array.from({ length: size }); + for (let i = 0; i < size; i++) { + out[i] = { + key: `key_${i}`, + value: `value_${i}_${"x".repeat(50)}`, + }; + } + + return out; } -async function runPerformanceBenchmarks () { - console.log("🔬 LRU Performance Benchmarks"); - console.log("=============================="); - console.log("(Using CustomTimer for high-resolution function timing)"); - - const timer = new CustomTimer(); - const cacheSize = 1000; - const iterations = 10000; - const testData = generateTestData(cacheSize * 2); - - console.log("Running operations..."); - - // Phase 1: Fill cache with initial data - console.log("Phase 1: Initial cache population"); - const phase1Cache = lru(cacheSize); - let phase1Index = 0; - await timer.timeFunction("lru.set (initial population)", () => { - const i = phase1Index % cacheSize; - phase1Cache.set(testData[i].key, testData[i].value); - phase1Index++; - }, iterations); - - // Phase 2: Mixed read/write operations - console.log("Phase 2: Mixed operations"); - const phase2Cache = lru(cacheSize); - // Pre-populate for realistic workload - for (let i = 0; i < cacheSize; i++) { - phase2Cache.set(testData[i].key, testData[i].value); - } - - // Deterministic mixed workload that exercises the entire cache without conditionals - const getIndices = new Uint32Array(iterations); - const setIndices = new Uint32Array(iterations); - const hasIndices = new Uint32Array(iterations); - const deleteIndices = new Uint32Array(iterations); - - for (let i = 0; i < iterations; i++) { - const idx = i % cacheSize; - getIndices[i] = idx; - setIndices[i] = idx; - hasIndices[i] = idx; - deleteIndices[i] = idx; - } - - let mixedGetIndex = 0; - await timer.timeFunction("lru.get", () => { - const idx = getIndices[mixedGetIndex % iterations]; - phase2Cache.get(testData[idx].key); - mixedGetIndex++; - }, iterations); - - let mixedSetIndex = 0; - await timer.timeFunction("lru.set", () => { - const idx = setIndices[mixedSetIndex % iterations]; - phase2Cache.set(testData[idx].key, testData[idx].value); - mixedSetIndex++; - }, iterations); - - let mixedHasIndex = 0; - await timer.timeFunction("lru.has", () => { - const idx = hasIndices[mixedHasIndex % iterations]; - phase2Cache.has(testData[idx].key); - mixedHasIndex++; - }, iterations); - - // keys() - await timer.timeFunction("lru.keys", () => { - phase2Cache.keys(); - }, iterations); - - // values() - await timer.timeFunction("lru.values", () => { - phase2Cache.values(); - }, iterations); - - // entries() - await timer.timeFunction("lru.entries", () => { - phase2Cache.entries(); - }, iterations); - - let mixedDeleteIndex = 0; - await timer.timeFunction("lru.delete", () => { - const idx = deleteIndices[mixedDeleteIndex % iterations]; - phase2Cache.delete(testData[idx].key); - mixedDeleteIndex++; - }, iterations); - - // Phase 3: Cache eviction stress test - console.log("Phase 3: Cache eviction stress test"); - const phase3Cache = lru(2); - let phase3Index = 1; - phase3Cache.set(`evict_key_${phase3Index}`, `evict__value_${phase3Index++}`); - await timer.timeFunction("lru.set (eviction stress)", () => { - phase3Cache.set(`evict_key_${phase3Index}`, `evict_value_${phase3Index++}`); - }, iterations); - - // Phase 4: Some clear operations - console.log("Phase 4: Clear operations"); - const phase4Cache = lru(1); - await timer.timeFunction("lru.clear", () => { - phase4Cache.set("temp_1", "temp_value_1"); - phase4Cache.clear(); - }, iterations); - - // Phase 5: Additional API method benchmarks - console.log("Phase 5: Additional API method benchmarks"); - - // setWithEvicted() - const setWithEvictedCache = lru(2); - setWithEvictedCache.set("a", "value_a"); - setWithEvictedCache.set("b", "value_b"); - let setWithEvictedIndex = 0; - await timer.timeFunction("lru.setWithEvicted", () => { - const key = `extra_key_${setWithEvictedIndex}`; - const value = `extra_value_${setWithEvictedIndex}`; - setWithEvictedCache.setWithEvicted(key, value); - setWithEvictedIndex++; - }, iterations); - - // expiresAt() - const expiresCache = lru(cacheSize, 6e4); - const expiresKey = "expires_key"; - expiresCache.set(expiresKey, "expires_value"); - await timer.timeFunction("lru.expiresAt", () => { - expiresCache.expiresAt(expiresKey); - }, iterations); - - timer.printResults(); +async function runPerformanceBenchmarks() { + console.log("🔬 LRU Performance Benchmarks"); + console.log("=============================="); + console.log("(Using CustomTimer for high-resolution function timing)"); + + const timer = new CustomTimer(); + const cacheSize = 1000; + const iterations = 10000; + const testData = generateTestData(cacheSize * 2); + + console.log("Running operations..."); + + // Phase 1: Fill cache with initial data + console.log("Phase 1: Initial cache population"); + const phase1Cache = lru(cacheSize); + let phase1Index = 0; + await timer.timeFunction( + "lru.set (initial population)", + () => { + const i = phase1Index % cacheSize; + phase1Cache.set(testData[i].key, testData[i].value); + phase1Index++; + }, + iterations, + ); + + // Phase 2: Mixed read/write operations + console.log("Phase 2: Mixed operations"); + const phase2Cache = lru(cacheSize); + // Pre-populate for realistic workload + for (let i = 0; i < cacheSize; i++) { + phase2Cache.set(testData[i].key, testData[i].value); + } + + // Deterministic mixed workload that exercises the entire cache without conditionals + const getIndices = new Uint32Array(iterations); + const setIndices = new Uint32Array(iterations); + const hasIndices = new Uint32Array(iterations); + const deleteIndices = new Uint32Array(iterations); + + for (let i = 0; i < iterations; i++) { + const idx = i % cacheSize; + getIndices[i] = idx; + setIndices[i] = idx; + hasIndices[i] = idx; + deleteIndices[i] = idx; + } + + let mixedGetIndex = 0; + await timer.timeFunction( + "lru.get", + () => { + const idx = getIndices[mixedGetIndex % iterations]; + phase2Cache.get(testData[idx].key); + mixedGetIndex++; + }, + iterations, + ); + + let mixedSetIndex = 0; + await timer.timeFunction( + "lru.set", + () => { + const idx = setIndices[mixedSetIndex % iterations]; + phase2Cache.set(testData[idx].key, testData[idx].value); + mixedSetIndex++; + }, + iterations, + ); + + let mixedHasIndex = 0; + await timer.timeFunction( + "lru.has", + () => { + const idx = hasIndices[mixedHasIndex % iterations]; + phase2Cache.has(testData[idx].key); + mixedHasIndex++; + }, + iterations, + ); + + // keys() + await timer.timeFunction( + "lru.keys", + () => { + phase2Cache.keys(); + }, + iterations, + ); + + // values() + await timer.timeFunction( + "lru.values", + () => { + phase2Cache.values(); + }, + iterations, + ); + + // entries() + await timer.timeFunction( + "lru.entries", + () => { + phase2Cache.entries(); + }, + iterations, + ); + + let mixedDeleteIndex = 0; + await timer.timeFunction( + "lru.delete", + () => { + const idx = deleteIndices[mixedDeleteIndex % iterations]; + phase2Cache.delete(testData[idx].key); + mixedDeleteIndex++; + }, + iterations, + ); + + // Phase 3: Cache eviction stress test + console.log("Phase 3: Cache eviction stress test"); + const phase3Cache = lru(2); + let phase3Index = 1; + phase3Cache.set(`evict_key_${phase3Index}`, `evict__value_${phase3Index++}`); + await timer.timeFunction( + "lru.set (eviction stress)", + () => { + phase3Cache.set(`evict_key_${phase3Index}`, `evict_value_${phase3Index++}`); + }, + iterations, + ); + + // Phase 4: Some clear operations + console.log("Phase 4: Clear operations"); + const phase4Cache = lru(1); + await timer.timeFunction( + "lru.clear", + () => { + phase4Cache.set("temp_1", "temp_value_1"); + phase4Cache.clear(); + }, + iterations, + ); + + // Phase 5: Additional API method benchmarks + console.log("Phase 5: Additional API method benchmarks"); + + // setWithEvicted() + const setWithEvictedCache = lru(2); + setWithEvictedCache.set("a", "value_a"); + setWithEvictedCache.set("b", "value_b"); + let setWithEvictedIndex = 0; + await timer.timeFunction( + "lru.setWithEvicted", + () => { + const key = `extra_key_${setWithEvictedIndex}`; + const value = `extra_value_${setWithEvictedIndex}`; + setWithEvictedCache.setWithEvicted(key, value); + setWithEvictedIndex++; + }, + iterations, + ); + + // expiresAt() + const expiresCache = lru(cacheSize, 6e4); + const expiresKey = "expires_key"; + expiresCache.set(expiresKey, "expires_value"); + await timer.timeFunction( + "lru.expiresAt", + () => { + expiresCache.expiresAt(expiresKey); + }, + iterations, + ); + + timer.printResults(); } // Comparison with different cache sizes -async function runScalabilityTest () { - console.log("\n📈 Scalability Test"); - console.log("==================="); - - const sizes = [100, 500, 1000, 5000, 10000]; - const results = []; - - for (const size of sizes) { - console.log(`Testing cache size: ${size}`); - const testData = generateTestData(size); - - // Test set performance - const cache = lru(size); - const setStart = performance.now(); - testData.forEach(item => cache.set(item.key, item.value)); - const setEnd = performance.now(); - const setTime = setEnd - setStart; - - // Test get performance - const getStart = performance.now(); - for (let i = 0; i < 1000; i++) { - const item = testData[Math.floor(Math.random() * testData.length)]; - cache.get(item.key); - } - const getEnd = performance.now(); - const getTime = getEnd - getStart; - - results.push({ - "Size": size, - "Set Total (ms)": setTime.toFixed(2), - "Set Per Item (ms)": (setTime / size).toFixed(4), - "Get 1K Items (ms)": getTime.toFixed(2), - "Get Per Item (ms)": (getTime / 1000).toFixed(4) - }); - } - - console.table(results); +async function runScalabilityTest() { + console.log("\n📈 Scalability Test"); + console.log("==================="); + + const sizes = [100, 500, 1000, 5000, 10000]; + const results = []; + + for (const size of sizes) { + console.log(`Testing cache size: ${size}`); + const testData = generateTestData(size); + + // Test set performance + const cache = lru(size); + const setStart = performance.now(); + testData.forEach((item) => cache.set(item.key, item.value)); + const setEnd = performance.now(); + const setTime = setEnd - setStart; + + // Test get performance + const getStart = performance.now(); + for (let i = 0; i < 1000; i++) { + const item = testData[Math.floor(Math.random() * testData.length)]; + cache.get(item.key); + } + const getEnd = performance.now(); + const getTime = getEnd - getStart; + + results.push({ + Size: size, + "Set Total (ms)": setTime.toFixed(2), + "Set Per Item (ms)": (setTime / size).toFixed(4), + "Get 1K Items (ms)": getTime.toFixed(2), + "Get Per Item (ms)": (getTime / 1000).toFixed(4), + }); + } + + console.table(results); } // Main execution -async function runAllPerformanceTests () { - console.log("🔬 Node.js Performance API Benchmarks"); - console.log("======================================"); - console.log(`Node.js version: ${process.version}`); - console.log(`Platform: ${process.platform} ${process.arch}`); - console.log(`Date: ${new Date().toISOString()}`); - - try { - await runPerformanceBenchmarks(); - await runScalabilityTest(); - - console.log("\n✅ Performance tests completed!"); - console.log("\n📋 Notes:"); - console.log("- Benchmarks: High-resolution timing with statistical analysis using CustomTimer (based on performance.now())"); - console.log("- Scalability Test: Shows how performance scales with cache size"); - - } catch (error) { - console.error("❌ Performance test failed:", error); - process.exit(1); - } +async function runAllPerformanceTests() { + console.log("🔬 Node.js Performance API Benchmarks"); + console.log("======================================"); + console.log(`Node.js version: ${process.version}`); + console.log(`Platform: ${process.platform} ${process.arch}`); + console.log(`Date: ${new Date().toISOString()}`); + + try { + await runPerformanceBenchmarks(); + await runScalabilityTest(); + + console.log("\n✅ Performance tests completed!"); + console.log("\n📋 Notes:"); + console.log( + "- Benchmarks: High-resolution timing with statistical analysis using CustomTimer (based on performance.now())", + ); + console.log("- Scalability Test: Shows how performance scales with cache size"); + } catch (error) { + console.error("❌ Performance test failed:", error); + process.exit(1); + } } // Allow running this file directly if (import.meta.url === `file://${process.argv[1]}`) { - runAllPerformanceTests(); + runAllPerformanceTests(); } -export { - runAllPerformanceTests, - runPerformanceBenchmarks, - runScalabilityTest, - CustomTimer -}; +export { runAllPerformanceTests, runPerformanceBenchmarks, runScalabilityTest, CustomTimer }; diff --git a/coverage.txt b/coverage.txt new file mode 100644 index 0000000..a965e38 --- /dev/null +++ b/coverage.txt @@ -0,0 +1,10 @@ +ℹ start of coverage report +ℹ ---------------------------------------------------------- +ℹ file | line % | branch % | funcs % | uncovered lines +ℹ ---------------------------------------------------------- +ℹ src | | | | +ℹ lru.js | 100.00 | 95.00 | 100.00 | +ℹ ---------------------------------------------------------- +ℹ all files | 100.00 | 95.00 | 100.00 | +ℹ ---------------------------------------------------------- +ℹ end of coverage report diff --git a/dist/tiny-lru.cjs b/dist/tiny-lru.cjs index 7baf605..67d0824 100644 --- a/dist/tiny-lru.cjs +++ b/dist/tiny-lru.cjs @@ -26,432 +26,443 @@ * // After 5 seconds, key1 will be expired */ 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 accessing existing items via get(). - * @example - * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access - * @see {@link lru} For parameter validation - * @since 1.0.0 - */ - constructor (max = 0, ttl = 0, resetTtl = false) { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.max = max; - this.resetTtl = resetTtl; - this.size = 0; - this.ttl = ttl; - } - - /** - * Removes all items from the cache. - * - * @method clear - * @memberof LRU - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.clear(); - * console.log(cache.size); // 0 - * @since 1.0.0 - */ - clear () { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.size = 0; - - return this; - } - - /** - * Removes an item from the cache by key. - * - * @method delete - * @memberof LRU - * @param {string} key - The key of the item to delete. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1'); - * cache.delete('key1'); - * console.log(cache.has('key1')); // false - * @see {@link LRU#has} - * @see {@link LRU#clear} - * @since 1.0.0 - */ - delete (key) { - if (this.has(key)) { - const item = this.items[key]; - - delete this.items[key]; - this.size--; - - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - if (this.first === item) { - this.first = item.next; - } - - if (this.last === item) { - this.last = item.prev; - } - } - - return this; - } - - /** - * Returns an array of [key, value] pairs for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method entries - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. - * @returns {Array>} Array of [key, value] pairs in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.entries()); // [['a', 1], ['b', 2]] - * console.log(cache.entries(['a'])); // [['a', 1]] - * @see {@link LRU#keys} - * @see {@link LRU#values} - * @since 11.1.0 - */ - entries (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - result[i] = [key, this.get(key)]; - } - - return result; - } - - /** - * Removes the least recently used item from the cache. - * - * @method evict - * @memberof LRU - * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('old', 'value').set('new', 'value'); - * cache.evict(); // Removes 'old' item - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - evict (bypass = false) { - if (bypass || this.size > 0) { - const item = this.first; - - delete this.items[item.key]; - - if (--this.size === 0) { - this.first = null; - this.last = null; - } else { - this.first = item.next; - this.first.prev = null; - } - } - - return this; - } - - /** - * Returns the expiration timestamp for a given key. - * - * @method expiresAt - * @memberof LRU - * @param {string} key - The key to check expiration for. - * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. - * @example - * const cache = new LRU(100, 5000); // 5 second TTL - * cache.set('key1', 'value1'); - * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now - * @see {@link LRU#get} - * @see {@link LRU#has} - * @since 1.0.0 - */ - expiresAt (key) { - let result; - - if (this.has(key)) { - result = this.items[key].expiry; - } - - return result; - } - - /** - * Retrieves a value from the cache by key. Updates the item's position to most recently used. - * - * @method get - * @memberof LRU - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found or expired. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.get('key1')); // 'value1' - * console.log(cache.get('nonexistent')); // undefined - * @see {@link LRU#set} - * @see {@link LRU#has} - * @since 1.0.0 - */ - get (key) { - const item = this.items[key]; - - if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - - return undefined; - } - } - - // Fast LRU update without full set() overhead - this.moveToEnd(item); - - return item.value; - } - - return undefined; - } - - /** - * Checks if a key exists in the cache. - * - * @method has - * @memberof LRU - * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.has('key1')); // true - * console.log(cache.has('nonexistent')); // false - * @see {@link LRU#get} - * @see {@link LRU#delete} - * @since 9.0.0 - */ - has (key) { - return key in this.items; - } - - /** - * Efficiently moves an item to the end of the LRU list (most recently used position). - * This is an internal optimization method that avoids the overhead of the full set() operation - * when only LRU position needs to be updated. - * - * @method moveToEnd - * @memberof LRU - * @param {Object} item - The cache item with prev/next pointers to reposition. - * @private - * @since 11.3.5 - */ - moveToEnd (item) { - // If already at the end, nothing to do - if (this.last === item) { - return; - } - - // Remove item from current position in the list - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - // Update first pointer if this was the first item - if (this.first === item) { - this.first = item.next; - } - - // Add item to the end - item.prev = this.last; - item.next = null; - - if (this.last !== null) { - this.last.next = item; - } - - this.last = item; - - // Handle edge case: if this was the only item, it's also first - if (this.first === null) { - this.first = item; - } - } - - /** - * Returns an array of all keys in the cache, ordered from least to most recently used. - * - * @method keys - * @memberof LRU - * @returns {string[]} Array of keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * cache.get('a'); // Move 'a' to most recent - * console.log(cache.keys()); // ['b', 'a'] - * @see {@link LRU#values} - * @see {@link LRU#entries} - * @since 9.0.0 - */ - keys () { - const result = new Array(this.size); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; - } - - return result; - } - - /** - * Sets a value in the cache and returns any evicted item. - * - * @method setWithEvicted - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. - * @example - * const cache = new LRU(2); - * cache.set('a', 1).set('b', 2); - * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} - * @see {@link LRU#set} - * @see {@link LRU#evict} - * @since 11.3.0 - */ - setWithEvicted (key, value, resetTtl = this.resetTtl) { - let evicted = null; - - if (this.has(key)) { - this.set(key, value, true, resetTtl); - } else { - if (this.max > 0 && this.size === this.max) { - evicted = {...this.first}; - this.evict(true); - } - - let item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return evicted; - } - - /** - * Sets a value in the cache. Updates the item's position to most recently used. - * - * @method set - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1') - * .set('key2', 'value2') - * .set('key3', 'value3'); - * @see {@link LRU#get} - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - set (key, value, bypass = false, resetTtl = this.resetTtl) { - let item = this.items[key]; - - if (bypass || item !== undefined) { - // Existing item: update value and position - item.value = value; - - if (bypass === false && resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - - // Always move to end, but the bypass parameter affects TTL reset behavior - this.moveToEnd(item); - } else { - // New item: check for eviction and create - if (this.max > 0 && this.size === this.max) { - this.evict(true); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return this; - } - - /** - * Returns an array of all values in the cache for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method values - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. - * @returns {Array<*>} Array of values corresponding to the keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.values()); // [1, 2] - * console.log(cache.values(['a'])); // [1] - * @see {@link LRU#keys} - * @see {@link LRU#entries} - * @since 11.1.0 - */ - values (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - result[i] = this.get(keys[i]); - } - - return result; - } + /** + * 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 accessing existing items via get(). + * @example + * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access + * @see {@link lru} For parameter validation + * @since 1.0.0 + */ + constructor(max = 0, ttl = 0, resetTtl = false) { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.max = max; + this.resetTtl = resetTtl; + this.size = 0; + this.ttl = ttl; + } + + /** + * Removes all items from the cache. + * + * @method clear + * @memberof LRU + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.clear(); + * console.log(cache.size); // 0 + * @since 1.0.0 + */ + clear() { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.size = 0; + + return this; + } + + /** + * Removes an item from the cache by key. + * + * @method delete + * @memberof LRU + * @param {string} key - The key of the item to delete. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1'); + * cache.delete('key1'); + * console.log(cache.has('key1')); // false + * @see {@link LRU#has} + * @see {@link LRU#clear} + * @since 1.0.0 + */ + delete(key) { + const item = this.items[key]; + + if (item !== undefined) { + delete this.items[key]; + this.size--; + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + if (this.last === item) { + this.last = item.prev; + } + + item.prev = null; + item.next = null; + } + + return this; + } + + /** + * Returns an array of [key, value] pairs for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method entries + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. + * @returns {Array>} Array of [key, value] pairs in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.entries()); // [['a', 1], ['b', 2]] + * console.log(cache.entries(['a'])); // [['a', 1]] + * @see {@link LRU#keys} + * @see {@link LRU#values} + * @since 11.1.0 + */ + entries(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const item = this.items[key]; + result[i] = [key, item !== undefined ? item.value : undefined]; + } + + return result; + } + + /** + * Removes the least recently used item from the cache. + * + * @method evict + * @memberof LRU + * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('old', 'value').set('new', 'value'); + * cache.evict(); // Removes 'old' item + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + evict(bypass = false) { + if (bypass || this.size > 0) { + const item = this.first; + + if (!item) { + return this; + } + + delete this.items[item.key]; + + if (--this.size === 0) { + this.first = null; + this.last = null; + } else { + this.first = item.next; + this.first.prev = null; + } + + item.next = null; + } + + return this; + } + + /** + * Returns the expiration timestamp for a given key. + * + * @method expiresAt + * @memberof LRU + * @param {string} key - The key to check expiration for. + * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. + * @example + * const cache = new LRU(100, 5000); // 5 second TTL + * cache.set('key1', 'value1'); + * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now + * @see {@link LRU#get} + * @see {@link LRU#has} + * @since 1.0.0 + */ + expiresAt(key) { + const item = this.items[key]; + return item !== undefined ? item.expiry : undefined; + } + + /** + * Retrieves a value from the cache by key. Updates the item's position to most recently used. + * + * @method get + * @memberof LRU + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found or expired. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.get('key1')); // 'value1' + * console.log(cache.get('nonexistent')); // undefined + * @see {@link LRU#set} + * @see {@link LRU#has} + * @since 1.0.0 + */ + get(key) { + const item = this.items[key]; + + if (item !== undefined) { + // Check TTL only if enabled to avoid unnecessary Date.now() calls + if (this.ttl > 0) { + if (item.expiry <= Date.now()) { + this.delete(key); + + return undefined; + } + } + + // Fast LRU update without full set() overhead + this.moveToEnd(item); + + return item.value; + } + + return undefined; + } + + /** + * Checks if a key exists in the cache. + * + * @method has + * @memberof LRU + * @param {string} key - The key to check for. + * @returns {boolean} True if the key exists, false otherwise. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.has('key1')); // true + * console.log(cache.has('nonexistent')); // false + * @see {@link LRU#get} + * @see {@link LRU#delete} + * @since 9.0.0 + */ + has(key) { + const item = this.items[key]; + return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + } + + /** + * Efficiently moves an item to the end of the LRU list (most recently used position). + * This is an internal optimization method that avoids the overhead of the full set() operation + * when only LRU position needs to be updated. + * + * @method moveToEnd + * @memberof LRU + * @param {Object} item - The cache item with prev/next pointers to reposition. + * @private + * @since 11.3.5 + */ + moveToEnd(item) { + if (this.last === item) { + return; + } + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + item.prev = this.last; + item.next = null; + this.last.next = item; + this.last = item; + } + + /** + * Returns an array of all keys in the cache, ordered from least to most recently used. + * + * @method keys + * @memberof LRU + * @returns {string[]} Array of keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * cache.get('a'); // Move 'a' to most recent + * console.log(cache.keys()); // ['b', 'a'] + * @see {@link LRU#values} + * @see {@link LRU#entries} + * @since 9.0.0 + */ + keys() { + const result = Array.from({ length: this.size }); + let x = this.first; + let i = 0; + + while (x !== null) { + result[i++] = x.key; + x = x.next; + } + + return result; + } + + /** + * Sets a value in the cache and returns any evicted item. + * + * @method setWithEvicted + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. + * @example + * const cache = new LRU(2); + * cache.set('a', 1).set('b', 2); + * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} + * @see {@link LRU#set} + * @see {@link LRU#evict} + * @since 11.3.0 + */ + setWithEvicted(key, value, resetTtl = this.resetTtl) { + let evicted = null; + let item = this.items[key]; + + if (item !== undefined) { + item.value = value; + if (resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + this.moveToEnd(item); + } else { + if (this.max > 0 && this.size === this.max) { + evicted = { + key: this.first.key, + value: this.first.value, + expiry: this.first.expiry, + }; + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return evicted; + } + + /** + * Sets a value in the cache. Updates the item's position to most recently used. + * + * @method set + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1') + * .set('key2', 'value2') + * .set('key3', 'value3'); + * @see {@link LRU#get} + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + set(key, value, bypass = false, resetTtl = this.resetTtl) { + let item = this.items[key]; + + if (bypass || item !== undefined) { + // Existing item: update value and position + item.value = value; + + if (bypass === false && resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + + // Always move to end, but the bypass parameter affects TTL reset behavior + this.moveToEnd(item); + } else { + // New item: check for eviction and create + if (this.max > 0 && this.size === this.max) { + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return this; + } + + /** + * Returns an array of all values in the cache for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method values + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @returns {Array<*>} Array of values corresponding to the keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.values()); // [1, 2] + * console.log(cache.values(['a'])); // [1] + * @see {@link LRU#keys} + * @see {@link LRU#entries} + * @since 11.1.0 + */ + values(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + result[i] = item !== undefined ? item.value : undefined; + } + + return result; + } } /** @@ -478,20 +489,20 @@ class LRU { * @see {@link LRU} * @since 1.0.0 */ -function lru (max = 1000, ttl = 0, resetTtl = false) { - if (isNaN(max) || max < 0) { - throw new TypeError("Invalid max value"); - } +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 (isNaN(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); - } + if (typeof resetTtl !== "boolean") { + throw new TypeError("Invalid resetTtl value"); + } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTtl); } exports.LRU = LRU; diff --git a/dist/tiny-lru.js b/dist/tiny-lru.js index cdec409..be888e9 100644 --- a/dist/tiny-lru.js +++ b/dist/tiny-lru.js @@ -24,432 +24,443 @@ * // After 5 seconds, key1 will be expired */ 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 accessing existing items via get(). - * @example - * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access - * @see {@link lru} For parameter validation - * @since 1.0.0 - */ - constructor (max = 0, ttl = 0, resetTtl = false) { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.max = max; - this.resetTtl = resetTtl; - this.size = 0; - this.ttl = ttl; - } - - /** - * Removes all items from the cache. - * - * @method clear - * @memberof LRU - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.clear(); - * console.log(cache.size); // 0 - * @since 1.0.0 - */ - clear () { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.size = 0; - - return this; - } - - /** - * Removes an item from the cache by key. - * - * @method delete - * @memberof LRU - * @param {string} key - The key of the item to delete. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1'); - * cache.delete('key1'); - * console.log(cache.has('key1')); // false - * @see {@link LRU#has} - * @see {@link LRU#clear} - * @since 1.0.0 - */ - delete (key) { - if (this.has(key)) { - const item = this.items[key]; - - delete this.items[key]; - this.size--; - - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - if (this.first === item) { - this.first = item.next; - } - - if (this.last === item) { - this.last = item.prev; - } - } - - return this; - } - - /** - * Returns an array of [key, value] pairs for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method entries - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. - * @returns {Array>} Array of [key, value] pairs in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.entries()); // [['a', 1], ['b', 2]] - * console.log(cache.entries(['a'])); // [['a', 1]] - * @see {@link LRU#keys} - * @see {@link LRU#values} - * @since 11.1.0 - */ - entries (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - result[i] = [key, this.get(key)]; - } - - return result; - } - - /** - * Removes the least recently used item from the cache. - * - * @method evict - * @memberof LRU - * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('old', 'value').set('new', 'value'); - * cache.evict(); // Removes 'old' item - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - evict (bypass = false) { - if (bypass || this.size > 0) { - const item = this.first; - - delete this.items[item.key]; - - if (--this.size === 0) { - this.first = null; - this.last = null; - } else { - this.first = item.next; - this.first.prev = null; - } - } - - return this; - } - - /** - * Returns the expiration timestamp for a given key. - * - * @method expiresAt - * @memberof LRU - * @param {string} key - The key to check expiration for. - * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. - * @example - * const cache = new LRU(100, 5000); // 5 second TTL - * cache.set('key1', 'value1'); - * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now - * @see {@link LRU#get} - * @see {@link LRU#has} - * @since 1.0.0 - */ - expiresAt (key) { - let result; - - if (this.has(key)) { - result = this.items[key].expiry; - } - - return result; - } - - /** - * Retrieves a value from the cache by key. Updates the item's position to most recently used. - * - * @method get - * @memberof LRU - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found or expired. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.get('key1')); // 'value1' - * console.log(cache.get('nonexistent')); // undefined - * @see {@link LRU#set} - * @see {@link LRU#has} - * @since 1.0.0 - */ - get (key) { - const item = this.items[key]; - - if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - - return undefined; - } - } - - // Fast LRU update without full set() overhead - this.moveToEnd(item); - - return item.value; - } - - return undefined; - } - - /** - * Checks if a key exists in the cache. - * - * @method has - * @memberof LRU - * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.has('key1')); // true - * console.log(cache.has('nonexistent')); // false - * @see {@link LRU#get} - * @see {@link LRU#delete} - * @since 9.0.0 - */ - has (key) { - return key in this.items; - } - - /** - * Efficiently moves an item to the end of the LRU list (most recently used position). - * This is an internal optimization method that avoids the overhead of the full set() operation - * when only LRU position needs to be updated. - * - * @method moveToEnd - * @memberof LRU - * @param {Object} item - The cache item with prev/next pointers to reposition. - * @private - * @since 11.3.5 - */ - moveToEnd (item) { - // If already at the end, nothing to do - if (this.last === item) { - return; - } - - // Remove item from current position in the list - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - // Update first pointer if this was the first item - if (this.first === item) { - this.first = item.next; - } - - // Add item to the end - item.prev = this.last; - item.next = null; - - if (this.last !== null) { - this.last.next = item; - } - - this.last = item; - - // Handle edge case: if this was the only item, it's also first - if (this.first === null) { - this.first = item; - } - } - - /** - * Returns an array of all keys in the cache, ordered from least to most recently used. - * - * @method keys - * @memberof LRU - * @returns {string[]} Array of keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * cache.get('a'); // Move 'a' to most recent - * console.log(cache.keys()); // ['b', 'a'] - * @see {@link LRU#values} - * @see {@link LRU#entries} - * @since 9.0.0 - */ - keys () { - const result = new Array(this.size); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; - } - - return result; - } - - /** - * Sets a value in the cache and returns any evicted item. - * - * @method setWithEvicted - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. - * @example - * const cache = new LRU(2); - * cache.set('a', 1).set('b', 2); - * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} - * @see {@link LRU#set} - * @see {@link LRU#evict} - * @since 11.3.0 - */ - setWithEvicted (key, value, resetTtl = this.resetTtl) { - let evicted = null; - - if (this.has(key)) { - this.set(key, value, true, resetTtl); - } else { - if (this.max > 0 && this.size === this.max) { - evicted = {...this.first}; - this.evict(true); - } - - let item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return evicted; - } - - /** - * Sets a value in the cache. Updates the item's position to most recently used. - * - * @method set - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1') - * .set('key2', 'value2') - * .set('key3', 'value3'); - * @see {@link LRU#get} - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - set (key, value, bypass = false, resetTtl = this.resetTtl) { - let item = this.items[key]; - - if (bypass || item !== undefined) { - // Existing item: update value and position - item.value = value; - - if (bypass === false && resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - - // Always move to end, but the bypass parameter affects TTL reset behavior - this.moveToEnd(item); - } else { - // New item: check for eviction and create - if (this.max > 0 && this.size === this.max) { - this.evict(true); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return this; - } - - /** - * Returns an array of all values in the cache for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method values - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. - * @returns {Array<*>} Array of values corresponding to the keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.values()); // [1, 2] - * console.log(cache.values(['a'])); // [1] - * @see {@link LRU#keys} - * @see {@link LRU#entries} - * @since 11.1.0 - */ - values (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - result[i] = this.get(keys[i]); - } - - return result; - } + /** + * 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 accessing existing items via get(). + * @example + * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access + * @see {@link lru} For parameter validation + * @since 1.0.0 + */ + constructor(max = 0, ttl = 0, resetTtl = false) { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.max = max; + this.resetTtl = resetTtl; + this.size = 0; + this.ttl = ttl; + } + + /** + * Removes all items from the cache. + * + * @method clear + * @memberof LRU + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.clear(); + * console.log(cache.size); // 0 + * @since 1.0.0 + */ + clear() { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.size = 0; + + return this; + } + + /** + * Removes an item from the cache by key. + * + * @method delete + * @memberof LRU + * @param {string} key - The key of the item to delete. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1'); + * cache.delete('key1'); + * console.log(cache.has('key1')); // false + * @see {@link LRU#has} + * @see {@link LRU#clear} + * @since 1.0.0 + */ + delete(key) { + const item = this.items[key]; + + if (item !== undefined) { + delete this.items[key]; + this.size--; + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + if (this.last === item) { + this.last = item.prev; + } + + item.prev = null; + item.next = null; + } + + return this; + } + + /** + * Returns an array of [key, value] pairs for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method entries + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. + * @returns {Array>} Array of [key, value] pairs in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.entries()); // [['a', 1], ['b', 2]] + * console.log(cache.entries(['a'])); // [['a', 1]] + * @see {@link LRU#keys} + * @see {@link LRU#values} + * @since 11.1.0 + */ + entries(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const item = this.items[key]; + result[i] = [key, item !== undefined ? item.value : undefined]; + } + + return result; + } + + /** + * Removes the least recently used item from the cache. + * + * @method evict + * @memberof LRU + * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('old', 'value').set('new', 'value'); + * cache.evict(); // Removes 'old' item + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + evict(bypass = false) { + if (bypass || this.size > 0) { + const item = this.first; + + if (!item) { + return this; + } + + delete this.items[item.key]; + + if (--this.size === 0) { + this.first = null; + this.last = null; + } else { + this.first = item.next; + this.first.prev = null; + } + + item.next = null; + } + + return this; + } + + /** + * Returns the expiration timestamp for a given key. + * + * @method expiresAt + * @memberof LRU + * @param {string} key - The key to check expiration for. + * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. + * @example + * const cache = new LRU(100, 5000); // 5 second TTL + * cache.set('key1', 'value1'); + * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now + * @see {@link LRU#get} + * @see {@link LRU#has} + * @since 1.0.0 + */ + expiresAt(key) { + const item = this.items[key]; + return item !== undefined ? item.expiry : undefined; + } + + /** + * Retrieves a value from the cache by key. Updates the item's position to most recently used. + * + * @method get + * @memberof LRU + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found or expired. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.get('key1')); // 'value1' + * console.log(cache.get('nonexistent')); // undefined + * @see {@link LRU#set} + * @see {@link LRU#has} + * @since 1.0.0 + */ + get(key) { + const item = this.items[key]; + + if (item !== undefined) { + // Check TTL only if enabled to avoid unnecessary Date.now() calls + if (this.ttl > 0) { + if (item.expiry <= Date.now()) { + this.delete(key); + + return undefined; + } + } + + // Fast LRU update without full set() overhead + this.moveToEnd(item); + + return item.value; + } + + return undefined; + } + + /** + * Checks if a key exists in the cache. + * + * @method has + * @memberof LRU + * @param {string} key - The key to check for. + * @returns {boolean} True if the key exists, false otherwise. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.has('key1')); // true + * console.log(cache.has('nonexistent')); // false + * @see {@link LRU#get} + * @see {@link LRU#delete} + * @since 9.0.0 + */ + has(key) { + const item = this.items[key]; + return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + } + + /** + * Efficiently moves an item to the end of the LRU list (most recently used position). + * This is an internal optimization method that avoids the overhead of the full set() operation + * when only LRU position needs to be updated. + * + * @method moveToEnd + * @memberof LRU + * @param {Object} item - The cache item with prev/next pointers to reposition. + * @private + * @since 11.3.5 + */ + moveToEnd(item) { + if (this.last === item) { + return; + } + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + item.prev = this.last; + item.next = null; + this.last.next = item; + this.last = item; + } + + /** + * Returns an array of all keys in the cache, ordered from least to most recently used. + * + * @method keys + * @memberof LRU + * @returns {string[]} Array of keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * cache.get('a'); // Move 'a' to most recent + * console.log(cache.keys()); // ['b', 'a'] + * @see {@link LRU#values} + * @see {@link LRU#entries} + * @since 9.0.0 + */ + keys() { + const result = Array.from({ length: this.size }); + let x = this.first; + let i = 0; + + while (x !== null) { + result[i++] = x.key; + x = x.next; + } + + return result; + } + + /** + * Sets a value in the cache and returns any evicted item. + * + * @method setWithEvicted + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. + * @example + * const cache = new LRU(2); + * cache.set('a', 1).set('b', 2); + * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} + * @see {@link LRU#set} + * @see {@link LRU#evict} + * @since 11.3.0 + */ + setWithEvicted(key, value, resetTtl = this.resetTtl) { + let evicted = null; + let item = this.items[key]; + + if (item !== undefined) { + item.value = value; + if (resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + this.moveToEnd(item); + } else { + if (this.max > 0 && this.size === this.max) { + evicted = { + key: this.first.key, + value: this.first.value, + expiry: this.first.expiry, + }; + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return evicted; + } + + /** + * Sets a value in the cache. Updates the item's position to most recently used. + * + * @method set + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1') + * .set('key2', 'value2') + * .set('key3', 'value3'); + * @see {@link LRU#get} + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + set(key, value, bypass = false, resetTtl = this.resetTtl) { + let item = this.items[key]; + + if (bypass || item !== undefined) { + // Existing item: update value and position + item.value = value; + + if (bypass === false && resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + + // Always move to end, but the bypass parameter affects TTL reset behavior + this.moveToEnd(item); + } else { + // New item: check for eviction and create + if (this.max > 0 && this.size === this.max) { + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return this; + } + + /** + * Returns an array of all values in the cache for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method values + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @returns {Array<*>} Array of values corresponding to the keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.values()); // [1, 2] + * console.log(cache.values(['a'])); // [1] + * @see {@link LRU#keys} + * @see {@link LRU#entries} + * @since 11.1.0 + */ + values(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + result[i] = item !== undefined ? item.value : undefined; + } + + return result; + } } /** @@ -476,18 +487,18 @@ class LRU { * @see {@link LRU} * @since 1.0.0 */ -function lru (max = 1000, ttl = 0, resetTtl = false) { - if (isNaN(max) || max < 0) { - throw new TypeError("Invalid max value"); - } +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 (isNaN(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); - } + if (typeof resetTtl !== "boolean") { + throw new TypeError("Invalid resetTtl value"); + } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTtl); }export{LRU,lru}; \ No newline at end of file diff --git a/dist/tiny-lru.min.js b/dist/tiny-lru.min.js index 63f8d9a..382c0c0 100644 --- a/dist/tiny-lru.min.js +++ b/dist/tiny-lru.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 11.4.7 */ -class t{constructor(t=0,s=0,e=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=e,this.size=0,this.ttl=s}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){if(this.has(t)){const s=this.items[t];delete this.items[t],this.size--,null!==s.prev&&(s.prev.next=s.next),null!==s.next&&(s.next.prev=s.prev),this.first===s&&(this.first=s.next),this.last===s&&(this.last=s.prev)}return this}entries(t=this.keys()){const s=new Array(t.length);for(let e=0;e0){const t=this.first;delete this.items[t.key],0==--this.size?(this.first=null,this.last=null):(this.first=t.next,this.first.prev=null)}return this}expiresAt(t){let s;return this.has(t)&&(s=this.items[t].expiry),s}get(t){const s=this.items[t];if(void 0!==s)return this.ttl>0&&s.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(s),s.value)}has(t){return t in this.items}moveToEnd(t){this.last!==t&&(null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),t.prev=this.last,t.next=null,null!==this.last&&(this.last.next=t),this.last=t,null===this.first&&(this.first=t))}keys(){const t=new Array(this.size);let s=this.first,e=0;for(;null!==s;)t[e++]=s.key,s=s.next;return t}setWithEvicted(t,s,e=this.resetTtl){let i=null;if(this.has(t))this.set(t,s,!0,e);else{this.max>0&&this.size===this.max&&(i={...this.first},this.evict(!0));let 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}return i}set(t,s,e=!1,i=this.resetTtl){let l=this.items[t];return e||void 0!==l?(l.value=s,!1===e&&i&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&this.evict(!0),l=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=l:this.last.next=l,this.last=l),this}values(t=this.keys()){const s=new Array(t.length);for(let e=0;e0){const t=this.first;if(!t)return this;delete this.items[t.key],0==--this.size?(this.first=null,this.last=null):(this.first=t.next,this.first.prev=null),t.next=null}return this}expiresAt(t){const s=this.items[t];return void 0!==s?s.expiry:void 0}get(t){const s=this.items[t];if(void 0!==s)return this.ttl>0&&s.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(s),s.value)}has(t){const s=this.items[t];return void 0!==s&&(0===this.ttl||s.expiry>Date.now())}moveToEnd(t){this.last!==t&&(null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let s=this.first,e=0;for(;null!==s;)t[e++]=s.key,s=s.next;return t}setWithEvicted(t,s,e=this.resetTtl){let i=null,l=this.items[t];return void 0!==l?(l.value=s,e&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&(i={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict(!0)),l=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=l:this.last.next=l,this.last=l),i}set(t,s,e=!1,i=this.resetTtl){let l=this.items[t];return e||void 0!==l?(l.value=s,!1===e&&i&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&this.evict(!0),l=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=l:this.last.next=l,this.last=l),this}values(t){void 0===t&&(t=this.keys());const s=Array.from({length:t.length});for(let e=0;e>} Array of [key, value] pairs in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * console.log(cache.entries()); // [['a', 1], ['b', 2]]\n\t * console.log(cache.entries(['a'])); // [['a', 1]]\n\t * @see {@link LRU#keys}\n\t * @see {@link LRU#values}\n\t * @since 11.1.0\n\t */\n\tentries (keys = this.keys()) {\n\t\tconst result = new Array(keys.length);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[i] = [key, this.get(key)];\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 * @method evict\n\t * @memberof LRU\n\t * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t * @example\n\t * cache.set('old', 'value').set('new', 'value');\n\t * cache.evict(); // Removes 'old' item\n\t * @see {@link LRU#setWithEvicted}\n\t * @since 1.0.0\n\t */\n\tevict (bypass = false) {\n\t\tif (bypass || this.size > 0) {\n\t\t\tconst item = this.first;\n\n\t\t\tdelete this.items[item.key];\n\n\t\t\tif (--this.size === 0) {\n\t\t\t\tthis.first = null;\n\t\t\t\tthis.last = null;\n\t\t\t} else {\n\t\t\t\tthis.first = item.next;\n\t\t\t\tthis.first.prev = null;\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 * @method expiresAt\n\t * @memberof LRU\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 * @example\n\t * const cache = new LRU(100, 5000); // 5 second TTL\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#has}\n\t * @since 1.0.0\n\t */\n\texpiresAt (key) {\n\t\tlet result;\n\n\t\tif (this.has(key)) {\n\t\t\tresult = this.items[key].expiry;\n\t\t}\n\n\t\treturn result;\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 * @method get\n\t * @memberof LRU\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 * @example\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.get('key1')); // 'value1'\n\t * console.log(cache.get('nonexistent')); // undefined\n\t * @see {@link LRU#set}\n\t * @see {@link LRU#has}\n\t * @since 1.0.0\n\t */\n\tget (key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @method has\n\t * @memberof LRU\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t * @example\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.has('key1')); // true\n\t * console.log(cache.has('nonexistent')); // false\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#delete}\n\t * @since 9.0.0\n\t */\n\thas (key) {\n\t\treturn key in this.items;\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 * @method moveToEnd\n\t * @memberof LRU\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t * @since 11.3.5\n\t */\n\tmoveToEnd (item) {\n\t\t// If already at the end, nothing to do\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove item from current position in the list\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\t// Update first pointer if this was the first item\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\t// Add item to the end\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\n\t\tif (this.last !== null) {\n\t\t\tthis.last.next = item;\n\t\t}\n\n\t\tthis.last = item;\n\n\t\t// Handle edge case: if this was the only item, it's also first\n\t\tif (this.first === null) {\n\t\t\tthis.first = item;\n\t\t}\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 * @method keys\n\t * @memberof LRU\n\t * @returns {string[]} Array of keys in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * cache.get('a'); // Move 'a' to most recent\n\t * console.log(cache.keys()); // ['b', 'a']\n\t * @see {@link LRU#values}\n\t * @see {@link LRU#entries}\n\t * @since 9.0.0\n\t */\n\tkeys () {\n\t\tconst result = new Array(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 * @method setWithEvicted\n\t * @memberof LRU\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null.\n\t * @example\n\t * const cache = new LRU(2);\n\t * cache.set('a', 1).set('b', 2);\n\t * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...}\n\t * @see {@link LRU#set}\n\t * @see {@link LRU#evict}\n\t * @since 11.3.0\n\t */\n\tsetWithEvicted (key, value, resetTtl = this.resetTtl) {\n\t\tlet evicted = null;\n\n\t\tif (this.has(key)) {\n\t\t\tthis.set(key, value, true, resetTtl);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {...this.first};\n\t\t\t\tthis.evict(true);\n\t\t\t}\n\n\t\t\tlet item = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @method set\n\t * @memberof LRU\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method.\n\t * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t * @example\n\t * cache.set('key1', 'value1')\n\t * .set('key2', 'value2')\n\t * .set('key3', 'value3');\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#setWithEvicted}\n\t * @since 1.0.0\n\t */\n\tset (key, value, bypass = false, resetTtl = this.resetTtl) {\n\t\tlet item = this.items[key];\n\n\t\tif (bypass || item !== undefined) {\n\t\t\t// Existing item: update value and position\n\t\t\titem.value = value;\n\n\t\t\tif (bypass === false && resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\t// Always move to end, but the bypass parameter affects TTL reset behavior\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\t// New item: check for eviction and create\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict(true);\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * Order follows LRU order (least to most recently used).\n\t *\n\t * @method values\n\t * @memberof LRU\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * console.log(cache.values()); // [1, 2]\n\t * console.log(cache.values(['a'])); // [1]\n\t * @see {@link LRU#keys}\n\t * @see {@link LRU#entries}\n\t * @since 11.1.0\n\t */\n\tvalues (keys = this.keys()) {\n\t\tconst result = new Array(keys.length);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tresult[i] = this.get(keys[i]);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n * @example\n * // Create cache with factory function\n * const cache = lru(100, 5000, true);\n * cache.set('key', 'value');\n *\n * @example\n * // Error handling\n * try {\n * const cache = lru(-1); // Invalid max\n * } catch (error) {\n * console.error(error.message); // \"Invalid max value\"\n * }\n * @see {@link LRU}\n * @since 1.0.0\n */\nexport function lru (max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","clear","key","has","item","prev","next","entries","keys","result","Array","length","i","get","evict","bypass","expiresAt","expiry","undefined","Date","now","delete","moveToEnd","value","x","setWithEvicted","evicted","set","values","lru","isNaN","TypeError"],"mappings":";;;;AAkBO,MAAMA,EAcZ,WAAAC,CAAaC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACzCC,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKH,IAAMA,EACXG,KAAKD,SAAWA,EAChBC,KAAKM,KAAO,EACZN,KAAKF,IAAMA,CACZ,CAaA,KAAAS,GAMC,OALAP,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKM,KAAO,EAELN,IACR,CAiBA,OAAQQ,GACP,GAAIR,KAAKS,IAAID,GAAM,CAClB,MAAME,EAAOV,KAAKE,MAAMM,UAEjBR,KAAKE,MAAMM,GAClBR,KAAKM,OAEa,OAAdI,EAAKC,OACRD,EAAKC,KAAKC,KAAOF,EAAKE,MAGL,OAAdF,EAAKE,OACRF,EAAKE,KAAKD,KAAOD,EAAKC,MAGnBX,KAAKC,QAAUS,IAClBV,KAAKC,MAAQS,EAAKE,MAGfZ,KAAKK,OAASK,IACjBV,KAAKK,KAAOK,EAAKC,KAEnB,CAEA,OAAOX,IACR,CAkBA,OAAAa,CAASC,EAAOd,KAAKc,QACpB,MAAMC,EAAS,IAAIC,MAAMF,EAAKG,QAC9B,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAAKG,OAAQC,IAAK,CACrC,MAAMV,EAAMM,EAAKI,GACjBH,EAAOG,GAAK,CAACV,EAAKR,KAAKmB,IAAIX,GAC5B,CAEA,OAAOO,CACR,CAeA,KAAAK,CAAOC,GAAS,GACf,GAAIA,GAAUrB,KAAKM,KAAO,EAAG,CAC5B,MAAMI,EAAOV,KAAKC,aAEXD,KAAKE,MAAMQ,EAAKF,KAEH,KAAdR,KAAKM,MACVN,KAAKC,MAAQ,KACbD,KAAKK,KAAO,OAEZL,KAAKC,MAAQS,EAAKE,KAClBZ,KAAKC,MAAMU,KAAO,KAEpB,CAEA,OAAOX,IACR,CAiBA,SAAAsB,CAAWd,GACV,IAAIO,EAMJ,OAJIf,KAAKS,IAAID,KACZO,EAASf,KAAKE,MAAMM,GAAKe,QAGnBR,CACR,CAiBA,GAAAI,CAAKX,GACJ,MAAME,EAAOV,KAAKE,MAAMM,GAExB,QAAagB,IAATd,EAEH,OAAIV,KAAKF,IAAM,GACVY,EAAKa,QAAUE,KAAKC,WACvB1B,KAAK2B,OAAOnB,IAOdR,KAAK4B,UAAUlB,GAERA,EAAKmB,MAId,CAiBA,GAAApB,CAAKD,GACJ,OAAOA,KAAOR,KAAKE,KACpB,CAaA,SAAA0B,CAAWlB,GAENV,KAAKK,OAASK,IAKA,OAAdA,EAAKC,OACRD,EAAKC,KAAKC,KAAOF,EAAKE,MAGL,OAAdF,EAAKE,OACRF,EAAKE,KAAKD,KAAOD,EAAKC,MAInBX,KAAKC,QAAUS,IAClBV,KAAKC,MAAQS,EAAKE,MAInBF,EAAKC,KAAOX,KAAKK,KACjBK,EAAKE,KAAO,KAEM,OAAdZ,KAAKK,OACRL,KAAKK,KAAKO,KAAOF,GAGlBV,KAAKK,KAAOK,EAGO,OAAfV,KAAKC,QACRD,KAAKC,MAAQS,GAEf,CAgBA,IAAAI,GACC,MAAMC,EAAS,IAAIC,MAAMhB,KAAKM,MAC9B,IAAIwB,EAAI9B,KAAKC,MACTiB,EAAI,EAER,KAAa,OAANY,GACNf,EAAOG,KAAOY,EAAEtB,IAChBsB,EAAIA,EAAElB,KAGP,OAAOG,CACR,CAmBA,cAAAgB,CAAgBvB,EAAKqB,EAAO9B,EAAWC,KAAKD,UAC3C,IAAIiC,EAAU,KAEd,GAAIhC,KAAKS,IAAID,GACZR,KAAKiC,IAAIzB,EAAKqB,GAAO,EAAM9B,OACrB,CACFC,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,MACtCmC,EAAU,IAAIhC,KAAKC,OACnBD,KAAKoB,OAAM,IAGZ,IAAIV,EAAOV,KAAKE,MAAMM,GAAO,CAC5Be,OAAQvB,KAAKF,IAAM,EAAI2B,KAAKC,MAAQ1B,KAAKF,IAAME,KAAKF,IACpDU,IAAKA,EACLG,KAAMX,KAAKK,KACXO,KAAM,KACNiB,SAGmB,KAAd7B,KAAKM,KACVN,KAAKC,MAAQS,EAEbV,KAAKK,KAAKO,KAAOF,EAGlBV,KAAKK,KAAOK,CACb,CAEA,OAAOsB,CACR,CAoBA,GAAAC,CAAKzB,EAAKqB,EAAOR,GAAS,EAAOtB,EAAWC,KAAKD,UAChD,IAAIW,EAAOV,KAAKE,MAAMM,GAmCtB,OAjCIa,QAAmBG,IAATd,GAEbA,EAAKmB,MAAQA,GAEE,IAAXR,GAAoBtB,IACvBW,EAAKa,OAASvB,KAAKF,IAAM,EAAI2B,KAAKC,MAAQ1B,KAAKF,IAAME,KAAKF,KAI3DE,KAAK4B,UAAUlB,KAGXV,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,KACtCG,KAAKoB,OAAM,GAGZV,EAAOV,KAAKE,MAAMM,GAAO,CACxBe,OAAQvB,KAAKF,IAAM,EAAI2B,KAAKC,MAAQ1B,KAAKF,IAAME,KAAKF,IACpDU,IAAKA,EACLG,KAAMX,KAAKK,KACXO,KAAM,KACNiB,SAGmB,KAAd7B,KAAKM,KACVN,KAAKC,MAAQS,EAEbV,KAAKK,KAAKO,KAAOF,EAGlBV,KAAKK,KAAOK,GAGNV,IACR,CAkBA,MAAAkC,CAAQpB,EAAOd,KAAKc,QACnB,MAAMC,EAAS,IAAIC,MAAMF,EAAKG,QAC9B,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAAKG,OAAQC,IAChCH,EAAOG,GAAKlB,KAAKmB,IAAIL,EAAKI,IAG3B,OAAOH,CACR,EA2BM,SAASoB,EAAKtC,EAAM,IAAMC,EAAM,EAAGC,GAAW,GACpD,GAAIqC,MAAMvC,IAAQA,EAAM,EACvB,MAAM,IAAIwC,UAAU,qBAGrB,GAAID,MAAMtC,IAAQA,EAAM,EACvB,MAAM,IAAIuC,UAAU,qBAGrB,GAAwB,kBAAbtC,EACV,MAAM,IAAIsC,UAAU,0BAGrB,OAAO,IAAI1C,EAAIE,EAAKC,EAAKC,EAC1B,QAAAJ,SAAAwC"} \ 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 * @example\n * // Create a cache with max 100 items\n * const cache = new LRU(100);\n * cache.set('key1', 'value1');\n * console.log(cache.get('key1')); // 'value1'\n *\n * @example\n * // Create a cache with TTL\n * const cache = new LRU(100, 5000); // 5 second TTL\n * cache.set('key1', 'value1');\n * // After 5 seconds, key1 will be expired\n */\nexport class LRU {\n /**\n * Creates a new LRU cache instance.\n * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n *\n * @constructor\n * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @example\n * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access\n * @see {@link lru} For parameter validation\n * @since 1.0.0\n */\n constructor(max = 0, ttl = 0, resetTtl = false) {\n this.first = null;\n this.items = Object.create(null);\n this.last = null;\n this.max = max;\n this.resetTtl = resetTtl;\n this.size = 0;\n this.ttl = ttl;\n }\n\n /**\n * Removes all items from the cache.\n *\n * @method clear\n * @memberof LRU\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.clear();\n * console.log(cache.size); // 0\n * @since 1.0.0\n */\n clear() {\n this.first = null;\n this.items = Object.create(null);\n this.last = null;\n this.size = 0;\n\n return this;\n }\n\n /**\n * Removes an item from the cache by key.\n *\n * @method delete\n * @memberof LRU\n * @param {string} key - The key of the item to delete.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('key1', 'value1');\n * cache.delete('key1');\n * console.log(cache.has('key1')); // false\n * @see {@link LRU#has}\n * @see {@link LRU#clear}\n * @since 1.0.0\n */\n delete(key) {\n const item = this.items[key];\n\n if (item !== undefined) {\n delete this.items[key];\n this.size--;\n\n if (item.prev !== null) {\n item.prev.next = item.next;\n }\n\n if (item.next !== null) {\n item.next.prev = item.prev;\n }\n\n if (this.first === item) {\n this.first = item.next;\n }\n\n if (this.last === item) {\n this.last = item.prev;\n }\n\n item.prev = null;\n item.next = null;\n }\n\n return this;\n }\n\n /**\n * Returns an array of [key, value] pairs for the specified keys.\n * Order follows LRU order (least to most recently used).\n *\n * @method entries\n * @memberof LRU\n * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n * @returns {Array>} Array of [key, value] pairs in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * console.log(cache.entries()); // [['a', 1], ['b', 2]]\n * console.log(cache.entries(['a'])); // [['a', 1]]\n * @see {@link LRU#keys}\n * @see {@link LRU#values}\n * @since 11.1.0\n */\n entries(keys) {\n if (keys === undefined) {\n keys = this.keys();\n }\n\n const result = Array.from({ length: keys.length });\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const item = this.items[key];\n result[i] = [key, item !== undefined ? item.value : undefined];\n }\n\n return result;\n }\n\n /**\n * Removes the least recently used item from the cache.\n *\n * @method evict\n * @memberof LRU\n * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('old', 'value').set('new', 'value');\n * cache.evict(); // Removes 'old' item\n * @see {@link LRU#setWithEvicted}\n * @since 1.0.0\n */\n evict(bypass = false) {\n if (bypass || this.size > 0) {\n const item = this.first;\n\n if (!item) {\n return this;\n }\n\n delete this.items[item.key];\n\n if (--this.size === 0) {\n this.first = null;\n this.last = null;\n } else {\n this.first = item.next;\n this.first.prev = null;\n }\n\n item.next = null;\n }\n\n return this;\n }\n\n /**\n * Returns the expiration timestamp for a given key.\n *\n * @method expiresAt\n * @memberof LRU\n * @param {string} key - The key to check expiration for.\n * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n * @example\n * const cache = new LRU(100, 5000); // 5 second TTL\n * cache.set('key1', 'value1');\n * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now\n * @see {@link LRU#get}\n * @see {@link LRU#has}\n * @since 1.0.0\n */\n expiresAt(key) {\n const item = this.items[key];\n return item !== undefined ? item.expiry : undefined;\n }\n\n /**\n * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n *\n * @method get\n * @memberof LRU\n * @param {string} key - The key to retrieve.\n * @returns {*} The value associated with the key, or undefined if not found or expired.\n * @example\n * cache.set('key1', 'value1');\n * console.log(cache.get('key1')); // 'value1'\n * console.log(cache.get('nonexistent')); // undefined\n * @see {@link LRU#set}\n * @see {@link LRU#has}\n * @since 1.0.0\n */\n get(key) {\n const item = this.items[key];\n\n if (item !== undefined) {\n // Check TTL only if enabled to avoid unnecessary Date.now() calls\n if (this.ttl > 0) {\n if (item.expiry <= Date.now()) {\n this.delete(key);\n\n return undefined;\n }\n }\n\n // Fast LRU update without full set() overhead\n this.moveToEnd(item);\n\n return item.value;\n }\n\n return undefined;\n }\n\n /**\n * Checks if a key exists in the cache.\n *\n * @method has\n * @memberof LRU\n * @param {string} key - The key to check for.\n * @returns {boolean} True if the key exists, false otherwise.\n * @example\n * cache.set('key1', 'value1');\n * console.log(cache.has('key1')); // true\n * console.log(cache.has('nonexistent')); // false\n * @see {@link LRU#get}\n * @see {@link LRU#delete}\n * @since 9.0.0\n */\n has(key) {\n const item = this.items[key];\n return item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n }\n\n /**\n * Efficiently moves an item to the end of the LRU list (most recently used position).\n * This is an internal optimization method that avoids the overhead of the full set() operation\n * when only LRU position needs to be updated.\n *\n * @method moveToEnd\n * @memberof LRU\n * @param {Object} item - The cache item with prev/next pointers to reposition.\n * @private\n * @since 11.3.5\n */\n moveToEnd(item) {\n if (this.last === item) {\n return;\n }\n\n if (item.prev !== null) {\n item.prev.next = item.next;\n }\n\n if (item.next !== null) {\n item.next.prev = item.prev;\n }\n\n if (this.first === item) {\n this.first = item.next;\n }\n\n item.prev = this.last;\n item.next = null;\n this.last.next = item;\n this.last = item;\n }\n\n /**\n * Returns an array of all keys in the cache, ordered from least to most recently used.\n *\n * @method keys\n * @memberof LRU\n * @returns {string[]} Array of keys in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * cache.get('a'); // Move 'a' to most recent\n * console.log(cache.keys()); // ['b', 'a']\n * @see {@link LRU#values}\n * @see {@link LRU#entries}\n * @since 9.0.0\n */\n keys() {\n const result = Array.from({ length: this.size });\n let x = this.first;\n let i = 0;\n\n while (x !== null) {\n result[i++] = x.key;\n x = x.next;\n }\n\n return result;\n }\n\n /**\n * Sets a value in the cache and returns any evicted item.\n *\n * @method setWithEvicted\n * @memberof LRU\n * @param {string} key - The key to set.\n * @param {*} value - The value to store.\n * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null.\n * @example\n * const cache = new LRU(2);\n * cache.set('a', 1).set('b', 2);\n * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...}\n * @see {@link LRU#set}\n * @see {@link LRU#evict}\n * @since 11.3.0\n */\n setWithEvicted(key, value, resetTtl = this.resetTtl) {\n let evicted = null;\n let item = this.items[key];\n\n if (item !== undefined) {\n item.value = value;\n if (resetTtl) {\n item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n }\n this.moveToEnd(item);\n } else {\n if (this.max > 0 && this.size === this.max) {\n evicted = {\n key: this.first.key,\n value: this.first.value,\n expiry: this.first.expiry,\n };\n this.evict(true);\n }\n\n item = this.items[key] = {\n expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n key: key,\n prev: this.last,\n next: null,\n value,\n };\n\n if (++this.size === 1) {\n this.first = item;\n } else {\n this.last.next = item;\n }\n\n this.last = item;\n }\n\n return evicted;\n }\n\n /**\n * Sets a value in the cache. Updates the item's position to most recently used.\n *\n * @method set\n * @memberof LRU\n * @param {string} key - The key to set.\n * @param {*} value - The value to store.\n * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method.\n * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('key1', 'value1')\n * .set('key2', 'value2')\n * .set('key3', 'value3');\n * @see {@link LRU#get}\n * @see {@link LRU#setWithEvicted}\n * @since 1.0.0\n */\n set(key, value, bypass = false, resetTtl = this.resetTtl) {\n let item = this.items[key];\n\n if (bypass || item !== undefined) {\n // Existing item: update value and position\n item.value = value;\n\n if (bypass === false && resetTtl) {\n item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n }\n\n // Always move to end, but the bypass parameter affects TTL reset behavior\n this.moveToEnd(item);\n } else {\n // New item: check for eviction and create\n if (this.max > 0 && this.size === this.max) {\n this.evict(true);\n }\n\n item = this.items[key] = {\n expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n key: key,\n prev: this.last,\n next: null,\n value,\n };\n\n if (++this.size === 1) {\n this.first = item;\n } else {\n this.last.next = item;\n }\n\n this.last = item;\n }\n\n return this;\n }\n\n /**\n * Returns an array of all values in the cache for the specified keys.\n * Order follows LRU order (least to most recently used).\n *\n * @method values\n * @memberof LRU\n * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n * @returns {Array<*>} Array of values corresponding to the keys in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * console.log(cache.values()); // [1, 2]\n * console.log(cache.values(['a'])); // [1]\n * @see {@link LRU#keys}\n * @see {@link LRU#entries}\n * @since 11.1.0\n */\n values(keys) {\n if (keys === undefined) {\n keys = this.keys();\n }\n\n const result = Array.from({ length: keys.length });\n for (let i = 0; i < keys.length; i++) {\n const item = this.items[keys[i]];\n result[i] = item !== undefined ? item.value : undefined;\n }\n\n return result;\n }\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n * @example\n * // Create cache with factory function\n * const cache = lru(100, 5000, true);\n * cache.set('key', 'value');\n *\n * @example\n * // Error handling\n * try {\n * const cache = lru(-1); // Invalid max\n * } catch (error) {\n * console.error(error.message); // \"Invalid max value\"\n * }\n * @see {@link LRU}\n * @since 1.0.0\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n if (isNaN(max) || max < 0) {\n throw new TypeError(\"Invalid max value\");\n }\n\n if (isNaN(ttl) || ttl < 0) {\n throw new TypeError(\"Invalid ttl value\");\n }\n\n if (typeof resetTtl !== \"boolean\") {\n throw new TypeError(\"Invalid resetTtl value\");\n }\n\n return new LRU(max, ttl, resetTtl);\n}\n"],"names":["LRU","constructor","max","ttl","resetTtl","this","first","items","Object","create","last","size","clear","key","item","undefined","prev","next","entries","keys","result","Array","from","length","i","value","evict","bypass","expiresAt","expiry","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","lru","isNaN","TypeError"],"mappings":";;;;AAkBO,MAAMA,EAcX,WAAAC,CAAYC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACvCC,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKH,IAAMA,EACXG,KAAKD,SAAWA,EAChBC,KAAKM,KAAO,EACZN,KAAKF,IAAMA,CACb,CAaA,KAAAS,GAME,OALAP,KAAKC,MAAQ,KACbD,KAAKE,MAAQC,OAAOC,OAAO,MAC3BJ,KAAKK,KAAO,KACZL,KAAKM,KAAO,EAELN,IACT,CAiBA,OAAOQ,GACL,MAAMC,EAAOT,KAAKE,MAAMM,GA0BxB,YAxBaE,IAATD,WACKT,KAAKE,MAAMM,GAClBR,KAAKM,OAEa,OAAdG,EAAKE,OACPF,EAAKE,KAAKC,KAAOH,EAAKG,MAGN,OAAdH,EAAKG,OACPH,EAAKG,KAAKD,KAAOF,EAAKE,MAGpBX,KAAKC,QAAUQ,IACjBT,KAAKC,MAAQQ,EAAKG,MAGhBZ,KAAKK,OAASI,IAChBT,KAAKK,KAAOI,EAAKE,MAGnBF,EAAKE,KAAO,KACZF,EAAKG,KAAO,MAGPZ,IACT,CAkBA,OAAAa,CAAQC,QACOJ,IAATI,IACFA,EAAOd,KAAKc,QAGd,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACpC,MAAMX,EAAMM,EAAKK,GACXV,EAAOT,KAAKE,MAAMM,GACxBO,EAAOI,GAAK,CAACX,OAAcE,IAATD,EAAqBA,EAAKW,WAAQV,EACtD,CAEA,OAAOK,CACT,CAeA,KAAAM,CAAMC,GAAS,GACb,GAAIA,GAAUtB,KAAKM,KAAO,EAAG,CAC3B,MAAMG,EAAOT,KAAKC,MAElB,IAAKQ,EACH,OAAOT,YAGFA,KAAKE,MAAMO,EAAKD,KAEH,KAAdR,KAAKM,MACTN,KAAKC,MAAQ,KACbD,KAAKK,KAAO,OAEZL,KAAKC,MAAQQ,EAAKG,KAClBZ,KAAKC,MAAMU,KAAO,MAGpBF,EAAKG,KAAO,IACd,CAEA,OAAOZ,IACT,CAiBA,SAAAuB,CAAUf,GACR,MAAMC,EAAOT,KAAKE,MAAMM,GACxB,YAAgBE,IAATD,EAAqBA,EAAKe,YAASd,CAC5C,CAiBA,GAAAe,CAAIjB,GACF,MAAMC,EAAOT,KAAKE,MAAMM,GAExB,QAAaE,IAATD,EAEF,OAAIT,KAAKF,IAAM,GACTW,EAAKe,QAAUE,KAAKC,WACtB3B,KAAK4B,OAAOpB,IAOhBR,KAAK6B,UAAUpB,GAERA,EAAKW,MAIhB,CAiBA,GAAAU,CAAItB,GACF,MAAMC,EAAOT,KAAKE,MAAMM,GACxB,YAAgBE,IAATD,IAAoC,IAAbT,KAAKF,KAAaW,EAAKe,OAASE,KAAKC,MACrE,CAaA,SAAAE,CAAUpB,GACJT,KAAKK,OAASI,IAIA,OAAdA,EAAKE,OACPF,EAAKE,KAAKC,KAAOH,EAAKG,MAGN,OAAdH,EAAKG,OACPH,EAAKG,KAAKD,KAAOF,EAAKE,MAGpBX,KAAKC,QAAUQ,IACjBT,KAAKC,MAAQQ,EAAKG,MAGpBH,EAAKE,KAAOX,KAAKK,KACjBI,EAAKG,KAAO,KACZZ,KAAKK,KAAKO,KAAOH,EACjBT,KAAKK,KAAOI,EACd,CAgBA,IAAAK,GACE,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQlB,KAAKM,OACzC,IAAIyB,EAAI/B,KAAKC,MACTkB,EAAI,EAER,KAAa,OAANY,GACLhB,EAAOI,KAAOY,EAAEvB,IAChBuB,EAAIA,EAAEnB,KAGR,OAAOG,CACT,CAmBA,cAAAiB,CAAexB,EAAKY,EAAOrB,EAAWC,KAAKD,UACzC,IAAIkC,EAAU,KACVxB,EAAOT,KAAKE,MAAMM,GAmCtB,YAjCaE,IAATD,GACFA,EAAKW,MAAQA,EACTrB,IACFU,EAAKe,OAASxB,KAAKF,IAAM,EAAI4B,KAAKC,MAAQ3B,KAAKF,IAAME,KAAKF,KAE5DE,KAAK6B,UAAUpB,KAEXT,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,MACrCoC,EAAU,CACRzB,IAAKR,KAAKC,MAAMO,IAChBY,MAAOpB,KAAKC,MAAMmB,MAClBI,OAAQxB,KAAKC,MAAMuB,QAErBxB,KAAKqB,OAAM,IAGbZ,EAAOT,KAAKE,MAAMM,GAAO,CACvBgB,OAAQxB,KAAKF,IAAM,EAAI4B,KAAKC,MAAQ3B,KAAKF,IAAME,KAAKF,IACpDU,IAAKA,EACLG,KAAMX,KAAKK,KACXO,KAAM,KACNQ,SAGkB,KAAdpB,KAAKM,KACTN,KAAKC,MAAQQ,EAEbT,KAAKK,KAAKO,KAAOH,EAGnBT,KAAKK,KAAOI,GAGPwB,CACT,CAoBA,GAAAC,CAAI1B,EAAKY,EAAOE,GAAS,EAAOvB,EAAWC,KAAKD,UAC9C,IAAIU,EAAOT,KAAKE,MAAMM,GAmCtB,OAjCIc,QAAmBZ,IAATD,GAEZA,EAAKW,MAAQA,GAEE,IAAXE,GAAoBvB,IACtBU,EAAKe,OAASxB,KAAKF,IAAM,EAAI4B,KAAKC,MAAQ3B,KAAKF,IAAME,KAAKF,KAI5DE,KAAK6B,UAAUpB,KAGXT,KAAKH,IAAM,GAAKG,KAAKM,OAASN,KAAKH,KACrCG,KAAKqB,OAAM,GAGbZ,EAAOT,KAAKE,MAAMM,GAAO,CACvBgB,OAAQxB,KAAKF,IAAM,EAAI4B,KAAKC,MAAQ3B,KAAKF,IAAME,KAAKF,IACpDU,IAAKA,EACLG,KAAMX,KAAKK,KACXO,KAAM,KACNQ,SAGkB,KAAdpB,KAAKM,KACTN,KAAKC,MAAQQ,EAEbT,KAAKK,KAAKO,KAAOH,EAGnBT,KAAKK,KAAOI,GAGPT,IACT,CAkBA,MAAAmC,CAAOrB,QACQJ,IAATI,IACFA,EAAOd,KAAKc,QAGd,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACpC,MAAMV,EAAOT,KAAKE,MAAMY,EAAKK,IAC7BJ,EAAOI,QAAcT,IAATD,EAAqBA,EAAKW,WAAQV,CAChD,CAEA,OAAOK,CACT,EA2BK,SAASqB,EAAIvC,EAAM,IAAMC,EAAM,EAAGC,GAAW,GAClD,GAAIsC,MAAMxC,IAAQA,EAAM,EACtB,MAAM,IAAIyC,UAAU,qBAGtB,GAAID,MAAMvC,IAAQA,EAAM,EACtB,MAAM,IAAIwC,UAAU,qBAGtB,GAAwB,kBAAbvC,EACT,MAAM,IAAIuC,UAAU,0BAGtB,OAAO,IAAI3C,EAAIE,EAAKC,EAAKC,EAC3B,QAAAJ,SAAAyC"} \ No newline at end of file diff --git a/dist/tiny-lru.umd.js b/dist/tiny-lru.umd.js index 9222cf9..6d40874 100644 --- a/dist/tiny-lru.umd.js +++ b/dist/tiny-lru.umd.js @@ -24,432 +24,443 @@ * // After 5 seconds, key1 will be expired */ 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 accessing existing items via get(). - * @example - * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access - * @see {@link lru} For parameter validation - * @since 1.0.0 - */ - constructor (max = 0, ttl = 0, resetTtl = false) { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.max = max; - this.resetTtl = resetTtl; - this.size = 0; - this.ttl = ttl; - } - - /** - * Removes all items from the cache. - * - * @method clear - * @memberof LRU - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.clear(); - * console.log(cache.size); // 0 - * @since 1.0.0 - */ - clear () { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.size = 0; - - return this; - } - - /** - * Removes an item from the cache by key. - * - * @method delete - * @memberof LRU - * @param {string} key - The key of the item to delete. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1'); - * cache.delete('key1'); - * console.log(cache.has('key1')); // false - * @see {@link LRU#has} - * @see {@link LRU#clear} - * @since 1.0.0 - */ - delete (key) { - if (this.has(key)) { - const item = this.items[key]; - - delete this.items[key]; - this.size--; - - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - if (this.first === item) { - this.first = item.next; - } - - if (this.last === item) { - this.last = item.prev; - } - } - - return this; - } - - /** - * Returns an array of [key, value] pairs for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method entries - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. - * @returns {Array>} Array of [key, value] pairs in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.entries()); // [['a', 1], ['b', 2]] - * console.log(cache.entries(['a'])); // [['a', 1]] - * @see {@link LRU#keys} - * @see {@link LRU#values} - * @since 11.1.0 - */ - entries (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - result[i] = [key, this.get(key)]; - } - - return result; - } - - /** - * Removes the least recently used item from the cache. - * - * @method evict - * @memberof LRU - * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('old', 'value').set('new', 'value'); - * cache.evict(); // Removes 'old' item - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - evict (bypass = false) { - if (bypass || this.size > 0) { - const item = this.first; - - delete this.items[item.key]; - - if (--this.size === 0) { - this.first = null; - this.last = null; - } else { - this.first = item.next; - this.first.prev = null; - } - } - - return this; - } - - /** - * Returns the expiration timestamp for a given key. - * - * @method expiresAt - * @memberof LRU - * @param {string} key - The key to check expiration for. - * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. - * @example - * const cache = new LRU(100, 5000); // 5 second TTL - * cache.set('key1', 'value1'); - * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now - * @see {@link LRU#get} - * @see {@link LRU#has} - * @since 1.0.0 - */ - expiresAt (key) { - let result; - - if (this.has(key)) { - result = this.items[key].expiry; - } - - return result; - } - - /** - * Retrieves a value from the cache by key. Updates the item's position to most recently used. - * - * @method get - * @memberof LRU - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found or expired. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.get('key1')); // 'value1' - * console.log(cache.get('nonexistent')); // undefined - * @see {@link LRU#set} - * @see {@link LRU#has} - * @since 1.0.0 - */ - get (key) { - const item = this.items[key]; - - if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - - return undefined; - } - } - - // Fast LRU update without full set() overhead - this.moveToEnd(item); - - return item.value; - } - - return undefined; - } - - /** - * Checks if a key exists in the cache. - * - * @method has - * @memberof LRU - * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.has('key1')); // true - * console.log(cache.has('nonexistent')); // false - * @see {@link LRU#get} - * @see {@link LRU#delete} - * @since 9.0.0 - */ - has (key) { - return key in this.items; - } - - /** - * Efficiently moves an item to the end of the LRU list (most recently used position). - * This is an internal optimization method that avoids the overhead of the full set() operation - * when only LRU position needs to be updated. - * - * @method moveToEnd - * @memberof LRU - * @param {Object} item - The cache item with prev/next pointers to reposition. - * @private - * @since 11.3.5 - */ - moveToEnd (item) { - // If already at the end, nothing to do - if (this.last === item) { - return; - } - - // Remove item from current position in the list - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - // Update first pointer if this was the first item - if (this.first === item) { - this.first = item.next; - } - - // Add item to the end - item.prev = this.last; - item.next = null; - - if (this.last !== null) { - this.last.next = item; - } - - this.last = item; - - // Handle edge case: if this was the only item, it's also first - if (this.first === null) { - this.first = item; - } - } - - /** - * Returns an array of all keys in the cache, ordered from least to most recently used. - * - * @method keys - * @memberof LRU - * @returns {string[]} Array of keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * cache.get('a'); // Move 'a' to most recent - * console.log(cache.keys()); // ['b', 'a'] - * @see {@link LRU#values} - * @see {@link LRU#entries} - * @since 9.0.0 - */ - keys () { - const result = new Array(this.size); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; - } - - return result; - } - - /** - * Sets a value in the cache and returns any evicted item. - * - * @method setWithEvicted - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. - * @example - * const cache = new LRU(2); - * cache.set('a', 1).set('b', 2); - * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} - * @see {@link LRU#set} - * @see {@link LRU#evict} - * @since 11.3.0 - */ - setWithEvicted (key, value, resetTtl = this.resetTtl) { - let evicted = null; - - if (this.has(key)) { - this.set(key, value, true, resetTtl); - } else { - if (this.max > 0 && this.size === this.max) { - evicted = {...this.first}; - this.evict(true); - } - - let item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return evicted; - } - - /** - * Sets a value in the cache. Updates the item's position to most recently used. - * - * @method set - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1') - * .set('key2', 'value2') - * .set('key3', 'value3'); - * @see {@link LRU#get} - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - set (key, value, bypass = false, resetTtl = this.resetTtl) { - let item = this.items[key]; - - if (bypass || item !== undefined) { - // Existing item: update value and position - item.value = value; - - if (bypass === false && resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - - // Always move to end, but the bypass parameter affects TTL reset behavior - this.moveToEnd(item); - } else { - // New item: check for eviction and create - if (this.max > 0 && this.size === this.max) { - this.evict(true); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return this; - } - - /** - * Returns an array of all values in the cache for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method values - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. - * @returns {Array<*>} Array of values corresponding to the keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.values()); // [1, 2] - * console.log(cache.values(['a'])); // [1] - * @see {@link LRU#keys} - * @see {@link LRU#entries} - * @since 11.1.0 - */ - values (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - result[i] = this.get(keys[i]); - } - - return result; - } + /** + * 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 accessing existing items via get(). + * @example + * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access + * @see {@link lru} For parameter validation + * @since 1.0.0 + */ + constructor(max = 0, ttl = 0, resetTtl = false) { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.max = max; + this.resetTtl = resetTtl; + this.size = 0; + this.ttl = ttl; + } + + /** + * Removes all items from the cache. + * + * @method clear + * @memberof LRU + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.clear(); + * console.log(cache.size); // 0 + * @since 1.0.0 + */ + clear() { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.size = 0; + + return this; + } + + /** + * Removes an item from the cache by key. + * + * @method delete + * @memberof LRU + * @param {string} key - The key of the item to delete. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1'); + * cache.delete('key1'); + * console.log(cache.has('key1')); // false + * @see {@link LRU#has} + * @see {@link LRU#clear} + * @since 1.0.0 + */ + delete(key) { + const item = this.items[key]; + + if (item !== undefined) { + delete this.items[key]; + this.size--; + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + if (this.last === item) { + this.last = item.prev; + } + + item.prev = null; + item.next = null; + } + + return this; + } + + /** + * Returns an array of [key, value] pairs for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method entries + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. + * @returns {Array>} Array of [key, value] pairs in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.entries()); // [['a', 1], ['b', 2]] + * console.log(cache.entries(['a'])); // [['a', 1]] + * @see {@link LRU#keys} + * @see {@link LRU#values} + * @since 11.1.0 + */ + entries(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const item = this.items[key]; + result[i] = [key, item !== undefined ? item.value : undefined]; + } + + return result; + } + + /** + * Removes the least recently used item from the cache. + * + * @method evict + * @memberof LRU + * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('old', 'value').set('new', 'value'); + * cache.evict(); // Removes 'old' item + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + evict(bypass = false) { + if (bypass || this.size > 0) { + const item = this.first; + + if (!item) { + return this; + } + + delete this.items[item.key]; + + if (--this.size === 0) { + this.first = null; + this.last = null; + } else { + this.first = item.next; + this.first.prev = null; + } + + item.next = null; + } + + return this; + } + + /** + * Returns the expiration timestamp for a given key. + * + * @method expiresAt + * @memberof LRU + * @param {string} key - The key to check expiration for. + * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. + * @example + * const cache = new LRU(100, 5000); // 5 second TTL + * cache.set('key1', 'value1'); + * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now + * @see {@link LRU#get} + * @see {@link LRU#has} + * @since 1.0.0 + */ + expiresAt(key) { + const item = this.items[key]; + return item !== undefined ? item.expiry : undefined; + } + + /** + * Retrieves a value from the cache by key. Updates the item's position to most recently used. + * + * @method get + * @memberof LRU + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found or expired. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.get('key1')); // 'value1' + * console.log(cache.get('nonexistent')); // undefined + * @see {@link LRU#set} + * @see {@link LRU#has} + * @since 1.0.0 + */ + get(key) { + const item = this.items[key]; + + if (item !== undefined) { + // Check TTL only if enabled to avoid unnecessary Date.now() calls + if (this.ttl > 0) { + if (item.expiry <= Date.now()) { + this.delete(key); + + return undefined; + } + } + + // Fast LRU update without full set() overhead + this.moveToEnd(item); + + return item.value; + } + + return undefined; + } + + /** + * Checks if a key exists in the cache. + * + * @method has + * @memberof LRU + * @param {string} key - The key to check for. + * @returns {boolean} True if the key exists, false otherwise. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.has('key1')); // true + * console.log(cache.has('nonexistent')); // false + * @see {@link LRU#get} + * @see {@link LRU#delete} + * @since 9.0.0 + */ + has(key) { + const item = this.items[key]; + return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + } + + /** + * Efficiently moves an item to the end of the LRU list (most recently used position). + * This is an internal optimization method that avoids the overhead of the full set() operation + * when only LRU position needs to be updated. + * + * @method moveToEnd + * @memberof LRU + * @param {Object} item - The cache item with prev/next pointers to reposition. + * @private + * @since 11.3.5 + */ + moveToEnd(item) { + if (this.last === item) { + return; + } + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + item.prev = this.last; + item.next = null; + this.last.next = item; + this.last = item; + } + + /** + * Returns an array of all keys in the cache, ordered from least to most recently used. + * + * @method keys + * @memberof LRU + * @returns {string[]} Array of keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * cache.get('a'); // Move 'a' to most recent + * console.log(cache.keys()); // ['b', 'a'] + * @see {@link LRU#values} + * @see {@link LRU#entries} + * @since 9.0.0 + */ + keys() { + const result = Array.from({ length: this.size }); + let x = this.first; + let i = 0; + + while (x !== null) { + result[i++] = x.key; + x = x.next; + } + + return result; + } + + /** + * Sets a value in the cache and returns any evicted item. + * + * @method setWithEvicted + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. + * @example + * const cache = new LRU(2); + * cache.set('a', 1).set('b', 2); + * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} + * @see {@link LRU#set} + * @see {@link LRU#evict} + * @since 11.3.0 + */ + setWithEvicted(key, value, resetTtl = this.resetTtl) { + let evicted = null; + let item = this.items[key]; + + if (item !== undefined) { + item.value = value; + if (resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + this.moveToEnd(item); + } else { + if (this.max > 0 && this.size === this.max) { + evicted = { + key: this.first.key, + value: this.first.value, + expiry: this.first.expiry, + }; + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return evicted; + } + + /** + * Sets a value in the cache. Updates the item's position to most recently used. + * + * @method set + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1') + * .set('key2', 'value2') + * .set('key3', 'value3'); + * @see {@link LRU#get} + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + set(key, value, bypass = false, resetTtl = this.resetTtl) { + let item = this.items[key]; + + if (bypass || item !== undefined) { + // Existing item: update value and position + item.value = value; + + if (bypass === false && resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + + // Always move to end, but the bypass parameter affects TTL reset behavior + this.moveToEnd(item); + } else { + // New item: check for eviction and create + if (this.max > 0 && this.size === this.max) { + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return this; + } + + /** + * Returns an array of all values in the cache for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method values + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @returns {Array<*>} Array of values corresponding to the keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.values()); // [1, 2] + * console.log(cache.values(['a'])); // [1] + * @see {@link LRU#keys} + * @see {@link LRU#entries} + * @since 11.1.0 + */ + values(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + result[i] = item !== undefined ? item.value : undefined; + } + + return result; + } } /** @@ -476,18 +487,18 @@ class LRU { * @see {@link LRU} * @since 1.0.0 */ -function lru (max = 1000, ttl = 0, resetTtl = false) { - if (isNaN(max) || max < 0) { - throw new TypeError("Invalid max value"); - } +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 (isNaN(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); - } + if (typeof resetTtl !== "boolean") { + throw new TypeError("Invalid resetTtl value"); + } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTtl); }exports.LRU=LRU;exports.lru=lru;})); \ No newline at end of file diff --git a/dist/tiny-lru.umd.min.js b/dist/tiny-lru.umd.min.js index 8406124..d2b0b70 100644 --- a/dist/tiny-lru.umd.min.js +++ b/dist/tiny-lru.umd.min.js @@ -2,4 +2,4 @@ 2026 Jason Mulligan @version 11.4.7 */ -!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports):"function"==typeof define&&define.amd?define(["exports"],s):s((t="undefined"!=typeof globalThis?globalThis:t||self).lru={})}(this,(function(t){"use strict";class s{constructor(t=0,s=0,e=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=e,this.size=0,this.ttl=s}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){if(this.has(t)){const s=this.items[t];delete this.items[t],this.size--,null!==s.prev&&(s.prev.next=s.next),null!==s.next&&(s.next.prev=s.prev),this.first===s&&(this.first=s.next),this.last===s&&(this.last=s.prev)}return this}entries(t=this.keys()){const s=new Array(t.length);for(let e=0;e0){const t=this.first;delete this.items[t.key],0==--this.size?(this.first=null,this.last=null):(this.first=t.next,this.first.prev=null)}return this}expiresAt(t){let s;return this.has(t)&&(s=this.items[t].expiry),s}get(t){const s=this.items[t];if(void 0!==s)return this.ttl>0&&s.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(s),s.value)}has(t){return t in this.items}moveToEnd(t){this.last!==t&&(null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),t.prev=this.last,t.next=null,null!==this.last&&(this.last.next=t),this.last=t,null===this.first&&(this.first=t))}keys(){const t=new Array(this.size);let s=this.first,e=0;for(;null!==s;)t[e++]=s.key,s=s.next;return t}setWithEvicted(t,s,e=this.resetTtl){let i=null;if(this.has(t))this.set(t,s,!0,e);else{this.max>0&&this.size===this.max&&(i={...this.first},this.evict(!0));let 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}return i}set(t,s,e=!1,i=this.resetTtl){let l=this.items[t];return e||void 0!==l?(l.value=s,!1===e&&i&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&this.evict(!0),l=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=l:this.last.next=l,this.last=l),this}values(t=this.keys()){const s=new Array(t.length);for(let e=0;e0){const t=this.first;if(!t)return this;delete this.items[t.key],0==--this.size?(this.first=null,this.last=null):(this.first=t.next,this.first.prev=null),t.next=null}return this}expiresAt(t){const e=this.items[t];return void 0!==e?e.expiry:void 0}get(t){const e=this.items[t];if(void 0!==e)return this.ttl>0&&e.expiry<=Date.now()?void this.delete(t):(this.moveToEnd(e),e.value)}has(t){const e=this.items[t];return void 0!==e&&(0===this.ttl||e.expiry>Date.now())}moveToEnd(t){this.last!==t&&(null!==t.prev&&(t.prev.next=t.next),null!==t.next&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),t.prev=this.last,t.next=null,this.last.next=t,this.last=t)}keys(){const t=Array.from({length:this.size});let e=this.first,i=0;for(;null!==e;)t[i++]=e.key,e=e.next;return t}setWithEvicted(t,e,i=this.resetTtl){let s=null,l=this.items[t];return void 0!==l?(l.value=e,i&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&(s={key:this.first.key,value:this.first.value,expiry:this.first.expiry},this.evict(!0)),l=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=l:this.last.next=l,this.last=l),s}set(t,e,i=!1,s=this.resetTtl){let l=this.items[t];return i||void 0!==l?(l.value=e,!1===i&&s&&(l.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(l)):(this.max>0&&this.size===this.max&&this.evict(!0),l=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},1==++this.size?this.first=l:this.last.next=l,this.last=l),this}values(t){void 0===t&&(t=this.keys());const e=Array.from({length:t.length});for(let i=0;i>} Array of [key, value] pairs in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * console.log(cache.entries()); // [['a', 1], ['b', 2]]\n\t * console.log(cache.entries(['a'])); // [['a', 1]]\n\t * @see {@link LRU#keys}\n\t * @see {@link LRU#values}\n\t * @since 11.1.0\n\t */\n\tentries (keys = this.keys()) {\n\t\tconst result = new Array(keys.length);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tconst key = keys[i];\n\t\t\tresult[i] = [key, this.get(key)];\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 * @method evict\n\t * @memberof LRU\n\t * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t * @example\n\t * cache.set('old', 'value').set('new', 'value');\n\t * cache.evict(); // Removes 'old' item\n\t * @see {@link LRU#setWithEvicted}\n\t * @since 1.0.0\n\t */\n\tevict (bypass = false) {\n\t\tif (bypass || this.size > 0) {\n\t\t\tconst item = this.first;\n\n\t\t\tdelete this.items[item.key];\n\n\t\t\tif (--this.size === 0) {\n\t\t\t\tthis.first = null;\n\t\t\t\tthis.last = null;\n\t\t\t} else {\n\t\t\t\tthis.first = item.next;\n\t\t\t\tthis.first.prev = null;\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 * @method expiresAt\n\t * @memberof LRU\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 * @example\n\t * const cache = new LRU(100, 5000); // 5 second TTL\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#has}\n\t * @since 1.0.0\n\t */\n\texpiresAt (key) {\n\t\tlet result;\n\n\t\tif (this.has(key)) {\n\t\t\tresult = this.items[key].expiry;\n\t\t}\n\n\t\treturn result;\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 * @method get\n\t * @memberof LRU\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 * @example\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.get('key1')); // 'value1'\n\t * console.log(cache.get('nonexistent')); // undefined\n\t * @see {@link LRU#set}\n\t * @see {@link LRU#has}\n\t * @since 1.0.0\n\t */\n\tget (key) {\n\t\tconst item = this.items[key];\n\n\t\tif (item !== undefined) {\n\t\t\t// Check TTL only if enabled to avoid unnecessary Date.now() calls\n\t\t\tif (this.ttl > 0) {\n\t\t\t\tif (item.expiry <= Date.now()) {\n\t\t\t\t\tthis.delete(key);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fast LRU update without full set() overhead\n\t\t\tthis.moveToEnd(item);\n\n\t\t\treturn item.value;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a key exists in the cache.\n\t *\n\t * @method has\n\t * @memberof LRU\n\t * @param {string} key - The key to check for.\n\t * @returns {boolean} True if the key exists, false otherwise.\n\t * @example\n\t * cache.set('key1', 'value1');\n\t * console.log(cache.has('key1')); // true\n\t * console.log(cache.has('nonexistent')); // false\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#delete}\n\t * @since 9.0.0\n\t */\n\thas (key) {\n\t\treturn key in this.items;\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 * @method moveToEnd\n\t * @memberof LRU\n\t * @param {Object} item - The cache item with prev/next pointers to reposition.\n\t * @private\n\t * @since 11.3.5\n\t */\n\tmoveToEnd (item) {\n\t\t// If already at the end, nothing to do\n\t\tif (this.last === item) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove item from current position in the list\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\t// Update first pointer if this was the first item\n\t\tif (this.first === item) {\n\t\t\tthis.first = item.next;\n\t\t}\n\n\t\t// Add item to the end\n\t\titem.prev = this.last;\n\t\titem.next = null;\n\n\t\tif (this.last !== null) {\n\t\t\tthis.last.next = item;\n\t\t}\n\n\t\tthis.last = item;\n\n\t\t// Handle edge case: if this was the only item, it's also first\n\t\tif (this.first === null) {\n\t\t\tthis.first = item;\n\t\t}\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 * @method keys\n\t * @memberof LRU\n\t * @returns {string[]} Array of keys in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * cache.get('a'); // Move 'a' to most recent\n\t * console.log(cache.keys()); // ['b', 'a']\n\t * @see {@link LRU#values}\n\t * @see {@link LRU#entries}\n\t * @since 9.0.0\n\t */\n\tkeys () {\n\t\tconst result = new Array(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 * @method setWithEvicted\n\t * @memberof LRU\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n\t * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null.\n\t * @example\n\t * const cache = new LRU(2);\n\t * cache.set('a', 1).set('b', 2);\n\t * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...}\n\t * @see {@link LRU#set}\n\t * @see {@link LRU#evict}\n\t * @since 11.3.0\n\t */\n\tsetWithEvicted (key, value, resetTtl = this.resetTtl) {\n\t\tlet evicted = null;\n\n\t\tif (this.has(key)) {\n\t\t\tthis.set(key, value, true, resetTtl);\n\t\t} else {\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tevicted = {...this.first};\n\t\t\t\tthis.evict(true);\n\t\t\t}\n\n\t\t\tlet item = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn evicted;\n\t}\n\n\t/**\n\t * Sets a value in the cache. Updates the item's position to most recently used.\n\t *\n\t * @method set\n\t * @memberof LRU\n\t * @param {string} key - The key to set.\n\t * @param {*} value - The value to store.\n\t * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method.\n\t * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n\t * @returns {LRU} The LRU instance for method chaining.\n\t * @example\n\t * cache.set('key1', 'value1')\n\t * .set('key2', 'value2')\n\t * .set('key3', 'value3');\n\t * @see {@link LRU#get}\n\t * @see {@link LRU#setWithEvicted}\n\t * @since 1.0.0\n\t */\n\tset (key, value, bypass = false, resetTtl = this.resetTtl) {\n\t\tlet item = this.items[key];\n\n\t\tif (bypass || item !== undefined) {\n\t\t\t// Existing item: update value and position\n\t\t\titem.value = value;\n\n\t\t\tif (bypass === false && resetTtl) {\n\t\t\t\titem.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n\t\t\t}\n\n\t\t\t// Always move to end, but the bypass parameter affects TTL reset behavior\n\t\t\tthis.moveToEnd(item);\n\t\t} else {\n\t\t\t// New item: check for eviction and create\n\t\t\tif (this.max > 0 && this.size === this.max) {\n\t\t\t\tthis.evict(true);\n\t\t\t}\n\n\t\t\titem = this.items[key] = {\n\t\t\t\texpiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n\t\t\t\tkey: key,\n\t\t\t\tprev: this.last,\n\t\t\t\tnext: null,\n\t\t\t\tvalue\n\t\t\t};\n\n\t\t\tif (++this.size === 1) {\n\t\t\t\tthis.first = item;\n\t\t\t} else {\n\t\t\t\tthis.last.next = item;\n\t\t\t}\n\n\t\t\tthis.last = item;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns an array of all values in the cache for the specified keys.\n\t * Order follows LRU order (least to most recently used).\n\t *\n\t * @method values\n\t * @memberof LRU\n\t * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n\t * @returns {Array<*>} Array of values corresponding to the keys in LRU order.\n\t * @example\n\t * cache.set('a', 1).set('b', 2);\n\t * console.log(cache.values()); // [1, 2]\n\t * console.log(cache.values(['a'])); // [1]\n\t * @see {@link LRU#keys}\n\t * @see {@link LRU#entries}\n\t * @since 11.1.0\n\t */\n\tvalues (keys = this.keys()) {\n\t\tconst result = new Array(keys.length);\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tresult[i] = this.get(keys[i]);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n * @example\n * // Create cache with factory function\n * const cache = lru(100, 5000, true);\n * cache.set('key', 'value');\n *\n * @example\n * // Error handling\n * try {\n * const cache = lru(-1); // Invalid max\n * } catch (error) {\n * console.error(error.message); // \"Invalid max value\"\n * }\n * @see {@link LRU}\n * @since 1.0.0\n */\nexport function lru (max = 1000, ttl = 0, resetTtl = false) {\n\tif (isNaN(max) || max < 0) {\n\t\tthrow new TypeError(\"Invalid max value\");\n\t}\n\n\tif (isNaN(ttl) || ttl < 0) {\n\t\tthrow new TypeError(\"Invalid ttl value\");\n\t}\n\n\tif (typeof resetTtl !== \"boolean\") {\n\t\tthrow new TypeError(\"Invalid resetTtl value\");\n\t}\n\n\treturn new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","clear","key","has","item","prev","next","entries","keys","result","Array","length","i","get","evict","bypass","expiresAt","expiry","undefined","Date","now","delete","moveToEnd","value","x","setWithEvicted","evicted","set","values","isNaN","TypeError"],"mappings":";;;;CAAA,SAAAA,EAAAC,GAAA,iBAAAC,SAAA,oBAAAC,OAAAF,EAAAC,SAAA,mBAAAE,QAAAA,OAAAC,IAAAD,OAAA,CAAA,WAAAH,GAAAA,GAAAD,EAAA,oBAAAM,WAAAA,WAAAN,GAAAO,MAAAC,IAAA,CAAA,EAAA,CAAA,CAAAC,MAAA,SAAAP,GAAA,aAkBO,MAAMQ,EAcZ,WAAAC,CAAaC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACzCL,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKG,IAAMA,EACXH,KAAKK,SAAWA,EAChBL,KAAKW,KAAO,EACZX,KAAKI,IAAMA,CACZ,CAaA,KAAAQ,GAMC,OALAZ,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKW,KAAO,EAELX,IACR,CAiBA,OAAQa,GACP,GAAIb,KAAKc,IAAID,GAAM,CAClB,MAAME,EAAOf,KAAKO,MAAMM,UAEjBb,KAAKO,MAAMM,GAClBb,KAAKW,OAEa,OAAdI,EAAKC,OACRD,EAAKC,KAAKC,KAAOF,EAAKE,MAGL,OAAdF,EAAKE,OACRF,EAAKE,KAAKD,KAAOD,EAAKC,MAGnBhB,KAAKM,QAAUS,IAClBf,KAAKM,MAAQS,EAAKE,MAGfjB,KAAKU,OAASK,IACjBf,KAAKU,KAAOK,EAAKC,KAEnB,CAEA,OAAOhB,IACR,CAkBA,OAAAkB,CAASC,EAAOnB,KAAKmB,QACpB,MAAMC,EAAS,IAAIC,MAAMF,EAAKG,QAC9B,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAAKG,OAAQC,IAAK,CACrC,MAAMV,EAAMM,EAAKI,GACjBH,EAAOG,GAAK,CAACV,EAAKb,KAAKwB,IAAIX,GAC5B,CAEA,OAAOO,CACR,CAeA,KAAAK,CAAOC,GAAS,GACf,GAAIA,GAAU1B,KAAKW,KAAO,EAAG,CAC5B,MAAMI,EAAOf,KAAKM,aAEXN,KAAKO,MAAMQ,EAAKF,KAEH,KAAdb,KAAKW,MACVX,KAAKM,MAAQ,KACbN,KAAKU,KAAO,OAEZV,KAAKM,MAAQS,EAAKE,KAClBjB,KAAKM,MAAMU,KAAO,KAEpB,CAEA,OAAOhB,IACR,CAiBA,SAAA2B,CAAWd,GACV,IAAIO,EAMJ,OAJIpB,KAAKc,IAAID,KACZO,EAASpB,KAAKO,MAAMM,GAAKe,QAGnBR,CACR,CAiBA,GAAAI,CAAKX,GACJ,MAAME,EAAOf,KAAKO,MAAMM,GAExB,QAAagB,IAATd,EAEH,OAAIf,KAAKI,IAAM,GACVW,EAAKa,QAAUE,KAAKC,WACvB/B,KAAKgC,OAAOnB,IAOdb,KAAKiC,UAAUlB,GAERA,EAAKmB,MAId,CAiBA,GAAApB,CAAKD,GACJ,OAAOA,KAAOb,KAAKO,KACpB,CAaA,SAAA0B,CAAWlB,GAENf,KAAKU,OAASK,IAKA,OAAdA,EAAKC,OACRD,EAAKC,KAAKC,KAAOF,EAAKE,MAGL,OAAdF,EAAKE,OACRF,EAAKE,KAAKD,KAAOD,EAAKC,MAInBhB,KAAKM,QAAUS,IAClBf,KAAKM,MAAQS,EAAKE,MAInBF,EAAKC,KAAOhB,KAAKU,KACjBK,EAAKE,KAAO,KAEM,OAAdjB,KAAKU,OACRV,KAAKU,KAAKO,KAAOF,GAGlBf,KAAKU,KAAOK,EAGO,OAAff,KAAKM,QACRN,KAAKM,MAAQS,GAEf,CAgBA,IAAAI,GACC,MAAMC,EAAS,IAAIC,MAAMrB,KAAKW,MAC9B,IAAIwB,EAAInC,KAAKM,MACTiB,EAAI,EAER,KAAa,OAANY,GACNf,EAAOG,KAAOY,EAAEtB,IAChBsB,EAAIA,EAAElB,KAGP,OAAOG,CACR,CAmBA,cAAAgB,CAAgBvB,EAAKqB,EAAO7B,EAAWL,KAAKK,UAC3C,IAAIgC,EAAU,KAEd,GAAIrC,KAAKc,IAAID,GACZb,KAAKsC,IAAIzB,EAAKqB,GAAO,EAAM7B,OACrB,CACFL,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,MACtCkC,EAAU,IAAIrC,KAAKM,OACnBN,KAAKyB,OAAM,IAGZ,IAAIV,EAAOf,KAAKO,MAAMM,GAAO,CAC5Be,OAAQ5B,KAAKI,IAAM,EAAI0B,KAAKC,MAAQ/B,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLG,KAAMhB,KAAKU,KACXO,KAAM,KACNiB,SAGmB,KAAdlC,KAAKW,KACVX,KAAKM,MAAQS,EAEbf,KAAKU,KAAKO,KAAOF,EAGlBf,KAAKU,KAAOK,CACb,CAEA,OAAOsB,CACR,CAoBA,GAAAC,CAAKzB,EAAKqB,EAAOR,GAAS,EAAOrB,EAAWL,KAAKK,UAChD,IAAIU,EAAOf,KAAKO,MAAMM,GAmCtB,OAjCIa,QAAmBG,IAATd,GAEbA,EAAKmB,MAAQA,GAEE,IAAXR,GAAoBrB,IACvBU,EAAKa,OAAS5B,KAAKI,IAAM,EAAI0B,KAAKC,MAAQ/B,KAAKI,IAAMJ,KAAKI,KAI3DJ,KAAKiC,UAAUlB,KAGXf,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,KACtCH,KAAKyB,OAAM,GAGZV,EAAOf,KAAKO,MAAMM,GAAO,CACxBe,OAAQ5B,KAAKI,IAAM,EAAI0B,KAAKC,MAAQ/B,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLG,KAAMhB,KAAKU,KACXO,KAAM,KACNiB,SAGmB,KAAdlC,KAAKW,KACVX,KAAKM,MAAQS,EAEbf,KAAKU,KAAKO,KAAOF,EAGlBf,KAAKU,KAAOK,GAGNf,IACR,CAkBA,MAAAuC,CAAQpB,EAAOnB,KAAKmB,QACnB,MAAMC,EAAS,IAAIC,MAAMF,EAAKG,QAC9B,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAAKG,OAAQC,IAChCH,EAAOG,GAAKvB,KAAKwB,IAAIL,EAAKI,IAG3B,OAAOH,CACR,EAyCD3B,EAAAQ,IAAAA,EAAAR,EAAAM,IAdO,SAAcI,EAAM,IAAMC,EAAM,EAAGC,GAAW,GACpD,GAAImC,MAAMrC,IAAQA,EAAM,EACvB,MAAM,IAAIsC,UAAU,qBAGrB,GAAID,MAAMpC,IAAQA,EAAM,EACvB,MAAM,IAAIqC,UAAU,qBAGrB,GAAwB,kBAAbpC,EACV,MAAM,IAAIoC,UAAU,0BAGrB,OAAO,IAAIxC,EAAIE,EAAKC,EAAKC,EAC1B,CAAA"} \ No newline at end of file +{"version":3,"file":"tiny-lru.umd.min.js","sources":["../src/lru.js"],"sourcesContent":["/**\n * A high-performance Least Recently Used (LRU) cache implementation with optional TTL support.\n * Items are automatically evicted when the cache reaches its maximum size,\n * removing the least recently used items first. All core operations (get, set, delete) are O(1).\n *\n * @class LRU\n * @example\n * // Create a cache with max 100 items\n * const cache = new LRU(100);\n * cache.set('key1', 'value1');\n * console.log(cache.get('key1')); // 'value1'\n *\n * @example\n * // Create a cache with TTL\n * const cache = new LRU(100, 5000); // 5 second TTL\n * cache.set('key1', 'value1');\n * // After 5 seconds, key1 will be expired\n */\nexport class LRU {\n /**\n * Creates a new LRU cache instance.\n * Note: Constructor does not validate parameters. Use lru() factory function for parameter validation.\n *\n * @constructor\n * @param {number} [max=0] - Maximum number of items to store. 0 means unlimited.\n * @param {number} [ttl=0] - Time to live in milliseconds. 0 means no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @example\n * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access\n * @see {@link lru} For parameter validation\n * @since 1.0.0\n */\n constructor(max = 0, ttl = 0, resetTtl = false) {\n this.first = null;\n this.items = Object.create(null);\n this.last = null;\n this.max = max;\n this.resetTtl = resetTtl;\n this.size = 0;\n this.ttl = ttl;\n }\n\n /**\n * Removes all items from the cache.\n *\n * @method clear\n * @memberof LRU\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.clear();\n * console.log(cache.size); // 0\n * @since 1.0.0\n */\n clear() {\n this.first = null;\n this.items = Object.create(null);\n this.last = null;\n this.size = 0;\n\n return this;\n }\n\n /**\n * Removes an item from the cache by key.\n *\n * @method delete\n * @memberof LRU\n * @param {string} key - The key of the item to delete.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('key1', 'value1');\n * cache.delete('key1');\n * console.log(cache.has('key1')); // false\n * @see {@link LRU#has}\n * @see {@link LRU#clear}\n * @since 1.0.0\n */\n delete(key) {\n const item = this.items[key];\n\n if (item !== undefined) {\n delete this.items[key];\n this.size--;\n\n if (item.prev !== null) {\n item.prev.next = item.next;\n }\n\n if (item.next !== null) {\n item.next.prev = item.prev;\n }\n\n if (this.first === item) {\n this.first = item.next;\n }\n\n if (this.last === item) {\n this.last = item.prev;\n }\n\n item.prev = null;\n item.next = null;\n }\n\n return this;\n }\n\n /**\n * Returns an array of [key, value] pairs for the specified keys.\n * Order follows LRU order (least to most recently used).\n *\n * @method entries\n * @memberof LRU\n * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys.\n * @returns {Array>} Array of [key, value] pairs in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * console.log(cache.entries()); // [['a', 1], ['b', 2]]\n * console.log(cache.entries(['a'])); // [['a', 1]]\n * @see {@link LRU#keys}\n * @see {@link LRU#values}\n * @since 11.1.0\n */\n entries(keys) {\n if (keys === undefined) {\n keys = this.keys();\n }\n\n const result = Array.from({ length: keys.length });\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const item = this.items[key];\n result[i] = [key, item !== undefined ? item.value : undefined];\n }\n\n return result;\n }\n\n /**\n * Removes the least recently used item from the cache.\n *\n * @method evict\n * @memberof LRU\n * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('old', 'value').set('new', 'value');\n * cache.evict(); // Removes 'old' item\n * @see {@link LRU#setWithEvicted}\n * @since 1.0.0\n */\n evict(bypass = false) {\n if (bypass || this.size > 0) {\n const item = this.first;\n\n if (!item) {\n return this;\n }\n\n delete this.items[item.key];\n\n if (--this.size === 0) {\n this.first = null;\n this.last = null;\n } else {\n this.first = item.next;\n this.first.prev = null;\n }\n\n item.next = null;\n }\n\n return this;\n }\n\n /**\n * Returns the expiration timestamp for a given key.\n *\n * @method expiresAt\n * @memberof LRU\n * @param {string} key - The key to check expiration for.\n * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist.\n * @example\n * const cache = new LRU(100, 5000); // 5 second TTL\n * cache.set('key1', 'value1');\n * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now\n * @see {@link LRU#get}\n * @see {@link LRU#has}\n * @since 1.0.0\n */\n expiresAt(key) {\n const item = this.items[key];\n return item !== undefined ? item.expiry : undefined;\n }\n\n /**\n * Retrieves a value from the cache by key. Updates the item's position to most recently used.\n *\n * @method get\n * @memberof LRU\n * @param {string} key - The key to retrieve.\n * @returns {*} The value associated with the key, or undefined if not found or expired.\n * @example\n * cache.set('key1', 'value1');\n * console.log(cache.get('key1')); // 'value1'\n * console.log(cache.get('nonexistent')); // undefined\n * @see {@link LRU#set}\n * @see {@link LRU#has}\n * @since 1.0.0\n */\n get(key) {\n const item = this.items[key];\n\n if (item !== undefined) {\n // Check TTL only if enabled to avoid unnecessary Date.now() calls\n if (this.ttl > 0) {\n if (item.expiry <= Date.now()) {\n this.delete(key);\n\n return undefined;\n }\n }\n\n // Fast LRU update without full set() overhead\n this.moveToEnd(item);\n\n return item.value;\n }\n\n return undefined;\n }\n\n /**\n * Checks if a key exists in the cache.\n *\n * @method has\n * @memberof LRU\n * @param {string} key - The key to check for.\n * @returns {boolean} True if the key exists, false otherwise.\n * @example\n * cache.set('key1', 'value1');\n * console.log(cache.has('key1')); // true\n * console.log(cache.has('nonexistent')); // false\n * @see {@link LRU#get}\n * @see {@link LRU#delete}\n * @since 9.0.0\n */\n has(key) {\n const item = this.items[key];\n return item !== undefined && (this.ttl === 0 || item.expiry > Date.now());\n }\n\n /**\n * Efficiently moves an item to the end of the LRU list (most recently used position).\n * This is an internal optimization method that avoids the overhead of the full set() operation\n * when only LRU position needs to be updated.\n *\n * @method moveToEnd\n * @memberof LRU\n * @param {Object} item - The cache item with prev/next pointers to reposition.\n * @private\n * @since 11.3.5\n */\n moveToEnd(item) {\n if (this.last === item) {\n return;\n }\n\n if (item.prev !== null) {\n item.prev.next = item.next;\n }\n\n if (item.next !== null) {\n item.next.prev = item.prev;\n }\n\n if (this.first === item) {\n this.first = item.next;\n }\n\n item.prev = this.last;\n item.next = null;\n this.last.next = item;\n this.last = item;\n }\n\n /**\n * Returns an array of all keys in the cache, ordered from least to most recently used.\n *\n * @method keys\n * @memberof LRU\n * @returns {string[]} Array of keys in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * cache.get('a'); // Move 'a' to most recent\n * console.log(cache.keys()); // ['b', 'a']\n * @see {@link LRU#values}\n * @see {@link LRU#entries}\n * @since 9.0.0\n */\n keys() {\n const result = Array.from({ length: this.size });\n let x = this.first;\n let i = 0;\n\n while (x !== null) {\n result[i++] = x.key;\n x = x.next;\n }\n\n return result;\n }\n\n /**\n * Sets a value in the cache and returns any evicted item.\n *\n * @method setWithEvicted\n * @memberof LRU\n * @param {string} key - The key to set.\n * @param {*} value - The value to store.\n * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null.\n * @example\n * const cache = new LRU(2);\n * cache.set('a', 1).set('b', 2);\n * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...}\n * @see {@link LRU#set}\n * @see {@link LRU#evict}\n * @since 11.3.0\n */\n setWithEvicted(key, value, resetTtl = this.resetTtl) {\n let evicted = null;\n let item = this.items[key];\n\n if (item !== undefined) {\n item.value = value;\n if (resetTtl) {\n item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n }\n this.moveToEnd(item);\n } else {\n if (this.max > 0 && this.size === this.max) {\n evicted = {\n key: this.first.key,\n value: this.first.value,\n expiry: this.first.expiry,\n };\n this.evict(true);\n }\n\n item = this.items[key] = {\n expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n key: key,\n prev: this.last,\n next: null,\n value,\n };\n\n if (++this.size === 1) {\n this.first = item;\n } else {\n this.last.next = item;\n }\n\n this.last = item;\n }\n\n return evicted;\n }\n\n /**\n * Sets a value in the cache. Updates the item's position to most recently used.\n *\n * @method set\n * @memberof LRU\n * @param {string} key - The key to set.\n * @param {*} value - The value to store.\n * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method.\n * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation.\n * @returns {LRU} The LRU instance for method chaining.\n * @example\n * cache.set('key1', 'value1')\n * .set('key2', 'value2')\n * .set('key3', 'value3');\n * @see {@link LRU#get}\n * @see {@link LRU#setWithEvicted}\n * @since 1.0.0\n */\n set(key, value, bypass = false, resetTtl = this.resetTtl) {\n let item = this.items[key];\n\n if (bypass || item !== undefined) {\n // Existing item: update value and position\n item.value = value;\n\n if (bypass === false && resetTtl) {\n item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;\n }\n\n // Always move to end, but the bypass parameter affects TTL reset behavior\n this.moveToEnd(item);\n } else {\n // New item: check for eviction and create\n if (this.max > 0 && this.size === this.max) {\n this.evict(true);\n }\n\n item = this.items[key] = {\n expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,\n key: key,\n prev: this.last,\n next: null,\n value,\n };\n\n if (++this.size === 1) {\n this.first = item;\n } else {\n this.last.next = item;\n }\n\n this.last = item;\n }\n\n return this;\n }\n\n /**\n * Returns an array of all values in the cache for the specified keys.\n * Order follows LRU order (least to most recently used).\n *\n * @method values\n * @memberof LRU\n * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys.\n * @returns {Array<*>} Array of values corresponding to the keys in LRU order.\n * @example\n * cache.set('a', 1).set('b', 2);\n * console.log(cache.values()); // [1, 2]\n * console.log(cache.values(['a'])); // [1]\n * @see {@link LRU#keys}\n * @see {@link LRU#entries}\n * @since 11.1.0\n */\n values(keys) {\n if (keys === undefined) {\n keys = this.keys();\n }\n\n const result = Array.from({ length: keys.length });\n for (let i = 0; i < keys.length; i++) {\n const item = this.items[keys[i]];\n result[i] = item !== undefined ? item.value : undefined;\n }\n\n return result;\n }\n}\n\n/**\n * Factory function to create a new LRU cache instance with parameter validation.\n *\n * @function lru\n * @param {number} [max=1000] - Maximum number of items to store. Must be >= 0. Use 0 for unlimited size.\n * @param {number} [ttl=0] - Time to live in milliseconds. Must be >= 0. Use 0 for no expiration.\n * @param {boolean} [resetTtl=false] - Whether to reset TTL when accessing existing items via get().\n * @returns {LRU} A new LRU cache instance.\n * @throws {TypeError} When parameters are invalid (negative numbers or wrong types).\n * @example\n * // Create cache with factory function\n * const cache = lru(100, 5000, true);\n * cache.set('key', 'value');\n *\n * @example\n * // Error handling\n * try {\n * const cache = lru(-1); // Invalid max\n * } catch (error) {\n * console.error(error.message); // \"Invalid max value\"\n * }\n * @see {@link LRU}\n * @since 1.0.0\n */\nexport function lru(max = 1000, ttl = 0, resetTtl = false) {\n if (isNaN(max) || max < 0) {\n throw new TypeError(\"Invalid max value\");\n }\n\n if (isNaN(ttl) || ttl < 0) {\n throw new TypeError(\"Invalid ttl value\");\n }\n\n if (typeof resetTtl !== \"boolean\") {\n throw new TypeError(\"Invalid resetTtl value\");\n }\n\n return new LRU(max, ttl, resetTtl);\n}\n"],"names":["g","f","exports","module","define","amd","globalThis","self","lru","this","LRU","constructor","max","ttl","resetTtl","first","items","Object","create","last","size","clear","key","item","undefined","prev","next","entries","keys","result","Array","from","length","i","value","evict","bypass","expiresAt","expiry","get","Date","now","delete","moveToEnd","has","x","setWithEvicted","evicted","set","values","isNaN","TypeError"],"mappings":";;;;CAAA,SAAAA,EAAAC,GAAA,iBAAAC,SAAA,oBAAAC,OAAAF,EAAAC,SAAA,mBAAAE,QAAAA,OAAAC,IAAAD,OAAA,CAAA,WAAAH,GAAAA,GAAAD,EAAA,oBAAAM,WAAAA,WAAAN,GAAAO,MAAAC,IAAA,CAAA,EAAA,CAAA,CAAAC,MAAA,SAAAP,GAAA,aAkBO,MAAMQ,EAcX,WAAAC,CAAYC,EAAM,EAAGC,EAAM,EAAGC,GAAW,GACvCL,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKG,IAAMA,EACXH,KAAKK,SAAWA,EAChBL,KAAKW,KAAO,EACZX,KAAKI,IAAMA,CACb,CAaA,KAAAQ,GAME,OALAZ,KAAKM,MAAQ,KACbN,KAAKO,MAAQC,OAAOC,OAAO,MAC3BT,KAAKU,KAAO,KACZV,KAAKW,KAAO,EAELX,IACT,CAiBA,OAAOa,GACL,MAAMC,EAAOd,KAAKO,MAAMM,GA0BxB,YAxBaE,IAATD,WACKd,KAAKO,MAAMM,GAClBb,KAAKW,OAEa,OAAdG,EAAKE,OACPF,EAAKE,KAAKC,KAAOH,EAAKG,MAGN,OAAdH,EAAKG,OACPH,EAAKG,KAAKD,KAAOF,EAAKE,MAGpBhB,KAAKM,QAAUQ,IACjBd,KAAKM,MAAQQ,EAAKG,MAGhBjB,KAAKU,OAASI,IAChBd,KAAKU,KAAOI,EAAKE,MAGnBF,EAAKE,KAAO,KACZF,EAAKG,KAAO,MAGPjB,IACT,CAkBA,OAAAkB,CAAQC,QACOJ,IAATI,IACFA,EAAOnB,KAAKmB,QAGd,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACpC,MAAMX,EAAMM,EAAKK,GACXV,EAAOd,KAAKO,MAAMM,GACxBO,EAAOI,GAAK,CAACX,OAAcE,IAATD,EAAqBA,EAAKW,WAAQV,EACtD,CAEA,OAAOK,CACT,CAeA,KAAAM,CAAMC,GAAS,GACb,GAAIA,GAAU3B,KAAKW,KAAO,EAAG,CAC3B,MAAMG,EAAOd,KAAKM,MAElB,IAAKQ,EACH,OAAOd,YAGFA,KAAKO,MAAMO,EAAKD,KAEH,KAAdb,KAAKW,MACTX,KAAKM,MAAQ,KACbN,KAAKU,KAAO,OAEZV,KAAKM,MAAQQ,EAAKG,KAClBjB,KAAKM,MAAMU,KAAO,MAGpBF,EAAKG,KAAO,IACd,CAEA,OAAOjB,IACT,CAiBA,SAAA4B,CAAUf,GACR,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,EAAqBA,EAAKe,YAASd,CAC5C,CAiBA,GAAAe,CAAIjB,GACF,MAAMC,EAAOd,KAAKO,MAAMM,GAExB,QAAaE,IAATD,EAEF,OAAId,KAAKI,IAAM,GACTU,EAAKe,QAAUE,KAAKC,WACtBhC,KAAKiC,OAAOpB,IAOhBb,KAAKkC,UAAUpB,GAERA,EAAKW,MAIhB,CAiBA,GAAAU,CAAItB,GACF,MAAMC,EAAOd,KAAKO,MAAMM,GACxB,YAAgBE,IAATD,IAAoC,IAAbd,KAAKI,KAAaU,EAAKe,OAASE,KAAKC,MACrE,CAaA,SAAAE,CAAUpB,GACJd,KAAKU,OAASI,IAIA,OAAdA,EAAKE,OACPF,EAAKE,KAAKC,KAAOH,EAAKG,MAGN,OAAdH,EAAKG,OACPH,EAAKG,KAAKD,KAAOF,EAAKE,MAGpBhB,KAAKM,QAAUQ,IACjBd,KAAKM,MAAQQ,EAAKG,MAGpBH,EAAKE,KAAOhB,KAAKU,KACjBI,EAAKG,KAAO,KACZjB,KAAKU,KAAKO,KAAOH,EACjBd,KAAKU,KAAOI,EACd,CAgBA,IAAAK,GACE,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQvB,KAAKW,OACzC,IAAIyB,EAAIpC,KAAKM,MACTkB,EAAI,EAER,KAAa,OAANY,GACLhB,EAAOI,KAAOY,EAAEvB,IAChBuB,EAAIA,EAAEnB,KAGR,OAAOG,CACT,CAmBA,cAAAiB,CAAexB,EAAKY,EAAOpB,EAAWL,KAAKK,UACzC,IAAIiC,EAAU,KACVxB,EAAOd,KAAKO,MAAMM,GAmCtB,YAjCaE,IAATD,GACFA,EAAKW,MAAQA,EACTpB,IACFS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,KAE5DJ,KAAKkC,UAAUpB,KAEXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,MACrCmC,EAAU,CACRzB,IAAKb,KAAKM,MAAMO,IAChBY,MAAOzB,KAAKM,MAAMmB,MAClBI,OAAQ7B,KAAKM,MAAMuB,QAErB7B,KAAK0B,OAAM,IAGbZ,EAAOd,KAAKO,MAAMM,GAAO,CACvBgB,OAAQ7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLG,KAAMhB,KAAKU,KACXO,KAAM,KACNQ,SAGkB,KAAdzB,KAAKW,KACTX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKO,KAAOH,EAGnBd,KAAKU,KAAOI,GAGPwB,CACT,CAoBA,GAAAC,CAAI1B,EAAKY,EAAOE,GAAS,EAAOtB,EAAWL,KAAKK,UAC9C,IAAIS,EAAOd,KAAKO,MAAMM,GAmCtB,OAjCIc,QAAmBZ,IAATD,GAEZA,EAAKW,MAAQA,GAEE,IAAXE,GAAoBtB,IACtBS,EAAKe,OAAS7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,KAI5DJ,KAAKkC,UAAUpB,KAGXd,KAAKG,IAAM,GAAKH,KAAKW,OAASX,KAAKG,KACrCH,KAAK0B,OAAM,GAGbZ,EAAOd,KAAKO,MAAMM,GAAO,CACvBgB,OAAQ7B,KAAKI,IAAM,EAAI2B,KAAKC,MAAQhC,KAAKI,IAAMJ,KAAKI,IACpDS,IAAKA,EACLG,KAAMhB,KAAKU,KACXO,KAAM,KACNQ,SAGkB,KAAdzB,KAAKW,KACTX,KAAKM,MAAQQ,EAEbd,KAAKU,KAAKO,KAAOH,EAGnBd,KAAKU,KAAOI,GAGPd,IACT,CAkBA,MAAAwC,CAAOrB,QACQJ,IAATI,IACFA,EAAOnB,KAAKmB,QAGd,MAAMC,EAASC,MAAMC,KAAK,CAAEC,OAAQJ,EAAKI,SACzC,IAAK,IAAIC,EAAI,EAAGA,EAAIL,EAAKI,OAAQC,IAAK,CACpC,MAAMV,EAAOd,KAAKO,MAAMY,EAAKK,IAC7BJ,EAAOI,QAAcT,IAATD,EAAqBA,EAAKW,WAAQV,CAChD,CAEA,OAAOK,CACT,EAyCF3B,EAAAQ,IAAAA,EAAAR,EAAAM,IAdO,SAAaI,EAAM,IAAMC,EAAM,EAAGC,GAAW,GAClD,GAAIoC,MAAMtC,IAAQA,EAAM,EACtB,MAAM,IAAIuC,UAAU,qBAGtB,GAAID,MAAMrC,IAAQA,EAAM,EACtB,MAAM,IAAIsC,UAAU,qBAGtB,GAAwB,kBAAbrC,EACT,MAAM,IAAIqC,UAAU,0BAGtB,OAAO,IAAIzC,EAAIE,EAAKC,EAAKC,EAC3B,CAAA"} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..3a4fd83 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,388 @@ +# API Reference + +Complete API documentation for tiny-lru. + +## Table of Contents + +- [Factory Function](#factory-function) +- [LRU Class](#lru-class) +- [Properties](#properties) +- [Methods](#methods) + +--- + +## Factory Function + +### `lru(max?, ttl?, resetTtl?)` + +Creates a new LRU cache instance with parameter validation. + +```javascript +import { lru } from "tiny-lru"; + +const cache = lru(); +const cache = lru(100); +const cache = lru(100, 5000); +const cache = lru(100, 5000, true); +``` + +**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 + +```javascript +lru(-1); // TypeError: Invalid max value +lru(100, -1); // TypeError: Invalid ttl value +lru(100, 0, "yes"); // TypeError: Invalid resetTtl value +``` + +--- + +## LRU Class + +### `new LRU(max?, ttl?, resetTtl?)` + +Creates an LRU cache instance. Does not validate parameters. + +```javascript +import { LRU } from "tiny-lru"; + +const cache = new LRU(100, 5000, true); +``` + +**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 + +### `size` + +`number` - Current number of items in cache. + +```javascript +const cache = lru(10); +cache.set("a", 1).set("b", 2); +console.log(cache.size); // 2 +``` + +### `max` + +`number` - Maximum number of items allowed. + +```javascript +const cache = lru(100); +console.log(cache.max); // 100 +``` + +### `ttl` + +`number` - Time-to-live in milliseconds. `0` = no expiration. + +```javascript +const cache = lru(100, 60000); +console.log(cache.ttl); // 60000 +``` + +### `resetTtl` + +`boolean` - Whether TTL resets on `set()` updates. + +```javascript +const cache = lru(100, 5000, true); +console.log(cache.resetTtl); // true +``` + +### `first` + +`Object | null` - Least recently used item (node with `key`, `value`, `prev`, `next`, `expiry`). + +```javascript +const cache = lru(2); +cache.set("a", 1).set("b", 2); +console.log(cache.first.key); // "a" +console.log(cache.first.value); // 1 +``` + +### `last` + +`Object | null` - Most recently used item. + +```javascript +const cache = lru(2); +cache.set("a", 1).set("b", 2); +console.log(cache.last.key); // "b" +``` + +--- + +## Methods + +### `clear()` + +Removes all items from cache. + +```javascript +cache.set("a", 1).set("b", 2); +cache.clear(); +console.log(cache.size); // 0 +``` + +**Returns:** `LRU` - this instance (for chaining) + +--- + +### `delete(key)` + +Removes item by key. + +```javascript +cache.set("a", 1).set("b", 2); +cache.delete("a"); +console.log(cache.has("a")); // false +console.log(cache.size); // 1 +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key` | `string` | Key to delete | + +**Returns:** `LRU` - this instance (for chaining) + +--- + +### `entries(keys?)` + +Returns `[key, value]` pairs in LRU order. + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +console.log(cache.entries()); +// [['a', 1], ['b', 2], ['c', 3]] + +console.log(cache.entries(["c", "a"])); +// [['c', 3], ['a', 1]] - respects LRU order +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `keys` | `string[]` | Optional specific keys to retrieve | + +**Returns:** `Array<[string, *]>` - Array of key-value pairs + +--- + +### `evict(bypass?)` + +Removes the least recently used item. + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +cache.evict(); +console.log(cache.has("a")); // false +console.log(cache.keys()); // ['b', 'c'] +``` + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `bypass` | `boolean` | `false` | Force eviction even when empty | + +**Returns:** `LRU` - this instance (for chaining) + +--- + +### `expiresAt(key)` + +Gets expiration timestamp for a key. + +```javascript +const cache = lru(100, 5000); +cache.set("key", "value"); +console.log(cache.expiresAt("key")); // timestamp ~5 seconds from now +console.log(cache.expiresAt("nonexistent")); // undefined +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key` | `string` | Key to check | + +**Returns:** `number | undefined` - Expiration timestamp or undefined + +--- + +### `get(key)` + +Retrieves value and promotes to most recently used. + +```javascript +cache.set("a", 1).set("b", 2); +cache.get("a"); // 1 +console.log(cache.keys()); // ['b', 'a'] - 'a' moved to end +``` + +Expired items are deleted and return `undefined`. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key` | `string` | Key to retrieve | + +**Returns:** `* | undefined` - Value or undefined if not found/expired + +--- + +### `has(key)` + +Checks if key exists and is not expired. + +```javascript +cache.set("a", 1); +cache.has("a"); // true +cache.has("nonexistent"); // false +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `key` | `string` | Key to check | + +**Returns:** `boolean` + +--- + +### `keys()` + +Returns all keys in LRU order (oldest first). + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +cache.get("a"); // Promote 'a' +console.log(cache.keys()); // ['b', 'c', 'a'] +``` + +**Returns:** `string[]` + +--- + +### `set(key, value, bypass?, resetTtl?)` + +Stores value and moves to most recently used. + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +console.log(cache.keys()); // ['a', 'b', 'c'] +``` + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `key` | `string` | - | Item key | +| `value` | `*` | - | Item value | +| `bypass` | `boolean` | `false` | Internal flag for `setWithEvicted` | +| `resetTtl` | `boolean` | `this.resetTtl` | Reset TTL for this operation | + +**Returns:** `LRU` - this instance (for chaining) + +--- + +### `setWithEvicted(key, value, resetTtl?)` + +Stores value and returns evicted item if cache was full. + +```javascript +const cache = lru(2); +cache.set("a", 1).set("b", 2); + +const evicted = cache.setWithEvicted("c", 3); +console.log(evicted); // { key: 'a', value: 1, expiry: 0 } +console.log(cache.keys()); // ['b', 'c'] +``` + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `key` | `string` | - | Item key | +| `value` | `*` | - | Item value | +| `resetTtl` | `boolean` | `this.resetTtl` | Reset TTL for this operation | + +**Returns:** `{ key, value, expiry } | null` - Evicted item or null + +--- + +### `values(keys?)` + +Returns values in LRU order. + +```javascript +cache.set("a", 1).set("b", 2).set("c", 3); +console.log(cache.values()); +// [1, 2, 3] + +console.log(cache.values(["c", "a"])); +// [3, 1] - respects LRU order +``` + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `keys` | `string[]` | Optional specific keys to retrieve | + +**Returns:** `*[]` - Array of values + +--- + +## Evicted Item Shape + +When `setWithEvicted` returns an evicted item: + +```javascript +{ + key: string, // The evicted key + value: *, // The evicted value + expiry: number // Expiration timestamp (0 if no TTL) +} +``` + +--- + +## Method Chaining + +All mutation methods return `this` for chaining: + +```javascript +cache + .set("a", 1) + .set("b", 2) + .set("c", 3) + .delete("a") + .evict(); + +console.log(cache.keys()); // ['c'] +``` diff --git a/docs/CODE_STYLE_GUIDE.md b/docs/CODE_STYLE_GUIDE.md index 15de74b..684941b 100644 --- a/docs/CODE_STYLE_GUIDE.md +++ b/docs/CODE_STYLE_GUIDE.md @@ -1,279 +1,159 @@ # Code Style Guide -This document outlines the coding standards and best practices for the tiny-lru project. Following these guidelines ensures consistency, maintainability, and readability across the codebase. +Coding conventions for tiny-lru source code. -## Table of Contents +## Editor Configuration -- [General Principles](#general-principles) -- [Naming Conventions](#naming-conventions) -- [Code Formatting](#code-formatting) -- [Documentation](#documentation) -- [Functions and Methods](#functions-and-methods) -- [Classes](#classes) -- [Error Handling](#error-handling) -- [Testing](#testing) -- [Security](#security) -- [Performance](#performance) -- [File Organization](#file-organization) +Set your editor to use **tabs** for indentation. -## General Principles +## JavaScript Style -We adhere to the following software engineering principles: - -- **DRY (Don't Repeat Yourself)**: Avoid code duplication -- **KISS (Keep It Simple, Stupid)**: Favor simple solutions over complex ones -- **YAGNI (You Aren't Gonna Need It)**: Don't implement features until they're needed -- **SOLID Principles**: Follow object-oriented design principles -- **OWASP Security Guidelines**: Implement secure coding practices - -## Naming Conventions - -### Variables and Functions -- Use **camelCase** for all variable and function names -- Use descriptive names that clearly indicate purpose +### Formatting ```javascript -// ✅ Good -const userCache = new LRU(100); -const calculateExpiry = (ttl) => Date.now() + ttl; - -// ❌ Bad -const uc = new LRU(100); -const calc = (t) => Date.now() + t; -``` - -### Constants -- Use **UPPER_CASE_SNAKE_CASE** for constants -- Group related constants together - -```javascript -// ✅ Good -const DEFAULT_MAX_SIZE = 1000; -const DEFAULT_TTL = 0; -const CACHE_MISS_PENALTY = 100; - -// ❌ Bad -const defaultMaxSize = 1000; -const default_ttl = 0; -``` - -### Classes -- Use **PascalCase** for class names -- Use descriptive names that indicate the class purpose - -```javascript -// ✅ Good -class LRU { - // implementation +// Tabs for indentation +function example() { + const cache = new LRU(); } -class CacheNode { - // implementation -} +// Double quotes +const name = "tiny-lru"; -// ❌ Bad -class lru { - // implementation -} -``` +// Semicolons required +const result = cache.get("key"); -### Files and Directories -- Use **kebab-case** for file and directory names -- Use descriptive names that indicate content purpose +// K&R braces +if (condition) { + doSomething(); +} else { + doSomethingElse(); +} +// Space before function parens +function myFunction() { } +const arrowFn = () => { }; +const x = function() { }; ``` -src/ - lru.js - cache-utils.js -test/ - integration/ - lru-integration-test.js - unit/ - lru-unit-test.js -``` - -## Code Formatting - -### Indentation -- Use **tabs** for indentation (as per existing codebase) -- Be consistent throughout the project -### Line Length -- Keep lines under **120 characters** when possible -- Break long lines at logical points - -### Spacing -- Use spaces around operators -- Use spaces after commas -- No trailing whitespace +### Comparisons ```javascript -// ✅ Good -const result = a + b * c; -const array = [1, 2, 3, 4]; -const object = { key: value, another: data }; - -// ❌ Bad -const result=a+b*c; -const array=[1,2,3,4]; -const object={key:value,another:data}; +// Use === and !== for comparisons +if (item !== undefined) { } +if (this.first === null) { } ``` -### Semicolons -- Use semicolons consistently throughout the codebase -- Follow the existing project pattern - -### Braces -- Use consistent brace style (K&R style as per existing code) +### Object Creation ```javascript -// ✅ Good -if (condition) { - doSomething(); -} else { - doSomethingElse(); -} +// Use Object.create(null) for hash maps - avoids prototype pollution +this.items = Object.create(null); -// ❌ Bad -if (condition) -{ - doSomething(); -} -else -{ - doSomethingElse(); -} +// Use Array.from() for pre-allocated arrays +const result = Array.from({ length: this.size }); ``` -## Documentation +## JSDoc Comments -### JSDoc Standards -- Use **JSDoc standard** for all function and class documentation -- Include comprehensive descriptions, parameters, return values, and examples +Every exported function and class method must have JSDoc: ```javascript /** - * Retrieves a value from the cache by key. + * Short description of the method. * - * @method get + * @method methodName * @memberof LRU - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found. + * @param {string} key - Description of parameter. + * @returns {LRU} Description of return value. * @example - * cache.set('key1', 'value1'); - * console.log(cache.get('key1')); // 'value1' - * @see {@link LRU#set} + * cache.set('key', 'value'); + * @see {@link LRU#get} * @since 1.0.0 */ -get(key) { +methodName(key) { // implementation } ``` -### Required JSDoc Tags -- `@param` for all parameters with type and description -- `@returns` for return values with type and description -- `@throws` for exceptions that may be thrown -- `@example` for usage examples -- `@since` for version information -- `@see` for related methods/classes -- `@memberof` for class methods +### JSDoc Tags -### Code Comments -- Use inline comments sparingly and only when code logic is complex -- Write self-documenting code when possible -- Explain **why**, not **what** +- `@method` - Method name +- `@memberof` - Parent class +- `@param` - Parameters with type and description +- `@returns` - Return value with type +- `@example` - Usage example +- `@see` - Related methods +- `@since` - Version introduced +- `@private` - For internal methods + +## Naming ```javascript -// ✅ Good - explains why -if (this.ttl > 0 && item.expiry <= Date.now()) { - // Item has expired, remove it from cache - this.delete(key); -} +// Classes: PascalCase +export class LRU { } -// ❌ Bad - explains what (obvious from code) -// Check if ttl is greater than 0 and item expiry is less than or equal to current time -if (this.ttl > 0 && item.expiry <= Date.now()) { - this.delete(key); -} +// Methods: camelCase +clear() { } +setWithEvicted() { } + +// Variables: camelCase +const maxSize = 1000; +let currentItem = null; + +// Constants: camelCase (not UPPER_SNAKE) +const defaultMax = 1000; ``` -## Functions and Methods +## Method Patterns -### Function Design -- Keep functions small and focused on a single responsibility -- Use pure functions when possible (no side effects) -- Limit function parameters (prefer 3 or fewer) +### Method Chaining -```javascript -// ✅ Good - single responsibility -function isExpired(item, currentTime) { - return item.expiry > 0 && item.expiry <= currentTime; -} +Methods that modify state return `this`: -// ❌ Bad - multiple responsibilities -function processItemAndUpdateCache(item, cache, currentTime) { - if (item.expiry > 0 && item.expiry <= currentTime) { - cache.delete(item.key); - cache.stats.evictions++; - return null; - } - cache.stats.hits++; - return item.value; +```javascript +clear() { + this.size = 0; + return this; } ``` -### Method Chaining -- Return `this` from methods that modify state to enable chaining -- Document chaining capability in JSDoc +### Null Safety + +Always check for null/undefined: ```javascript -/** - * @returns {LRU} The LRU instance for method chaining. - */ -set(key, value) { - // implementation +if (item.prev !== null) { + item.prev.next = item.next; +} + +if (!item) { return this; } ``` -### Parameter Validation -- Validate parameters at function entry -- Throw appropriate errors for invalid inputs -- Use meaningful error messages +### Early Returns + +Use early returns to reduce nesting: ```javascript -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"); +get(key) { + const item = this.items[key]; + + if (item === undefined) { + return undefined; } - - return new LRU(max, ttl, resetTtl); + + // Main logic here + return item.value; } ``` -## Classes - -### Class Structure -- Order class members logically: constructor, public methods, private methods -- Use consistent method ordering across similar classes -- Initialize all properties in constructor +## Class Structure ```javascript export class LRU { - /** - * Constructor with full documentation - */ + // Constructor first constructor(max = 0, ttl = 0, resetTtl = false) { - // Initialize all properties this.first = null; this.items = Object.create(null); this.last = null; @@ -283,169 +163,26 @@ export class LRU { this.ttl = ttl; } - // Public methods first - get(key) { /* implementation */ } - - set(key, value) { /* implementation */ } - - // Private methods last (if any) - _internalMethod() { /* implementation */ } + // Public methods + clear() { } + get(key) { } + set(key, value) { } + + // Private methods at end (if any) + moveToEnd(item) { } } ``` -### Property Access -- Use public properties for API surface -- Use private properties (starting with underscore) for internal state -- Avoid getter/setter overhead unless necessary - ## Error Handling -### Error Types -- Use built-in error types when appropriate (`TypeError`, `RangeError`, etc.) -- Create custom error classes for domain-specific errors -- Include helpful error messages +Use TypeError with clear messages: ```javascript -// ✅ Good -if (typeof key !== 'string') { - throw new TypeError(`Expected string key, got ${typeof key}`); -} - -// ❌ Bad -if (typeof key !== 'string') { - throw new Error('Bad key'); +if (isNaN(max) || max < 0) { + throw new TypeError("Invalid max value"); } ``` -### Error Documentation -- Document all errors that functions may throw -- Include error conditions in JSDoc - -```javascript -/** - * @throws {TypeError} When key is not a string. - * @throws {RangeError} When value exceeds maximum size. - */ -``` - -## Testing - -### Test Organization -- **Unit tests** go in `tests/unit/` using node-assert and mocha -- **Integration tests** go in `tests/integration/` using node-assert and mocha -- Follow the same naming conventions as source files - -### Test Structure -- Use descriptive test names that explain the scenario -- Follow Arrange-Act-Assert pattern -- Test both success and failure cases - -```javascript -import assert from 'node:assert'; -import { describe, it } from 'mocha'; -import { LRU } from '../src/lru.js'; - -describe('LRU Cache', () => { - it('should return undefined for non-existent keys', () => { - // Arrange - const cache = new LRU(10); - - // Act - const result = cache.get('nonexistent'); - - // Assert - assert.strictEqual(result, undefined); - }); - - it('should throw TypeError for invalid max value', () => { - // Assert - assert.throws(() => { - new LRU(-1); - }, TypeError, 'Invalid max value'); - }); -}); -``` - -## Security - -### Input Validation -- Validate all external inputs -- Sanitize data before processing -- Use parameterized queries for database operations - -### Memory Safety -- Avoid memory leaks by properly cleaning up references -- Be careful with circular references -- Monitor memory usage in long-running operations - -### OWASP Guidelines -- Follow OWASP security guidelines for all code -- Avoid common vulnerabilities (injection, XSS, etc.) -- Use secure coding practices - -## Performance - -### Algorithmic Efficiency -- Choose appropriate data structures for the use case -- Consider time and space complexity -- Profile code to identify bottlenecks - -### Memory Management -- Reuse objects when possible -- Avoid unnecessary object creation in hot paths -- Use `Object.create(null)` for hash maps without prototype pollution - -```javascript -// ✅ Good - no prototype pollution -this.items = Object.create(null); - -// ❌ Potentially problematic -this.items = {}; -``` - -### Micro-optimizations -- Avoid premature optimization -- Measure before optimizing -- Focus on algorithmic improvements over micro-optimizations - -## File Organization - -### Project Structure -``` -tiny-lru/ -├── src/ # Source code -│ └── lru.js -├── test/ # Test files -│ ├── unit/ # Unit tests -│ └── integration/ # Integration tests -├── types/ # TypeScript definitions -├── docs/ # Documentation -├── benchmarks/ # Performance benchmarks -└── dist/ # Built files -``` - -### Import/Export -- Use ES6 modules (`import`/`export`) -- Use named exports for utilities, default exports for main classes -- Group imports logically - -```javascript -// ✅ Good - grouped imports -import assert from 'node:assert'; -import { describe, it } from 'mocha'; - -import { LRU, lru } from '../src/lru.js'; -import { helper } from './test-utils.js'; - -// ❌ Bad - mixed import order -import { LRU } from '../src/lru.js'; -import assert from 'node:assert'; -import { helper } from './test-utils.js'; -import { describe, it } from 'mocha'; -``` - -## Conclusion - -This style guide ensures consistency and quality across the tiny-lru codebase. When in doubt, refer to existing code patterns and prioritize readability and maintainability over cleverness. +## ESLint Configuration -For questions or suggestions about this style guide, please open an issue in the project repository. \ No newline at end of file +The project uses oxlint. Run `npm run lint` to check code style. diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md index f60d026..2840241 100644 --- a/docs/TECHNICAL_DOCUMENTATION.md +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -56,7 +56,7 @@ graph TD - `max`: Maximum cache size (0 = unlimited) - `size`: Current number of items - `ttl`: Time-to-live in milliseconds (0 = no expiration) - - `resetTtl`: Whether to reset TTL on access + - `resetTtl`: Whether to reset TTL on `set()` operations (not on `get()`) ## Data Flow @@ -133,7 +133,7 @@ sequenceDiagram | `moveToEnd(item)` | O(1) | O(1) | O(1) | Internal: optimize LRU positioning | | `keys()` | O(n) | O(n) | O(n) | Array of all keys in LRU order | | `values(keys?)` | O(n) | O(n) | O(n) | Array of values for specified keys | -| `entries(keys?)` | O(n) | O(n) | O(n) | Array of [key, value] pairs | +| `entries([keys])` | O(n) | O(n) | O(n) | Array of [key, value] pairs | ### Memory Usage @@ -183,20 +183,25 @@ last.next \leftarrow H[k] & \text{otherwise} **Time Complexity:** $O(1)$ amortized -#### Set With Evicted Operation: $setWithEvicted(k, v, resetTtl = resetTtl) \rightarrow \{key: K, value: V, expiry: \mathbb{N}_0, prev: Object, next: Object\} \cup \{\bot\}$ +#### Set With Evicted Operation: $setWithEvicted(k, v, resetTtl = resetTtl) \rightarrow \{key: K, value: V, expiry: \mathbb{N}_0\} \cup \{\bot\}$ $$\begin{align} setWithEvicted(k, v, resetTtl) &= \begin{cases} -set(k, v, true, resetTtl) \land \bot & \text{if } k \in H \\ +update(k, v, resetTtl) \land \bot & \text{if } k \in H \\ evicted \land create(k, v) & \text{if } k \notin H \land max > 0 \land size = max \\ \bot \land create(k, v) & \text{if } k \notin H \land (max = 0 \lor size < max) \end{cases} \\ +update(k, v, resetTtl) &= H[k].value \leftarrow v \land moveToEnd(H[k]) \\ +& \quad \land \begin{cases} +H[k].expiry \leftarrow t_{now} + ttl & \text{if } resetTtl = true \land ttl > 0 \\ +\text{no-op} & \text{otherwise} +\end{cases} \\ \text{where } evicted &= \begin{cases} -\{...this.first\} & \text{if } size > 0 \\ +\{key: this.first.key, value: this.first.value, expiry: this.first.expiry\} & \text{if } size > 0 \\ \bot & \text{otherwise} \end{cases} \end{align}$$ -**Note:** `setWithEvicted()` always calls `set()` with `bypass = true`, which means TTL is never reset during `setWithEvicted()` operations, regardless of the `resetTtl` parameter. +**Note:** Unlike `set()`, `setWithEvicted()` does not use a `bypass` parameter, so TTL is reset when `resetTtl = true`. **Time Complexity:** $O(1)$ amortized @@ -217,6 +222,12 @@ $$\begin{align} delete(k) &= \begin{cases} removeFromList(H[k]) \land H \setminus \{k\} \land size \leftarrow size - 1 & \text{if } k \in H \\ \text{no-op} & \text{otherwise} +\end{cases} \\ +removeFromList(item) &= \begin{cases} +item.prev.next \leftarrow item.next \land item.next.prev \leftarrow item.prev \land first \leftarrow item.next \land last \leftarrow item.prev & \text{if } item.prev \neq null \land item.next \neq null \\ +item.prev.next \leftarrow item.next \land first \leftarrow item.next \land last \leftarrow null & \text{if } item.prev \neq null \land item.next = null \\ +item.next.prev \leftarrow item.prev \land first \leftarrow item.next \land last \leftarrow null & \text{if } item.prev = null \land item.next \neq null \\ +first \leftarrow null \land last \leftarrow null & \text{if } item.prev = null \land item.next = null \end{cases} \end{align}$$ @@ -226,10 +237,12 @@ removeFromList(H[k]) \land H \setminus \{k\} \land size \leftarrow size - 1 & \t $$\begin{align} moveToEnd(item) &= \begin{cases} \text{no-op} & \text{if } item = last \\ -removeFromList(item) \land appendToList(item) & \text{otherwise} +item.prev.next \leftarrow item.next \land item.next.prev \leftarrow item.prev \land first \leftarrow item.next \land item.prev \leftarrow last \land last.next \leftarrow item \land last \leftarrow item & \text{if } item \neq last \end{cases} \end{align}$$ +**Edge Case:** When item is the only node in the list ($item.prev = null \land item.next = null$), the condition $item = last$ is true since $first = last = item$, so the operation is a no-op. + **Time Complexity:** $O(1)$ ### Eviction Policy @@ -257,7 +270,7 @@ delete(k) & \text{if } isExpired(k) \\ **TTL Reset Behavior:** - TTL is only reset during `set()` operations when `resetTtl = true` and `bypass = false` - `get()` operations never reset TTL, regardless of the `resetTtl` setting -- `setWithEvicted()` operations never reset TTL because they always call `set()` with `bypass = true` +- `setWithEvicted()` operations reset TTL when `resetTtl = true` (does not use bypass parameter) ### Space Complexity @@ -272,8 +285,8 @@ delete(k) & \text{if } isExpired(k) \\ 2. **List Consistency:** $first \neq null \iff last \neq null \iff size > 0$ 3. **Hash Consistency:** $|H| = size$ 4. **LRU Order:** Items in list are ordered from least to most recently used -5. **TTL Validity:** $ttl = 0 \lor \forall k \in H: H[k].expiry > t_{now}$ -6. **TTL Reset Invariant:** TTL is only reset during `set()` operations when `bypass = false`, never during `get()` or `setWithEvicted()` operations +5. **TTL Validity:** $(ttl = 0 \Rightarrow \forall k \in H: H[k].expiry = 0) \land (ttl > 0 \Rightarrow \forall k \in H: H[k].expiry \geq t_{now})$ +6. **TTL Reset Invariant:** TTL is only reset during `set()` operations when `bypass = false`, and during `setWithEvicted()` operations when `resetTtl = true` ## TypeScript Support @@ -308,7 +321,7 @@ export class LRU { has(key: any): boolean; keys(): any[]; set(key: any, value: T, bypass?: boolean, resetTtl?: boolean): this; - setWithEvicted(key: any, value: T, resetTtl?: boolean): LRUItem | null; + setWithEvicted(key: any, value: T, resetTtl?: boolean): { key: any; value: T; expiry: number } | null; values(keys?: any[]): T[]; } diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 6de2781..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,176 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; - -export default [ - { - languageOptions: { - globals: { - ...globals.node, - it: true, - describe: true, - beforeEach: true - }, - parserOptions: { - ecmaVersion: 2022 - } - }, - rules: { - "arrow-parens": [2, "as-needed"], - "arrow-spacing": [2, {"before": true, "after": true}], - "block-scoped-var": [0], - "brace-style": [2, "1tbs", {"allowSingleLine": true}], - "camelcase": [0], - "comma-dangle": [2, "never"], - "comma-spacing": [2], - "comma-style": [2, "last"], - "complexity": [0, 11], - "consistent-return": [2], - "consistent-this": [0, "that"], - "curly": [2, "multi-line"], - "default-case": [2], - "dot-notation": [2, {"allowKeywords": true}], - "eol-last": [2], - "eqeqeq": [2], - "func-names": [0], - "func-style": [0, "declaration"], - "generator-star-spacing": [2, "after"], - "guard-for-in": [0], - "handle-callback-err": [0], - "indent": ["error", "tab", {"VariableDeclarator": {"var": 1, "let": 1, "const": 1}, "SwitchCase": 1}], - "key-spacing": [2, {"beforeColon": false, "afterColon": true}], - "quotes": [2, "double", "avoid-escape"], - "max-depth": [0, 4], - "max-len": [0, 80, 4], - "max-nested-callbacks": [0, 2], - "max-params": [0, 3], - "max-statements": [0, 10], - "new-parens": [2], - "new-cap": [2, {"capIsNewExceptions": ["ToInteger", "ToObject", "ToPrimitive", "ToUint32"]}], - "newline-after-var": [0], - "newline-before-return": [2], - "no-alert": [2], - "no-array-constructor": [2], - "no-bitwise": [0], - "no-caller": [2], - "no-catch-shadow": [2], - "no-cond-assign": [2], - "no-console": [0], - "no-constant-condition": [1], - "no-continue": [2], - "no-control-regex": [2], - "no-debugger": [2], - "no-delete-var": [2], - "no-div-regex": [0], - "no-dupe-args": [2], - "no-dupe-keys": [2], - "no-duplicate-case": [2], - "no-else-return": [0], - "no-empty": [2], - "no-eq-null": [0], - "no-eval": [2], - "no-ex-assign": [2], - "no-extend-native": [1], - "no-extra-bind": [2], - "no-extra-boolean-cast": [2], - "no-extra-semi": [1], - "no-empty-character-class": [2], - "no-fallthrough": [2], - "no-floating-decimal": [2], - "no-func-assign": [2], - "no-implied-eval": [2], - "no-inline-comments": [0], - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": [2], - "no-irregular-whitespace": [2], - "no-iterator": [2], - "no-label-var": [2], - "no-labels": [2], - "no-lone-blocks": [2], - "no-lonely-if": [2], - "no-loop-func": [2], - "no-mixed-requires": [0, false], - "no-mixed-spaces-and-tabs": [2, false], - "no-multi-spaces": [2], - "no-multi-str": [2], - "no-multiple-empty-lines": [2, {"max": 2}], - "no-native-reassign": [0], - "no-negated-in-lhs": [2], - "no-nested-ternary": [0], - "no-new": [2], - "no-new-func": [0], - "no-new-object": [2], - "no-new-require": [0], - "no-new-wrappers": [2], - "no-obj-calls": [2], - "no-octal": [2], - "no-octal-escape": [2], - "no-param-reassign": [0], - "no-path-concat": [0], - "no-plusplus": [0], - "no-process-env": [0], - "no-process-exit": [0], - "no-proto": [2], - "no-redeclare": [2], - "no-regex-spaces": [2], - "no-reserved-keys": [0], - "no-reno-new-funced-modules": [0], - "no-return-assign": [2], - "no-script-url": [2], - "no-self-compare": [0], - "no-sequences": [2], - "no-shadow": [2], - "no-shadow-restricted-names": [2], - "no-spaced-func": [2], - "no-sparse-arrays": [2], - "no-sync": [0], - "no-ternary": [0], - "no-throw-literal": [2], - "no-trailing-spaces": [2], - "no-undef": [2], - "no-undef-init": [2], - "no-undefined": [0], - "no-underscore-dangle": [0], - "no-unreachable": [2], - "no-unused-expressions": [2], - "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], - "no-use-before-define": [2], - "no-void": [0], - "no-warning-comments": [0, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], - "no-with": [2], - "no-extra-parens": [2], - "one-var": [0], - "operator-assignment": [0, "always"], - "operator-linebreak": [2, "after"], - "padded-blocks": [0], - "quote-props": [0], - "radix": [0], - "semi": [2], - "semi-spacing": [2, {before: false, after: true}], - "sort-vars": [0], - "keyword-spacing": [2], - "space-before-function-paren": [2, {anonymous: "always", named: "always"}], - "space-before-blocks": [2, "always"], - "space-in-brackets": [0, "never", { - singleValue: true, - arraysInArrays: false, - arraysInObjects: false, - objectsInArrays: true, - objectsInObjects: true, - propertyName: false - }], - "space-in-parens": [2, "never"], - "space-infix-ops": [2], - "space-unary-ops": [2, {words: true, nonwords: false}], - "spaced-line-comment": [0, "always"], - strict: [0], - "use-isnan": [2], - "valid-jsdoc": [0], - "valid-typeof": [2], - "vars-on-top": [0], - "wrap-iife": [2], - "wrap-regex": [2], - yoda: [2, "never", {exceptRange: true}] - } - }, - pluginJs.configs.recommended -]; diff --git a/package-lock.json b/package-lock.json index abc1572..0af45fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,9 @@ "devDependencies": { "@rollup/plugin-terser": "^1.0.0", "auto-changelog": "^2.5.0", - "c8": "^11.0.0", - "eslint": "^9.29.0", "husky": "^9.1.7", - "mocha": "^11.7.0", + "oxfmt": "^0.41.0", + "oxlint": "^1.56.0", "rollup": "^4.43.0", "tinybench": "^6.0.0" }, @@ -22,328 +21,6 @@ "node": ">=12" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -399,54 +76,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", - "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "serialize-javascript": "^7.0.3", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.41.0.tgz", + "integrity": "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==", "cpu": [ "arm" ], @@ -455,12 +88,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.41.0.tgz", + "integrity": "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g==", "cpu": [ "arm64" ], @@ -469,12 +105,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.41.0.tgz", + "integrity": "sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA==", "cpu": [ "arm64" ], @@ -483,12 +122,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.41.0.tgz", + "integrity": "sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw==", "cpu": [ "x64" ], @@ -497,40 +139,49 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.41.0.tgz", + "integrity": "sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.41.0.tgz", + "integrity": "sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.41.0.tgz", + "integrity": "sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA==", "cpu": [ "arm" ], @@ -539,166 +190,226 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.41.0.tgz", + "integrity": "sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA==", "cpu": [ - "arm" + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.41.0.tgz", + "integrity": "sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.41.0.tgz", + "integrity": "sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.41.0.tgz", + "integrity": "sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.41.0.tgz", + "integrity": "sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.41.0.tgz", + "integrity": "sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==", "cpu": [ - "ppc64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.41.0.tgz", + "integrity": "sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==", "cpu": [ - "ppc64" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.41.0.tgz", + "integrity": "sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==", "cpu": [ - "riscv64" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.41.0.tgz", + "integrity": "sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.41.0.tgz", + "integrity": "sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.41.0.tgz", + "integrity": "sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.41.0.tgz", + "integrity": "sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A==", "cpu": [ "x64" ], @@ -706,27 +417,33 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz", + "integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz", + "integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==", "cpu": [ "arm64" ], @@ -734,13 +451,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz", + "integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==", "cpu": [ "arm64" ], @@ -748,27 +468,33 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz", + "integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz", + "integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==", "cpu": [ "x64" ], @@ -776,1192 +502,834 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz", + "integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "linux" + ], "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz", + "integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz", + "integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz", + "integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/auto-changelog": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz", - "integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==", + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz", + "integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "commander": "^7.2.0", - "handlebars": "^4.7.7", - "import-cwd": "^3.0.0", - "node-fetch": "^2.6.1", - "parse-github-url": "^1.0.3", - "semver": "^7.3.5" - }, - "bin": { - "auto-changelog": "src/index.js" - }, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.3" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz", + "integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "dev": true, - "license": "ISC" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/c8": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", - "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz", + "integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.1", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^8.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "monocart-coverage-reports": "^2" - }, - "peerDependenciesMeta": { - "monocart-coverage-reports": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz", + "integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==", + "cpu": [ + "s390x" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/chalk": { - "version": "4.1.2", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz", + "integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz", + "integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz", + "integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz", + "integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=7.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "7.2.0", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz", + "integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz", + "integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" }, "engines": { - "node": ">=6.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "rollup": { "optional": true } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" + "node": ">=20.0.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/emoji-regex": { - "version": "8.0.0", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/find-up": { - "version": "5.0.0", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } + "freebsd" + ] }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/handlebars": { - "version": "4.7.8", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/has-flag": { - "version": "4.0.0", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/he": { - "version": "1.2.0", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", - "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", - "dev": true, - "dependencies": { - "import-from": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-from/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/imurmurhash": { - "version": "0.1.4", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">=0.8.19" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-extglob": { - "version": "2.1.1", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-glob": { - "version": "4.0.3", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-plain-obj": { - "version": "2.1.0", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.4.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", + "node_modules/auto-changelog": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz", + "integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==", "dev": true, - "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "import-cwd": "^3.0.0", + "node-fetch": "^2.6.1", + "parse-github-url": "^1.0.3", + "semver": "^7.3.5" }, - "engines": { - "node": ">=10" + "bin": { + "auto-changelog": "src/index.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8.3" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", + "node_modules/buffer-from": { + "version": "1.1.2", "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", + "node_modules/commander": { + "version": "7.2.0", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 10" } }, - "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "BlueOak-1.0.0", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "20 || >=22" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/handlebars": { + "version": "4.7.8", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=10" + "node": ">=0.4.7" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/minimatch": { - "version": "3.1.2", + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "bin": { + "husky": "bin.js" }, "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" + "url": "https://github.com/sponsors/typicode" } }, - "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", "dev": true, - "license": "MIT", "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" + "import-from": "^3.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", + "node_modules/minimist": { + "version": "1.2.8", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, "node_modules/neo-async": { "version": "2.6.2", "dev": true, @@ -1986,185 +1354,101 @@ } } }, - "node_modules/optionator": { - "version": "0.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/oxfmt": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.41.0.tgz", + "integrity": "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "tinypool": "2.1.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-github-url": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz", - "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==", - "dev": true, "bin": { - "parse-github-url": "cli.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "oxfmt": "bin/oxfmt" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" + "url": "https://github.com/sponsors/Boshen" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.41.0", + "@oxfmt/binding-android-arm64": "0.41.0", + "@oxfmt/binding-darwin-arm64": "0.41.0", + "@oxfmt/binding-darwin-x64": "0.41.0", + "@oxfmt/binding-freebsd-x64": "0.41.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.41.0", + "@oxfmt/binding-linux-arm64-gnu": "0.41.0", + "@oxfmt/binding-linux-arm64-musl": "0.41.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.41.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.41.0", + "@oxfmt/binding-linux-riscv64-musl": "0.41.0", + "@oxfmt/binding-linux-s390x-gnu": "0.41.0", + "@oxfmt/binding-linux-x64-gnu": "0.41.0", + "@oxfmt/binding-linux-x64-musl": "0.41.0", + "@oxfmt/binding-openharmony-arm64": "0.41.0", + "@oxfmt/binding-win32-arm64-msvc": "0.41.0", + "@oxfmt/binding-win32-ia32-msvc": "0.41.0", + "@oxfmt/binding-win32-x64-msvc": "0.41.0" + } + }, + "node_modules/oxlint": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz", + "integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==", "dev": true, "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.56.0", + "@oxlint/binding-android-arm64": "1.56.0", + "@oxlint/binding-darwin-arm64": "1.56.0", + "@oxlint/binding-darwin-x64": "1.56.0", + "@oxlint/binding-freebsd-x64": "1.56.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", + "@oxlint/binding-linux-arm-musleabihf": "1.56.0", + "@oxlint/binding-linux-arm64-gnu": "1.56.0", + "@oxlint/binding-linux-arm64-musl": "1.56.0", + "@oxlint/binding-linux-ppc64-gnu": "1.56.0", + "@oxlint/binding-linux-riscv64-gnu": "1.56.0", + "@oxlint/binding-linux-riscv64-musl": "1.56.0", + "@oxlint/binding-linux-s390x-gnu": "1.56.0", + "@oxlint/binding-linux-x64-gnu": "1.56.0", + "@oxlint/binding-linux-x64-musl": "1.56.0", + "@oxlint/binding-openharmony-arm64": "1.56.0", + "@oxlint/binding-win32-arm64-msvc": "1.56.0", + "@oxlint/binding-win32-ia32-msvc": "1.56.0", + "@oxlint/binding-win32-x64-msvc": "1.56.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.15.0" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/parse-github-url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz", + "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==", "dev": true, - "license": "MIT", + "bin": { + "parse-github-url": "cli.js" + }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, "node_modules/rollup": { @@ -2212,25 +1496,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/semver": { "version": "7.5.4", "dev": true, @@ -2261,47 +1526,6 @@ "dev": true, "license": "ISC" }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/smob": { "version": "1.4.1", "dev": true, @@ -2324,82 +1548,6 @@ "source-map": "^0.6.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/terser": { "version": "5.21.0", "dev": true, @@ -2422,95 +1570,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", - "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tinybench": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.0.0.tgz", @@ -2521,22 +1580,21 @@ "node": ">=20.0.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/type-check": { - "version": "0.4.0", + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, "engines": { - "node": ">= 0.8.0" + "node": "^20.0.0 || >=22.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.17.4", "dev": true, @@ -2549,31 +1607,6 @@ "node": ">=0.8.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "dev": true, @@ -2588,154 +1621,10 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "dev": true, "license": "MIT" - }, - "node_modules/workerpool": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", - "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index c3bf37e..383207f 100644 --- a/package.json +++ b/package.json @@ -38,19 +38,19 @@ "benchmark:install-deps": "npm install --no-save lru-cache quick-lru mnemonist", "benchmark:all": "npm run benchmark:modern && npm run benchmark:perf && npm run benchmark:comparison", "changelog": "auto-changelog -p", - "lint": "eslint --fix *.js src/*.js tests/**/*.js benchmarks/*.js", - "mocha": "c8 mocha \"tests/**/*.js\"", + "fix": "oxlint --fix *.js benchmarks src tests/unit && oxfmt *.js benchmarks src tests/unit --write", + "lint": "oxlint *.js benchmarks src tests/unit && oxfmt *.js benchmarks/*.js src/*.js tests/unit/*.js --check", + "coverage": "node --test --experimental-test-coverage --test-coverage-exclude=dist/** --test-coverage-exclude=tests/** --test-reporter=spec tests/**/*.test.js 2>&1 | grep -A 1000 \"start of coverage report\" > coverage.txt", "rollup": "rollup --config", - "test": "npm run lint && npm run mocha", + "test": "npm run lint && node --test tests/**/*.js", "prepare": "husky" }, "devDependencies": { "@rollup/plugin-terser": "^1.0.0", "auto-changelog": "^2.5.0", - "c8": "^11.0.0", - "eslint": "^9.29.0", "husky": "^9.1.7", - "mocha": "^11.7.0", + "oxfmt": "^0.41.0", + "oxlint": "^1.56.0", "rollup": "^4.43.0", "tinybench": "^6.0.0" }, diff --git a/rollup.config.js b/rollup.config.js index 31caa38..ff5d0e9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,41 +14,40 @@ const bannerShort = `/*! ${year} ${pkg.author} @version ${pkg.version} */`; -const defaultOutBase = {compact: true, banner: bannerLong, name: pkg.name}; -const cjOutBase = {...defaultOutBase, compact: false, format: "cjs", exports: "named"}; -const esmOutBase = {...defaultOutBase, format: "esm"}; -const umdOutBase = {...defaultOutBase, format: "umd"}; -const minOutBase = {banner: bannerShort, name: pkg.name, plugins: [terser()], sourcemap: true}; - +const defaultOutBase = { compact: true, banner: bannerLong, name: pkg.name }; +const cjOutBase = { ...defaultOutBase, compact: false, format: "cjs", exports: "named" }; +const esmOutBase = { ...defaultOutBase, format: "esm" }; +const umdOutBase = { ...defaultOutBase, format: "umd" }; +const minOutBase = { banner: bannerShort, name: pkg.name, plugins: [terser()], sourcemap: true }; export default [ - { - input: "./src/lru.js", - output: [ - { - ...cjOutBase, - file: `dist/${pkg.name}.cjs` - }, - { - ...esmOutBase, - file: `dist/${pkg.name}.js` - }, - { - ...esmOutBase, - ...minOutBase, - file: `dist/${pkg.name}.min.js` - }, - { - ...umdOutBase, - file: `dist/${pkg.name}.umd.js`, - name: "lru" - }, - { - ...umdOutBase, - ...minOutBase, - file: `dist/${pkg.name}.umd.min.js`, - name: "lru" - } - ] - } + { + input: "./src/lru.js", + output: [ + { + ...cjOutBase, + file: `dist/${pkg.name}.cjs`, + }, + { + ...esmOutBase, + file: `dist/${pkg.name}.js`, + }, + { + ...esmOutBase, + ...minOutBase, + file: `dist/${pkg.name}.min.js`, + }, + { + ...umdOutBase, + file: `dist/${pkg.name}.umd.js`, + name: "lru", + }, + { + ...umdOutBase, + ...minOutBase, + file: `dist/${pkg.name}.umd.min.js`, + name: "lru", + }, + ], + }, ]; diff --git a/src/lru.js b/src/lru.js index 8ed976d..0ee96de 100644 --- a/src/lru.js +++ b/src/lru.js @@ -17,432 +17,443 @@ * // After 5 seconds, key1 will be expired */ 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 accessing existing items via get(). - * @example - * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access - * @see {@link lru} For parameter validation - * @since 1.0.0 - */ - constructor (max = 0, ttl = 0, resetTtl = false) { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.max = max; - this.resetTtl = resetTtl; - this.size = 0; - this.ttl = ttl; - } - - /** - * Removes all items from the cache. - * - * @method clear - * @memberof LRU - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.clear(); - * console.log(cache.size); // 0 - * @since 1.0.0 - */ - clear () { - this.first = null; - this.items = Object.create(null); - this.last = null; - this.size = 0; - - return this; - } - - /** - * Removes an item from the cache by key. - * - * @method delete - * @memberof LRU - * @param {string} key - The key of the item to delete. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1'); - * cache.delete('key1'); - * console.log(cache.has('key1')); // false - * @see {@link LRU#has} - * @see {@link LRU#clear} - * @since 1.0.0 - */ - delete (key) { - if (this.has(key)) { - const item = this.items[key]; - - delete this.items[key]; - this.size--; - - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - if (this.first === item) { - this.first = item.next; - } - - if (this.last === item) { - this.last = item.prev; - } - } - - return this; - } - - /** - * Returns an array of [key, value] pairs for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method entries - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. - * @returns {Array>} Array of [key, value] pairs in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.entries()); // [['a', 1], ['b', 2]] - * console.log(cache.entries(['a'])); // [['a', 1]] - * @see {@link LRU#keys} - * @see {@link LRU#values} - * @since 11.1.0 - */ - entries (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - result[i] = [key, this.get(key)]; - } - - return result; - } - - /** - * Removes the least recently used item from the cache. - * - * @method evict - * @memberof LRU - * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('old', 'value').set('new', 'value'); - * cache.evict(); // Removes 'old' item - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - evict (bypass = false) { - if (bypass || this.size > 0) { - const item = this.first; - - delete this.items[item.key]; - - if (--this.size === 0) { - this.first = null; - this.last = null; - } else { - this.first = item.next; - this.first.prev = null; - } - } - - return this; - } - - /** - * Returns the expiration timestamp for a given key. - * - * @method expiresAt - * @memberof LRU - * @param {string} key - The key to check expiration for. - * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. - * @example - * const cache = new LRU(100, 5000); // 5 second TTL - * cache.set('key1', 'value1'); - * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now - * @see {@link LRU#get} - * @see {@link LRU#has} - * @since 1.0.0 - */ - expiresAt (key) { - let result; - - if (this.has(key)) { - result = this.items[key].expiry; - } - - return result; - } - - /** - * Retrieves a value from the cache by key. Updates the item's position to most recently used. - * - * @method get - * @memberof LRU - * @param {string} key - The key to retrieve. - * @returns {*} The value associated with the key, or undefined if not found or expired. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.get('key1')); // 'value1' - * console.log(cache.get('nonexistent')); // undefined - * @see {@link LRU#set} - * @see {@link LRU#has} - * @since 1.0.0 - */ - get (key) { - const item = this.items[key]; - - if (item !== undefined) { - // Check TTL only if enabled to avoid unnecessary Date.now() calls - if (this.ttl > 0) { - if (item.expiry <= Date.now()) { - this.delete(key); - - return undefined; - } - } - - // Fast LRU update without full set() overhead - this.moveToEnd(item); - - return item.value; - } - - return undefined; - } - - /** - * Checks if a key exists in the cache. - * - * @method has - * @memberof LRU - * @param {string} key - The key to check for. - * @returns {boolean} True if the key exists, false otherwise. - * @example - * cache.set('key1', 'value1'); - * console.log(cache.has('key1')); // true - * console.log(cache.has('nonexistent')); // false - * @see {@link LRU#get} - * @see {@link LRU#delete} - * @since 9.0.0 - */ - has (key) { - return key in this.items; - } - - /** - * Efficiently moves an item to the end of the LRU list (most recently used position). - * This is an internal optimization method that avoids the overhead of the full set() operation - * when only LRU position needs to be updated. - * - * @method moveToEnd - * @memberof LRU - * @param {Object} item - The cache item with prev/next pointers to reposition. - * @private - * @since 11.3.5 - */ - moveToEnd (item) { - // If already at the end, nothing to do - if (this.last === item) { - return; - } - - // Remove item from current position in the list - if (item.prev !== null) { - item.prev.next = item.next; - } - - if (item.next !== null) { - item.next.prev = item.prev; - } - - // Update first pointer if this was the first item - if (this.first === item) { - this.first = item.next; - } - - // Add item to the end - item.prev = this.last; - item.next = null; - - if (this.last !== null) { - this.last.next = item; - } - - this.last = item; - - // Handle edge case: if this was the only item, it's also first - if (this.first === null) { - this.first = item; - } - } - - /** - * Returns an array of all keys in the cache, ordered from least to most recently used. - * - * @method keys - * @memberof LRU - * @returns {string[]} Array of keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * cache.get('a'); // Move 'a' to most recent - * console.log(cache.keys()); // ['b', 'a'] - * @see {@link LRU#values} - * @see {@link LRU#entries} - * @since 9.0.0 - */ - keys () { - const result = new Array(this.size); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; - } - - return result; - } - - /** - * Sets a value in the cache and returns any evicted item. - * - * @method setWithEvicted - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. - * @example - * const cache = new LRU(2); - * cache.set('a', 1).set('b', 2); - * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} - * @see {@link LRU#set} - * @see {@link LRU#evict} - * @since 11.3.0 - */ - setWithEvicted (key, value, resetTtl = this.resetTtl) { - let evicted = null; - - if (this.has(key)) { - this.set(key, value, true, resetTtl); - } else { - if (this.max > 0 && this.size === this.max) { - evicted = {...this.first}; - this.evict(true); - } - - let item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return evicted; - } - - /** - * Sets a value in the cache. Updates the item's position to most recently used. - * - * @method set - * @memberof LRU - * @param {string} key - The key to set. - * @param {*} value - The value to store. - * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. - * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. - * @returns {LRU} The LRU instance for method chaining. - * @example - * cache.set('key1', 'value1') - * .set('key2', 'value2') - * .set('key3', 'value3'); - * @see {@link LRU#get} - * @see {@link LRU#setWithEvicted} - * @since 1.0.0 - */ - set (key, value, bypass = false, resetTtl = this.resetTtl) { - let item = this.items[key]; - - if (bypass || item !== undefined) { - // Existing item: update value and position - item.value = value; - - if (bypass === false && resetTtl) { - item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; - } - - // Always move to end, but the bypass parameter affects TTL reset behavior - this.moveToEnd(item); - } else { - // New item: check for eviction and create - if (this.max > 0 && this.size === this.max) { - this.evict(true); - } - - item = this.items[key] = { - expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, - key: key, - prev: this.last, - next: null, - value - }; - - if (++this.size === 1) { - this.first = item; - } else { - this.last.next = item; - } - - this.last = item; - } - - return this; - } - - /** - * Returns an array of all values in the cache for the specified keys. - * Order follows LRU order (least to most recently used). - * - * @method values - * @memberof LRU - * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. - * @returns {Array<*>} Array of values corresponding to the keys in LRU order. - * @example - * cache.set('a', 1).set('b', 2); - * console.log(cache.values()); // [1, 2] - * console.log(cache.values(['a'])); // [1] - * @see {@link LRU#keys} - * @see {@link LRU#entries} - * @since 11.1.0 - */ - values (keys = this.keys()) { - const result = new Array(keys.length); - for (let i = 0; i < keys.length; i++) { - result[i] = this.get(keys[i]); - } - - return result; - } + /** + * 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 accessing existing items via get(). + * @example + * const cache = new LRU(1000, 60000, true); // 1000 items, 1 minute TTL, reset on access + * @see {@link lru} For parameter validation + * @since 1.0.0 + */ + constructor(max = 0, ttl = 0, resetTtl = false) { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.max = max; + this.resetTtl = resetTtl; + this.size = 0; + this.ttl = ttl; + } + + /** + * Removes all items from the cache. + * + * @method clear + * @memberof LRU + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.clear(); + * console.log(cache.size); // 0 + * @since 1.0.0 + */ + clear() { + this.first = null; + this.items = Object.create(null); + this.last = null; + this.size = 0; + + return this; + } + + /** + * Removes an item from the cache by key. + * + * @method delete + * @memberof LRU + * @param {string} key - The key of the item to delete. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1'); + * cache.delete('key1'); + * console.log(cache.has('key1')); // false + * @see {@link LRU#has} + * @see {@link LRU#clear} + * @since 1.0.0 + */ + delete(key) { + const item = this.items[key]; + + if (item !== undefined) { + delete this.items[key]; + this.size--; + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + if (this.last === item) { + this.last = item.prev; + } + + item.prev = null; + item.next = null; + } + + return this; + } + + /** + * Returns an array of [key, value] pairs for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method entries + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get entries for. Defaults to all keys. + * @returns {Array>} Array of [key, value] pairs in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.entries()); // [['a', 1], ['b', 2]] + * console.log(cache.entries(['a'])); // [['a', 1]] + * @see {@link LRU#keys} + * @see {@link LRU#values} + * @since 11.1.0 + */ + entries(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const item = this.items[key]; + result[i] = [key, item !== undefined ? item.value : undefined]; + } + + return result; + } + + /** + * Removes the least recently used item from the cache. + * + * @method evict + * @memberof LRU + * @param {boolean} [bypass=false] - Whether to force eviction even when cache is empty. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('old', 'value').set('new', 'value'); + * cache.evict(); // Removes 'old' item + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + evict(bypass = false) { + if (bypass || this.size > 0) { + const item = this.first; + + if (!item) { + return this; + } + + delete this.items[item.key]; + + if (--this.size === 0) { + this.first = null; + this.last = null; + } else { + this.first = item.next; + this.first.prev = null; + } + + item.next = null; + } + + return this; + } + + /** + * Returns the expiration timestamp for a given key. + * + * @method expiresAt + * @memberof LRU + * @param {string} key - The key to check expiration for. + * @returns {number|undefined} The expiration timestamp in milliseconds, or undefined if key doesn't exist. + * @example + * const cache = new LRU(100, 5000); // 5 second TTL + * cache.set('key1', 'value1'); + * console.log(cache.expiresAt('key1')); // timestamp 5 seconds from now + * @see {@link LRU#get} + * @see {@link LRU#has} + * @since 1.0.0 + */ + expiresAt(key) { + const item = this.items[key]; + return item !== undefined ? item.expiry : undefined; + } + + /** + * Retrieves a value from the cache by key. Updates the item's position to most recently used. + * + * @method get + * @memberof LRU + * @param {string} key - The key to retrieve. + * @returns {*} The value associated with the key, or undefined if not found or expired. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.get('key1')); // 'value1' + * console.log(cache.get('nonexistent')); // undefined + * @see {@link LRU#set} + * @see {@link LRU#has} + * @since 1.0.0 + */ + get(key) { + const item = this.items[key]; + + if (item !== undefined) { + // Check TTL only if enabled to avoid unnecessary Date.now() calls + if (this.ttl > 0) { + if (item.expiry <= Date.now()) { + this.delete(key); + + return undefined; + } + } + + // Fast LRU update without full set() overhead + this.moveToEnd(item); + + return item.value; + } + + return undefined; + } + + /** + * Checks if a key exists in the cache. + * + * @method has + * @memberof LRU + * @param {string} key - The key to check for. + * @returns {boolean} True if the key exists, false otherwise. + * @example + * cache.set('key1', 'value1'); + * console.log(cache.has('key1')); // true + * console.log(cache.has('nonexistent')); // false + * @see {@link LRU#get} + * @see {@link LRU#delete} + * @since 9.0.0 + */ + has(key) { + const item = this.items[key]; + return item !== undefined && (this.ttl === 0 || item.expiry > Date.now()); + } + + /** + * Efficiently moves an item to the end of the LRU list (most recently used position). + * This is an internal optimization method that avoids the overhead of the full set() operation + * when only LRU position needs to be updated. + * + * @method moveToEnd + * @memberof LRU + * @param {Object} item - The cache item with prev/next pointers to reposition. + * @private + * @since 11.3.5 + */ + moveToEnd(item) { + if (this.last === item) { + return; + } + + if (item.prev !== null) { + item.prev.next = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } + + if (this.first === item) { + this.first = item.next; + } + + item.prev = this.last; + item.next = null; + this.last.next = item; + this.last = item; + } + + /** + * Returns an array of all keys in the cache, ordered from least to most recently used. + * + * @method keys + * @memberof LRU + * @returns {string[]} Array of keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * cache.get('a'); // Move 'a' to most recent + * console.log(cache.keys()); // ['b', 'a'] + * @see {@link LRU#values} + * @see {@link LRU#entries} + * @since 9.0.0 + */ + keys() { + const result = Array.from({ length: this.size }); + let x = this.first; + let i = 0; + + while (x !== null) { + result[i++] = x.key; + x = x.next; + } + + return result; + } + + /** + * Sets a value in the cache and returns any evicted item. + * + * @method setWithEvicted + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {Object|null} The evicted item (if any) with shape {key, value, expiry, prev, next}, or null. + * @example + * const cache = new LRU(2); + * cache.set('a', 1).set('b', 2); + * const evicted = cache.setWithEvicted('c', 3); // evicted = {key: 'a', value: 1, ...} + * @see {@link LRU#set} + * @see {@link LRU#evict} + * @since 11.3.0 + */ + setWithEvicted(key, value, resetTtl = this.resetTtl) { + let evicted = null; + let item = this.items[key]; + + if (item !== undefined) { + item.value = value; + if (resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + this.moveToEnd(item); + } else { + if (this.max > 0 && this.size === this.max) { + evicted = { + key: this.first.key, + value: this.first.value, + expiry: this.first.expiry, + }; + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return evicted; + } + + /** + * Sets a value in the cache. Updates the item's position to most recently used. + * + * @method set + * @memberof LRU + * @param {string} key - The key to set. + * @param {*} value - The value to store. + * @param {boolean} [bypass=false] - Internal parameter for setWithEvicted method. + * @param {boolean} [resetTtl=this.resetTtl] - Whether to reset the TTL for this operation. + * @returns {LRU} The LRU instance for method chaining. + * @example + * cache.set('key1', 'value1') + * .set('key2', 'value2') + * .set('key3', 'value3'); + * @see {@link LRU#get} + * @see {@link LRU#setWithEvicted} + * @since 1.0.0 + */ + set(key, value, bypass = false, resetTtl = this.resetTtl) { + let item = this.items[key]; + + if (bypass || item !== undefined) { + // Existing item: update value and position + item.value = value; + + if (bypass === false && resetTtl) { + item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl; + } + + // Always move to end, but the bypass parameter affects TTL reset behavior + this.moveToEnd(item); + } else { + // New item: check for eviction and create + if (this.max > 0 && this.size === this.max) { + this.evict(true); + } + + item = this.items[key] = { + expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl, + key: key, + prev: this.last, + next: null, + value, + }; + + if (++this.size === 1) { + this.first = item; + } else { + this.last.next = item; + } + + this.last = item; + } + + return this; + } + + /** + * Returns an array of all values in the cache for the specified keys. + * Order follows LRU order (least to most recently used). + * + * @method values + * @memberof LRU + * @param {string[]} [keys=this.keys()] - Array of keys to get values for. Defaults to all keys. + * @returns {Array<*>} Array of values corresponding to the keys in LRU order. + * @example + * cache.set('a', 1).set('b', 2); + * console.log(cache.values()); // [1, 2] + * console.log(cache.values(['a'])); // [1] + * @see {@link LRU#keys} + * @see {@link LRU#entries} + * @since 11.1.0 + */ + values(keys) { + if (keys === undefined) { + keys = this.keys(); + } + + const result = Array.from({ length: keys.length }); + for (let i = 0; i < keys.length; i++) { + const item = this.items[keys[i]]; + result[i] = item !== undefined ? item.value : undefined; + } + + return result; + } } /** @@ -469,18 +480,18 @@ export class LRU { * @see {@link LRU} * @since 1.0.0 */ -export function lru (max = 1000, ttl = 0, resetTtl = false) { - if (isNaN(max) || max < 0) { - throw new TypeError("Invalid max value"); - } +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 (isNaN(ttl) || ttl < 0) { + throw new TypeError("Invalid ttl value"); + } - if (typeof resetTtl !== "boolean") { - throw new TypeError("Invalid resetTtl value"); - } + if (typeof resetTtl !== "boolean") { + throw new TypeError("Invalid resetTtl value"); + } - return new LRU(max, ttl, resetTtl); + return new LRU(max, ttl, resetTtl); } diff --git a/tests/unit/lru.js b/tests/unit/lru.js deleted file mode 100644 index 4137fad..0000000 --- a/tests/unit/lru.js +++ /dev/null @@ -1,622 +0,0 @@ -import {LRU, lru} from "../../src/lru.js"; -import {strict as assert} from "assert"; - -describe("LRU Cache", function () { - describe("Constructor", function () { - it("should create an LRU instance with default parameters", function () { - const cache = new LRU(); - assert.equal(cache.max, 0); - assert.equal(cache.ttl, 0); - assert.equal(cache.resetTtl, false); - assert.equal(cache.size, 0); - assert.equal(cache.first, null); - assert.equal(cache.last, null); - assert.notEqual(cache.items, null); - assert.equal(typeof cache.items, "object"); - }); - - it("should create an LRU instance with custom parameters", function () { - const cache = new LRU(10, 5000, true); - assert.equal(cache.max, 10); - assert.equal(cache.ttl, 5000); - assert.equal(cache.resetTtl, true); - assert.equal(cache.size, 0); - }); - }); - - describe("lru factory function", function () { - it("should create an LRU instance with default parameters", function () { - const cache = lru(); - assert.equal(cache.max, 1000); - assert.equal(cache.ttl, 0); - assert.equal(cache.resetTtl, false); - }); - - it("should create an LRU instance with custom parameters", function () { - const cache = lru(50, 1000, true); - assert.equal(cache.max, 50); - assert.equal(cache.ttl, 1000); - assert.equal(cache.resetTtl, true); - }); - - it("should throw TypeError for invalid max value", function () { - assert.throws(() => lru("invalid"), TypeError, "Invalid max value"); - assert.throws(() => lru(-1), TypeError, "Invalid max value"); - assert.throws(() => lru(NaN), TypeError, "Invalid max value"); - }); - - it("should throw TypeError for invalid ttl value", function () { - assert.throws(() => lru(10, "invalid"), TypeError, "Invalid ttl value"); - assert.throws(() => lru(10, -1), TypeError, "Invalid ttl value"); - assert.throws(() => lru(10, NaN), TypeError, "Invalid ttl value"); - }); - - it("should throw TypeError for invalid resetTtl value", function () { - assert.throws(() => lru(10, 0, "invalid"), TypeError, "Invalid resetTtl value"); - assert.throws(() => lru(10, 0, 1), TypeError, "Invalid resetTtl value"); - }); - }); - - describe("Basic operations", function () { - let cache; - - beforeEach(function () { - cache = new LRU(3); - }); - - it("should set and get values", function () { - cache.set("key1", "value1"); - assert.equal(cache.get("key1"), "value1"); - assert.equal(cache.size, 1); - }); - - it("should return undefined for non-existent keys", function () { - assert.equal(cache.get("nonexistent"), undefined); - }); - - it("should check if key exists with has()", function () { - cache.set("key1", "value1"); - assert.equal(cache.has("key1"), true); - assert.equal(cache.has("nonexistent"), false); - }); - - it("should delete items", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - assert.equal(cache.size, 2); - - cache.delete("key1"); - assert.equal(cache.size, 1); - assert.equal(cache.has("key1"), false); - assert.equal(cache.get("key1"), undefined); - assert.equal(cache.get("key2"), "value2"); - }); - - it("should delete non-existent key gracefully", function () { - cache.set("key1", "value1"); - cache.delete("nonexistent"); - assert.equal(cache.size, 1); - assert.equal(cache.get("key1"), "value1"); - }); - - it("should clear all items", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - assert.equal(cache.size, 2); - - cache.clear(); - assert.equal(cache.size, 0); - assert.equal(cache.first, null); - assert.equal(cache.last, null); - assert.notEqual(cache.items, null); - assert.equal(typeof cache.items, "object"); - }); - - it("should support method chaining", function () { - const result = cache.set("key1", "value1").set("key2", "value2").clear(); - assert.equal(result, cache); - }); - }); - - describe("LRU eviction", function () { - let cache; - - beforeEach(function () { - cache = new LRU(3); - }); - - it("should evict least recently used item when max is reached", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - cache.set("key4", "value4"); // Should evict key1 - - assert.equal(cache.size, 3); - assert.equal(cache.has("key1"), false); - assert.equal(cache.has("key2"), true); - assert.equal(cache.has("key3"), true); - assert.equal(cache.has("key4"), true); - }); - - it("should update position when accessing existing item", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - - // Access key1 to make it most recently used - cache.get("key1"); - - cache.set("key4", "value4"); // Should evict key2, not key1 - - assert.equal(cache.has("key1"), true); - assert.equal(cache.has("key2"), false); - assert.equal(cache.has("key3"), true); - assert.equal(cache.has("key4"), true); - }); - - it("should maintain correct order in keys()", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - - let keys = cache.keys(); - assert.deepEqual(keys, ["key1", "key2", "key3"]); - - // Access key1 to move it to end - cache.get("key1"); - keys = cache.keys(); - assert.deepEqual(keys, ["key2", "key3", "key1"]); - }); - - it("should handle unlimited cache size (max = 0)", function () { - const unlimitedCache = new LRU(0); - for (let i = 0; i < 1000; i++) { - unlimitedCache.set(`key${i}`, `value${i}`); - } - assert.equal(unlimitedCache.size, 1000); - }); - }); - - describe("Eviction methods", function () { - let cache; - - beforeEach(function () { - cache = new LRU(3); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - }); - - it("should evict first item with evict()", function () { - cache.evict(); - assert.equal(cache.size, 2); - assert.equal(cache.has("key1"), false); - assert.equal(cache.has("key2"), true); - assert.equal(cache.has("key3"), true); - }); - - it("should evict with bypass flag", function () { - cache.evict(true); - assert.equal(cache.size, 2); - }); - - it("should handle evict on empty cache", function () { - cache.clear(); - cache.evict(); - assert.equal(cache.size, 0); - }); - - it("should handle evict on single item cache", function () { - cache.clear(); - cache.set("only", "value"); - cache.evict(); - assert.equal(cache.size, 0); - assert.equal(cache.first, null); - assert.equal(cache.last, null); - }); - }); - - describe("setWithEvicted method", function () { - let cache; - - beforeEach(function () { - cache = new LRU(2); - }); - - it("should return null when no eviction occurs", function () { - const evicted = cache.setWithEvicted("key1", "value1"); - assert.equal(evicted, null); - }); - - it("should return evicted item when max is reached", function () { - cache.set("key1", "value1"); - cache.set("key2", "value2"); - - const evicted = cache.setWithEvicted("key3", "value3"); - assert.notEqual(evicted, null); - assert.equal(evicted.key, "key1"); - assert.equal(evicted.value, "value1"); - }); - - it("should update existing key without eviction", function () { - cache.set("key1", "value1"); - const evicted = cache.setWithEvicted("key1", "newvalue1"); - assert.equal(evicted, null); - assert.equal(cache.get("key1"), "newvalue1"); - }); - }); - - describe("Array methods", function () { - let cache; - - beforeEach(function () { - cache = new LRU(5); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - }); - - it("should return all keys in LRU order", function () { - const keys = cache.keys(); - assert.deepEqual(keys, ["key1", "key2", "key3"]); - }); - - it("should return all values in LRU order", function () { - const values = cache.values(); - assert.deepEqual(values, ["value1", "value2", "value3"]); - }); - - it("should return values for specific keys", function () { - const values = cache.values(["key3", "key1"]); - assert.deepEqual(values, ["value3", "value1"]); - }); - - it("should return entries as [key, value] pairs", function () { - const entries = cache.entries(); - assert.deepEqual(entries, [ - ["key1", "value1"], - ["key2", "value2"], - ["key3", "value3"] - ]); - }); - - it("should return entries for specific keys", function () { - const entries = cache.entries(["key3", "key1"]); - assert.deepEqual(entries, [ - ["key3", "value3"], - ["key1", "value1"] - ]); - }); - - it("should handle empty cache", function () { - cache.clear(); - assert.deepEqual(cache.keys(), []); - assert.deepEqual(cache.values(), []); - assert.deepEqual(cache.entries(), []); - }); - }); - - describe("TTL (Time To Live)", function () { - let cache; - - beforeEach(function () { - cache = new LRU(5, 100); // 100ms TTL - }); - - it("should set expiration time", function () { - const beforeTime = Date.now(); - cache.set("key1", "value1"); - const expiresAt = cache.expiresAt("key1"); - - assert.ok(expiresAt >= beforeTime + 100); - assert.ok(expiresAt <= beforeTime + 200); // Allow some margin - }); - - it("should return undefined for non-existent key expiration", function () { - assert.equal(cache.expiresAt("nonexistent"), undefined); - }); - - it("should expire items after TTL", function (done) { - cache.set("key1", "value1"); - assert.equal(cache.get("key1"), "value1"); - - setTimeout(() => { - assert.equal(cache.get("key1"), undefined); - assert.equal(cache.has("key1"), false); - assert.equal(cache.size, 0); - done(); - }, 150); - }); - - it("should handle TTL = 0 (no expiration)", function () { - const neverExpireCache = new LRU(5, 0); - neverExpireCache.set("key1", "value1"); - assert.equal(neverExpireCache.expiresAt("key1"), 0); - }); - - it("should reset TTL when accessing with resetTtl=true", function (done) { - const resetCache = new LRU(5, 1000, true); - resetCache.set("key1", "value1"); - - // Check that expiration timestamp changes when updating with resetTtl=true - const firstExpiry = resetCache.expiresAt("key1"); - - // Small delay to ensure timestamp difference - setTimeout(() => { - resetCache.set("key1", "value1", false, true); // This should reset TTL - const secondExpiry = resetCache.expiresAt("key1"); - - assert.ok(secondExpiry > firstExpiry, "TTL should be reset"); - done(); - }, 10); - }); - - it("should not reset TTL when resetTtl=false", function (done) { - const noResetCache = new LRU(5, 100, false); - noResetCache.set("key1", "value1"); - - setTimeout(() => { - // Access the key but don't reset TTL - assert.equal(noResetCache.get("key1"), "value1"); - - // Check that it expires at original time - setTimeout(() => { - assert.equal(noResetCache.get("key1"), undefined); - done(); - }, 75); - }, 50); - }); - }); - - describe("Edge cases and complex scenarios", function () { - it("should handle updating existing key with set()", function () { - const cache = new LRU(3); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key1", "newvalue1"); // Update existing key - - assert.equal(cache.get("key1"), "newvalue1"); - assert.equal(cache.size, 2); - }); - - it("should maintain correct first/last pointers during deletion", function () { - const cache = new LRU(3); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - - // Delete middle item - cache.delete("key2"); - assert.deepEqual(cache.keys(), ["key1", "key3"]); - - // Delete first item - cache.delete("key1"); - assert.deepEqual(cache.keys(), ["key3"]); - - // Delete last item - cache.delete("key3"); - assert.deepEqual(cache.keys(), []); - assert.equal(cache.first, null); - assert.equal(cache.last, null); - }); - - it("should handle complex LRU repositioning", function () { - const cache = new LRU(4); - cache.set("a", 1); - cache.set("b", 2); - cache.set("c", 3); - cache.set("d", 4); - - // Access items in different order - cache.set("b", 22); // Move b to end - cache.get("a"); // Move a to end - cache.set("c", 33); // Move c to end - - assert.deepEqual(cache.keys(), ["d", "b", "a", "c"]); - }); - - it("should handle set with bypass parameter", function () { - const cache = new LRU(3); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - - // Set with bypass=true should not reposition but still updates to last - cache.set("key1", "newvalue1", true); - assert.deepEqual(cache.keys(), ["key2", "key1"]); - }); - - it("should handle resetTtl parameter in set method", function () { - const cache = new LRU(3, 1000, false); - const beforeTime = Date.now(); - cache.set("key1", "value1"); - - // Set with resetTtl=true should update expiry - cache.set("key1", "newvalue1", false, true); - const expiresAt = cache.expiresAt("key1"); - assert.ok(expiresAt > beforeTime + 900); // Should be close to current time + TTL - }); - - it("should handle single item cache operations", function () { - const cache = new LRU(1); - - // Set first item - cache.set("key1", "value1"); - assert.equal(cache.first, cache.last); - assert.equal(cache.size, 1); - - // Replace with second item - cache.set("key2", "value2"); - assert.equal(cache.first, cache.last); - assert.equal(cache.size, 1); - assert.equal(cache.has("key1"), false); - assert.equal(cache.has("key2"), true); - }); - - it("should handle empty cache operations", function () { - const cache = new LRU(3); - - // Operations on empty cache - assert.equal(cache.get("key1"), undefined); - assert.equal(cache.has("key1"), false); - cache.delete("key1"); // Should not throw - assert.equal(cache.expiresAt("key1"), undefined); - - // Evict on empty cache - cache.evict(); - assert.equal(cache.size, 0); - }); - - it("should handle accessing items that become last", function () { - const cache = new LRU(3); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - - // Access the last item (should not change position) - cache.get("key3"); - assert.deepEqual(cache.keys(), ["key1", "key2", "key3"]); - }); - }); - - describe("Memory and performance", function () { - it("should handle large number of operations", function () { - const cache = new LRU(1000); - - // Add 1000 items - for (let i = 0; i < 1000; i++) { - cache.set(`key${i}`, `value${i}`); - } - assert.equal(cache.size, 1000); - - // Access random items - for (let i = 0; i < 100; i++) { - const key = `key${Math.floor(Math.random() * 1000)}`; - cache.get(key); - } - - // Add more items to trigger eviction - for (let i = 1000; i < 1100; i++) { - cache.set(`key${i}`, `value${i}`); - } - assert.equal(cache.size, 1000); - }); - - it("should handle alternating set/get operations", function () { - const cache = new LRU(10); - - for (let i = 0; i < 100; i++) { - cache.set(`key${i % 10}`, `value${i}`); - cache.get(`key${(i + 5) % 10}`); - } - - assert.equal(cache.size, 10); - }); - }); - - describe("Additional coverage tests", function () { - it("should handle setWithEvicted with unlimited cache size", function () { - const cache = new LRU(0); // Unlimited size - const evicted = cache.setWithEvicted("key1", "value1"); - assert.equal(evicted, null); - assert.equal(cache.size, 1); - }); - - it("should handle setWithEvicted with first item insertion", function () { - const cache = new LRU(2); - cache.setWithEvicted("key1", "value1"); - assert.equal(cache.size, 1); - assert.equal(cache.first, cache.last); - }); - - it("should handle bypass parameter with resetTtl false", function () { - const cache = new LRU(3, 1000, false); - cache.set("key1", "value1"); - const originalExpiry = cache.expiresAt("key1"); - - // Call set with bypass=true, resetTtl=false - cache.set("key1", "newvalue1", true, false); - const newExpiry = cache.expiresAt("key1"); - - // TTL should not be reset - assert.equal(originalExpiry, newExpiry); - }); - - it("should set expiry when using setWithEvicted with ttl > 0", function () { - const cache = new LRU(2, 100); // ttl > 0 - const before = Date.now(); - cache.set("a", 1); - cache.set("b", 2); - const evicted = cache.setWithEvicted("c", 3); // triggers eviction and new item creation - assert.notEqual(evicted, null); - const expiry = cache.expiresAt("c"); - assert.ok(expiry >= before + 100); - assert.ok(expiry <= before + 250); // allow some margin - }); - - it("should set expiry to 0 when resetTtl=true and ttl=0 on update", function () { - const cache = new LRU(2, 0); // ttl = 0 - cache.set("x", 1); - assert.equal(cache.expiresAt("x"), 0); - // update existing key with resetTtl=true to exercise branch in set() - cache.set("x", 2, false, true); - assert.equal(cache.expiresAt("x"), 0); - }); - - it("should handle moveToEnd edge case by direct method invocation", function () { - const cache = new LRU(1); - - // Add a single item - cache.set("only", "value"); - - // Create a minimal test case that directly exercises the uncovered lines - // The edge case in moveToEnd (lines 275-276) occurs when: - // 1. An item is moved that was the first item (making first = item.next = null) - // 2. But the cache wasn't empty (last !== null) - // 3. The condition if (this.first === null) triggers to restore consistency - - const item = cache.first; - assert.equal(cache.first, cache.last); - assert.equal(item, cache.last); - - // Since moveToEnd has early return for item === last, we need to - // create a scenario where the item is first but not last - // Let's create a second dummy item and manipulate pointers - const dummyItem = { - key: "dummy", - value: "dummy", - prev: item, - next: null, - expiry: 0 - }; - - // Set up the linked list: item <-> dummyItem - item.next = dummyItem; - cache.last = dummyItem; - - // Now item is first but not last, so moveToEnd won't early return - // When moveToEnd processes item: - // 1. Sets first = item.next (which is dummyItem) - // 2. Removes item from its position - // 3. But then we manipulate to make first = null to trigger the edge case - - // Temporarily null out the next pointer to simulate the edge case - const originalNext = item.next; - item.next = null; - - // This manipulation will cause first to become null in moveToEnd - // triggering the if (this.first === null) condition on lines 274-276 - cache.first = null; - cache.last = dummyItem; // last is not null - - // Now call moveToEnd - this should trigger the uncovered lines - cache.moveToEnd(item); - - // Verify the edge case was handled correctly - assert.equal(cache.first, item); - - // Restore the item for cleanup - item.next = originalNext; - }); - }); -}); - diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js new file mode 100644 index 0000000..60c9de5 --- /dev/null +++ b/tests/unit/lru.test.js @@ -0,0 +1,552 @@ +import { LRU, lru } from "../../src/lru.js"; +import { describe, it, beforeEach } from "node:test"; +import assert from "node:assert"; + +describe("LRU Cache", function () { + describe("Constructor", function () { + it("should create an LRU instance with default parameters", function () { + const cache = new LRU(); + assert.equal(cache.max, 0); + assert.equal(cache.ttl, 0); + assert.equal(cache.resetTtl, false); + assert.equal(cache.size, 0); + assert.equal(cache.first, null); + assert.equal(cache.last, null); + assert.notEqual(cache.items, null); + assert.equal(typeof cache.items, "object"); + }); + + it("should create an LRU instance with custom parameters", function () { + const cache = new LRU(10, 5000, true); + assert.equal(cache.max, 10); + assert.equal(cache.ttl, 5000); + assert.equal(cache.resetTtl, true); + assert.equal(cache.size, 0); + }); + }); + + describe("lru factory function", function () { + it("should create an LRU instance with default parameters", function () { + const cache = lru(); + assert.equal(cache.max, 1000); + assert.equal(cache.ttl, 0); + assert.equal(cache.resetTtl, false); + }); + + it("should create an LRU instance with custom parameters", function () { + const cache = lru(50, 1000, true); + assert.equal(cache.max, 50); + assert.equal(cache.ttl, 1000); + assert.equal(cache.resetTtl, true); + }); + + it("should throw TypeError for invalid max value", function () { + assert.throws(() => lru("invalid"), TypeError, "Invalid max value"); + assert.throws(() => lru(-1), TypeError, "Invalid max value"); + assert.throws(() => lru(NaN), TypeError, "Invalid max value"); + }); + + it("should throw TypeError for invalid ttl value", function () { + assert.throws(() => lru(10, "invalid"), TypeError, "Invalid ttl value"); + assert.throws(() => lru(10, -1), TypeError, "Invalid ttl value"); + assert.throws(() => lru(10, NaN), TypeError, "Invalid ttl value"); + }); + + it("should throw TypeError for invalid resetTtl value", function () { + assert.throws(() => lru(10, 0, "invalid"), TypeError, "Invalid resetTtl value"); + assert.throws(() => lru(10, 0, 1), TypeError, "Invalid resetTtl value"); + }); + }); + + describe("Basic operations", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + }); + + it("should set and get values", function () { + cache.set("key1", "value1"); + assert.equal(cache.get("key1"), "value1"); + assert.equal(cache.size, 1); + }); + + it("should return undefined for non-existent keys", function () { + assert.equal(cache.get("nonexistent"), undefined); + }); + + it("should check if key exists with has()", function () { + cache.set("key1", "value1"); + assert.equal(cache.has("key1"), true); + assert.equal(cache.has("nonexistent"), false); + }); + + it("should delete items", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + assert.equal(cache.size, 2); + + cache.delete("key1"); + assert.equal(cache.size, 1); + assert.equal(cache.has("key1"), false); + assert.equal(cache.get("key1"), undefined); + assert.equal(cache.get("key2"), "value2"); + }); + + it("should delete non-existent key gracefully", function () { + cache.set("key1", "value1"); + cache.delete("nonexistent"); + assert.equal(cache.size, 1); + assert.equal(cache.get("key1"), "value1"); + }); + + it("should clear all items", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + assert.equal(cache.size, 2); + + cache.clear(); + assert.equal(cache.size, 0); + assert.equal(cache.first, null); + assert.equal(cache.last, null); + assert.notEqual(cache.items, null); + assert.equal(typeof cache.items, "object"); + }); + + it("should support method chaining", function () { + const result = cache.set("key1", "value1").set("key2", "value2").clear(); + assert.equal(result, cache); + }); + }); + + describe("LRU eviction", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + }); + + it("should evict least recently used item when max is reached", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + cache.set("key4", "value4"); + + assert.equal(cache.size, 3); + assert.equal(cache.has("key1"), false); + assert.equal(cache.has("key2"), true); + assert.equal(cache.has("key3"), true); + assert.equal(cache.has("key4"), true); + }); + + it("should update position when accessing existing item", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + cache.get("key1"); + + cache.set("key4", "value4"); + + assert.equal(cache.has("key1"), true); + assert.equal(cache.has("key2"), false); + assert.equal(cache.has("key3"), true); + assert.equal(cache.has("key4"), true); + }); + + it("should maintain correct order in keys()", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + let keys = cache.keys(); + assert.deepEqual(keys, ["key1", "key2", "key3"]); + + cache.get("key1"); + keys = cache.keys(); + assert.deepEqual(keys, ["key2", "key3", "key1"]); + }); + + it("should handle unlimited cache size (max = 0)", function () { + const unlimitedCache = new LRU(0); + for (let i = 0; i < 1000; i++) { + unlimitedCache.set(`key${i}`, `value${i}`); + } + assert.equal(unlimitedCache.size, 1000); + }); + }); + + describe("Eviction methods", function () { + let cache; + + beforeEach(function () { + cache = new LRU(3); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + }); + + it("should evict first item with evict()", function () { + cache.evict(); + assert.equal(cache.size, 2); + assert.equal(cache.has("key1"), false); + assert.equal(cache.has("key2"), true); + assert.equal(cache.has("key3"), true); + }); + + it("should evict with bypass flag", function () { + cache.evict(true); + assert.equal(cache.size, 2); + }); + + it("should handle evict on empty cache", function () { + cache.clear(); + cache.evict(); + assert.equal(cache.size, 0); + }); + + it("should handle evict on single item cache", function () { + cache.clear(); + cache.set("only", "value"); + cache.evict(); + assert.equal(cache.size, 0); + assert.equal(cache.first, null); + assert.equal(cache.last, null); + }); + }); + + describe("setWithEvicted method", function () { + let cache; + + beforeEach(function () { + cache = new LRU(2); + }); + + it("should return null when no eviction occurs", function () { + const evicted = cache.setWithEvicted("key1", "value1"); + assert.equal(evicted, null); + }); + + it("should return evicted item when max is reached", function () { + cache.set("key1", "value1"); + cache.set("key2", "value2"); + + const evicted = cache.setWithEvicted("key3", "value3"); + assert.notEqual(evicted, null); + assert.equal(evicted.key, "key1"); + assert.equal(evicted.value, "value1"); + }); + + it("should update existing key without eviction", function () { + cache.set("key1", "value1"); + const evicted = cache.setWithEvicted("key1", "newvalue1"); + assert.equal(evicted, null); + assert.equal(cache.get("key1"), "newvalue1"); + }); + }); + + describe("Array methods", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + }); + + it("should return all keys in LRU order", function () { + const keys = cache.keys(); + assert.deepEqual(keys, ["key1", "key2", "key3"]); + }); + + it("should return all values in LRU order", function () { + const values = cache.values(); + assert.deepEqual(values, ["value1", "value2", "value3"]); + }); + + it("should return values for specific keys", function () { + const values = cache.values(["key3", "key1"]); + assert.deepEqual(values, ["value3", "value1"]); + }); + + it("should return entries as [key, value] pairs", function () { + const entries = cache.entries(); + assert.deepEqual(entries, [ + ["key1", "value1"], + ["key2", "value2"], + ["key3", "value3"], + ]); + }); + + it("should return entries for specific keys", function () { + const entries = cache.entries(["key3", "key1"]); + assert.deepEqual(entries, [ + ["key3", "value3"], + ["key1", "value1"], + ]); + }); + + it("should handle empty cache", function () { + cache.clear(); + assert.deepEqual(cache.keys(), []); + assert.deepEqual(cache.values(), []); + assert.deepEqual(cache.entries(), []); + }); + }); + + describe("TTL (Time To Live)", function () { + let cache; + + beforeEach(function () { + cache = new LRU(5, 100); + }); + + it("should set expiration time", function () { + const beforeTime = Date.now(); + cache.set("key1", "value1"); + const expiresAt = cache.expiresAt("key1"); + + assert.ok(expiresAt >= beforeTime + 100); + assert.ok(expiresAt <= beforeTime + 200); + }); + + it("should return undefined for non-existent key expiration", function () { + assert.equal(cache.expiresAt("nonexistent"), undefined); + }); + + it("should expire items after TTL", async function () { + cache.set("key1", "value1"); + assert.equal(cache.get("key1"), "value1"); + + await new Promise((resolve) => setTimeout(resolve, 150)); + assert.equal(cache.get("key1"), undefined); + assert.equal(cache.has("key1"), false); + assert.equal(cache.size, 0); + }); + + it("should handle TTL = 0 (no expiration)", function () { + const neverExpireCache = new LRU(5, 0); + neverExpireCache.set("key1", "value1"); + assert.equal(neverExpireCache.expiresAt("key1"), 0); + }); + + it("should reset TTL when accessing with resetTtl=true", async function () { + const resetCache = new LRU(5, 1000, true); + resetCache.set("key1", "value1"); + + const firstExpiry = resetCache.expiresAt("key1"); + + await new Promise((resolve) => setTimeout(resolve, 10)); + resetCache.set("key1", "value1", false, true); + const secondExpiry = resetCache.expiresAt("key1"); + + assert.ok(secondExpiry > firstExpiry, "TTL should be reset"); + }); + + it("should not reset TTL when resetTtl=false", async function () { + const noResetCache = new LRU(5, 100, false); + noResetCache.set("key1", "value1"); + + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.equal(noResetCache.get("key1"), "value1"); + + await new Promise((resolve) => setTimeout(resolve, 75)); + assert.equal(noResetCache.get("key1"), undefined); + }); + }); + + describe("Edge cases and complex scenarios", function () { + it("should handle updating existing key with set()", function () { + const cache = new LRU(3); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key1", "newvalue1"); + + assert.equal(cache.get("key1"), "newvalue1"); + assert.equal(cache.size, 2); + }); + + it("should maintain correct first/last pointers during deletion", function () { + const cache = new LRU(3); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + cache.delete("key2"); + assert.deepEqual(cache.keys(), ["key1", "key3"]); + + cache.delete("key1"); + assert.deepEqual(cache.keys(), ["key3"]); + + cache.delete("key3"); + assert.deepEqual(cache.keys(), []); + assert.equal(cache.first, null); + assert.equal(cache.last, null); + }); + + it("should handle complex LRU repositioning", function () { + const cache = new LRU(4); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + cache.set("d", 4); + + cache.set("b", 22); + cache.get("a"); + cache.set("c", 33); + + assert.deepEqual(cache.keys(), ["d", "b", "a", "c"]); + }); + + it("should handle set with bypass parameter", function () { + const cache = new LRU(3); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + + cache.set("key1", "newvalue1", true); + assert.deepEqual(cache.keys(), ["key2", "key1"]); + }); + + it("should handle resetTtl parameter in set method", function () { + const cache = new LRU(3, 1000, false); + const beforeTime = Date.now(); + cache.set("key1", "value1"); + + cache.set("key1", "newvalue1", false, true); + const expiresAt = cache.expiresAt("key1"); + assert.ok(expiresAt > beforeTime + 900); + }); + + it("should handle single item cache operations", function () { + const cache = new LRU(1); + + cache.set("key1", "value1"); + assert.equal(cache.first, cache.last); + assert.equal(cache.size, 1); + + cache.set("key2", "value2"); + assert.equal(cache.first, cache.last); + assert.equal(cache.size, 1); + assert.equal(cache.has("key1"), false); + assert.equal(cache.has("key2"), true); + }); + + it("should handle empty cache operations", function () { + const cache = new LRU(3); + + assert.equal(cache.get("key1"), undefined); + assert.equal(cache.has("key1"), false); + cache.delete("key1"); + assert.equal(cache.expiresAt("key1"), undefined); + + cache.evict(); + assert.equal(cache.size, 0); + }); + + it("should handle accessing items that become last", function () { + const cache = new LRU(3); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + + cache.get("key3"); + assert.deepEqual(cache.keys(), ["key1", "key2", "key3"]); + }); + }); + + describe("Memory and performance", function () { + it("should handle large number of operations", function () { + const cache = new LRU(1000); + + for (let i = 0; i < 1000; i++) { + cache.set(`key${i}`, `value${i}`); + } + assert.equal(cache.size, 1000); + + for (let i = 0; i < 100; i++) { + const key = `key${Math.floor(Math.random() * 1000)}`; + cache.get(key); + } + + for (let i = 1000; i < 1100; i++) { + cache.set(`key${i}`, `value${i}`); + } + assert.equal(cache.size, 1000); + }); + + it("should handle alternating set/get operations", function () { + const cache = new LRU(10); + + for (let i = 0; i < 100; i++) { + cache.set(`key${i % 10}`, `value${i}`); + cache.get(`key${(i + 5) % 10}`); + } + + assert.equal(cache.size, 10); + }); + }); + + describe("Additional coverage tests", function () { + it("should handle setWithEvicted with unlimited cache size", function () { + const cache = new LRU(0); + const evicted = cache.setWithEvicted("key1", "value1"); + assert.equal(evicted, null); + assert.equal(cache.size, 1); + }); + + it("should handle setWithEvicted with first item insertion", function () { + const cache = new LRU(2); + cache.setWithEvicted("key1", "value1"); + assert.equal(cache.size, 1); + assert.equal(cache.first, cache.last); + }); + + it("should handle bypass parameter with resetTtl false", function () { + const cache = new LRU(3, 1000, false); + cache.set("key1", "value1"); + const originalExpiry = cache.expiresAt("key1"); + + cache.set("key1", "newvalue1", true, false); + const newExpiry = cache.expiresAt("key1"); + + assert.equal(originalExpiry, newExpiry); + }); + + it("should set expiry when using setWithEvicted with ttl > 0", function () { + const cache = new LRU(2, 100); + const before = Date.now(); + cache.set("a", 1); + cache.set("b", 2); + const evicted = cache.setWithEvicted("c", 3); + assert.notEqual(evicted, null); + const expiry = cache.expiresAt("c"); + assert.ok(expiry >= before + 100); + assert.ok(expiry <= before + 250); + }); + + it("should set expiry to 0 when resetTtl=true and ttl=0 on update", function () { + const cache = new LRU(2, 0); + cache.set("x", 1); + assert.equal(cache.expiresAt("x"), 0); + cache.set("x", 2, false, true); + assert.equal(cache.expiresAt("x"), 0); + }); + + it("should handle evict with bypass on empty cache", function () { + const cache = new LRU(3); + cache.evict(true); + assert.equal(cache.size, 0); + assert.equal(cache.first, null); + assert.equal(cache.last, null); + }); + + it("should set expiry to 0 when resetTtl=true and ttl=0 on setWithEvicted", function () { + const cache = new LRU(2, 0); + cache.set("x", 1); + assert.equal(cache.expiresAt("x"), 0); + cache.setWithEvicted("x", 2, true); + assert.equal(cache.expiresAt("x"), 0); + }); + }); +}); diff --git a/types/lru.d.ts b/types/lru.d.ts index 3f83b9d..f401d7d 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -24,6 +24,18 @@ export interface LRUItem { value: T; } +/** + * Represents the evicted item returned by setWithEvicted(). + */ +export interface EvictedItem { + /** The key of the evicted item */ + key: any; + /** The value of the evicted item */ + value: T; + /** The expiration timestamp of the evicted item */ + expiry: number; +} + /** * High-performance Least Recently Used (LRU) cache with optional TTL support. * All core operations (get, set, delete) are O(1). @@ -37,7 +49,7 @@ export class LRU { * @param resetTtl Whether to reset TTL when accessing existing items via get() (default: false) */ constructor(max?: number, ttl?: number, resetTtl?: boolean); - + /** Pointer to the least recently used item (first to be evicted) */ readonly first: LRUItem | null; /** Hash map for O(1) key-based access to cache nodes */ @@ -52,86 +64,86 @@ export class LRU { readonly size: number; /** Time-to-live in milliseconds (0 = no expiration) */ readonly ttl: number; - + /** * Removes all items from the cache. * @returns The LRU instance for method chaining */ clear(): this; - + /** * Removes an item from the cache by key. * @param key The key of the item to delete * @returns The LRU instance for method chaining */ delete(key: any): this; - + /** * Returns an array of [key, value] pairs for the specified keys. * Order follows LRU order (least to most recently used). * @param keys Array of keys to get entries for (defaults to all keys) * @returns Array of [key, value] pairs in LRU order */ - entries(keys?: any[]): [any, T][]; - + entries(keys?: any[]): [any, T | undefined][]; + /** * Removes the least recently used item from the cache. - * @param bypass Whether to force eviction even when cache is empty + * @param bypass Whether to force eviction even when cache is empty (default: false) * @returns The LRU instance for method chaining */ evict(bypass?: boolean): this; - + /** * Returns the expiration timestamp for a given key. * @param key The key to check expiration for * @returns The expiration timestamp in milliseconds, or undefined if key doesn't exist */ expiresAt(key: any): number | undefined; - + /** * Retrieves a value from the cache by key. Updates the item's position to most recently used. * @param key The key to retrieve * @returns The value associated with the key, or undefined if not found or expired */ get(key: any): T | undefined; - + /** - * Checks if a key exists in the cache. + * Checks if a key exists in the cache (not expired). * @param key The key to check for - * @returns True if the key exists, false otherwise + * @returns True if the key exists and is not expired, false otherwise */ has(key: any): boolean; - + /** * Returns an array of all keys in the cache, ordered from least to most recently used. * @returns Array of keys in LRU order */ keys(): any[]; - + /** * Sets a value in the cache. Updates the item's position to most recently used. * @param key The key to set * @param value The value to store - * @param bypass Internal parameter for setWithEvicted method - * @param resetTtl Whether to reset the TTL for this operation + * @param bypass Internal parameter for setWithEvicted method (default: false) + * @param resetTtl Whether to reset the TTL for this operation (default: this.resetTtl) * @returns The LRU instance for method chaining */ set(key: any, value: T, bypass?: boolean, resetTtl?: boolean): this; - + /** * Sets a value in the cache and returns any evicted item. * @param key The key to set * @param value The value to store - * @param resetTtl Whether to reset the TTL for this operation - * @returns The evicted item (if any) or null + * @param resetTtl Whether to reset the TTL for this operation (default: this.resetTtl) + * @returns The evicted item (if any) with {key, value, expiry} or null */ - setWithEvicted(key: any, value: T, resetTtl?: boolean): LRUItem | null; - + setWithEvicted(key: any, value: T, resetTtl?: boolean): EvictedItem | null; + /** * Returns an array of all values in the cache for the specified keys. * Order follows LRU order (least to most recently used). * @param keys Array of keys to get values for (defaults to all keys) - * @returns Array of values corresponding to the keys in LRU order + * @returns Array of values corresponding to the keys (undefined for missing/expired keys) */ - values(keys?: any[]): T[]; + values(keys?: any[]): (T | undefined)[]; }