feat(cache): 通用缓存按区统计与清空接口
GET /cache/regions 列出 kv_cache 各区的未过期条数、已过期条数、占用字节、最近写入;
POST /cache/regions/{region}/clear 清空一个区(admin)。contracts 加区名中文映射
cacheRegionLabel,web 加对应 hooks。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
ad3bf8926b
commit
b7460d5d0a
5 files changed
+190
-6
No files matched your search
+61
@@ -0,0 +1,61 @@
|
||||
import { PgCache } from "@umt/db";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createCacheModule } from "../../src/modules/cache/index.ts";
|
||||
import { bearer, createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts";
|
||||
import { DEV_KEY, json } from "../helpers.ts";
|
||||
|
||||
// 设置 → 缓存:通用缓存(kv_cache)按区统计与单区清空(GET /cache/regions、POST /cache/regions/{region}/clear)
|
||||
|
||||
const hasPg = await pgAvailable();
|
||||
|
||||
describe.skipIf(!hasPg)("通用缓存按区管理(真实 PG)", () => {
|
||||
let h: Harness;
|
||||
let admin: ReturnType<typeof bearer>;
|
||||
|
||||
beforeAll(async () => {
|
||||
h = await createHarness({ extraModules: [createCacheModule()] });
|
||||
admin = bearer(h.app, DEV_KEY);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await h?.close();
|
||||
});
|
||||
|
||||
it("按区列出未过期条数、已过期条数、占用字节、最近写入;清空一个区不影响其它区", async () => {
|
||||
const cache = new PgCache(h.db);
|
||||
await cache.set("douban.subject", "1", { title: "a" });
|
||||
await cache.set("douban.subject", "2", { title: "b" });
|
||||
await cache.set("site.detail", "x", { body: "y".repeat(200) });
|
||||
// 写入后立刻过期的一条(ttl 1 秒,等它过期)
|
||||
await cache.set("site.detail", "old", { v: 1 }, { ttl: 1 });
|
||||
await new Promise((r) => setTimeout(r, 1100));
|
||||
|
||||
const res = await admin("GET", "/cache/regions");
|
||||
expect(res.status).toBe(200);
|
||||
const items = (await json(res)).data.items as {
|
||||
region: string;
|
||||
entries: number;
|
||||
expired: number;
|
||||
bytes: number;
|
||||
lastWriteAt: string | null;
|
||||
}[];
|
||||
const douban = items.find((x) => x.region === "douban.subject")!;
|
||||
const site = items.find((x) => x.region === "site.detail")!;
|
||||
expect(douban).toMatchObject({ entries: 2, expired: 0 });
|
||||
expect(douban.bytes).toBeGreaterThan(0);
|
||||
expect(Date.parse(douban.lastWriteAt!)).toBeGreaterThan(Date.now() - 60_000);
|
||||
expect(site).toMatchObject({ entries: 1, expired: 1 });
|
||||
expect(site.bytes).toBeGreaterThan(200);
|
||||
|
||||
const cleared = await json(await admin("POST", "/cache/regions/site.detail/clear"));
|
||||
expect(cleared.data.deleted).toBe(2);
|
||||
const after = (await json(await admin("GET", "/cache/regions"))).data.items as { region: string }[];
|
||||
expect(after.map((x) => x.region)).toContain("douban.subject");
|
||||
expect(after.map((x) => x.region)).not.toContain("site.detail");
|
||||
expect(await cache.count("douban.subject")).toBe(2);
|
||||
});
|
||||
|
||||
it("区名不合法时拒绝(422),不执行删除", async () => {
|
||||
const res = await admin("POST", "/cache/regions/BAD%20NAME/clear");
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user