diff --git a/.github/workflows/ci_benchmark.yml b/.github/workflows/ci_benchmark.yml
new file mode 100644
index 0000000..5014401
--- /dev/null
+++ b/.github/workflows/ci_benchmark.yml
@@ -0,0 +1,48 @@
+name: '🎖️ CI — Benchmark'
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ benchmark:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ strategy:
+ fail-fast: false
+ name: Compare
+ steps:
+ - name: ➕ Actions - Checkout
+ uses: actions/checkout@v4
+
+ - name: ➕ Actions - Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ check-latest: true
+ node-version: lts/*
+
+ - name: ➕ Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: npm-linux-${{ hashFiles('package-lock.json') }}
+ restore-keys: npm-linux-
+
+ - name: ➕ Cache dependencies (Benchmark)
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: npm-benchmark-${{ hashFiles('benchmark/package-lock.json') }}
+ restore-keys: npm-benchmark-
+
+ - name: 📦 Installing Dependencies
+ run: npm ci
+
+ - name: 🚀 Building Poku
+ run: npm run build
+
+ - name: 🎖️ Rock it (ESM)
+ run: npm run benchmark:esm
+
+ - name: 🎖️ Rock it (CJS)
+ run: npm run benchmark:cjs
diff --git a/.github/workflows/ci_codeql.yml b/.github/workflows/ci_codeql.yml
new file mode 100644
index 0000000..3d6b52c
--- /dev/null
+++ b/.github/workflows/ci_codeql.yml
@@ -0,0 +1,80 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: '🔎 CI — CodeQL'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ analyze:
+ name: Analyze
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners
+ # Consider using larger runners for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript-typescript']
+ # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
+ # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+ config-file: ./.github/codeql/codeql-config.yml
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
+ with:
+ category: '/language:${{matrix.language}}'
diff --git a/README.md b/README.md
index b74d995..249470f 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[![GitHub Workflow Status (Bun)](https://img.shields.io/github/actions/workflow/status/wellwelwel/lru.min/ci_bun.yml?event=push&label=&branch=main&logo=bun&logoColor=ffffff&color=f368e0)](https://github.com/wellwelwel/lru.min/actions/workflows/ci_bun.yml?query=branch%3Amain)
[![GitHub Workflow Status (Deno)](https://img.shields.io/github/actions/workflow/status/wellwelwel/lru.min/ci_deno.yml?event=push&label=&branch=main&logo=deno&logoColor=ffffff&color=079992)](https://github.com/wellwelwel/lru.min/actions/workflows/ci_deno.yml?query=branch%3Amain)
-🔥 An extremely fast and efficient LRU Cache for JavaScript (Browser compatible) — **6.4KB**.
+🔥 An extremely fast and efficient LRU Cache for JavaScript (Browser compatible) — **6.7KB**.
@@ -87,6 +87,8 @@ Adds a key-value pair to the cache. Updates the value if the key already exists
LRU.set('key', 'value');
```
+> `undefined` keys will simply be ignored.
+
### Get a cache
Retrieves the value for a given key and moves the key to the most recent position.
@@ -207,56 +209,29 @@ LRU.forEach((key, value) => {
The benchmark is performed by comparing `1,000,000` runs through a maximum cache limit of `100,000`, getting `333,333` caches and delenting `200,000` keys 10 consecutive times, clearing the cache every run.
-> You can see how the tests are run and compared in the [benchmark](https://github.com/wellwelwel/lru.min/tree/main/benchmark) directory.
->
> - [**lru-cache**](https://github.com/isaacs/node-lru-cache) `v11.0.0`
> - [**quick-lru**](https://github.com/sindresorhus/quick-lru) `v7.0.0`
-#### Node.js
-
-```sh
-# ES Modules
-lru.min: 247.45ms
-lru-cache: 255.93ms
-quick-lru: 312.90ms
-```
-
-```sh
-# CommonJS
-lru.min: 236.35ms
-lru-cache: 258.74ms
-quick-lru: not compatible
-```
-
-#### Bun
-
-```sh
-# ES Modules
-lru.min: 298.42ms
-quick-lru: 315.37ms
-lru-cache: 359.23ms
-```
-
```sh
-# CommonJS
-lru.min: 363.79ms
-lru-cache: 371.39ms
-quick-lru: not compatible
+# Time:
+ lru.min: 240.45ms
+ lru-cache: 258.32ms
+ quick-lru: 279.89ms
+
+# CPU:
+ lru.min: 275558.30µs
+ lru-cache: 306858.30µs
+ quick-lru: 401318.80µs
```
-#### Deno
-
-```sh
-# ES Modules
-lru.min: 222.60ms
-lru-cache: 227.80ms
-quick-lru: 253.00ms
-```
+- See detailed results and how the tests are run and compared in the [benchmark](https://github.com/wellwelwel/lru.min/tree/main/benchmark) directory.
---
## Security Policy
+[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/wellwelwel/lru.min/ci_codeql.yml?event=push&label=&branch=main&logo=github&logoColor=white&color=f368e0)](https://github.com/wellwelwel/lru.min/actions/workflows/ci_codeql.yml?query=branch%3Amain)
+
Please check the [**SECURITY.md**](https://github.com/wellwelwel/lru.min/blob/main/SECURITY.md).
---
diff --git a/benchmark/README.md b/benchmark/README.md
new file mode 100644
index 0000000..11e6d4b
--- /dev/null
+++ b/benchmark/README.md
@@ -0,0 +1,92 @@
+# Benchmark
+
+The benchmark is performed by comparing `1,000,000` runs through a maximum cache limit of `100,000`, getting `333,333` caches and delenting `200,000` keys 10 consecutive times, clearing the cache every run.
+
+> - [**lru-cache**](https://github.com/isaacs/node-lru-cache) `v11.0.0`
+> - [**quick-lru**](https://github.com/sindresorhus/quick-lru) `v7.0.0`
+
+#### Node.js
+
+- ES Modules
+
+```sh
+# Time:
+ lru.min: 240.45ms
+ lru-cache: 258.32ms
+ quick-lru: 279.89ms
+
+# CPU:
+ lru.min: 275558.30µs
+ lru-cache: 306858.30µs
+ quick-lru: 401318.80µs
+```
+
+- CommonJS
+
+```sh
+# Time:
+ lru.min: 242.86ms
+ lru-cache: 264.30ms
+ quick-lru: not compatible
+
+# CPU:
+ lru.min: 280118.00µs
+ lru-cache: 310327.20µs
+ quick-lru: not compatible
+```
+
+#### Bun
+
+- ES Modules
+
+```sh
+# Time:
+ lru.min: 298.42ms
+ quick-lru: 315.37ms
+ lru-cache: 359.23ms
+
+# CPU:
+ lru.min: 401869.50µs
+ quick-lru: 478517.40µs
+ lru-cache: 510357.60µs
+```
+
+- CommonJS
+
+```sh
+# Time:
+ lru.min: 324.41ms
+ lru-cache: 370.22ms
+ quick-lru: not compatible
+
+# CPU:
+ lru.min: 396790.50µs
+ lru-cache: 488574.50µs
+ quick-lru: not compatible
+```
+
+#### Deno
+
+- ES Modules
+
+```sh
+# Time:
+ lru.min: 222.60ms
+ lru-cache: 227.80ms
+ quick-lru: 253.00ms
+```
+
+> **Deno** benchmarks were carried out without an isolated process.
+
+---
+
+## Running
+
+To run the benchmark tests, follow these steps in the `./lru.min` directory:
+
+```sh
+npm ci
+npm run build
+npm run benchmark:esm
+npm run benchmark:cjs
+```
diff --git a/benchmark/index.cjs b/benchmark/index.cjs
index 877185e..399b387 100644
--- a/benchmark/index.cjs
+++ b/benchmark/index.cjs
@@ -1,69 +1,46 @@
-const { performance } = require('node:perf_hooks');
-const { LRUCache } = require('lru-cache');
-const { createLRU } = require('../lib/index.js');
+const process = require('node:process');
+const { fork } = require('node:child_process');
+const benchmarks = ['lru-cache', 'lru.min'];
const results = new Map();
-const measurePerformance = (fn) => {
- const startTime = performance.now();
+const runBenchmark = (benchmarkName) => {
+ return new Promise((resolve) => {
+ const child = fork('worker.cjs', [benchmarkName]);
- fn();
-
- const endTime = performance.now();
-
- return {
- time: endTime - startTime,
- };
-};
-
-const times = 10;
-const max = 100000;
-const brute = 1000000;
-
-const benchmark = (createCache) => {
- const cache = createCache();
- const results = { time: 0 };
-
- for (let i = 0; i < times; i++) {
- const result = measurePerformance(() => {
- for (let j = 0; j < brute; j++) {
- cache.set(`key-${j}`, j);
-
- if (j > 0 && j % 3 === 0) {
- const randomIndex = Math.floor(Math.random() * j);
- cache.get(`key-${randomIndex}`);
- }
-
- if (j > 0 && j % 5 === 0) {
- const randomIndex = Math.floor(Math.random() * j);
- cache.delete(`key-${randomIndex}`);
- }
- }
+ child.on('message', (result) => {
+ resolve(result);
});
- results.time += result.time;
-
- cache.clear();
- }
-
- return {
- time: results.time / times,
- };
+ child.on('exit', () => {
+ resolve(null);
+ });
+ });
};
-results.set(
- 'lru-cache',
- benchmark(() => new LRUCache({ max }))
-);
-results.set(
- 'lru.min',
- benchmark(() => createLRU({ max }))
-);
-
-const time = [...results.entries()].sort((a, b) => a[1].time - b[1].time);
-
-console.log('CommonJS');
+(async () => {
+ for (const benchmark of benchmarks) {
+ const result = await runBenchmark(benchmark);
+ results.set(benchmark, result);
+ }
-for (const [name, result] of time)
- console.log(`${name}: ${result.time.toFixed(2)}ms`);
-console.log('quick-lru: not compatible');
+ const sortedByTime = [...results.entries()].sort(
+ (a, b) => a[1].time - b[1].time
+ );
+ const sortedByCpu = [...results.entries()].sort(
+ (a, b) => a[1].cpu - b[1].cpu
+ );
+
+ console.log('Time:');
+ for (const [name, result] of sortedByTime)
+ console.log(` ${name}: ${result.time.toFixed(2)}ms`);
+ console.log(' quick-lru: not compatible');
+
+ console.log('\nCPU:');
+ for (const [name, result] of sortedByCpu)
+ console.log(` ${name}: ${result.cpu.toFixed(2)}µs`);
+ console.log(' quick-lru: not compatible');
+
+ if (sortedByTime[0][0] !== 'lru.min' || sortedByCpu[0][0] !== 'lru.min')
+ process.exit(1);
+})();
diff --git a/benchmark/index.mjs b/benchmark/index.mjs
index 0ac2e6a..eda5cd8 100644
--- a/benchmark/index.mjs
+++ b/benchmark/index.mjs
@@ -1,73 +1,44 @@
-import { performance } from 'node:perf_hooks';
-import { LRUCache } from 'lru-cache';
-import { createLRU } from '../lib/index.mjs';
-import QuickLRU from 'quick-lru';
+import process from 'node:process';
+import { fork } from 'node:child_process';
+const benchmarks = ['lru-cache', 'quick-lru', 'lru.min'];
const results = new Map();
-const measurePerformance = (fn) => {
- const startTime = performance.now();
+const runBenchmark = (benchmarkName) => {
+ return new Promise((resolve) => {
+ const child = fork('worker.mjs', [benchmarkName]);
- fn();
-
- const endTime = performance.now();
-
- return {
- time: endTime - startTime,
- };
-};
-
-const times = 10;
-const max = 100000;
-const brute = 1000000;
-
-const benchmark = (createCache) => {
- const cache = createCache();
- const results = { time: 0 };
-
- for (let i = 0; i < times; i++) {
- const result = measurePerformance(() => {
- for (let j = 0; j < brute; j++) {
- cache.set(`key-${j}`, j);
-
- if (j > 0 && j % 3 === 0) {
- const randomIndex = Math.floor(Math.random() * j);
- cache.get(`key-${randomIndex}`);
- }
-
- if (j > 0 && j % 5 === 0) {
- const randomIndex = Math.floor(Math.random() * j);
- cache.delete(`key-${randomIndex}`);
- }
- }
+ child.on('message', (result) => {
+ resolve(result);
});
- results.time += result.time;
+ child.on('exit', () => {
+ resolve(null);
+ });
+ });
+};
- cache.clear();
+(async () => {
+ for (const benchmark of benchmarks) {
+ const result = await runBenchmark(benchmark);
+ results.set(benchmark, result);
}
- return {
- time: results.time / times,
- };
-};
-
-results.set(
- 'lru-cache',
- benchmark(() => new LRUCache({ max }))
-);
-results.set(
- 'quick-lru',
- benchmark(() => new QuickLRU({ maxSize: max }))
-);
-results.set(
- 'lru.min',
- benchmark(() => createLRU({ max }))
-);
+ const sortedByTime = [...results.entries()].sort(
+ (a, b) => a[1].time - b[1].time
+ );
+ const sortedByCpu = [...results.entries()].sort(
+ (a, b) => a[1].cpu - b[1].cpu
+ );
-const time = [...results.entries()].sort((a, b) => a[1].time - b[1].time);
+ console.log('Time:');
+ for (const [name, result] of sortedByTime)
+ console.log(` ${name}: ${result.time.toFixed(2)}ms`);
-console.log('ES Modules');
+ console.log('\nCPU:');
+ for (const [name, result] of sortedByCpu)
+ console.log(` ${name}: ${result.cpu.toFixed(2)}µs`);
-for (const [name, result] of time)
- console.log(`${name}: ${result.time.toFixed(2)}ms`);
+ if (sortedByTime[0][0] !== 'lru.min' || sortedByCpu[0][0] !== 'lru.min')
+ process.exit(1);
+})();
diff --git a/benchmark/worker.cjs b/benchmark/worker.cjs
new file mode 100644
index 0000000..5ffebec
--- /dev/null
+++ b/benchmark/worker.cjs
@@ -0,0 +1,71 @@
+const process = require('node:process');
+const { cpuUsage } = require('node:process');
+const { performance } = require('node:perf_hooks');
+const { LRUCache } = require('lru-cache');
+const { createLRU } = require('../lib/index.js');
+
+const benchmarkName = process.argv[2];
+
+const measurePerformance = (fn) => {
+ const startTime = performance.now();
+ const startCpu = cpuUsage();
+
+ fn();
+
+ const endTime = performance.now();
+ const endCpu = cpuUsage(startCpu);
+
+ return {
+ time: endTime - startTime,
+ cpu: endCpu.user + endCpu.system,
+ };
+};
+
+const times = 10;
+const max = 100000;
+const brute = 1000000;
+
+const benchmarks = {
+ 'lru-cache': () => new LRUCache({ max }),
+ 'lru.min': () => createLRU({ max }),
+};
+
+const benchmark = (createCache) => {
+ if (global?.gc) global.gc();
+
+ const cache = createCache();
+ const results = { time: 0, cpu: 0 };
+
+ for (let i = 0; i < times; i++) {
+ const result = measurePerformance(() => {
+ for (let j = 0; j < brute; j++) {
+ cache.set(`key-${j}`, j);
+
+ if (j > 0 && j % 3 === 0) {
+ const randomIndex = Math.floor(Math.random() * j);
+ cache.get(`key-${randomIndex}`);
+ }
+
+ if (j > 0 && j % 5 === 0) {
+ const randomIndex = Math.floor(Math.random() * j);
+ cache.delete(`key-${randomIndex}`);
+ }
+ }
+ });
+
+ results.time += result.time;
+ results.cpu += result.cpu;
+
+ cache.clear();
+ }
+
+ return {
+ time: results.time / times,
+ cpu: results.cpu / times,
+ };
+};
+
+const result = benchmark(benchmarks[benchmarkName]);
+
+process.send(result);
+process.exit();
diff --git a/benchmark/worker.mjs b/benchmark/worker.mjs
new file mode 100644
index 0000000..1a23a00
--- /dev/null
+++ b/benchmark/worker.mjs
@@ -0,0 +1,72 @@
+import process from 'node:process';
+import { performance } from 'node:perf_hooks';
+import { LRUCache } from 'lru-cache';
+import QuickLRU from 'quick-lru';
+import { createLRU } from '../lib/index.mjs';
+
+const benchmarkName = process.argv[2];
+
+const measurePerformance = (fn) => {
+ const startTime = performance.now();
+ const startCpu = process.cpuUsage();
+
+ fn();
+
+ const endTime = performance.now();
+ const endCpu = process.cpuUsage(startCpu);
+
+ return {
+ time: endTime - startTime,
+ cpu: endCpu.user + endCpu.system,
+ };
+};
+
+const times = 10;
+const max = 100000;
+const brute = 1000000;
+
+const benchmarks = {
+ 'lru-cache': () => new LRUCache({ max }),
+ 'quick-lru': () => new QuickLRU({ maxSize: max }),
+ 'lru.min': () => createLRU({ max }),
+};
+
+const benchmark = (createCache) => {
+ if (global?.gc) global.gc();
+
+ const cache = createCache();
+ const results = { time: 0, cpu: 0 };
+
+ for (let i = 0; i < times; i++) {
+ const result = measurePerformance(() => {
+ for (let j = 0; j < brute; j++) {
+ cache.set(`key-${j}`, j);
+
+ if (j > 0 && j % 3 === 0) {
+ const randomIndex = Math.floor(Math.random() * j);
+ cache.get(`key-${randomIndex}`);
+ }
+
+ if (j > 0 && j % 5 === 0) {
+ const randomIndex = Math.floor(Math.random() * j);
+ cache.delete(`key-${randomIndex}`);
+ }
+ }
+ });
+
+ results.time += result.time;
+ results.cpu += result.cpu;
+
+ cache.clear();
+ }
+
+ return {
+ time: results.time / times,
+ cpu: results.cpu / times,
+ };
+};
+
+const result = benchmark(benchmarks[benchmarkName]);
+
+process.send(result);
+process.exit();
diff --git a/package-lock.json b/package-lock.json
index 875d0f2..c7581ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"packages-update": "^2.0.0",
"poku": "^2.5.0",
"prettier": "^3.3.3",
- "tsx": "^4.17.0",
+ "tsx": "^4.19.0",
"typescript": "^5.5.4"
},
"engines": {
@@ -1072,9 +1072,9 @@
}
},
"node_modules/tsx": {
- "version": "4.17.0",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz",
- "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==",
+ "version": "4.19.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz",
+ "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 93e5f59..9e6f3eb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "lru.min",
"version": "0.3.1",
- "description": "🔥 An extremely fast and efficient LRU cache for JavaScript (Browser compatible) — 6.4KB.",
+ "description": "🔥 An extremely fast and efficient LRU cache for JavaScript (Browser compatible) — 6.7KB.",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
"types": "./lib/index.d.ts",
@@ -28,7 +28,8 @@
"deno": ">=1.30.0"
},
"scripts": {
- "benchmark": "cd benchmark && npm ci && node lib/index.js",
+ "benchmark:esm": "cd benchmark && npm ci && node index.mjs",
+ "benchmark:cjs": "cd benchmark && npm ci && node index.cjs",
"prebuild": "rm -rf ./browser ./lib",
"build:browser": "tsx tools/browserfy.ts",
"build:esm": "esbuild src/index.ts --outfile=lib/index.mjs --platform=node --target=node12 --format=esm",
@@ -52,7 +53,7 @@
"packages-update": "^2.0.0",
"poku": "^2.5.0",
"prettier": "^3.3.3",
- "tsx": "^4.17.0",
+ "tsx": "^4.19.0",
"typescript": "^5.5.4"
},
"exports": {
diff --git a/src/index.ts b/src/index.ts
index 70ce469..8c62e40 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,14 +20,14 @@ export const createLRU = (options: {
const next: number[] = new Array(max).fill(0);
const prev: number[] = new Array(max).fill(0);
- const moveToTail = (index: number): undefined => {
+ const setTail = (index: number, type: 'set' | 'get'): undefined => {
if (index === tail) return;
const nextIndex = next[index];
const prevIndex = prev[index];
if (index === head) head = nextIndex;
- else if (prevIndex !== 0) next[prevIndex] = nextIndex;
+ else if (type === 'get' || prevIndex !== 0) next[prevIndex] = nextIndex;
if (nextIndex !== 0) prev[nextIndex] = prevIndex;
@@ -54,12 +54,16 @@ export const createLRU = (options: {
if (size === 0) head = tail = 0;
+ free.push(evictHead);
+
return evictHead;
};
return {
/** Adds a key-value pair to the cache. Updates the value if the key already exists. */
set(key: Key, value: Value): undefined {
+ if (key === undefined) return;
+
let index = keyMap.get(key);
if (index === undefined) {
@@ -72,7 +76,7 @@ export const createLRU = (options: {
valList[index] = value;
if (size === 1) head = tail = index;
- else moveToTail(index);
+ else setTail(index, 'set');
},
/** Retrieves the value for a given key and moves the key to the most recent position. */
@@ -80,8 +84,8 @@ export const createLRU = (options: {
const index = keyMap.get(key);
if (index === undefined) return;
+ if (index !== tail) setTail(index, 'get');
- moveToTail(index);
return valList[index];
},
@@ -143,20 +147,27 @@ export const createLRU = (options: {
delete(key: Key): boolean {
const index = keyMap.get(key);
- if (index !== undefined) {
- onEviction?.(key, valList[index]!);
- keyMap.delete(key);
- free.push(index);
+ if (index === undefined) return false;
- keyList[index] = undefined;
- valList[index] = undefined;
+ onEviction?.(key, valList[index]!);
+ keyMap.delete(key);
+ free.push(index);
- size--;
+ keyList[index] = undefined;
+ valList[index] = undefined;
- return true;
- }
+ const prevIndex = prev[index];
+ const nextIndex = next[index];
+
+ if (prevIndex !== 0) next[prevIndex] = nextIndex;
+ if (nextIndex !== 0) prev[nextIndex] = prevIndex;
+
+ if (index === head) head = nextIndex;
+ if (index === tail) tail = prevIndex;
+
+ size--;
- return false;
+ return true;
},
/** Evicts the oldest item or the specified number of the oldest items from the cache. */
diff --git a/test/order.test.ts b/test/order.test.ts
index 3ba0f6f..fe65dac 100644
--- a/test/order.test.ts
+++ b/test/order.test.ts
@@ -21,6 +21,19 @@ describe('Order Suite', () => {
['key1', 'value1'],
]
);
+
+ assert.strictEqual(LRU.get('key2'), 'value2');
+ assert.strictEqual(LRU.get('key1'), 'value1');
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key1', 'value1'],
+ ['key2', 'value2'],
+ ['key3', 'value3'],
+ ['key4', 'value4'],
+ ]
+ );
});
it('should not change the order when an item is peeked', () => {
diff --git a/test/size.test.ts b/test/size.test.ts
index 227ec0c..8edad11 100644
--- a/test/size.test.ts
+++ b/test/size.test.ts
@@ -108,6 +108,19 @@ describe('Size suite', () => {
['key6', 'value6'],
]
);
+
+ for (let i = 100; i <= 1000; i++) LRU.set(`key${i}`, `value${i}`);
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key1000', 'value1000'],
+ ['key999', 'value999'],
+ ['key998', 'value998'],
+ ['key996', 'value996'],
+ ['key995', 'value995'],
+ ]
+ );
});
it('should evict the specified number of items and update the size', () => {
@@ -135,6 +148,24 @@ describe('Size suite', () => {
['key4', 'value4'],
]
);
+
+ for (let i = 100; i <= 1000; i++) LRU.set(`key${i}`, `value${i}`);
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key1000', 'value1000'],
+ ['key999', 'value999'],
+ ['key998', 'value998'],
+ ['key997', 'value997'],
+ ['key996', 'value996'],
+ ['key995', 'value995'],
+ ['key994', 'value994'],
+ ['key993', 'value993'],
+ ['key992', 'value992'],
+ ['key991', 'value991'],
+ ]
+ );
});
it('should ignore resize', () => {
@@ -164,6 +195,83 @@ describe('Size suite', () => {
assert.strictEqual(LRU.delete('key3'), false, 'Invalid key');
assert.strictEqual(LRU.size, 9);
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key10', 'value10'],
+ ['key9', 'value9'],
+ ['key8', 'value8'],
+ ['key7', 'value7'],
+ ['key6', 'value6'],
+ ['key5', 'value5'],
+ ['key4', 'value4'],
+ ['key2', 'value2'],
+ ['key1', 'value1'],
+ ]
+ );
+
+ LRU.set('key100', 'value100');
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key100', 'value100'],
+ ['key10', 'value10'],
+ ['key9', 'value9'],
+ ['key8', 'value8'],
+ ['key7', 'value7'],
+ ['key6', 'value6'],
+ ['key5', 'value5'],
+ ['key4', 'value4'],
+ ['key2', 'value2'],
+ ['key1', 'value1'],
+ ]
+ );
+
+ LRU.delete('key1');
+ LRU.delete('key2');
+ LRU.delete('key3');
+ LRU.delete('key100');
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key10', 'value10'],
+ ['key9', 'value9'],
+ ['key8', 'value8'],
+ ['key7', 'value7'],
+ ['key6', 'value6'],
+ ['key5', 'value5'],
+ ['key4', 'value4'],
+ ]
+ );
+
+ LRU.set('key100', 'value100');
+
+ assert.deepStrictEqual(
+ [...LRU.entries()],
+ [
+ ['key100', 'value100'],
+ ['key10', 'value10'],
+ ['key9', 'value9'],
+ ['key8', 'value8'],
+ ['key7', 'value7'],
+ ['key6', 'value6'],
+ ['key5', 'value5'],
+ ['key4', 'value4'],
+ ]
+ );
+
+ assert.strictEqual(LRU.has('key5'), true);
+ assert.strictEqual(LRU.peek('key5'), 'value5');
+ assert.strictEqual(LRU.get('key5'), 'value5');
+
+ LRU.delete('key5');
+
+ assert.strictEqual(LRU.has('key5'), false);
+ assert.strictEqual(LRU.peek('key5'), undefined);
+ assert.strictEqual(LRU.get('key5'), undefined);
});
it('should clear the cache', () => {
diff --git a/test/types.test.ts b/test/types.test.ts
index fde98e0..278c839 100644
--- a/test/types.test.ts
+++ b/test/types.test.ts
@@ -131,35 +131,33 @@ const snapshop = {
};
describe('Types Suite', () => {
- it('should match a symbol key', () => {
+ it('should not match an undefined key', () => {
const LRU = createLRU({ max: 5 });
-
- const key = Symbol('undefined');
+ const key = undefined;
LRU.set(key, snapshop);
- assert.deepStrictEqual(LRU.get(key), snapshop);
+ assert.deepStrictEqual(LRU.get(key), undefined);
- assert.deepStrictEqual([...LRU.entries()], [[key, snapshop]]);
+ assert.deepStrictEqual([...LRU.entries()], []);
});
- it('should match an object key', () => {
+ it('should match a symbol key', () => {
const LRU = createLRU({ max: 5 });
-
- const key = {
- test: true,
- };
+ const key = Symbol('undefined');
LRU.set(key, snapshop);
assert.deepStrictEqual(LRU.get(key), snapshop);
+
assert.deepStrictEqual([...LRU.entries()], [[key, snapshop]]);
});
- it('should match an intentional undefined key', () => {
+ it('should match an object key', () => {
const LRU = createLRU({ max: 5 });
-
- const key = undefined;
+ const key = {
+ test: true,
+ };
LRU.set(key, snapshop);
@@ -169,7 +167,6 @@ describe('Types Suite', () => {
it('should match an number key', () => {
const LRU = createLRU({ max: 5 });
-
const key = 941235;
LRU.set(key, snapshop);
@@ -180,7 +177,6 @@ describe('Types Suite', () => {
it('should match a number key', () => {
const LRU = createLRU({ max: 5 });
-
const key = 941235;
LRU.set(key, snapshop);
@@ -191,7 +187,6 @@ describe('Types Suite', () => {
it('should match a big int key', () => {
const LRU = createLRU({ max: 5 });
-
const key = 941283745934857639487563945235n;
LRU.set(key, snapshop);
@@ -202,7 +197,6 @@ describe('Types Suite', () => {
it('should match a multi line key', () => {
const LRU = createLRU({ max: 5 });
-
const key = `
ln
`;
@@ -215,7 +209,6 @@ describe('Types Suite', () => {
it('should match a `false` key', () => {
const LRU = createLRU({ max: 5 });
-
const key = false;
LRU.set(key, snapshop);
@@ -226,7 +219,6 @@ describe('Types Suite', () => {
it('should match a `true` key', () => {
const LRU = createLRU({ max: 5 });
-
const key = true;
LRU.set(key, snapshop);
@@ -246,7 +238,6 @@ describe('Types Suite', () => {
it('should match an emoji key', () => {
const LRU = createLRU({ max: 5 });
-
const key = '🧑🏻🔬';
LRU.set(key, snapshop);
@@ -257,7 +248,6 @@ describe('Types Suite', () => {
it('should match an emoji key', () => {
const LRU = createLRU({ max: 5 });
-
const key = '🧑🏻🔬';
LRU.set(key, snapshop);
@@ -268,7 +258,6 @@ describe('Types Suite', () => {
it('should match an ideogram key', () => {
const LRU = createLRU({ max: 5 });
-
const key = 'テスト試験בדיקה测试測試тест';
LRU.set(key, snapshop);