feat(cache): PG kv_cache ported from MP TTLCache, TMDB request and recognize caches

Migration 0042_cache adds UNLOGGED kv_cache. PgCache mirrors MP
runtime/cache.py (region, per-key TTL, TLRU maxsize eviction, @cached as
cachedCall); only the storage moves from memory to PG. TMDB request
responses (MP TMDb.request @cached) and recognize results (MP TmdbCache:
same key, {id:0} negative cache with EMPTY_RESULT_CACHE_TTL, type-conflict
checks, backfill after manual assignment) live in kv_cache, shared by all
processes. Settings cache.tmdbTtlHours / tmdbMaxSize / emptyResultTtlSec
map to META_CACHE_EXPIRE / CONF.tmdb / EMPTY_RESULT_CACHE_TTL. TMDB
details now also store posterPath / backdropPath.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 22:26:29 +08:00
1 parent 61f585a707
commit 946fca5e55
22 files changed
+743 -14

No files matched your search

+25
View File
@@ -110,6 +110,31 @@ describe("TmdbClient 缓存/限流/错误分类", () => {
expect(mock.calls.length).toBe(n1 * 2);
});
it("共享响应缓存:给了 sharedCache 就走它,key 不含凭证,空结果 30 分钟,其余按 ttlMs", async () => {
const store = new Map<string, { v: unknown; ttl: number }>();
const { mock, tmdb } = await setup({
cache: { ttlMs: 7 * 3600_000 },
sharedCache: {
get: async (key) => store.get(key)?.v ?? null,
set: async (key, v, ttl) => {
store.set(key, { v, ttl });
},
},
});
await tmdb.details("movie", 872585);
const n1 = mock.calls.length;
await tmdb.details("movie", 872585);
expect(mock.calls.length).toBe(n1);
await tmdb.search("不存在的片名xyz", { type: "movie" });
const keys = [...store.keys()];
expect(keys.some((k) => k.includes("test-tmdb-key"))).toBe(false);
const detailKey = keys.find((k) => k.startsWith("/movie/872585?"))!;
expect(store.get(detailKey)!.ttl).toBe(7 * 3600_000);
const emptyKey = keys.find((k) => k.startsWith("/search/movie?"))!;
expect(store.get(emptyKey)!.ttl).toBe(30 * 60_000);
expect(tmdb.cacheStats().size).toBe(0);
});
it("并发合并:同一请求并发只发一次", async () => {
const { mock, tmdb } = await setup();
await Promise.all(Array.from({ length: 5 }, () => tmdb.season(100088, 1)));