更新
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 asamuzaK (Kazz)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# generational-cache
|
||||
|
||||
[](https://github.com/asamuzaK/generationalCache/actions/workflows/ci.yaml)
|
||||
[](https://github.com/asamuzaK/generationalCache/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://www.npmjs.com/package/@asamuzakjp/generational-cache)
|
||||
|
||||
A lightweight, **generational pseudo-LRU (Least Recently Used) cache** with strict maximum size limits.
|
||||
|
||||
## How it Works
|
||||
|
||||
`GenerationalCache` maintains two internal `Map` objects: `current` and `old`.
|
||||
|
||||
1. **Insertion**: New items are always added to the `current` generation.
|
||||
2. **Promotion**: If you `get` an item that exists in the `old` generation, it is promoted to the `current` generation to ensure it stays in the cache longer.
|
||||
3. **Generation Swapping**: Once the `current` generation reaches the boundary size ($max / 2$), the `old` generation is discarded, the `current` generation becomes the `old` generation, and a new empty `current` generation is created.
|
||||
|
||||
This "pseudo-LRU" approach avoids the overhead of updating timestamps or complex linked list pointers on every single access.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
npm i @asamuzakjp/generational-cache
|
||||
```
|
||||
|
||||
## Usage
|
||||
```javascript
|
||||
import { GenerationalCache } from '@asamuzakjp/generational-cache';
|
||||
|
||||
// Initialize with a max capacity of 1024 items
|
||||
const cache = new GenerationalCache(1024);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `new GenerationalCache(max)`
|
||||
|
||||
Creates a new cache instance.
|
||||
|
||||
* **`max`** *(number)*: The maximum number of items the cache can hold.
|
||||
If the specified value is less than 4, or if an invalid value is specified, the default value of 4 will be used.
|
||||
|
||||
### Properties
|
||||
|
||||
* **`cache.size`** *(number, read-only)*: Returns the total number of *entries* currently in the cache.
|
||||
**Note:** To optimize for write speed, this library allows temporary key duplication between generations.
|
||||
Therefore, this value may not always reflect the exact count of unique *keys*.
|
||||
* **`cache.max`** *(number)*: Gets or sets the maximum capacity.
|
||||
**Note:** Updating this property dynamically will invoke `cache.clear()` to safely recalculate boundaries.
|
||||
|
||||
### Methods
|
||||
|
||||
* **`cache.get(key)`**
|
||||
Retrieves an item.
|
||||
If the item is found in the older generation, it is automatically promoted to the current generation to prevent it from being evicted during the next swap.
|
||||
* **Returns:** The value associated with the key, or `undefined`.
|
||||
* **`cache.set(key, value)`**
|
||||
Adds or updates an item. If adding this item pushes the current generation's size to the boundary threshold (`max / 2`), a generation swap is triggered, and the old generation is discarded.
|
||||
* **Returns:** The cache instance itself (allows chaining).
|
||||
* **`cache.has(key)`**
|
||||
Checks if a key exists in the cache (in either generation).
|
||||
* **Returns:** `true` if the key exists, otherwise `false`.
|
||||
* **`cache.delete(key)`**
|
||||
Removes an item from the cache.
|
||||
* **Returns:** `true` if the item existed and was removed, otherwise `false`.
|
||||
* **`cache.clear()`**
|
||||
Empties all items from the cache by dropping references to the internal Maps.
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks are divided into two states to simulate real-world conditions:
|
||||
- **Cold State**: Measured with aggressive internal Garbage Collection to observe performance before full V8 TurboFan optimizations.
|
||||
- **Warm State**: Measured after sufficient warmup, representing sustained throughput under optimal JIT compilation.
|
||||
|
||||
*The results below reflect the sustained operations per second (ops/sec), calculated from the average latency (`ns/iter`). Higher values indicate better performance.*
|
||||
|
||||
### Benchmark Environment
|
||||
- **Engine:** Node.js v24.x (V8)
|
||||
- **Measurement:** [mitata](https://github.com/evanwashere/mitata).
|
||||
- **Comparison:** [LRUCache](https://www.npmjs.com/package/lru-cache) (v11.x), [QuickLRU](https://www.npmjs.com/package/quick-lru) (v7.x), [Mnemonist](https://www.npmjs.com/package/mnemonist) (v0.40.x)
|
||||
|
||||
### 1. Small Cache (Max Size = 512)
|
||||
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Set** | Cold | **17,733,640 ops/sec** | 4,933,885 ops/sec | 13,506,212 ops/sec | **17,229,496 ops/sec** |
|
||||
| | Warm | **23,030,861 ops/sec** | 15,216,068 ops/sec | 18,175,209 ops/sec | 19,409,937 ops/sec |
|
||||
| **Get** | Cold | 17,717,930 ops/sec | 7,633,587 ops/sec | 13,734,377 ops/sec | **30,731,407 ops/sec** |
|
||||
| | Warm | 21,724,961 ops/sec | 24,148,756 ops/sec | 16,385,384 ops/sec | **35,688,793 ops/sec** |
|
||||
| **Eviction** | Cold | **16,700,066 ops/sec** | 6,953,619 ops/sec | 13,285,505 ops/sec | 4,925,865 ops/sec |
|
||||
| | Warm | **23,148,148 ops/sec** | 9,040,773 ops/sec | 16,903,313 ops/sec | 8,037,293 ops/sec |
|
||||
|
||||
### 2. Medium Cache (Max Size = 2,048)
|
||||
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Set** | Cold | **15,987,210 ops/sec** | 4,874,957 ops/sec | 11,849,745 ops/sec | **15,309,246 ops/sec** |
|
||||
| | Warm | **19,716,088 ops/sec** | 13,345,789 ops/sec | 14,755,791 ops/sec | 17,325,017 ops/sec |
|
||||
| **Get** | Cold | 14,994,751 ops/sec | 7,950,389 ops/sec | 11,503,508 ops/sec | **23,651,844 ops/sec** |
|
||||
| | Warm | 17,825,311 ops/sec | 18,789,928 ops/sec | 13,838,915 ops/sec | **31,289,111 ops/sec** |
|
||||
| **Eviction** | Cold | **16,355,904 ops/sec** | 6,757,669 ops/sec | 12,074,378 ops/sec | 5,175,983 ops/sec |
|
||||
| | Warm | **21,982,853 ops/sec** | 8,089,305 ops/sec | 15,309,246 ops/sec | 7,132,158 ops/sec |
|
||||
|
||||
### 3. Large Cache (Max Size = 8,192)
|
||||
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Set** | Cold | **13,679,890 ops/sec** | 3,954,288 ops/sec | 8,126,777 ops/sec | 10,972,130 ops/sec |
|
||||
| | Warm | **20,593,080 ops/sec** | 12,054,001 ops/sec | 12,995,451 ops/sec | 15,600,624 ops/sec |
|
||||
| **Get** | Cold | 11,918,951 ops/sec | 5,785,363 ops/sec | 9,067,827 ops/sec | **16,784,155 ops/sec** |
|
||||
| | Warm | 16,781,339 ops/sec | 17,247,326 ops/sec | 12,733,987 ops/sec | **31,436,655 ops/sec** |
|
||||
| **Eviction** | Cold | **13,561,160 ops/sec** | 5,510,249 ops/sec | 9,642,271 ops/sec | 4,040,404 ops/sec |
|
||||
| | Warm | **21,128,248 ops/sec** | 7,082,152 ops/sec | 13,208,294 ops/sec | 6,023,007 ops/sec |
|
||||
|
||||
### 4. Cyclic Access (Max Size = 8,192 / Working Set = 5,000)
|
||||
| Metric | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Hit Rate** | 78.30% | **100.00%** | **100.00%** | **100.00%** |
|
||||
| **Throughput** | 10,365,916 ops/sec | 40,832,993 ops/sec | 40,950,040 ops/sec | **48,426,150 ops/sec** |
|
||||
|
||||
## Key Characteristics
|
||||
|
||||
* **High Eviction Efficiency**: `GenerationalCache` demonstrates strong throughput during high-turnover workloads, maintaining a performance margin compared to standard LRU designs in large-scale eviction scenarios.
|
||||
* **Predictable Scalability**: While other libraries may experience performance degradation as cache size increases, `GenerationalCache` maintains consistent throughput due to its generational swap mechanism.
|
||||
* **Balanced Read/Write**: It provides stable and competitive performance across all basic operations (`get`, `set`), making it suitable for both read-heavy and write-heavy environments.
|
||||
* **Trade-offs**: In cyclic access patterns where the working set is greater than `max / 2` but smaller than `max`, `GenerationalCache` will experience frequent generation swaps and cache misses. To maximize the performance benefits of `GenerationalCache`, it is often better to keep the `max` size small enough to allow some evictions, rather than trying to fit the entire working set.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@asamuzakjp/generational-cache",
|
||||
"description": "A generational pseudo-LRU cache with strict maximum size limits.",
|
||||
"author": "asamuzaK",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/asamuzaK/generationalCache",
|
||||
"bugs": "https://github.com/asamuzaK/generationalCache/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/asamuzaK/generationalCache.git"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"types"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"default": "./src/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"c8": "^11.0.0",
|
||||
"commander": "^14.0.3",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-regexp": "^3.1.0",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"globals": "^17.5.0",
|
||||
"lru-cache": "^11.3.5",
|
||||
"mitata": "^1.0.34",
|
||||
"mnemonist": "^0.40.3",
|
||||
"mocha": "^11.7.5",
|
||||
"neostandard": "^0.13.0",
|
||||
"prettier": "^3.8.2",
|
||||
"quick-lru": "^7.3.0",
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"c8": {
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"eslint": {
|
||||
"brace-expansion": "^1.1.13"
|
||||
},
|
||||
"serialize-javascript": "^7.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"bench": "node --expose-gc ./benchmark/benchmark.js",
|
||||
"bench:worst": "node --expose-gc ./benchmark/worst-case-benchmark.js",
|
||||
"build": "npm run tsc && npm run lint && npm test",
|
||||
"lint": "eslint --fix .",
|
||||
"test": "c8 --reporter=text mocha --exit test/*.test.js",
|
||||
"tsc": "node scripts/index clean --dir=types -i && npx tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"version": "1.0.1"
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @file generational-cache.js
|
||||
* A generational pseudo-LRU cache with strict maximum size limits.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template K, V
|
||||
*/
|
||||
export class GenerationalCache {
|
||||
#max;
|
||||
#boundary;
|
||||
#current = new Map();
|
||||
#old = new Map();
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the GenerationalCache class.
|
||||
* @param {number} max - The maximum number of items the cache can hold.
|
||||
*/
|
||||
constructor(max) {
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of `entries` currently in the cache.
|
||||
* @note To optimize for write speed, this library allows temporary key
|
||||
* duplication between generations. Therefore, this value may not always
|
||||
* reflect the exact count of unique `keys`.
|
||||
* @returns {number} The total entry count.
|
||||
*/
|
||||
get size() {
|
||||
return this.#current.size + this.#old.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum capacity of the cache.
|
||||
* @returns {number} The maximum size limit.
|
||||
*/
|
||||
get max() {
|
||||
return this.#max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum capacity of the cache and recalculates the boundary.
|
||||
* Clears the cache when updated.
|
||||
* @param {number} value - The new maximum capacity to set.
|
||||
*/
|
||||
set max(value) {
|
||||
if (Number.isFinite(value) && value > 4) {
|
||||
this.#max = value;
|
||||
this.#boundary = Math.ceil(value / 2);
|
||||
} else {
|
||||
this.#max = 4;
|
||||
this.#boundary = 2;
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item from the cache.
|
||||
* If the item is in the older generation, it gets promoted to the current
|
||||
* generation.
|
||||
* @param {K} key - The key of the element to return.
|
||||
* @returns {V | undefined} The element associated with the specified key, or
|
||||
* undefined if the key cannot be found.
|
||||
*/
|
||||
get(key) {
|
||||
let value = this.#current.get(key);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
value = this.#old.get(key);
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates an element with a specified key and a value to the cache.
|
||||
* @param {K} key - The key of the element to add.
|
||||
* @param {V} value - The value of the element to add.
|
||||
* @returns {GenerationalCache} The cache object itself.
|
||||
*/
|
||||
set(key, value) {
|
||||
this.#current.set(key, value);
|
||||
// Swap generations if the current map reaches the boundary
|
||||
if (this.#current.size >= this.#boundary) {
|
||||
this.#old = this.#current;
|
||||
this.#current = new Map();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether an element with the specified key
|
||||
* exists or not.
|
||||
* @param {K} key - The key of the element to test for presence.
|
||||
* @returns {boolean} true if an element with the specified key exists in the
|
||||
* cache; otherwise false.
|
||||
*/
|
||||
has(key) {
|
||||
return this.#current.has(key) || this.#old.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified element from the cache.
|
||||
* @param {K} key - The key of the element to remove.
|
||||
* @returns {boolean} true if an element in the cache existed and has been
|
||||
* removed, or false if the element does not exist.
|
||||
*/
|
||||
delete(key) {
|
||||
const deletedFromCurrent = this.#current.delete(key);
|
||||
const deletedFromOld = this.#old.delete(key);
|
||||
return deletedFromCurrent || deletedFromOld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from the cache.
|
||||
*/
|
||||
clear() {
|
||||
this.#current.clear();
|
||||
this.#old.clear();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export class GenerationalCache<K, V> {
|
||||
constructor(max: number);
|
||||
set max(value: number);
|
||||
get max(): number;
|
||||
get size(): number;
|
||||
get(key: K): V | undefined;
|
||||
set(key: K, value: V): GenerationalCache<any, any>;
|
||||
has(key: K): boolean;
|
||||
delete(key: K): boolean;
|
||||
clear(): void;
|
||||
#private;
|
||||
}
|
||||
Reference in New Issue
Block a user