import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { CacheKeys } from '@box/common/cache/cache-keys'; import { RedisService } from '@box/db/redis/redis.service'; import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service'; import type { AdType } from '@box/common/ads/ad-types'; interface CachedAd { id: string; channelId: string; adsModuleId: string; advertiser: string; title: string; adsContent: string | null; adsCoverImg: string | null; adsUrl: string | null; adType: string; } /** * AdCacheWarmupService * * Responsible for warming up individual ad caches (app:ad:by-id:${adId}) * on application startup or on-demand. * * This complements AdPoolWarmupService which handles ad pools. */ @Injectable() export class AdCacheWarmupService implements OnModuleInit { private readonly logger = new Logger(AdCacheWarmupService.name); private readonly AD_CACHE_TTL = 300; // 5 minutes constructor( private readonly redis: RedisService, private readonly mongoPrisma: MongoPrismaService, ) {} async onModuleInit(): Promise { try { this.logger.log('Individual ad cache warmup starting...'); await this.warmupAllAdCaches(); } catch (err) { this.logger.error( 'Individual ad cache warmup encountered an error but will not block startup', err instanceof Error ? err.stack : String(err), ); } } /** * Warm up all active ads' individual caches. * Only caches ads that are enabled and within their date range. */ async warmupAllAdCaches(): Promise { const startTime = Date.now(); const now = BigInt(Date.now()); try { // Fetch all active ads const ads = await this.mongoPrisma.ads.findMany({ where: { status: 1, // enabled startDt: { lte: now }, OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }], }, include: { adsModule: { select: { adType: true } }, }, }); this.logger.log(`Found ${ads.length} active ads to cache`); let successCount = 0; let errorCount = 0; // Cache each ad individually for (const ad of ads) { try { await this.cacheAd(ad.id, { id: ad.id, channelId: ad.channelId, adsModuleId: ad.adsModuleId, advertiser: ad.advertiser, title: ad.title, adsContent: ad.adsContent ?? null, adsCoverImg: ad.adsCoverImg ?? null, adsUrl: ad.adsUrl ?? null, adType: ad.adsModule.adType, }); successCount++; } catch (err) { errorCount++; this.logger.warn( `Failed to cache ad ${ad.id}: ${err instanceof Error ? err.message : String(err)}`, ); } } const duration = Date.now() - startTime; this.logger.log( `Ad cache warmup completed: ${successCount} cached, ${errorCount} errors, ${duration}ms`, ); } catch (err) { this.logger.error( 'Ad cache warmup failed', err instanceof Error ? err.stack : String(err), ); throw err; } } /** * Cache a single ad by ID */ private async cacheAd(adId: string, cachedAd: CachedAd): Promise { const key = CacheKeys.appAdById(adId); await this.redis.setJson(key, cachedAd, this.AD_CACHE_TTL); } /** * Warm up a single ad cache (used for on-demand refresh) */ async warmupSingleAd(adId: string): Promise { const now = BigInt(Date.now()); const ad = await this.mongoPrisma.ads.findUnique({ where: { id: adId }, include: { adsModule: { select: { adType: true } }, }, }); if (!ad) { // Ad doesn't exist - remove from cache const key = CacheKeys.appAdById(adId); await this.redis.del(key); this.logger.debug(`Ad ${adId} not found, removed from cache`); return; } // Check if ad is active const isActive = ad.status === 1 && ad.startDt <= now && (ad.expiryDt === BigInt(0) || ad.expiryDt >= now); if (!isActive) { // Ad is not active - remove from cache const key = CacheKeys.appAdById(adId); await this.redis.del(key); this.logger.debug(`Ad ${adId} is not active, removed from cache`); return; } // Cache the ad await this.cacheAd(adId, { id: ad.id, channelId: ad.channelId, adsModuleId: ad.adsModuleId, advertiser: ad.advertiser, title: ad.title, adsContent: ad.adsContent ?? null, adsCoverImg: ad.adsCoverImg ?? null, adsUrl: ad.adsUrl ?? null, adType: ad.adsModule.adType, }); this.logger.debug(`Cached ad ${adId}`); } }