feat(cache): 新增 INVALIDATION_MAP 集中式失效映射表(classes 模块)

This commit is contained in:
SpecialX
2026-07-05 17:52:41 +08:00
parent 13409e55f1
commit 6baa60b2a1
2 changed files with 117 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest"
import { INVALIDATION_MAP, fillTemplate } from "./invalidation-map"
describe("INVALIDATION_MAP", () => {
it("classes.create 包含 tags + queryKeys + paths", () => {
const rule = INVALIDATION_MAP["classes.create"]
expect(rule.tags).toContain("classes")
expect(rule.tags).toContain("classes:list")
expect(rule.queryKeys).toContainEqual(["classes", "list"])
expect(rule.paths).toContain("/teacher/classes/my")
})
it("classes.update 支持 {id} 占位符", () => {
const rule = INVALIDATION_MAP["classes.update"]
expect(rule.tags).toContain("classes:detail:{id}")
})
})
describe("fillTemplate", () => {
it("填充 {id} 占位符", () => {
expect(fillTemplate("classes:detail:{id}", { id: "cls_123" })).toBe(
"classes:detail:cls_123",
)
})
it("无占位符时原样返回", () => {
expect(fillTemplate("classes:list", {})).toBe("classes:list")
})
it("多占位符同时填充", () => {
expect(
fillTemplate("{module}:{resource}:{id}", {
module: "classes",
resource: "students",
id: "cls_123",
}),
).toBe("classes:students:cls_123")
})
it("缺少参数时保留占位符(不抛错)", () => {
expect(fillTemplate("classes:detail:{id}", {})).toBe("classes:detail:{id}")
})
})

View File

@@ -0,0 +1,74 @@
import type { InvalidationRule } from "./types"
/**
* 模板占位符填充。
*
* @param template 含 {key} 占位符的字符串
* @param params 参数字典
* @returns 填充后的字符串;缺失参数时保留原占位符
*/
export function fillTemplate(
template: string,
params: Record<string, string>,
): string {
return template.replace(/\{(\w+)\}/g, (match, key: string) => {
return params[key] ?? match
})
}
/**
* 集中式失效映射表。
*
* 每个 mutation actionId 声明其副作用:
* - tags服务端 cacheStore + Next.js revalidateTag 失效的标签
* - queryKeys客户端 TanStack Query 失效的 queryKey 前缀
* - pathsNext.js revalidatePath 失效的路径
*
* 新增写操作必须在此登记,否则 invalidateFor 会抛错。
*/
export const INVALIDATION_MAP = {
// ===== classes 模块 =====
"classes.create": {
tags: ["classes", "classes:list"],
queryKeys: [["classes", "list"]],
paths: ["/teacher/classes/my", "/admin/classes"],
},
"classes.update": {
tags: ["classes", "classes:detail", "classes:detail:{id}"],
queryKeys: [["classes", "detail"], ["classes", "list"]],
// paths 不含动态段revalidatePath("/teacher/classes/my") 已能覆盖详情页([id] 路由)
paths: ["/teacher/classes/my", "/admin/classes"],
},
"classes.delete": {
tags: ["classes", "classes:list", "classes:detail:{id}"],
queryKeys: [["classes", "list"], ["classes", "detail"]],
paths: ["/teacher/classes/my", "/admin/classes"],
},
"classes.students.update": {
tags: ["classes:students:{classId}"],
queryKeys: [["classes", "students"]],
paths: ["/teacher/classes/my"],
},
"classes.schedule.update": {
tags: ["classes:schedule:{classId}"],
queryKeys: [["classes", "schedule"]],
paths: ["/teacher/classes/schedule"],
},
"classes.grade.update": {
tags: ["classes"],
queryKeys: [["classes", "list"]],
paths: [],
},
"classes.invitation.create": {
tags: ["classes:invitations:{classId}"],
queryKeys: [["classes", "invitations"]],
paths: ["/teacher/classes/my/{classId}"],
},
"classes.invitation.revoke": {
tags: ["classes:invitations:{classId}"],
queryKeys: [["classes", "invitations"]],
paths: ["/teacher/classes/my/{classId}"],
},
} as const satisfies Record<string, InvalidationRule>
export type InvalidationActionId = keyof typeof INVALIDATION_MAP