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:423
|
||||||||||||
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:860
|
||||||||
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:353
|
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:508
|
||||||||
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:390
|
||||||||
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:683
|
||||||||
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:595
|
||||||||
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:718
|
||||||||||||
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:842
|
|||||||||||||||
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:790
|
||||||||
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:757
|
||||||||||||
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:649
|
||||||||
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:495
|
||||||||
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:581
|
||||||||
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:405
|
||||||||||||
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:523
|
|||||||||||||||||||||||||
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:476
|
||||||||||||||||||||
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:616
|
||||||||||||||||||||
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:735
|
||||||||||||||||||||
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:662
|
||||||||||||
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:813
|
||||||||||||||||||||
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:696
|
||||||||||||
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:773
|
||||||||||||||||
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('redis error', err);
});
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('pub error', err);
});
this.subClient.on('error', (err) => {
/* Simply catch the error, it's logged in GameModule. */
this.logger.error('sub error', err);
});
this.subClient.subscribe('gameSaved');
this.subClient.on('message', async (...data) => {
let info = null;
try {
info = JSON.parse(data[1]);
} catch (err) {
this.logger.error(`json reading gameSaved message error ${err}`);
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('rhgetall unjson error', er);
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 = 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(`${pack.game.code} unpacking game error ${err}`);
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(`${game.code} unpack player ${clientId}`);
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.error(
'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,
boosterScore: 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;
// this.logger.log(`${gameCode} has ${num} connections`);
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,
boosterScore: 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} unregister player ${clientId}`);
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 ? 'kick' : 'remove'} ${clientId} clear ${clear}`,
);
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) {
const errStr =
err && typeof err.toString === 'function'
? err.toString()
: JSON.stringify(err);
this.logger.error(`removing player error ${errStr}`);
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) {
const str = err
? typeof err.toString === 'function'
? err.toString()
: JSON.stringify(err)
: 'unknown';
this.logger.error(`update game info error ${gameCode} ${str}`);
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;
// this.logger.error(`updateGameTtl <${gameCode}> ${nextTtl}`);
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`);
}
}