From ffa12b05cbdfaa4e309141cf30e92a3caa1cdca6 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:46:59 +0300 Subject: [PATCH] Replace lru-cache dep with light util --- app/components/Avatar.tsx | 2 +- app/modules/cache/index.test.ts | 67 +++++++++++++++++++++++++++++++++ app/modules/cache/index.ts | 48 +++++++++++++++++++++++ app/utils/cache.server.ts | 2 +- package.json | 1 - pnpm-lock.yaml | 3 -- 6 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 app/modules/cache/index.test.ts create mode 100644 app/modules/cache/index.ts diff --git a/app/components/Avatar.tsx b/app/components/Avatar.tsx index 496099769..0dbfcd3df 100644 --- a/app/components/Avatar.tsx +++ b/app/components/Avatar.tsx @@ -1,8 +1,8 @@ import clsx from "clsx"; -import { LRUCache } from "lru-cache"; import * as React from "react"; import type { Tables } from "~/db/tables"; import { useHydrated } from "~/hooks/useHydrated"; +import { LRUCache } from "~/modules/cache"; import { BLANK_IMAGE_URL, discordAvatarUrl } from "~/utils/urls"; import styles from "./Avatar.module.css"; diff --git a/app/modules/cache/index.test.ts b/app/modules/cache/index.test.ts new file mode 100644 index 000000000..191eec42e --- /dev/null +++ b/app/modules/cache/index.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { LRUCache } from "./index"; + +describe("LRUCache", () => { + it("stores and retrieves values", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + + expect(cache.get("a")).toBe(1); + expect(cache.has("a")).toBe(true); + }); + + it("returns undefined for missing keys", () => { + const cache = new LRUCache({ max: 2 }); + + expect(cache.get("missing")).toBeUndefined(); + expect(cache.has("missing")).toBe(false); + }); + + it("evicts the least recently used entry once over capacity", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + cache.set("b", 2); + cache.set("c", 3); + + expect(cache.has("a")).toBe(false); + expect(cache.get("b")).toBe(2); + expect(cache.get("c")).toBe(3); + }); + + it("counts reads as recent use", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + cache.set("b", 2); + cache.get("a"); + cache.set("c", 3); + + expect(cache.has("a")).toBe(true); + expect(cache.has("b")).toBe(false); + }); + + it("updates value without growing past capacity on re-set", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + cache.set("a", 2); + + expect(cache.get("a")).toBe(2); + }); + + it("deletes a single entry", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + cache.delete("a"); + + expect(cache.has("a")).toBe(false); + }); + + it("clears all entries", () => { + const cache = new LRUCache({ max: 2 }); + cache.set("a", 1); + cache.set("b", 2); + cache.clear(); + + expect(cache.has("a")).toBe(false); + expect(cache.has("b")).toBe(false); + }); +}); diff --git a/app/modules/cache/index.ts b/app/modules/cache/index.ts new file mode 100644 index 000000000..85a6a0524 --- /dev/null +++ b/app/modules/cache/index.ts @@ -0,0 +1,48 @@ +/** + * A lightweight least-recently-used cache backed by a `Map`. Once `max` entries + * are stored, inserting a new key evicts the least recently used one. Reading a + * key via `get` marks it as most recently used. + */ +export class LRUCache { + private readonly max: number; + private readonly map = new Map(); + + constructor({ max }: { max: number }) { + this.max = max; + } + + get(key: K): V | undefined { + if (!this.map.has(key)) return undefined; + + const value = this.map.get(key) as V; + this.map.delete(key); + this.map.set(key, value); + + return value; + } + + has(key: K): boolean { + return this.map.has(key); + } + + set(key: K, value: V): void { + if (this.map.has(key)) { + this.map.delete(key); + } + + this.map.set(key, value); + + if (this.map.size > this.max) { + const oldest = this.map.keys().next().value as K; + this.map.delete(oldest); + } + } + + delete(key: K): void { + this.map.delete(key); + } + + clear(): void { + this.map.clear(); + } +} diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index edd777ea0..bc35ca66c 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -1,5 +1,5 @@ import type { CacheEntry } from "@epic-web/cachified"; -import { LRUCache } from "lru-cache"; +import { LRUCache } from "~/modules/cache"; declare global { // This preserves the LRU cache during development diff --git a/package.json b/package.json index 208fd85d5..ce3e92797 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "isbot": "5.1.40", "jsoncrush": "1.1.8", "kysely": "0.29.0", - "lru-cache": "11.5.1", "lucide-react": "1.17.0", "markdown-to-jsx": "9.8.1", "nanoid": "5.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7b0f361c..91c78285e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,9 +102,6 @@ importers: kysely: specifier: 0.29.0 version: 0.29.0(patch_hash=6f395b25414c1ef852485fa3a03d2d521816064697d8c4bff337e2b24d19daa6) - lru-cache: - specifier: 11.5.1 - version: 11.5.1 lucide-react: specifier: 1.17.0 version: 1.17.0(react@19.2.7)