const { Parent, MESSAGE_EVENT } = require('./thread');
const SocketWrapper = require('./SocketWrapper');
const socketio = require('socket.io');
const { Object3D, constant } = require('@ud-viz/game_shared');
/**
* @callback SocketCallback
* @param {SocketWrapper} socketWrapper
*/
/**
* @callback ReadyForGameCallback
* @param {SocketWrapper} socketWrapper
* @param {string} entryGameObject3DUUID
* @param {object} readyForGameParams
*/
/**
* @classdesc Websocket game service, create threads to simulate gameobject + socket
*/
const SocketService = class {
/**
*
* @param {import('http').Server} httpServer - http server
* @param {object} options - options
* @param {number} [options.pingInterval=2000] - ping interval of the socket connection in ms
* @param {number} [options.pingTimeout=5000] - ping timeout in ms
* @param {Array<SocketCallback>} [options.socketConnectionCallbacks=[]] - callback to apply when socket is connected
* @param {Array<SocketCallback>} [options.socketDisconnectionCallbacks=[]] - callback to apply when socket is disconnected
* @param {Array<ReadyForGameCallback>} [options.socketReadyForGamePromises=[]] - callback to apply when socket is ready for game
*/
constructor(httpServer, options = {}) {
/**
* @type {socketio.Server}
*/
this.io = new socketio.Server(httpServer, {
pingInterval: options.pingInterval || 2000,
pingTimeout: options.pingTimeout || 20000, // 20sec
});
/** @type {Array<SocketCallback>} */
this.socketConnectionCallbacks = options.socketConnectionCallbacks || [];
/** @type {Array<SocketCallback>} */
this.socketDisconnectionCallbacks =
options.socketDisconnectionCallbacks || [];
/** @type {Array<ReadyForGameCallback>} */
this.socketReadyForGamePromises = options.socketReadyForGamePromises || [];
this.io.on('connection', this.onSocketConnection.bind(this));
/**
* threads running a gamecontext
*
@type {Object<string,Parent>} */
this.threads = {};
/**
* socket wrappers currently connected
*
@type {Object<string,SocketWrapper>} */
this.socketWrappers = {};
}
/**
* Stop threads + disconnect socket client + close websocket connection
*
* @returns {Promise} - a promise resolving when all thread have been closed
*/
stop() {
const promises = [];
for (const key in this.threads) {
const thread = this.threads[key];
promises.push(thread.worker.terminate());
}
this.io.disconnectSockets();
this.io.close();
return Promise.all(promises);
}
/**
* Launch thread running game context simulation
*
* @param {Array<object>} gameObjects3D - array of gameobject3D json to simulate
* @param {string} threadProcessPath - path to the thread process
* @param {string=} entryGameObject3DUUID - uuid of default gameobject to connect socket connected
* @returns {Promise} - a promises resolving when all thread have been initialized
*/
loadGameThreads(gameObjects3D, threadProcessPath, entryGameObject3DUUID) {
const promises = [];
gameObjects3D = gameObjects3D.map((json) => Object3D.parseJSON(json));
// default gameobject3D when socket connect
this.defaultEntryGameObject3DUUID =
entryGameObject3DUUID || gameObjects3D[0].uuid;
gameObjects3D.forEach((gameObject3D) => {
this.threads[gameObject3D.uuid] = new Parent(threadProcessPath);
promises.push(
this.threads[gameObject3D.uuid].apply(MESSAGE_EVENT.INIT, {
gameObject3D: gameObject3D,
})
);
this.threads[gameObject3D.uuid].on(
MESSAGE_EVENT.CURRENT_STATE,
(state) => {
this.threads[gameObject3D.uuid].socketWrappers.forEach((sW) => {
sW.sendState(state);
});
}
);
});
return Promise.all(promises);
}
/**
* init
*
* @param {socketio.Socket} socket - new socket connected to game service
*/
onSocketConnection(socket) {
const socketWrapper = new SocketWrapper(socket);
this.socketWrappers[socket.id] = socketWrapper; // register
// wait for client to be ready for game
socket.on(
constant.WEBSOCKET.MSG_TYPE.READY_FOR_GAME,
(readyForGameParams) => {
const entryGameObject3DUUID =
readyForGameParams.entryGameObject3DUUID ||
this.defaultEntryGameObject3DUUID;
if (!this.threads[entryGameObject3DUUID]) {
console.warn('no thread');
return;
}
// apply promises
const promises = [];
this.socketReadyForGamePromises.forEach((c) => {
const p = c(socketWrapper, entryGameObject3DUUID, readyForGameParams);
if (p) promises.push(p);
});
Promise.all(promises).then(() => {
this.threads[entryGameObject3DUUID].addSocketWrapper(socketWrapper);
});
}
);
socket.on('disconnect', () => {
console.log('socket', socket.id, 'disconnected');
// remove socketwrapper in thread
for (const key in this.threads) {
const s = this.threads[key].socketWrappers.filter((el) => {
if (el.socket.id == socket.id) {
return true;
}
});
if (s.length > 0) {
if (s.length != 1)
throw new Error('socket should only be there once');
this.threads[key].removeSocketWrapper(s[0]);
this.socketDisconnectionCallbacks.forEach((c) => {
c(s[0], this.threads[key]);
});
}
}
delete this.socketWrappers[socket.id]; // remove from current socket connected
});
// apply callbacks
this.socketConnectionCallbacks.forEach((c) => {
c(socketWrapper);
});
}
};
module.exports = SocketService;