From 2ed732fe7bb9c803aed56e9a3d6610bd3bf72a6c Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 4 Sep 2019 15:04:48 +0200 Subject: [PATCH 01/17] Add password check and error messages --- src/network/server.ts | 31 ++++++++++++++++++++++++++++++- src/network/types.ts | 7 ++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/network/server.ts b/src/network/server.ts index 4f2709e..0bb1886 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -1,6 +1,12 @@ import NetworkPeer from "./peer"; import { DataConnection } from "peerjs"; -import { RoomInfo, PasswordRequest, Room } from "./types"; +import { + RoomInfo, + PasswordRequest, + Room, + ErrorMessage, + PasswordResponse +} from "./types"; export default class PeerServer extends NetworkPeer { private room: Room; @@ -19,15 +25,38 @@ export default class PeerServer extends NetworkPeer { // Check if room is full if (this.playerCount >= this.room.info.max_players) { //TODO Reject + this.send(conn, { kind: "error", error: "room is full" }); + conn.close(); + return; } if (this.room.info.password != "") { this.send(conn, { kind: "password-req" }); + conn.on("data", this.checkPasswordResponse.bind(this, conn)); } else { //TODO Add player } } + private checkPasswordResponse(conn: DataConnection, data: any) { + try { + let resp = data as PasswordResponse; + if (resp.password != this.room.info.password) { + this.send(conn, { + kind: "error", + error: "invalid password" + }); + return; + } + //TODO Add player + } catch (e) { + this.send(conn, { + kind: "error", + error: "not a password" + }); + } + } + private get playerCount(): number { return Object.keys(this.room.players).length; } diff --git a/src/network/types.ts b/src/network/types.ts index 7ee6881..414656d 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -46,7 +46,7 @@ type DraftInfo = { // Message schemas -export type NetworkMessage = PasswordRequest | PasswordResponse; +export type NetworkMessage = PasswordRequest | PasswordResponse | ErrorMessage; export interface PasswordRequest { kind: "password-req"; @@ -56,3 +56,8 @@ export interface PasswordResponse { kind: "password-resp"; password: string; } + +export interface ErrorMessage { + kind: "error"; + error: string; +} -- 2.40.1 From 34ab5d4a10ac2dc5766aabfb0f191c4c7828c101 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 4 Sep 2019 15:34:53 +0200 Subject: [PATCH 02/17] Handle players joining --- src/network/client.ts | 3 ++- src/network/server.ts | 52 ++++++++++++++++++++++++++++++++++++++----- src/network/types.ts | 22 +++++++++++++++++- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/network/client.ts b/src/network/client.ts index 243a3bb..1b98418 100644 --- a/src/network/client.ts +++ b/src/network/client.ts @@ -1,9 +1,10 @@ import NetworkPeer from "./peer"; import { DataConnection } from "peerjs"; +import { PeerMetadata } from "./types"; export default class PeerClient extends NetworkPeer { private connection: DataConnection; - public constructor(peerid: string, metadata: Object) { + public constructor(peerid: string, metadata: PeerMetadata) { super(); this.connection = this.peer.connect(peerid, { label: "server", diff --git a/src/network/server.ts b/src/network/server.ts index 0bb1886..5a2d707 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -5,11 +5,15 @@ import { PasswordRequest, Room, ErrorMessage, - PasswordResponse + PasswordResponse, + PeerMetadata, + JoinMessage, + RoomInfoMessage } from "./types"; export default class PeerServer extends NetworkPeer { private room: Room; + public constructor(roomInfo: RoomInfo) { super(); this.room = { @@ -18,13 +22,17 @@ export default class PeerServer extends NetworkPeer { }; this.peer.on("connection", this._connection); } + private async _connection(conn: DataConnection) { // Check if this connection should be allowed - console.info("%s (%s) connected!", conn.metadata.name, conn.label); + console.info( + "%s (%s) connected!", + (conn.metadata as PeerMetadata).name, + conn.label + ); // Check if room is full if (this.playerCount >= this.room.info.max_players) { - //TODO Reject this.send(conn, { kind: "error", error: "room is full" }); conn.close(); return; @@ -33,9 +41,10 @@ export default class PeerServer extends NetworkPeer { if (this.room.info.password != "") { this.send(conn, { kind: "password-req" }); conn.on("data", this.checkPasswordResponse.bind(this, conn)); - } else { - //TODO Add player + return; } + + this.addPlayer(conn); } private checkPasswordResponse(conn: DataConnection, data: any) { @@ -48,7 +57,7 @@ export default class PeerServer extends NetworkPeer { }); return; } - //TODO Add player + this.addPlayer(conn); } catch (e) { this.send(conn, { kind: "error", @@ -57,7 +66,38 @@ export default class PeerServer extends NetworkPeer { } } + private addPlayer(conn: DataConnection) { + const playerName = conn.metadata.name; + this.room.players[playerName] = { + name: conn.metadata.name, + conn: conn + }; + + // Send the player info about the room + this.send(conn, { + kind: "room-info", + room: { + ...this.room.info, + password: "" + }, + players: Object.keys(this.room.players) + }); + + // Notify other players + this.broadcast({ + kind: "player-joined", + name: conn.metadata.name + }); + } + private get playerCount(): number { return Object.keys(this.room.players).length; } + + private broadcast(message: T) { + for (const playerName in this.room.players) { + const player = this.room.players[playerName]; + this.send(player.conn, message); + } + } } diff --git a/src/network/types.ts b/src/network/types.ts index 414656d..f055f87 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -5,6 +5,10 @@ export interface NetworkPlayer { conn: DataConnection; } +export interface PeerMetadata { + name: string; +} + export interface Room { info: RoomInfo; players: Record; @@ -46,7 +50,12 @@ type DraftInfo = { // Message schemas -export type NetworkMessage = PasswordRequest | PasswordResponse | ErrorMessage; +export type NetworkMessage = + | PasswordRequest + | PasswordResponse + | ErrorMessage + | RoomInfoMessage + | JoinMessage; export interface PasswordRequest { kind: "password-req"; @@ -57,6 +66,17 @@ export interface PasswordResponse { password: string; } +export interface RoomInfoMessage { + kind: "room-info"; + room: RoomInfo; + players: string[]; +} + +export interface JoinMessage { + kind: "player-joined"; + name: string; +} + export interface ErrorMessage { kind: "error"; error: string; -- 2.40.1 From c164a70fc1d32964fda3e88cb85d68c45043b268 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 4 Sep 2019 16:17:21 +0200 Subject: [PATCH 03/17] Start adding shim for local player instance on servers --- src/network/server.ts | 11 +++++++++-- src/network/types.ts | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/network/server.ts b/src/network/server.ts index 5a2d707..92aa7b0 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -18,7 +18,9 @@ export default class PeerServer extends NetworkPeer { super(); this.room = { info: roomInfo, - players: {} + players: { + //TODO Add local player + } }; this.peer.on("connection", this._connection); } @@ -69,6 +71,7 @@ export default class PeerServer extends NetworkPeer { private addPlayer(conn: DataConnection) { const playerName = conn.metadata.name; this.room.players[playerName] = { + kind: "remote", name: conn.metadata.name, conn: conn }; @@ -97,7 +100,11 @@ export default class PeerServer extends NetworkPeer { private broadcast(message: T) { for (const playerName in this.room.players) { const player = this.room.players[playerName]; - this.send(player.conn, message); + if (player.kind == "remote") { + this.send(player.conn, message); + } else { + //TODO Local wrapper + } } } } diff --git a/src/network/types.ts b/src/network/types.ts index f055f87..24edf57 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -1,6 +1,12 @@ import { DataConnection } from "peerjs"; +export interface LocalPlayer { + kind: "local"; + name: string; +} + export interface NetworkPlayer { + kind: "remote"; name: string; conn: DataConnection; } @@ -11,7 +17,7 @@ export interface PeerMetadata { export interface Room { info: RoomInfo; - players: Record; + players: Record; } export interface RoomInfo { -- 2.40.1 From 005290d057dc18b6141af160b77d76fa07193d81 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 4 Sep 2019 17:57:19 +0200 Subject: [PATCH 04/17] Add received handlers, force rename on name conflicts --- src/network/client.ts | 8 +++- src/network/local.ts | 19 ++++++++ src/network/peer.ts | 3 +- src/network/server.ts | 108 +++++++++++++++++++++++++++++------------- src/network/types.ts | 15 +++++- 5 files changed, 116 insertions(+), 37 deletions(-) create mode 100644 src/network/local.ts diff --git a/src/network/client.ts b/src/network/client.ts index 1b98418..50ace8a 100644 --- a/src/network/client.ts +++ b/src/network/client.ts @@ -1,6 +1,6 @@ import NetworkPeer from "./peer"; import { DataConnection } from "peerjs"; -import { PeerMetadata } from "./types"; +import { PeerMetadata, NetworkMessage } from "./types"; export default class PeerClient extends NetworkPeer { private connection: DataConnection; @@ -11,5 +11,11 @@ export default class PeerClient extends NetworkPeer { metadata, reliable: true }); + this.connection.on("open", () => { + console.info("Connected to server"); + }); + this.connection.on("data", this._received); } + + private _received(data: NetworkMessage) {} } diff --git a/src/network/local.ts b/src/network/local.ts new file mode 100644 index 0000000..4ee071a --- /dev/null +++ b/src/network/local.ts @@ -0,0 +1,19 @@ +import NetworkPeer from "./peer"; +import { DataConnection } from "peerjs"; +import { PeerMetadata, NetworkMessage } from "./types"; + +export default class LocalClient { + public metadata: PeerMetadata; + + public constructor(metadata: PeerMetadata) { + this.metadata = metadata; + } + + public receive(data: NetworkMessage) { + //TODO + } + + public send(data: NetworkMessage) { + //TODO + } +} diff --git a/src/network/peer.ts b/src/network/peer.ts index d4b8db3..7658fca 100644 --- a/src/network/peer.ts +++ b/src/network/peer.ts @@ -1,4 +1,5 @@ import Peer, { DataConnection } from "peerjs"; +import { NetworkMessage } from "./types"; export default class NetworkPeer { protected peer: Peer; @@ -9,7 +10,7 @@ export default class NetworkPeer { }); } - protected send(conn: DataConnection, data: T) { + protected send(conn: DataConnection, data: T) { //TODO Debugging support? conn.send(data); } diff --git a/src/network/server.ts b/src/network/server.ts index 92aa7b0..25e60f4 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -8,30 +8,53 @@ import { PasswordResponse, PeerMetadata, JoinMessage, - RoomInfoMessage + RoomInfoMessage, + Player, + NetworkMessage, + RenameMessage } from "./types"; +import LocalClient from "./local"; + +// Increment name, add number at the end if not present +// Examples: +// Guest -> Guest1 +// Guest1 -> Guest2 +// Guest9 -> Guest10 +function nextName(name: string): string { + let i = 1; + for (; i < name.length; i++) { + if (!isNaN(Number(name.slice(i - name.length)))) { + break; + } + } + return name.substr(0, i) + (Number(name.slice(i)) + 1); +} export default class PeerServer extends NetworkPeer { private room: Room; - public constructor(roomInfo: RoomInfo) { + public constructor(roomInfo: RoomInfo, local: LocalClient) { super(); + let players: Record = {}; + players[local.metadata.name] = { + kind: "local", + name: local.metadata.name, + client: local + }; this.room = { info: roomInfo, - players: { - //TODO Add local player - } + players }; this.peer.on("connection", this._connection); } - private async _connection(conn: DataConnection) { + private _connection(conn: DataConnection) { + const metadata = conn.metadata as PeerMetadata; + console.info("%s (%s) connected!", metadata.name, conn.label); + + // // Check if this connection should be allowed - console.info( - "%s (%s) connected!", - (conn.metadata as PeerMetadata).name, - conn.label - ); + // // Check if room is full if (this.playerCount >= this.room.info.max_players) { @@ -40,34 +63,48 @@ export default class PeerServer extends NetworkPeer { return; } + // Check if there is already a player called that way + if (this.room.players[metadata.name]) { + // Force rename + const newname = nextName(metadata.name); + this.send(conn, { + kind: "rename", + oldname: metadata.name, + newname: newname + }); + metadata.name = newname; + } + + // Check for password if (this.room.info.password != "") { + const checkPasswordResponse = (data: any) => { + try { + let resp = data as PasswordResponse; + if (resp.password != this.room.info.password) { + this.send(conn, { + kind: "error", + error: "invalid password" + }); + return; + } + conn.off("data", checkPasswordResponse); + this.addPlayer(conn); + } catch (e) { + this.send(conn, { + kind: "error", + error: "not a password" + }); + } + }; + this.send(conn, { kind: "password-req" }); - conn.on("data", this.checkPasswordResponse.bind(this, conn)); + conn.on("data", checkPasswordResponse); return; } this.addPlayer(conn); } - private checkPasswordResponse(conn: DataConnection, data: any) { - try { - let resp = data as PasswordResponse; - if (resp.password != this.room.info.password) { - this.send(conn, { - kind: "error", - error: "invalid password" - }); - return; - } - this.addPlayer(conn); - } catch (e) { - this.send(conn, { - kind: "error", - error: "not a password" - }); - } - } - private addPlayer(conn: DataConnection) { const playerName = conn.metadata.name; this.room.players[playerName] = { @@ -76,6 +113,9 @@ export default class PeerServer extends NetworkPeer { conn: conn }; + // Start listening for new messages + conn.on("data", this._received.bind(this, this.room.players[playerName])); + // Send the player info about the room this.send(conn, { kind: "room-info", @@ -93,17 +133,19 @@ export default class PeerServer extends NetworkPeer { }); } + private _received(player: Player, data: NetworkMessage) {} + private get playerCount(): number { return Object.keys(this.room.players).length; } - private broadcast(message: T) { + private broadcast(message: T) { for (const playerName in this.room.players) { const player = this.room.players[playerName]; if (player.kind == "remote") { this.send(player.conn, message); } else { - //TODO Local wrapper + player.client.receive(message); } } } diff --git a/src/network/types.ts b/src/network/types.ts index 24edf57..d8d0c47 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -1,8 +1,10 @@ import { DataConnection } from "peerjs"; +import LocalClient from "./local"; export interface LocalPlayer { kind: "local"; name: string; + client: LocalClient; } export interface NetworkPlayer { @@ -11,13 +13,15 @@ export interface NetworkPlayer { conn: DataConnection; } +export type Player = NetworkPlayer | LocalPlayer; + export interface PeerMetadata { name: string; } export interface Room { info: RoomInfo; - players: Record; + players: Record; } export interface RoomInfo { @@ -61,7 +65,8 @@ export type NetworkMessage = | PasswordResponse | ErrorMessage | RoomInfoMessage - | JoinMessage; + | JoinMessage + | RenameMessage; export interface PasswordRequest { kind: "password-req"; @@ -83,6 +88,12 @@ export interface JoinMessage { name: string; } +export interface RenameMessage { + kind: "rename"; + oldname: string; + newname: string; +} + export interface ErrorMessage { kind: "error"; error: string; -- 2.40.1 From dd279573e68f01f99b77039af76a00f9bf3762a9 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 4 Sep 2019 18:01:56 +0200 Subject: [PATCH 05/17] Set up local player send hook --- src/network/local.ts | 7 +++---- src/network/server.ts | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/network/local.ts b/src/network/local.ts index 4ee071a..980a054 100644 --- a/src/network/local.ts +++ b/src/network/local.ts @@ -1,9 +1,8 @@ -import NetworkPeer from "./peer"; -import { DataConnection } from "peerjs"; import { PeerMetadata, NetworkMessage } from "./types"; export default class LocalClient { public metadata: PeerMetadata; + public receiver!: (data: NetworkMessage) => void; public constructor(metadata: PeerMetadata) { this.metadata = metadata; @@ -13,7 +12,7 @@ export default class LocalClient { //TODO } - public send(data: NetworkMessage) { - //TODO + public send(data: T) { + this.receiver(data); } } diff --git a/src/network/server.ts b/src/network/server.ts index 25e60f4..d3aec55 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -36,11 +36,15 @@ export default class PeerServer extends NetworkPeer { public constructor(roomInfo: RoomInfo, local: LocalClient) { super(); let players: Record = {}; + + // Add local player to server players[local.metadata.name] = { kind: "local", name: local.metadata.name, client: local }; + local.receiver = this._received.bind(this, players[local.metadata.name]); + this.room = { info: roomInfo, players -- 2.40.1 From 5c65ff1900c7fdf9390a6b96832751a3a509f46c Mon Sep 17 00:00:00 2001 From: Hamcha Date: Thu, 5 Sep 2019 10:20:27 +0200 Subject: [PATCH 06/17] Add leaving --- src/network/server.ts | 36 +++++++++++++++++++++++++++++++++--- src/network/types.ts | 19 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/network/server.ts b/src/network/server.ts index d3aec55..f49d70a 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -11,7 +11,10 @@ import { RoomInfoMessage, Player, NetworkMessage, - RenameMessage + RenameMessage, + LeaveMessage, + NetworkPlayer, + AckMessage } from "./types"; import LocalClient from "./local"; @@ -133,11 +136,38 @@ export default class PeerServer extends NetworkPeer { // Notify other players this.broadcast({ kind: "player-joined", - name: conn.metadata.name + name: playerName }); } - private _received(player: Player, data: NetworkMessage) {} + private removePlayer(player: NetworkPlayer) { + // Tell the player everything's fine + this.send(player.conn, { kind: "ok", what: "leave-req" }); + + // Close connection with player + player.conn.close(); + + // Notify other players + this.broadcast({ + kind: "player-left", + name: player.name + }); + } + + private _received(player: Player, data: NetworkMessage) { + switch (data.kind) { + // Player is leaving! + case "leave-req": + // If we're leaving, end the server + if (player.kind == "local") { + //TODO + } else { + // Remove and disconnect player + this.removePlayer(player); + } + break; + } + } private get playerCount(): number { return Object.keys(this.room.players).length; diff --git a/src/network/types.ts b/src/network/types.ts index d8d0c47..a84f039 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -63,10 +63,13 @@ type DraftInfo = { export type NetworkMessage = | PasswordRequest | PasswordResponse + | LeaveRequest | ErrorMessage | RoomInfoMessage | JoinMessage - | RenameMessage; + | LeaveMessage + | RenameMessage + | AckMessage; export interface PasswordRequest { kind: "password-req"; @@ -83,17 +86,31 @@ export interface RoomInfoMessage { players: string[]; } +export interface LeaveRequest { + kind: "leave-req"; +} + export interface JoinMessage { kind: "player-joined"; name: string; } +export interface LeaveMessage { + kind: "player-left"; + name: string; +} + export interface RenameMessage { kind: "rename"; oldname: string; newname: string; } +export interface AckMessage { + kind: "ok"; + what: string; +} + export interface ErrorMessage { kind: "error"; error: string; -- 2.40.1 From ebf846c15407ff08ae6f56e829649a0677017c44 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Thu, 5 Sep 2019 12:08:21 +0200 Subject: [PATCH 07/17] Add mocking for peerJS and first test --- README.md | 2 + package.json | 1 + src/network/client.ts | 17 +- src/network/index.ts | 5 + src/network/local.ts | 4 +- src/network/peer.ts | 8 +- src/network/server.ts | 18 +- src/network/types.ts | 2 +- src/testing/LocalDataConnection.ts | 219 +++++++++++++++++++++++++ src/testing/LocalPeer.ts | 88 ++++++++++ {tests => src/tests}/unit/.eslintrc.js | 0 src/tests/unit/server.spec.ts | 23 +++ yarn.lock | 5 + 13 files changed, 375 insertions(+), 17 deletions(-) create mode 100644 src/network/index.ts create mode 100644 src/testing/LocalDataConnection.ts create mode 100644 src/testing/LocalPeer.ts rename {tests => src/tests}/unit/.eslintrc.js (100%) create mode 100644 src/tests/unit/server.spec.ts diff --git a/README.md b/README.md index 627e18d..36c499a 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ Work in progress name, work in progress game ## License Code is ISC, Assets "depends", some stuff is taken from FreeSound and DeviantArt so your best bet is to just praise the copyright gods. + +PeerJS mocking is based on works by Rolf Erik Lekang, which is licensed under MIT. Check [peerjs-mock](https://github.com/relekang/peerjs-mock) for more details. diff --git a/package.json b/package.json index 78af567..26304fa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "buefy": "^0.8.2", "core-js": "^2.6.5", "dexie": "^2.0.4", + "eventemitter3": "^4.0.0", "peerjs": "^1.0.4", "register-service-worker": "^1.6.2", "vue": "^2.6.10", diff --git a/src/network/client.ts b/src/network/client.ts index 50ace8a..7019928 100644 --- a/src/network/client.ts +++ b/src/network/client.ts @@ -1,21 +1,26 @@ import NetworkPeer from "./peer"; -import { DataConnection } from "peerjs"; +import Peer, { DataConnection } from "peerjs"; import { PeerMetadata, NetworkMessage } from "./types"; -export default class PeerClient extends NetworkPeer { +export class PeerClient extends NetworkPeer { private connection: DataConnection; - public constructor(peerid: string, metadata: PeerMetadata) { - super(); + public constructor( + peerid: string, + metadata: PeerMetadata, + customPeer?: Peer + ) { + super(customPeer); this.connection = this.peer.connect(peerid, { - label: "server", metadata, reliable: true }); this.connection.on("open", () => { console.info("Connected to server"); }); - this.connection.on("data", this._received); + this.connection.on("data", this._received.bind(this)); } private _received(data: NetworkMessage) {} } + +export default PeerClient; diff --git a/src/network/index.ts b/src/network/index.ts new file mode 100644 index 0000000..da0b10c --- /dev/null +++ b/src/network/index.ts @@ -0,0 +1,5 @@ +export * from "./client"; +export * from "./local"; +export * from "./peer"; +export * from "./server"; +export * from "./types"; diff --git a/src/network/local.ts b/src/network/local.ts index 980a054..6f5d4dc 100644 --- a/src/network/local.ts +++ b/src/network/local.ts @@ -1,6 +1,6 @@ import { PeerMetadata, NetworkMessage } from "./types"; -export default class LocalClient { +export class LocalClient { public metadata: PeerMetadata; public receiver!: (data: NetworkMessage) => void; @@ -16,3 +16,5 @@ export default class LocalClient { this.receiver(data); } } + +export default LocalClient; diff --git a/src/network/peer.ts b/src/network/peer.ts index 7658fca..d8402b9 100644 --- a/src/network/peer.ts +++ b/src/network/peer.ts @@ -1,10 +1,10 @@ import Peer, { DataConnection } from "peerjs"; import { NetworkMessage } from "./types"; -export default class NetworkPeer { +export class NetworkPeer { protected peer: Peer; - protected constructor() { - this.peer = new Peer(); + protected constructor(peer?: Peer) { + this.peer = peer ? peer : new Peer(); this.peer.on("open", function(id) { console.info("Peer ID assigned: %s", id); }); @@ -15,3 +15,5 @@ export default class NetworkPeer { conn.send(data); } } + +export default NetworkPeer; diff --git a/src/network/server.ts b/src/network/server.ts index f49d70a..1867bd0 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -1,5 +1,5 @@ import NetworkPeer from "./peer"; -import { DataConnection } from "peerjs"; +import Peer, { DataConnection } from "peerjs"; import { RoomInfo, PasswordRequest, @@ -33,11 +33,15 @@ function nextName(name: string): string { return name.substr(0, i) + (Number(name.slice(i)) + 1); } -export default class PeerServer extends NetworkPeer { +export class PeerServer extends NetworkPeer { private room: Room; - public constructor(roomInfo: RoomInfo, local: LocalClient) { - super(); + public constructor( + roomInfo: RoomInfo, + local: LocalClient, + customPeer?: Peer + ) { + super(customPeer); let players: Record = {}; // Add local player to server @@ -52,7 +56,7 @@ export default class PeerServer extends NetworkPeer { info: roomInfo, players }; - this.peer.on("connection", this._connection); + this.peer.on("connection", this._connection.bind(this)); } private _connection(conn: DataConnection) { @@ -105,7 +109,7 @@ export default class PeerServer extends NetworkPeer { }; this.send(conn, { kind: "password-req" }); - conn.on("data", checkPasswordResponse); + conn.on("data", checkPasswordResponse.bind(this)); return; } @@ -184,3 +188,5 @@ export default class PeerServer extends NetworkPeer { } } } + +export default PeerServer; diff --git a/src/network/types.ts b/src/network/types.ts index a84f039..d5b2033 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -27,7 +27,7 @@ export interface Room { export interface RoomInfo { max_players: number; password: string; - game: GameInfo; + game?: GameInfo; } type GameInfo = MatchInfo | DraftInfo; diff --git a/src/testing/LocalDataConnection.ts b/src/testing/LocalDataConnection.ts new file mode 100644 index 0000000..ee6e83f --- /dev/null +++ b/src/testing/LocalDataConnection.ts @@ -0,0 +1,219 @@ +import EventEmitter from "eventemitter3"; +import LocalPeer from "./LocalPeer"; +import { PeerConnectOption } from "peerjs"; + +export default class LocalDataConnection extends EventEmitter { + public options: PeerConnectOption; + public peer: string; + public provider: LocalPeer; + public open: boolean = false; + private _connection?: LocalDataConnection; + public id: string; + public label: string; + + public constructor( + peer: string, + provider: LocalPeer, + options?: PeerConnectOption + ) { + super(); + this.options = { + serialization: "binary", + reliable: false + }; + if (options) { + this.options = Object.assign(this.options, options); + } + this.id = "fake_" + Math.random().toString(32); + this.label = this.options.label ? this.options.label : this.id; + this.provider = provider; + this.peer = peer; + } + + public send(message: any) { + if (!this.open) { + this.emit( + "error", + new Error( + "Connection is not open. You should listen for the `open` event before sending messages." + ) + ); + } + const connection = this._connection; + if (connection) { + connection.receive(message); + } + } + + public receive(message: any) { + this.emit("data", message); + } + + public close() { + if (!this.open) { + return; + } + this.open = false; + this.emit("close"); + } + + public _setRemote(connection: LocalDataConnection) { + this._connection = connection; + this._connection.on("open", () => { + if (!this.open) { + this.open = true; + this.emit("open"); + } + }); + } + + public _configureDataChannel( + mocks: Record, + options?: PeerConnectOption + ) { + if (!this._connection && this.peer in mocks) { + this._setRemote( + mocks[this.peer]._negotiate(this.provider.id, this, options) + ); + this.open = true; + this.emit("open"); + } else { + throw new Error(`Peer(${this.peer}) not found`); + } + } + + public get metadata() { + return this.options.metadata; + } + + public get reliable() { + return this.options.reliable ? this.options.reliable : false; + } + + public get serialization() { + return this.options.serialization ? this.options.serialization : "binary"; + } + + public get peerConnection() { + return this._connection; + } + + /* + UNIMPLEMENTED STUFF + */ + + public dataChannel: unknown; + public bufferSize: unknown; + public type: unknown; + public canTrickleIceCandidates: unknown; + public connectionState: unknown; + public currentLocalDescription: unknown; + public currentRemoteDescription: unknown; + public iceConnectionState: unknown; + public iceGatheringState: unknown; + public idpErrorInfo: unknown; + public idpLoginUrl: unknown; + public localDescription: unknown; + public onconnectionstatechange: unknown; + public ondatachannel: unknown; + public onicecandidate: unknown; + public onicecandidateerror: unknown; + public oniceconnectionstatechange: unknown; + public onicegatheringstatechange: unknown; + public onnegotiationneeded: unknown; + public onsignalingstatechange: unknown; + public onstatsended: unknown; + public ontrack: unknown; + public peerIdentity: unknown; + public pendingLocalDescription: unknown; + public pendingRemoteDescription: unknown; + public remoteDescription: unknown; + public sctp: unknown; + public signalingState: unknown; + + public addIceCandidate( + candidate: RTCIceCandidateInit | RTCIceCandidate + ): Promise { + throw new Error("not implemented"); + } + public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): unknown { + throw new Error("not implemented"); + } + public addTransceiver( + trackOrKind: MediaStreamTrack | string, + init?: RTCRtpTransceiverInit + ): unknown { + throw new Error("not implemented"); + } + public createAnswer(options?: RTCOfferOptions): Promise { + throw new Error("not implemented"); + } + public createDataChannel( + label: string, + dataChannelDict?: RTCDataChannelInit + ): unknown { + throw new Error("not implemented"); + } + public createOffer(options?: RTCOfferOptions): Promise { + throw new Error("not implemented"); + } + public getConfiguration(): unknown { + throw new Error("not implemented"); + } + public getIdentityAssertion(): Promise { + throw new Error("not implemented"); + } + public getReceivers(): unknown[] { + throw new Error("not implemented"); + } + public getSenders(): unknown[] { + throw new Error("not implemented"); + } + public getStats(selector?: MediaStreamTrack | null): Promise { + throw new Error("not implemented"); + } + public getTransceivers(): unknown[] { + throw new Error("not implemented"); + } + public removeTrack(sender: RTCRtpSender): void { + throw new Error("not implemented"); + } + public setConfiguration(configuration: RTCConfiguration): void { + throw new Error("not implemented"); + } + public setIdentityProvider( + provider: string, + options?: RTCIdentityProviderOptions + ) { + throw new Error("not implemented"); + } + public setLocalDescription( + description: RTCSessionDescriptionInit + ): Promise { + throw new Error("not implemented"); + } + public setRemoteDescription( + description: RTCSessionDescriptionInit + ): Promise { + throw new Error("not implemented"); + } + + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + throw new Error("not implemented"); + } + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + throw new Error("not implemented"); + } + + dispatchEvent(event: Event): boolean { + throw new Error("not implemented"); + } +} diff --git a/src/testing/LocalPeer.ts b/src/testing/LocalPeer.ts new file mode 100644 index 0000000..661fec1 --- /dev/null +++ b/src/testing/LocalPeer.ts @@ -0,0 +1,88 @@ +import EventEmitter from "eventemitter3"; +import Peer, { PeerConnectOption, PeerJSOption } from "peerjs"; +import LocalDataConnection from "./LocalDataConnection"; + +let mocks: Record = {}; + +export default class LocalPeer extends EventEmitter { + public connections: Record; + public id: string; + public options: PeerJSOption; + public disconnected: boolean; + public destroyed: boolean; + public prototype: unknown; + + public constructor(id: string, options?: PeerJSOption) { + super(); + this.id = id; + this.options = options ? options : {}; + this.connections = {}; + this.disconnected = false; + this.destroyed = false; + mocks[id] = this; + } + + public connect(id: string, options?: PeerConnectOption): LocalDataConnection { + let connection = new LocalDataConnection(id, this, options); + connection._configureDataChannel(mocks, options); + this._addConnection(id, connection); + return connection; + } + + public disconnect() { + this.disconnected = true; + this.emit("disconnected", this.id); + this.id = ""; + } + + public destroy() { + this.destroyed = true; + this.emit("destroyed", this.id); + this.disconnect(); + } + + private _addConnection(peer: string, connection: LocalDataConnection) { + if (!this.connections[peer]) { + this.connections[peer] = []; + } + this.connections[peer].push(connection); + } + + public _negotiate( + peer: string, + remoteConnection: LocalDataConnection, + options?: PeerConnectOption + ) { + let localConnection = new LocalDataConnection(peer, this, options); + localConnection._setRemote(remoteConnection); + this._addConnection(peer, localConnection); + this.emit("connection", localConnection); + return localConnection; + } + + public call( + id: string, + stream: MediaStream, + options?: Peer.CallOption + ): Peer.MediaConnection { + throw new Error("unimplemented"); + } + + public reconnect() {} + + public getConnection( + peerId: string, + connectionId: string + ): LocalDataConnection | null { + if (peerId in mocks) { + let map = mocks[peerId].connections; + if (connectionId in map) { + return map[connectionId][0]; + } + return null; + } + return null; + } + + public listAllPeers() {} +} diff --git a/tests/unit/.eslintrc.js b/src/tests/unit/.eslintrc.js similarity index 100% rename from tests/unit/.eslintrc.js rename to src/tests/unit/.eslintrc.js diff --git a/src/tests/unit/server.spec.ts b/src/tests/unit/server.spec.ts new file mode 100644 index 0000000..581aa91 --- /dev/null +++ b/src/tests/unit/server.spec.ts @@ -0,0 +1,23 @@ +import LocalPeer from "@/testing/LocalPeer"; +import Peer from "peerjs"; +import { PeerServer, LocalClient, PeerClient } from "@/network"; + +describe("network/PeerServer", () => { + test("Create server and client", () => { + const serverPeer = new LocalPeer("test-server", {}); + const clientPeer = new LocalPeer("test-client", {}); + const serverPlayer = new LocalClient({ + name: "server-client" + }); + const roomInfo = { + max_players: 3, + password: "" + }; + const server = new PeerServer(roomInfo, serverPlayer, serverPeer as Peer); + const client = new PeerClient( + "test-server", + { name: "test" }, + clientPeer as Peer + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index cf86421..6cdb382 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3889,6 +3889,11 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.2: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + events@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" -- 2.40.1 From ab2ef3e80339710125334f0bfc1bdd6392cd8154 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 11:50:40 +0200 Subject: [PATCH 08/17] Add tests and expect library --- jest.config.js | 38 ++++----- src/network/{local.ts => LocalClient.ts} | 5 +- src/network/{peer.ts => NetworkPeer.ts} | 5 +- src/network/PeerClient.ts | 35 ++++++++ src/network/{server.ts => PeerServer.ts} | 12 ++- src/network/client.ts | 26 ------ src/network/index.ts | 8 +- src/network/types.ts | 2 +- src/testing/EventHook.ts | 63 +++++++++++++++ ...ataConnection.ts => MockDataConnection.ts} | 16 ++-- src/testing/{LocalPeer.ts => MockPeer.ts} | 39 +++++---- src/testing/index.ts | 2 + src/tests/unit/network.spec.ts | 80 +++++++++++++++++++ src/tests/unit/server.spec.ts | 23 ------ src/utils/index.ts | 1 + src/utils/timeout.ts | 9 +++ 16 files changed, 256 insertions(+), 108 deletions(-) rename src/network/{local.ts => LocalClient.ts} (74%) rename src/network/{peer.ts => NetworkPeer.ts} (80%) create mode 100644 src/network/PeerClient.ts rename src/network/{server.ts => PeerServer.ts} (94%) delete mode 100644 src/network/client.ts create mode 100644 src/testing/EventHook.ts rename src/testing/{LocalDataConnection.ts => MockDataConnection.ts} (94%) rename src/testing/{LocalPeer.ts => MockPeer.ts} (60%) create mode 100644 src/testing/index.ts create mode 100644 src/tests/unit/network.spec.ts delete mode 100644 src/tests/unit/server.spec.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/timeout.ts diff --git a/jest.config.js b/jest.config.js index 0d19e00..c1b9492 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,37 +1,27 @@ module.exports = { - moduleFileExtensions: [ - 'js', - 'jsx', - 'json', - 'vue', - 'ts', - 'tsx' - ], + moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], transform: { - '^.+\\.vue$': 'vue-jest', - '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', - '^.+\\.tsx?$': 'ts-jest' + "^.+\\.vue$": "vue-jest", + ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": + "jest-transform-stub", + "^.+\\.tsx?$": "ts-jest" }, - transformIgnorePatterns: [ - '/node_modules/' - ], + transformIgnorePatterns: ["/node_modules/"], moduleNameMapper: { - '^@/(.*)$': '/src/$1' + "^@/(.*)$": "/src/$1" }, - snapshotSerializers: [ - 'jest-serializer-vue' - ], + snapshotSerializers: ["jest-serializer-vue"], testMatch: [ - '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" ], - testURL: 'http://localhost/', + testURL: "http://localhost/", watchPlugins: [ - 'jest-watch-typeahead/filename', - 'jest-watch-typeahead/testname' + "jest-watch-typeahead/filename", + "jest-watch-typeahead/testname" ], globals: { - 'ts-jest': { + "ts-jest": { babelConfig: true } } -} +}; diff --git a/src/network/local.ts b/src/network/LocalClient.ts similarity index 74% rename from src/network/local.ts rename to src/network/LocalClient.ts index 6f5d4dc..154d818 100644 --- a/src/network/local.ts +++ b/src/network/LocalClient.ts @@ -1,14 +1,17 @@ import { PeerMetadata, NetworkMessage } from "./types"; +import EventEmitter from "eventemitter3"; -export class LocalClient { +export class LocalClient extends EventEmitter { public metadata: PeerMetadata; public receiver!: (data: NetworkMessage) => void; public constructor(metadata: PeerMetadata) { + super(); this.metadata = metadata; } public receive(data: NetworkMessage) { + this.emit("data", data); //TODO } diff --git a/src/network/peer.ts b/src/network/NetworkPeer.ts similarity index 80% rename from src/network/peer.ts rename to src/network/NetworkPeer.ts index d8402b9..b889266 100644 --- a/src/network/peer.ts +++ b/src/network/NetworkPeer.ts @@ -1,9 +1,11 @@ import Peer, { DataConnection } from "peerjs"; import { NetworkMessage } from "./types"; +import EventEmitter from "eventemitter3"; -export class NetworkPeer { +export class NetworkPeer extends EventEmitter { protected peer: Peer; protected constructor(peer?: Peer) { + super(); this.peer = peer ? peer : new Peer(); this.peer.on("open", function(id) { console.info("Peer ID assigned: %s", id); @@ -11,7 +13,6 @@ export class NetworkPeer { } protected send(conn: DataConnection, data: T) { - //TODO Debugging support? conn.send(data); } } diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts new file mode 100644 index 0000000..f15a3dd --- /dev/null +++ b/src/network/PeerClient.ts @@ -0,0 +1,35 @@ +import NetworkPeer from "./NetworkPeer"; +import Peer, { DataConnection } from "peerjs"; +import { PeerMetadata, NetworkMessage } from "./types"; + +export class PeerClient extends NetworkPeer { + private connection?: DataConnection; + private metadata: PeerMetadata; + + public constructor(metadata: PeerMetadata, customPeer?: Peer) { + super(customPeer); + this.metadata = metadata; + } + + public connect(peerid: string) { + this.connection = this.peer.connect(peerid, { + metadata: this.metadata, + reliable: true + }); + if (!this.connection) { + throw new Error("Could not connect"); + } + this.connection.on("open", () => { + this.emit("connected"); + }); + this.connection.on("data", data => { + this._received(data); + }); + } + + private _received(data: NetworkMessage) { + this.emit("data", data); + } +} + +export default PeerClient; diff --git a/src/network/server.ts b/src/network/PeerServer.ts similarity index 94% rename from src/network/server.ts rename to src/network/PeerServer.ts index 1867bd0..ba0049e 100644 --- a/src/network/server.ts +++ b/src/network/PeerServer.ts @@ -1,4 +1,3 @@ -import NetworkPeer from "./peer"; import Peer, { DataConnection } from "peerjs"; import { RoomInfo, @@ -16,7 +15,7 @@ import { NetworkPlayer, AckMessage } from "./types"; -import LocalClient from "./local"; +import { NetworkPeer, LocalClient } from "."; // Increment name, add number at the end if not present // Examples: @@ -56,7 +55,9 @@ export class PeerServer extends NetworkPeer { info: roomInfo, players }; - this.peer.on("connection", this._connection.bind(this)); + this.peer.on("connection", conn => { + this._connection(conn); + }); } private _connection(conn: DataConnection) { @@ -77,7 +78,10 @@ export class PeerServer extends NetworkPeer { // Check if there is already a player called that way if (this.room.players[metadata.name]) { // Force rename - const newname = nextName(metadata.name); + let newname = metadata.name; + while (this.room.players[newname]) { + newname = nextName(metadata.name); + } this.send(conn, { kind: "rename", oldname: metadata.name, diff --git a/src/network/client.ts b/src/network/client.ts deleted file mode 100644 index 7019928..0000000 --- a/src/network/client.ts +++ /dev/null @@ -1,26 +0,0 @@ -import NetworkPeer from "./peer"; -import Peer, { DataConnection } from "peerjs"; -import { PeerMetadata, NetworkMessage } from "./types"; - -export class PeerClient extends NetworkPeer { - private connection: DataConnection; - public constructor( - peerid: string, - metadata: PeerMetadata, - customPeer?: Peer - ) { - super(customPeer); - this.connection = this.peer.connect(peerid, { - metadata, - reliable: true - }); - this.connection.on("open", () => { - console.info("Connected to server"); - }); - this.connection.on("data", this._received.bind(this)); - } - - private _received(data: NetworkMessage) {} -} - -export default PeerClient; diff --git a/src/network/index.ts b/src/network/index.ts index da0b10c..66b428b 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -1,5 +1,5 @@ -export * from "./client"; -export * from "./local"; -export * from "./peer"; -export * from "./server"; +export * from "./PeerClient"; +export * from "./LocalClient"; +export * from "./NetworkPeer"; +export * from "./PeerServer"; export * from "./types"; diff --git a/src/network/types.ts b/src/network/types.ts index d5b2033..e6991af 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -1,5 +1,5 @@ import { DataConnection } from "peerjs"; -import LocalClient from "./local"; +import LocalClient from "./LocalClient"; export interface LocalPlayer { kind: "local"; diff --git a/src/testing/EventHook.ts b/src/testing/EventHook.ts new file mode 100644 index 0000000..2ba2333 --- /dev/null +++ b/src/testing/EventHook.ts @@ -0,0 +1,63 @@ +import EventEmitter from "eventemitter3"; +import { withTimeout } from "@/utils"; + +interface HookedEvent { + date: Date; + arguments: any[]; +} + +const defaultCmp = () => {}; + +export class EventHook extends EventEmitter { + public backlog: Record = {}; + + public hookEmitter(emitter: EventEmitter, event: string, id: string) { + emitter.on(event, this._receivedEmitter.bind(this, id)); + } + + public expect( + id: string, + timeout: number = 1000, + cmp: (...args: any) => void = defaultCmp + ): Promise { + // Build promise + const promise = new Promise((resolve, reject) => { + const getLastEvent = () => { + // Take first element in the backlog + let event = this.backlog[id].shift(); + if (!event) { + // Should never happen + return reject(`undefined event in list (??)`); + } + // Send to check function, then resolve + resolve(cmp(...event.arguments)); + return; + }; + if (this.backlog[id] && this.backlog[id].length > 0) { + // Event is already in the backlog + getLastEvent(); + } else { + // If the event hasn't fired yet, subscribe to the next time it will + this.once(id, getLastEvent); + } + }); + + // Return promise with added timeout + return withTimeout(promise, timeout); + } + + // When events are received + private _receivedEmitter(id: string, ...args: any) { + const event: HookedEvent = { + date: new Date(), + arguments: args + }; + // Create backlog if it doesn't exist + if (!this.backlog[id]) { + this.backlog[id] = []; + } + // Push to backlog and emit event (for pending expects) + this.backlog[id].push(event); + this.emit(id, event); + } +} diff --git a/src/testing/LocalDataConnection.ts b/src/testing/MockDataConnection.ts similarity index 94% rename from src/testing/LocalDataConnection.ts rename to src/testing/MockDataConnection.ts index ee6e83f..92cda23 100644 --- a/src/testing/LocalDataConnection.ts +++ b/src/testing/MockDataConnection.ts @@ -1,19 +1,19 @@ import EventEmitter from "eventemitter3"; -import LocalPeer from "./LocalPeer"; +import MockPeer from "./MockPeer"; import { PeerConnectOption } from "peerjs"; -export default class LocalDataConnection extends EventEmitter { +export class MockDataConnection extends EventEmitter { public options: PeerConnectOption; public peer: string; - public provider: LocalPeer; + public provider: MockPeer; public open: boolean = false; - private _connection?: LocalDataConnection; + private _connection?: MockDataConnection; public id: string; public label: string; public constructor( peer: string, - provider: LocalPeer, + provider: MockPeer, options?: PeerConnectOption ) { super(); @@ -57,7 +57,7 @@ export default class LocalDataConnection extends EventEmitter { this.emit("close"); } - public _setRemote(connection: LocalDataConnection) { + public _setRemote(connection: MockDataConnection) { this._connection = connection; this._connection.on("open", () => { if (!this.open) { @@ -68,7 +68,7 @@ export default class LocalDataConnection extends EventEmitter { } public _configureDataChannel( - mocks: Record, + mocks: Record, options?: PeerConnectOption ) { if (!this._connection && this.peer in mocks) { @@ -217,3 +217,5 @@ export default class LocalDataConnection extends EventEmitter { throw new Error("not implemented"); } } + +export default MockDataConnection; diff --git a/src/testing/LocalPeer.ts b/src/testing/MockPeer.ts similarity index 60% rename from src/testing/LocalPeer.ts rename to src/testing/MockPeer.ts index 661fec1..ee02a24 100644 --- a/src/testing/LocalPeer.ts +++ b/src/testing/MockPeer.ts @@ -1,30 +1,35 @@ import EventEmitter from "eventemitter3"; import Peer, { PeerConnectOption, PeerJSOption } from "peerjs"; -import LocalDataConnection from "./LocalDataConnection"; +import MockDataConnection from "./MockDataConnection"; -let mocks: Record = {}; - -export default class LocalPeer extends EventEmitter { - public connections: Record; +export class MockPeer extends EventEmitter { + public connections: Record; public id: string; public options: PeerJSOption; public disconnected: boolean; public destroyed: boolean; public prototype: unknown; + private mocks: Record; - public constructor(id: string, options?: PeerJSOption) { + public constructor( + mockSource: Record, + id: string, + options?: PeerJSOption + ) { super(); this.id = id; this.options = options ? options : {}; this.connections = {}; this.disconnected = false; this.destroyed = false; - mocks[id] = this; + this.mocks = mockSource; + this.mocks[id] = this; } - public connect(id: string, options?: PeerConnectOption): LocalDataConnection { - let connection = new LocalDataConnection(id, this, options); - connection._configureDataChannel(mocks, options); + public connect(id: string, options?: PeerConnectOption): MockDataConnection { + let connection = new MockDataConnection(id, this, options); + // hacky way to avoid race conditions + setTimeout(() => connection._configureDataChannel(this.mocks, options), 1); this._addConnection(id, connection); return connection; } @@ -41,7 +46,7 @@ export default class LocalPeer extends EventEmitter { this.disconnect(); } - private _addConnection(peer: string, connection: LocalDataConnection) { + private _addConnection(peer: string, connection: MockDataConnection) { if (!this.connections[peer]) { this.connections[peer] = []; } @@ -50,10 +55,10 @@ export default class LocalPeer extends EventEmitter { public _negotiate( peer: string, - remoteConnection: LocalDataConnection, + remoteConnection: MockDataConnection, options?: PeerConnectOption ) { - let localConnection = new LocalDataConnection(peer, this, options); + let localConnection = new MockDataConnection(peer, this, options); localConnection._setRemote(remoteConnection); this._addConnection(peer, localConnection); this.emit("connection", localConnection); @@ -73,9 +78,9 @@ export default class LocalPeer extends EventEmitter { public getConnection( peerId: string, connectionId: string - ): LocalDataConnection | null { - if (peerId in mocks) { - let map = mocks[peerId].connections; + ): MockDataConnection | null { + if (peerId in this.mocks) { + let map = this.mocks[peerId].connections; if (connectionId in map) { return map[connectionId][0]; } @@ -86,3 +91,5 @@ export default class LocalPeer extends EventEmitter { public listAllPeers() {} } + +export default MockPeer; diff --git a/src/testing/index.ts b/src/testing/index.ts new file mode 100644 index 0000000..46190f0 --- /dev/null +++ b/src/testing/index.ts @@ -0,0 +1,2 @@ +export * from "./MockDataConnection"; +export * from "./MockPeer"; diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts new file mode 100644 index 0000000..743e757 --- /dev/null +++ b/src/tests/unit/network.spec.ts @@ -0,0 +1,80 @@ +import Peer from "peerjs"; +import { MockPeer } from "@/testing"; +import { PeerServer, LocalClient, PeerClient, NetworkMessage } from "@/network"; +import { EventHook } from "@/testing/EventHook"; + +const sampleRoom = () => ({ + max_players: 3, + password: "" +}); + +function mockSource(): Record { + return {}; +} + +function createServer( + mockSource: Record, + id: string = "test-server", + name: string = "server-client" +) { + const serverPeer = new MockPeer(mockSource, id, {}); + const serverPlayer = new LocalClient({ + name: name + }); + return new PeerServer(sampleRoom(), serverPlayer, serverPeer as Peer); +} + +function createClient( + mockSource: Record, + id: string = "test-client", + name: string = "client-peer" +) { + const clientPeer = new MockPeer(mockSource, id, {}); + return new PeerClient({ name }, clientPeer as Peer); +} + +function messageKind(kind: string): (msg: NetworkMessage) => void { + return msg => { + expect(msg.kind).toEqual(kind); + }; +} + +describe("network/PeerServer", () => { + test("Create server", () => { + const mox = mockSource(); + createServer(mox); + }); + + test("Test client join", async () => { + const mox = mockSource(); + const hook = new EventHook(); + createServer(mox); + const client = createClient(mox); + hook.hookEmitter(client, "data", "client-data"); + client.connect("test-server"); + await hook.expect("client-data", 1000, messageKind("room-info")); + await hook.expect("client-data", 1000, messageKind("player-joined")); + }); + + test("Test multiple clients", async () => { + const mox = mockSource(); + const hook = new EventHook(); + createServer(mox); + const client1 = createClient(mox); + const client2 = createClient(mox); + hook.hookEmitter(client1, "data", "client1-data"); + hook.hookEmitter(client2, "data", "client2-data"); + client1.connect("test-server"); + await hook.expect("client1-data", 1000, messageKind("room-info")); + await hook.expect("client1-data", 1000, messageKind("player-joined")); + client2.connect("test-server"); + await hook.expect("client2-data", 1000, messageKind("rename")); + await hook.expect("client2-data", 1000, messageKind("room-info")); + await hook.expect("client2-data", 1000, messageKind("player-joined")); + await hook.expect("client1-data", 1000, messageKind("player-joined")); + }); +}); + +describe("network/PeerClient", () => { + //TODO Client tests +}); diff --git a/src/tests/unit/server.spec.ts b/src/tests/unit/server.spec.ts deleted file mode 100644 index 581aa91..0000000 --- a/src/tests/unit/server.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import LocalPeer from "@/testing/LocalPeer"; -import Peer from "peerjs"; -import { PeerServer, LocalClient, PeerClient } from "@/network"; - -describe("network/PeerServer", () => { - test("Create server and client", () => { - const serverPeer = new LocalPeer("test-server", {}); - const clientPeer = new LocalPeer("test-client", {}); - const serverPlayer = new LocalClient({ - name: "server-client" - }); - const roomInfo = { - max_players: 3, - password: "" - }; - const server = new PeerServer(roomInfo, serverPlayer, serverPeer as Peer); - const client = new PeerClient( - "test-server", - { name: "test" }, - clientPeer as Peer - ); - }); -}); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1a5ee3d --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./timeout"; diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts new file mode 100644 index 0000000..55ca11b --- /dev/null +++ b/src/utils/timeout.ts @@ -0,0 +1,9 @@ +export function withTimeout(promise: Promise, ms: number): Promise { + const timeout = new Promise((resolve, reject) => { + let id = setTimeout(() => { + clearTimeout(id); + reject("Timed out after " + ms + "ms."); + }, ms); + }); + return Promise.race([promise, timeout]); +} -- 2.40.1 From 0784365285b66bf2170a29173655b0c6737e4ea9 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 12:13:22 +0200 Subject: [PATCH 09/17] Add basic event handling for clients --- src/network/PeerClient.ts | 35 ++++++++++++++++++++++++++++++++++ src/tests/unit/network.spec.ts | 16 ++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts index f15a3dd..efa36c2 100644 --- a/src/network/PeerClient.ts +++ b/src/network/PeerClient.ts @@ -5,10 +5,12 @@ import { PeerMetadata, NetworkMessage } from "./types"; export class PeerClient extends NetworkPeer { private connection?: DataConnection; private metadata: PeerMetadata; + public players: string[]; public constructor(metadata: PeerMetadata, customPeer?: Peer) { super(customPeer); this.metadata = metadata; + this.players = []; } public connect(peerid: string) { @@ -29,6 +31,39 @@ export class PeerClient extends NetworkPeer { private _received(data: NetworkMessage) { this.emit("data", data); + switch (data.kind) { + // Someone changed name (or was forced to) + case "rename": + if (data.oldname == this.metadata.name) { + // We got a name change! + this.metadata.name = data.newname; + } else { + let idx = this.players.indexOf(data.oldname); + if (idx < 0) { + // Weird + console.warn( + `Someone (${data.oldname}) changed name but wasn't on the player list` + ); + this.players.push(data.newname); + break; + } + this.players[idx] = data.newname; + } + this.emit("rename", data.oldname, data.newname); + break; + // A new player joined the room (this includes us) + case "player-joined": + this.players.push(data.name); + this.emit("player-joined", data.name); + break; + default: + // For most cases, we can just use the kind as event type + this.emit(data.kind, data); + } + } + + public get name(): string { + return this.metadata.name; } } diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index 743e757..3c6ab1f 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -56,7 +56,7 @@ describe("network/PeerServer", () => { await hook.expect("client-data", 1000, messageKind("player-joined")); }); - test("Test multiple clients", async () => { + test("Test multiple clients (w/ name collision)", async () => { const mox = mockSource(); const hook = new EventHook(); createServer(mox); @@ -76,5 +76,17 @@ describe("network/PeerServer", () => { }); describe("network/PeerClient", () => { - //TODO Client tests + test("Client handles forced name change", async () => { + const mox = mockSource(); + const hook = new EventHook(); + createServer(mox); + const client1 = createClient(mox); + const client2 = createClient(mox); + hook.hookEmitter(client2, "rename", "client-rename"); + client1.connect("test-server"); + client2.connect("test-server"); + await hook.expect("client-rename", 1000); + // Client must have changed its internal data to match the new name + expect(client1.name).not.toEqual(client2.name); + }); }); -- 2.40.1 From 862d84adf24a9accbfbf0d682cbed9488f4e9606 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 12:20:12 +0200 Subject: [PATCH 10/17] Refactor some helper functions to its own module --- src/testing/MockHelper.ts | 24 +++++++++++++++ src/testing/index.ts | 1 + src/tests/unit/network.spec.ts | 56 +++++++++------------------------- 3 files changed, 40 insertions(+), 41 deletions(-) create mode 100644 src/testing/MockHelper.ts diff --git a/src/testing/MockHelper.ts b/src/testing/MockHelper.ts new file mode 100644 index 0000000..8b63404 --- /dev/null +++ b/src/testing/MockHelper.ts @@ -0,0 +1,24 @@ +import Peer from "peerjs"; +import { MockPeer } from "."; +import { LocalClient, PeerServer, PeerClient, RoomInfo } from "@/network"; + +export class MockHelper { + private mocks: Record = {}; + + createServer( + room: RoomInfo, + id: string = "test-server", + name: string = "server-client" + ) { + const serverPeer = new MockPeer(this.mocks, id, {}); + const serverPlayer = new LocalClient({ name: name }); + return new PeerServer(room, serverPlayer, serverPeer as Peer); + } + + createClient(id: string = "test-client", name: string = "client-peer") { + const clientPeer = new MockPeer(this.mocks, id, {}); + return new PeerClient({ name }, clientPeer as Peer); + } +} + +export default MockHelper; diff --git a/src/testing/index.ts b/src/testing/index.ts index 46190f0..647d674 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -1,2 +1,3 @@ export * from "./MockDataConnection"; export * from "./MockPeer"; +export * from "./MockHelper"; diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index 3c6ab1f..a4425f0 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -1,6 +1,5 @@ -import Peer from "peerjs"; -import { MockPeer } from "@/testing"; -import { PeerServer, LocalClient, PeerClient, NetworkMessage } from "@/network"; +import { MockHelper } from "@/testing"; +import { NetworkMessage } from "@/network"; import { EventHook } from "@/testing/EventHook"; const sampleRoom = () => ({ @@ -8,31 +7,6 @@ const sampleRoom = () => ({ password: "" }); -function mockSource(): Record { - return {}; -} - -function createServer( - mockSource: Record, - id: string = "test-server", - name: string = "server-client" -) { - const serverPeer = new MockPeer(mockSource, id, {}); - const serverPlayer = new LocalClient({ - name: name - }); - return new PeerServer(sampleRoom(), serverPlayer, serverPeer as Peer); -} - -function createClient( - mockSource: Record, - id: string = "test-client", - name: string = "client-peer" -) { - const clientPeer = new MockPeer(mockSource, id, {}); - return new PeerClient({ name }, clientPeer as Peer); -} - function messageKind(kind: string): (msg: NetworkMessage) => void { return msg => { expect(msg.kind).toEqual(kind); @@ -41,15 +15,15 @@ function messageKind(kind: string): (msg: NetworkMessage) => void { describe("network/PeerServer", () => { test("Create server", () => { - const mox = mockSource(); - createServer(mox); + const mox = new MockHelper(); + mox.createServer(sampleRoom()); }); test("Test client join", async () => { - const mox = mockSource(); + const mox = new MockHelper(); const hook = new EventHook(); - createServer(mox); - const client = createClient(mox); + mox.createServer(sampleRoom()); + const client = mox.createClient(); hook.hookEmitter(client, "data", "client-data"); client.connect("test-server"); await hook.expect("client-data", 1000, messageKind("room-info")); @@ -57,11 +31,11 @@ describe("network/PeerServer", () => { }); test("Test multiple clients (w/ name collision)", async () => { - const mox = mockSource(); + const mox = new MockHelper(); const hook = new EventHook(); - createServer(mox); - const client1 = createClient(mox); - const client2 = createClient(mox); + mox.createServer(sampleRoom()); + const client1 = mox.createClient(); + const client2 = mox.createClient(); hook.hookEmitter(client1, "data", "client1-data"); hook.hookEmitter(client2, "data", "client2-data"); client1.connect("test-server"); @@ -77,11 +51,11 @@ describe("network/PeerServer", () => { describe("network/PeerClient", () => { test("Client handles forced name change", async () => { - const mox = mockSource(); + const mox = new MockHelper(); const hook = new EventHook(); - createServer(mox); - const client1 = createClient(mox); - const client2 = createClient(mox); + mox.createServer(sampleRoom()); + const client1 = mox.createClient(); + const client2 = mox.createClient(); hook.hookEmitter(client2, "rename", "client-rename"); client1.connect("test-server"); client2.connect("test-server"); -- 2.40.1 From 942aef279d1393dcf5ce9aef99910e8a0ef4a835 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 12:36:39 +0200 Subject: [PATCH 11/17] Refactor clients to have common base --- src/network/Client.ts | 52 ++++++++++++++++++++++++++++++++++ src/network/LocalClient.ts | 11 +++---- src/network/NetworkPeer.ts | 20 ------------- src/network/PeerClient.ts | 52 ++++++++-------------------------- src/network/PeerServer.ts | 30 +++++++++++++++----- src/network/index.ts | 2 +- src/testing/MockHelper.ts | 9 ++++-- src/tests/unit/network.spec.ts | 13 ++++++++- 8 files changed, 110 insertions(+), 79 deletions(-) create mode 100644 src/network/Client.ts delete mode 100644 src/network/NetworkPeer.ts diff --git a/src/network/Client.ts b/src/network/Client.ts new file mode 100644 index 0000000..cf4e87b --- /dev/null +++ b/src/network/Client.ts @@ -0,0 +1,52 @@ +import { PeerMetadata, NetworkMessage } from "./types"; +import EventEmitter from "eventemitter3"; + +export class Client extends EventEmitter { + public metadata: PeerMetadata; + public players: string[]; + + public constructor(metadata: PeerMetadata) { + super(); + this.metadata = metadata; + this.players = []; + } + + protected _received(data: NetworkMessage) { + this.emit("data", data); + switch (data.kind) { + // Someone changed name (or was forced to) + case "rename": + if (data.oldname == this.metadata.name) { + // We got a name change! + this.metadata.name = data.newname; + } else { + let idx = this.players.indexOf(data.oldname); + if (idx < 0) { + // Weird + console.warn( + `Someone (${data.oldname}) changed name but wasn't on the player list` + ); + this.players.push(data.newname); + break; + } + this.players[idx] = data.newname; + } + this.emit("rename", data.oldname, data.newname); + break; + // A new player joined the room (this includes us) + case "player-joined": + this.players.push(data.name); + this.emit("player-joined", data.name); + break; + default: + // For most cases, we can just use the kind as event type + this.emit(data.kind, data); + } + } + + public get name(): string { + return this.metadata.name; + } +} + +export default Client; diff --git a/src/network/LocalClient.ts b/src/network/LocalClient.ts index 154d818..152bb59 100644 --- a/src/network/LocalClient.ts +++ b/src/network/LocalClient.ts @@ -1,18 +1,15 @@ import { PeerMetadata, NetworkMessage } from "./types"; -import EventEmitter from "eventemitter3"; +import Client from "./Client"; -export class LocalClient extends EventEmitter { - public metadata: PeerMetadata; +export class LocalClient extends Client { public receiver!: (data: NetworkMessage) => void; public constructor(metadata: PeerMetadata) { - super(); - this.metadata = metadata; + super(metadata); } public receive(data: NetworkMessage) { - this.emit("data", data); - //TODO + this._received(data); } public send(data: T) { diff --git a/src/network/NetworkPeer.ts b/src/network/NetworkPeer.ts deleted file mode 100644 index b889266..0000000 --- a/src/network/NetworkPeer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Peer, { DataConnection } from "peerjs"; -import { NetworkMessage } from "./types"; -import EventEmitter from "eventemitter3"; - -export class NetworkPeer extends EventEmitter { - protected peer: Peer; - protected constructor(peer?: Peer) { - super(); - this.peer = peer ? peer : new Peer(); - this.peer.on("open", function(id) { - console.info("Peer ID assigned: %s", id); - }); - } - - protected send(conn: DataConnection, data: T) { - conn.send(data); - } -} - -export default NetworkPeer; diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts index efa36c2..76b5253 100644 --- a/src/network/PeerClient.ts +++ b/src/network/PeerClient.ts @@ -1,16 +1,17 @@ -import NetworkPeer from "./NetworkPeer"; import Peer, { DataConnection } from "peerjs"; import { PeerMetadata, NetworkMessage } from "./types"; +import Client from "./Client"; -export class PeerClient extends NetworkPeer { +export class PeerClient extends Client { + protected peer: Peer; private connection?: DataConnection; - private metadata: PeerMetadata; - public players: string[]; public constructor(metadata: PeerMetadata, customPeer?: Peer) { - super(customPeer); - this.metadata = metadata; - this.players = []; + super(metadata); + this.peer = customPeer ? customPeer : new Peer(); + this.peer.on("open", function(id) { + console.info("Peer ID assigned: %s", id); + }); } public connect(peerid: string) { @@ -29,42 +30,13 @@ export class PeerClient extends NetworkPeer { }); } - private _received(data: NetworkMessage) { - this.emit("data", data); - switch (data.kind) { - // Someone changed name (or was forced to) - case "rename": - if (data.oldname == this.metadata.name) { - // We got a name change! - this.metadata.name = data.newname; - } else { - let idx = this.players.indexOf(data.oldname); - if (idx < 0) { - // Weird - console.warn( - `Someone (${data.oldname}) changed name but wasn't on the player list` - ); - this.players.push(data.newname); - break; - } - this.players[idx] = data.newname; - } - this.emit("rename", data.oldname, data.newname); - break; - // A new player joined the room (this includes us) - case "player-joined": - this.players.push(data.name); - this.emit("player-joined", data.name); - break; - default: - // For most cases, we can just use the kind as event type - this.emit(data.kind, data); - } - } - public get name(): string { return this.metadata.name; } + + protected send(conn: DataConnection, data: T) { + conn.send(data); + } } export default PeerClient; diff --git a/src/network/PeerServer.ts b/src/network/PeerServer.ts index ba0049e..12b34ee 100644 --- a/src/network/PeerServer.ts +++ b/src/network/PeerServer.ts @@ -15,7 +15,8 @@ import { NetworkPlayer, AckMessage } from "./types"; -import { NetworkPeer, LocalClient } from "."; +import { LocalClient } from "."; +import EventEmitter from "eventemitter3"; // Increment name, add number at the end if not present // Examples: @@ -32,7 +33,8 @@ function nextName(name: string): string { return name.substr(0, i) + (Number(name.slice(i)) + 1); } -export class PeerServer extends NetworkPeer { +export class PeerServer extends EventEmitter { + protected peer: Peer; private room: Room; public constructor( @@ -40,21 +42,27 @@ export class PeerServer extends NetworkPeer { local: LocalClient, customPeer?: Peer ) { - super(customPeer); + super(); let players: Record = {}; // Add local player to server - players[local.metadata.name] = { + players[local.name] = { kind: "local", - name: local.metadata.name, + name: local.name, client: local }; - local.receiver = this._received.bind(this, players[local.metadata.name]); + local.receiver = this._received.bind(this, players[local.name]); this.room = { info: roomInfo, players }; + + // Setup peer + this.peer = customPeer ? customPeer : new Peer(); + this.peer.on("open", function(id) { + console.info("Peer ID assigned: %s", id); + }); this.peer.on("connection", conn => { this._connection(conn); }); @@ -177,10 +185,18 @@ export class PeerServer extends NetworkPeer { } } - private get playerCount(): number { + public get playerCount(): number { return Object.keys(this.room.players).length; } + public get players() { + return this.room.players; + } + + protected send(conn: DataConnection, data: T) { + conn.send(data); + } + private broadcast(message: T) { for (const playerName in this.room.players) { const player = this.room.players[playerName]; diff --git a/src/network/index.ts b/src/network/index.ts index 66b428b..b90e236 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -1,5 +1,5 @@ export * from "./PeerClient"; export * from "./LocalClient"; -export * from "./NetworkPeer"; +export * from "./Client"; export * from "./PeerServer"; export * from "./types"; diff --git a/src/testing/MockHelper.ts b/src/testing/MockHelper.ts index 8b63404..dc1bdd1 100644 --- a/src/testing/MockHelper.ts +++ b/src/testing/MockHelper.ts @@ -8,11 +8,14 @@ export class MockHelper { createServer( room: RoomInfo, id: string = "test-server", - name: string = "server-client" + player?: LocalClient ) { const serverPeer = new MockPeer(this.mocks, id, {}); - const serverPlayer = new LocalClient({ name: name }); - return new PeerServer(room, serverPlayer, serverPeer as Peer); + return new PeerServer( + room, + player ? player : new LocalClient({ name: "server-player" }), + serverPeer as Peer + ); } createClient(id: string = "test-client", name: string = "client-peer") { diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index a4425f0..cc77397 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -1,5 +1,5 @@ import { MockHelper } from "@/testing"; -import { NetworkMessage } from "@/network"; +import { NetworkMessage, LocalClient } from "@/network"; import { EventHook } from "@/testing/EventHook"; const sampleRoom = () => ({ @@ -47,6 +47,17 @@ describe("network/PeerServer", () => { await hook.expect("client2-data", 1000, messageKind("player-joined")); await hook.expect("client1-data", 1000, messageKind("player-joined")); }); + + test("Local server clients receives client events", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "local-joined"); + client.connect("test-server"); + await hook.expect("local-joined", 1000); + }); }); describe("network/PeerClient", () => { -- 2.40.1 From dd37f3233c83fb9b4328d636f86dc7103154bee1 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:06:19 +0200 Subject: [PATCH 12/17] Add chatting, leaving --- src/network/Client.ts | 19 ++++++++- src/network/PeerClient.ts | 7 ++- src/network/PeerServer.ts | 78 ++++++++++++++++++++++------------ src/network/types.ts | 8 ++++ src/tests/unit/network.spec.ts | 34 ++++++++++++++- 5 files changed, 115 insertions(+), 31 deletions(-) diff --git a/src/network/Client.ts b/src/network/Client.ts index cf4e87b..1268a2d 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -1,7 +1,7 @@ import { PeerMetadata, NetworkMessage } from "./types"; import EventEmitter from "eventemitter3"; -export class Client extends EventEmitter { +export abstract class Client extends EventEmitter { public metadata: PeerMetadata; public players: string[]; @@ -47,6 +47,23 @@ export class Client extends EventEmitter { public get name(): string { return this.metadata.name; } + + public abstract send(data: T): void; + + public leave(): void { + this.send({ + kind: "leave-req" + }); + } + + public say(to: string | null, message: string) { + this.send({ + kind: "chat", + from: this.name, + to: to ? to : "", + message + }); + } } export default Client; diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts index 76b5253..b728dab 100644 --- a/src/network/PeerClient.ts +++ b/src/network/PeerClient.ts @@ -34,8 +34,11 @@ export class PeerClient extends Client { return this.metadata.name; } - protected send(conn: DataConnection, data: T) { - conn.send(data); + public send(data: T) { + if (!this.connection) { + throw new Error("Client is not connected to a server"); + } + this.connection.send(data); } } diff --git a/src/network/PeerServer.ts b/src/network/PeerServer.ts index 12b34ee..40da2ce 100644 --- a/src/network/PeerServer.ts +++ b/src/network/PeerServer.ts @@ -13,7 +13,8 @@ import { RenameMessage, LeaveMessage, NetworkPlayer, - AckMessage + AckMessage, + ChatMessage } from "./types"; import { LocalClient } from "."; import EventEmitter from "eventemitter3"; @@ -70,7 +71,12 @@ export class PeerServer extends EventEmitter { private _connection(conn: DataConnection) { const metadata = conn.metadata as PeerMetadata; - console.info("%s (%s) connected!", metadata.name, conn.label); + + let player: NetworkPlayer = { + kind: "remote", + name: metadata.name, + conn: conn + }; // // Check if this connection should be allowed @@ -78,7 +84,7 @@ export class PeerServer extends EventEmitter { // Check if room is full if (this.playerCount >= this.room.info.max_players) { - this.send(conn, { kind: "error", error: "room is full" }); + this.send(player, { kind: "error", error: "room is full" }); conn.close(); return; } @@ -90,12 +96,13 @@ export class PeerServer extends EventEmitter { while (this.room.players[newname]) { newname = nextName(metadata.name); } - this.send(conn, { + this.send(player, { kind: "rename", oldname: metadata.name, newname: newname }); - metadata.name = newname; + player.name = newname; + player.conn.metadata.name = newname; } // Check for password @@ -104,43 +111,42 @@ export class PeerServer extends EventEmitter { try { let resp = data as PasswordResponse; if (resp.password != this.room.info.password) { - this.send(conn, { + this.send(player, { kind: "error", error: "invalid password" }); return; } conn.off("data", checkPasswordResponse); - this.addPlayer(conn); + this.addPlayer(player); } catch (e) { - this.send(conn, { + this.send(player, { kind: "error", error: "not a password" }); } }; - this.send(conn, { kind: "password-req" }); + this.send(player, { kind: "password-req" }); conn.on("data", checkPasswordResponse.bind(this)); return; } - this.addPlayer(conn); + this.addPlayer(player); } - private addPlayer(conn: DataConnection) { - const playerName = conn.metadata.name; - this.room.players[playerName] = { - kind: "remote", - name: conn.metadata.name, - conn: conn - }; + private addPlayer(player: NetworkPlayer) { + const playerName = player.name; + this.room.players[playerName] = player; // Start listening for new messages - conn.on("data", this._received.bind(this, this.room.players[playerName])); + player.conn.on( + "data", + this._received.bind(this, this.room.players[playerName]) + ); // Send the player info about the room - this.send(conn, { + this.send(player, { kind: "room-info", room: { ...this.room.info, @@ -158,7 +164,7 @@ export class PeerServer extends EventEmitter { private removePlayer(player: NetworkPlayer) { // Tell the player everything's fine - this.send(player.conn, { kind: "ok", what: "leave-req" }); + this.send(player, { kind: "ok", what: "leave-req" }); // Close connection with player player.conn.close(); @@ -172,6 +178,24 @@ export class PeerServer extends EventEmitter { private _received(player: Player, data: NetworkMessage) { switch (data.kind) { + // Player wants to say something in chat + case "chat": + data.from = player.name; + if (data.to == "") { + // Players is saying that out loud + this.broadcast(data); + } else { + // Player is telling someone specifically + if (data.to in this.players) { + this.send(this.players[data.to], data); + } else { + this.send(player, { + kind: "error", + error: `player not found: ${data.to}` + }); + } + } + break; // Player is leaving! case "leave-req": // If we're leaving, end the server @@ -193,18 +217,18 @@ export class PeerServer extends EventEmitter { return this.room.players; } - protected send(conn: DataConnection, data: T) { - conn.send(data); + protected send(player: Player, message: T) { + if (player.kind == "remote") { + player.conn.send(message); + } else { + player.client.receive(message); + } } private broadcast(message: T) { for (const playerName in this.room.players) { const player = this.room.players[playerName]; - if (player.kind == "remote") { - this.send(player.conn, message); - } else { - player.client.receive(message); - } + this.send(player, message); } } } diff --git a/src/network/types.ts b/src/network/types.ts index e6991af..a01b192 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -69,6 +69,7 @@ export type NetworkMessage = | JoinMessage | LeaveMessage | RenameMessage + | ChatMessage | AckMessage; export interface PasswordRequest { @@ -106,6 +107,13 @@ export interface RenameMessage { newname: string; } +export interface ChatMessage { + kind: "chat"; + from: string; + to: string; // "" means everyone + message: string; +} + export interface AckMessage { kind: "ok"; what: string; diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index cc77397..cf17778 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -1,5 +1,5 @@ import { MockHelper } from "@/testing"; -import { NetworkMessage, LocalClient } from "@/network"; +import { NetworkMessage, LocalClient, ChatMessage } from "@/network"; import { EventHook } from "@/testing/EventHook"; const sampleRoom = () => ({ @@ -74,4 +74,36 @@ describe("network/PeerClient", () => { // Client must have changed its internal data to match the new name expect(client1.name).not.toEqual(client2.name); }); + + test("Client can leave the room", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "client-joined"); + hook.hookEmitter(local, "player-left", "client-left"); + client.connect("test-server"); + await hook.expect("client-joined", 1000); + client.leave(); + await hook.expect("client-left", 1000); + }); + + test("Clients can chat", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "client-joined"); + hook.hookEmitter(local, "chat", "client-chat"); + client.connect("test-server"); + await hook.expect("client-joined", 1000); + const hellomsg = "Hello everyone"; + client.say("", hellomsg); + await hook.expect("client-chat", 1000, (msg: ChatMessage) => { + expect(msg.from).toEqual("client-peer"); + expect(msg.message).toEqual(hellomsg); + }); + }); }); -- 2.40.1 From 93558b030497fb8f9760312408246e9de5116b59 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:11:38 +0200 Subject: [PATCH 13/17] Handle player leaving on clients --- src/network/Client.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/network/Client.ts b/src/network/Client.ts index 1268a2d..e7c96ec 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -38,6 +38,16 @@ export abstract class Client extends EventEmitter { this.players.push(data.name); this.emit("player-joined", data.name); break; + case "player-left": + let idx = this.players.indexOf(data.name); + if (idx < 0) { + // Weird + console.warn( + `Someone (${data.name}) left but wasn't on the player list` + ); + break; + } + this.players.splice(idx, 1); default: // For most cases, we can just use the kind as event type this.emit(data.kind, data); -- 2.40.1 From d7ba4f911b16c188864a44989818d24b482fda34 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:11:52 +0200 Subject: [PATCH 14/17] Add Jest-generated coverage files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a0dddc6..4583f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store node_modules /dist +coverage # local env files .env.local -- 2.40.1 From d5735661265074d0a2b7465117d23c3a940a43b2 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:15:49 +0200 Subject: [PATCH 15/17] Rename server tests --- src/tests/unit/network.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index cf17778..4fbc8d1 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -14,12 +14,12 @@ function messageKind(kind: string): (msg: NetworkMessage) => void { } describe("network/PeerServer", () => { - test("Create server", () => { + test("A server can be created", () => { const mox = new MockHelper(); mox.createServer(sampleRoom()); }); - test("Test client join", async () => { + test("Clients can join servers and receive data", async () => { const mox = new MockHelper(); const hook = new EventHook(); mox.createServer(sampleRoom()); @@ -30,7 +30,7 @@ describe("network/PeerServer", () => { await hook.expect("client-data", 1000, messageKind("player-joined")); }); - test("Test multiple clients (w/ name collision)", async () => { + test("Servers handle multiple clients and resolve name collisions", async () => { const mox = new MockHelper(); const hook = new EventHook(); mox.createServer(sampleRoom()); -- 2.40.1 From 6f8fac89aab1c98770fca0daa68ddaacbfa162dd Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:23:12 +0200 Subject: [PATCH 16/17] Add password management and tests --- src/network/Client.ts | 11 ++++++++++- src/tests/unit/network.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/network/Client.ts b/src/network/Client.ts index e7c96ec..2ac333a 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -1,4 +1,4 @@ -import { PeerMetadata, NetworkMessage } from "./types"; +import { PeerMetadata, NetworkMessage, PasswordResponse } from "./types"; import EventEmitter from "eventemitter3"; export abstract class Client extends EventEmitter { @@ -48,12 +48,21 @@ export abstract class Client extends EventEmitter { break; } this.players.splice(idx, 1); + case "password-req": + this.emit("password-required"); default: // For most cases, we can just use the kind as event type this.emit(data.kind, data); } } + public password(password: string) { + this.send({ + kind: "password-resp", + password + }); + } + public get name(): string { return this.metadata.name; } diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index 4fbc8d1..3a5a8a5 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -58,6 +58,32 @@ describe("network/PeerServer", () => { client.connect("test-server"); await hook.expect("local-joined", 1000); }); + + test("Server enforces password protection", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const room = sampleRoom(); + const roomPassword = "test-pwd"; + room.password = roomPassword; + const server = mox.createServer(room, "test-server"); + + // This client will try the right password + const client1 = mox.createClient(); + hook.hookEmitter(client1, "data", "client1-data"); + client1.connect("test-server"); + await hook.expect("client1-data", 1000, messageKind("password-req")); + client1.password(roomPassword); + await hook.expect("client1-data", 1000, messageKind("room-info")); + await hook.expect("client1-data", 1000, messageKind("player-joined")); + + // This client will try an incorrect password + const client2 = mox.createClient("test-client2", "another-client"); + hook.hookEmitter(client2, "data", "client2-data"); + client2.connect("test-server"); + await hook.expect("client2-data", 1000, messageKind("password-req")); + client2.password("not the right password"); + await hook.expect("client2-data", 1000, messageKind("error")); + }); }); describe("network/PeerClient", () => { -- 2.40.1 From 03e295b41eed4acf1b62f11328997752c44c5f24 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 14:32:51 +0200 Subject: [PATCH 17/17] Add handshake data handling and tests --- src/network/Client.ts | 16 +++++++++++++--- src/network/PeerServer.ts | 3 +++ src/tests/unit/network.spec.ts | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/network/Client.ts b/src/network/Client.ts index 2ac333a..a6a43c1 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -1,19 +1,29 @@ -import { PeerMetadata, NetworkMessage, PasswordResponse } from "./types"; +import { + PeerMetadata, + NetworkMessage, + PasswordResponse, + RoomInfo +} from "./types"; import EventEmitter from "eventemitter3"; export abstract class Client extends EventEmitter { public metadata: PeerMetadata; - public players: string[]; + public players!: string[]; + public roomInfo!: RoomInfo; public constructor(metadata: PeerMetadata) { super(); this.metadata = metadata; - this.players = []; } protected _received(data: NetworkMessage) { this.emit("data", data); switch (data.kind) { + // Server is sending over player list and room info + case "room-info": + this.roomInfo = data.room; + this.players = data.players; + break; // Someone changed name (or was forced to) case "rename": if (data.oldname == this.metadata.name) { diff --git a/src/network/PeerServer.ts b/src/network/PeerServer.ts index 40da2ce..bfadda2 100644 --- a/src/network/PeerServer.ts +++ b/src/network/PeerServer.ts @@ -59,6 +59,9 @@ export class PeerServer extends EventEmitter { players }; + local.players = Object.keys(this.players); + local.roomInfo = this.room.info; + // Setup peer this.peer = customPeer ? customPeer : new Peer(); this.peer.on("open", function(id) { diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index 3a5a8a5..dfe425d 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -132,4 +132,20 @@ describe("network/PeerClient", () => { expect(msg.message).toEqual(hellomsg); }); }); + + test("Client internal player list gets correctly updated", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "client-joined"); + hook.hookEmitter(local, "player-left", "client-left"); + client.connect("test-server"); + await hook.expect("client-joined", 1000); + expect(local.players).toHaveLength(2); + client.leave(); + await hook.expect("client-left", 1000); + expect(local.players).toHaveLength(1); + }); }); -- 2.40.1