From 63b35ba1115e3d4886a2d2caf8dccd1ef1cb562e Mon Sep 17 00:00:00 2001 From: MAC Date: Fri, 26 Jun 2026 05:32:29 +0100 Subject: [PATCH] fix(api): add response caching to /insurance/:slab [BUG-106] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /insurance/:slab was wrapped by neither cacheMiddleware nor withDbCacheFallback — every single request, concurrent or not, ran two sequential un-batched Supabase queries (market_stats then insurance_history) with zero protection. Its sibling /open-interest/:slab (same query shape, same data source) already has cacheMiddleware(15). Applied the identical cacheMiddleware(15) to /insurance/:slab, matching the established pattern for this class of route. The existing test file didn't call clearCache() between tests, which the new caching behavior exposed: several tests reusing the same slab address across different mock setups started getting cached responses from earlier tests instead of hitting their own mocks. Added clearCache() to beforeEach, matching open-interest.test.ts's existing convention. Added a regression test proving a second request for the same slab is served from cache (X-Cache: HIT, no additional Supabase calls). Verified it fails against the pre-fix code (no caching, X-Cache header absent) and passes against the fix. Co-authored-by: Claude Sonnet 4.6 --- src/routes/insurance.ts | 3 ++- tests/routes/insurance.test.ts | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/routes/insurance.ts b/src/routes/insurance.ts index e41eb16..30c83cf 100644 --- a/src/routes/insurance.ts +++ b/src/routes/insurance.ts @@ -8,6 +8,7 @@ */ import { Hono } from "hono"; import { validateSlab } from "../middleware/validateSlab.js"; +import { cacheMiddleware } from "../middleware/cache.js"; import { getSupabase, createLogger, truncateErrorMessage } from "@percolator/shared"; const logger = createLogger("api:insurance"); @@ -31,7 +32,7 @@ export function insuranceRoutes(): Hono { * ] * } */ - app.get("/insurance/:slab", validateSlab, async (c) => { + app.get("/insurance/:slab", cacheMiddleware(15), validateSlab, async (c) => { const slab = c.req.param("slab"); try { diff --git a/tests/routes/insurance.test.ts b/tests/routes/insurance.test.ts index 1e3d7f7..46225d8 100644 --- a/tests/routes/insurance.test.ts +++ b/tests/routes/insurance.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { insuranceRoutes } from "../../src/routes/insurance.js"; +import { clearCache } from "../../src/middleware/cache.js"; // Mock @percolator/shared vi.mock("@percolator/shared", () => ({ @@ -29,6 +30,7 @@ describe("insurance routes", () => { beforeEach(() => { vi.clearAllMocks(); + clearCache(); mockSupabase = { from: vi.fn(() => mockSupabase), @@ -98,6 +100,50 @@ describe("insurance routes", () => { expect(data.history).toHaveLength(2); }); + it("caches the response so a second request for the same slab does not re-query Supabase (BUG-106)", async () => { + const mockStats = { + insurance_balance: "1000000000", + insurance_fee_revenue: "50000000", + total_open_interest: "5000000000", + }; + let fromCallCount = 0; + mockSupabase.from.mockImplementation((table: string) => { + fromCallCount++; + if (table === "market_stats") { + return { + select: vi.fn(() => ({ + eq: vi.fn(() => ({ + single: vi.fn().mockResolvedValue({ data: mockStats, error: null }), + })), + })), + }; + } else if (table === "insurance_history") { + return { + select: vi.fn(() => ({ + eq: vi.fn(() => ({ + order: vi.fn(() => ({ + limit: vi.fn().mockResolvedValue({ data: [], error: null }), + })), + })), + })), + }; + } + return mockSupabase; + }); + + const app = insuranceRoutes(); + const res1 = await app.request("/insurance/5gX6nn6Jhh3Sxsb6FMvbVGdFspKT2vtdXxkWi2zwXmHp"); + expect(res1.status).toBe(200); + const callsAfterFirst = fromCallCount; + expect(callsAfterFirst).toBeGreaterThan(0); + + const res2 = await app.request("/insurance/5gX6nn6Jhh3Sxsb6FMvbVGdFspKT2vtdXxkWi2zwXmHp"); + expect(res2.status).toBe(200); + expect(res2.headers.get("X-Cache")).toBe("HIT"); + // No new Supabase calls — served entirely from cache. + expect(fromCallCount).toBe(callsAfterFirst); + }); + it("should return 404 when market not found", async () => { mockSupabase.from.mockImplementation((table: string) => { if (table === "market_stats") {