Basic networking #9
13 changed files with 375 additions and 17 deletions
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
5
src/network/index.ts
Normal file
5
src/network/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from "./client";
|
||||
export * from "./local";
|
||||
export * from "./peer";
|
||||
export * from "./server";
|
||||
export * from "./types";
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<string, Player> = {};
|
||||
|
||||
// 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<PasswordRequest>(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;
|
||||
|
|
|
@ -27,7 +27,7 @@ export interface Room {
|
|||
export interface RoomInfo {
|
||||
max_players: number;
|
||||
password: string;
|
||||
game: GameInfo;
|
||||
game?: GameInfo;
|
||||
}
|
||||
|
||||
type GameInfo = MatchInfo | DraftInfo;
|
||||
|
|
219
src/testing/LocalDataConnection.ts
Normal file
219
src/testing/LocalDataConnection.ts
Normal file
|
@ -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<string, LocalPeer>,
|
||||
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<void> {
|
||||
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<unknown> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public createDataChannel(
|
||||
label: string,
|
||||
dataChannelDict?: RTCDataChannelInit
|
||||
): unknown {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public createOffer(options?: RTCOfferOptions): Promise<unknown> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public getConfiguration(): unknown {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public getIdentityAssertion(): Promise<string> {
|
||||
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<RTCStatsReport> {
|
||||
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<void> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public setRemoteDescription(
|
||||
description: RTCSessionDescriptionInit
|
||||
): Promise<void> {
|
||||
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");
|
||||
}
|
||||
}
|
88
src/testing/LocalPeer.ts
Normal file
88
src/testing/LocalPeer.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import Peer, { PeerConnectOption, PeerJSOption } from "peerjs";
|
||||
import LocalDataConnection from "./LocalDataConnection";
|
||||
|
||||
let mocks: Record<string, LocalPeer> = {};
|
||||
|
||||
export default class LocalPeer extends EventEmitter {
|
||||
public connections: Record<string, LocalDataConnection[]>;
|
||||
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() {}
|
||||
}
|
23
src/tests/unit/server.spec.ts
Normal file
23
src/tests/unit/server.spec.ts
Normal file
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue