diff --git a/README.md b/README.md index 380cf7d..d1878a2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,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.8KB**. +🔥 An extremely fast and efficient LRU Cache for JavaScript (Browser compatible) — **8.8KB**. @@ -45,6 +45,7 @@ deno add npm:lru.min import { createLRU } from 'lru.min'; const max = 2; +const maxAge = 300000; // 5m const onEviction = (key, value) => { console.log(`Key "${key}" with value "${value}" has been evicted.`); }; @@ -52,6 +53,7 @@ const onEviction = (key, value) => { const LRU = createLRU({ max, onEviction, + maxAge, }); LRU.set('A', 'My Value'); @@ -72,7 +74,9 @@ LRU.clear(); // LRU.evict(max) // => Key "C" with value "Another Value" has been evicted. -LRU.set('D', "You're amazing 💛"); +LRU.set('D', "You're amazing 💛", { + maxAge: Number.POSITIVE_INFINITY, +}); LRU.size; // 1 LRU.max; // 2 @@ -83,6 +87,30 @@ LRU.resize(10); LRU.size; // 1 LRU.max; // 10 LRU.available; // 9 + +LRU.set('E', 'Yet Another Value'); + +[...LRU.debug()]; +/** + * [ + * { + * key: 'E', + * value: 'Yet Another Value', + * maxAge: 300000, + * expiresAt: 299999.90000000596, + * isExpired: false, + * position: 0 + * }, + * { + * key: 'D', + * value: "You're amazing 💛", + * maxAge: Infinity, + * expiresAt: Infinity, + * isExpired: false, + * position: 1 + * }, + * ] + */ ``` > For _up-to-date_ documentation, always follow the [**README.md**](https://github.com/wellwelwel/lru.min?tab=readme-ov-file#readme) in the **GitHub** repository. diff --git a/benchmark/worker.cjs b/benchmark/worker.cjs index 39ada81..b056aae 100644 --- a/benchmark/worker.cjs +++ b/benchmark/worker.cjs @@ -20,7 +20,7 @@ const measurePerformance = (fn) => { }; }; -const times = 100; +const times = 10; const max = 100000; const brute = 1000000; diff --git a/benchmark/worker.mjs b/benchmark/worker.mjs index 1e53bc7..8ddd765 100644 --- a/benchmark/worker.mjs +++ b/benchmark/worker.mjs @@ -21,7 +21,7 @@ const measurePerformance = (fn) => { }; }; -const times = 100; +const times = 10; const max = 100000; const brute = 1000000; diff --git a/package.json b/package.json index e8566a6..f16ec58 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lru.min", "version": "1.1.1", - "description": "🔥 An extremely fast and efficient LRU cache for JavaScript with high compatibility (including Browsers) — 6.8KB.", + "description": "🔥 An extremely fast and efficient LRU cache for JavaScript with high compatibility (including Browsers) — 8.8KB.", "main": "./lib/index.js", "module": "./lib/index.mjs", "types": "./lib/index.d.ts", @@ -34,6 +34,7 @@ "build:browser": "tsx tools/browserfy.ts", "build:esm": "esbuild src/index.ts --outfile=lib/index.mjs --platform=node --target=node12 --format=esm", "build": "tsc && npm run build:esm && npm run build:browser", + "postbuild": "tsx tools/strip-comments.ts", "test:node": "poku --node -p", "test:bun": "poku --bun -p", "test:deno": "poku --deno -p", diff --git a/src/index.ts b/src/index.ts index 37f5ab5..93fb607 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,7 @@ -import { performance } from 'node:perf_hooks'; - export type CacheOptions = { /** Maximum number of items the cache can hold. */ max: number; + /** Maximum age (in ms) for items before they are considered stale. */ maxAge?: number; /** Function called when an item is evicted from the cache. */ onEviction?: (key: Key, value: Value) => unknown; @@ -17,7 +16,13 @@ export const createLRU = (options: CacheOptions) => { if (typeof maxAge === 'number' && maxAge <= 0) throw new TypeError('`maxAge` must be a positive number'); - const Age = typeof performance?.now === 'object' ? performance : Date; + const Age = (() => { + try { + return typeof performance.now === 'function' ? performance : Date; + } catch { + return Date; + } + })(); let size = 0; let head = 0; @@ -116,6 +121,35 @@ export const createLRU = (options: CacheOptions) => { return true; }; + const _debug = (key: Key) => { + const index = keyMap.get(key); + + if (index === undefined) return; + + const ageItem = ageList[index]; + const expiresAt = expList[index]; + const now = Age.now(); + const timeRemaining = expiresAt !== 0 ? expiresAt - now : 0; + const isExpired = expiresAt !== 0 && now > expiresAt; + + let current = tail; + let position = 0; + + while (current !== index && current !== 0) { + current = prev[current]; + position++; + } + + return { + key, + value: valList[index], + maxAge: ageItem, + expiresAt: timeRemaining > 0 ? timeRemaining : 0, + isExpired, + position, + }; + }; + return { /** Adds a key-value pair to the cache. Updates the value if the key already exists. */ set(key: Key, value: Value, options?: { maxAge?: number }): undefined { @@ -344,6 +378,39 @@ export const createLRU = (options: CacheOptions) => { max = newMax; }, + *debug(key?: Key): Generator< + | { + /** Item key. */ + key: Key; + /** Item value. */ + value: Value | undefined; + /** Time in milliseconds. */ + maxAge: number; + /** Time in milliseconds. */ + expiresAt: number; + /** When it's `true`, the next interaction with the key will evict it. */ + isExpired: boolean; + /** From the most recent (`0`) to the oldest (`max`). */ + position: number; + } + | undefined + > { + if (key !== undefined) { + const result = _debug(key); + + if (result) yield result; + + return; + } + + let current = tail; + + for (let i = 0; i < size; i++) { + yield _debug(keyList[current]!); + current = prev[current]; + } + }, + /** Returns the maximum number of items that can be stored in the cache. */ get max() { return max; diff --git a/t.ts b/t.ts deleted file mode 100644 index 7a24f2f..0000000 --- a/t.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createLRU } from './src/index.js'; - -const LRU = createLRU({ - max: 100, - maxAge: 1000, -}); - -LRU.set('A', 'Value'); - -setTimeout(() => { - console.log(LRU.get('A')); -}, 2000); diff --git a/tools/browserfy.ts b/tools/browserfy.ts index 0f615cf..252f002 100644 --- a/tools/browserfy.ts +++ b/tools/browserfy.ts @@ -4,9 +4,10 @@ import { transformAsync } from '@babel/core'; import { minify } from 'terser'; (async () => { - const contents = (await readFile('src/index.ts', 'utf8')) - .replace(/export const/gim, 'window.') - .replace(/import { performance } from 'node:perf_hooks';/gim, ''); + const contents = (await readFile('src/index.ts', 'utf8')).replace( + /export const/gim, + 'window.' + ); const result = await esbuild.build({ stdin: { diff --git a/tools/strip-comments.ts b/tools/strip-comments.ts new file mode 100644 index 0000000..e34342a --- /dev/null +++ b/tools/strip-comments.ts @@ -0,0 +1,19 @@ +import { promises as fs } from 'node:fs'; +import { listFiles } from 'poku'; + +const ensureNodeCompatibility = async (path: string) => { + const files = await listFiles(path, { + filter: /\.(|m)?(j)?s$/, + }); + + console.log('Ensuring no unnecessary comments for:', files); + + for (const file of files) { + const raw = await fs.readFile(file, 'utf8'); + const content = raw.replace(/(\/\*)((?:|[^1])+?)\//gim, ''); + + await fs.writeFile(file, content, { encoding: 'utf8' }); + } +}; + +ensureNodeCompatibility('./lib');