1+ import HawkCatcher from '@hawk.so/nodejs' ;
2+ import { createClient , RedisClientType } from 'redis' ;
3+ import { Effect , sgr } from './utils/ansi' ;
4+
5+ /**
6+ * Helper class for working with Redis
7+ */
8+ export default class RedisHelper {
9+ /**
10+ * TTL for lock records in Redis (in seconds)
11+ */
12+ private static readonly LOCK_TTL = 10 ;
13+
14+ /**
15+ * Redis client instance
16+ */
17+ private readonly redisClient ! : RedisClientType ;
18+
19+ /**
20+ * Constructor
21+ * Initializes the Redis client and sets up error handling
22+ */
23+ constructor ( ) {
24+ try {
25+ this . redisClient = createClient ( { url : process . env . REDIS_URL } ) ;
26+
27+ this . redisClient . on ( 'error' , ( error ) => {
28+ console . error ( '[Redis] Client error:' , error ) ;
29+ if ( error ) {
30+ HawkCatcher . send ( error ) ;
31+ }
32+ } ) ;
33+ } catch ( error ) {
34+ console . error ( '[Redis] Error creating client:' , error ) ;
35+ }
36+ }
37+
38+ /**
39+ * Connect to Redis
40+ */
41+ public async initialize ( ) : Promise < void > {
42+ try {
43+ await this . redisClient . connect ( ) ;
44+ console . log ( '[Redis] Connected successfully' ) ;
45+ } catch ( error ) {
46+ console . error ( '[Redis] Connection failed:' , error ) ;
47+ HawkCatcher . send ( error as Error ) ;
48+ }
49+ }
50+
51+ /**
52+ * Close Redis client
53+ */
54+ public async close ( ) : Promise < void > {
55+ if ( this . redisClient . isOpen ) {
56+ await this . redisClient . quit ( ) ;
57+ console . log ( '[Redis] Connection closed' ) ;
58+ }
59+ }
60+
61+ public async getChartDataFromRedis (
62+ hours : number , // количество интервалов (часов или дней)
63+ timezoneOffset = 0 ,
64+ projectId = '' ,
65+ groupHash = ''
66+ ) : Promise < { timestamp : number ; count : number } [ ] > {
67+ if ( ! this . redisClient . isOpen ) {
68+ throw new Error ( 'Redis client not connected' ) ;
69+ }
70+
71+ const key = groupHash
72+ ? `ts:events:${ groupHash } :hourly`
73+ : projectId
74+ ? `ts:events:${ projectId } :hourly`
75+ : `ts:events:hourly` ;
76+
77+ const now = Date . now ( ) ;
78+
79+ // определяем начало выборки
80+ const fromDate = new Date ( now ) ;
81+ fromDate . setMinutes ( 0 , 0 , 0 ) ;
82+ fromDate . setMilliseconds ( fromDate . getMilliseconds ( ) - ( hours * 60 * 60 * 1000 ) ) ;
83+ const from = fromDate . getTime ( ) ;
84+
85+ let result : [ string , string ] [ ] = [ ] ;
86+ try {
87+ result = ( await this . redisClient . sendCommand ( [
88+ 'TS.RANGE' ,
89+ key ,
90+ from . toString ( ) ,
91+ now . toString ( ) ,
92+ ] ) ) as [ string , string ] [ ] | [ ] ;
93+ } catch ( err : any ) {
94+ if ( err . message . includes ( 'TSDB: the key does not exist' ) ) {
95+ console . warn ( `[Redis] Key ${ key } does not exist, returning zeroed data` ) ;
96+ result = [ ] ;
97+ } else {
98+ throw err ;
99+ }
100+ }
101+
102+ console . log ( groupHash , result )
103+
104+ // агрегируем события по интервалу
105+ const dataPoints : { [ ts : number ] : number } = { } ;
106+ for ( const [ tsStr ] of result ) {
107+ const tsMs = Number ( tsStr ) ;
108+ const date = new Date ( tsMs ) ;
109+
110+ let intervalStart : number ;
111+ date . setMinutes ( 0 , 0 , 0 ) ;
112+ intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) , date . getUTCHours ( ) ) ;
113+
114+ const intervalWithOffset = intervalStart + timezoneOffset * 60 * 1000 ;
115+
116+ dataPoints [ intervalWithOffset ] = ( dataPoints [ intervalWithOffset ] || 0 ) + 1 ;
117+ }
118+
119+ // заполняем пропущенные интервалы нулями
120+ const filled : { timestamp : number ; count : number } [ ] = [ ] ;
121+ const nowDate = new Date ( now ) ;
122+
123+ for ( let i = 0 ; i < hours ; i ++ ) {
124+ const date = new Date ( nowDate ) ;
125+
126+ date . setHours ( date . getHours ( ) - i , 0 , 0 , 0 ) ;
127+ var intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) , date . getUTCHours ( ) ) ;
128+
129+ const intervalWithOffset = intervalStart + timezoneOffset * 60 * 1000 ;
130+ filled . push ( {
131+ timestamp : Math . floor ( intervalWithOffset / 1000 ) ,
132+ count : dataPoints [ intervalWithOffset ] || 0 ,
133+ } ) ;
134+ }
135+
136+ return filled . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
137+ }
138+ }
0 commit comments