Skip to content

"Simple" Cache

cache.ts

ts
interface CacheEntry<T> {
  value: T;
  expiresAt: number;
}

export default class SimpleCache<T> {
  private cache = new Map<string, CacheEntry<T>>();

  constructor(private ttl: number) {}

  get(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return null;
    }

    return entry.value;
  }

  getAll(): Array<T> {
    const now = Date.now();
    const values: Array<T> = [];

    for (const [key, entry] of this.cache.entries()) {
      if (now > entry.expiresAt) {
        this.cache.delete(key);
      } else {
        values.push(entry.value);
      }
    }

    return values;
  }

  set(key: string, value: T) {
    const expiresAt = Date.now() + this.ttl;
    this.cache.set(key, {
      value,
      expiresAt
    });
  }

  has(key: string): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;

    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return false;
    }

    return true;
  }

  expire(key: string) {
    const entry = this.cache.get(key);
    if (!entry) return;

    this.cache.set(key, {
      value: entry.value,
      expiresAt: Date.now()
    });
  }

  purge() {
    const now = Date.now();

    for (const [key, entry] of this.cache.entries()) {
      if (now > entry.expiresAt) {
        this.cache.delete(key);
      }
    }
  }

  clear() {
    this.cache.clear();
  }

  keys(): string[] {
    return [...this.cache.keys()];
  }

  size(): number {
    return this.cache.size;
  }
}

cache.test.ts

ts
interface Post {
  title: string;
  subTitle?: string;
  releaseDate: string;
  categories: string[];
}

describe("SimpleCache", () => {
  let simpleCache: SimpleCache<Post>;

  beforeAll(() => {
    simpleCache = new SimpleCache<Post>(300_000); // 5 Minutes
  });

  afterAll(() => {
    simpleCache.clear();
  });

  it("should has one post", () => {
    const wtfIsReactivityPost: Post = {
      title: "WTF Is Reactivity !?",
      subTitle: "Reactivity Models Explained",
      releaseDate: "2024-12-18",
      categories: ["programming", "javascript"]
    };

    simpleCache.set("wtf-is-reactivity", wtfIsReactivityPost);
    expect(simpleCache.has("wtf-is-reactivity")).toBe(true);
  });

  it("should get one post", () => {
    const tupleContextPatternPost: Post = {
      title: "This Is Tuple Context Pattern",
      releaseDate: "2025-04-05",
      categories: ["react", "preact", "javascript", "typescript"]
    };

    simpleCache.set("tuple-context-pattern", tupleContextPatternPost);
    expect(simpleCache.get("tuple-context-pattern")).toEqual(tupleContextPatternPost);
  });

  it("should get many posts", () => {
    expect(simpleCache.getAll()).toHaveLength(simpleCache.size());
  });

  it("should expire and purge one post", async () => {
    const nodeCleanArchPost: Post = {
      title: "How To Build An API With Node, Without Additional Framework",
      releaseDate: "2025-07-25",
      categories: ["programming", "node", "sql", "javascript"]
    };

    simpleCache.set("node-clean-arch", nodeCleanArchPost);
    expect(simpleCache.keys().includes("node-clean-arch")).toBe(true);

    simpleCache.expire("node-clean-arch");
    expect(simpleCache.keys().includes("node-clean-arch")).toBe(true);

    await vi.waitFor(() => simpleCache.purge());
    expect(simpleCache.keys().includes("node-clean-arch")).toBe(false);
  });
});

Released under the MIT License.