@needle-tools/networking
Version:
Networking backend functionality for Needle Engine
202 lines (185 loc) • 6.25 kB
JavaScript
const { saveBlob } = require("./storage.js");
(() => {
// import types
const Room = require("./room");
const Utils = require("./networking_utils");
const ClientSettings = require("./clientSettings");
const SchemesUtils = require("./schemeUtility");
// max users
let currentUserCount = 0;
let maxConcurrentUsers = 50;
let defaultRoomTimeoutInSeconds = 30;
/**
* @param {import("express").Express} app
* @param {import("../@types/index").NetworkingOptions} options
*/
module.exports.init = function (app, options) {
if (options) {
if (options.maxUsers !== undefined && typeof options.maxUsers === "number") {
maxConcurrentUsers = options.maxUsers;
}
if (options.defaultUserTimeout !== undefined && options.defaultUserTimeout > 0) {
defaultRoomTimeoutInSeconds = options.defaultUserTimeout;
if (options.defaultUserTimeout < 5) console.warn("User Room timeout is set very low: " + options.defaultUserTimeout + " seconds. Consider increasing it to ~30 seconds or more.");
}
}
// close room after x minutes
const idleTime = 5 * 60 * 1000;
setInterval(function () {
module.exports.validateRooms(idleTime);
}, 5000);
// save rooms every minute
setInterval(function () {
module.exports.saveRooms();
}, 10 * 1000);
}
module.exports.onConnection = async function (ws) {
if (ws.id === undefined) ws.id = Utils.getUniqueID();
console.log("new user", ws.id);
ws.send(
JSON.stringify({ key: "connection-start-info", data: { id: ws.id } })
);
ws.on("close", function (arg) {
const room = ws.room;
if (!room) return;
console.log("Connection closed to " + ws.id);
room.remove(ws);
});
ws.on("message", function (data) {
try {
const type = typeof data;
if (type === "string") {
// JSON messages
const obj = JSON.parse(data);
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const msg = obj[i];
const type = msg.key;
const payload = msg.data;
const raw = JSON.stringify(msg);
handleMessage(ws, type, payload, raw);
}
} else {
const type = obj.key;
const payload = obj.data;
handleMessage(ws, type, payload, data);
}
}
else {
// BINARY messages
const payload = SchemesUtils.unpackBinaryIntoPayload(data);
handleMessage(ws, payload.key, payload, data);
// if(ws.room){
// ws.room.broadcast(ws, data, true);
// }
// const bb = new flatbuffers.ByteBuffer(data);
// const type = bb.getBufferIdentifier();
// handleMessage(ws, type, bb, data);
// ws.send(data);
}
} catch (err) {
console.error(err);
}
});
};
module.exports.getUserCount = function () {
let sum = 0;
for (const room of rooms) {
sum += room.userCount();
}
currentUserCount = sum;
return sum;
};
module.exports.saveRooms = function () {
for (let i = rooms.length - 1; i >= 0; i--) {
const room = rooms[i];
if (room.userCount() > 0 && room.isDirty) {
room.saveState();
}
}
}
module.exports.validateRooms = function (maxAge) {
const logThreshold = maxAge * 0.5;
for (let i = rooms.length - 1; i >= 0; i--) {
const room = rooms[i];
if (!room) {
room.splice(i, 1);
continue;
}
room.validateClients();
const age = room.timeSinceLastMessage();
if (age > logThreshold)
console.log(`Room ${room.id} time since last update: ${age}/${maxAge}`);
if (age > maxAge) {
console.log(`Room ${room.id} timeout!`);
room.saveState(true);
room.close();
rooms.splice(i, 1);
}
}
};
/** @type {import("./room.js")[]} */
const rooms = [];
function getOrCreateRoom(roomName, viewOnly) {
for (const room of rooms) {
if (room.id === roomName) {
console.log("found " + roomName);
return room;
}
}
//if(viewOnly) return;
console.log("open new room: " + roomName);
const nr = new Room(roomName);
nr.clientTimeout = defaultRoomTimeoutInSeconds;
nr.loadState();
rooms.push(nr);
return nr;
}
function handleMessage(ws, type, payload, rawData) {
if (type === "join-room") {
if (currentUserCount > maxConcurrentUsers) {
// server is full
console.log("Server is full");
return;
}
let roomName = payload.room;
const viewOnly = payload.viewOnly;
// if the request is view only we know that the roomName is encrypted here
// we now need to get the original name (e.g. if the room is currently not open we still want to be able to open/load the original)
// TODO: add check if the room exists (is either open or was saved)
if (viewOnly === true) {
roomName = Utils.decryptID(roomName);
console.log(roomName);
}
const room = getOrCreateRoom(roomName, viewOnly);
if (room) {
const cs = new ClientSettings(viewOnly)
room.add(ws, cs);
}
else {
console.warn("Could not find room for " + roomName);
}
return;
} else if (type === "leave-room") {
const roomName = payload.room;
const room = ws.room;
if (room && room.id === roomName) {
room.remove(ws);
}
return;
}
else if (type === "upload-blob") {
const md5 = payload.md5;
if (!md5) {
return ws.send(JSON.stringify({ key: "upload-blob", data: { error: "missing md5" } }));
}
}
if (ws.room) {
ws.room.handleMessage(ws, type, payload, rawData);
} else if (type === "ping") {
// it is a ping but user is not in a room
} else {
//console.warn("networking: unhandled message", rawData);
}
}
})();