docs(architecture): 同步缓存基础设施 + classes 标杆迁移 + 缓存策略规则
- 004: 补全 shared/lib/cache 9 文件说明(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ shared/lib/redis-client + shared/lib/query-keys - 005: shared.lib.exports.functions 补 5 个节点(MemoryCacheStore / RedisCacheStore / CLIENT_INVALIDATION_MAP / getRedisClient / queryKeys) - known-issues.md 新增「二十四、缓存策略规则」章节,含 14 条规则表与涉及文件清单
This commit is contained in:
@@ -4366,12 +4366,16 @@ formatNumber(v: number | null | undefined, digits?: number): string
|
||||
// shared/lib/search-params.ts (re-export from utils.ts)
|
||||
getParam(params: SearchParams, key: string): string | undefined // = getSearchParam
|
||||
|
||||
// shared/lib/cache/ (服务端缓存基础设施,P1 重构新增)
|
||||
// shared/lib/cache/ (服务端缓存基础设施 + 客户端失效映射,P1 重构新增)
|
||||
// types.ts —— CacheFnOptions / CacheStore / InvalidationRule 接口定义(纯类型,无运行时依赖)
|
||||
// memory-store.ts —— MemoryCacheStore 实现(Map + LRU + tag 反查索引,单实例 dev/test 用)
|
||||
// redis-store.ts —— RedisCacheStore 实现(多实例共享,故障降级回退 producer)
|
||||
// store-factory.ts —— getCacheStore() 单例工厂:默认 MemoryCacheStore,CACHE_DRIVER=redis 时懒加载 RedisCacheStore
|
||||
// memory-store.ts / redis-store.ts —— CacheStore 两种实现
|
||||
// cache-fn.ts —— cacheFn 双层包装器:外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求缓存(按 tag 失效,TTL 可选)
|
||||
// invalidation-map.ts —— INVALIDATION_MAP 集中式失效映射表 + fillTemplate 模板填充
|
||||
// invalidation-map.ts —— INVALIDATION_MAP 集中式失效映射表 + fillTemplate 模板填充(含服务端 tags/queryKeys/paths 三段)
|
||||
// client-invalidation-map.ts —— CLIENT_INVALIDATION_MAP 客户端可见子集(仅 queryKeys 字段,避免客户端 bundle 拉入 revalidateTag 等 server-only 依赖)
|
||||
// invalidate.ts —— invalidateFor 三步编排函数(Server Action 写后调用)
|
||||
// index.ts —— barrel re-export:cacheFn / invalidateFor / INVALIDATION_MAP / fillTemplate / CLIENT_INVALIDATION_MAP + 类型
|
||||
getCacheStore(): Promise<CacheStore>
|
||||
cacheFn<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, options: CacheFnOptions): (...args: TArgs) => Promise<TResult>
|
||||
fillTemplate(template: string, params: Record<string, string>): string
|
||||
@@ -4379,6 +4383,17 @@ INVALIDATION_MAP // 集中式失效映射表常量
|
||||
invalidateFor(actionId: string, params?: Record<string, string>): Promise<void>
|
||||
// 三步编排:1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N
|
||||
// 未知 actionId 抛错 "[cache] Unknown actionId: <id>. Update INVALIDATION_MAP."
|
||||
CLIENT_INVALIDATION_MAP // 客户端可见子集:仅 queryKeys 字段,由 useActionMutation 在 onSuccess 中查询
|
||||
|
||||
// shared/lib/redis-client.ts (P1 重构新增:共享 Redis 客户端单例)
|
||||
// rate-limit + cache 共用,复用 env.UPSTASH_REDIS_REST_URL/TOKEN
|
||||
// 动态 import + webpackIgnore,包未安装/未配置返回 null(fail-open 降级)
|
||||
getRedisClient(): Promise<unknown | null>
|
||||
|
||||
// shared/lib/query-keys.ts (P1 重构新增:客户端 queryKey 工厂)
|
||||
// 命名约定:[module, resource, ...args]
|
||||
// 失效时使用前缀匹配:invalidateQueries({ queryKey: ["classes", "detail"] }) → 失效所有 ["classes", "detail", *]
|
||||
queryKeys // 工厂对象,含 classes.{all,lists,list,details,detail,students,schedule,stats,invitations}
|
||||
```
|
||||
|
||||
### 业务模块核心 Actions
|
||||
|
||||
@@ -1420,6 +1420,33 @@
|
||||
"api/notifications/stream"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MemoryCacheStore",
|
||||
"file": "lib/cache/memory-store.ts",
|
||||
"signature": "class MemoryCacheStore implements CacheStore { getOrSet<T>(key, producer, options): Promise<T>; invalidateTags(tags): Promise<void> }",
|
||||
"purpose": "CacheStore 内存实现:Map + LRU 淘汰(maxEntries 默认 500)+ tag 反查索引 Map<tag, Set<key>>,TTL 惰性删除。适用于单实例 dev/test",
|
||||
"deps": [
|
||||
"server-only",
|
||||
"./types"
|
||||
],
|
||||
"usedBy": [
|
||||
"shared/lib/cache/store-factory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RedisCacheStore",
|
||||
"file": "lib/cache/redis-store.ts",
|
||||
"signature": "class RedisCacheStore implements CacheStore { getOrSet<T>(key, producer, options): Promise<T>; invalidateTags(tags): Promise<void> }",
|
||||
"purpose": "CacheStore Redis 实现:key 格式 next-edu:cache:{key},value JSON.stringify,tag 反查索引用 Redis Set,TTL 通过 expire 原子设置。Redis 不可用时故障降级透传 producer",
|
||||
"deps": [
|
||||
"server-only",
|
||||
"@/shared/lib/redis-client",
|
||||
"./types"
|
||||
],
|
||||
"usedBy": [
|
||||
"shared/lib/cache/store-factory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getCacheStore",
|
||||
"file": "lib/cache/store-factory.ts",
|
||||
@@ -1462,7 +1489,7 @@
|
||||
"name": "INVALIDATION_MAP",
|
||||
"file": "lib/cache/invalidation-map.ts",
|
||||
"signature": "INVALIDATION_MAP: Record<string, InvalidationRule>",
|
||||
"purpose": "集中式失效映射表:每个 mutation actionId 声明其副作用 tags/queryKeys/paths",
|
||||
"purpose": "集中式失效映射表:每个 mutation actionId 声明其副作用 tags/queryKeys/paths(服务端三段)",
|
||||
"deps": [
|
||||
"./types"
|
||||
],
|
||||
@@ -1470,6 +1497,16 @@
|
||||
"shared/lib/cache/invalidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CLIENT_INVALIDATION_MAP",
|
||||
"file": "lib/cache/client-invalidation-map.ts",
|
||||
"signature": "CLIENT_INVALIDATION_MAP: Record<string, { queryKeys: readonly (readonly string[])[] }>",
|
||||
"purpose": "客户端可见的失效映射子集(仅 queryKeys 字段),从 invalidation-map 提取以避免客户端 bundle 拉入 revalidateTag 等 server-only 依赖。由 useActionMutation 在 onSuccess 中查询以自动 invalidateQueries",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"shared/hooks/use-action-mutation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "invalidateFor",
|
||||
"file": "lib/cache/invalidate.ts",
|
||||
@@ -1483,6 +1520,33 @@
|
||||
"usedBy": [
|
||||
"modules/* Server Actions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getRedisClient",
|
||||
"file": "lib/redis-client.ts",
|
||||
"signature": "getRedisClient(): Promise<unknown | null>",
|
||||
"purpose": "共享 Redis 客户端单例(rate-limit + cache 共用)。复用 env.UPSTASH_REDIS_REST_URL/TOKEN,动态 import + webpackIgnore。包未安装或 env 未配置时返回 null(fail-open 降级)",
|
||||
"deps": [
|
||||
"server-only",
|
||||
"@/env.mjs",
|
||||
"@upstash/redis (optional, webpackIgnore)"
|
||||
],
|
||||
"usedBy": [
|
||||
"shared/lib/cache/redis-store",
|
||||
"shared/lib/rate-limit/redis-limiter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "queryKeys",
|
||||
"file": "lib/query-keys.ts",
|
||||
"signature": "queryKeys: { classes: { all, lists, list(filters), details, detail(id), students(classId), schedule(classId), stats(classId), invitations(classId) } }",
|
||||
"purpose": "客户端 queryKey 工厂对象。命名约定 [module, resource, ...args],失效时使用前缀匹配 invalidateQueries({ queryKey: ['classes', 'detail'] }) 失效所有 ['classes', 'detail', *]。后续模块按此模式扩展",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"modules/classes/components/*",
|
||||
"shared/hooks/use-action-query",
|
||||
"shared/hooks/use-action-mutation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"hooks": [
|
||||
|
||||
@@ -1236,3 +1236,38 @@ if (announcement.type === "school") {
|
||||
|
||||
**涉及文件**:`src/shared/lib/cache/invalidate.ts`、`src/shared/lib/cache/invalidate.test.ts`、`src/shared/lib/cache/cache-fn.ts`、`src/shared/lib/cache/cache-fn.test.ts`
|
||||
|
||||
---
|
||||
|
||||
## 二十四、缓存策略规则(2026-07-05 缓存策略重构)
|
||||
|
||||
> 标杆模块:`classes`。后续模块按此模式扩展。底层基础设施见 `src/shared/lib/cache/`。
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| data-access 查询函数包装 | `export const fn = cacheFn(fnRaw, { tags, ttl, keyParts })` | `cache(fn)` 或不包装 |
|
||||
| 双导出 raw 版本 | `export const fnRaw = ...; export const fn = cacheFn(fnRaw, ...)` | 仅导出缓存版本(单测无法 mock) |
|
||||
| Server Action 写后失效 | `await invalidateFor("module.action", { id })` | `revalidatePath("/path")` 直接调用 |
|
||||
| 未知 actionId | 抛错(必须先登记 INVALIDATION_MAP) | 静默忽略 |
|
||||
| 客户端 useQuery queryKey | `queryKeys.classes.detail(id)` 工厂调用 | 手写字符串数组 `["classes", "detail", id]` |
|
||||
| useActionQuery 传 queryKey | `useActionQuery(action, { queryKey: queryKeys.classes.detail(id) })` | 不传 queryKey(旧模式,不跨页共享) |
|
||||
| useActionMutation 自动 invalidate | `useActionMutation({ mutationFn, actionId: "classes.update", params: { id } })` | onSuccess 中手动 invalidateQueries |
|
||||
| Tag 命名 | `classes:detail:{id}` 模块:资源:ID | `class_detail` 或 `ClassesDetail` |
|
||||
| TTL 选择 | 列表 300s / 学生 60s / 课表 600s / 统计 60s | 全局统一 TTL |
|
||||
| CLIENT_INVALIDATION_MAP 与 INVALIDATION_MAP 同步 | 服务端 invalidation-map.ts 增项后,同步在 client-invalidation-map.ts 增 queryKeys 子项 | 只改服务端,客户端 useActionMutation 自动 invalidate 失效 |
|
||||
| client-invalidation-map 仅含 queryKeys | `CLIENT_INVALIDATION_MAP["x.y"] = { queryKeys: [["x", "y"]] }` | 在客户端文件中引入 tags/paths(拉入 server-only) |
|
||||
| raw 函数命名 | `getXxxRaw`(内部实现)+ `getXxx`(cacheFn 包装后的对外 API) | `getXxx` 与 `getXxxCached` 命名混用 |
|
||||
| cacheFn options.tags 必填 | `{ tags: ["classes:detail:{id}"], ttl: 60, keyParts: [id] }` | `{ ttl: 60 }` 缺 tags(无法失效) |
|
||||
| invalidateFor 调用位置 | mutation Action 成功分支末尾 `await invalidateFor(...)` | 在 catch 分支调用(失败也失效缓存) |
|
||||
| queryKey 工厂扩展 | 在 `query-keys.ts` 按模块命名空间扩展 `xxx: { all, lists, list, detail, ... }` | 在组件内就地定义 queryKey 字面量 |
|
||||
|
||||
### 缓存策略重构涉及文件清单
|
||||
|
||||
- `src/shared/lib/cache/{types,memory-store,redis-store,store-factory,cache-fn,invalidation-map,client-invalidation-map,invalidate,index}.ts` — 缓存基础设施(9 个文件)
|
||||
- `src/shared/lib/redis-client.ts` — 共享 Redis 客户端单例
|
||||
- `src/shared/lib/query-keys.ts` — 客户端 queryKey 工厂
|
||||
- `src/shared/hooks/use-action-query.ts` — 新增 queryKey 入参走 QueryClient
|
||||
- `src/shared/hooks/use-action-mutation.ts` — 新增 actionId 自动 invalidate
|
||||
- `src/modules/classes/data-access-*.ts` — 5+ 文件全部 cacheFn 包装 + 双导出 raw
|
||||
- `src/modules/classes/actions-*.ts` — 5 文件全部 invalidateFor 替换 revalidatePath
|
||||
- `src/modules/classes/components/*` — useActionQuery/useActionMutation 适配新版 API
|
||||
|
||||
|
||||
Reference in New Issue
Block a user