ProvenPin Install

ioredis cache-aside with TTL, tested with ioredis-mock (Vitest): working versions (tested 2026-10-15)

Cache-aside helper on ioredis with JSON values, TTLs, stampede-safe single flight and invalidation, tested offline with ioredis-mock in Vitest.

passed3 passed · 0 failed2026-10-15 11:54 UTC
Status
passed
Last passed
2026-10-15 11:54 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
ioredis@5.11.1@types/node@24.19.2vitest@5.0.4ioredis-mock@8.13.1
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("ioredis-cache-aside-mock") (install), or see them on GitHub.

package.json
{
  "name": "ioredis-cache-aside-mock",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {
    "ioredis": "5.11.1"
  },
  "devDependencies": {
    "@types/node": "24.19.2",
    "vitest": "5.0.4",
    "ioredis-mock": "8.13.1"
  }
}
src/cache.ts
import type { Redis } from "ioredis";

export function createCache(redis: Redis, prefix = "cache:") {
  const inflight = new Map<string, Promise<unknown>>();
  return {
    async getOrSet<T>(key: string, ttlSeconds: number, load: () => Promise<T>): Promise<T> {
      const k = prefix + key;
      const hit = await redis.get(k);
      if (hit !== null) return JSON.parse(hit) as T;
      // single flight: concurrent misses share one load
      if (!inflight.has(k)) {
        inflight.set(k, (async () => {
          try {
            const value = await load();
            await redis.set(k, JSON.stringify(value), "EX", ttlSeconds);
            return value;
          } finally { inflight.delete(k); }
        })());
      }
      return inflight.get(k) as Promise<T>;
    },
    invalidate: (key: string) => redis.del(prefix + key),
    ttl: (key: string) => redis.ttl(prefix + key),
  };
}
test/cache.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import RedisMock from "ioredis-mock";
import type { Redis } from "ioredis";
import { createCache } from "../src/cache";

let redis: Redis;
beforeEach(async () => { redis = new (RedisMock as any)() as Redis; await redis.flushall(); });

describe("cache-aside", () => {
  it("loads once then serves from redis", async () => {
    const cache = createCache(redis);
    const load = vi.fn(async () => ({ id: 1, name: "Ada" }));
    expect(await cache.getOrSet("user:1", 60, load)).toEqual({ id: 1, name: "Ada" });
    expect(await cache.getOrSet("user:1", 60, load)).toEqual({ id: 1, name: "Ada" });
    expect(load).toHaveBeenCalledTimes(1);
    expect(await cache.ttl("user:1")).toBeGreaterThan(0);
  });

  it("concurrent misses share one load", async () => {
    const cache = createCache(redis);
    const load = vi.fn(async () => { await new Promise((r) => setTimeout(r, 10)); return 42; });
    expect(await Promise.all([1, 2, 3].map(() => cache.getOrSet("n", 60, load)))).toEqual([42, 42, 42]);
    expect(load).toHaveBeenCalledTimes(1);
  });

  it("invalidate forces a reload; failed loads are not cached", async () => {
    const cache = createCache(redis);
    await cache.getOrSet("k", 60, async () => 1);
    await cache.invalidate("k");
    expect(await cache.getOrSet("k", 60, async () => 2)).toBe(2);
    await expect(cache.getOrSet("x", 60, async () => { throw new Error("db down"); })).rejects.toThrow("db down");
    expect(await redis.exists("cache:x")).toBe(0);
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.