@asamuzakjp/generational-cache 2.0.2 → 2.0.4
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/README.md +90 -52
- package/package.json +11 -10
- package/src/index.js +275 -46
- package/types/index.d.ts +7 -1
package/README.md
CHANGED
|
@@ -10,23 +10,29 @@ A lightweight, **generational pseudo-LRU (Least Recently Used) cache** with stri
|
|
|
10
10
|
|
|
11
11
|
`GenerationalCache` maintains two internal `Map` objects: `current` and `old`.
|
|
12
12
|
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
13
|
+
1. **Insertion & Validation**: New items are validated against byte size limits before being added to the `current` generation.
|
|
14
|
+
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.
|
|
15
|
+
3. **Generation Swapping**: Once the `current` generation's size meets or exceeds the boundary threshold ($max / 2$), a generation swap is triggered: the existing `old` generation is discarded, the `current` generation becomes the new `old` generation, and a new empty `current` generation is created.
|
|
16
16
|
|
|
17
17
|
This "pseudo-LRU" approach avoids the overhead of updating timestamps or linked list pointers on every access. While not a drop-in replacement for standard LRU caches, it prioritizes raw throughput over strict eviction ordering.
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
```console
|
|
21
22
|
npm i @asamuzakjp/generational-cache
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
## Usage
|
|
26
|
+
|
|
25
27
|
```javascript
|
|
26
28
|
import { GenerationalCache } from '@asamuzakjp/generational-cache';
|
|
27
29
|
|
|
28
|
-
// Initialize with a max capacity of 1024 items
|
|
29
|
-
const cache = new GenerationalCache(1024
|
|
30
|
+
// Initialize with a max capacity of 1024 items and custom size limits
|
|
31
|
+
const cache = new GenerationalCache(1024, {
|
|
32
|
+
maxKeySize: 4096, // 4 KB limit for keys
|
|
33
|
+
maxValueSize: 1024 * 1024, // 1 MB limit for values
|
|
34
|
+
strictValidate: true // Enable strict deep validation for objects (default)
|
|
35
|
+
});
|
|
30
36
|
```
|
|
31
37
|
|
|
32
38
|
## API
|
|
@@ -35,51 +41,56 @@ const cache = new GenerationalCache(1024);
|
|
|
35
41
|
|
|
36
42
|
Creates a new cache instance.
|
|
37
43
|
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
**Note:**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
* **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.
|
|
45
|
+
* **opt** *(object, optional)*:
|
|
46
|
+
* **maxKeySize** *(number)*: Maximum allowed size for a `key` in bytes. Defaults to 8192 (8 KB).
|
|
47
|
+
* **maxValueSize** *(number)*: Maximum allowed size for a `value` in bytes. Defaults to 8388608 (8 MB).
|
|
48
|
+
* **strictValidate** *(boolean)*: Strictly validate object payload structures and sizes if `true`. Defaults to `true`.
|
|
49
|
+
|
|
50
|
+
### **Properties**
|
|
51
|
+
|
|
52
|
+
* **cache.size** *(number, read-only)*: Returns the total number of underlying `entries` currently stored across both generations.
|
|
53
|
+
**Note:** To maximize write throughput, this library allows temporary key duplication between the `current` and `old` generations (e.g., when an item exists in both generations simultaneously). Consequently, this value represents the combined internal map sizes and **may temporarily be higher than the actual number of unique keys**.
|
|
54
|
+
* **cache.max** *(number)*: Gets or sets the maximum item capacity.
|
|
55
|
+
**Note:** Updating this property dynamically **will clear all existing cached items** (it implicitly invokes cache.clear() to safely recalculate boundaries).
|
|
56
|
+
|
|
57
|
+
### **Methods**
|
|
58
|
+
|
|
59
|
+
* **cache.get(key)**
|
|
60
|
+
Retrieves an item. If the item is found in the `old` generation, it is automatically promoted to the `current` generation to prevent it from being evicted during the next swap.
|
|
61
|
+
* **Returns** *(any | undefined)*: The value associated with the `key`, or `undefined` if the `key` is not found.
|
|
62
|
+
* **cache.set(key, value)**
|
|
63
|
+
Adds or updates an item. It validates the byte size of both the key and value (if they are strings) before inserting. If this operation causes the `current` generation's size to meet or exceed the boundary threshold, a generation swap is triggered.
|
|
64
|
+
* **Note:** Storing `undefined` as a value **is not supported**, as cache.get(`key`) treats `undefined` as a cache miss.
|
|
65
|
+
* **Returns**: The cache instance itself (allows chaining).
|
|
66
|
+
* **cache.has(key)**
|
|
67
|
+
Checks if a `key` exists in the cache (in either generation).
|
|
68
|
+
* **Returns:** `true` if the key exists, otherwise `false`.
|
|
69
|
+
* **cache.delete(key)**
|
|
62
70
|
Removes an item from the cache.
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
Empties all items from the cache
|
|
71
|
+
* **Returns:** `true` if the item existed and was removed, otherwise `false`.
|
|
72
|
+
* **cache.clear()**
|
|
73
|
+
Empties all items from the cache.
|
|
66
74
|
|
|
67
75
|
## Performance
|
|
68
76
|
|
|
69
77
|
Benchmarks are divided into two states to simulate real-world conditions:
|
|
70
|
-
- **Cold State**: Measured with aggressive internal Garbage Collection to observe performance before full V8 TurboFan optimizations.
|
|
71
|
-
- **Warm State**: Measured after sufficient warmup, representing sustained throughput under optimal JIT compilation.
|
|
72
78
|
|
|
73
|
-
*
|
|
79
|
+
* **Cold State**: Measured with aggressive internal Garbage Collection to observe performance before full V8 TurboFan optimizations.
|
|
80
|
+
* **Warm State**: Measured after sufficient warmup, representing sustained throughput under optimal JIT compilation.
|
|
81
|
+
|
|
82
|
+
*The results below reflect the sustained operations per second (ops/sec), calculated from the average latency (ns/iter). Higher values indicate better performance.*
|
|
74
83
|
|
|
75
84
|
### Benchmark Environment
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
|
|
86
|
+
* **Engine:** Node.js v24.x (V8)
|
|
87
|
+
* **Measurement:** [mitata](https://github.com/evanwashere/mitata).
|
|
88
|
+
* **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)
|
|
79
89
|
|
|
80
90
|
### 1. Small Cache (Max Size = 512)
|
|
81
|
-
|
|
82
|
-
|
|
|
91
|
+
|
|
92
|
+
| Scenario | State | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
93
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
83
94
|
| **Set** | Cold | **17,733,640 ops/sec** | 4,933,885 ops/sec | 13,506,212 ops/sec | **17,229,496 ops/sec** |
|
|
84
95
|
| | Warm | **23,030,861 ops/sec** | 15,216,068 ops/sec | 18,175,209 ops/sec | 19,409,937 ops/sec |
|
|
85
96
|
| **Get** | Cold | 17,717,930 ops/sec | 7,633,587 ops/sec | 13,734,377 ops/sec | **30,731,407 ops/sec** |
|
|
@@ -88,8 +99,9 @@ Benchmarks are divided into two states to simulate real-world conditions:
|
|
|
88
99
|
| | Warm | **23,148,148 ops/sec** | 9,040,773 ops/sec | 16,903,313 ops/sec | 8,037,293 ops/sec |
|
|
89
100
|
|
|
90
101
|
### 2. Medium Cache (Max Size = 2,048)
|
|
91
|
-
|
|
92
|
-
|
|
|
102
|
+
|
|
103
|
+
| Scenario | State | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
104
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
93
105
|
| **Set** | Cold | **15,987,210 ops/sec** | 4,874,957 ops/sec | 11,849,745 ops/sec | **15,309,246 ops/sec** |
|
|
94
106
|
| | Warm | **19,716,088 ops/sec** | 13,345,789 ops/sec | 14,755,791 ops/sec | 17,325,017 ops/sec |
|
|
95
107
|
| **Get** | Cold | 14,994,751 ops/sec | 7,950,389 ops/sec | 11,503,508 ops/sec | **23,651,844 ops/sec** |
|
|
@@ -98,8 +110,9 @@ Benchmarks are divided into two states to simulate real-world conditions:
|
|
|
98
110
|
| | Warm | **21,982,853 ops/sec** | 8,089,305 ops/sec | 15,309,246 ops/sec | 7,132,158 ops/sec |
|
|
99
111
|
|
|
100
112
|
### 3. Large Cache (Max Size = 8,192)
|
|
101
|
-
|
|
102
|
-
|
|
|
113
|
+
|
|
114
|
+
| Scenario | State | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
115
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
103
116
|
| **Set** | Cold | **13,679,890 ops/sec** | 3,954,288 ops/sec | 8,126,777 ops/sec | 10,972,130 ops/sec |
|
|
104
117
|
| | Warm | **20,593,080 ops/sec** | 12,054,001 ops/sec | 12,995,451 ops/sec | 15,600,624 ops/sec |
|
|
105
118
|
| **Get** | Cold | 11,918,951 ops/sec | 5,785,363 ops/sec | 9,067,827 ops/sec | **16,784,155 ops/sec** |
|
|
@@ -107,15 +120,39 @@ Benchmarks are divided into two states to simulate real-world conditions:
|
|
|
107
120
|
| **Eviction** | Cold | **13,561,160 ops/sec** | 5,510,249 ops/sec | 9,642,271 ops/sec | 4,040,404 ops/sec |
|
|
108
121
|
| | Warm | **21,128,248 ops/sec** | 7,082,152 ops/sec | 13,208,294 ops/sec | 6,023,007 ops/sec |
|
|
109
122
|
|
|
110
|
-
### 4.
|
|
111
|
-
|
|
112
|
-
|
|
|
123
|
+
### 4. Non-Primitive Payload (Max Size = 8,192 / strictValidate = true)
|
|
124
|
+
|
|
125
|
+
| Scenario | State | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
126
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
127
|
+
| **Set** | Cold | 374,531 ops/sec | 1,292,758 ops/sec | 4,452,161 ops/sec | **5,354,465 ops/sec** |
|
|
128
|
+
| | Warm | 518,134 ops/sec | 7,691,124 ops/sec | 8,514,986 ops/sec | **10,217,635 ops/sec** |
|
|
129
|
+
| **Get** | Cold | **7,426,661 ops/sec** | 4,289,268 ops/sec | 4,451,368 ops/sec | 6,655,574 ops/sec |
|
|
130
|
+
| | Warm | 15,342,129 ops/sec | 13,590,649 ops/sec | 9,823,182 ops/sec | **21,240,441 ops/sec** |
|
|
131
|
+
| **Eviction** | Cold | 378,787 ops/sec | 1,824,500 ops/sec | **5,031,700 ops/sec** | 1,100,666 ops/sec |
|
|
132
|
+
| | Warm | 529,100 ops/sec | 4,524,272 ops/sec | **7,826,563 ops/sec** | 4,026,251 ops/sec |
|
|
133
|
+
|
|
134
|
+
### 5. Non-Primitive Payload (Max Size = 8,192 / strictValidate = false)
|
|
135
|
+
|
|
136
|
+
| Scenario | State | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
137
|
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
|
138
|
+
| **Set** | Cold | **6,328,312 ops/sec** | 1,317,592 ops/sec | 5,003,752 ops/sec | 5,304,476 ops/sec |
|
|
139
|
+
| | Warm | 9,648,784 ops/sec | 8,122,817 ops/sec | 8,040,524 ops/sec | **10,378,827 ops/sec** |
|
|
140
|
+
| **Get** | Cold | 6,740,361 ops/sec | 4,473,472 ops/sec | 4,405,092 ops/sec | **7,171,543 ops/sec** |
|
|
141
|
+
| | Warm | 14,898,688 ops/sec | 13,674,278 ops/sec | 9,323,140 ops/sec | **21,748,586 ops/sec** |
|
|
142
|
+
| **Eviction** | Cold | **5,552,470 ops/sec** | 1,947,571 ops/sec | 4,745,859 ops/sec | 1,126,494 ops/sec |
|
|
143
|
+
| | Warm | **10,757,314 ops/sec** | 4,898,119 ops/sec | 9,266,123 ops/sec | 4,500,450 ops/sec |
|
|
144
|
+
|
|
145
|
+
### 6. Cyclic Access (Max Size = 8,192 / Working Set = 5,000)
|
|
146
|
+
|
|
147
|
+
| Metric | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
148
|
+
| :---- | :---- | :---- | :---- | :---- |
|
|
113
149
|
| **Hit Rate** | 78.30% | **100.00%** | **100.00%** | **100.00%** |
|
|
114
150
|
| **Throughput** | 10,365,916 ops/sec | 40,832,993 ops/sec | 40,950,040 ops/sec | **48,426,150 ops/sec** |
|
|
115
151
|
|
|
116
|
-
###
|
|
117
|
-
|
|
118
|
-
|
|
|
152
|
+
### 7. Cyclic Access (Max Size = 4,096 / Working Set = 5,000)
|
|
153
|
+
|
|
154
|
+
| Metric | GenerationalCache | LRUCache | QuickLRU | Mnemonist |
|
|
155
|
+
| :---- | :---- | :---- | :---- | :---- |
|
|
119
156
|
| **Hit Rate** | 18.06% | 18.06% | **99.98%** | 18.06% |
|
|
120
157
|
| **Throughput** | **13,192,612 ops/sec** | 9,672,115 ops/sec | 10,188,487 ops/sec | 7,564,868 ops/sec |
|
|
121
158
|
|
|
@@ -124,7 +161,8 @@ Benchmarks are divided into two states to simulate real-world conditions:
|
|
|
124
161
|
* **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.
|
|
125
162
|
* **Predictable Scalability**: While other libraries may experience performance degradation as cache size increases, `GenerationalCache` maintains consistent throughput due to its generational swap mechanism.
|
|
126
163
|
* **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.
|
|
127
|
-
* **
|
|
164
|
+
* **Strict Validation Toggle**: By default, non-primitive payloads undergo deep validation to ensure memory safety (DoS protection), trading off write throughput. Disabling `strictValidate` regains write performance, assuming payload sizes are managed externally (See 4 & 5).
|
|
165
|
+
* **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 (See 6 & 7).
|
|
128
166
|
|
|
129
167
|
## License
|
|
130
168
|
|
package/package.json
CHANGED
|
@@ -23,22 +23,22 @@
|
|
|
23
23
|
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^25.
|
|
26
|
+
"@types/node": "^25.9.3",
|
|
27
27
|
"c8": "^11.0.0",
|
|
28
|
-
"commander": "^
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
29
|
"eslint": "^9.39.4",
|
|
30
30
|
"eslint-config-prettier": "^10.1.8",
|
|
31
|
-
"eslint-plugin-jsdoc": "^
|
|
32
|
-
"eslint-plugin-prettier": "^5.5.
|
|
31
|
+
"eslint-plugin-jsdoc": "^63.0.2",
|
|
32
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
33
33
|
"eslint-plugin-regexp": "^3.1.0",
|
|
34
|
-
"eslint-plugin-unicorn": "^
|
|
34
|
+
"eslint-plugin-unicorn": "^65.0.1",
|
|
35
35
|
"globals": "^17.6.0",
|
|
36
|
-
"lru-cache": "^11.
|
|
36
|
+
"lru-cache": "^11.5.1",
|
|
37
37
|
"mitata": "^1.0.34",
|
|
38
38
|
"mnemonist": "^0.40.4",
|
|
39
|
-
"mocha": "^11.7.
|
|
39
|
+
"mocha": "^11.7.6",
|
|
40
40
|
"neostandard": "^0.13.0",
|
|
41
|
-
"prettier": "^3.8.
|
|
41
|
+
"prettier": "^3.8.4",
|
|
42
42
|
"quick-lru": "^7.3.0",
|
|
43
43
|
"typescript": "^6.0.3"
|
|
44
44
|
},
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"bench": "node --expose-gc ./benchmark/benchmark.js",
|
|
59
|
-
"bench:
|
|
59
|
+
"bench:cycle": "node --expose-gc ./benchmark/benchmark-cyclic.js",
|
|
60
|
+
"bench:deep": "node --expose-gc ./benchmark/benchmark-non-primitive.js",
|
|
60
61
|
"build": "npm run tsc && npm run lint && npm test",
|
|
61
62
|
"lint": "eslint --fix .",
|
|
62
63
|
"test": "c8 --reporter=text mocha --exit test/*.test.js",
|
|
@@ -65,5 +66,5 @@
|
|
|
65
66
|
"engines": {
|
|
66
67
|
"node": "^22.13.0 || >=24.0.0"
|
|
67
68
|
},
|
|
68
|
-
"version": "2.0.
|
|
69
|
+
"version": "2.0.4"
|
|
69
70
|
}
|
package/src/index.js
CHANGED
|
@@ -3,65 +3,292 @@
|
|
|
3
3
|
* A generational pseudo-LRU cache with strict maximum size limits.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/* constants */
|
|
6
7
|
/**
|
|
8
|
+
* Maximum number of bytes per character.
|
|
9
|
+
* @type {number}
|
|
10
|
+
*/
|
|
11
|
+
const MAX_BYTES_PER_CHAR = 4;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default maximum allowed size for a key in bytes (8 KB).
|
|
15
|
+
* @type {number}
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_KEY_SIZE = 8 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default maximum allowed size for a value in bytes (8 MB).
|
|
21
|
+
* @type {number}
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_VALUE_SIZE = 8 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Flag indicating whether the current runtime environment is Node.js.
|
|
27
|
+
* @type {boolean}
|
|
28
|
+
*/
|
|
29
|
+
const IS_NODE = globalThis.process?.versions?.node !== undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A generational cache.
|
|
7
33
|
* @template K, V
|
|
8
34
|
*/
|
|
9
35
|
export class GenerationalCache {
|
|
10
|
-
#
|
|
36
|
+
#encoder;
|
|
11
37
|
#boundary;
|
|
12
38
|
#current = new Map();
|
|
13
39
|
#old = new Map();
|
|
40
|
+
#cacheFunction;
|
|
41
|
+
#cacheSymbol;
|
|
42
|
+
#maxItemsCount;
|
|
43
|
+
#maxKeySize;
|
|
44
|
+
#maxValueSize;
|
|
45
|
+
#strictValidate;
|
|
14
46
|
|
|
15
47
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {number}
|
|
48
|
+
* Creates an instance of GenerationalCache.
|
|
49
|
+
* @param {number} maxItems - The total maximum number of items allowed.
|
|
50
|
+
* @param {object} [opt] - Optional configuration parameters.
|
|
51
|
+
* @param {boolean} [opt.cacheFunction] - Caches functions if true.
|
|
52
|
+
* @param {boolean} [opt.cacheSymbol] - Caches symbols if true.
|
|
53
|
+
* @param {number} [opt.maxKeySize] - Maximum allowed size for a key in bytes.
|
|
54
|
+
* @param {number} [opt.maxValueSize] - Maximum allowed size for a value in bytes.
|
|
55
|
+
* @param {boolean} [opt.strictValidate] - Strictly validate if true.
|
|
18
56
|
*/
|
|
19
|
-
constructor(
|
|
20
|
-
|
|
57
|
+
constructor(maxItems, opt = {}) {
|
|
58
|
+
const {
|
|
59
|
+
cacheFunction,
|
|
60
|
+
cacheSymbol,
|
|
61
|
+
maxKeySize,
|
|
62
|
+
maxValueSize,
|
|
63
|
+
strictValidate
|
|
64
|
+
} = opt;
|
|
65
|
+
this.max = maxItems;
|
|
66
|
+
this.#cacheFunction = !!cacheFunction;
|
|
67
|
+
this.#cacheSymbol = !!cacheSymbol;
|
|
68
|
+
this.#maxKeySize =
|
|
69
|
+
Number.isInteger(maxKeySize) && maxKeySize
|
|
70
|
+
? maxKeySize
|
|
71
|
+
: DEFAULT_KEY_SIZE;
|
|
72
|
+
this.#maxValueSize =
|
|
73
|
+
Number.isInteger(maxValueSize) && maxValueSize
|
|
74
|
+
? maxValueSize
|
|
75
|
+
: DEFAULT_VALUE_SIZE;
|
|
76
|
+
this.#strictValidate =
|
|
77
|
+
typeof strictValidate === 'boolean' ? strictValidate : true;
|
|
21
78
|
}
|
|
22
79
|
|
|
23
80
|
/**
|
|
24
|
-
*
|
|
81
|
+
* Promotes old key/value to current generation.
|
|
82
|
+
* @param {K} key - The key to promote.
|
|
83
|
+
* @param {V} value - The value to promote.
|
|
84
|
+
*/
|
|
85
|
+
#promote(key, value) {
|
|
86
|
+
this.#current.set(key, value);
|
|
87
|
+
if (this.#current.size >= this.#boundary) {
|
|
88
|
+
this.#old = this.#current;
|
|
89
|
+
this.#current = new Map();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validates if the given string fits within the specified maximum byte size.
|
|
95
|
+
* @param {string} input - The input string to validate.
|
|
96
|
+
* @param {number} max - The maximum allowable size in bytes.
|
|
97
|
+
* @returns {boolean} True if the input is within limits, false otherwise.
|
|
98
|
+
*/
|
|
99
|
+
#validateString(input, max) {
|
|
100
|
+
if (input.length > max) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (input.length * MAX_BYTES_PER_CHAR <= max) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (IS_NODE && globalThis.Buffer) {
|
|
107
|
+
return globalThis.Buffer.byteLength(input, 'utf8') <= max;
|
|
108
|
+
}
|
|
109
|
+
if (!this.#encoder && globalThis.TextEncoder) {
|
|
110
|
+
this.#encoder = new globalThis.TextEncoder();
|
|
111
|
+
}
|
|
112
|
+
if (this.#encoder) {
|
|
113
|
+
return this.#encoder.encode(input).byteLength <= max;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#isForbidden(value, visited) {
|
|
119
|
+
const type = typeof value;
|
|
120
|
+
if (type === 'function' && !this.#cacheFunction) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (type === 'symbol' && !this.#cacheSymbol) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (value !== null && type === 'object') {
|
|
127
|
+
return this.#hasForbiddenTypes(value, visited);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recursively inspects an object to determine if it contains any forbidden
|
|
134
|
+
* types (e.g., functions or symbols).
|
|
135
|
+
* @param {object} obj - The target object or data structure to inspect.
|
|
136
|
+
* @param {WeakSet<object>} [visited] - Tracker for visited object references.
|
|
137
|
+
* @returns {boolean} True if a forbidden type is detected, false otherwise.
|
|
138
|
+
*/
|
|
139
|
+
#hasForbiddenTypes(obj, visited = new WeakSet()) {
|
|
140
|
+
if (visited.has(obj)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
visited.add(obj);
|
|
144
|
+
if (obj instanceof Map) {
|
|
145
|
+
for (const key of obj.keys()) {
|
|
146
|
+
if (this.#isForbidden(key, visited)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const value of obj.values()) {
|
|
151
|
+
if (this.#isForbidden(value, visited)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (obj instanceof Set) {
|
|
158
|
+
for (const value of obj.values()) {
|
|
159
|
+
if (this.#isForbidden(value, visited)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const symbols = Object.getOwnPropertySymbols(obj);
|
|
166
|
+
if (symbols.length) {
|
|
167
|
+
if (!this.#cacheSymbol) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
for (const sym of symbols) {
|
|
171
|
+
if (this.#isForbidden(obj[sym], visited)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const values = Object.values(obj);
|
|
177
|
+
for (const value of values) {
|
|
178
|
+
if (this.#isForbidden(value, visited)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Validates if the given input fits within the specified maximum byte size.
|
|
187
|
+
* @param {K|V} input - The input data to validate (usually a string).
|
|
188
|
+
* @param {number} max - The maximum allowable size in bytes.
|
|
189
|
+
* @returns {boolean} True if the input is within limits, false otherwise.
|
|
190
|
+
*/
|
|
191
|
+
#validate(input, max) {
|
|
192
|
+
if (input === null || input === undefined) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
const type = typeof input;
|
|
196
|
+
switch (type) {
|
|
197
|
+
case 'string': {
|
|
198
|
+
return this.#validateString(input, max);
|
|
199
|
+
}
|
|
200
|
+
case 'boolean': {
|
|
201
|
+
return max >= 5;
|
|
202
|
+
}
|
|
203
|
+
case 'number': {
|
|
204
|
+
return max >= 8;
|
|
205
|
+
}
|
|
206
|
+
case 'bigint': {
|
|
207
|
+
// Approximate.
|
|
208
|
+
return input.toString().length * MAX_BYTES_PER_CHAR <= max;
|
|
209
|
+
}
|
|
210
|
+
case 'function': {
|
|
211
|
+
return this.#cacheFunction;
|
|
212
|
+
}
|
|
213
|
+
case 'symbol': {
|
|
214
|
+
return this.#cacheSymbol;
|
|
215
|
+
}
|
|
216
|
+
default: {
|
|
217
|
+
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
|
|
218
|
+
return input.byteLength <= max;
|
|
219
|
+
}
|
|
220
|
+
if (input instanceof Date) {
|
|
221
|
+
return this.#validateString(input.toISOString(), max);
|
|
222
|
+
}
|
|
223
|
+
if (input instanceof RegExp) {
|
|
224
|
+
return input.toString().length * MAX_BYTES_PER_CHAR <= max;
|
|
225
|
+
}
|
|
226
|
+
if (!this.#strictValidate) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (this.#hasForbiddenTypes(input)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
let targetForJson = input;
|
|
233
|
+
if (input instanceof Map || input instanceof Set) {
|
|
234
|
+
if (!input.size) {
|
|
235
|
+
return max >= 2;
|
|
236
|
+
}
|
|
237
|
+
targetForJson = [...input];
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const serialized = JSON.stringify(targetForJson);
|
|
241
|
+
if (serialized === undefined) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return this.#validateString(serialized, max);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Gets the current number of cached entries.
|
|
25
254
|
* @note To optimize for write speed, this library allows temporary key
|
|
26
255
|
* duplication between generations. Therefore, this value may not always
|
|
27
256
|
* reflect the exact count of unique `keys`.
|
|
28
|
-
* @
|
|
257
|
+
* @type {number}
|
|
29
258
|
*/
|
|
30
259
|
get size() {
|
|
31
260
|
return this.#current.size + this.#old.size;
|
|
32
261
|
}
|
|
33
262
|
|
|
34
263
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
264
|
+
* Gets the maximum item capacity configured for the cache.
|
|
265
|
+
* @type {number}
|
|
37
266
|
*/
|
|
38
267
|
get max() {
|
|
39
|
-
return this.#
|
|
268
|
+
return this.#maxItemsCount;
|
|
40
269
|
}
|
|
41
270
|
|
|
42
271
|
/**
|
|
43
|
-
* Sets the maximum capacity
|
|
44
|
-
*
|
|
45
|
-
* @param {number} value - The new maximum capacity
|
|
272
|
+
* Sets the maximum item capacity and recalculates internal boundaries.
|
|
273
|
+
* Setting this will clear all currently cached items.
|
|
274
|
+
* @param {number} value - The new maximum capacity.
|
|
46
275
|
*/
|
|
47
276
|
set max(value) {
|
|
48
|
-
if (Number.
|
|
49
|
-
this.#
|
|
277
|
+
if (Number.isInteger(value) && value >= 4) {
|
|
278
|
+
this.#maxItemsCount = value;
|
|
50
279
|
this.#boundary = Math.ceil(value / 2);
|
|
51
280
|
} else {
|
|
52
|
-
this.#
|
|
281
|
+
this.#maxItemsCount = 4;
|
|
53
282
|
this.#boundary = 2;
|
|
54
283
|
}
|
|
55
284
|
this.clear();
|
|
56
285
|
}
|
|
57
286
|
|
|
58
287
|
/**
|
|
59
|
-
* Retrieves
|
|
60
|
-
* If the item
|
|
61
|
-
*
|
|
62
|
-
* @
|
|
63
|
-
* @returns {V | undefined} The element associated with the specified key, or
|
|
64
|
-
* undefined if the key cannot be found.
|
|
288
|
+
* Retrieves a value associated with the specified key.
|
|
289
|
+
* If the item exists in the old generation, it is promoted to the current.
|
|
290
|
+
* @param {K} key - The key of the item to retrieve.
|
|
291
|
+
* @returns {V|undefined} The cached value, or undefined.
|
|
65
292
|
*/
|
|
66
293
|
get(key) {
|
|
67
294
|
let value = this.#current.get(key);
|
|
@@ -69,45 +296,47 @@ export class GenerationalCache {
|
|
|
69
296
|
return value;
|
|
70
297
|
}
|
|
71
298
|
value = this.#old.get(key);
|
|
72
|
-
if (value
|
|
73
|
-
|
|
74
|
-
return value;
|
|
299
|
+
if (value === undefined) {
|
|
300
|
+
return undefined;
|
|
75
301
|
}
|
|
76
|
-
|
|
302
|
+
this.#old.delete(key);
|
|
303
|
+
this.#promote(key, value);
|
|
304
|
+
return value;
|
|
77
305
|
}
|
|
78
306
|
|
|
79
307
|
/**
|
|
80
|
-
*
|
|
81
|
-
* @
|
|
82
|
-
* @param {
|
|
83
|
-
* @
|
|
308
|
+
* Stores a key-value pair in the cache.
|
|
309
|
+
* @note `undefined` values are not cached.
|
|
310
|
+
* @param {K} key - The key to associate with the value.
|
|
311
|
+
* @param {V} value - The value to store.
|
|
312
|
+
* @returns {GenerationalCache} The GenerationalCache instance for chaining.
|
|
84
313
|
*/
|
|
85
314
|
set(key, value) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.#
|
|
315
|
+
if (value === undefined) {
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
this.#validate(key, this.#maxKeySize) &&
|
|
320
|
+
this.#validate(value, this.#maxValueSize)
|
|
321
|
+
) {
|
|
322
|
+
this.#promote(key, value);
|
|
91
323
|
}
|
|
92
324
|
return this;
|
|
93
325
|
}
|
|
94
326
|
|
|
95
327
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* @
|
|
99
|
-
* @returns {boolean} true if an element with the specified key exists in the
|
|
100
|
-
* cache; otherwise false.
|
|
328
|
+
* Checks whether an entry with the specified key exists in either generation.
|
|
329
|
+
* @param {K} key - The key to search for.
|
|
330
|
+
* @returns {boolean} True if the key exists; otherwise false.
|
|
101
331
|
*/
|
|
102
332
|
has(key) {
|
|
103
333
|
return this.#current.has(key) || this.#old.has(key);
|
|
104
334
|
}
|
|
105
335
|
|
|
106
336
|
/**
|
|
107
|
-
* Removes the specified element from the cache.
|
|
337
|
+
* Removes the specified element from the cache by its key.
|
|
108
338
|
* @param {K} key - The key of the element to remove.
|
|
109
|
-
* @returns {boolean}
|
|
110
|
-
* removed, or false if the element does not exist.
|
|
339
|
+
* @returns {boolean} True if an element in the cache existed and removed; false otherwise.
|
|
111
340
|
*/
|
|
112
341
|
delete(key) {
|
|
113
342
|
const deletedFromCurrent = this.#current.delete(key);
|
|
@@ -116,10 +345,10 @@ export class GenerationalCache {
|
|
|
116
345
|
}
|
|
117
346
|
|
|
118
347
|
/**
|
|
119
|
-
*
|
|
348
|
+
* Empties the cache completely, resetting both current and old generations.
|
|
120
349
|
*/
|
|
121
350
|
clear() {
|
|
122
|
-
this.#current
|
|
123
|
-
this.#old
|
|
351
|
+
this.#current = new Map();
|
|
352
|
+
this.#old = new Map();
|
|
124
353
|
}
|
|
125
354
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export class GenerationalCache<K, V> {
|
|
2
|
-
constructor(
|
|
2
|
+
constructor(maxItems: number, opt?: {
|
|
3
|
+
cacheFunction?: boolean | undefined;
|
|
4
|
+
cacheSymbol?: boolean | undefined;
|
|
5
|
+
maxKeySize?: number | undefined;
|
|
6
|
+
maxValueSize?: number | undefined;
|
|
7
|
+
strictValidate?: boolean | undefined;
|
|
8
|
+
});
|
|
3
9
|
set max(value: number);
|
|
4
10
|
get max(): number;
|
|
5
11
|
get size(): number;
|