• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hicommonwealth / commonwealth / 15926786676

27 Jun 2025 12:54PM UTC coverage: 39.941% (-0.2%) from 40.122%
15926786676

push

github

web-flow
Merge pull request #12045 from hicommonwealth/tim/rank-updates

1850 of 5017 branches covered (36.87%)

Branch coverage included in aggregate %.

12 of 50 new or added lines in 2 files covered. (24.0%)

1 existing line in 1 file now uncovered.

3278 of 7822 relevant lines covered (41.91%)

36.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.0
/libs/adapters/src/redis/RedisCacheAdapter.ts
1
import {
2
  Cache,
3
  ILogger,
4
  logger,
5
  type CacheNamespaces,
6
} from '@hicommonwealth/core';
7
import { delay } from '@hicommonwealth/shared';
8
import { RedisClientOptions, createClient, type RedisClientType } from 'redis';
9

10
const CONNECT_TIMEOUT = 5000;
5✔
11

12
export function redisRetryStrategy(retries: number) {
13
  // Don't stop retrying while app is running
14
  // if (retries > 5) {
15
  //   return new Error('Redis max connection retries exceeded');
16
  // }
17

18
  // timetable: 0, 1000, 8000, 27000, 64000, 125000, 216000, 343000, 512000, 729000, 1000000
19
  // from 1 sec to 16.67 minutes
20
  return Math.min((retries * 10) ** 3, 10 * 60 * 1000);
×
21
}
22

23
/**
24
 * This class facilitates interacting with Redis and constructing a Redis Cache. Note that all keys must use a namespace
25
 * prefix to divide the Redis keyspace. If a specific Redis command is not supported by this class you can access the
26
 * client directly or better yet open a PR that implements the class methods necessary to support the new Redis command.
27
 * WARNING: If running blocking arbitrary commands using the client directly be sure to include the 'isolated' option
28
 * in order to avoid blocking the client for other requests that may be occurring.
29
 */
