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
npm install --no-audit --no-fund --ignore-scripts=false npm test
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.
{
"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"
}
}
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),
};
}
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.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
Hono + Valibot request validation (Vitest)
Validate JSON bodies and query strings in Hono with Valibot schemas via @hono/valibot-validator, with custom error responses, tested with Vitest.
ky HTTP client with hooks and retries, tested with MSW + Vitest
A ky 2 API client (prefix option, state-object hooks) with auth hooks, retry on 5xx and typed JSON, tested offline with MSW's Node server in Vitest.
All ioredis recipesAll ioredis-mock recipesAll vitest recipes