Webatrice websocket refactor (#4435)

* add unit tests for websocket events

* add unit tests for KeepAliveService, clean up keepAlive termination flow

* put keepAlive command in protobuf service and expose thru webClient

* secure wss

* rename files tsx to ts

* add localhost support for ws/wss connection

Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
Jeremy Letto 2021-10-17 19:52:59 -05:00 committed by GitHub
parent f75ff2a7c8
commit 586f23cfa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 568 additions and 77 deletions

View file

@ -69,14 +69,13 @@ export class WebClient {
SessionPersistence.updateStatus(status, description); SessionPersistence.updateStatus(status, description);
if (status === StatusEnum.DISCONNECTED) { if (status === StatusEnum.DISCONNECTED) {
this.resetConnectionvars(); this.protobuf.resetCommands();
this.clearStores(); this.clearStores();
} }
} }
public resetConnectionvars() { public keepAlive(pingReceived: Function) {
this.protobuf.resetCommands(); this.protobuf.sendKeepAliveCommand(pingReceived);
this.socket.keepAliveService.resetPingFlag();
} }
private clearStores() { private clearStores() {

View file

@ -2,7 +2,7 @@ import { RoomPersistence } from '../persistence';
import webClient from "../WebClient"; import webClient from "../WebClient";
export class RoomCommands { export class RoomCommands {
static roomSay(roomId: number, message: string) { static roomSay(roomId: number, message: string): void {
const trimmed = message.trim(); const trimmed = message.trim();
if (!trimmed) return; if (!trimmed) return;
@ -18,7 +18,7 @@ export class RoomCommands {
webClient.protobuf.sendRoomCommand(roomId, rc); webClient.protobuf.sendRoomCommand(roomId, rc);
} }
static leaveRoom(roomId: number) { static leaveRoom(roomId: number): void {
var CmdLeaveRoom = webClient.protobuf.controller.Command_LeaveRoom.create(); var CmdLeaveRoom = webClient.protobuf.controller.Command_LeaveRoom.create();
var rc = webClient.protobuf.controller.RoomCommand.create({ var rc = webClient.protobuf.controller.RoomCommand.create({

View file

@ -6,17 +6,17 @@ import webClient from '../WebClient';
import { guid } from '../utils'; import { guid } from '../utils';
export class SessionCommands { export class SessionCommands {
static connect(options: ServerConnectParams) { static connect(options: ServerConnectParams): void {
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
webClient.connect(options); webClient.connect(options);
} }
static disconnect() { static disconnect(): void {
SessionCommands.updateStatus(StatusEnum.DISCONNECTING, 'Disconnecting...'); SessionCommands.updateStatus(StatusEnum.DISCONNECTING, 'Disconnecting...');
webClient.disconnect(); webClient.disconnect();
} }
static login() { static login(): void {
const loginConfig = { const loginConfig = {
...webClient.clientConfig, ...webClient.clientConfig,
userName: webClient.options.user, userName: webClient.options.user,
@ -86,7 +86,7 @@ export class SessionCommands {
}); });
} }
static listUsers() { static listUsers(): void {
const CmdListUsers = webClient.protobuf.controller.Command_ListUsers.create(); const CmdListUsers = webClient.protobuf.controller.Command_ListUsers.create();
const sc = webClient.protobuf.controller.SessionCommand.create({ const sc = webClient.protobuf.controller.SessionCommand.create({
@ -110,7 +110,7 @@ export class SessionCommands {
}); });
} }
static listRooms() { static listRooms(): void {
const CmdListRooms = webClient.protobuf.controller.Command_ListRooms.create(); const CmdListRooms = webClient.protobuf.controller.Command_ListRooms.create();
const sc = webClient.protobuf.controller.SessionCommand.create({ const sc = webClient.protobuf.controller.SessionCommand.create({
@ -120,7 +120,7 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc); webClient.protobuf.sendSessionCommand(sc);
} }
static joinRoom(roomId: number) { static joinRoom(roomId: number): void {
const CmdJoinRoom = webClient.protobuf.controller.Command_JoinRoom.create({ roomId }); const CmdJoinRoom = webClient.protobuf.controller.Command_JoinRoom.create({ roomId });
const sc = webClient.protobuf.controller.SessionCommand.create({ const sc = webClient.protobuf.controller.SessionCommand.create({
@ -158,23 +158,23 @@ export class SessionCommands {
}); });
} }
static addToBuddyList(userName: string) { static addToBuddyList(userName: string): void {
this.addToList('buddy', userName); this.addToList('buddy', userName);
} }
static removeFromBuddyList(userName: string) { static removeFromBuddyList(userName: string): void {
this.removeFromList('buddy', userName); this.removeFromList('buddy', userName);
} }
static addToIgnoreList(userName: string) { static addToIgnoreList(userName: string): void {
this.addToList('ignore', userName); this.addToList('ignore', userName);
} }
static removeFromIgnoreList(userName: string) { static removeFromIgnoreList(userName: string): void {
this.removeFromList('ignore', userName); this.removeFromList('ignore', userName);
} }
static addToList(list: string, userName: string) { static addToList(list: string, userName: string): void {
const CmdAddToList = webClient.protobuf.controller.Command_AddToList.create({ list, userName }); const CmdAddToList = webClient.protobuf.controller.Command_AddToList.create({ list, userName });
const sc = webClient.protobuf.controller.SessionCommand.create({ const sc = webClient.protobuf.controller.SessionCommand.create({
@ -186,7 +186,7 @@ export class SessionCommands {
}); });
} }
static removeFromList(list: string, userName: string) { static removeFromList(list: string, userName: string): void {
const CmdRemoveFromList = webClient.protobuf.controller.Command_RemoveFromList.create({ list, userName }); const CmdRemoveFromList = webClient.protobuf.controller.Command_RemoveFromList.create({ list, userName });
const sc = webClient.protobuf.controller.SessionCommand.create({ const sc = webClient.protobuf.controller.SessionCommand.create({
@ -198,7 +198,7 @@ export class SessionCommands {
}); });
} }
static viewLogHistory(filters) { static viewLogHistory(filters): void {
const CmdViewLogHistory = webClient.protobuf.controller.Command_ViewLogHistory.create(filters); const CmdViewLogHistory = webClient.protobuf.controller.Command_ViewLogHistory.create(filters);
const sc = webClient.protobuf.controller.ModeratorCommand.create({ const sc = webClient.protobuf.controller.ModeratorCommand.create({
@ -226,7 +226,7 @@ export class SessionCommands {
}); });
} }
static updateStatus(status: StatusEnum, description: string) { static updateStatus(status: StatusEnum, description: string): void {
webClient.updateStatus(status, description); webClient.updateStatus(status, description);
} }
} }

View file

@ -0,0 +1,64 @@
import { Message } from 'types';
import {
RoomEvents,
RoomEvent,
JoinRoomData,
LeaveRoomData,
ListGamesData,
} from './RoomEvents';
import { RoomPersistence } from '../persistence/RoomPersistence';
describe('RoomEvents', () => {
it('.Event_JoinRoom.ext should call RoomPersistence.userJoined', () => {
spyOn(RoomPersistence, 'userJoined');
const data: JoinRoomData = { userInfo: {} as any };
const event: RoomEvent = { roomEvent: { roomId: 1 } };
RoomEvents['.Event_JoinRoom.ext'](data, event);
expect(RoomPersistence.userJoined).toHaveBeenCalledWith(
event.roomEvent.roomId,
data.userInfo
);
});
it('.Event_LeaveRoom.ext should call RoomPersistence.userLeft', () => {
spyOn(RoomPersistence, 'userLeft');
const data: LeaveRoomData = { name: '' };
const event: RoomEvent = { roomEvent: { roomId: 1 } };
RoomEvents['.Event_LeaveRoom.ext'](data, event);
expect(RoomPersistence.userLeft).toHaveBeenCalledWith(
event.roomEvent.roomId,
data.name
);
});
it('.Event_ListGames.ext should call RoomPersistence.updateGames', () => {
spyOn(RoomPersistence, 'updateGames');
const data: ListGamesData = { gameList: [] };
const event: RoomEvent = { roomEvent: { roomId: 1 } };
RoomEvents['.Event_ListGames.ext'](data, event);
expect(RoomPersistence.updateGames).toHaveBeenCalledWith(
event.roomEvent.roomId,
data.gameList
);
});
it('.Event_RoomSay.ext should call RoomPersistence.addMessage', () => {
spyOn(RoomPersistence, 'addMessage');
const data: Message = {} as any;
const event: RoomEvent = { roomEvent: { roomId: 1 } };
RoomEvents['.Event_RoomSay.ext'](data, event);
expect(RoomPersistence.addMessage).toHaveBeenCalledWith(
event.roomEvent.roomId,
data
);
});
});

View file

@ -30,20 +30,20 @@ function roomSay(message: Message, { roomEvent }: RoomEvent) {
RoomPersistence.addMessage(roomId, message); RoomPersistence.addMessage(roomId, message);
} }
interface RoomEvent { export interface RoomEvent {
roomEvent: { roomEvent: {
roomId: number; roomId: number;
} }
} }
interface JoinRoomData { export interface JoinRoomData {
userInfo: User; userInfo: User;
} }
interface LeaveRoomData { export interface LeaveRoomData {
name: string; name: string;
} }
interface ListGamesData { export interface ListGamesData {
gameList: Game[]; gameList: Game[];
} }

View file

@ -0,0 +1,355 @@
import { StatusEnum } from "types";
import {
SessionEvents,
SessionEvent,
AddToListData,
ConnectionClosedData,
ListRoomsData,
RemoveFromListData,
ServerIdentificationData,
ServerMessageData,
UserJoinedData,
UserLeftData,
} from './SessionEvents';
import { SessionCommands } from "../commands";
import { RoomPersistence, SessionPersistence } from '../persistence';
import webClient from '../WebClient';
describe('SessionEvents', () => {
const roomId = 1;
beforeEach(() => {
spyOn(SessionCommands, 'updateStatus');
});
describe('.Event_AddToList.ext', () => {
it('should call SessionPersistence.addToBuddyList if buddy listName', () => {
spyOn(SessionPersistence, 'addToBuddyList');
const data: AddToListData = { listName: 'buddy', userInfo: {} as any };
SessionEvents['.Event_AddToList.ext'](data);
expect(SessionPersistence.addToBuddyList).toHaveBeenCalledWith(
data.userInfo
);
});
it('should call SessionPersistence.addToIgnoreList if ignore listName', () => {
spyOn(SessionPersistence, 'addToIgnoreList');
const data: AddToListData = { listName: 'ignore', userInfo: {} as any };
SessionEvents['.Event_AddToList.ext'](data);
expect(SessionPersistence.addToIgnoreList).toHaveBeenCalledWith(
data.userInfo
);
});
it('should call console.log if unknown listName', () => {
spyOn(console, 'log');
const data: AddToListData = { listName: 'unknown', userInfo: {} as any };
SessionEvents['.Event_AddToList.ext'](data);
expect(console.log).toHaveBeenCalledWith(
`Attempted to add to unknown list: ${data.listName}`
);
});
});
describe('.Event_ConnectionClosed.ext', () => {
describe('with reasonStr', () => {
it('should call SessionCommands.updateStatus', () => {
const data: ConnectionClosedData = { endTime: 0, reason: 0, reasonStr: 'reasonStr' };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
data.reasonStr
);
});
});
describe('without reasonStr', () => {
beforeEach(() => {
webClient.protobuf.controller.Event_ConnectionClosed = { CloseReason: {} };
});
describe('USER_LIMIT_REACHED', () => {
it('should call SessionCommands.updateStatus', () => {
const USER_LIMIT_REACHED = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED = USER_LIMIT_REACHED;
const data: ConnectionClosedData = { endTime: 0, reason: USER_LIMIT_REACHED, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'The server has reached its maximum user capacity'
);
});
});
describe('TOO_MANY_CONNECTIONS', () => {
it('should call SessionCommands.updateStatus', () => {
const TOO_MANY_CONNECTIONS = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS = TOO_MANY_CONNECTIONS;
const data: ConnectionClosedData = { endTime: 0, reason: TOO_MANY_CONNECTIONS, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'There are too many concurrent connections from your address'
);
});
});
describe('BANNED', () => {
it('should call SessionCommands.updateStatus', () => {
const BANNED = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.BANNED = BANNED;
const data: ConnectionClosedData = { endTime: 0, reason: BANNED, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'You are banned'
);
});
});
describe('DEMOTED', () => {
it('should call SessionCommands.updateStatus', () => {
const DEMOTED = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.DEMOTED = DEMOTED;
const data: ConnectionClosedData = { endTime: 0, reason: DEMOTED, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'You were demoted'
);
});
});
describe('SERVER_SHUTDOWN', () => {
it('should call SessionCommands.updateStatus', () => {
const SERVER_SHUTDOWN = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN = SERVER_SHUTDOWN;
const data: ConnectionClosedData = { endTime: 0, reason: SERVER_SHUTDOWN, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'Scheduled server shutdown'
);
});
});
describe('USERNAMEINVALID', () => {
it('should call SessionCommands.updateStatus', () => {
const USERNAMEINVALID = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USERNAMEINVALID = USERNAMEINVALID;
const data: ConnectionClosedData = { endTime: 0, reason: USERNAMEINVALID, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'Invalid username'
);
});
});
describe('LOGGEDINELSEWERE', () => {
it('should call SessionCommands.updateStatus', () => {
const LOGGEDINELSEWERE = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE = LOGGEDINELSEWERE;
const data: ConnectionClosedData = { endTime: 0, reason: LOGGEDINELSEWERE, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'You have been logged out due to logging in at another location'
);
});
});
describe('OTHER', () => {
it('should call SessionCommands.updateStatus', () => {
const OTHER = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.OTHER = OTHER;
const data: ConnectionClosedData = { endTime: 0, reason: OTHER, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'Unknown reason'
);
});
});
describe('UNKNOWN', () => {
it('should call SessionCommands.updateStatus', () => {
const UNKNOWN = 1;
webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.UNKNOWN = UNKNOWN;
const data: ConnectionClosedData = { endTime: 0, reason: UNKNOWN, reasonStr: null };
SessionEvents['.Event_ConnectionClosed.ext'](data);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(
StatusEnum.DISCONNECTED,
'Unknown reason'
);
});
});
});
});
describe('.Event_ListRooms.ext', () => {
beforeEach(() => {
webClient.options.autojoinrooms = false;
spyOn(RoomPersistence, 'updateRooms');
});
it('should call RoomPersistence.updateRooms', () => {
const data: ListRoomsData = { roomList: [{ roomId, autoJoin: false } as any] };
SessionEvents['.Event_ListRooms.ext'](data);
expect(RoomPersistence.updateRooms).toHaveBeenCalledWith(data.roomList);
});
it('should call SessionCommands.joinRoom if webClient and room is configured for autojoin', () => {
webClient.options.autojoinrooms = true;
spyOn(SessionCommands, 'joinRoom');
const data: ListRoomsData = { roomList: [{ roomId, autoJoin: true } as any, { roomId: 2, autoJoin: false } as any] };
SessionEvents['.Event_ListRooms.ext'](data);
expect(SessionCommands.joinRoom).toHaveBeenCalledTimes(1);
expect(SessionCommands.joinRoom).toHaveBeenCalledWith(data.roomList[0].roomId);
});
});
describe('.Event_RemoveFromList.ext', () => {
it('should call SessionPersistence.removeFromBuddyList if buddy listName', () => {
spyOn(SessionPersistence, 'removeFromBuddyList');
const data: RemoveFromListData = { listName: 'buddy', userName: '' };
SessionEvents['.Event_RemoveFromList.ext'](data);
expect(SessionPersistence.removeFromBuddyList).toHaveBeenCalledWith(
data.userName
);
});
it('should call SessionPersistence.removeFromIgnoreList if ignore listName', () => {
spyOn(SessionPersistence, 'removeFromIgnoreList');
const data: RemoveFromListData = { listName: 'ignore', userName: '' };
SessionEvents['.Event_RemoveFromList.ext'](data);
expect(SessionPersistence.removeFromIgnoreList).toHaveBeenCalledWith(
data.userName
);
});
it('should call console.log if unknown listName', () => {
spyOn(console, 'log');
const data: RemoveFromListData = { listName: 'unknown', userName: '' };
SessionEvents['.Event_RemoveFromList.ext'](data);
expect(console.log).toHaveBeenCalledWith(
`Attempted to remove from unknown list: ${data.listName}`
);
});
});
describe('.Event_ServerIdentification.ext', () => {
it('update status/info and login', () => {
spyOn(SessionPersistence, 'updateInfo');
spyOn(SessionCommands, 'login');
webClient.protocolVersion = 0;
const data: ServerIdentificationData = {
serverName: 'serverName',
serverVersion: 'serverVersion',
protocolVersion: 0,
};
SessionEvents['.Event_ServerIdentification.ext'](data);
expect(SessionPersistence.updateInfo).toHaveBeenCalledWith(data.serverName, data.serverVersion);
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGINGIN, 'Logging in...');
expect(SessionCommands.login).toHaveBeenCalled();
});
it('should disconnect if protocolVersion mismatched', () => {
spyOn(SessionCommands, 'login');
spyOn(SessionCommands, 'disconnect');
webClient.protocolVersion = 0;
const data: ServerIdentificationData = {
serverName: '',
serverVersion: '',
protocolVersion: 1,
};
SessionEvents['.Event_ServerIdentification.ext'](data);
expect(SessionCommands.disconnect).toHaveBeenCalled();
expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${data.protocolVersion}`);
expect(SessionCommands.login).not.toHaveBeenCalled();
});
});
describe('.Event_ServerMessage.ext', () => {
it('should call SessionPersistence.serverMessage', () => {
spyOn(SessionPersistence, 'serverMessage');
const data: ServerMessageData = { message: 'message' };
SessionEvents['.Event_ServerMessage.ext'](data);
expect(SessionPersistence.serverMessage).toHaveBeenCalledWith(
data.message
);
});
});
describe('.Event_UserJoined.ext', () => {
it('should call SessionPersistence.userJoined', () => {
spyOn(SessionPersistence, 'userJoined');
const data: UserJoinedData = { userInfo: {} as any };
SessionEvents['.Event_UserJoined.ext'](data);
expect(SessionPersistence.userJoined).toHaveBeenCalledWith(
data.userInfo
);
});
});
describe('.Event_UserLeft.ext', () => {
it('should call SessionPersistence.userLeft', () => {
spyOn(SessionPersistence, 'userLeft');
const data: UserLeftData = { name: '' };
SessionEvents['.Event_UserLeft.ext'](data);
expect(SessionPersistence.userLeft).toHaveBeenCalledWith(
data.name
);
});
});
});

View file

@ -1,23 +1,23 @@
import { Room, StatusEnum, User } from "types"; import { Room, StatusEnum, User } from 'types';
import { SessionCommands } from "../commands"; import { SessionCommands } from '../commands';
import { RoomPersistence, SessionPersistence } from '../persistence'; import { RoomPersistence, SessionPersistence } from '../persistence';
import { ProtobufEvents } from '../services/ProtobufService'; import { ProtobufEvents } from '../services/ProtobufService';
import webClient from '../WebClient'; import webClient from '../WebClient';
export const SessionEvents: ProtobufEvents = { export const SessionEvents: ProtobufEvents = {
".Event_AddToList.ext": addToList, '.Event_AddToList.ext': addToList,
".Event_ConnectionClosed.ext": connectionClosed, '.Event_ConnectionClosed.ext': connectionClosed,
".Event_ListRooms.ext": listRooms, '.Event_ListRooms.ext': listRooms,
".Event_NotifyUser.ext": notifyUser, '.Event_NotifyUser.ext': notifyUser,
".Event_PlayerPropertiesChanges.ext": playerPropertiesChanges, '.Event_PlayerPropertiesChanges.ext': playerPropertiesChanges,
".Event_RemoveFromList.ext": removeFromList, '.Event_RemoveFromList.ext': removeFromList,
".Event_ServerIdentification.ext": serverIdentification, '.Event_ServerIdentification.ext': serverIdentification,
".Event_ServerMessage.ext": serverMessage, '.Event_ServerMessage.ext': serverMessage,
".Event_ServerShutdown.ext": serverShutdown, '.Event_ServerShutdown.ext': serverShutdown,
".Event_UserJoined.ext": userJoined, '.Event_UserJoined.ext': userJoined,
".Event_UserLeft.ext": userLeft, '.Event_UserLeft.ext': userLeft,
".Event_UserMessage.ext": userMessage, '.Event_UserMessage.ext': userMessage,
} }
function addToList({ listName, userInfo}: AddToListData) { function addToList({ listName, userInfo}: AddToListData) {
@ -31,13 +31,13 @@ function addToList({ listName, userInfo}: AddToListData) {
break; break;
} }
default: { default: {
console.log('Attempted to add to unknown list: ', listName); console.log(`Attempted to add to unknown list: ${listName}`);
} }
} }
} }
function connectionClosed({ reason, reasonStr }: ConnectionClosedData) { function connectionClosed({ reason, reasonStr }: ConnectionClosedData) {
let message = ""; let message = '';
// @TODO (5) // @TODO (5)
if (reasonStr) { if (reasonStr) {
@ -45,29 +45,29 @@ function connectionClosed({ reason, reasonStr }: ConnectionClosedData) {
} else { } else {
switch(reason) { switch(reason) {
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED:
message = "The server has reached its maximum user capacity"; message = 'The server has reached its maximum user capacity';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS:
message = "There are too many concurrent connections from your address"; message = 'There are too many concurrent connections from your address';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.BANNED: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.BANNED:
message = "You are banned"; message = 'You are banned';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.DEMOTED: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.DEMOTED:
message = "You were demoted"; message = 'You were demoted';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN:
message = "Scheduled server shutdown"; message = 'Scheduled server shutdown';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USERNAMEINVALID: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USERNAMEINVALID:
message = "Invalid username"; message = 'Invalid username';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE:
message = "You have been logged out due to logging in at another location"; message = 'You have been logged out due to logging in at another location';
break; break;
case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.OTHER: case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.OTHER:
default: default:
message = "Unknown reason"; message = 'Unknown reason';
break; break;
} }
} }
@ -88,11 +88,11 @@ function listRooms({ roomList }: ListRoomsData) {
} }
function notifyUser(payload) { function notifyUser(payload) {
// console.info("Event_NotifyUser", payload); // console.info('Event_NotifyUser', payload);
} }
function playerPropertiesChanges(payload) { function playerPropertiesChanges(payload) {
// console.info("Event_PlayerPropertiesChanges", payload); // console.info('Event_PlayerPropertiesChanges', payload);
} }
function removeFromList({ listName, userName }: RemoveFromListData) { function removeFromList({ listName, userName }: RemoveFromListData) {
@ -106,7 +106,7 @@ function removeFromList({ listName, userName }: RemoveFromListData) {
break; break;
} }
default: { default: {
console.log('Attempted to remove from unknown list: ', listName); console.log(`Attempted to remove from unknown list: ${listName}`);
} }
} }
} }
@ -116,13 +116,12 @@ function serverIdentification(info: ServerIdentificationData) {
if (protocolVersion !== webClient.protocolVersion) { if (protocolVersion !== webClient.protocolVersion) {
SessionCommands.disconnect(); SessionCommands.disconnect();
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, "Protocol version mismatch: " + protocolVersion); SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
return; return;
} }
webClient.resetConnectionvars();
SessionPersistence.updateInfo(serverName, serverVersion); SessionPersistence.updateInfo(serverName, serverVersion);
SessionCommands.updateStatus(StatusEnum.LOGGINGIN, "Logging in..."); SessionCommands.updateStatus(StatusEnum.LOGGINGIN, 'Logging in...');
SessionCommands.login(); SessionCommands.login();
} }
@ -131,7 +130,7 @@ function serverMessage({ message }: ServerMessageData) {
} }
function serverShutdown(payload) { function serverShutdown(payload) {
// console.info("Event_ServerShutdown", payload); // console.info('Event_ServerShutdown', payload);
} }
function userJoined({ userInfo }: UserJoinedData) { function userJoined({ userInfo }: UserJoinedData) {
@ -143,47 +142,47 @@ function userLeft({ name }: UserLeftData) {
} }
function userMessage(payload) { function userMessage(payload) {
// console.info("Event_UserMessage", payload); // console.info('Event_UserMessage', payload);
} }
interface SessionEvent { export interface SessionEvent {
sessionEvent: {} sessionEvent: {}
} }
interface AddToListData { export interface AddToListData {
listName: string; listName: string;
userInfo: User; userInfo: User;
} }
interface ConnectionClosedData { export interface ConnectionClosedData {
endTime: number; endTime: number;
reason: number; reason: number;
reasonStr: string; reasonStr: string;
} }
interface ListRoomsData { export interface ListRoomsData {
roomList: Room[]; roomList: Room[];
} }
interface RemoveFromListData { export interface RemoveFromListData {
listName: string; listName: string;
userName: string; userName: string;
} }
interface ServerIdentificationData { export interface ServerIdentificationData {
protocolVersion: number; protocolVersion: number;
serverName: string; serverName: string;
serverVersion: string; serverVersion: string;
} }
interface ServerMessageData { export interface ServerMessageData {
message: string; message: string;
} }
interface UserJoinedData { export interface UserJoinedData {
userInfo: User; userInfo: User;
} }
interface UserLeftData { export interface UserLeftData {
name: string; name: string;
} }

View file

@ -0,0 +1,71 @@
import { KeepAliveService } from './KeepAliveService';
import { WebSocketService } from "./WebSocketService";
import webClient from '../WebClient';
describe('KeepAliveService', () => {
let service: KeepAliveService;
let socket: WebSocketService;
beforeEach(() => {
jest.useFakeTimers();
socket = new WebSocketService(webClient);
service = new KeepAliveService(socket);
});
it('should create', () => {
expect(service).toBeDefined();
});
describe('startPingLoop', () => {
let resolvePing;
let interval;
let promise;
let ping;
let checkReadyStateSpy;
beforeEach(() => {
interval = 100;
promise = new Promise(resolve => resolvePing = resolve);
ping = (done) => promise.then(done);
checkReadyStateSpy = spyOn(socket, 'checkReadyState');
checkReadyStateSpy.and.returnValue(true);
service.startPingLoop(interval, ping);
jest.advanceTimersByTime(interval);
});
it('should start ping loop', () => {
expect((service as any).keepalivecb).toBeDefined();
expect((service as any).lastPingPending).toBeTruthy();
});
it('should call ping callback when done', (done: jest.DoneCallback) => {
resolvePing();
promise.then(() => {
expect((service as any).lastPingPending).toBeFalsy();
done();
});
});
it('should fire disconnected$ if lastPingPending is still true', () => {
spyOn(service.disconnected$, 'next');
jest.advanceTimersByTime(interval);
expect(service.disconnected$.next).toHaveBeenCalled();
});
it('should endPingLoop if socket is not open', () => {
spyOn(service, 'endPingLoop');
checkReadyStateSpy.and.returnValue(false);
resolvePing();
jest.advanceTimersByTime(interval);
expect(service.endPingLoop).toHaveBeenCalled();
});
});
});

View file

@ -35,9 +35,6 @@ export class KeepAliveService {
public endPingLoop() { public endPingLoop() {
clearInterval(this.keepalivecb); clearInterval(this.keepalivecb);
this.keepalivecb = null; this.keepalivecb = null;
}
public resetPingFlag() {
this.lastPingPending = false; this.lastPingPending = false;
} }
} }

View file

@ -65,6 +65,14 @@ export class ProtobufService {
} }
} }
public sendKeepAliveCommand(pingReceived: Function) {
const command = this.controller.SessionCommand.create({
".Command_Ping.ext" : this.controller.Command_Ping.create()
});
this.sendSessionCommand(command, pingReceived);
}
public handleMessageEvent({ data }: MessageEvent): void { public handleMessageEvent({ data }: MessageEvent): void {
try { try {
const uint8msg = new Uint8Array(data); const uint8msg = new Uint8Array(data);

View file

@ -17,7 +17,7 @@ export interface WebSocketOptions {
export class WebSocketService { export class WebSocketService {
private socket: WebSocket; private socket: WebSocket;
private webClient: WebClient; private webClient: WebClient;
public keepAliveService: KeepAliveService; private keepAliveService: KeepAliveService;
public message$: Subject<MessageEvent> = new Subject(); public message$: Subject<MessageEvent> = new Subject();
public statusChange$: Subject<ServerStatus> = new Subject(); public statusChange$: Subject<ServerStatus> = new Subject();
@ -36,6 +36,10 @@ export class WebSocketService {
} }
public connect(options: WebSocketOptions, protocol: string = 'wss'): void { public connect(options: WebSocketOptions, protocol: string = 'wss'): void {
if (window.location.hostname === 'localhost') {
protocol = 'ws';
}
const { host, port, keepalive } = options; const { host, port, keepalive } = options;
this.keepalive = keepalive; this.keepalive = keepalive;
@ -49,7 +53,7 @@ export class WebSocketService {
} }
public checkReadyState(state: number): boolean { public checkReadyState(state: number): boolean {
return this.socket.readyState === state; return this.socket?.readyState === state;
} }
public send(message): void { public send(message): void {
@ -69,13 +73,7 @@ export class WebSocketService {
this.updateStatus(StatusEnum.CONNECTED, "Connected"); this.updateStatus(StatusEnum.CONNECTED, "Connected");
this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: Function) => { this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: Function) => {
const command = this.webClient.protobuf.controller.SessionCommand.create({ this.webClient.keepAlive(pingReceived);
".Command_Ping.ext" : this.webClient.protobuf.controller.Command_Ping.create()
});
this.webClient.protobuf.sendSessionCommand(command, () => {
pingReceived();
});
}); });
}; };