boardgame.io
Version:
library for turn-based games
158 lines (138 loc) • 4.35 kB
text/typescript
/*
* Copyright 2018 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
import IO from 'koa-socket-2';
import { ServerOptions as SocketOptions } from 'socket.io';
import { ServerOptions as HttpsOptions } from 'https';
import {
Master,
TransportAPI as MasterTransport,
AuthFn,
} from '../../master/master';
import { PlayerID } from '../../types';
const PING_TIMEOUT = 20 * 1e3;
const PING_INTERVAL = 10 * 1e3;
/**
* API that's exposed by SocketIO for the Master to send
* information to the clients.
*/
export function TransportAPI(
gameID: string,
socket,
clientInfo: Map<any, any>,
roomInfo: Map<any, any>
): MasterTransport {
/**
* Send a message to a specific client.
*/
const send: MasterTransport['send'] = ({ type, playerID, args }) => {
const clients = roomInfo.get(gameID).values();
for (const client of clients) {
const info = clientInfo.get(client);
if (info.playerID == playerID) {
if (socket.id == client) {
socket.emit.apply(socket, [type, ...args]);
} else {
socket.to(info.socket.id).emit.apply(socket, [type, ...args]);
}
}
}
};
/**
* Send a message to all clients.
*/
const sendAll: MasterTransport['sendAll'] = makePlayerData => {
roomInfo.get(gameID).forEach(c => {
const playerID: PlayerID = clientInfo.get(c).playerID;
const data = makePlayerData(playerID);
send({ playerID, ...data });
});
};
return { send, sendAll };
}
export interface SocketOpts {
auth?: boolean | AuthFn;
https?: HttpsOptions;
socketOpts?: SocketOptions;
socketAdapter?: any;
}
/**
* Transport interface that uses socket.io
*/
export class SocketIO {
protected clientInfo: Map<any, any>;
protected roomInfo: Map<any, any>;
private auth: boolean | AuthFn;
private https: HttpsOptions;
private socketAdapter: any;
private socketOpts: SocketOptions;
constructor({ auth = true, https, socketAdapter, socketOpts }: SocketOpts = {}) {
this.clientInfo = new Map();
this.roomInfo = new Map();
this.auth = auth;
this.https = https;
this.socketAdapter = socketAdapter;
this.socketOpts = socketOpts;
}
init(app, games) {
const io = new IO({
ioOptions: {
pingTimeout: PING_TIMEOUT,
pingInterval: PING_INTERVAL,
...this.socketOpts,
},
});
app.context.io = io;
io.attach(app, !!this.https, this.https);
if (this.socketAdapter) {
io.adapter(this.socketAdapter);
}
for (const game of games) {
const nsp = app._io.of(game.name);
nsp.on('connection', socket => {
socket.on('update', async (action, stateID, gameID, playerID) => {
const master = new Master(
game,
app.context.db,
TransportAPI(gameID, socket, this.clientInfo, this.roomInfo),
this.auth
);
await master.onUpdate(action, stateID, gameID, playerID);
});
socket.on('sync', async (gameID, playerID, numPlayers) => {
socket.join(gameID);
// Remove client from any previous game that it was a part of.
if (this.clientInfo.has(socket.id)) {
const { gameID: oldGameID } = this.clientInfo.get(socket.id);
this.roomInfo.get(oldGameID).delete(socket.id);
}
let roomClients = this.roomInfo.get(gameID);
if (roomClients === undefined) {
roomClients = new Set();
this.roomInfo.set(gameID, roomClients);
}
roomClients.add(socket.id);
this.clientInfo.set(socket.id, { gameID, playerID, socket });
const master = new Master(
game,
app.context.db,
TransportAPI(gameID, socket, this.clientInfo, this.roomInfo),
this.auth
);
await master.onSync(gameID, playerID, numPlayers);
});
socket.on('disconnect', () => {
if (this.clientInfo.has(socket.id)) {
const { gameID } = this.clientInfo.get(socket.id);
this.roomInfo.get(gameID).delete(socket.id);
this.clientInfo.delete(socket.id);
}
});
});
}
}
}