30
export class RedisCache implements Cache {
31
  private _client: RedisClientType;
32
  private _log: ILogger;
33

34
  constructor(redis_url: string) {
35
    const redisOptions: RedisClientOptions = {};
3✔
36
    redisOptions['url'] = redis_url;
3✔
37
    if (redis_url.includes('rediss')) {
3!
38
      redisOptions['socket'] = {
×
39
        tls: true,
40
        rejectUnauthorized: false,
41
        reconnectStrategy: redisRetryStrategy,
42
        connectTimeout: CONNECT_TIMEOUT,
43
      };
44
    } else {
45
      redisOptions['socket'] = {
3✔
46
        reconnectStrategy: redisRetryStrategy,
47
        connectTimeout: CONNECT_TIMEOUT,
48
      };
49
    }
50

51
    this._log = logger(import.meta);
3✔
52
    this._log.info(`Connecting to Redis at: ${redis_url}`);
3✔
53
    this._client = createClient(redisOptions) as RedisClientType;
3✔
54

55
    this._client.on('ready', () =>
3✔
56
      this._log.info(`RedisCache connection ready`),
3✔
57
    );
58
    this._client.on('reconnecting', () =>
3✔
59
      this._log.info(`RedisCache reconnecting...`),
×
60
    );
61
    this._client.on('end', () => this._log.info(`RedisCache disconnected`));
3✔
62
    this._client.on('error', (err: Error) => {
3✔
63
      this._log.error(err.message, err);
×
64
    });
65

66
    void this._client.connect();
3✔
67
  }
68

69
  // get namespace key for redis
70
  static getNamespaceKey(namespace: CacheNamespaces, key: string) {
71
    return `${namespace}_${key}`;
47✔
72
  }
73

74
  public get name(): string {
75
    return 'RedisCache';
9✔
76
  }
77

78
  public get client(): RedisClientType {
79
    return this._client;
×
80
  }
81

82
  public async dispose(): Promise<void> {
83
    try {
3✔
84
      await this._client.disconnect();
3✔
85
      await this._client.quit();
3✔
86
    } catch {
87
      // ignore this
88
    }
89
  }
90

91
  /**
92
   * Awaits redis connection / cache ready
93
   * @returns
94
   */
95
  public ready(retries = 3, retryDelay = 1000) {
6✔
96
    // eslint-disable-next-line no-async-promise-executor, @typescript-eslint/no-misused-promises
97
    return new Promise<boolean>(async (resolve, reject) => {
3✔
98
      for (let i = 0; i < retries; i++) {
3✔
99
        if (this.isReady()) {
6✔
100
          resolve(true);
3✔
101
          return;
3✔
102
        }
103
        await delay(retryDelay);
3✔
104
      }
105
      reject('RedisCache ready timeout');
×
106
    });
107
  }
108

109
  /**
110
   * Check if redis is initialized
111
   * @returns boolean
112
   */
113
  public isReady(): boolean {
114
    return this._client.isReady;
79✔
115
  }
116

117
  /**
118
   * This function facilitates setting a key-value pair in Redis. Since Redis has a single keyspace we include a prefix
119
   * to simulate many keyspaces. That is, all key-value pairs for a specific functionality should use a matching prefix.
120
   * For example, for if we had chat WebSockets we store user_id => address key-value pairs. Since we may want to store
121
   * other data in which the key would be the user_id we use the 'chat_socket' prefix for all key-pairs pertaining to
122
   * the chat websocket. The resulting key would thus be 'chat_socket_[user_id]'. The prefix can be thought of as the
123
   * namespace of the data that you are trying to store.
124
   * @param namespace The prefix to append to the dynamic key i.e. the namespace. An instance of the
125
   * CacheNamespaces enum.
126
   * @param key The actual key you want to store (can be any valid string).
127
   * @param value The value to associate with the namespace and key
128
   * @param duration The number of seconds after which the key should be automatically 'deleted' by Redis i.e. TTL
129
   * @param notExists If true and the key already exists the key will not be set
130
   */
131
  public async setKey(
132
    namespace: CacheNamespaces,
133
    key: string,
134
    value: string,
135
    duration = 0,
7✔
136
    notExists = false,
13✔
137
  ): Promise<boolean> {
138
    if (!this.isReady()) return false;
13!
139
    try {
13✔
140
      const options: { NX: boolean; EX?: number } = {
13✔
141
        NX: notExists,
142
      };
143
      duration > 0 && (options.EX = duration);
13✔
144
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
13✔
145
      if (typeof value !== 'string') {
13!
146
        value = JSON.stringify(value);
×
147
      }
148
      const res = await this._client.set(
13✔
149
        finalKey as any,
150
        value,
151
        options as any,
152
      );
153
      return res === 'OK';
13✔
154
    } catch (e) {
155
      const msg = `An error occurred while setting the following key value pair '${namespace} ${key}: ${value}'`;
×
156
      this._log.error(msg, e as Error);
×
157
      return false;
×
158
    }
159
  }
160

161
  public async getKey(
162
    namespace: CacheNamespaces,
163
    key: string,
164
  ): Promise<string | null> {
165
    if (!this.isReady()) return null;
16!
166
    try {
16✔
167
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
16✔
168
      return await this._client.get(finalKey);
16✔
169
    } catch (e) {
170
      const msg = `An error occurred while getting the following key '${key}'`;
×
171
      this._log.error(msg, e as Error);
×
172
      return null;
×
173
    }
174
  }
175

176
  /**
177
   * This function works the same way at the 'setKey' function above but is meant to be used when multiple key value
178
   * pairs need to be inserted at the same time in an 'all or none' fashion i.e. SQL transaction style.
179
   * @param namespace The prefix to append to the key.
180
   * @param data The key-value pairs to set in Redis
181
   * @param duration The TTL for each key in data.
182
   * @param transaction This boolean indicates whether keys should all be set within a transaction when
183
   * the duration parameter is set. Specifically, if transaction is true we use multi-exec and if
184
   * transaction is false we use a pipeline and return any keys that failed to set. Note that if transaction
185
   * is true, a blocking and potentially less performant operation is executed.
186
   */
187
  public async setKeys(
188
    namespace: CacheNamespaces,
189
    data: { [key: string]: string },
190
    duration = 0,
×
191
    transaction = true,
×
192
  ): Promise<false | Array<'OK' | null>> {
193
    if (!this.isReady()) return false;
×
194

195
    // add the namespace prefix to all keys
196
    const transformedData = Object.keys(data).reduce((result, key) => {
×
197
      result[RedisCache.getNamespaceKey(namespace, key)] = data[key];
×
198
      return result;
×
199
    }, {} as any);
200

201
    if (duration > 0) {
×
202
      // MSET doesn't support setting TTL, so we need use
203
      // a multi-exec to process many SET commands
204
      const multi = this._client.multi();
×
205
      for (const key of Object.keys(transformedData)) {
×
206
        multi.set(key, transformedData[key], { EX: duration });
×
207
      }
208

209
      try {
×
210
        if (transaction) return (await multi.exec()) as Array<'OK' | null>;
×
211
        else return (await multi.execAsPipeline()) as Array<'OK' | null>;
×
212
      } catch (e) {
213
        const msg =
214
          `Error occurred while setting multiple keys ` +
×
215
          `${transaction ? 'in a transaction' : 'in a pipeline'}`;
×
216
        this._log.error(msg, e as Error);
×
217
        return false;
×
218
      }
219
    } else {
220
      try {
×
221
        return [(await this._client.MSET(transformedData)) as 'OK'];
×
222
      } catch (e) {
223
        const msg = 'Error occurred while setting multiple keys';
×
224
        this._log.error(msg, e as Error);
×
225
        return false;
×
226
      }
227
    }
228
  }
229

230
  public async getKeys(
231
    namespace: CacheNamespaces,
232
    keys: string[],
233
  ): Promise<false | Record<string, unknown>> {
234
    if (!this.isReady()) return false;
×
235
    const transformedKeys = keys.map((k) =>
×
236
      RedisCache.getNamespaceKey(namespace, k),
×
237
    );
238
    try {
×
239
      const values = await this._client.MGET(transformedKeys);
×
240
      return transformedKeys.reduce((obj, key, index) => {
×
241
        if (values[index] !== null) {
×
242
          obj[key] = values[index];
×
243
        }
244
        return obj;
×
245
      }, {} as any);
246
    } catch (e) {
247
      const msg = 'An error occurred while getting many keys';
×
248
      this._log.error(msg, e as Error);
×
249
      return false;
×
250
    }
251
  }
252

253
  /**
254
   * Increments the integer value of a key by the given amount.
255
   * @param namespace The namespace of the key to increment.
256
   * @param key The key whose value is to be incremented.
257
   * @param increment The amount by which the key's value should be incremented.
258
   * @param duration Optional duration for the key to live in the cache. (in seconds)
259
   * @returns The new value of the key after the increment.
260
   */
261
  public async incrementKey(
262
    namespace: CacheNamespaces,
263
    key: string,
264
    increment = 1,
×
265
    duration?: number,
266
  ): Promise<number | null> {
267
    if (!this.isReady()) return null;
×
268
    try {
×
269
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
×
270
      if (duration) {
×
271
        const multi = this._client.multi();
×
272
        multi.incrBy(finalKey, increment);
×
273
        multi.expire(finalKey, duration, 'NX');
×
274
        const [incr] = (await multi.exec()) as [number, boolean];
×
275
        return incr;
×
276
      }
277
      // plain increment
278
      return await this._client.incrBy(finalKey, increment);
×
279
    } catch (e) {
280
      const msg = `An error occurred while incrementing the key: ${key}`;
×
281
      this._log.error(msg, e as Error);
×
282
      return null;
×
283
    }
284
  }
285

286
  /**
287
   * Decrements the integer value of a key by the given amount.
288
   * @param namespace The namespace of the key to decrement.
289
   * @param key The key whose value is to be decremented.
290
   * @param decrement The amount by which the key's value should be decremented.
291
   * @returns The new value of the key after the decrement.
292
   */
293
  public async decrementKey(
294
    namespace: CacheNamespaces,
295
    key: string,
296
    decrement = 1,
×
297
  ): Promise<number | null> {
298
    if (!this.isReady()) return null;
×
299
    try {
×
300
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
×
301
      return await this._client.decrBy(finalKey, decrement);
×
302
    } catch (e) {
303
      const msg = `An error occurred while decrementing the key: ${key}`;
×
304
      this._log.error(msg, e as Error);
×
305
      return null;
×
306
    }
307
  }
308

309
  /**
310
   * Sets the expiration (TTL) of a key within a specific namespace.
311
   * @param namespace The namespace of the key for which to set the expiration.
312
   * @param key The key for which to set the expiration.
313
   * @param ttlInSeconds The time to live (TTL) in seconds for the key. Use 0 to remove the expiration.
314
   * @returns True if the expiration was set successfully, false otherwise.
315
   */
316
  public async setKeyTTL(
317
    namespace: CacheNamespaces,
318
    key: string,
319
    ttlInSeconds: number,
320
  ): Promise<boolean> {
321
    if (!this.isReady()) return false;
×
322
    try {
×
323
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
×
324
      if (ttlInSeconds === 0) {
×
325
        // If ttlInSeconds is 0, remove the expiration (PERSIST command).
326
        return await this._client.persist(finalKey);
×
327
      } else {
328
        // Set the expiration using the EXPIRE command.
329
        return await this._client.expire(finalKey, ttlInSeconds);
×
330
      }
331
    } catch (e) {
332
      const msg = `An error occurred while setting the expiration of the key: ${key}`;
×
333
      this._log.error(msg, e as Error);
×
334
      return false;
×
335
    }
336
  }
337

338
  /**
339
   * Retrieves the current Time to Live (TTL) of a key within a specific namespace.
340
   * @param namespace The namespace of the key for which to get the TTL.
341
   * @param key The key for which to get the TTL.
342
   * @returns The TTL in seconds for the specified key, or -1 if the key does not exist or has no associated expiration.
343
   */
344
  public async getKeyTTL(
345
    namespace: CacheNamespaces,
346
    key: string,
347
  ): Promise<number> {
348
    if (!this.isReady()) return -2;
×
349
    try {
×
350
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
×
351
      // TTL in seconds; -2 if the key does not exist, -1 if the key exists but has no associated expire.
352
      return await this._client.ttl(finalKey);
×
353
    } catch (e) {
354
      const msg = `An error occurred while retrieving the TTL of the key: ${key}`;
×
355
      this._log.error(msg, e as Error);
×
356
      return -2;
×
357
    }
358
  }
359

360
  /**
361
   * Get all the key-value pairs of a specific namespace.
362
   * @param namespace The name of the namespace to retrieve keys from
363
   * @param maxResults The maximum number of keys to retrieve from the given namespace
364
   * @param withData
365
   */
366
  public async getNamespaceKeys(
367
    namespace: CacheNamespaces,
368
  ): Promise<{ [key: string]: string } | boolean> {
369
    const data = {} as any;
2✔
370
    if (!this.isReady()) return false;
2!
371
    try {
2✔
372
      const keys = await this.scanNamespaceKeys(namespace);
2✔
373
      for (const key of keys) {
2✔
374
        const keyType = await this._client.type(key);
3✔
375
        if (keyType === 'string') {
3!
376
          data[key] = await this._client.get(key);
3✔
377
        } else {
UNCOV
378
          this._log.warn(
×
379
            `Skipping key with non-string type: ${key} (type: ${keyType})`,
380
          );
381
        }
382
      }
383
      return data;
2✔
384
    } catch (e) {
385
      const msg = 'An error occurred while fetching the namespace keys';
×
386
      this._log.error(msg, e as Error);
×
387
      return false;
×
388
    }
389
  }
390

391
  private async scanNamespaceKeys(
392
    namespace: CacheNamespaces,
393
    resultsPerIteration = 100,
2✔
394
  ): Promise<string[]> {
395
    const keys: string[] = [];
2✔
396
    if (!this.isReady()) return [];
2!
397
    try {
2✔
398
      for await (const key of this._client.scanIterator({
2✔
399
        MATCH: `${namespace}*`,
400
        COUNT: resultsPerIteration,
401
      })) {
402
        keys.push(key);
3✔
403
      }
404
    } catch (e) {
NEW
405
      this._log.error(
×
406
        'An error occurred while scanning namespace keys',
407
        e as Error,
408
      );
NEW
409
      return [];
×
410
    }
411
    return keys;
2✔
412
  }
413

414
  /**
415
   * delete redis key by namespace and key
416
   * @returns boolean
417
   */
418
  public async deleteNamespaceKeys(
419
    namespace: CacheNamespaces,
420
  ): Promise<number | boolean> {
421
    if (!this.isReady()) return false;
10!
422
    try {
10✔
423
      let count = 0;
10✔
424
      for await (const key of this._client.scanIterator({
10✔
425
        MATCH: `${namespace}*`,
426
        COUNT: 100,
427
      })) {
428
        try {
10✔
429
          const resp = await this._client.del(key);
10✔
430
          count += resp ?? 0;
10!
431
          this._log.trace(`deleted key ${key} ${resp} ${count}`);
10✔
432
        } catch (err) {
NEW
433
          this._log.trace(`error deleting key ${key}`);
×
NEW
434
          this._log.trace((err as Error).message);
×
435
        }
436
      }
437
      return count;
10✔
438
    } catch (e) {
439
      const msg = `An error occurred while deleting a all keys in the ${namespace} namespace`;
×
440
      this._log.error(msg, e as Error);
×
441
      return false;
×
442
    }
443
  }
444

445
  public async deleteKey(
446
    namespace: CacheNamespaces,
447
    key: string,
448
  ): Promise<number> {
449
    if (!this.isReady()) return 0;
×
450
    const finalKey = RedisCache.getNamespaceKey(namespace, key);
×
451
    try {
×
452
      return this._client.del(finalKey);
×
453
    } catch (e) {
454
      return 0;
×
455
    }
456
  }
457

458
  /**
459
   * Returns the paginated matching namespace:keys in the namespace.
460
   * @param namespace The prefix to check for keys in.
461
   * @param cursor Start index of the scan.
462
   * @param count How many keys to return.
463
   * @returns The cursor pointing to the next pagination and the keys scanned.
464
   */
465
  public async scan(
466
    namespace: CacheNamespaces,
467
    cursor: number,
468
    count: number,
469
  ): Promise<{ cursor: number; keys: string[] } | null> {
470
    if (!this.isReady()) return null;
×
471
    try {
×
472
      return this._client.scan(cursor, {
×
473
        MATCH: `${namespace}*`,
474
        COUNT: count,
475
      });
476
    } catch (e) {
477
      const msg = 'An error occurred while running scan';
×
478
      this._log.error(msg, e as Error);
×
479
      return null;
×
480
    }
481
  }
482

483
  public async incrementHashKey(
484
    namespace: CacheNamespaces,
485
    key: string,
486
    field: string,
487
    increment: number = 1,
×
488
  ): Promise<number> {
489
    if (!this.isReady()) return 0;
×
490
    try {
×
491
      return this._client.HINCRBY(
×
492
        RedisCache.getNamespaceKey(namespace, key),
493
        field,
494
        increment,
495
      );
496
    } catch (e) {
497
      this._log.error(
×
498
        'An error occurred while incrementing hash key',
499
        e as Error,
500
      );
501
    }
502
    return 0;
×
503
  }
504

505
  public async getHash(
506
    namespace: CacheNamespaces,
507
    key: string,
508
  ): Promise<Record<string, string>> {
509
    if (!this.isReady()) return {};
×
510
    try {
×
511
      return this._client.HGETALL(RedisCache.getNamespaceKey(namespace, key));
×
512
    } catch (e) {
513
      this._log.error('An error occurred while getting hash', e as Error);
×
514
    }
515
    return {};
×
516
  }
517

518
  public async setHashKey(
519
    namespace: CacheNamespaces,
520
    key: string,
521
    field: string,
522
    value: string,
523
  ): Promise<number> {
524
    if (!this.isReady()) return 0;
×
525
    try {
×
526
      return this._client.HSET(
×
527
        RedisCache.getNamespaceKey(namespace, key),
528
        field,
529
        value,
530
      );
531
    } catch (e) {
532
      this._log.error('An error occurred while getting hash', e as Error);
×
533
    }
534
    return 0;
×
535
  }
536

537
  public async addToSet(
538
    namespace: CacheNamespaces,
539
    key: string,
540
    value: string,
541
  ): Promise<number> {
542
    if (!this.isReady()) return 0;
×
543
    try {
×
544
      return this._client.SADD(
×
545
        RedisCache.getNamespaceKey(namespace, key),
546
        value,
547
      );
548
    } catch (e) {
549
      this._log.error('An error occurred while adding item to set', e as Error);
×
550
    }
551
    return 0;
×
552
  }
553

554
  public async getSet(
555
    namespace: CacheNamespaces,
556
    key: string,
557
  ): Promise<string[]> {
558
    if (!this.isReady()) return [];
×
559
    try {
×
560
      return this._client.SMEMBERS(RedisCache.getNamespaceKey(namespace, key));
×
561
    } catch (e) {
562
      this._log.error('An error occurred while getting set', e as Error);
×
563
    }
564
    return [];
×
565
  }
566

567
  public async getSortedSetSize(
568
    namespace: CacheNamespaces,
569
    key: string,
570
  ): Promise<number> {
571
    if (!this.isReady()) throw new Error('Redis is not ready');
×
572
    return this._client.zCard(RedisCache.getNamespaceKey(namespace, key));
×
573
  }
574

575
  public async sliceSortedSetWithScores(
576
    namespace: CacheNamespaces,
577
    key: string,
578
    start = 0,
×
579
    stop = 0,
×
580
    options?: { order?: 'ASC' | 'DESC' },
581
  ): Promise<{ value: string; score: number }[]> {
582
    if (!this.isReady()) throw new Error('Redis is not ready');
×
583
    return this._client.zRangeWithScores(
×
584
      RedisCache.getNamespaceKey(namespace, key),
585
      start,
586
      stop,
587
      {
588
        ...(options?.order === 'ASC' ? { REV: true } : {}),
×
589
      },
590
    );
591
  }
592

593
  public async sliceSortedSet(
594
    namespace: CacheNamespaces,
595
    key: string,
596
    start = 0,
×
597
    stop = 0,
×
598
    options?: { order?: 'ASC' | 'DESC' },
599
  ): Promise<string[]> {
600
    if (!this.isReady()) throw new Error('Redis is not ready');
×
601
    return this._client.zRange(
×
602
      RedisCache.getNamespaceKey(namespace, key),
603
      start,
604
      stop,
605
      {
606
        ...(options?.order === 'ASC' ? { REV: true } : {}),
×
607
      },
608
    );
609
  }
610

611
  public async delSortedSetItemsByRank(
612
    namespace: CacheNamespaces,
613
    key: string,
614
    start = 0,
×
615
    stop = 0,
×
616
  ): Promise<number> {
617
    if (!this.isReady()) throw new Error('Redis is not ready');
×
618
    return this._client.zRemRangeByRank(
×
619
      RedisCache.getNamespaceKey(namespace, key),
620
      start,
621
      stop,
622
    );
623
  }
624

625
  public async addToSortedSet(
626
    namespace: CacheNamespaces,
627
    key: string,
628
    items:
629
      | { value: string; score: number }[]
630
      | { value: string; score: number },
631
    options?: {
632
      updateOnly?: boolean;
633
    },
634
  ): Promise<number> {
635
    if (!this.isReady()) throw new Error('Redis is not ready');
×
636
    return this._client.zAdd(
×
637
      RedisCache.getNamespaceKey(namespace, key),
638
      items,
639
      {
640
        ...(options?.updateOnly ? { XX: true } : {}),
×
641
      },
642
    );
643
  }
644

645
  public async sortedSetPopMin(
646
    namespace: CacheNamespaces,
647
    key: string,
648
    numToPop = 1,
×
649
  ): Promise<{ value: string; score: number }[]> {
650
    if (!this.isReady()) throw new Error('Redis is not ready');
×
651
    return this._client.zPopMinCount(
×
652
      RedisCache.getNamespaceKey(namespace, key),
653
      numToPop,
654
    );
655
  }
656

657
  public async delSortedSetItemsByValue(
658
    namespace: CacheNamespaces,
659
    key: string,
660
    values: string[] | string,
661
  ): Promise<number> {
662
    if (!this.isReady()) throw new Error('Redis is not ready');
×
663
    return this._client.zRem(
×
664
      RedisCache.getNamespaceKey(namespace, key),
665
      values,
666
    );
667
  }
668

669
  public async flushAll(): Promise<void> {
670
    if (!this.isReady()) return;
×
671
    try {
×
672
      await this._client.flushAll();
×
673
    } catch (e) {
674
      this._log.error('An error occurred while flushing redis', e as Error);
×
675
    }
676
  }
677

678
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
679
  public async sendCommand(args: string[]): Promise<any> {
680
    return await this._client.sendCommand(args);
×
681
  }
682

683
  /**
684
   * Push a value to the beginning of a list and trim it to a specific size in a single transaction.
685
   * This is an atomic operation that adds an item to a list and ensures the list doesn't exceed the specified max length.
686
   *
687
   * @param namespace The namespace of the key
688
   * @param key The key of the list
689
   * @param value The value to push to the list
690
   * @param maxLength The maximum length to maintain for the list
691
   * @returns The new length of the list after the push operation, or false if an error occurred
692
   */
693
  public async lpushAndTrim(
694
    namespace: CacheNamespaces,
695
    key: string,
696
    value: string,
697
    maxLength: number,
698
  ): Promise<number | false> {
699
    if (!this.isReady()) return false;
8!
700
    try {
8✔
701
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
8✔
702
      const multi = this._client.multi();
8✔
703
      multi.lPush(finalKey, value);
8✔
704
      multi.lTrim(finalKey, 0, maxLength - 1);
8✔
705
      const [newLength] = (await multi.exec()) as [number, unknown];
8✔
706
      return newLength;
8✔
707
    } catch (e) {
708
      const msg = `An error occurred during lpushAndTrim for key: ${key}`;
×
709
      this._log.error(msg, e as Error);
×
710
      return false;
×
711
    }
712
  }
713

714
  /**
715
   * Retrieve a range of elements from a list stored at the specified key.
716
   *
717
   * @param namespace The namespace of the key
718
   * @param key The key of the list
719
   * @param start The starting index (0-based, inclusive)
720
   * @param stop The ending index (0-based, inclusive)
721
   * @returns Array of elements in the specified range, or an empty array if the key doesn't exist or an error occurred
722
   */
723
  public async getList(
724
    namespace: CacheNamespaces,
725
    key: string,
726
    start: number = 0,
4✔
727
    stop: number = -1,
4✔
728
  ): Promise<string[]> {
729
    if (!this.isReady()) return [];
4!
730
    try {
4✔
731
      const finalKey = RedisCache.getNamespaceKey(namespace, key);
4✔
732
      return await this._client.lRange(finalKey, start, stop);
4✔
733
    } catch (e) {
734
      const msg = `An error occurred while getting list range for key: ${key}`;
×
735
      this._log.error(msg, e as Error);
×
736
      return [];
×
737
    }
738
  }
739
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc