Files
ratebubble/src/services/cache.service.ts
szbk 5c496277f2 feat(backend): realtime event system, admin API ve metrics altyapısı
- socket.ts: ContentRealtimeEvent, CacheRealtimeEvent, MetricsRealtimeEvent
  tipleri eklendi; emitContentEvent / emitCacheEvent / emitMetricsEvent
  fonksiyonları ile tüm istemcilere broadcast desteği getirildi.
  emitJobCompleted imzası GetInfoResponse + DataSource ile güçlendirildi.

- auth.middleware.ts: require() tabanlı env erişimi static import'a
  dönüştürüldü; admin-only endpointler için adminOnlyMiddleware eklendi
  (X-API-Key !== API_KEY_ADMIN → 403).

- cache.service.ts: set / delete / clearAll işlemlerinden sonra
  emitCacheEvent çağrısı eklenerek cache mutasyonları anlık yayınlanıyor.

- content.service.ts: create / update / delete sonrasında emitContentEvent
  çağrısı eklenerek DB yazımları Socket.IO üzerinden duyuruluyor.

- job.service.ts: async ve sync akışa MetricsService entegrasyonu eklendi;
  cache hit/miss ve kaynak (cache/database/netflix) sayaçları her işlemde
  artırılıyor.

- types/index.ts: AdminOverviewResponse ve AdminActionResponse tipleri
  merkezi olarak tanımlandı.

- admin.service.ts (yeni): getOverview, clearCache, warmupCacheFromDatabase,
  retryFailedJobs, refreshStaleContent operasyonları implement edildi.
  Redis pipeline ile TTL/boyut analizi ve DB metrikleri paralel toplanıyor.

- metrics.service.ts (yeni): Redis hash tabanlı cache hit/miss ve kaynak
  sayaçları; her artışta MetricsRealtimeEvent yayınlanıyor.

- api.routes.ts: Admin endpointleri eklendi:
    GET  /api/admin/overview
    POST /api/admin/cache/clear
    POST /api/admin/cache/warmup
    POST /api/admin/jobs/retry-failed
    POST /api/admin/content/refresh-stale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:59:59 +03:00

164 lines
3.8 KiB
TypeScript

import redis from '../config/redis.js';
import { env } from '../config/env.js';
import { emitCacheEvent } from '../config/socket.js';
import logger from '../utils/logger.js';
import type { GetInfoResponse, CacheEntry } from '../types/index.js';
/**
* Cache key prefix for Netflix content
*/
const CACHE_PREFIX = 'netflix:content:';
/**
* Generate cache key from URL
*/
function getCacheKey(url: string): string {
// Use URL hash or title ID as key
const titleId = url.match(/\/title\/(\d+)/)?.[1] || url;
return `${CACHE_PREFIX}${titleId}`;
}
/**
* Cache Service for Redis operations
* Handles caching with TTL support
*/
export class CacheService {
/**
* Get cached content by URL
*/
static async get(url: string): Promise<GetInfoResponse | null> {
const key = getCacheKey(url);
try {
const cached = await redis.get(key);
if (!cached) {
logger.debug('Cache miss', { url });
return null;
}
logger.debug('Cache hit', { url });
const entry: CacheEntry<GetInfoResponse> = JSON.parse(cached);
return entry.data;
} catch (error) {
logger.error('Cache get error', {
url,
error: error instanceof Error ? error.message : 'Unknown error',
});
return null;
}
}
/**
* Set cache entry with TTL
*/
static async set(url: string, data: GetInfoResponse): Promise<void> {
const key = getCacheKey(url);
const ttl = env.REDIS_TTL_SECONDS;
const entry: CacheEntry<GetInfoResponse> = {
data,
cachedAt: Date.now(),
ttl,
};
try {
await redis.setex(key, ttl, JSON.stringify(entry));
emitCacheEvent({
action: 'written',
key,
ttlSeconds: ttl,
occurredAt: new Date().toISOString(),
});
logger.debug('Cache set', { url, ttl });
} catch (error) {
logger.error('Cache set error', {
url,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Delete cached content
*/
static async delete(url: string): Promise<void> {
const key = getCacheKey(url);
try {
await redis.del(key);
emitCacheEvent({
action: 'deleted',
key,
occurredAt: new Date().toISOString(),
});
logger.debug('Cache deleted', { url });
} catch (error) {
logger.error('Cache delete error', {
url,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
/**
* Check if cache exists
*/
static async exists(url: string): Promise<boolean> {
const key = getCacheKey(url);
try {
const result = await redis.exists(key);
return result === 1;
} catch (error) {
logger.error('Cache exists check error', {
url,
error: error instanceof Error ? error.message : 'Unknown error',
});
return false;
}
}
/**
* Get cache TTL remaining
*/
static async getTTL(url: string): Promise<number> {
const key = getCacheKey(url);
try {
return await redis.ttl(key);
} catch (error) {
logger.error('Cache TTL check error', {
url,
error: error instanceof Error ? error.message : 'Unknown error',
});
return -1;
}
}
/**
* Clear all Netflix content cache
*/
static async clearAll(): Promise<void> {
try {
const keys = await redis.keys(`${CACHE_PREFIX}*`);
if (keys.length > 0) {
await redis.del(...keys);
emitCacheEvent({
action: 'cleared',
count: keys.length,
occurredAt: new Date().toISOString(),
});
logger.info('Cache cleared', { count: keys.length });
}
} catch (error) {
logger.error('Cache clear error', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
}
export default CacheService;