zarządco zapasów! Konsul Caesar.js zauważył, że legion ciągle biegnie do ładowni po te same przedmioty. Czas zorganizować strategiczne skrytki z najczęściej używanymi zapasami w łatwo dostępnych miejscach. W świecie NestJS oznacza to implementację systemu cache'owania!
Wyobraź sobie, że twoja legion co chwilę potrzebuje:
Cache to jak małe skrytki rozstawione po kastrum z kopiami najważniejszych informacji!
1// cache.module.ts
2import { CacheModule } from '@nestjs/cache-manager';
3import { Module } from '@nestjs/common';
4
5@Module({
6 imports: [
7 CacheModule.register({
8 ttl: 300, // 5 minut
9 max: 100, // maksymalnie 100 elementów
10 }),
11 ],
12})
13export class LegionaryCacheModule {}
14
15// tribute.service.ts
16import { CACHE_MANAGER } from '@nestjs/cache-manager';
17import { Cache } from 'cache-manager';
18import { Injectable, Inject } from '@nestjs/common';
19
20@Injectable()
21export class TributeService {
22 constructor(
23 @Inject(CACHE_MANAGER) private cacheManager: Cache,
24 private tributeRepository: TributeRepository,
25 ) {}
26
27 async findTributeById(id: number): Promise<Tribute> {
28 const cacheKey = `tribute:${id}`;
29
30 // Sprawdź cache
31 let tribute = await this.cacheManager.get<Tribute>(cacheKey);
32
33 if (tribute) {
34 console.log(' tributy znaleziony w skrytce!');
35 return tribute;
36 }
37
38 // Pobierz z bazy danych
39 console.log(' Szukam tributy w głównej tributów...');
40 tribute = await this.tributeRepository.findOne({ where: { id } });
41
42 if (tribute) {
43 // Zapisz w cache na 10 minut
44 await this.cacheManager.set(cacheKey, tribute, 600);
45 console.log(' tributy dodany do skrytki!');
46 }
47
48 return tribute;
49 }
50
51 async updateTribute(id: number, data: Partial<Tribute>): Promise<Tribute> {
52 const tribute = await this.tributeRepository.update(id, data);
53
54 // Usuń z cache po aktualizacji
55 const cacheKey = `tribute:${id}`;
56 await this.cacheManager.del(cacheKey);
57 console.log('Usunięto przestarzałe dane ze skrytki');
58
59 return tribute;
60 }
61
62 async getTributesByType(type: string): Promise<Tribute[]> {
63 const cacheKey = `tributes:type:${type}`;
64
65 let tributes = await this.cacheManager.get<Tribute[]>(cacheKey);
66
67 if (!tributes) {
68 tributes = await this.tributeRepository.find({
69 where: { type }
70 });
71
72 // Cache na 5 minut
73 await this.cacheManager.set(cacheKey, tributes, 300);
74 }
75
76 return tributes;
77 }
78}1// decorators/cacheable.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4export const CACHE_KEY_METADATA = 'cache_key';
5export const CACHE_TTL_METADATA = 'cache_ttl';
6
7export const Cacheable = (key?: string, ttl?: number) => {
8 return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
9 SetMetadata(CACHE_KEY_METADATA, key || propertyName)(target, propertyName, descriptor);
10 if (ttl) {
11 SetMetadata(CACHE_TTL_METADATA, ttl)(target, propertyName, descriptor);
12 }
13 };
14};
15
16// cache.interceptor.ts
17import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject } from '@nestjs/common';
18import { Reflector } from '@nestjs/core';
19import { CACHE_MANAGER } from '@nestjs/cache-manager';
20import { Cache } from 'cache-manager';
21import { Observable, of } from 'rxjs';
22import { tap } from 'rxjs/operators';
23
24@Injectable()
25export class CacheInterceptor implements NestInterceptor {
26 constructor(
27 @Inject(CACHE_MANAGER) private cacheManager: Cache,
28 private reflector: Reflector,
29 ) {}
30
31 async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
32 const cacheKey = this.reflector.get<string>(CACHE_KEY_METADATA, context.getHandler());
33 const ttl = this.reflector.get<number>(CACHE_TTL_METADATA, context.getHandler()) || 300;
34
35 if (!cacheKey) {
36 return next.handle();
37 }
38
39 const request = context.switchToHttp().getRequest();
40 const fullCacheKey = this.generateCacheKey(cacheKey, request);
41
42 // Sprawdź cache
43 const cachedResult = await this.cacheManager.get(fullCacheKey);
44 if (cachedResult) {
45 console.log(` Dane pobrane ze skrytki: ${fullCacheKey}`);
46 return of(cachedResult);
47 }
48
49 // Wykonaj operację i zapisz w cache
50 return next.handle().pipe(
51 tap(async (result) => {
52 await this.cacheManager.set(fullCacheKey, result, ttl);
53 console.log(` Dane zapisane w skrytce: ${fullCacheKey}`);
54 }),
55 );
56 }
57
58 private generateCacheKey(baseKey: string, request: any): string {
59 const params = { ...request.params, ...request.query };
60 const paramString = Object.keys(params)
61 .sort()
62 .map(key => `${key}:${params[key]}`)
63 .join(':');
64
65 return paramString ? `${baseKey}:${paramString}` : baseKey;
66 }
67}
68
69// Użycie w serwisie
70@Injectable()
71export class LegionService {
72 @Cacheable('legion_stats', 600) // 10 minut
73 async getLegionStatistics(legionId: number) {
74 // Skomplikowane obliczenia...
75 return {
76 totalLegions: 25,
77 totalLegion: 1250,
78 totalTributes: 5000,
79 averageSpeed: 12.5,
80 };
81 }
82
83 @Cacheable('legion_list', 300) // 5 minut
84 async getLegionByCohort(cohortId: number) {
85 // Zapytanie do bazy danych...
86 return this.legionRepository.find({ where: { cohortId } });
87 }
88}1// cache-warmer.service.ts
2@Injectable()
3export class CacheWarmerService {
4 constructor(
5 @Inject(CACHE_MANAGER) private cacheManager: Cache,
6 private tributeService: TributeService,
7 private legionService: LegionService,
8 private weatherService: WeatherService,
9 ) {}
10
11 @Cron('0 */30 * * * *') // Co 30 minut
12 async warmPopularCaches() {
13 console.log('Przygotowywanie popularnych skrytek...');
14
15 try {
16 // Najpopularniejsze tributy
17 const popularTributeIds = [1, 2, 3, 5, 8, 13, 21];
18 await Promise.all(
19 popularTributeIds.map(id =>
20 this.tributeService.findTributeById(id)
21 )
22 );
23
24 // Statystyki wszystkich flot
25 const legionIds = await this.legionService.getAllLegionIds();
26 await Promise.all(
27 legionIds.map(id =>
28 this.legionService.getLegionStatistics(id)
29 )
30 );
31
32 // Prognoza pogody dla głównych tras
33 const mainRoutes = ['Hispania', 'mediterranean', 'north_sea'];
34 await Promise.all(
35 mainRoutes.map(route =>
36 this.weatherService.getWeatherForecast(route)
37 )
38 );
39
40 console.log('✅ Skrytki przygotowane!');
41 } catch (error) {
42 console.error('❌ Błąd podczas przygotowywania skrytek:', error);
43 }
44 }
45
46 async warmCacheForUser(userId: number) {
47 // Przygotuj cache dla konkretnego użytkownika
48 const user = await this.userService.findById(userId);
49
50 if (user.favoriteLegionId) {
51 await this.legionService.getLegionDetails(user.favoriteLegionId);
52 }
53
54 if (user.recentTributes?.length) {
55 await Promise.all(
56 user.recentTributes.map(tributeId =>
57 this.tributeService.findTributeById(tributeId)
58 )
59 );
60 }
61 }
62
63 async preloadDashboardData() {
64 // Dane dla głównego dashboardu
65 const dashboardPromises = [
66 this.cacheManager.set('dashboard:total_legions', await this.getLegionCount(), 3600),
67 this.cacheManager.set('dashboard:active_cohorts', await this.getActiveLegionCount(), 1800),
68 this.cacheManager.set('dashboard:recent_tributes', await this.getRecentTributes(), 900),
69 this.cacheManager.set('dashboard:weather_alerts', await this.getWeatherAlerts(), 600),
70 ];
71
72 await Promise.all(dashboardPromises);
73 console.log(' Dashboard cache załadowany!');
74 }
75}1// cache-invalidation.service.ts
2@Injectable()
3export class CacheInvalidationService {
4 constructor(
5 @Inject(CACHE_MANAGER) private cacheManager: Cache,
6 ) {}
7
8 async invalidateTributeCache(tributeId: number) {
9 const patterns = [
10 `tribute:${tributeId}`,
11 `tributes:*`, // Wszystkie listy tributów
12 `dashboard:recent_tributes`,
13 ];
14
15 await this.invalidateByPatterns(patterns);
16 console.log(`Wyczyszczono cache dla tributy ${tributeId}`);
17 }
18
19 async invalidateLegionCache(legionId: number) {
20 const patterns = [
21 `legion:${legionId}:*`,
22 `legion_stats:${legionId}`,
23 `dashboard:*`,
24 ];
25
26 await this.invalidateByPatterns(patterns);
27 }
28
29 async invalidateUserCache(userId: number) {
30 const patterns = [
31 `user:${userId}:*`,
32 `legion_list:*`,
33 ];
34
35 await this.invalidateByPatterns(patterns);
36 }
37
38 private async invalidateByPatterns(patterns: string[]) {
39 // W prawdziwej implementacji z Redis użyj SCAN + DEL
40 for (const pattern of patterns) {
41 if (pattern.includes('*')) {
42 // Usuń wszystkie klucze pasujące do wzorca
43 await this.deleteByPattern(pattern);
44 } else {
45 // Usuń konkretny klucz
46 await this.cacheManager.del(pattern);
47 }
48 }
49 }
50
51 private async deleteByPattern(pattern: string) {
52 // Implementacja zależna od używanego cache managera
53 // Dla in-memory cache:
54 const keys = await this.cacheManager.store.keys();
55 const matchingKeys = keys.filter(key => this.matchPattern(key, pattern));
56
57 await Promise.all(
58 matchingKeys.map(key => this.cacheManager.del(key))
59 );
60 }
61
62 private matchPattern(key: string, pattern: string): boolean {
63 const regex = new RegExp(pattern.replace('*', '.*'));
64 return regex.test(key);
65 }
66}
67
68// Event-driven cache invalidation
69@Injectable()
70export class TributeEventHandler {
71 constructor(
72 private cacheInvalidation: CacheInvalidationService,
73 ) {}
74
75 @OnEvent('tribute.created')
76 async handleTributeCreated(event: TributeCreatedEvent) {
77 await this.cacheInvalidation.invalidateTributeCache(event.tributeId);
78 console.log('Cache unieważniony po dodaniu tributy');
79 }
80
81 @OnEvent('tribute.updated')
82 async handleTributeUpdated(event: TributeUpdatedEvent) {
83 await this.cacheInvalidation.invalidateTributeCache(event.tributeId);
84 console.log('Cache unieważniony po aktualizacji tributy');
85 }
86
87 @OnEvent('legion.disbanded')
88 async handleLegionDisbanded(event: LegionDisbandedEvent) {
89 await this.cacheInvalidation.invalidateLegionCache(event.legionId);
90 console.log(' Cache unieważniony po rozwiązaniu konfliktu');
91 }
92}1// cache-metrics.service.ts
2@Injectable()
3export class CacheMetricsService {
4 private metrics = {
5 hits: 0,
6 misses: 0,
7 sets: 0,
8 deletes: 0,
9 errors: 0,
10 };
11
12 constructor(
13 @Inject(CACHE_MANAGER) private cacheManager: Cache,
14 ) {}
15
16 recordHit() {
17 this.metrics.hits++;
18 }
19
20 recordMiss() {
21 this.metrics.misses++;
22 }
23
24 recordSet() {
25 this.metrics.sets++;
26 }
27
28 recordDelete() {
29 this.metrics.deletes++;
30 }
31
32 recordError() {
33 this.metrics.errors++;
34 }
35
36 getMetrics() {
37 const total = this.metrics.hits + this.metrics.misses;
38 const hitRate = total > 0 ? (this.metrics.hits / total) * 100 : 0;
39
40 return {
41 ...this.metrics,
42 hitRate: hitRate.toFixed(2) + '%',
43 totalRequests: total,
44 };
45 }
46
47 @Cron('0 */15 * * * *') // Co 15 minut
48 logMetrics() {
49 const metrics = this.getMetrics();
50 console.log('Cache Metrics:', {
51 hitRate: metrics.hitRate,
52 totalRequests: metrics.totalRequests,
53 errors: metrics.errors,
54 });
55
56 // Resetuj metryki co godzinę
57 if (new Date().getMinutes() === 0) {
58 this.resetMetrics();
59 }
60 }
61
62 private resetMetrics() {
63 this.metrics = {
64 hits: 0,
65 misses: 0,
66 sets: 0,
67 deletes: 0,
68 errors: 0,
69 };
70 }
71
72 async getCacheSize(): Promise<number> {
73 try {
74 const keys = await this.cacheManager.store.keys();
75 return keys.length;
76 } catch (error) {
77 return -1;
78 }
79 }
80
81 async getMemoryUsage(): Promise<string> {
82 // Implementacja zależna od typu cache
83 const used = process.memoryUsage();
84 return `${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`;
85 }
86}1// cache.config.ts
2import { ConfigService } from '@nestjs/config';
3
4export const cacheConfigFactory = (configService: ConfigService) => {
5 const environment = configService.get('NODE_ENV');
6
7 switch (environment) {
8 case 'production':
9 return {
10 ttl: 3600, // 1 godzina
11 max: 1000, // 1000 elementów
12 store: 'redis',
13 host: configService.get('REDIS_HOST'),
14 port: configService.get('REDIS_PORT'),
15 };
16
17 case 'staging':
18 return {
19 ttl: 1800, // 30 minut
20 max: 500,
21 store: 'redis',
22 host: configService.get('REDIS_HOST'),
23 port: configService.get('REDIS_PORT'),
24 };
25
26 default: // development
27 return {
28 ttl: 300, // 5 minut
29 max: 100,
30 store: 'memory',
31 };
32 }
33};
34
35// Użycie w module
36@Module({
37 imports: [
38 CacheModule.registerAsync({
39 imports: [ConfigModule],
40 useFactory: cacheConfigFactory,
41 inject: [ConfigService],
42 }),
43 ],
44})
45export class AppModule {}Cache to potężna broń w arsenale każdego legionariusza-programisty! Dobrze zorganizowane skrytki mogą przyspieszyć fort nawet 10-krotnie. Pamiętaj jednak - cache to nie uniwersalne rozwiązanie na wszystkie problemy wydajności!