Adds rabbitmq as message broker for frontend and backend

parent 828cb5b0
......@@ -16,6 +16,15 @@ To send E-Mails it is required to provide additional variables:
- `ARSNOVA_CLICK_BACKEND_MAIL_FROM [string]`: The `from` header of the E-Mails
- `ARSNOVA_CLICK_BACKEND_MAIL_TO [string]`: The `to` header of the E-Mails
###### RabbitMQ
The server uses RabbitMQ to send messages to the frontend. These variables can be adjusted to connect to the RabbitMQ server:
- `AMQP_PROTOCOL [string]`: Protocol for the connection (defaults to amqp)
- `AMQP_HOSTNAME [string]`: Hostname of the RabbitMQ Server (defaults to localhost)
- `AMQP_USER [string]`: The username to use for the connection (defaults to guest)
- `AMQP_PASSWORD [string]`: The username to use for the connection (defaults to guest)
As mentioned in the RabbitMQ installation guideline, the user should not be an management user!
###### Dumps
The server will generate dumps if an Error is thrown.
The dump will contain the serialized error and the state of the DAOs.
......
import { Channel, connect, Connection } from 'amqplib';
import { settings } from '../statistics';
class AMQPConnector {
private static _instance: AMQPConnector;
private _channel: Channel;
get channel(): Channel {
return this._channel;
}
private _connection: Connection;
constructor() {
}
public static getInstance(): AMQPConnector {
if (!this._instance) {
this._instance = new AMQPConnector();
}
return this._instance;
}
public async initConnection(): Promise<void> {
this._connection = await connect({
protocol: settings.amqp.protocol,
hostname: settings.amqp.hostname,
username: settings.amqp.user,
password: settings.amqp.password,
});
this._channel = await this._connection.createChannel();
}
}
export default AMQPConnector.getInstance();
......@@ -2,7 +2,6 @@ import { ObjectId } from 'bson';
import { MemberEntity } from '../entities/member/MemberEntity';
import { QuizEntity } from '../entities/quiz/QuizEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IMemberEntity } from '../interfaces/entities/Member/IMemberEntity';
import { IMemberSerialized } from '../interfaces/entities/Member/IMemberSerialized';
import { IQuizEntity } from '../interfaces/quizzes/IQuizEntity';
import { AbstractDAO } from './AbstractDAO';
......@@ -83,7 +82,7 @@ class MemberDAO extends AbstractDAO<Array<MemberEntity>> {
}
}
public getMembersOfQuiz(quizName: string): Array<IMemberEntity> {
public getMembersOfQuiz(quizName: string): Array<MemberEntity> {
return this.storage.filter(val => !!val.currentQuizName.match(new RegExp(`^${RegExp.escape(quizName)}$`, 'i')));
}
......
......@@ -2,6 +2,7 @@ import * as mongoose from 'mongoose';
import { Connection } from 'mongoose';
import { Database } from '../enums/DbOperation';
import LoggerService from '../services/LoggerService';
import AMQPConnector from './AMQPConnector';
class MongoDbConnector {
get dbName(): string {
......@@ -31,6 +32,10 @@ class MongoDbConnector {
resolve(db);
});
AMQPConnector.initConnection().then(() => {
AMQPConnector.channel.assertExchange('global', 'fanout');
});
await mongoose.connect(this._mongoURL, {
useCreateIndex: true,
autoIndex: true,
......
import { ObjectId } from 'bson';
import WebSocket from 'ws';
import { MemberGroupEntity } from '../../entities/member/MemberGroupEntity';
import { getQuestionForType } from '../../entities/question/QuizValidator';
import { QuizEntity } from '../../entities/quiz/QuizEntity';
......@@ -11,6 +10,7 @@ import { IQuizEntity, IQuizSerialized } from '../../interfaces/quizzes/IQuizEnti
import { generateToken } from '../../lib/generateToken';
import { setPath } from '../../lib/resolveNestedObjectProperty';
import { AbstractDAO } from '../AbstractDAO';
import AMQPConnector from '../AMQPConnector';
import DbDAO from '../DbDAO';
import MemberDAO from '../MemberDAO';
......@@ -55,7 +55,7 @@ class QuizDAO extends AbstractDAO<Array<IQuizEntity>> {
public removeQuiz(id: ObjectId): void {
const removedQuiz = this.storage.splice(this.storage.findIndex(val => val.id.equals(id)), 1);
removedQuiz[0].onRemove();
removedQuiz[0].state = 0;
removedQuiz[0].state = QuizState.Inactive;
MemberDAO.removeMembersOfQuiz(removedQuiz[0]);
}
......@@ -151,16 +151,13 @@ class QuizDAO extends AbstractDAO<Array<IQuizEntity>> {
}
}
public joinableQuizzesUpdated(): void {
this.updateEmitter.emit(DbEvent.Change, this.getJoinableQuizzes());
}
public async addQuiz(quizDoc: IQuizSerialized): Promise<IQuizEntity> {
if (this.getQuizByName(quizDoc.name)) {
throw new Error(`Duplicate quiz insertion: ${quizDoc.name}`);
}
const entity = new QuizEntity(quizDoc);
await AMQPConnector.channel.assertExchange(`quiz_${encodeURI(entity.name)}`, 'fanout');
this.storage.push(entity);
return entity;
}
......@@ -233,10 +230,6 @@ class QuizDAO extends AbstractDAO<Array<IQuizEntity>> {
return this.getActiveQuizzes().find(val => !!val.name.match(new RegExp(`^${RegExp.escape(quizName)}$`, 'i')));
}
public getQuizBySocket(ws: WebSocket): IQuizEntity {
return this.storage.find(quiz => quiz.containsSocket(ws));
}
public getQuizByToken(token: string): IQuizEntity {
return this.storage.find(quiz => quiz.privateKey === token);
}
......
import { ObjectId } from 'bson';
import { DeleteWriteOpResultObject } from 'mongodb';
import * as WebSocket from 'ws';
import AMQPConnector from '../../db/AMQPConnector';
import DbDAO from '../../db/DbDAO';
import MemberDAO from '../../db/MemberDAO';
import { DbCollection } from '../../enums/DbOperation';
......@@ -10,7 +10,6 @@ import { QuizState } from '../../enums/QuizState';
import { QuizVisibility } from '../../enums/QuizVisibility';
import { IQuizEntity, IQuizSerialized } from '../../interfaces/quizzes/IQuizEntity';
import { ISessionConfigurationEntity } from '../../interfaces/session_configuration/ISessionConfigurationEntity';
import { SendSocketMessageService } from '../../services/SendSocketMessageService';
import { AbstractEntity } from '../AbstractEntity';
import { MemberEntity } from '../member/MemberEntity';
import { MemberGroupEntity } from '../member/MemberGroupEntity';
......@@ -142,7 +141,8 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
private _dropEmptyQuizTimeout: any;
private _quizTimerInterval: any;
private _quizTimer: number;
private _socketChannel: Array<WebSocket> = [];
private readonly _exchangeName: string;
constructor(quiz: IQuizSerialized) {
super();
......@@ -159,87 +159,52 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
this._readingConfirmationRequested = !!quiz.readingConfirmationRequested;
this._visibility = quiz.visibility;
this._description = quiz.description;
this._exchangeName = encodeURI(`quiz_${quiz.name}`);
}
public onMemberAdded(member: MemberEntity): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
public async onMemberAdded(member: MemberEntity): Promise<void> {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Added,
payload: { member: member.serialize() },
}));
})));
}
public onMemberRemoved(member: MemberEntity): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
public async onMemberRemoved(member: MemberEntity): Promise<void> {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Removed,
payload: { name: member.name },
}));
})));
}
public onRemove(): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
MemberDAO.getMembersOfQuiz(this.name).forEach(member => {
AMQPConnector.channel.deleteQueue(encodeURI(`${member.currentQuizName}_${member.name}`));
});
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Closed,
}));
})));
}
public reset(): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Reset,
}));
})));
clearTimeout(this._quizTimerInterval);
}
public stop(): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Stop,
}));
})));
this.currentStartTimestamp = -1;
clearTimeout(this._quizTimerInterval);
}
public addSocketToChannel(socket: WebSocket): void {
if (this._socketChannel.find(value => value === socket)) {
console.error(`Cannot add socket to quiz channel ${this.name} since it is already added`);
return;
}
console.log(`Adding socket to quiz channel ${this.name}`);
this._socketChannel.push(socket);
clearTimeout(this._dropEmptyQuizTimeout);
this._dropEmptyQuizTimeout = null;
}
public removeSocketFromChannel(socket: WebSocket): void {
const index = this._socketChannel.findIndex(value => value === socket);
if (index === -1) {
console.log(`Cannot remove socket from quiz channel ${this.name} since it is not found`);
return;
}
console.log(`Removing socket from quiz channel ${this.name}`);
this._socketChannel.splice(index, 1);
if (!this._socketChannel.length) {
if (this._dropEmptyQuizTimeout !== null) {
clearTimeout(this._dropEmptyQuizTimeout);
}
this._dropEmptyQuizTimeout = setTimeout(() => {
if (!this._socketChannel.length) {
DbDAO.updateOne(DbCollection.Quizzes, { _id: this.id }, { state: QuizState.Inactive });
DbDAO.deleteMany(DbCollection.Members, { currentQuizName: this.name });
}
}, 300000); // 5 minutes
}
}
public containsSocket(socket: WebSocket): boolean {
return !!this._socketChannel.find(value => value === socket);
}
public addQuestion(question: AbstractQuestionEntity, index: number = -1): void {
if (index === -1 || index >= this.questionList.length) {
this.questionList.push(question);
......@@ -306,13 +271,13 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
DbDAO.updateOne(DbCollection.Quizzes, { _id: this.id }, { currentQuestionIndex: nextIndex });
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.NextQuestion,
payload: {
nextQuestionIndex: nextIndex,
},
}));
})));
return nextIndex;
}
......@@ -322,11 +287,11 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
}
public startNextQuestion(): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Start,
payload: {},
}));
})));
this._quizTimer = this._questionList[this._currentQuestionIndex].timer;
if (this._quizTimer <= 0) {
......@@ -338,13 +303,13 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
}
this._quizTimerInterval = setInterval(() => {
this._quizTimer--;
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Countdown,
payload: {
value: this._quizTimer,
},
}));
})));
if (this._quizTimer <= 0) {
clearInterval(this._quizTimerInterval);
......@@ -356,19 +321,20 @@ export class QuizEntity extends AbstractEntity implements IQuizEntity {
public requestReadingConfirmation(): void {
this._readingConfirmationRequested = true;
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.ReadingConfirmationRequested,
payload: {},
}));
})));
}
public updatedMemberResponse(payload: object): void {
this._socketChannel.forEach(socket => SendSocketMessageService.sendMessage(socket, {
AMQPConnector.channel.publish(this._exchangeName, '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.UpdatedResponse,
payload,
}));
})));
if (MemberDAO.getMembersOfQuiz(this.name).every(nick => {
const val = nick.responses[this.currentQuestionIndex].value;
return typeof val === 'number' ? val > -1 : val.length > 0;
......
export enum QuizState {
Inactive, //
Active, //
Running, //
Finished, //
Inactive = 'Inactive', //
Active = 'Active', //
Running = 'Running', //
Finished = 'Finished', //
}
import { ObjectId } from 'bson';
import { DeleteWriteOpResultObject } from 'mongodb';
import WebSocket from 'ws';
import { MemberEntity } from '../../entities/member/MemberEntity';
import { AbstractQuestionEntity } from '../../entities/question/AbstractQuestionEntity';
import { QuizState } from '../../enums/QuizState';
......@@ -30,12 +29,6 @@ export interface IQuizEntity extends IQuizBase {
addQuestion(question: AbstractQuestionEntity, index: number): void;
addSocketToChannel(socket: WebSocket): void;
removeSocketFromChannel(socket: WebSocket): void;
containsSocket(socket: WebSocket): boolean;
updatedMemberResponse(payload: object): void;
startNextQuestion(): void;
......@@ -46,9 +39,9 @@ export interface IQuizEntity extends IQuizBase {
onRemove(): void;
onMemberAdded(member: MemberEntity): void;
onMemberAdded(member: MemberEntity): Promise<void>;
onMemberRemoved(memberEntity: MemberEntity): void;
onMemberRemoved(memberEntity: MemberEntity): Promise<void>;
}
export interface IQuizSerialized extends IQuizBase {
......
......@@ -7,7 +7,6 @@ import * as Minimist from 'minimist';
import * as path from 'path';
import * as process from 'process';
import 'reflect-metadata';
import * as WebSocket from 'ws';
import App from './App';
import AssetDAO from './db/AssetDAO';
import CasDAO from './db/CasDAO';
......@@ -19,7 +18,6 @@ import QuizDAO from './db/quiz/QuizDAO';
import UserDAO from './db/UserDAO';
import { jsonCensor } from './lib/jsonCensor';
import { rejectionToCreateDump } from './lib/rejectionToCreateDump';
import { WebSocketRouter } from './routers/websocket/WebSocketRouter';
import LoggerService from './services/LoggerService';
import { staticStatistics } from './statistics';
import { LoadTester } from './tests/LoadTester';
......@@ -174,8 +172,6 @@ function onListening(): void {
const bind: string = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
LoggerService.info(`Listening on ${bind}`);
WebSocketRouter.wss = new WebSocket.Server({ server });
I18nDAO.reloadCache().catch(reason => {
console.error('Could not reload i18n dao cache', reason);
});
......@@ -193,6 +189,4 @@ function runTest(): void {
});
}
function onClose(): void {
WebSocketRouter.wss.close();
}
function onClose(): void {}
import { index, prop, Typegoose } from 'typegoose';
import { arrayProp, index, prop, Typegoose } from 'typegoose';
import DbDAO from '../../db/DbDAO';
import UserDAO from '../../db/UserDAO';
import { DbCollection, DbEvent, DbWatchStreamOperation } from '../../enums/DbOperation';
......@@ -10,7 +10,10 @@ export class UserModelItem extends Typegoose implements IUserSerialized {
@prop({ required: true }) public name: string;
@prop({ required: false }) public passwordHash: string;
@prop({ required: false }) public tokenHash: string;
@prop({ required: true }) public userAuthorizations: Array<string>;
@arrayProp({
required: true,
items: String,
}) public userAuthorizations: Array<string>;
@prop({ required: true }) public privateKey: string;
@prop() public gitlabToken?: string;
@prop() public token?: string;
......
import { index, prop, Typegoose } from 'typegoose';
import { arrayProp, index, prop, Typegoose } from 'typegoose';
import DbDAO from '../../db/DbDAO';
import MemberDAO from '../../db/MemberDAO';
import { DbCollection, DbEvent, DbWatchStreamOperation } from '../../enums/DbOperation';
......@@ -14,7 +14,7 @@ export class MemberModelItem extends Typegoose implements IMemberSerialized {
@prop() public colorCode: string;
@prop({ required: false }) public groupName: string;
@prop() public name: string;
@prop() public responses: Array<IQuizResponse>;
@arrayProp({ items: Object }) public responses: Array<IQuizResponse>;
@prop({ required: false }) public ticket: string;
@prop() public token: string;
@prop() public currentQuizName: string;
......
......@@ -7,6 +7,7 @@ import * as path from 'path';
import { Get, getMetadataArgsStorage, JsonController, NotFoundError, Param, Res } from 'routing-controllers';
import { OpenAPI, routingControllersToSpec } from 'routing-controllers-openapi';
import { routingControllerOptions } from '../../App';
import QuizDAO from '../../db/quiz/QuizDAO';
import { settings, staticStatistics } from '../../statistics';
import { AbstractRouter } from './AbstractRouter';
......@@ -47,6 +48,7 @@ export class ApiRouter extends AbstractRouter {
private getAll(): object {
return {
serverConfig: settings.public,
activeQuizzes: QuizDAO.getJoinableQuizzes().map(quiz => quiz.name),
};
}
......
import { BodyParam, Delete, Get, JsonController, Param, Put } from 'routing-controllers';
import AMQPConnector from '../../db/AMQPConnector';
import DbDAO from '../../db/DbDAO';
import QuizDAO from '../../db/quiz/QuizDAO';
import { DbCollection } from '../../enums/DbOperation';
......@@ -6,7 +7,6 @@ import { MessageProtocol, StatusProtocol } from '../../enums/Message';
import { QuizState } from '../../enums/QuizState';
import { IQuizSerialized } from '../../interfaces/quizzes/IQuizEntity';
import { QuizModel } from '../../models/quiz/QuizModelItem';
import { WebSocketRouter } from '../websocket/WebSocketRouter';
import { AbstractRouter } from './AbstractRouter';
@JsonController('/api/v1/lobby')
......@@ -18,14 +18,13 @@ export class LobbyRouter extends AbstractRouter {
@BodyParam('privateKey') privateKey: string, //
): Promise<object> {
const messageToWSSClients = JSON.stringify({
AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.SetActive,
payload: {
quizName: quiz.name,
},
});
WebSocketRouter.wss.clients.forEach(client => client.send(messageToWSSClients));
})));
quiz.state = QuizState.Active;
quiz.currentQuestionIndex = -1;
......@@ -73,6 +72,14 @@ export class LobbyRouter extends AbstractRouter {
DbDAO.updateOne(DbCollection.Quizzes, { _id: addedQuiz.id }, { state: QuizState.Inactive });
}
AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.SetInactive,
payload: {
quizName,
},
})));
return {
status: StatusProtocol.Success,
step: MessageProtocol.Closed,
......
......@@ -20,6 +20,7 @@ import {
UnauthorizedError,
UploadedFiles,
} from 'routing-controllers';
import AMQPConnector from '../../db/AMQPConnector';
import { default as DbDAO } from '../../db/DbDAO';
import MemberDAO from '../../db/MemberDAO';
import QuizDAO from '../../db/quiz/QuizDAO';
......@@ -471,6 +472,14 @@ export class QuizRouter extends AbstractRouter {
throw result;
}
AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: quiz.state === QuizState.Active ? MessageProtocol.SetActive : MessageProtocol.SetInactive,
payload: {
quizName: quiz.name,
},
})));
const existingQuiz = QuizDAO.getQuizByName(quiz.name);
if (existingQuiz) {
if (existingQuiz.privateKey !== privateKey) {
......@@ -564,6 +573,14 @@ export class QuizRouter extends AbstractRouter {
DbDAO.updateOne(DbCollection.Quizzes, { _id: quiz.id }, { state: QuizState.Inactive });
DbDAO.deleteMany(DbCollection.Members, { currentQuizName: quiz.name });
AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.SetInactive,
payload: {
quizName,
},
})));
return {
status: StatusProtocol.Success,
step: MessageProtocol.Closed,
......
import * as WebSocket from 'ws';
import MemberDAO from '../../db/MemberDAO';
import QuizDAO from '../../db/quiz/QuizDAO';
import { DbEvent } from '../../enums/DbOperation';
import { MessageProtocol, StatusProtocol } from '../../enums/Message';
import { WebSocketStatus } from '../../enums/WebSocketStatus';
import { IMessage } from '../../interfaces/communication/IMessage';
import { IQuizEntity } from '../../interfaces/quizzes/IQuizEntity';
import { IGlobal } from '../../main';
import LoggerService from '../../services/LoggerService';
export class WebSocketRouter {
private static _wss: WebSocket.Server;
static get wss(): WebSocket.Server {
return this._wss;
}
static set wss(value: WebSocket.Server) {
this._wss = value;
WebSocketRouter.init();
}
public static sendLobbyPlayerData(ws: WebSocket, message): void {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return;
}
const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(message.payload.quizName);
const res: any = { status: StatusProtocol.Success };
if (!activeQuiz) {
res.step = MessageProtocol.Inactive;
} else {
res.step = MessageProtocol.AllPlayers;
res.payload = {
members: MemberDAO.getMembersOfQuiz(activeQuiz.name).map(nickname => nickname.serialize()),
};
}
ws.send(JSON.stringify(res));
}
private static getWebSocketOpcode(match): string {
const matchedValues = Object.values(WebSocketStatus).filter(value => WebSocketStatus[value] === match) as string[];
return matchedValues.length ? matchedValues[0] : '[UNKNOWN]';
}
private static onPing(ws): void {
ws.pong();
ws['isAlive'] = true;
}
private static onPong(ws): void {
ws['isAlive'] = true;
}
private static keepalive(): void {
setInterval(() => {
this.wss.clients.forEach(socket => {
if (socket.readyState !== WebSocket.OPEN) {
return;
}
if (socket['isAlive']) {
socket['isAlive'] = false;
socket.ping();
} else {
socket.close(WebSocketStatus.PolicyViolation);
}
});
}, 30000);
}
private static init(): void {
this.keepalive();
WebSocketRouter._wss.on('connection', (ws: WebSocket) => {
const quizStatusUpdateHandler = () => {
WebSocketRouter.sendQuizStatusUpdate(ws, MessageProtocol.Connected, { activeQuizzes: QuizDAO.getJoinableQuizzes().map(val => val.name) });
};
const quizSessionUpdateHandler = () => {
const quiz = QuizDAO.getQuizBySocket(ws);
if (!quiz) {
console.error('Cannot publish session update to the socket. Could not find an attached quiz');
return;
}
WebSocketRouter.sendQuizStatusUpdate(ws, MessageProtocol.UpdatedSettings, { sessionConfig: quiz.sessionConfig.serialize() });
};
ws['isAlive'] = true;
ws.on('close', opcode => {
this.disconnectFromChannel(ws);
QuizDAO.updateEmitter.off(DbEvent.StateChanged, quizStatusUpdateHandler);
QuizDAO.updateEmitter.off(DbEvent.SessionConfigChanged, quizSessionUpdateHandler);
LoggerService.info('Closing socket connection', opcode, `(${this.getWebSocketOpcode(opcode)})`);
});
ws.on('ping', this.onPing.bind(this, ws));
ws.on('pong', this.onPong.bind(this, ws));
ws.on('error', (err) => {
WebSocketRouter.handleError(ws, err, '');
});
ws.on('message', (rawMessage: string | any) => {
try {
const message: IMessage = JSON.parse(rawMessage);
switch (message.step) {
case MessageProtocol.Connect:
WebSocketRouter.connectToChannel(ws, message);
break;
case MessageProtocol.Disconnect:
WebSocketRouter.disconnectFromChannel(ws);