diff --git a/BENCHMARK.md b/BENCHMARK.md
index 17bd996..9c93132 100644
--- a/BENCHMARK.md
+++ b/BENCHMARK.md
@@ -12,217 +12,38 @@ npm run benchmark
The benchmarks use [Testcontainers](https://www.testcontainers.org/) to automatically spin up a Redis instance, so you don't need to have Redis running locally.
-## Benchmark Suite
-
-The benchmark suite includes four comprehensive tests that measure different aspects of cache performance:
-
-### Benchmark 1: Cache Hit Rate Analysis
-
-**Purpose:** Measures how effectively the in-memory cache prevents Redis calls in a realistic scenario.
-
-**Methodology:**
-- Executes 10,000 cache read operations
-- Uses an 80/20 access pattern (80% of requests go to 20% of keys) which simulates real-world usage
-- Tracks hits at the memory level vs. Redis level
-- Measures cache misses
-
-**Key Metrics:**
-- Memory cache hit rate
-- Redis cache hit rate
-- Cache miss rate
-- Number of Redis calls prevented
-- Performance metrics (latency, throughput)
-
-**Expected Results:**
-- Memory cache should handle nearly 100% of requests for hot keys
-- Demonstrates massive reduction in Redis network calls
-- Shows sub-millisecond average latency
-
-**Example Output:**
-```
-Results:
- Total Calls: 10000
- Memory Cache Hits: 10000 (100.00%)
- Redis Cache Hits: 0 (0.00%)
- Cache Misses: 0 (0.00%)
-
- Performance Metrics:
- Total Duration: 62ms
- Average Latency: 0.01ms
- P50 Latency: 0ms
- P95 Latency: 0ms
- P99 Latency: 0ms
- Throughput: 161290.32 ops/sec
-
- Key Insights:
- - Memory cache prevented 10000 Redis calls
- - That's 100.00% reduction in Redis load
- - Redis was hit 0 times when memory cache missed
-```
-
-### Benchmark 2: Speed Comparison - Multi-Level vs Redis-Only
-
-**Purpose:** Compares the overall performance of a multi-level cache (Memory + Redis) against a Redis-only cache.
-
-**Methodology:**
-- Tests both cache configurations with 10,000 read operations
-- Uses random access pattern across 100 unique keys
-- Measures latency distribution (P50, P95, P99)
-- Calculates throughput in operations per second
-
-**Key Metrics:**
-- Total duration for all operations
-- Average latency
-- Latency percentiles (P50, P95, P99)
-- Throughput (ops/sec)
-- Performance improvement percentage
-
-**Expected Results:**
-- Multi-level cache should be 90%+ faster than Redis-only
-- Demonstrates the value of in-memory caching for frequently accessed data
-- Shows orders of magnitude improvement in throughput
-
-**Example Output:**
-```
-Multi-Level Cache Results:
- Total Duration: 16ms
- Avg Latency: 0.00ms
- P50 Latency: 0ms
- P95 Latency: 0ms
- P99 Latency: 0ms
- Throughput: 625000.00 ops/sec
-
-Redis-Only Cache Results:
- Total Duration: 6965ms
- Avg Latency: 0.70ms
- P50 Latency: 1ms
- P95 Latency: 1ms
- P99 Latency: 2ms
- Throughput: 1435.75 ops/sec
-
-Performance Comparison:
- Multi-Level Cache is 99.77% FASTER overall
- Multi-Level Cache has 99.80% LOWER average latency
- Multi-Level Cache has 43431.25% HIGHER throughput
-```
-
-### Benchmark 3: Write Performance and Consistency
-
-**Purpose:** Measures write performance and documents the trade-off between read and write performance.
-
-**Methodology:**
-- Executes 1,000 write operations for both cache types
-- Measures write latency distribution
-- Calculates write throughput
-
-**Key Metrics:**
-- Write latency (average, P50, P95)
-- Write throughput
-- Performance comparison between multi-level and Redis-only
-
-**Expected Results:**
-- Multi-level writes are slower (20-80%) than Redis-only
-- This is expected as data must be written to both memory and Redis
-- Documents the trade-off: slower writes for much faster reads
-
-**Example Output:**
-```
-Multi-Level Cache Write Performance:
- Total Duration: 9338ms
- Avg Latency: 0.93ms
- P50 Latency: 1ms
- P95 Latency: 2ms
- P99 Latency: 2ms
- Throughput: 1070.89 ops/sec
-
-Redis-Only Cache Write Performance:
- Total Duration: 5450ms
- Avg Latency: 0.54ms
- P50 Latency: 1ms
- P95 Latency: 1ms
- P99 Latency: 2ms
- Throughput: 1834.86 ops/sec
-
-Write Performance Comparison:
- Multi-Level writes are 71.34% SLOWER than Redis-only
- This is expected as writes must update both memory and Redis layers
-```
-
-### Benchmark 4: Memory Efficiency Analysis
-
-**Purpose:** Analyzes memory usage and efficiency of the in-memory cache layer.
-
-**Methodology:**
-- Populates cache with 100000 entries (~1KB each)
-- Measures actual memory usage
-- Calculates retention efficiency
-
-**Key Metrics:**
-- Number of items in memory cache
-- Estimated memory usage
-- Average memory per item
-- Memory efficiency (retention rate)
-
-**Expected Results:**
-- Shows predictable memory usage based on cache size
-- Demonstrates automatic eviction policy behavior
-- Documents memory footprint for capacity planning
-
-**Example Output:**
-```
-Memory Usage Statistics:
- Items in Memory Cache: 10000
- Estimated Memory Usage: ~9.54 MB
- Average Memory per Item: ~0.98 KB
- Memory Efficiency: 100.00% of written items retained
-
-Memory Cache Benefits:
- - Fast in-memory access for frequently accessed items
- - Automatic eviction based on configured strategies
- - Reduces network latency for cache hits
- - Offloads Redis for better resource utilization
-```
-
-## Interpreting Results
-
-### When Multi-Level Caching Excels
-
-Multi-level caching provides the most benefit when:
-
-1. **High Read-to-Write Ratio:** Applications with more reads than writes benefit from fast in-memory access
-2. **Hot Data Sets:** When a small subset of data is accessed frequently (e.g., 80/20 pattern)
-3. **Latency-Sensitive Operations:** When sub-millisecond response times are critical
-4. **High Concurrency:** When many operations need to be handled simultaneously
-
-### Trade-offs to Consider
-
-1. **Write Performance:** Writes are slower as data must be written to both cache levels
-2. **Memory Usage:** In-memory cache consumes application memory
-3. **Consistency:** Requires careful management in distributed systems
-4. **Complexity:** Additional layer adds operational complexity
-
-## Performance Tips
-
-1. **Tune Memory Strategies:** Adjust `RamPercentageLimitStrategy` threshold based on your application's memory profile
-2. **Choose Appropriate TTL:** Set cache TTL values that balance freshness and hit rate
-3. **Monitor Cache Metrics:** Track hit rates to optimize cache configuration
-4. **Size Your Cache:** Use Benchmark 4 to estimate memory requirements
-
-## System Requirements
-
-- Node.js 16+
-- Docker (for Testcontainers)
-- At least 2GB RAM for running benchmarks
-
-## Contributing
-
-If you'd like to add additional benchmarks or improve existing ones, please:
-
-1. Follow the existing benchmark structure
-2. Use Testcontainers for infrastructure dependencies
-3. Document expected results and methodology
-4. Ensure benchmarks are reproducible
-
-## License
-
-MIT License - See LICENSE file for details
+## Benchmarks
+
+### Read Performance
+
+As expected, read performance for multi level cache is (99x) faster. This is due to the memory layer contributing to additional speed and being ready for each request.
+
+ {
+ "name": "Multi-Level Cache",
+ "ops": 173913,
+ "margin": 6.93,
+ "percentSlower": 0
+ },
+ {
+ "name": "Redis-Only Cache",
+ "ops": 1621,
+ "margin": 7.28,
+ "percentSlower": 99.07
+ }
+
+### Write Performance
+
+Multi level cache is 33% slower at the moment at writing as it is writing to memory.
+
+ {
+ "name": "Multi-Level Cache",
+ "ops": 1002,
+ "margin": 6.6,
+ "percentSlower": 33.11
+ },
+ {
+ "name": "Redis-Only Cache",
+ "ops": 1498,
+ "margin": 9.15,
+ "percentSlower": 0
+ }
\ No newline at end of file
diff --git a/benchmark/results/cache-read-performance.json b/benchmark/results/cache-read-performance.json
new file mode 100644
index 0000000..d2b568e
--- /dev/null
+++ b/benchmark/results/cache-read-performance.json
@@ -0,0 +1,27 @@
+{
+ "name": "Cache Read Performance",
+ "date": "2025-11-11T01:31:28.741Z",
+ "version": null,
+ "results": [
+ {
+ "name": "Multi-Level Cache",
+ "ops": 221896,
+ "margin": 6.48,
+ "percentSlower": 0
+ },
+ {
+ "name": "Redis-Only Cache",
+ "ops": 3676,
+ "margin": 6.48,
+ "percentSlower": 98.34
+ }
+ ],
+ "fastest": {
+ "name": "Multi-Level Cache",
+ "index": 0
+ },
+ "slowest": {
+ "name": "Redis-Only Cache",
+ "index": 1
+ }
+}
\ No newline at end of file
diff --git a/benchmark/results/cache-read-performance.table.html b/benchmark/results/cache-read-performance.table.html
new file mode 100644
index 0000000..1dde643
--- /dev/null
+++ b/benchmark/results/cache-read-performance.table.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Cache Read Performance
+
+
+
+
+
+ | name | ops | margin | percentSlower |
+
+
+
+
+ | Multi-Level Cache | 175930 | 4.71 | 0 |
+
+ | Redis-Only Cache | 182 | 58.87 | 99.9 |
+
+
+
+
+
\ No newline at end of file
diff --git a/benchmark/results/cache-write-performance.json b/benchmark/results/cache-write-performance.json
new file mode 100644
index 0000000..fb493d8
--- /dev/null
+++ b/benchmark/results/cache-write-performance.json
@@ -0,0 +1,27 @@
+{
+ "name": "Cache Write Performance",
+ "date": "2025-11-11T03:40:53.610Z",
+ "version": null,
+ "results": [
+ {
+ "name": "Multi-Level Cache",
+ "ops": 1002,
+ "margin": 6.6,
+ "percentSlower": 33.11
+ },
+ {
+ "name": "Redis-Only Cache",
+ "ops": 1498,
+ "margin": 9.15,
+ "percentSlower": 0
+ }
+ ],
+ "fastest": {
+ "name": "Redis-Only Cache",
+ "index": 1
+ },
+ "slowest": {
+ "name": "Multi-Level Cache",
+ "index": 0
+ }
+}
\ No newline at end of file
diff --git a/benchmark/results/cache-write-performance.table.html b/benchmark/results/cache-write-performance.table.html
new file mode 100644
index 0000000..3e2da53
--- /dev/null
+++ b/benchmark/results/cache-write-performance.table.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Cache Write Performance
+
+
+
+
+
+ | name | ops | margin | percentSlower |
+
+
+
+
+ | Multi-Level Cache | 2301 | 6.32 | 0 |
+
+ | Redis-Only Cache | 1066 | 41.85 | 53.67 |
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 565c7b6..bf7e257 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
{
"name": "cacheforge",
- "version": "1.2.0",
+ "version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cacheforge",
- "version": "1.2.0",
+ "version": "1.3.0",
"dependencies": {
"@datastructures-js/heap": "^4.3.7",
"@sesamecare-oss/redlock": "^1.4.0",
+ "benny": "^3.7.1",
"json-with-bigint": "^3.4.4"
},
"devDependencies": {
@@ -18,6 +19,7 @@
"@testcontainers/redis": "^11.7.1",
"@vitest/coverage-v8": "^3.2.4",
"testcontainers": "^11.7.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
@@ -39,6 +41,48 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@arrows/array": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
+ "integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
+ "license": "ISC",
+ "dependencies": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "node_modules/@arrows/composition": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
+ "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ==",
+ "license": "ISC"
+ },
+ "node_modules/@arrows/dispatch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
+ "integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
+ "license": "ISC",
+ "dependencies": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "node_modules/@arrows/error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
+ "integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA==",
+ "license": "ISC"
+ },
+ "node_modules/@arrows/multimethod": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.4.1.tgz",
+ "integrity": "sha512-AZnAay0dgPnCJxn3We5uKiB88VL+1ZIF2SjZohLj6vqY2UyvB/sKdDnFP+LZNVsTC5lcnGPmLlRRkAh4sXkXsQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@arrows/array": "^1.4.1",
+ "@arrows/composition": "^1.2.2",
+ "@arrows/error": "^1.0.2",
+ "fast-deep-equal": "^3.1.3"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -269,6 +313,30 @@
"node": ">=14.21.3"
}
},
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
"node_modules/@datastructures-js/heap": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/@datastructures-js/heap/-/heap-4.3.7.tgz",
@@ -1286,6 +1354,34 @@
"testcontainers": "^11.7.1"
}
},
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/chai": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
@@ -1542,6 +1638,47 @@
"node": ">=6.5"
}
},
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1606,6 +1743,13 @@
"node": ">= 14"
}
},
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@@ -1638,6 +1782,15 @@
"js-tokens": "^9.0.1"
}
},
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -1802,6 +1955,36 @@
"tweetnacl": "^0.14.3"
}
},
+ "node_modules/benchmark": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
+ "integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.4",
+ "platform": "^1.3.3"
+ }
+ },
+ "node_modules/benny": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/benny/-/benny-3.7.1.tgz",
+ "integrity": "sha512-USzYxODdVfOS7JuQq/L0naxB788dWCiUgUTxvN+WLPt/JfcDURNNj8kN/N+uK6PDvuR67/9/55cVKGPleFQINA==",
+ "license": "ISC",
+ "dependencies": {
+ "@arrows/composition": "^1.0.0",
+ "@arrows/dispatch": "^1.0.2",
+ "@arrows/multimethod": "^1.1.6",
+ "benchmark": "^2.1.4",
+ "common-tags": "^1.8.0",
+ "fs-extra": "^10.0.0",
+ "json2csv": "^5.0.6",
+ "kleur": "^4.1.4",
+ "log-update": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -1963,6 +2146,18 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -2071,7 +2266,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -2084,9 +2278,26 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/compress-commons": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
@@ -2153,6 +2364,13 @@
"node": ">= 14"
}
},
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2205,6 +2423,16 @@
"node": ">=0.10"
}
},
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/docker-compose": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz",
@@ -2446,6 +2674,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -2495,6 +2729,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2558,7 +2806,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/has-flag": {
@@ -2635,7 +2882,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2751,6 +2997,55 @@
"integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==",
"license": "MIT"
},
+ "node_modules/json2csv": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz",
+ "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^6.1.0",
+ "jsonparse": "^1.3.1",
+ "lodash.get": "^4.4.2"
+ },
+ "bin": {
+ "json2csv": "bin/json2csv.js"
+ },
+ "engines": {
+ "node": ">= 10",
+ "npm": ">= 6.13.0"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
@@ -2801,7 +3096,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -2818,6 +3112,13 @@
"license": "MIT",
"peer": true
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
+ "license": "MIT"
+ },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -2825,6 +3126,94 @@
"license": "MIT",
"peer": true
},
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -2884,6 +3273,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -2983,6 +3388,21 @@
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -3054,6 +3474,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+ "license": "MIT"
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3245,6 +3671,25 @@
"node": ">=0.10.0"
}
},
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -3381,6 +3826,38 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3758,6 +4235,50 @@
"node": ">=14.14"
}
},
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -3765,6 +4286,18 @@
"dev": true,
"license": "Unlicense"
},
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3796,6 +4329,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3817,6 +4359,13 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz",
@@ -4223,6 +4772,16 @@
"node": ">=8"
}
},
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/zip-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
diff --git a/package.json b/package.json
index 5178076..1773039 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cacheforge",
- "version": "1.2.0",
+ "version": "1.3.0",
"description": "A multi-level caching library for Node.js applications, supporting in-memory and Redis, and custom cache levels.",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -15,6 +15,7 @@
"dependencies": {
"@datastructures-js/heap": "^4.3.7",
"@sesamecare-oss/redlock": "^1.4.0",
+ "benny": "^3.7.1",
"json-with-bigint": "^3.4.4"
},
"devDependencies": {
@@ -23,6 +24,7 @@
"@testcontainers/redis": "^11.7.1",
"@vitest/coverage-v8": "^3.2.4",
"testcontainers": "^11.7.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
@@ -32,7 +34,7 @@
"scripts": {
"test": "vitest --coverage",
"test:debug": "vitest --inspect-brk --no-file-parallelism",
- "benchmark": "vitest run tests/cache.benchmark.spec.ts",
+ "benchmark": "ts-node tests/benchmarks/cache-performance.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"lint": "biome check ./src",
diff --git a/src/cache.service.ts b/src/cache.service.ts
index ffc5ff0..945ada7 100644
--- a/src/cache.service.ts
+++ b/src/cache.service.ts
@@ -205,6 +205,10 @@ export class CacheService {
return newValue as T;
}
+ /**
+ * @description Loop through cache levels to delete the values for the given keys
+ * @param keys - cache keys
+ */
async mdel(keys: string[]): Promise {
await Promise.allSettled(
this.levels.map((level) =>
@@ -255,7 +259,7 @@ export class CacheService {
await Promise.allSettled(
this.levels.map((level) => {
return handleGracefully(
- () => level.set(key, value, ttl),
+ async () => level.set(key, value, ttl),
"Failed to set key in cache level",
);
}),
diff --git a/src/levels/interfaces/cache-level.ts b/src/levels/interfaces/cache-level.ts
index 6352c04..83d5be8 100644
--- a/src/levels/interfaces/cache-level.ts
+++ b/src/levels/interfaces/cache-level.ts
@@ -1,4 +1,11 @@
+export enum CacheType {
+ MEMORY = "memory",
+ DISK = "disk",
+ DISTRIBUTED = "distributed",
+}
+
export interface CacheLevel {
+ cacheType: CacheType;
/**
* Store multiple values in the cache.
* @param keys The cache keys.
diff --git a/src/levels/interfaces/in-memory.ts b/src/levels/interfaces/in-memory.ts
index 0ddf300..4008f84 100644
--- a/src/levels/interfaces/in-memory.ts
+++ b/src/levels/interfaces/in-memory.ts
@@ -1,6 +1,8 @@
import type { MemoryHeap } from "../../utils/heap.utils";
+import type { CacheType } from "./cache-level";
export interface InMemory {
+ cacheType: CacheType.MEMORY;
/**
* Get current memory usage as a percentage (entire system memory).
* @return Percentage of memory used.
diff --git a/src/levels/memory/memory.level.ts b/src/levels/memory/memory.level.ts
index eb2af15..e13a818 100644
--- a/src/levels/memory/memory.level.ts
+++ b/src/levels/memory/memory.level.ts
@@ -4,7 +4,7 @@ import type { AbstractMemoryEvictionPolicy } from "../../policies/abstract/abstr
import type { MemoryManagementStrategy } from "../../strategies/interfaces/memory-management-strategy";
import { createCacheHeap } from "../../utils/heap.utils";
import { serialize } from "../../utils/parsing.utils";
-import type { CacheLevel } from "../interfaces/cache-level";
+import { type CacheLevel, CacheType } from "../interfaces/cache-level";
import type { InMemory } from "../interfaces/in-memory";
import type { Purgable } from "../interfaces/purgable";
import { EvictionManager } from "./eviction-manager";
@@ -26,6 +26,8 @@ export interface MemoryLevelOptions {
export class MemoryCacheLevel
implements CacheLevel, Purgable, InMemory
{
+ public cacheType: CacheType.MEMORY = CacheType.MEMORY;
+
protected store = new Map();
protected size = 0;
protected heap = createCacheHeap((item) => item.expiry);
@@ -43,7 +45,7 @@ export class MemoryCacheLevel
await Promise.all(deletePromises);
}
- private updateStore(key: string, item: StoredItem) {
+ private async updateStore(key: string, item: StoredItem) {
this.store.set(key, item);
this.heap.insert({ ...item, key });
this.size += serialize(item).length;
@@ -99,12 +101,12 @@ export class MemoryCacheLevel
return cachedValue?.value as T;
}
- set(key: string, value: T, ttl: number = DEFAULT_TTL): Promise {
+ async set(key: string, value: T, ttl: number = DEFAULT_TTL): Promise {
const expiryDate = Date.now() + ttl * 1000;
const storedItem = { value, expiry: expiryDate };
- this.updateStore(key, storedItem);
+ await this.updateStore(key, storedItem);
- return Promise.resolve(value as T);
+ return value as T;
}
async del(key: string): Promise {
this.store.delete(key);
diff --git a/src/levels/redis/redis.level.ts b/src/levels/redis/redis.level.ts
index a73585c..55ecf1e 100644
--- a/src/levels/redis/redis.level.ts
+++ b/src/levels/redis/redis.level.ts
@@ -5,12 +5,14 @@ import { DEFAULT_TTL } from "../../constants";
import { parseIfJSON } from "../../utils/cache.utils";
import { deserialize, serialize } from "../../utils/parsing.utils";
import { generateVersionLookupKey } from "../../utils/version.utils";
-import type { CacheLevel } from "../interfaces/cache-level";
+import { type CacheLevel, CacheType } from "../interfaces/cache-level";
import type { Lockable } from "../interfaces/lockable";
export class RedisCacheLevel implements CacheLevel, Lockable {
private client: IoRedis | Cluster;
+ public cacheType: CacheType = CacheType.DISTRIBUTED;
+
constructor(client: IoRedis | Cluster) {
this.client = client;
}
diff --git a/tests/benchmarks/benchmark.ts b/tests/benchmarks/benchmark.ts
new file mode 100644
index 0000000..afc77f2
--- /dev/null
+++ b/tests/benchmarks/benchmark.ts
@@ -0,0 +1,2 @@
+import './read';
+import './write';
diff --git a/tests/benchmarks/cache.benchmark.spec.ts b/tests/benchmarks/cache.benchmark.spec.ts
deleted file mode 100644
index 2e37480..0000000
--- a/tests/benchmarks/cache.benchmark.spec.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-import {
- RedisContainer,
- type StartedRedisContainer,
-} from "@testcontainers/redis";
-import { Redis } from "ioredis";
-import { afterAll, beforeAll, describe, it } from "vitest";
-import { CacheService } from "../../src/cache.service";
-import {
- MemoryCacheLevel,
- RedisCacheLevel,
-} from "../../src/levels";
-import { FirstExpiringMemoryPolicy } from "../../src/policies/first-expiring-memory.policy";
-import { RamPercentageLimitStrategy } from "../../src/strategies/memory-percentage-limit.strategy";
-import type { StoredHeapItem } from "../../src/levels/memory/memory.level";
-import {
- type BenchmarkResult,
- calculateLatencyStats,
- calculateThroughput,
- get8020KeyIndex,
- getUniformKeyIndex,
- instrumentMemoryCache,
- instrumentRedisCache,
- populateCache,
- runBenchmark,
-} from "../utilities/benchmark.utilities";
-import {
- printBenchmarkHeader,
- printBenchmarkResults,
- printCacheHitRateResults,
- printMemoryEfficiency,
- printPerformanceComparison,
- printWritePerformanceComparison,
-} from "../utilities/benchmark-output.utilities";
-
-const TOTAL_CALLS = 10000;
-
-describe("Cache Performance Benchmarks", () => {
- let redisContainer: StartedRedisContainer;
-
- beforeAll(async () => {
- // Start Redis container
- redisContainer = await new RedisContainer("redis:7.2").start();
- }, 60000);
-
- afterAll(async () => {
- await redisContainer?.stop();
- });
-
- it("Benchmark 1: Cache Hit Rate Analysis - 10,000 calls with 80/20 access pattern", async () => {
- printBenchmarkHeader("BENCHMARK 1: CACHE HIT RATE ANALYSIS");
-
- // Create fresh cache instances for this test
- const redisClient = new Redis(redisContainer.getConnectionUrl());
- const memoryLevel = new MemoryCacheLevel({
- memoryStrategies: [new RamPercentageLimitStrategy(80)],
- evictionPolicy: new FirstExpiringMemoryPolicy(),
- });
- const redisLevel = new RedisCacheLevel(redisClient);
- const multiLevelCache = new CacheService({
- levels: [memoryLevel, redisLevel],
- defaultTTL: 3600,
- });
-
- // Pre-populate cache with data
- await populateCache(multiLevelCache, "benchmark_key", TOTAL_CALLS);
-
- // Wait a bit to ensure data is properly set
- await new Promise((resolve) => setTimeout(resolve, 100));
-
- // Set up hit/miss counters
- const memoryCacheHits = { count: 0 };
- const redisCacheHits = { count: 0 };
- const cacheMisses = { count: 0 };
-
- // Instrument cache levels to track hits
- instrumentMemoryCache(memoryLevel, memoryCacheHits);
- instrumentRedisCache(redisLevel, redisCacheHits, cacheMisses);
-
- // Run benchmark with 80/20 access pattern
- const { latencies, totalDuration } = await runBenchmark(async () => {
- const keyIndex = get8020KeyIndex(TOTAL_CALLS);
- const key = `benchmark_key_${keyIndex}`;
- await multiLevelCache.get(key, null);
- }, TOTAL_CALLS);
-
- // Calculate statistics
- const stats = calculateLatencyStats(latencies);
- const throughput = calculateThroughput(TOTAL_CALLS, totalDuration);
-
- const result: BenchmarkResult = {
- description: "Multi-Level Cache with 80/20 Access Pattern",
- totalCalls: TOTAL_CALLS,
- memoryCacheHits: memoryCacheHits.count,
- redisCacheHits: redisCacheHits.count,
- cacheMisses: cacheMisses.count,
- totalDuration,
- ...stats,
- throughputOps: throughput,
- };
-
- // Print results
- printCacheHitRateResults(result);
-
- // Cleanup
- await redisClient.quit();
- }, 120000);
-
- it("Benchmark 2: Speed Comparison - Multi-Level vs Redis-Only", async () => {
- printBenchmarkHeader("BENCHMARK 2: SPEED COMPARISON");
-
- // Create fresh cache instances for this test
- const redisClient = new Redis(redisContainer.getConnectionUrl());
- const redisOnlyClient = new Redis(redisContainer.getConnectionUrl());
-
- const memoryLevel = new MemoryCacheLevel({
- memoryStrategies: [new RamPercentageLimitStrategy(80)],
- evictionPolicy: new FirstExpiringMemoryPolicy(),
- });
- const redisLevel = new RedisCacheLevel(redisClient);
- const multiLevelCache = new CacheService({
- levels: [memoryLevel, redisLevel],
- defaultTTL: 3600,
- });
-
- const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient);
- const redisOnlyCache = new CacheService({
- levels: [redisOnlyLevel],
- defaultTTL: 3600,
- });
-
- const uniqueKeys = 100;
-
- // Pre-populate both caches
- await populateCache(multiLevelCache, "speed_test_key", uniqueKeys);
- await populateCache(redisOnlyCache, "speed_test_key", uniqueKeys);
-
- await new Promise((resolve) => setTimeout(resolve, 100));
-
- // Benchmark Multi-Level Cache
- console.log("Testing Multi-Level Cache (Memory + Redis)...");
- const multiLevelBenchmark = await runBenchmark(async () => {
- const keyIndex = getUniformKeyIndex(uniqueKeys);
- const key = `speed_test_key_${keyIndex}`;
- await multiLevelCache.get(key, null);
- }, TOTAL_CALLS);
-
- // Benchmark Redis-Only Cache
- console.log("Testing Redis-Only Cache...");
- const redisOnlyBenchmark = await runBenchmark(async () => {
- const keyIndex = getUniformKeyIndex(uniqueKeys);
- const key = `speed_test_key_${keyIndex}`;
- await redisOnlyCache.get(key, null);
- }, TOTAL_CALLS);
-
- // Calculate statistics
- const multiLevelStats = calculateLatencyStats(multiLevelBenchmark.latencies);
- const redisOnlyStats = calculateLatencyStats(redisOnlyBenchmark.latencies);
-
- const multiLevelResult: BenchmarkResult = {
- description: "Multi-Level Cache",
- totalCalls: TOTAL_CALLS,
- totalDuration: multiLevelBenchmark.totalDuration,
- ...multiLevelStats,
- throughputOps: calculateThroughput(TOTAL_CALLS, multiLevelBenchmark.totalDuration),
- };
-
- const redisOnlyResult: BenchmarkResult = {
- description: "Redis-Only Cache",
- totalCalls: TOTAL_CALLS,
- totalDuration: redisOnlyBenchmark.totalDuration,
- ...redisOnlyStats,
- throughputOps: calculateThroughput(TOTAL_CALLS, redisOnlyBenchmark.totalDuration),
- };
-
- // Print comparison
- printBenchmarkResults("Multi-Level Cache Results", multiLevelResult);
- printBenchmarkResults("Redis-Only Cache Results", redisOnlyResult);
- printPerformanceComparison(redisOnlyResult, multiLevelResult);
-
- // Cleanup
- await redisClient.quit();
- await redisOnlyClient.quit();
- }, 120000);
-
- it("Benchmark 3: Write Performance and Consistency", async () => {
- printBenchmarkHeader("BENCHMARK 3: WRITE PERFORMANCE");
-
- // Create fresh cache instances for this test
- const redisClient = new Redis(redisContainer.getConnectionUrl());
- const redisOnlyClient = new Redis(redisContainer.getConnectionUrl());
-
- const memoryLevel = new MemoryCacheLevel({
- memoryStrategies: [new RamPercentageLimitStrategy(80)],
- evictionPolicy: new FirstExpiringMemoryPolicy(),
- });
- const redisLevel = new RedisCacheLevel(redisClient);
- const multiLevelCache = new CacheService({
- levels: [memoryLevel, redisLevel],
- defaultTTL: 3600,
- });
-
- const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient);
- const redisOnlyCache = new CacheService({
- levels: [redisOnlyLevel],
- defaultTTL: 3600,
- });
-
- // Benchmark Multi-Level Cache Writes
- console.log("Testing Multi-Level Cache writes...");
- const multiLevelBenchmark = await runBenchmark(async () => {
- const i = Math.floor(Math.random() * TOTAL_CALLS);
- const key = `write_test_key_${i}`;
- const value = { id: i, data: `write_data_${i}_${"x".repeat(100)}` };
- await multiLevelCache.set(key, value);
- }, TOTAL_CALLS);
-
- // Benchmark Redis-Only Cache Writes
- console.log("Testing Redis-Only Cache writes...");
- const redisOnlyBenchmark = await runBenchmark(async () => {
- const i = Math.floor(Math.random() * TOTAL_CALLS);
- const key = `write_test_key_redis_${i}`;
- const value = { id: i, data: `write_data_${i}_${"x".repeat(100)}` };
- await redisOnlyCache.set(key, value);
- }, TOTAL_CALLS);
-
- // Calculate statistics
- const multiLevelStats = calculateLatencyStats(multiLevelBenchmark.latencies);
- const redisOnlyStats = calculateLatencyStats(redisOnlyBenchmark.latencies);
-
- const multiLevelResult: BenchmarkResult = {
- description: "Multi-Level Cache",
- totalCalls: TOTAL_CALLS,
- totalDuration: multiLevelBenchmark.totalDuration,
- ...multiLevelStats,
- throughputOps: calculateThroughput(TOTAL_CALLS, multiLevelBenchmark.totalDuration),
- };
-
- const redisOnlyResult: BenchmarkResult = {
- description: "Redis-Only Cache",
- totalCalls: TOTAL_CALLS,
- totalDuration: redisOnlyBenchmark.totalDuration,
- ...redisOnlyStats,
- throughputOps: calculateThroughput(TOTAL_CALLS, redisOnlyBenchmark.totalDuration),
- };
-
- // Print results
- printBenchmarkResults("Multi-Level Cache Write Performance", multiLevelResult);
- printBenchmarkResults("Redis-Only Cache Write Performance", redisOnlyResult);
- printWritePerformanceComparison(multiLevelResult, redisOnlyResult);
-
- // Cleanup
- await redisClient.quit();
- await redisOnlyClient.quit();
- }, 120000);
-
- it("Benchmark 4: Memory Efficiency Analysis", async () => {
- printBenchmarkHeader("BENCHMARK 4: MEMORY EFFICIENCY");
-
- // Create fresh cache instance for this test
- const redisClient = new Redis(redisContainer.getConnectionUrl());
- const memoryLevel = new MemoryCacheLevel({
- memoryStrategies: [new RamPercentageLimitStrategy(80)],
- evictionPolicy: new FirstExpiringMemoryPolicy(),
- });
- const redisLevel = new RedisCacheLevel(redisClient);
- const multiLevelCache = new CacheService({
- levels: [memoryLevel, redisLevel],
- defaultTTL: 3600,
- });
- const totalKeys = TOTAL_CALLS;
- const valueSizeBytes = 1000; // ~1KB per value
-
- // Clear memory cache first
- memoryLevel.purge();
-
- console.log("Populating cache with test data...");
- await populateCache(multiLevelCache, "memory_test", totalKeys, valueSizeBytes);
-
- // Get memory usage info
- const heap = memoryLevel.getHeap();
- const itemCount = heap.getCount();
- const estimatedMemoryMB = (itemCount * valueSizeBytes) / 1024 / 1024;
- const avgMemoryPerItemKB = valueSizeBytes / 1024;
-
- // Print results
- printMemoryEfficiency(itemCount, totalKeys, estimatedMemoryMB, avgMemoryPerItemKB);
-
- // Cleanup
- await redisClient.quit();
- }, 120000);
-});
diff --git a/tests/benchmarks/read.ts b/tests/benchmarks/read.ts
new file mode 100644
index 0000000..2f29793
--- /dev/null
+++ b/tests/benchmarks/read.ts
@@ -0,0 +1,76 @@
+import benny from 'benny';
+import {
+ RedisContainer,
+ StartedRedisContainer,
+} from '@testcontainers/redis';
+import { Redis } from 'ioredis';
+import { CacheService } from '../../src/cache.service';
+import {
+ MemoryCacheLevel,
+ RedisCacheLevel,
+} from '../../src/levels';
+import { FirstExpiringMemoryPolicy } from '../../src/policies/first-expiring-memory.policy';
+import { RamPercentageLimitStrategy } from '../../src/strategies/ram-percentage-limit.strategy';
+
+const TOTAL_CALLS = 10000;
+let redisContainer: StartedRedisContainer;
+let redisClient: Redis;
+let redisOnlyClient: Redis;
+let multiLevelCache: CacheService;
+let redisOnlyCache: CacheService;
+
+async function setup() {
+ redisContainer = await new RedisContainer('redis:7.2').start();
+ redisClient = new Redis(redisContainer.getConnectionUrl());
+ redisOnlyClient = new Redis(redisContainer.getConnectionUrl());
+
+ const memoryLevel = new MemoryCacheLevel({
+ memoryStrategies: [new RamPercentageLimitStrategy(80)],
+ evictionPolicy: new FirstExpiringMemoryPolicy(),
+ });
+ const redisLevel = new RedisCacheLevel(redisClient);
+ multiLevelCache = new CacheService({
+ levels: [memoryLevel, redisLevel],
+ defaultTTL: 3600,
+ });
+
+ const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient);
+ redisOnlyCache = new CacheService({
+ levels: [redisOnlyLevel],
+ defaultTTL: 3600,
+ });
+
+ // Pre-populate both caches
+ for (let i = 0; i < TOTAL_CALLS; i++) {
+ const key = `speed_test_key_${i}`;
+ const value = { id: i, data: `test_data_${i}` };
+ await multiLevelCache.set(key, value);
+ await redisOnlyCache.set(key, value);
+ }
+}
+
+async function teardown() {
+ await redisClient.quit();
+ await redisOnlyClient.quit();
+ await redisContainer.stop();
+}
+
+setup().then(() => {
+ benny.suite(
+ 'Cache Read Performance',
+ benny.add('Multi-Level Cache', async () => {
+ const keyIndex = Math.floor(Math.random() * TOTAL_CALLS);
+ const key = `speed_test_key_${keyIndex}`;
+ await multiLevelCache.get(key, null);
+ }),
+ benny.add('Redis-Only Cache', async () => {
+ const keyIndex = Math.floor(Math.random() * TOTAL_CALLS);
+ const key = `speed_test_key_${keyIndex}`;
+ await redisOnlyCache.get(key, null);
+ }),
+ benny.cycle(),
+ benny.complete(async () => {
+ await teardown();
+ }),
+ );
+});
diff --git a/tests/benchmarks/write.ts b/tests/benchmarks/write.ts
new file mode 100644
index 0000000..6b66f46
--- /dev/null
+++ b/tests/benchmarks/write.ts
@@ -0,0 +1,69 @@
+import benny from 'benny';
+import {
+ RedisContainer,
+ StartedRedisContainer,
+} from '@testcontainers/redis';
+import { Redis } from 'ioredis';
+import { CacheService } from '../../src/cache.service';
+import {
+ MemoryCacheLevel,
+ RedisCacheLevel,
+} from '../../src/levels';
+import { FirstExpiringMemoryPolicy } from '../../src/policies/first-expiring-memory.policy';
+
+const TOTAL_CALLS = 10000;
+let redisContainer: StartedRedisContainer;
+let redisClient: Redis;
+let redisOnlyClient: Redis;
+let multiLevelCache: CacheService;
+let redisOnlyCache: CacheService;
+let memoryOnlyCache: MemoryCacheLevel;
+
+async function setup() {
+ redisContainer = await new RedisContainer('redis:7.2').start();
+ redisClient = new Redis(redisContainer.getConnectionUrl());
+ redisOnlyClient = new Redis(redisContainer.getConnectionUrl());
+
+ memoryOnlyCache = new MemoryCacheLevel({
+ memoryStrategies: [],
+ evictionPolicy: new FirstExpiringMemoryPolicy(),
+ });
+ const redisLevel = new RedisCacheLevel(redisClient);
+ multiLevelCache = new CacheService({
+ levels: [memoryOnlyCache, redisLevel],
+ defaultTTL: 3600,
+ });
+
+ const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient);
+ redisOnlyCache = new CacheService({
+ levels: [redisOnlyLevel],
+ defaultTTL: 3600,
+ });
+}
+
+async function teardown() {
+ await redisClient.quit();
+ await redisOnlyClient.quit();
+ await redisContainer.stop();
+}
+
+setup().then(() => {
+ benny.suite(
+ 'Cache Write Performance',
+ benny.add('Multi-Level Cache', async () => {
+ const keyIndex = Math.floor(Math.random() * TOTAL_CALLS);
+ const key = `speed_test_key_${keyIndex}`;
+ await multiLevelCache.set(key, { id: keyIndex, data: `test_data_${keyIndex}` });
+ }),
+ benny.add('Redis-Only Cache', async () => {
+ const keyIndex = Math.floor(Math.random() * TOTAL_CALLS);
+ const key = `speed_test_key_${keyIndex}`;
+ await redisOnlyCache.set(key, { id: keyIndex, data: `test_data_${keyIndex}` });
+ }),
+ benny.cycle(),
+ benny.complete(async () => {
+ await teardown();
+ }),
+ benny.save({ file: 'cache-write-performance', format: 'json' })
+ );
+});
diff --git a/tests/utilities/benchmark-output.utilities.ts b/tests/utilities/benchmark-output.utilities.ts
deleted file mode 100644
index 2e27415..0000000
--- a/tests/utilities/benchmark-output.utilities.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Utility functions for formatting and printing benchmark results
- */
-
-import type { BenchmarkResult } from "./benchmark.utilities";
-
-/**
- * Print cache hit rate analysis results
- */
-export function printCacheHitRateResults(result: BenchmarkResult): void {
- console.log("Results:");
- console.log(` Total Calls: ${result.totalCalls}`);
- console.log(
- ` Memory Cache Hits: ${result.memoryCacheHits} (${((result.memoryCacheHits! / result.totalCalls) * 100).toFixed(2)}%)`,
- );
- console.log(
- ` Redis Cache Hits: ${result.redisCacheHits} (${((result.redisCacheHits! / result.totalCalls) * 100).toFixed(2)}%)`,
- );
- console.log(
- ` Cache Misses: ${result.cacheMisses} (${((result.cacheMisses! / result.totalCalls) * 100).toFixed(2)}%)`,
- );
- console.log("\n Performance Metrics:");
- console.log(` Total Duration: ${result.totalDuration}ms`);
- console.log(` Average Latency: ${result.avgLatencyMs.toFixed(2)}ms`);
- console.log(` P50 Latency: ${result.p50LatencyMs}ms`);
- console.log(` P95 Latency: ${result.p95LatencyMs}ms`);
- console.log(` P99 Latency: ${result.p99LatencyMs}ms`);
- console.log(` Throughput: ${result.throughputOps.toFixed(2)} ops/sec`);
- console.log("\n Key Insights:");
- console.log(
- ` - Memory cache prevented ${result.memoryCacheHits} Redis calls`,
- );
- console.log(
- ` - That's ${((result.memoryCacheHits! / result.totalCalls) * 100).toFixed(2)}% reduction in Redis load`,
- );
- console.log(
- ` - Redis was hit ${result.redisCacheHits} times when memory cache missed\n`,
- );
-}
-
-/**
- * Print benchmark results for a single cache configuration
- */
-export function printBenchmarkResults(
- title: string,
- result: BenchmarkResult,
-): void {
- console.log(`\n${title}:`);
- console.log(` Total Duration: ${result.totalDuration}ms`);
- console.log(` Avg Latency: ${result.avgLatencyMs.toFixed(2)}ms`);
- console.log(` P50 Latency: ${result.p50LatencyMs}ms`);
- console.log(` P95 Latency: ${result.p95LatencyMs}ms`);
- console.log(` P99 Latency: ${result.p99LatencyMs}ms`);
- console.log(` Throughput: ${result.throughputOps.toFixed(2)} ops/sec`);
-}
-
-/**
- * Print performance comparison between two benchmark results
- */
-export function printPerformanceComparison(
- baseline: BenchmarkResult,
- improved: BenchmarkResult,
-): void {
- const speedImprovement =
- ((baseline.totalDuration - improved.totalDuration) /
- baseline.totalDuration) *
- 100;
- const latencyImprovement =
- ((baseline.avgLatencyMs - improved.avgLatencyMs) / baseline.avgLatencyMs) *
- 100;
- const throughputImprovement =
- ((improved.throughputOps - baseline.throughputOps) /
- baseline.throughputOps) *
- 100;
-
- console.log("\nPerformance Comparison:");
- console.log(
- ` ${improved.description} is ${Math.abs(speedImprovement).toFixed(2)}% ${speedImprovement > 0 ? "FASTER" : "SLOWER"} overall`,
- );
- console.log(
- ` ${improved.description} has ${Math.abs(latencyImprovement).toFixed(2)}% ${latencyImprovement > 0 ? "LOWER" : "HIGHER"} average latency`,
- );
- console.log(
- ` ${improved.description} has ${Math.abs(throughputImprovement).toFixed(2)}% ${throughputImprovement > 0 ? "HIGHER" : "LOWER"} throughput\n`,
- );
-}
-
-/**
- * Print write performance comparison
- */
-export function printWritePerformanceComparison(
- multiLevel: BenchmarkResult,
- redisOnly: BenchmarkResult,
-): void {
- const writeDiff =
- ((multiLevel.totalDuration - redisOnly.totalDuration) /
- redisOnly.totalDuration) *
- 100;
-
- console.log("\nWrite Performance Comparison:");
- console.log(
- ` Multi-Level writes are ${Math.abs(writeDiff).toFixed(2)}% ${writeDiff > 0 ? "SLOWER" : "FASTER"} than Redis-only`,
- );
- console.log(
- " This is expected as writes must update both memory and Redis layers\n",
- );
-}
-
-/**
- * Print memory efficiency statistics
- */
-export function printMemoryEfficiency(
- itemCount: number,
- totalItems: number,
- estimatedMemoryMB: number,
- avgMemoryPerItemKB: number,
-): void {
- console.log("\nMemory Usage Statistics:");
- console.log(` Items in Memory Cache: ${itemCount}`);
- console.log(` Estimated Memory Usage: ~${estimatedMemoryMB.toFixed(2)} MB`);
- console.log(
- ` Average Memory per Item: ~${avgMemoryPerItemKB.toFixed(2)} KB`,
- );
- console.log(
- ` Memory Efficiency: ${((itemCount / totalItems) * 100).toFixed(2)}% of written items retained`,
- );
-
- console.log("\nMemory Cache Benefits:");
- console.log(" - Fast in-memory access for frequently accessed items");
- console.log(" - Automatic eviction based on configured strategies");
- console.log(" - Reduces network latency for cache hits");
- console.log(" - Offloads Redis for better resource utilization\n");
-}
-
-/**
- * Print benchmark section header
- */
-export function printBenchmarkHeader(title: string): void {
- console.log("\n========================================");
- console.log(title);
- console.log("========================================\n");
-}
diff --git a/tests/utilities/benchmark.utilities.ts b/tests/utilities/benchmark.utilities.ts
deleted file mode 100644
index de59f12..0000000
--- a/tests/utilities/benchmark.utilities.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * Utility functions for cache benchmarking
- */
-
-import type { MemoryCacheLevel } from "../../src/levels";
-import type { RedisCacheLevel } from "../../src/levels/redis/redis.level";
-
-export interface BenchmarkResult {
- description: string;
- totalCalls: number;
- memoryCacheHits?: number;
- redisCacheHits?: number;
- cacheMisses?: number;
- totalDuration: number;
- avgLatencyMs: number;
- p50LatencyMs: number;
- p95LatencyMs: number;
- p99LatencyMs: number;
- throughputOps: number;
-}
-
-export interface LatencyStats {
- avgLatencyMs: number;
- p50LatencyMs: number;
- p95LatencyMs: number;
- p99LatencyMs: number;
-}
-
-/**
- * Calculate latency statistics from an array of latency measurements
- */
-export function calculateLatencyStats(latencies: number[]): LatencyStats {
- const sortedLatencies = [...latencies].sort((a, b) => a - b);
- const avgLatency =
- sortedLatencies.reduce((a, b) => a + b, 0) / sortedLatencies.length;
- const p50 = sortedLatencies[Math.floor(sortedLatencies.length * 0.5)];
- const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)];
- const p99 = sortedLatencies[Math.floor(sortedLatencies.length * 0.99)];
-
- return {
- avgLatencyMs: avgLatency,
- p50LatencyMs: p50,
- p95LatencyMs: p95,
- p99LatencyMs: p99,
- };
-}
-
-/**
- * Calculate throughput in operations per second
- */
-export function calculateThroughput(
- totalCalls: number,
- totalDurationMs: number,
-): number {
- return (totalCalls / totalDurationMs) * 1000;
-}
-
-/**
- * Calculate percentage improvement between two values
- */
-export function calculateImprovement(baseline: number, improved: number): number {
- return ((baseline - improved) / baseline) * 100;
-}
-
-/**
- * Instrument a memory cache level to track hits
- */
-export function instrumentMemoryCache(
- memoryLevel: MemoryCacheLevel,
- hitCounter: { count: number },
-): void {
- const originalGet = memoryLevel.get.bind(memoryLevel);
- const instrumentedGet = async function (
- key: string,
- valueGetter?: (() => Promise) | T,
- ttl?: number,
- ): Promise {
- const result = await originalGet(key);
- if (result !== null && result !== undefined) {
- hitCounter.count++;
- }
- return result as T;
- };
- memoryLevel.get = instrumentedGet as any;
-}
-
-/**
- * Instrument a Redis cache level to track hits and misses
- */
-export function instrumentRedisCache(
- redisLevel: RedisCacheLevel,
- hitCounter: { count: number },
- missCounter: { count: number },
-): void {
- const originalGet = redisLevel.get.bind(redisLevel);
- const instrumentedGet = async function (
- key: string,
- valueGetter?: (() => Promise) | T,
- ttl?: number,
- ): Promise {
- const result = await originalGet(key);
- if (result !== null && result !== undefined) {
- hitCounter.count++;
- } else {
- missCounter.count++;
- }
- return result as T;
- };
- redisLevel.get = instrumentedGet as any;
-}
-
-/**
- * Generate a random key index following an 80/20 access pattern
- * 80% of requests go to 20% of the keys (hot keys)
- */
-export function get8020KeyIndex(totalKeys: number, hotKeyRatio = 0.2): number {
- const hotKeys = Math.floor(totalKeys * hotKeyRatio);
- if (Math.random() < 0.8) {
- // 80% of requests go to hot keys
- return Math.floor(Math.random() * hotKeys);
- }
- // 20% of requests go to cold keys
- return hotKeys + Math.floor(Math.random() * (totalKeys - hotKeys));
-}
-
-/**
- * Generate a random key index with uniform distribution
- */
-export function getUniformKeyIndex(totalKeys: number): number {
- return Math.floor(Math.random() * totalKeys);
-}
-
-/**
- * Populate cache with test data
- */
-export async function populateCache Promise }>(
- cache: T,
- keyPrefix: string,
- count: number,
- valueSize = 100,
-): Promise {
- for (let i = 0; i < count; i++) {
- const key = `${keyPrefix}_${i}`;
- const value = { id: i, data: `test_data_${i}_${"x".repeat(valueSize)}` };
- await cache.set(key, value);
- }
-}
-
-/**
- * Run a benchmark with latency tracking
- */
-export async function runBenchmark(
- operationFn: () => Promise,
- iterations: number,
-): Promise<{ latencies: number[]; totalDuration: number }> {
- const latencies: number[] = [];
- const startTime = Date.now();
-
- for (let i = 0; i < iterations; i++) {
- const callStart = Date.now();
- await operationFn();
- const callEnd = Date.now();
- latencies.push(callEnd - callStart);
- }
-
- const endTime = Date.now();
- const totalDuration = endTime - startTime;
-
- return { latencies, totalDuration };
-}
diff --git a/tests/utilities/data.utilities.ts b/tests/utilities/data.utilities.ts
index d1abd0b..fc85b94 100644
--- a/tests/utilities/data.utilities.ts
+++ b/tests/utilities/data.utilities.ts
@@ -1,6 +1,19 @@
import { faker } from "@faker-js/faker";
import type { CacheLevel } from "../../src/levels/interfaces/cache-level";
+export async function populateCache Promise }>(
+ cache: T,
+ keyPrefix: string,
+ count: number,
+ valueSize = 100,
+): Promise {
+ for (let i = 0; i < count; i++) {
+ const key = `${keyPrefix}_${i}`;
+ const value = { id: i, data: `test_data_${i}_${"x".repeat(valueSize)}` };
+ await cache.set(key, value);
+ }
+}
+
export async function generateJSONData(
cacheLevel: CacheLevel,
recordNum: number,
diff --git a/vitest.config.ts b/vitest.config.ts
index 5238aea..f6206fa 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,6 +1,6 @@
import { defineConfig } from "vitest/config";
-export default defineConfig({
+export const config = {
test: {
coverage: {
exclude: [
@@ -12,5 +12,9 @@ export default defineConfig({
],
},
exclude: ["**/benchmarks/**", "node_modules/**"],
- },
+ }
+};
+
+export default defineConfig({
+ ...config,
});