import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import { createRedisCache, withCacheInvalidation } from '../src/cache/redis-cache.mjs'; class FakeRedisClient extends EventEmitter { constructor({ connectError = null } = {}) { super(); this.connectError = connectError; this.isOpen = false; this.isReady = false; this.values = new Map(); } async connect() { if (this.connectError) throw this.connectError; this.isOpen = true; this.isReady = true; this.emit('ready'); } async get(key) { return this.values.has(key) ? this.values.get(key) : null; } async set(key, value, options = {}) { if (options.NX && this.values.has(key)) return null; this.values.set(key, value); return 'OK'; } async incr(key) { const next = Number(this.values.get(key) || 0) + 1; this.values.set(key, String(next)); return next; } async quit() { this.isReady = false; this.isOpen = false; } destroy() { this.isReady = false; this.isOpen = false; } } const silentLogger = { warn() {} }; { const client = new FakeRedisClient(); const cache = await createRedisCache({ env: { REDIS_URL: 'redis://test', REDIS_CACHE_PREFIX: 'test', REDIS_CACHE_TTL_SECONDS: '30' }, logger: silentLogger, clientFactory: () => client }); let loads = 0; const load = async () => ({ version: ++loads }); assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 }); assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 }); assert.equal(loads, 1, '相同缓存键应只读取一次数据源'); await cache.invalidate('public'); assert.deepEqual(await cache.remember('public', 'home', load), { version: 2 }); assert.equal(loads, 2, '命名空间失效后应重新读取数据源'); let resultLoads = 0; const loadResults = async () => ({ version: ++resultLoads }); assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 1 }); const database = withCacheInvalidation({ client: 'test', async read() { return {}; }, async save() { return 'saved'; }, async close() {} }, cache, method => method === 'save' ? ['public', 'results'] : ['public']); assert.equal(await database.save(), 'saved'); assert.deepEqual(await cache.remember('public', 'home', load), { version: 3 }); assert.equal(loads, 3, '数据库写入后应让公开缓存失效'); assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 2 }); assert.equal(resultLoads, 2, '成绩相关写入后应让成绩缓存失效'); await cache.invalidate('results'); assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 3 }); assert.equal(resultLoads, 3, '后台手动刷新后应重新生成成绩缓存'); client.isReady = false; let fallbackLoads = 0; assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1); assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1); assert.equal(fallbackLoads, 1, 'Redis 运行中断开后,相同热点读取应由本机缓存合并'); client.isReady = true; await cache.close(); } { const cache = await createRedisCache({ env: {}, logger: silentLogger }); let loads = 0; assert.equal(await cache.remember('public', 'home', async () => ++loads), 1); assert.equal(await cache.remember('public', 'home', async () => ++loads), 1); assert.equal(loads, 1, '未配置 Redis 时应使用有界本机缓存,避免重复回源'); await cache.invalidate('public'); assert.equal(await cache.remember('public', 'home', async () => ++loads), 2); assert.equal(loads, 2, '本机缓存应在数据库写入后立即失效'); assert.equal(cache.status, 'disabled'); let releaseOld; const oldLoad = cache.remember('public', 'race', () => new Promise(resolve => { releaseOld = () => resolve('old'); })); await cache.invalidate('public'); const newLoad = cache.remember('public', 'race', async () => 'new'); releaseOld(); assert.equal(await oldLoad, 'old'); assert.equal(await newLoad, 'new', '失效后不得等待失效前仍在运行的加载'); assert.equal(await cache.remember('public', 'race', async () => 'unexpected'), 'new', '旧加载完成后不得覆盖新缓存'); } { const client = new FakeRedisClient({ connectError: new Error('connection refused') }); const cache = await createRedisCache({ env: { REDIS_URL: 'redis://unavailable' }, logger: silentLogger, clientFactory: () => client }); assert.equal(cache.status, 'unavailable'); assert.equal(await cache.remember('public', 'home', async () => 'database'), 'database'); assert.equal(await cache.remember('public', 'home', async () => 'unexpected'), 'database', 'Redis 故障时本机缓存应继续承接重复读取'); } console.log('Redis 缓存测试通过');