@asamuzakjp/generational-cache 2.0.2 → 2.0.3

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 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. **Insertion**: New items are always 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 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.
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
- ```bash
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
- * **`max`** *(number)*: The maximum number of items the cache can hold.
39
- If the specified value is less than 4, or if an invalid value is specified, the default value of 4 will be used.
40
-
41
- ### Properties
42
-
43
- * **`cache.size`** *(number, read-only)*: Returns the total number of *entries* currently in the cache.
44
- **Note:** To optimize for write speed, this library allows temporary key duplication between generations.
45
- Therefore, this value may not always reflect the exact count of unique *keys*.
46
- * **`cache.max`** *(number)*: Gets or sets the maximum capacity.
47
- **Note:** Updating this property dynamically will invoke `cache.clear()` to safely recalculate boundaries.
48
-
49
- ### Methods
50
-
51
- * **`cache.get(key)`**
52
- Retrieves an item.
53
- 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.
54
- * **Returns:** The value associated with the key, or `undefined`.
55
- * **`cache.set(key, value)`**
56
- 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.
57
- * **Returns:** The cache instance itself (allows chaining).
58
- * **`cache.has(key)`**
59
- Checks if a key exists in the cache (in either generation).
60
- * **Returns:** `true` if the key exists, otherwise `false`.
61
- * **`cache.delete(key)`**
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
- * **Returns:** `true` if the item existed and was removed, otherwise `false`.
64
- * **`cache.clear()`**
65
- Empties all items from the cache by dropping references to the internal Maps.
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
- *The results below reflect the sustained operations per second (ops/sec), calculated from the average latency (`ns/iter`). Higher values indicate better performance.*
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
- - **Engine:** Node.js v24.x (V8)
77
- - **Measurement:** [mitata](https://github.com/evanwashere/mitata).
78
- - **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)
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
- | Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
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
- | Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
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
- | Scenario | State | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
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. Cyclic Access (Max Size = 8,192 / Working Set = 5,000)
111
- | Metric | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
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
- ### 5. Cyclic Access (Max Size = 4,096 / Working Set = 5,000)
117
- | Metric | **GenerationalCache** | LRUCache | QuickLRU | Mnemonist |
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
- * **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 4 & 5).
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.6.0",
26
+ "@types/node": "^25.9.3",
27
27
  "c8": "^11.0.0",
28
- "commander": "^14.0.3",
28
+ "commander": "^15.0.0",
29
29
  "eslint": "^9.39.4",
30
30
  "eslint-config-prettier": "^10.1.8",
31
- "eslint-plugin-jsdoc": "^62.9.0",
32
- "eslint-plugin-prettier": "^5.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": "^64.0.0",
34
+ "eslint-plugin-unicorn": "^65.0.1",
35
35
  "globals": "^17.6.0",
36
- "lru-cache": "^11.3.5",
36
+ "lru-cache": "^11.5.1",
37
37
  "mitata": "^1.0.34",
38
38
  "mnemonist": "^0.40.4",
39
- "mocha": "^11.7.5",
39
+ "mocha": "^11.7.6",
40
40
  "neostandard": "^0.13.0",
41
- "prettier": "^3.8.3",
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:worst": "node --expose-gc ./benchmark/worst-case-benchmark.js",
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.2"
69
+ "version": "2.0.3"
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
- #max;
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
- * Initializes a new instance of the GenerationalCache class.
17
- * @param {number} max - The maximum number of items the cache can hold.
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(max) {
20
- this.max = max;
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
- * Returns the total number of `entries` currently in the cache.
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
- * @returns {number} The total entry count.
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
- * Returns the maximum capacity of the cache.
36
- * @returns {number} The maximum size limit.
264
+ * Gets the maximum item capacity configured for the cache.
265
+ * @type {number}
37
266
  */
38
267
  get max() {
39
- return this.#max;
268
+ return this.#maxItemsCount;
40
269
  }
41
270
 
42
271
  /**
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.
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.isFinite(value) && value > 4) {
49
- this.#max = value;
277
+ if (Number.isInteger(value) && value >= 4) {
278
+ this.#maxItemsCount = value;
50
279
  this.#boundary = Math.ceil(value / 2);
51
280
  } else {
52
- this.#max = 4;
281
+ this.#maxItemsCount = 4;
53
282
  this.#boundary = 2;
54
283
  }
55
284
  this.clear();
56
285
  }
57
286
 
58
287
  /**
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.
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 !== undefined) {
73
- this.set(key, value);
74
- return value;
299
+ if (value === undefined) {
300
+ return undefined;
75
301
  }
76
- return undefined;
302
+ this.#promote(key, value);
303
+ this.#old.delete(key);
304
+ return value;
77
305
  }
78
306
 
79
307
  /**
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.
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
- 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();
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
- * 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.
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} true if an element in the cache existed and has been
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
- * Removes all elements from the cache.
348
+ * Empties the cache completely, resetting both current and old generations.
120
349
  */
121
350
  clear() {
122
- this.#current.clear();
123
- this.#old.clear();
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(max: number);
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;