forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalytics-cache.service.ts
More file actions
111 lines (94 loc) · 3.73 KB
/
Copy pathanalytics-cache.service.ts
File metadata and controls
111 lines (94 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* Analytics cache service — NestJS-managed Redis lifecycle (#466).
*
* Replaces the module-global Redis client in analytics.cache.ts with an
* injectable service that:
* - Creates the Redis connection inside `onModuleInit` (not at import time)
* - Gracefully closes the connection in `onModuleDestroy`
* - Supports health checks
* - Falls back gracefully when Redis is unavailable
*/
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { PlatformMetrics } from './analytics.metrics';
const METRICS_KEY = 'platform_metrics';
const METRICS_TTL_S = 10; // 10-second TTL for real-time freshness
@Injectable()
export class AnalyticsCacheService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(AnalyticsCacheService.name);
private redis: Redis | null = null;
private available = false;
constructor(private readonly configService: ConfigService) {}
// ── Lifecycle ──────────────────────────────────────────────────────────────
async onModuleInit(): Promise<void> {
const url = this.configService.get<string>('REDIS_URL', 'redis://localhost:6379');
try {
this.redis = new Redis(url, {
lazyConnect: true,
maxRetriesPerRequest: 2,
connectTimeout: 5_000,
enableOfflineQueue: false,
});
this.redis.on('error', (err) => {
if (this.available) {
this.logger.warn(`Redis connection error: ${err.message}`);
}
this.available = false;
});
this.redis.on('connect', () => {
this.available = true;
this.logger.log('Analytics Redis connected');
});
this.redis.on('close', () => {
this.available = false;
});
await this.redis.connect();
} catch (err) {
this.logger.warn(
`Analytics Redis unavailable — caching disabled. ${(err as Error).message}`,
);
this.available = false;
}
}
async onModuleDestroy(): Promise<void> {
if (this.redis) {
await this.redis.quit().catch(() => this.redis?.disconnect());
this.redis = null;
this.available = false;
this.logger.log('Analytics Redis connection closed');
}
}
// ── Health ─────────────────────────────────────────────────────────────────
isAvailable(): boolean {
return this.available;
}
async ping(): Promise<boolean> {
if (!this.redis || !this.available) return false;
try {
const pong = await this.redis.ping();
return pong === 'PONG';
} catch {
return false;
}
}
// ── Cache operations ───────────────────────────────────────────────────────
async cacheMetrics(metric: PlatformMetrics): Promise<void> {
if (!this.redis || !this.available) return;
try {
await this.redis.set(METRICS_KEY, JSON.stringify(metric), 'EX', METRICS_TTL_S);
} catch (err) {
this.logger.warn(`Failed to cache metrics: ${(err as Error).message}`);
}
}
async getCachedMetrics(): Promise<PlatformMetrics | null> {
if (!this.redis || !this.available) return null;
try {
const data = await this.redis.get(METRICS_KEY);
return data ? (JSON.parse(data) as PlatformMetrics) : null;
} catch (err) {
this.logger.warn(`Failed to read cached metrics: ${(err as Error).message}`);
return null;
}
}
}