Basic networking #9

Merged
hamcha merged 17 commits from feature/basic-networking into master 2019-09-06 12:36:11 +00:00
16 changed files with 256 additions and 108 deletions
Showing only changes of commit ab2ef3e803 - Show all commits

View file

@ -1,37 +1,27 @@
module.exports = { module.exports = {
moduleFileExtensions: [ moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"],
'js',
'jsx',
'json',
'vue',
'ts',
'tsx'
],
transform: { transform: {
'^.+\\.vue$': 'vue-jest', "^.+\\.vue$": "vue-jest",
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
'^.+\\.tsx?$': 'ts-jest' "jest-transform-stub",
"^.+\\.tsx?$": "ts-jest"
}, },
transformIgnorePatterns: [ transformIgnorePatterns: ["/node_modules/"],
'/node_modules/'
],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1' "^@/(.*)$": "<rootDir>/src/$1"
}, },
snapshotSerializers: [ snapshotSerializers: ["jest-serializer-vue"],
'jest-serializer-vue'
],
testMatch: [ 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: [ watchPlugins: [
'jest-watch-typeahead/filename', "jest-watch-typeahead/filename",
'jest-watch-typeahead/testname' "jest-watch-typeahead/testname"
], ],
globals: { globals: {
'ts-jest': { "ts-jest": {
babelConfig: true babelConfig: true
} }
} }
} };

View file

