fix: preserve cachedAt metadata on cache hits
This commit is contained in:
parent
fe938b2706
commit
b87ed761ed
65
apps/backend/src/modules/cache/cache.service.spec.ts
vendored
Normal file
65
apps/backend/src/modules/cache/cache.service.spec.ts
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
describe('CacheService', () => {
|
||||
const configService = {
|
||||
get: vi.fn((_key: string, fallback?: unknown) => fallback),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const createCache = () => ({
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
});
|
||||
|
||||
it('stores data with cachedAt metadata on cache miss', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-06-25T10:00:00.000Z'));
|
||||
const cache = createCache();
|
||||
cache.get.mockResolvedValue(undefined);
|
||||
const service = new CacheService(cache as never, configService);
|
||||
|
||||
try {
|
||||
const result = await service.getOrFetch('prefix', ['a'], async () => ({ value: 1 }), 'ttlKey');
|
||||
|
||||
expect(result).toEqual({
|
||||
data: { value: 1 },
|
||||
fromCache: false,
|
||||
cachedAt: '2026-06-25T10:00:00.000Z',
|
||||
});
|
||||
expect(cache.set).toHaveBeenCalledWith(
|
||||
'prefix:a',
|
||||
{ data: { value: 1 }, cachedAt: '2026-06-25T10:00:00.000Z' },
|
||||
900,
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns cachedAt metadata on cache hit', async () => {
|
||||
const cache = createCache();
|
||||
cache.get.mockResolvedValue({
|
||||
data: { value: 1 },
|
||||
cachedAt: '2026-06-25T10:00:00.000Z',
|
||||
});
|
||||
const service = new CacheService(cache as never, configService);
|
||||
|
||||
const result = await service.getOrFetch('prefix', ['a'], async () => ({ value: 2 }), 'ttlKey');
|
||||
|
||||
expect(result).toEqual({
|
||||
data: { value: 1 },
|
||||
fromCache: true,
|
||||
cachedAt: '2026-06-25T10:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('supports legacy raw cache values during rollout', async () => {
|
||||
const cache = createCache();
|
||||
cache.get.mockResolvedValue({ value: 1 });
|
||||
const service = new CacheService(cache as never, configService);
|
||||
|
||||
const result = await service.getOrFetch('prefix', ['a'], async () => ({ value: 2 }), 'ttlKey');
|
||||
|
||||
expect(result).toEqual({ data: { value: 1 }, fromCache: true, cachedAt: null });
|
||||
});
|
||||
});
|
||||
28
apps/backend/src/modules/cache/cache.service.ts
vendored
28
apps/backend/src/modules/cache/cache.service.ts
vendored
@ -3,6 +3,11 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
type CacheEntry<T> = {
|
||||
data: T;
|
||||
cachedAt: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
constructor(
|
||||
@ -18,6 +23,16 @@ export class CacheService {
|
||||
await this.cacheManager.set(key, value, ttl);
|
||||
}
|
||||
|
||||
private isCacheEntry<T>(value: unknown): value is CacheEntry<T> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'data' in value &&
|
||||
'cachedAt' in value &&
|
||||
typeof (value as { cachedAt?: unknown }).cachedAt === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
private buildKey(...parts: string[]): string {
|
||||
return parts.join(':');
|
||||
}
|
||||
@ -31,14 +46,19 @@ export class CacheService {
|
||||
const key = this.buildKey(keyPrefix, ...keyParts);
|
||||
const ttl = this.configService.get<number>(`app.cache.${ttlConfigKey}`, 900);
|
||||
|
||||
const cached = await this.get<T>(key);
|
||||
const cached = await this.get<CacheEntry<T> | T>(key);
|
||||
if (cached !== undefined) {
|
||||
return { data: cached, fromCache: true, cachedAt: null };
|
||||
if (this.isCacheEntry<T>(cached)) {
|
||||
return { data: cached.data, fromCache: true, cachedAt: cached.cachedAt };
|
||||
}
|
||||
|
||||
return { data: cached as T, fromCache: true, cachedAt: null };
|
||||
}
|
||||
|
||||
const data = await fetchFn();
|
||||
await this.set(key, data, ttl);
|
||||
const cachedAt = new Date().toISOString();
|
||||
await this.set(key, { data, cachedAt }, ttl);
|
||||
|
||||
return { data, fromCache: false, cachedAt: new Date().toISOString() };
|
||||
return { data, fromCache: false, cachedAt };
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user