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);