tbl/src/game-tbl/redis.service.ts
        
Application redis service. Used by tbl game service and pub/sub channel connection with core app
| Properties | 
| 
 | 
| Methods | 
| 
 | 
| constructor(gameService: GameService, configService: ConfigService) | |||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:61 | |||||||||
| constructor 
                                    Parameters :
                                     
 | 
| Async addPlayer | ||||||||||||
| addPlayer(gameCode: string, playerInfo: RedisPlayerInfo) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:421 | ||||||||||||
|  Add player to game. Register player and sets player info
 at hset  @param gameCode {string} - game code @param playerInfo {RedisPlayerInfo} - player info @return {boolean} - success or fail 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async clearGame | ||||||||
| clearGame(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:845 | ||||||||
|  Immediately removes game data from redis including players and creator
 links to game  @param gameCode {string} - game code 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<void> | 
| Async createGame | 
| createGame(game: RedisGame, hostId: string) | 
| Defined in tbl/src/game-tbl/redis.service.ts:352 | 
| Creates new game @param game {RedisGame} - game info 
                            Returns :          Promise<RedisGame> | 
| Async decrementGameConnections | ||||||||
| decrementGameConnections(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:505 | ||||||||
| Decrement number of connections to game @param gameCode {string} - game code @return {number} - updated number of connections 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<number>
 | 
| Async gameConnections | ||||||||
| gameConnections(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:389 | ||||||||
| Number of connections to game @param gameCode {string} - game code @return {number} - number of connections 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<number>
 | 
| Async gameHostInfo | ||||||||
| gameHostInfo(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:669 | ||||||||
| Host info @param gameCode {string} - game code @return {RedisHostInfo} - game host info 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisHostInfo>
 | 
| Async gameInfo | ||||||||
| gameInfo(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:586 | ||||||||
|  Basic game info. Used if information stats/players is not required,
 because in hset  @param gameCode {string} - game code @return {RedisGame} - game info or null if game not found 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisGame>
 | 
| Async gamePlayerInfo | ||||||||||||
| gamePlayerInfo(gameCode: string, clientId: string) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:704 | ||||||||||||
| Game player info @param gameCode {string} - game code @param clientId {string} - client id @return {RedisPlayerInfo} - player info or null 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisPlayerInfo | null>
 | 
| Async gamePlayers | |||||||||||||||
| gamePlayers(gameCode: string, onlineOnly) | |||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:827 | |||||||||||||||
| All game players including disconnected and kicked @param gameCode {string} - game code @return {RedisPlayerInfo[]} - array of game players or empty array if game/players not found 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisPlayerInfo[]>
 | 
| Async gamePlayersStats | ||||||||
| gamePlayersStats(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:776 | ||||||||
| Game players stats (all players) @param gameCode {string} - game code @return {{[clientId:string]: RedisPlayerStats}} - object with {clientId} -> RedisPlayerStats data or null if game not found 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<literal type>
 | 
| Async gamePlayerStats | ||||||||||||
| gamePlayerStats(gameCode: string, clientId: string) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:743 | ||||||||||||
| Game player stats @param gameCode {string} - game code @param clientId {string} - client id @return {RedisPlayerStats} - player stats in game or null 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisPlayerStats>
 | 
| Async gameStats | ||||||||
| gameStats(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:635 | ||||||||
| Game stats @param gameCode {string} - game code @return {RedisGameStats} - game stats 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<RedisGameStats>
 | 
| Async incrementGameConnections | ||||||||
| incrementGameConnections(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:492 | ||||||||
| Increment number of connections to game @param gameCode {string} - game code @return {number} - updated number of connections 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<number>
 | 
| Async playerDisconnected | ||||||||
| playerDisconnected(clientId: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:572 | ||||||||
| Player disconnection handler Removes player from game if player's game exists @param clientId {string} - client id 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<void> | 
| publishGameSave | ||||||||
| publishGameSave(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:125 | ||||||||
| Method to publish for core event to save finished game @param gameCode {string} - redis game code 
                                Parameters :
                                
                                 
 
                            Returns :          void | 
| Async registerPlayer | ||||||||||||
| registerPlayer(gameCode: string, clientId: string) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:403 | ||||||||||||
|  Register player (ex: teacher or student) at game.
 Set  @param gameCode {string} - game code @param clientId {string} - client id @return {boolean} - success or fail 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async removePlayer | |||||||||||||||||||||||||
| removePlayer(gameCode: string, clientId: string, kick, clear) | |||||||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:520 | |||||||||||||||||||||||||
| Removes/kick player from game @param gameCode {string} - game code @param clientId {string} - client id @param kick {boolean = false} - flag to kick (block client reconnections to this game) @param clear {boolean = true} - flag to clear player data from redis @return {boolean} - success/fail result 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| rget | |||||||||||||||
| rget(key: string, unjson) | |||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:159 | |||||||||||||||
| Redis GET async helper with parsing json option @param key {string} - redis key to read as string @param unjson {boolean = false} - flag to parse resut as json and return parsed @return {string|null|any} - null on error/not existing key, string or object of key value 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<string | null | LooseObject>
 | 
| rhget | ||||||||||||||||||||
| rhget(key: string, field: string, unjson) | ||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:177 | ||||||||||||||||||||
| Redis HGET async helper with parsing json option @param key {string} - redis key to read @param field {string} - redis field to read as string @param unjson {boolean = false} - flag to parse resut as json and return parsed @return {string|null|any} - null on error/not existing key, string or object of key value 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<string | null | LooseObject>
 | 
| rhgetall | |||||||||||||||
| rhgetall(key: string, unjson) | |||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:199 | |||||||||||||||
| Redis HGETALL async helper with parsing json support @param key {string} - redis key to read @param field {string} - redis field to read as string @param unjson {boolean = false} - flag to parse resut as json and return parsed @return {object|null} - null on error/not existing key, object of key value with string or json parsed fields 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<LooseObject | null>
 | 
| rset | ||||||||||||
| rset(key: string, value: string) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:138 | ||||||||||||
| Redis SET async helper with converting to json option @param key {string} - redis key to set as string @param value {any} - redis value to set (finally - as string) @param json {boolean = false} - flag to convert value as json  @return {Promise 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async unpackGame | ||||||||
| unpackGame(gameCode: string) | ||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:225 | ||||||||
| Unpack (create) game from core @param gameCode {string} - game code @return {boolean} - success on player unpacked or exists 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisGame>
 | 
| Async unpackPlayer | ||||||||||||
| unpackPlayer(game: RedisGame, clientId: string) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:262 | ||||||||||||
| Unpack player on connection to websocket @param game {RedisGame} - game info @param clientId {string} - connected player/host clientId @return {boolean} - success on player unpacked or exists 
                                Parameters :
                                
                                 
 
                            Returns :          Promise<RedisPlayerInfo>
 | 
| Async unregisterPlayer | ||||||||||||||||||||
| unregisterPlayer(gameCode: string, clientId: string, ttl: number) | ||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:473 | ||||||||||||||||||||
|  Unregister player (student/teacher/...) from game
 Set  @param gameCode {string} - game code @param playerInfo {RedisPlayerInfo} - player info @return {boolean} - success or fail 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async updateGameInfo | ||||||||||||||||||||
| updateGameInfo(gameCode: string, info: RedisGame, noTtl) | ||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:607 | ||||||||||||||||||||
| Update game info @param gameCode {string} - game code @param info {RedisGame} - basic game info @param noTtl {boolean = false} - flag to not update game data ttl in redis @return {boolean} - success/fail result 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async updateGamePlayerInfo | ||||||||||||||||||||
| updateGamePlayerInfo(gameCode: string, playerInfo: RedisPlayerInfo, noTtl) | ||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:721 | ||||||||||||||||||||
| Update game player info @param gameCode {string} - game code @param playerInfo {RedisPlayerInfo} - client info @return {boolean} - success/fail result 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<void>
 | 
| Async updateGameStats | ||||||||||||
| updateGameStats(gameCode: string, stats: RedisGameStats) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:648 | ||||||||||||
| Update game stats @param gameCode {string} - game code @param stats {RedisGameStats} - client id @return {boolean} - success/fail result 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async updateGameTtl | ||||||||||||||||||||
| updateGameTtl(gameCode?: string, clientId?: string, ttl?: number, clientGameCode?: string) | ||||||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:799 | ||||||||||||||||||||
| Updates redis game data ttl for game or user or both @param gameCode {string?} - game code to update @param clientId {string?} - client id to upda @param ttl {number?} - custom ttl (default is taken from env.CACHE_TTL_GAME) 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<void> | 
| Async updateHostInfo | ||||||||||||
| updateHostInfo(gameCode: string, info: RedisHostInfo) | ||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:682 | ||||||||||||
| Update game host info @param gameCode {string} - game code @param info {RedisHostInfo} - host info @return {boolean} - success/fail result 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<boolean>
 | 
| Async updatePlayerStats | ||||||||||||||||
| updatePlayerStats(gameCode: string, clientId: string, stats: RedisPlayerStats) | ||||||||||||||||
| Defined in tbl/src/game-tbl/redis.service.ts:759 | ||||||||||||||||
| Updates player stats @param gameCode {string} - game code @param clientId {string} - client id @param stats {RedisPlayerStats} - player stats 
                                Parameters :
                                
                                 
 
                            Returns :      Promise<void> | 
| Private Readonly logger | 
| Default value : new Logger(RedisService.name) | 
| Defined in tbl/src/game-tbl/redis.service.ts:54 | 
| logger | 
| Private Readonly pubClient | 
| Defined in tbl/src/game-tbl/redis.service.ts:59 | 
| core channel pub client | 
| Private Readonly redisClient | 
| Defined in tbl/src/game-tbl/redis.service.ts:52 | 
| main redis reader/writer | 
| Private Readonly subClient | 
| Defined in tbl/src/game-tbl/redis.service.ts:61 | 
| core channel sub client | 
| Private Readonly ttl | 
| Default value : process.env.CACHE_TTL_TBL_GAME || -1 | 
| Defined in tbl/src/game-tbl/redis.service.ts:56 | 
| time to live for redis records | 
import { Inject, Injectable, forwardRef, Logger } from '@nestjs/common';
import { LooseObject } from '../../../shared/common/types';
import * as moment from 'moment';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import {
  RedisGame,
  RedisGameStats,
  RedisHostInfo,
  RedisPlayerInfo,
  RedisPlayerStats,
  TBLGameCreateDTO,
} from '../../../shared/interfaces/game';
import { GameService } from './game-tbl.service';
import {
  CatchAndLogError,
  ApplyDecoratorToAll,
} from '../../../shared/common/catch-and-log-error.decorator';
/*
  Redis storage doc v2:
    clientId = user.userId || user.guestId
    get:    `${gameCode}-pack`          -> game creation dto
    get:    `${gameCode}-endpack`       -> game end dto
    get:    `${clientId}-player`        -> `${gameCode}`
    get:    `${gameCode}-${clientId}`   -> player connection info
    get:    `${gameCode}-connections`   -> number of connections. use incr/decr for atomic changes
    hget:   `${gameCode}-game`
                'info'                  -> RedisGame
                'stats'                 -> RedisGameStats
                'host'                  -> RedisHostInfo
                `${clientId}`           -> RedisPlayerStats
    hget:   `${gameCode}-players`
                `${clientId}`           -> RedisPlayerInfo
*/
/**
 *  Application redis service. Used by tbl game service and pub/sub channel connection with core app
 */
@Injectable()
@ApplyDecoratorToAll(CatchAndLogError(RedisService.name), {
  syncFunc: ['publishGameSave'],
})
export class RedisService {
  /** main redis reader/writer */
  private readonly redisClient;
  /** logger */
  private readonly logger = new Logger(RedisService.name);
  /** time to live for redis records */
  private readonly ttl = process.env.CACHE_TTL_TBL_GAME || -1;
  /** core channel pub client */
  private readonly pubClient;
  /** core channel sub client */
  private readonly subClient;
  /** constructor */
  constructor(
    @Inject(forwardRef(() => GameService))
    private readonly gameService: GameService,
    private readonly configService: ConfigService,
  ) {
    this.redisClient = new Redis({
      host: configService.get('TBL_REDIS_HOST'),
      port: parseInt(configService.get('TBL_REDIS_PORT'), 10),
      db: parseInt(configService.get('TBL_REDIS_DB'), 10) || 0,
    });
    this.redisClient.on('error', (err) => {
      this.logger.error(err, 'Redis error');
    });
    this.redisClient.on('connect', () => {
      this.logger.log('Redis connected');
    });
    this.redisClient.on('reconnecting', () => {
      this.logger.log('Redis reconnecting');
    });
    this.redisClient.on('ready', () => {
      this.logger.log('Redis ready');
    });
    this.redisClient.on('end', () => {
      this.logger.log('Redis end');
    });
    this.pubClient = this.redisClient.duplicate();
    this.subClient = this.pubClient.duplicate();
    this.pubClient.on('error', (err) => {
      /* Simply catch the error, it's logged in GameModule. */
      this.logger.error(err, 'Redis pub error');
    });
    this.subClient.on('error', (err) => {
      /* Simply catch the error, it's logged in GameModule. */
      this.logger.error(err, 'Redis sub error');
    });
    this.subClient.subscribe('gameSaved');
    this.subClient.on('message', async (...data) => {
      let info = null;
      try {
        info = JSON.parse(data[1]);
      } catch (err) {
        this.logger.error(err, 'Json reading gameSaved message error');
        info = null;
      }
      if (!info || !info.redisGameCode || !info.mongoGameCode) return;
      await this.gameService.gameSavedToDB(
        info.redisGameCode,
        info.mongoGameCode,
      );
    });
  }
  /**
   *  Method to publish for core event to save finished game
   *
   *  @param gameCode {string} - redis game code
   */
  publishGameSave(gameCode: string): void {
    this.pubClient.publish('saveGame', JSON.stringify({ gameCode }));
  }
  /**
   *  Redis SET async helper with converting to json option
   *
   *  @param key {string} - redis key to set as string
   *  @param value {any} - redis value to set (finally - as string)
   *  @param json {boolean = false} - flag to convert value as json
   *
   *  @return {Promise<boolean>} - success/failed
   */
  rset(
    key: string,
    // eslint-disable-next-line
    value: string,
  ): Promise<boolean> {
    return new Promise((resolve) => {
      this.redisClient.set(key, value, (err, val) => {
        if (err) return resolve(false);
        resolve(val === 'OK');
      });
    });
  }
  /**
   *  Redis GET async helper with parsing json option
   *
   *  @param key {string} - redis key to read as string
   *  @param unjson {boolean = false} - flag to parse resut as json and return parsed
   *
   *  @return {string|null|any} - null on error/not existing key, string or object of key value
   */
  rget(key: string, unjson = false): Promise<string | null | LooseObject> {
    return new Promise((resolve) => {
      this.redisClient.get(key, (err, val) => {
        if (err) return resolve(null);
        resolve(unjson ? JSON.parse(val) : val);
      });
    });
  }
  /**
   *  Redis HGET async helper with parsing json option
   *
   *  @param key {string} - redis key to read
   *  @param field {string} - redis field to read as string
   *  @param unjson {boolean = false} - flag to parse resut as json and return parsed
   *
   *  @return {string|null|any} - null on error/not existing key, string or object of key value
   */
  rhget(
    key: string,
    field: string,
    unjson = false,
  ): Promise<string | null | LooseObject> {
    return new Promise((resolve) => {
      this.redisClient.hget(key, field, (err, val) => {
        if (err) return resolve(null);
        resolve(unjson ? JSON.parse(val) : val);
      });
    });
  }
  /**
   *  Redis HGETALL async helper with parsing json support
   *
   *  @param key {string} - redis key to read
   *  @param field {string} - redis field to read as string
   *  @param unjson {boolean = false} - flag to parse resut as json and return parsed
   *
   *  @return {object|null} - null on error/not existing key, object of key value with string or json parsed fields
   */
  rhgetall(key: string, unjson = false): Promise<LooseObject | null> {
    return new Promise((resolve) => {
      this.redisClient.hgetall(key, (err, val) => {
        if (err || !val) return resolve(null);
        if (!unjson) return resolve(val);
        try {
          Object.keys(val || {}).forEach((ke) => {
            val[ke] = JSON.parse(val[ke]);
          });
        } catch (er) {
          this.logger.error(er, 'rhgetall unjson error');
          val = null;
        }
        resolve(val);
      });
    });
  }
  // End redis helpers
  /**
   *  Unpack (create) game from core
   *
   *  @param gameCode {string} - game code
   *
   *  @return {boolean} - success on player unpacked or exists
   */
  async unpackGame(gameCode: string): Promise<RedisGame> {
    let game = await this.gameInfo(gameCode);
    if (game) return game;
    this.logger.log({ gameCode }, 'Unpack game');
    const pack: TBLGameCreateDTO = (await this.rget(
      `${gameCode}-pack`,
      true,
    )) as TBLGameCreateDTO;
    if (!pack) return null;
    const hostId: string =
      typeof pack.host === 'string' ? pack.host : pack.host.clientId;
    try {
      this.logger.log({ gameCode }, 'CREATE game');
      game = await this.createGame(pack.game, hostId);
      if (typeof pack.host === 'string') {
        this.logger.log({ gameCode }, 'CREATE REG');
        await this.registerPlayer(`${pack.game.code}`, pack.host);
        await this.updateGameTtl(gameCode, pack.host);
      } else {
        this.logger.log({ gameCode }, 'CREATE ADD');
        await this.addPlayer(`${pack.game.code}`, pack.host);
      }
    } catch (err) {
      this.logger.error({ gameCode: pack.game.code, err }, 'Unpacking game');
      game = null;
    }
    return game;
  }
  /**
   *  Unpack player on connection to websocket
   *
   *  @param game {RedisGame} - game info
   *  @param clientId {string} - connected player/host clientId
   *
   *  @return {boolean} - success on player unpacked or exists
   */
  async unpackPlayer(
    game: RedisGame,
    clientId: string,
  ): Promise<RedisPlayerInfo> {
    const exists = await this.gamePlayerInfo(game.code, clientId);
    // console.log('HIex', exists);
    if (exists) {
      if (!exists.isConnected) {
        exists.isConnected = true;
        game.players = game.players.map((p) =>
          p.clientId === clientId ? exists : p,
        );
        await this.updateGamePlayerInfo(game.code, exists);
        await this.updateGameInfo(game.code, game);
        await this.incrementGameConnections(game.code);
      }
      return exists;
    }
    const hostInfo = await this.gameHostInfo(game.code);
    // console.log('HI', hostInfo);
    if (hostInfo.clientId === clientId) {
      await this.redisClient.set(`${clientId}-player`, game.code);
      await this.redisClient.expire(`${clientId}-player`, this.ttl);
      if (!hostInfo.isConnected) {
        hostInfo.isConnected = true;
        await this.updateHostInfo(game.code, hostInfo);
        await this.incrementGameConnections(game.code);
      }
      return null;
    }
    this.logger.log({ gameCode: game.code, clientId }, 'Unpack player');
    const player = (await this.rget(
      `${game.code}-${clientId}`,
      true,
    )) as RedisPlayerInfo;
    if (!player) return null;
    let playerI = -1;
    game.players.forEach((p, pi) => {
      if (p.clientId === clientId) {
        // player = p;
        playerI = pi;
      }
    });
    let incConnections = false;
    player.isConnected = true;
    if (playerI === -1) {
      game.players.push(player);
      incConnections = true;
      // await this.incrementGameConnections(game.code);
    } else {
      this.logger.warn(
        'Connected player unpacked but exists in game.players - replacing',
      );
      if (!game.players[playerI].isConnected) {
        incConnections = true;
      }
      game.players[playerI] = player;
    }
    // if (!player || playerI === -1) return false;
    // if (player.isConnected) return true;
    // game.players[playerI].isConnected = true;
    await this.updateGameInfo(game.code, game);
    await this.redisClient.set(`${clientId}-player`, game.code);
    await this.redisClient.expire(`${clientId}-player`, this.ttl);
    await this.redisClient.hset(
      `${game.code}-players`,
      clientId,
      JSON.stringify(player),
    );
    await this.redisClient.hset(
      `${game.code}-game`,
      clientId,
      JSON.stringify({
        score: 0,
        correctAnswers: 0,
        wrongAnswers: 0,
        rounds: [],
        xp: 0,
      }),
    );
    if (incConnections) await this.incrementGameConnections(game.code);
    await this.updateGameTtl(game.code, clientId);
    return player;
  }
  /**
   *  Creates new game
   *
   *  @param game {RedisGame} - game info
   */
  async createGame(game: RedisGame, hostId: string): Promise<RedisGame> {
    game.code = `${game.code}`;
    await this.redisClient.hset(
      `${game.code}-game`,
      'info',
      JSON.stringify(game),
    );
    await this.redisClient.hset(
      `${game.code}-game`,
      'stats',
      JSON.stringify({
        started: null,
        finished: null,
        rounds: [],
      }),
    );
    await this.redisClient.hset(
      `${game.code}-game`,
      'host',
      JSON.stringify({
        clientId: hostId,
        isConnected: false,
        lastDisconnect: null,
      }),
    );
    await this.redisClient.set(`${game.code}-connections`, 0, 'EX', this.ttl);
    await this.redisClient.expire(`${game.code}-game`, this.ttl);
    return game;
  }
  /**
   *  Number of connections to game
   *
   *  @param gameCode {string} - game code
   *
   *  @return {number} - number of connections
   */
  async gameConnections(gameCode: string): Promise<number> {
    const num = (await this.rget(`${gameCode}-connections`)) as string;
    return parseInt(num, 10) || 0;
  }
  /**
   *  Register player (ex: teacher or student) at game.
   *  Set `${clientId}-player` -> gameCode relation
   *
   *  @param gameCode {string} - game code
   *  @param clientId {string} - client id
   *
   *  @return {boolean} - success or fail
   */
  async registerPlayer(gameCode: string, clientId: string): Promise<boolean> {
    if (!gameCode || !clientId) return false;
    this.logger.log({ gameCode, clientId }, 'Register player');
    await this.redisClient.set(`${clientId}-player`, gameCode);
    await this.redisClient.expire(`${clientId}-player`, this.ttl);
    await this.updateGameTtl(gameCode, clientId);
    return true;
  }
  /**
   *  Add player to game. Register player and sets player info
   *  at hset `${gameCode}` -> `${clientId} -> RedisPlayerInfo
   *
   *  @param gameCode {string} - game code
   *  @param playerInfo {RedisPlayerInfo} - player info
   *
   *  @return {boolean} - success or fail
   */
  async addPlayer(
    gameCode: string,
    playerInfo: RedisPlayerInfo,
  ): Promise<boolean> {
    const register = await this.registerPlayer(gameCode, playerInfo.clientId);
    if (!register) {
      return false;
    }
    const players = await this.gamePlayers(gameCode);
    if (
      players.some(
        (p) =>
          p.name.toLowerCase() === playerInfo.name.toLowerCase() &&
          p.clientId !== playerInfo.clientId &&
          !p.kicked,
      )
    ) {
      await this.unregisterPlayer(gameCode, playerInfo.clientId);
      return false;
    }
    playerInfo.isConnected = false;
    const jinfo = JSON.stringify(playerInfo);
    await this.redisClient.hset(
      `${gameCode}-players`,
      playerInfo.clientId,
      jinfo,
    );
    await this.redisClient.hset(
      `${gameCode}-game`,
      playerInfo.clientId,
      JSON.stringify({
        score: 0,
        correctAnswers: 0,
        wrongAnswers: 0,
        rounds: [],
        xp: 0,
      }),
    );
    await this.updateGameTtl(gameCode, playerInfo.clientId);
    return true;
  }
  /**
   *  Unregister player (student/teacher/...) from game
   *  Set `${clientId}-player` ttl to {ttl} sec.
   *  if player was unregistered by some lag - they have 30sec to reconnect back
   *
   *  @param gameCode {string} - game code
   *  @param playerInfo {RedisPlayerInfo} - player info
   *
   *  @return {boolean} - success or fail
   */
  async unregisterPlayer(
    gameCode: string,
    clientId: string,
    ttl = 30,
  ): Promise<boolean> {
    if (!clientId) return false;
    this.logger.log({ gameCode, clientId }, 'Unregister player');
    await this.redisClient.expire(`${clientId}-player`, ttl);
    await this.redisClient.expire(`${gameCode}-${clientId}`, ttl);
    return true;
  }
  /**
   *  Increment number of connections to game
   *
   *  @param gameCode {string} - game code
   *
   *  @return {number} - updated number of connections
   */
  async incrementGameConnections(gameCode: string): Promise<number> {
    await this.redisClient.incr(`${gameCode}-connections`);
    const ret = await this.gameConnections(gameCode);
    return ret;
  }
  /**
   *  Decrement number of connections to game
   *
   *  @param gameCode {string} - game code
   *
   *  @return {number} - updated number of connections
   */
  async decrementGameConnections(gameCode: string): Promise<number> {
    await this.redisClient.decr(`${gameCode}-connections`);
    const ret = await this.gameConnections(gameCode);
    return ret;
  }
  /**
   *  Removes/kick player from game
   *
   *  @param gameCode {string} - game code
   *  @param clientId {string} - client id
   *  @param kick {boolean = false} - flag to kick (block client reconnections to this game)
   *  @param clear {boolean = true} - flag to clear player data from redis
   *  @return {boolean} - success/fail result
   */
  async removePlayer(
    gameCode: string,
    clientId: string,
    kick = false,
    clear = true,
  ): Promise<boolean> {
    try {
      // don't clear player stats - player may reconnect
      this.logger.log({ gameCode, kick, clientId, clear }, 'Remove player');
      const gameInfo = await this.gameInfo(gameCode);
      if (kick) {
        gameInfo.players = gameInfo.players.map((p) => {
          if (p.clientId !== clientId || p.kicked) return p;
          return {
            ...p,
            isConnected: false,
            kicked: moment().format(),
          };
        });
      } else {
        gameInfo.players = clear
          ? gameInfo.players.filter((p) => p.clientId !== clientId || p.kicked)
          : gameInfo.players.map((p) => {
              if (p.clientId !== clientId || p.kicked) return p;
              return {
                ...p,
                isConnected: false,
              };
            });
      }
      // clear player info
      if (clear) {
        await Promise.all([
          this.redisClient.hdel(`${gameCode}-game`, clientId),
          this.redisClient.hdel(`${gameCode}-players`, clientId),
          this.redisClient.del(`${clientId}-player`),
        ]);
      }
      await this.updateGameInfo(gameCode, gameInfo);
    } catch (err) {
      this.logger.error(err, 'Removing player error');
      return false;
    }
    return true;
  }
  /**
   *  Player disconnection handler
   *  Removes player from game if player's game exists
   *
   *  @param clientId {string} - client id
   */
  async playerDisconnected(clientId: string): Promise<void> {
    const gameCode = (await this.rget(`${clientId}-player`)) as string;
    if (!gameCode) return;
    await this.removePlayer(gameCode, clientId, false, false);
  }
  /**
   *  Basic game info. Used if information stats/players is not required,
   *  because in hset `${gameCode}-game` -> 'info' players data is not stored (or stored without guaranty to be updated) while game is running. Final full game info is merged from all game-related redis keys/fields on game saving to mongo.
   *
   *  @param gameCode {string} - game code
   *
   *  @return {RedisGame} - game info or null if game not found
   */
  async gameInfo(gameCode: string): Promise<RedisGame> {
    const ret = (await this.rhget(
      `${gameCode}-game`,
      'info',
      true,
    )) as RedisGame;
    if (!ret) return null;
    ret.code = `${ret.code}`;
    const stats = await this.gameStats(gameCode);
    return { ...ret, gameRounds: [...stats.rounds] };
  }
  /**
   *  Update game info
   *
   *  @param gameCode {string} - game code
   *  @param info {RedisGame} - basic game info
   *  @param noTtl {boolean = false} - flag to not update game data ttl in redis
   *
   *  @return {boolean} - success/fail result
   */
  async updateGameInfo(
    gameCode: string,
    info: RedisGame,
    noTtl = false,
  ): Promise<boolean> {
    try {
      await this.redisClient.hset(
        `${info.code}-game`,
        'info',
        JSON.stringify(info),
      );
      if (!noTtl && !info.finished) {
        await this.updateGameTtl(gameCode);
      }
      return true;
    } catch (err) {
      this.logger.error({ err, gameCode }, 'Update game info error');
      return false;
    }
  }
  /**
   *  Game stats
   *
   *  @param gameCode {string} - game code
   *
   *  @return {RedisGameStats} - game stats
   */
  async gameStats(gameCode: string): Promise<RedisGameStats> {
    const info = await this.rhget(`${gameCode}-game`, 'stats', true);
    return info as RedisGameStats;
  }
  /**
   *  Update game stats
   *
   *  @param gameCode {string} - game code
   *  @param stats {RedisGameStats} - client id
   *
   *  @return {boolean} - success/fail result
   */
  async updateGameStats(
    gameCode: string,
    stats: RedisGameStats,
  ): Promise<boolean> {
    if (!stats) return false;
    await this.redisClient.hset(
      `${gameCode}-game`,
      'stats',
      JSON.stringify(stats),
    );
    await this.updateGameTtl(gameCode);
    return true;
  }
  /**
   *  Host info
   *
   *  @param gameCode {string} - game code
   *
   *  @return {RedisHostInfo} - game host info
   */
  async gameHostInfo(gameCode: string): Promise<RedisHostInfo> {
    const info = await this.rhget(`${gameCode}-game`, 'host', true);
    return info as RedisHostInfo;
  }
  /**
   *  Update game host info
   *
   *  @param gameCode {string} - game code
   *  @param info {RedisHostInfo} - host info
   *
   *  @return {boolean} - success/fail result
   */
  async updateHostInfo(
    gameCode: string,
    info: RedisHostInfo,
  ): Promise<boolean> {
    if (!info) return false;
    await this.redisClient.hset(
      `${gameCode}-game`,
      'host',
      JSON.stringify(info),
    );
    await this.updateGameTtl(gameCode);
    return true;
  }
  /**
   *  Game player info
   *
   *  @param gameCode {string} - game code
   *  @param clientId {string} - client id
   *
   *  @return {RedisPlayerInfo} - player info or null
   */
  async gamePlayerInfo(
    gameCode: string,
    clientId: string,
  ): Promise<RedisPlayerInfo | null> {
    const info = await this.rhget(`${gameCode}-players`, clientId);
    if (typeof info === 'string') return JSON.parse(info);
    return null;
  }
  /**
   *  Update game player info
   *
   *  @param gameCode {string} - game code
   *  @param playerInfo {RedisPlayerInfo} - client info
   *
   *  @return {boolean} - success/fail result
   */
  async updateGamePlayerInfo(
    gameCode: string,
    playerInfo: RedisPlayerInfo,
    noTtl = false,
  ): Promise<void> {
    const jinfo = JSON.stringify(playerInfo);
    await this.redisClient.hset(
      `${gameCode}-players`,
      playerInfo.clientId,
      jinfo,
    );
    if (!noTtl) await this.updateGameTtl(gameCode, playerInfo.clientId);
  }
  /**
   *  Game player stats
   *
   *  @param gameCode {string} - game code
   *  @param clientId {string} - client id
   *
   *  @return {RedisPlayerStats} - player stats in game or null
   */
  async gamePlayerStats(
    gameCode: string,
    clientId: string,
  ): Promise<RedisPlayerStats> {
    const info = await this.rhget(`${gameCode}-game`, clientId);
    if (typeof info === 'string') return JSON.parse(info);
    return null;
  }
  /**
   *  Updates player stats
   *
   *  @param gameCode {string} - game code
   *  @param clientId {string} - client id
   *  @param stats {RedisPlayerStats} - player stats
   */
  async updatePlayerStats(
    gameCode: string,
    clientId: string,
    stats: RedisPlayerStats,
  ): Promise<void> {
    const jstats = JSON.stringify(stats);
    await this.redisClient.hset(`${gameCode}-game`, clientId, jstats);
    await this.updateGameTtl(gameCode, clientId);
  }
  /**
   *  Game players stats (all players)
   *
   *  @param gameCode {string} - game code
   *
   *  @return {{[clientId:string]: RedisPlayerStats}} - object with {clientId} -> RedisPlayerStats data or null if game not found
   */
  async gamePlayersStats(
    gameCode: string,
  ): Promise<{ [cid: string]: RedisPlayerStats }> {
    const allStats = await this.rhgetall(`${gameCode}-game`, true);
    if (!allStats) return null;
    const players = await this.gamePlayers(gameCode);
    // filter from game fields, disconnected and kicked players
    return Object.keys(allStats).reduce((acc, key) => {
      if (key === 'info' || key === 'stats' || key === 'host') {
        return acc;
      }
      if (players[key] && players[key].kicked) return acc;
      return { ...acc, [key]: allStats[key] };
    }, {});
  }
  /**
   *  Updates redis game data ttl for game or user or both
   *
   *  @param gameCode {string?} - game code to update
   *  @param clientId {string?} - client id to upda
   *  @param ttl {number?} - custom ttl (default is taken from env.CACHE_TTL_GAME)
   */
  async updateGameTtl(
    gameCode?: string,
    clientId?: string,
    ttl?: number,
    clientGameCode?: string,
  ): Promise<void> {
    const nextTtl = typeof ttl === 'number' ? ttl : this.ttl;
    if (gameCode) {
      await this.redisClient.expire(`${gameCode}-pack`, nextTtl);
      await this.redisClient.expire(`${gameCode}-endpack`, nextTtl);
      await this.redisClient.expire(`${gameCode}-game`, nextTtl);
      await this.redisClient.expire(`${gameCode}-players`, nextTtl);
      await this.redisClient.expire(`${gameCode}-connections`, nextTtl);
    }
    if (clientId) {
      await this.redisClient.expire(`${clientId}-player`, nextTtl);
      const code = gameCode || clientGameCode;
      await this.redisClient.expire(`${code}-${clientId}`, nextTtl);
    }
  }
  /**
   *  All game players including disconnected and kicked
   *
   *  @param gameCode {string} - game code
   *
   *  @return {RedisPlayerInfo[]} - array of game players or empty array if game/players not found
   */
  async gamePlayers(
    gameCode: string,
    onlineOnly = false,
  ): Promise<RedisPlayerInfo[]> {
    const players = await this.rhgetall(`${gameCode}-players`, true);
    return onlineOnly
      ? Object.values(players || {}).filter(
          (player) => player && !player.kicked && player.isConnected,
        )
      : Object.values(players || {});
  }
  /**
   *  Immediately removes game data from redis including players and creator
   *  links to game `${clientId}-player` -> `${gameCode}`
   *
   *  @param gameCode {string} - game code
   */
  async clearGame(gameCode: string): Promise<void> {
    const stats: LooseObject = await this.rhgetall(`${gameCode}-game`, true);
    if (!stats) return;
    const pids = Object.keys(stats).filter(
      (key) => key !== 'info' && key !== 'stats' && key !== 'host',
    );
    // creator also
    pids.push(stats.info.userId);
    await Promise.all(
      pids.map((clientId) => {
        return this.redisClient
          .del(`${clientId}-player`)
          .then(() => this.redisClient.del(`${gameCode}-${clientId}`));
      }),
    );
    await this.redisClient.del(`${stats.info.code}-players`);
    await this.redisClient.del(`${stats.info.code}-game`);
    await this.redisClient.del(`${stats.info.code}-pack`);
    await this.redisClient.del(`${stats.info.code}-endpack`);
  }
}