Files
cloudflare__vinext/tests/cache-for-request.test.ts
JamesbbBriz 73f4ca8d8d feat: implement cacheForRequest() per-request factory cache (#646)
* feat: add cacheForRequest() — per-request factory cache

Implements the cacheForRequest API discussed in #623.

API: import { cacheForRequest } from 'vinext/cache'

- Factory function reference is the cache key (no string collision)
- Async factories cache the Promise; rejected Promises clear their entry
  so subsequent calls within the same request can retry
- Outside request scope: factory runs every time (no caching)
- Public barrel at src/cache.ts, separate from the next/cache shim
- Vite dev alias registered in index.ts
- 6 unit tests covering sync, async, isolation, nested scopes, reject+retry

* fix: address review feedback for cacheForRequest

- Switch test imports from vitest to vite-plus/test (repo standard)
- Use runWithUnifiedStateMutation in nested scope test (real code path)
- Add requestCache assertion to createRequestContext defaults test
- Fix JSDoc @param accuracy for async factory retry behavior
2026-03-28 09:01:03 +00:00

109 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi } from "vite-plus/test";
import {
runWithRequestContext,
runWithUnifiedStateMutation,
createRequestContext,
} from "../packages/vinext/src/shims/unified-request-context";
import { cacheForRequest } from "../packages/vinext/src/shims/cache-for-request";
describe("cacheForRequest", () => {
it("does not cache outside request scope", () => {
const factory = vi.fn(() => ({ id: Math.random() }));
const get = cacheForRequest(factory);
const a = get();
const b = get();
expect(a).not.toBe(b);
expect(factory).toHaveBeenCalledTimes(2);
});
it("caches within the same request", () => {
const factory = vi.fn(() => ({ id: Math.random() }));
const get = cacheForRequest(factory);
const ctx = createRequestContext();
void runWithRequestContext(ctx, () => {
const a = get();
const b = get();
expect(a).toBe(b);
expect(factory).toHaveBeenCalledTimes(1);
});
});
it("caches different factories separately", () => {
const factoryA = vi.fn(() => "a");
const factoryB = vi.fn(() => "b");
const getA = cacheForRequest(factoryA);
const getB = cacheForRequest(factoryB);
const ctx = createRequestContext();
void runWithRequestContext(ctx, () => {
expect(getA()).toBe("a");
expect(getB()).toBe("b");
expect(factoryA).toHaveBeenCalledTimes(1);
expect(factoryB).toHaveBeenCalledTimes(1);
});
});
it("isolates between different requests", () => {
let counter = 0;
const factory = vi.fn(() => ++counter);
const get = cacheForRequest(factory);
const ctx1 = createRequestContext();
const val1 = runWithRequestContext(ctx1, () => get());
const ctx2 = createRequestContext();
const val2 = runWithRequestContext(ctx2, () => get());
expect(val1).toBe(1);
expect(val2).toBe(2);
expect(factory).toHaveBeenCalledTimes(2);
});
it("shares cache across nested unified scopes", async () => {
const factory = vi.fn(() => "cached");
const get = cacheForRequest(factory);
const ctx = createRequestContext();
await runWithRequestContext(ctx, async () => {
const outer = get();
// Exercise the real nested scope path via runWithUnifiedStateMutation
const inner = await runWithUnifiedStateMutation(
(child) => {
child.dynamicUsageDetected = true;
},
() => get(),
);
expect(outer).toBe("cached");
expect(inner).toBe("cached");
expect(factory).toHaveBeenCalledTimes(1);
});
});
it("caches async Promise and clears on rejection", async () => {
let callCount = 0;
const factory = vi.fn(async () => {
callCount++;
if (callCount === 1) throw new Error("fail");
return "success";
});
const get = cacheForRequest(factory);
const ctx = createRequestContext();
await runWithRequestContext(ctx, async () => {
// First call: rejects
await expect(get()).rejects.toThrow("fail");
// Wait a tick for the .catch() to clear the cache
await new Promise((r) => setTimeout(r, 0));
// Second call: should retry (cache was cleared)
const result = await get();
expect(result).toBe("success");
expect(factory).toHaveBeenCalledTimes(2);
});
});
});