@asamuzakjp/generational-cache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/package.json +63 -0
- package/src/index.js +125 -0
- package/types/index.d.ts +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 asamuzaK (Kazz)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# generational-cache
|
|
2
|
+
|
|
3
|
+
[](https://github.com/asamuzaK/generationalCache/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://github.com/asamuzaK/generationalCache/actions/workflows/github-code-scanning/codeql)
|
|
5
|
+
[](https://www.npmjs.com/package/@asamuzakjp/generational-cache)
|
|
6
|
+
|
|
7
|
+
A lightweight, **generational pseudo-LRU (Least Recently Used) cache** with strict maximum size limits.
|
|
8
|
+
|
|
9
|
+
`GenerationalCache` uses a two-generation strategy (Current and Old) to provide a balance between memory efficiency and high-speed access, making it particularly effective for workloads with frequent evictions.
|
|
10
|
+
|
|
11
|
+
## How it Works
|
|
12
|
+
|
|
13
|
+
`GenerationalCache` maintains two internal `Map` objects: `current` and `old`.
|
|
14
|
+
|
|
15
|
+
1. **Insertion**: New items are always added to the `current` generation.
|
|
16
|
+
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.
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
This "pseudo-LRU" approach avoids the overhead of updating timestamps or complex linked list pointers on every single access.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
(Under development. Not yet published to npm.)
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
```javascript
|
|
27
|
+
import { GenerationalCache } from '@asamuzakjp/generational-cache';
|
|
28
|
+
|
|
29
|
+
// Initialize with a max capacity of 1024 items
|
|
30
|
+
const cache = new GenerationalCache(1024);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### `new GenerationalCache(max)`
|
|
36
|
+
|
|
37
|
+
Creates a new cache instance.
|
|
38
|
+
|
|
39
|
+
* **`max`** *(number)*: The maximum number of items the cache can hold.
|
|
40
|
+
If the specified value is less than 4, or if an invalid value is specified, the default value of 4 will be used.
|
|
41
|
+
|
|
42
|
+
### Properties
|
|
43
|
+
|
|
44
|
+
* **`cache.size`** *(number, read-only)*: Returns the total number of *entries* currently in the cache.
|
|
45
|
+
**Note:** To optimize for write speed, this library allows temporary key duplication between generations.
|
|
46
|
+
Therefore, this value may not always reflect the exact count of unique *keys*.
|
|
47
|
+
* **`cache.max`** *(number)*: Gets or sets the maximum capacity.
|
|
48
|
+
**Note:** Updating this property dynamically will invoke `cache.clear()` to safely recalculate boundaries.
|
|
49
|
+
|
|
50
|
+
### Methods
|
|
51
|
+
|
|
52
|
+
* **`cache.get(key)`**
|
|
53
|
+
Retrieves an item.
|
|
54
|
+
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.
|
|
55
|
+
* **Returns:** The value associated with the key, or `undefined`.
|
|
56
|
+
* **`cache.set(key, value)`**
|
|
57
|
+
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.
|
|
58
|
+
* **Returns:** The cache instance itself (allows chaining).
|
|
59
|
+
* **`cache.has(key)`**
|
|
60
|
+
Checks if a key exists in the cache (in either generation).
|
|
61
|
+
* **Returns:** `true` if the key exists, otherwise `false`.
|
|
62
|
+
* **`cache.delete(key)`**
|
|
63
|
+
Removes an item from the cache.
|
|
64
|
+
* **Returns:** `true` if the item existed and was removed, otherwise `false`.
|
|
65
|
+
* **`cache.clear()`**
|
|
66
|
+
Empties all items from the cache by dropping references to the internal Maps.
|
|
67
|
+
|
|
68
|
+
## Performance
|
|
69
|
+
|
|
70
|
+
Benchmarks are divided into two states to simulate real-world conditions:
|
|
71
|
+
- **Cold State**: Measured with aggressive internal Garbage Collection to observe performance before full V8 TurboFan optimizations.
|
|
72
|
+
- **Warm State**: Measured after sufficient warmup, representing sustained throughput under optimal JIT compilation.
|
|
73
|
+
|
|
74
|
+
*The results below reflect the sustained operations per second (ops/sec), calculated from the average latency (`ns/iter`). Higher values indicate better performance.*
|
|
75
|
+
|
|
76
|
+
### Benchmark Environment
|
|
77
|
+
- **Engine:** Node.js v24.x (V8)
|
|
78
|
+
- **Measurement:** [mitata](https://github.com/evanwashere/mitata).
|
|
79
|
+
- **Comparison:** [LRUCache](https://www.npmjs.com/package/lru-cache) (v11.x), [QuickLRU](https://www.npmjs.com/package/quick-lru) (v7.x)
|
|
80
|
+
|
|
81
|
+
### 1. Small Cache (Max Size = 512)
|
|
82
|
+
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU |
|
|
83
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
84
|
+
| **Set** | Cold | **17,699,115 ops/sec** | 4,343,671 ops/sec | 15,057,973 ops/sec |
|
|
85
|
+
| | Warm | **21,949,078 ops/sec** | 14,664,906 ops/sec | 17,067,759 ops/sec |
|
|
86
|
+
| **Get** | Cold | **16,136,840 ops/sec** | 8,646,779 ops/sec | 13,262,599 ops/sec |
|
|
87
|
+
| | Warm | 20,462,451 ops/sec | **22,351,363 ops/sec** | 15,398,829 ops/sec |
|
|
88
|
+
| **Eviction** | Cold | **17,391,304 ops/sec** | 7,231,703 ops/sec | 14,788,524 ops/sec |
|
|
89
|
+
| | Warm | **21,640,337 ops/sec** | 7,785,130 ops/sec | 15,656,802 ops/sec |
|
|
90
|
+
|
|
91
|
+
### 2. Medium Cache (Max Size = 2,048)
|
|
92
|
+
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU |
|
|
93
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
94
|
+
| **Set** | Cold | **16,382,699 ops/sec** | 3,798,526 ops/sec | 12,558,081 ops/sec |
|
|
95
|
+
| | Warm | **19,245,573 ops/sec** | 13,097,576 ops/sec | 14,940,983 ops/sec |
|
|
96
|
+
| **Get** | Cold | **14,124,293 ops/sec** | 7,747,133 ops/sec | 11,675,423 ops/sec |
|
|
97
|
+
| | Warm | 17,870,443 ops/sec | **18,238,190 ops/sec** | 13,477,088 ops/sec |
|
|
98
|
+
| **Eviction** | Cold | **16,619,577 ops/sec** | 7,020,499 ops/sec | 11,646,866 ops/sec |
|
|
99
|
+
| | Warm | **20,635,575 ops/sec** | 7,540,909 ops/sec | 13,908,205 ops/sec |
|
|
100
|
+
|
|
101
|
+
### 3. Large Cache (Max Size = 8,192)
|
|
102
|
+
| Scenario | State | **GenerationalCache** | LRUCache | QuickLRU |
|
|
103
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
104
|
+
| **Set** | Cold | **15,216,068 ops/sec** | 3,727,587 ops/sec | 9,911,785 ops/sec |
|
|
105
|
+
| | Warm | **19,432,568 ops/sec** | 11,793,843 ops/sec | 13,817,880 ops/sec |
|
|
106
|
+
| **Get** | Cold | **11,542,012 ops/sec** | 5,955,216 ops/sec | 8,841,732 ops/sec |
|
|
107
|
+
| | Warm | 17,322,016 ops/sec | **17,818,959 ops/sec** | 13,342,228 ops/sec |
|
|
108
|
+
| **Eviction** | Cold | **13,340,448 ops/sec** | 5,236,973 ops/sec | 9,320,533 ops/sec |
|
|
109
|
+
| | Warm | **19,409,937 ops/sec** | 6,889,424 ops/sec | 12,671,059 ops/sec |
|
|
110
|
+
|
|
111
|
+
### Key Characteristics
|
|
112
|
+
|
|
113
|
+
* **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.
|
|
114
|
+
* **Predictable Scalability**: While other libraries may experience performance degradation as cache size increases, `GenerationalCache` maintains consistent throughput due to its generational swap mechanism.
|
|
115
|
+
* **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.
|
|
116
|
+
* **Conclusion**: As a developer, I'm more surprised than anyone by these benchmark results.
|
|
117
|
+
I'm confident in the quality of the library, but benchmark results should probably be taken with a grain of salt :)
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asamuzakjp/generational-cache",
|
|
3
|
+
"description": "A generational pseudo-LRU cache with strict maximum size limits.",
|
|
4
|
+
"author": "asamuzaK",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/asamuzaK/generationalCache",
|
|
7
|
+
"bugs": "https://github.com/asamuzaK/generationalCache/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/asamuzaK/generationalCache.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"types"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./types/index.d.ts",
|
|
20
|
+
"default": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.6.0",
|
|
26
|
+
"c8": "^11.0.0",
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"eslint": "^9.39.4",
|
|
29
|
+
"eslint-config-prettier": "^10.1.8",
|
|
30
|
+
"eslint-plugin-jsdoc": "^62.9.0",
|
|
31
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
32
|
+
"eslint-plugin-regexp": "^3.1.0",
|
|
33
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
34
|
+
"globals": "^17.4.0",
|
|
35
|
+
"lru-cache": "^11.3.3",
|
|
36
|
+
"mitata": "^1.0.34",
|
|
37
|
+
"mocha": "^11.7.5",
|
|
38
|
+
"neostandard": "^0.13.0",
|
|
39
|
+
"prettier": "^3.8.2",
|
|
40
|
+
"quick-lru": "^7.3.0",
|
|
41
|
+
"typescript": "^6.0.2"
|
|
42
|
+
},
|
|
43
|
+
"overrides": {
|
|
44
|
+
"c8": {
|
|
45
|
+
"yargs": "^18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"eslint": {
|
|
48
|
+
"brace-expansion": "^1.1.13"
|
|
49
|
+
},
|
|
50
|
+
"serialize-javascript": "^7.0.4"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"bench": "node --expose-gc ./benchmark/benchmark.js",
|
|
54
|
+
"build": "npm run tsc && npm run lint && npm test",
|
|
55
|
+
"lint": "eslint --fix .",
|
|
56
|
+
"test": "c8 --reporter=text mocha --exit test/*.test.js",
|
|
57
|
+
"tsc": "node scripts/index clean --dir=types -i && npx tsc"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
|
61
|
+
},
|
|
62
|
+
"version": "0.1.0"
|
|
63
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file generational-cache.js
|
|
3
|
+
* A generational pseudo-LRU cache with strict maximum size limits.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @template K, V
|
|
8
|
+
*/
|
|
9
|
+
export class GenerationalCache {
|
|
10
|
+
#max;
|
|
11
|
+
#boundary;
|
|
12
|
+
#current = new Map();
|
|
13
|
+
#old = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a new instance of the GenerationalCache class.
|
|
17
|
+
* @param {number} max - The maximum number of items the cache can hold.
|
|
18
|
+
*/
|
|
19
|
+
constructor(max) {
|
|
20
|
+
this.max = max;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the total number of `entries` currently in the cache.
|
|
25
|
+
* @note To optimize for write speed, this library allows temporary key
|
|
26
|
+
* duplication between generations. Therefore, this value may not always
|
|
27
|
+
* reflect the exact count of unique `keys`.
|
|
28
|
+
* @returns {number} The total entry count.
|
|
29
|
+
*/
|
|
30
|
+
get size() {
|
|
31
|
+
return this.#current.size + this.#old.size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the maximum capacity of the cache.
|
|
36
|
+
* @returns {number} The maximum size limit.
|
|
37
|
+
*/
|
|
38
|
+
get max() {
|
|
39
|
+
return this.#max;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sets the maximum capacity of the cache and recalculates the boundary.
|
|
44
|
+
* Clears the cache when updated.
|
|
45
|
+
* @param {number} value - The new maximum capacity to set.
|
|
46
|
+
*/
|
|
47
|
+
set max(value) {
|
|
48
|
+
if (Number.isFinite(value) && value > 4) {
|
|
49
|
+
this.#max = value;
|
|
50
|
+
this.#boundary = Math.ceil(value / 2);
|
|
51
|
+
} else {
|
|
52
|
+
this.#max = 4;
|
|
53
|
+
this.#boundary = 2;
|
|
54
|
+
}
|
|
55
|
+
this.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Retrieves an item from the cache.
|
|
60
|
+
* If the item is in the older generation, it gets promoted to the current
|
|
61
|
+
* generation.
|
|
62
|
+
* @param {K} key - The key of the element to return.
|
|
63
|
+
* @returns {V | undefined} The element associated with the specified key, or
|
|
64
|
+
* undefined if the key cannot be found.
|
|
65
|
+
*/
|
|
66
|
+
get(key) {
|
|
67
|
+
let value = this.#current.get(key);
|
|
68
|
+
if (value !== undefined) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
value = this.#old.get(key);
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
this.set(key, value);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Adds or updates an element with a specified key and a value to the cache.
|
|
81
|
+
* @param {K} key - The key of the element to add.
|
|
82
|
+
* @param {V} value - The value of the element to add.
|
|
83
|
+
* @returns {GenerationalCache} The cache object itself.
|
|
84
|
+
*/
|
|
85
|
+
set(key, value) {
|
|
86
|
+
this.#current.set(key, value);
|
|
87
|
+
// Swap generations if the current map reaches the boundary
|
|
88
|
+
if (this.#current.size >= this.#boundary) {
|
|
89
|
+
this.#old = this.#current;
|
|
90
|
+
this.#current = new Map();
|
|
91
|
+
}
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns a boolean indicating whether an element with the specified key
|
|
97
|
+
* exists or not.
|
|
98
|
+
* @param {K} key - The key of the element to test for presence.
|
|
99
|
+
* @returns {boolean} true if an element with the specified key exists in the
|
|
100
|
+
* cache; otherwise false.
|
|
101
|
+
*/
|
|
102
|
+
has(key) {
|
|
103
|
+
return this.#current.has(key) || this.#old.has(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Removes the specified element from the cache.
|
|
108
|
+
* @param {K} key - The key of the element to remove.
|
|
109
|
+
* @returns {boolean} true if an element in the cache existed and has been
|
|
110
|
+
* removed, or false if the element does not exist.
|
|
111
|
+
*/
|
|
112
|
+
delete(key) {
|
|
113
|
+
const deletedFromCurrent = this.#current.delete(key);
|
|
114
|
+
const deletedFromOld = this.#old.delete(key);
|
|
115
|
+
return deletedFromCurrent || deletedFromOld;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Removes all elements from the cache.
|
|
120
|
+
*/
|
|
121
|
+
clear() {
|
|
122
|
+
this.#current.clear();
|
|
123
|
+
this.#old.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class GenerationalCache<K, V> {
|
|
2
|
+
constructor(max: number);
|
|
3
|
+
set max(value: number);
|
|
4
|
+
get max(): number;
|
|
5
|
+
get size(): number;
|
|
6
|
+
get(key: K): V | undefined;
|
|
7
|
+
set(key: K, value: V): GenerationalCache<any, any>;
|
|
8
|
+
has(key: K): boolean;
|
|
9
|
+
delete(key: K): boolean;
|
|
10
|
+
clear(): void;
|
|
11
|
+
#private;
|
|
12
|
+
}
|