@ -1,14 +1,17 @@
import { PeerMetadata, NetworkMessage } from "./types"; import { PeerMetadata, NetworkMessage } from "./types";
import EventEmitter from "eventemitter3";
export class LocalClient { export class LocalClient extends EventEmitter {
public metadata: PeerMetadata; public metadata: PeerMetadata;
public receiver!: (data: NetworkMessage) => void; public receiver!: (data: NetworkMessage) => void;
public constructor(metadata: PeerMetadata) { public constructor(metadata: PeerMetadata) {
super();
this.metadata = metadata; this.metadata = metadata;
} }
public receive(data: NetworkMessage) { public receive(data: NetworkMessage) {
this.emit("data", data);
//TODO //TODO
} }

View file

@ -1,9 +1,11 @@
import Peer, { DataConnection } from "peerjs"; import Peer, { DataConnection } from "peerjs";
import { NetworkMessage } from "./types"; import { NetworkMessage } from "./types";
import EventEmitter from "eventemitter3";
export class NetworkPeer { export class NetworkPeer extends EventEmitter {
protected peer: Peer; protected peer: Peer;
protected constructor(peer?: Peer) { protected constructor(peer?: Peer) {
super();
this.peer = peer ? peer : new Peer(); this.peer = peer ? peer : new Peer();
this.peer.on("open", function(id) { this.peer.on("open", function(id) {
console.info("Peer ID assigned: %s", id); console.info("Peer ID assigned: %s", id);
@ -11,7 +13,6 @@ export class NetworkPeer {
} }
protected send<T extends NetworkMessage>(conn: DataConnection, data: T) { protected send<T extends NetworkMessage>(conn: DataConnection, data: T) {
//TODO Debugging support?
conn.send(data); conn.send(data);
} }
} }

35
src/network/PeerClient.ts Normal file
View file

@ -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;

View file

@ -1,4 +1,3 @@
import NetworkPeer from "./peer";
import Peer, { DataConnection } from "peerjs"; import Peer, { DataConnection } from "peerjs";
import { import {
RoomInfo, RoomInfo,
@ -16,7 +15,7 @@ import {
NetworkPlayer, NetworkPlayer,
AckMessage AckMessage
} from "./types"; } from "./types";
import LocalClient from "./local"; import { NetworkPeer, LocalClient } from ".";
// Increment name, add number at the end if not present // Increment name, add number at the end if not present
// Examples: // Examples:
@ -56,7 +55,9 @@ export class PeerServer extends NetworkPeer {
info: roomInfo, info: roomInfo,
players players
}; };
this.peer.on("connection", this._connection.bind(this)); this.peer.on("connection", conn => {
this._connection(conn);
});
} }
private _connection(conn: DataConnection) { private _connection(conn: DataConnection) {
@ -77,7 +78,10 @@ export class PeerServer extends NetworkPeer {
// Check if there is already a player called that way // Check if there is already a player called that way
if (this.room.players[metadata.name]) { if (this.room.players[metadata.name]) {
// Force rename // Force rename
const newname = nextName(metadata.name); let newname = metadata.name;
while (this.room.players[newname]) {
newname = nextName(metadata.name);
}
this.send<RenameMessage>(conn, { this.send<RenameMessage>(conn, {
kind: "rename", kind: "rename",
oldname: metadata.name, oldname: metadata.name,

View file

@ -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;

View file

@ -1,5 +1,5 @@
export * from "./client"; export * from "./PeerClient";
export * from "./local"; export * from "./LocalClient";
export * from "./peer"; export * from "./NetworkPeer";
export * from "./server"; export * from "./PeerServer";
export * from "./types"; export * from "./types";

View file

@ -1,5 +1,5 @@
import { DataConnection } from "peerjs"; import { DataConnection } from "peerjs";
import LocalClient from "./local"; import LocalClient from "./LocalClient";
export interface LocalPlayer { export interface LocalPlayer {
kind: "local"; kind: "local";

63
src/testing/EventHook.ts Normal file
View file

@ -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<string, HookedEvent[]> = {};
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<void> {
// Build promise
const promise = new Promise<void>((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);
}
}

View file

@ -1,19 +1,19 @@
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import LocalPeer from "./LocalPeer"; import MockPeer from "./MockPeer";
import { PeerConnectOption } from "peerjs"; import { PeerConnectOption } from "peerjs";
export default class LocalDataConnection extends EventEmitter { export class MockDataConnection extends EventEmitter {
public options: PeerConnectOption; public options: PeerConnectOption;
public peer: string; public peer: string;
public provider: LocalPeer; public provider: MockPeer;
public open: boolean = false; public open: boolean = false;
private _connection?: LocalDataConnection; private _connection?: MockDataConnection;
public id: string; public id: string;
public label: string; public label: string;
public constructor( public constructor(
peer: string, peer: string,
provider: LocalPeer, provider: MockPeer,
options?: PeerConnectOption options?: PeerConnectOption
) { ) {
super(); super();
@ -57,7 +57,7 @@ export default class LocalDataConnection extends EventEmitter {
this.emit("close"); this.emit("close");
} }
public _setRemote(connection: LocalDataConnection) { public _setRemote(connection: MockDataConnection) {
this._connection = connection; this._connection = connection;
this._connection.on("open", () => { this._connection.on("open", () => {
if (!this.open) { if (!this.open) {
@ -68,7 +68,7 @@ export default class LocalDataConnection extends EventEmitter {
} }
public _configureDataChannel( public _configureDataChannel(
mocks: Record<string, LocalPeer>, mocks: Record<string, MockPeer>,
options?: PeerConnectOption options?: PeerConnectOption
) { ) {
if (!this._connection && this.peer in mocks) { if (!this._connection && this.peer in mocks) {
@ -217,3 +217,5 @@ export default class LocalDataConnection extends EventEmitter {
throw new Error("not implemented"); throw new Error("not implemented");
} }
} }
export default MockDataConnection;

View file

@ -1,30 +1,35 @@
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import Peer, { PeerConnectOption, PeerJSOption } from "peerjs"; import Peer, { PeerConnectOption, PeerJSOption } from "peerjs";
import LocalDataConnection from "./LocalDataConnection"; import MockDataConnection from "./MockDataConnection";
let mocks: Record<string, LocalPeer> = {}; export class MockPeer extends EventEmitter {
public connections: Record<string, MockDataConnection[]>;
export default class LocalPeer extends EventEmitter {
public connections: Record<string, LocalDataConnection[]>;
public id: string; public id: string;
public options: PeerJSOption; public options: PeerJSOption;
public disconnected: boolean; public disconnected: boolean;
public destroyed: boolean; public destroyed: boolean;
public prototype: unknown; public prototype: unknown;
private mocks: Record<string, MockPeer>;
public constructor(id: string, options?: PeerJSOption) { public constructor(
mockSource: Record<string, MockPeer>,
id: string,
options?: PeerJSOption
) {
super(); super();
this.id = id; this.id = id;
this.options = options ? options : {}; this.options = options ? options : {};
this.connections = {}; this.connections = {};
this.disconnected = false; this.disconnected = false;
this.destroyed = false; this.destroyed = false;
mocks[id] = this; this.mocks = mockSource;
this.mocks[id] = this;
} }
public connect(id: string, options?: PeerConnectOption): LocalDataConnection { public connect(id: string, options?: PeerConnectOption): MockDataConnection {
let connection = new LocalDataConnection(id, this, options); let connection = new MockDataConnection(id, this, options);
connection._configureDataChannel(mocks, options); // hacky way to avoid race conditions
setTimeout(() => connection._configureDataChannel(this.mocks, options), 1);
this._addConnection(id, connection); this._addConnection(id, connection);
return connection; return connection;
} }
@ -41,7 +46,7 @@ export default class LocalPeer extends EventEmitter {
this.disconnect(); this.disconnect();
} }
private _addConnection(peer: string, connection: LocalDataConnection) { private _addConnection(peer: string, connection: MockDataConnection) {
if (!this.connections[peer]) { if (!this.connections[peer]) {
this.connections[peer] = []; this.connections[peer] = [];
} }
@ -50,10 +55,10 @@ export default class LocalPeer extends EventEmitter {
public _negotiate( public _negotiate(
peer: string, peer: string,
remoteConnection: LocalDataConnection, remoteConnection: MockDataConnection,
options?: PeerConnectOption options?: PeerConnectOption
) { ) {
let localConnection = new LocalDataConnection(peer, this, options); let localConnection = new MockDataConnection(peer, this, options);
localConnection._setRemote(remoteConnection); localConnection._setRemote(remoteConnection);
this._addConnection(peer, localConnection); this._addConnection(peer, localConnection);
this.emit("connection", localConnection); this.emit("connection", localConnection);
@ -73,9 +78,9 @@ export default class LocalPeer extends EventEmitter {
public getConnection( public getConnection(
peerId: string, peerId: string,
connectionId: string connectionId: string
): LocalDataConnection | null { ): MockDataConnection | null {
if (peerId in mocks) { if (peerId in this.mocks) {
let map = mocks[peerId].connections; let map = this.mocks[peerId].connections;
if (connectionId in map) { if (connectionId in map) {
return map[connectionId][0]; return map[connectionId][0];
} }
@ -86,3 +91,5 @@ export default class LocalPeer extends EventEmitter {
public listAllPeers() {} public listAllPeers() {}
} }
export default MockPeer;

2
src/testing/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from "./MockDataConnection";
export * from "./MockPeer";

View file

@ -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<string, MockPeer> {
return {};
}
function createServer(
mockSource: Record<string, MockPeer>,
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<string, MockPeer>,
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
});

View file

@ -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
);
});
});

1
src/utils/index.ts Normal file
View file

@ -0,0 +1 @@
export * from "./timeout";

9
src/utils/timeout.ts Normal file
View file

@ -0,0 +1,9 @@
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<T>((resolve, reject) => {
let id = setTimeout(() => {
clearTimeout(id);
reject("Timed out after " + ms + "ms.");
}, ms);
});
return Promise.race<T>([promise, timeout]);
}