From 55f83ee3a769130c1af0c8e5cf237c34ac89eae1 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Wed, 6 Oct 2021 18:26:41 -0500 Subject: [PATCH 01/24] implemented test refactor suggestions --- test/test-sync.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/test/test-sync.js b/test/test-sync.js index 7b84d60..68e1e28 100644 --- a/test/test-sync.js +++ b/test/test-sync.js @@ -115,7 +115,6 @@ describe("Sync Server: Sessions", function (done) { // assert.deepStrictEqual(singularEntry[1].writers, expectedSession.writers); assert.deepStrictEqual(singularEntry[1].message_buffer, expectedSession.message_buffer); - }); it("should return failure on getting a nonexistent session", function () { @@ -438,28 +437,34 @@ describe("Sync Server: Integration", function (done) { //this.joinSessionAction(session_id, client_id); }); - it("should perform a bump properly", function () { + it("should create a correct clients array", function () { let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); sessions = syncServer.getSessions(); let singularEntry; - // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? - for (let entry of sessions) { singularEntry = entry; } - //TODO - factor this out into a separate test? - it("should create a correct clients array" - const expectedClients = [ CLIENT_ID ]; singularEntry[1].clients.length.should.equal(expectedClients.length); singularEntry[1].clients[0].should.equal(expectedClients[0]); + }); - //TODO - factor this out into a separate test? - it("should create a correct sockets object" + it("should create a correct sockets object", function () { + let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + + sessions = syncServer.getSessions(); + + let singularEntry; + + for (let entry of sessions) { + singularEntry = entry; + } const socketA = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; @@ -468,9 +473,9 @@ describe("Sync Server: Integration", function (done) { numSockets.should.equal(1); singularEntry[1].sockets[DUMMY_SOCKET_A.id].should.eql(socketA); - - // duplicated here - + }); + + it("should perform a bump properly", function () { syncServer.bumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); @@ -484,6 +489,8 @@ describe("Sync Server: Integration", function (done) { client_id.should.equal(CLIENT_ID); }; + + let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); success = syncServer.handleJoin(null, DUMMY_SOCKET_B, SESSION_ID, CLIENT_ID, true); @@ -497,14 +504,12 @@ describe("Sync Server: Integration", function (done) { singularEntry = entry; } - //TODO - factor this out into a separate test? - it("should create a correct clients array" + const expectedClients = [ CLIENT_ID ]; singularEntry[1].clients.length.should.equal(expectedClients.length); singularEntry[1].clients[0].should.equal(expectedClients[0]); - //TODO - factor this out into a separate test? - it("should create a correct sockets object" - numSockets = Object.keys(singularEntry[1].sockets).length; numSockets.should.equal(1); @@ -514,5 +519,7 @@ describe("Sync Server: Integration", function (done) { assert(singularEntry[1].sockets[DUMMY_SOCKET_B.id] != null); singularEntry[1].sockets[DUMMY_SOCKET_B.id].should.eql(socketB); + + assert(singularEntry[1].sockets[DUMMY_SOCKET_A.id] == null); }); }); \ No newline at end of file From a178cfff8bf3c00b7536f4876bcf41611ac158ac Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 7 Oct 2021 13:25:59 -0500 Subject: [PATCH 02/24] made state catch-up private; added two bump tests --- sync.js | 4 ++-- test/test-sync.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/sync.js b/sync.js index 5b7eda0..7d0561d 100644 --- a/sync.js +++ b/sync.js @@ -1486,6 +1486,7 @@ module.exports = { }); }); + // When a client requests a state catch-up, send the current session state. Supports versioning. socket.on('state', function(data) { let { session_id, state } = self.handleState(socket, data); @@ -1497,7 +1498,7 @@ module.exports = { try { // emit versioned state data - io.to(session_id).emit('state', state); + io.to(socket.id).emit('state', state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. } catch (err) { this.logErrorSessionClientSocketAction(session_id, null, socket.id, err.message); } @@ -1532,7 +1533,6 @@ module.exports = { // get reference to session and parse message payload for state updates, if needed. let session = self.sessions.get(session_id); if (session) { - if (type == "interaction") { // `message` here will be in the legacy packed-array format. diff --git a/test/test-sync.js b/test/test-sync.js index 68e1e28..82a441d 100644 --- a/test/test-sync.js +++ b/test/test-sync.js @@ -233,7 +233,56 @@ describe("Sync Server: Clients and Sockets", function (done) { session.clients.should.eql(expectedClients); }); - it("should be able to bump an existing socket", function () { + it("should be able to bump one existing socket", function () { + let session = { + clients: [ CLIENT_ID, CLIENT_ID ], + sockets: { } + }; + + session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + session.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + syncServer.sessions.set(SESSION_ID, session); + + let outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_A.id, DUMMY_SOCKET_B.id ] ); + + let bumpCount = 0; + + syncServer.bumpAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.equal(DUMMY_SOCKET_A); + + bumpCount += 1; + }; + + let disconnectCount = 0; + + syncServer.disconnectAction = function (socket, session_id, client_id) { + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + + socket.should.equal(DUMMY_SOCKET_A); + + disconnectCount += 1; + }; + + syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_B.id); + + bumpCount.should.eql(1); + + disconnectCount.should.eql(1); + + outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_B.id ] ); + }); + + it("should be able to bump two existing sockets", function () { let session = { clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], sockets: { } From d004a5f42ee5aaa794d2e9792523fcf53313643d Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Wed, 13 Oct 2021 19:13:34 -0500 Subject: [PATCH 03/24] WIP: renamed events to clarify built-in vs Komodo events --- sync.js | 103 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 37 deletions(-) diff --git a/sync.js b/sync.js index 7d0561d..11ac526 100644 --- a/sync.js +++ b/sync.js @@ -77,6 +77,35 @@ function compareKeys(a, b) { return JSON.stringify(aKeys) === JSON.stringify(bKeys); } +const SocketIOEvents = { + disconnect: 'disconnect', +}; + +const KomodoReceiveEvents = { + join: 'join', + sessionInfo: 'sessionInfo', + state: 'state', + draw: 'draw', + message: 'message', + update: 'update', + interact: 'interact', + start_recording: 'start_recording', + end_recording: 'end_recording', + playback: 'playback', +}; + +const KomodoSendEvents = { + connectionError: 'connectionError', + interactionUpdate: 'interactionUpdate', + joined: 'joined', + disconnected: 'disconnected', + sessionInfo: 'sessionInfo', + state: 'state', + draw: 'draw', + message: 'message', + relayUpdate: 'relayUpdate', +}; + module.exports = { // NOTE(rob): deprecated. sessions must use message_buffer. // // write buffers are multiples of corresponding chunks @@ -179,7 +208,7 @@ module.exports = { }, start_recording: function (pool, session_id) {// TODO(rob): require client id and token - console.log(`start_recording called with pool: ${pool}, session: ${session_id}`) + console.log(`start_recording called with pool: ${pool}, session: ${session_id}`); let session = this.sessions.get(session_id); if (!session) { this.logErrorSessionClientSocketAction(session_id, null, null, `Tried to start recording, but session was null`); @@ -381,7 +410,7 @@ module.exports = { // // } // // // emit audio manifest to connected clients - // // io.of('chat').to(session_id.toString()).emit('playbackAudioManifest', audioManifest); + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioManifest', audioManifest); // // // stream all audio files for caching and playback by client // // audioManifest.forEach((file) => { @@ -389,7 +418,7 @@ module.exports = { // // file.data = data; // // if(err) if (this.logger) this.logger.error(`Error reading audio file: ${file.path}`); // // // console.log('emitting audio packet:', file); - // // io.of('chat').to(session_id.toString()).emit('playbackAudioData', file); + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioData', file); // // }); // // }); @@ -401,7 +430,7 @@ module.exports = { // let playbackStart = Date.now(); // // position data emit loop - // stream.on('data', function(chunk) { + // stream.on(KomodoReceiveEvents.data, function(chunk) { // stream.pause(); // // start data buffer loop @@ -426,30 +455,30 @@ module.exports = { // // if (!audioStarted) { // // // HACK(rob): trigger clients to begin playing buffered audio // // audioStarted = true; - // // io.of('chat').to(session_id.toString()).emit('startPlaybackAudio'); + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.startPlaybackAudio'); // // } - // io.to(session_id.toString()).emit('relayUpdate', arr); + // io.to(session_id.toString()).emit(KomodoSendEvents.relayUpdate', arr); // stream.resume(); // clearInterval(timer); // } // }, 1); // }); - // stream.on('error', function(err) { + // stream.on(KomodoReceiveEvents.error, function(err) { // if (this.logger) this.logger.error(`Error creating position playback stream for ${playback_id} ${start}: ${err}`); - // io.to(session_id.toString()).emit('playbackEnd'); + // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); // }); - // stream.on('end', function() { + // stream.on(KomodoReceiveEvents.end, function() { // if (this.logger) this.logger.info(`End of pos data for playback session: ${session_id}`); - // io.to(session_id.toString()).emit('playbackEnd'); + // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); // }); // // interaction streaming // let ipath = this.getCapturePath(capture_id, start, 'int'); // let istream = fs.createReadStream(ipath, { highWaterMark: interactionChunkSize() }); - // istream.on('data', function(chunk) { + // istream.on(KomodoReceiveEvents.data, function(chunk) { // istream.pause(); // let buff = Buffer.from(chunk); @@ -463,7 +492,7 @@ module.exports = { // // console.log(`=== INT === current seq ${current_seq}; arr seq ${arr[INT_FIELDS-1]}`); // if (arr[INT_FIELDS-1] <= current_seq) { - // io.to(session_id.toString()).emit('interactionUpdate', arr); + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionUpdate', arr); // istream.resume(); // clearInterval(timer); // } @@ -471,14 +500,14 @@ module.exports = { // }); - // istream.on('error', function(err) { + // istream.on(KomodoReceiveEvents.error, function(err) { // if (this.logger) this.logger.error(`Error creating interaction playback stream for session ${session_id}: ${err}`); - // io.to(session_id.toString()).emit('interactionpPlaybackEnd'); + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionpPlaybackEnd'); // }); - // istream.on('end', function() { + // istream.on(KomodoReceiveEvents.end, function() { // if (this.logger) this.logger.info(`End of int data for playback session: ${session_id}`); - // io.to(session_id.toString()).emit('interactionPlaybackEnd'); + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionPlaybackEnd'); // }); // } }, @@ -597,7 +626,7 @@ module.exports = { if (session_id && client_id) { // relay interaction events to all connected clients - socket.to(session_id.toString()).emit('interactionUpdate', data); + socket.to(session_id.toString()).emit(KomodoSendEvents.interactionUpdate, data); // do session state update if needed let source_id = data[3]; @@ -1422,16 +1451,16 @@ module.exports = { }; this.joinSessionAction = function (session_id, client_id) { - io.to(session_id.toString()).emit('joined', client_id); + io.to(session_id.toString()).emit(KomodoSendEvents.joined, client_id); }; this.disconnectAction = function (socket, session_id, client_id) { // notify and log event - socket.to(session_id.toString()).emit('disconnected', client_id); + socket.to(session_id.toString()).emit(KomodoSendEvents.disconnected, client_id); }; this.stateErrorAction = function (socket, message) { - socket.emit('stateError', message); + socket.emit(KomodoSendEvents.stateError, message); }; // returns true for successful reconnection @@ -1444,10 +1473,10 @@ module.exports = { }; // main relay handler - io.on('connection', function(socket) { + io.on(KomodoReceiveEvents.connection, function(socket) { self.logInfoSessionClientSocketAction(null, null, socket.id, `Session connection`); - socket.on('sessionInfo', function (session_id) { + socket.on(KomodoReceiveEvents.sessionInfo, function (session_id) { let session = self.sessions.get(session_id); if (!session) { @@ -1456,11 +1485,11 @@ module.exports = { return; } - socket.to(session_id.toString()).emit('sessionInfo', session); + socket.to(session_id.toString()).emit(KomodoSendEvents.sessionInfo, session); }); //Note: "join" is our own event name, and should not be confused with socket.join. (It does not automatically listen for socket.join either.) - socket.on('join', function(data) { + socket.on(KomodoReceiveEvents.join, function(data) { let session_id = data[0]; let client_id = data[1]; @@ -1487,7 +1516,7 @@ module.exports = { }); // When a client requests a state catch-up, send the current session state. Supports versioning. - socket.on('state', function(data) { + socket.on(KomodoReceiveEvents.state, function(data) { let { session_id, state } = self.handleState(socket, data); if (session_id == -1 || !state) { @@ -1498,19 +1527,19 @@ module.exports = { try { // emit versioned state data - io.to(socket.id).emit('state', state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. + io.to(socket.id).emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. } catch (err) { this.logErrorSessionClientSocketAction(session_id, null, socket.id, err.message); } }); - socket.on('draw', function(data) { + socket.on(KomodoReceiveEvents.draw, function(data) { let session_id = data[1]; let client_id = data[2]; if (session_id && client_id) { - socket.to(session_id.toString()).emit('draw', data); + socket.to(session_id.toString()).emit(KomodoSendEvents.draw, data); } }); @@ -1520,7 +1549,7 @@ module.exports = { // of the various interactions we care about, eg. grab, drop, start/end recording, etc. // in order to update the session state accordingly. we will probably need to protect against // garbage values that might be passed by devs who are overwriting reserved message events. - socket.on('message', function(data) { + socket.on(KomodoReceiveEvents.message, function(data) { if (data) { let session_id = data.session_id; let client_id = data.client_id; @@ -1528,7 +1557,7 @@ module.exports = { if (session_id && client_id && type && data.message) { // relay the message - socket.to(session_id.toString()).emit('message', data); + socket.to(session_id.toString()).emit(KomodoSendEvents.message, data); // get reference to session and parse message payload for state updates, if needed. let session = self.sessions.get(session_id); @@ -1685,7 +1714,7 @@ module.exports = { }); // client position update handler - socket.on('update', function(data) { + socket.on(KomodoReceiveEvents.update, function(data) { if (!self.isValidRelayPacket(data)) { return; } @@ -1693,7 +1722,7 @@ module.exports = { let session_id = data[1]; // relay packet if client is valid - socket.to(session_id.toString()).emit('relayUpdate', data); + socket.to(session_id.toString()).emit(KomodoSendEvents.relayUpdate, data); // self.writeRecordedRelayData(data); NOTE(rob): DEPRECATED. 8/5/21. @@ -1702,24 +1731,24 @@ module.exports = { // handle interaction events // see `INTERACTION_XXX` declarations for type values - socket.on('interact', function(data) { + socket.on(KomodoReceiveEvents.interact, function(data) { self.handleInteraction(socket, data); }); // session capture handler - socket.on('start_recording', function (session_id) { + socket.on(KomodoReceiveEvents.start_recording, function (session_id) { self.start_recording(pool, session_id); }); - socket.on('end_recording', function (session_id) { + socket.on(KomodoReceiveEvents.end_recording, function (session_id) { self.end_recording(pool, session_id); }); - socket.on('playback', function(data) { + socket.on(KomodoReceiveEvents.playback, function(data) { self.handlePlayback(io, data); }); - socket.on('disconnect', function (reason) { + socket.on(SocketIOEvents.disconnect, function (reason) { const { session_id, client_id } = self.whoDisconnected(socket); let didReconnect = self.handleDisconnect(socket, reason); From e56c0c2d4ba65acd5308c6b436e9678c07c1e5d6 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 14 Oct 2021 00:30:55 -0500 Subject: [PATCH 04/24] WIP: refactoring sync and interaction message processing, introducing constants --- sync.js | 3512 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 2094 insertions(+), 1418 deletions(-) diff --git a/sync.js b/sync.js index 11ac526..7afdc4f 100644 --- a/sync.js +++ b/sync.js @@ -34,17 +34,17 @@ /* jshint esversion: 6 */ // configuration -const config = require('./config'); +const config = require("./config"); -const fs = require('fs'); +const fs = require("fs"); -const path = require('path'); +const path = require("path"); -const util = require('util'); -const { syslog } = require('winston/lib/winston/config'); +const util = require("util"); +const { syslog } = require("winston/lib/winston/config"); // event data globals -// NOTE(rob): deprecated. +// NOTE(rob): deprecated. // const POS_FIELDS = 14; // const POS_BYTES_PER_FIELD = 4; // const POS_COUNT = 10000; @@ -56,1712 +56,2388 @@ const { syslog } = require('winston/lib/winston/config'); // TODO(rob): finish deprecate. // const INTERACTION_LOOK = 0; // const INTERACTION_LOOK_END = 1; -const INTERACTION_RENDER = 2; -const INTERACTION_RENDER_END = 3; +const INTERACTION_RENDER = 2; +const INTERACTION_RENDER_END = 3; // const INTERACTION_GRAB = 4; // const INTERACTION_GRAB_END = 5; -const INTERACTION_SCENE_CHANGE = 6; +const INTERACTION_SCENE_CHANGE = 6; // const INTERACTION_UNSET = 7; // NOTE(rob): this value is currently unused. 2020-12-1 -const INTERACTION_LOCK = 8; -const INTERACTION_LOCK_END = 9; +const INTERACTION_LOCK = 8; +const INTERACTION_LOCK_END = 9; + +const SYNC_OBJECTS = 3; //TODO refactor this.sessions into instances of the Session object. // Courtesy of Casey Foster on Stack Overflow // https://stackoverflow.com/a/14368628 function compareKeys(a, b) { - var aKeys = Object.keys(a).sort(); + var aKeys = Object.keys(a).sort(); - var bKeys = Object.keys(b).sort(); + var bKeys = Object.keys(b).sort(); - return JSON.stringify(aKeys) === JSON.stringify(bKeys); + return JSON.stringify(aKeys) === JSON.stringify(bKeys); } const SocketIOEvents = { - disconnect: 'disconnect', + disconnect: "disconnect", }; const KomodoReceiveEvents = { - join: 'join', - sessionInfo: 'sessionInfo', - state: 'state', - draw: 'draw', - message: 'message', - update: 'update', - interact: 'interact', - start_recording: 'start_recording', - end_recording: 'end_recording', - playback: 'playback', + join: "join", + sessionInfo: "sessionInfo", + state: "state", + draw: "draw", + message: "message", + update: "update", + interact: "interact", + start_recording: "start_recording", + end_recording: "end_recording", + playback: "playback", }; const KomodoSendEvents = { - connectionError: 'connectionError', - interactionUpdate: 'interactionUpdate', - joined: 'joined', - disconnected: 'disconnected', - sessionInfo: 'sessionInfo', - state: 'state', - draw: 'draw', - message: 'message', - relayUpdate: 'relayUpdate', + connectionError: "connectionError", + interactionUpdate: "interactionUpdate", + joined: "joined", + disconnected: "disconnected", + sessionInfo: "sessionInfo", + state: "state", + draw: "draw", + message: "message", + relayUpdate: "relayUpdate", }; -module.exports = { - // NOTE(rob): deprecated. sessions must use message_buffer. - // // write buffers are multiples of corresponding chunks - // positionWriteBufferSize: function () { - // return POS_COUNT * positionChunkSize(); - // }, - - // // write buffers are multiples of corresponding chunks - // interactionWriteBufferSize: function () { - // return INT_COUNT * interactionChunkSize(); - // }, - - logInfoSessionClientSocketAction: function (session_id, client_id, socket_id, action) { - if (session_id == null) { - session_id = "---"; - } - - session_id = `s${session_id}`; - - if (client_id == null) { - client_id = "---"; - } +const KomodoMessages = { + interaction: { + type: "interaction", + minLength: 5, //TODO: what should this number be? + indices: { + sourceId: 3, + targetId: 4, + interactionType: 5, + }, + }, + sync: { + type: "sync", + minLength: 4, //TODO: what should this number be? + indices: { + entityId: 3, + entityType: 4, + }, + } +}; - client_id = `c${client_id}`; +module.exports = { + // NOTE(rob): deprecated. sessions must use message_buffer. + // // write buffers are multiples of corresponding chunks + // positionWriteBufferSize: function () { + // return POS_COUNT * positionChunkSize(); + // }, + + // // write buffers are multiples of corresponding chunks + // interactionWriteBufferSize: function () { + // return INT_COUNT * interactionChunkSize(); + // }, + + logInfoSessionClientSocketAction: function ( + session_id, + client_id, + socket_id, + action + ) { + if (session_id == null) { + session_id = "---"; + } - if (socket_id == null) { - socket_id = "---................."; - } + session_id = `s${session_id}`; - if (action == null) { - action = "---"; - } + if (client_id == null) { + client_id = "---"; + } - if (!this.logger) { - return; - } + client_id = `c${client_id}`; - if (this.logger) this.logger.info(` ${socket_id} ${session_id} ${client_id} ${action}`); - }, + if (socket_id == null) { + socket_id = "---................."; + } - logErrorSessionClientSocketAction: function (session_id, client_id, socket_id, action) { - if (session_id == null) { - session_id = "---"; - } + if (action == null) { + action = "---"; + } - session_id = `s${session_id}`; + if (!this.logger) { + return; + } - if (client_id == null) { - client_id = "---"; - } + if (this.logger) + this.logger.info( + ` ${socket_id} ${session_id} ${client_id} ${action}` + ); + }, + + logErrorSessionClientSocketAction: function ( + session_id, + client_id, + socket_id, + action + ) { + if (session_id == null) { + session_id = "---"; + } - client_id = `c${client_id}`; + session_id = `s${session_id}`; - if (socket_id == null) { - socket_id = "---................."; - } + if (client_id == null) { + client_id = "---"; + } - if (action == null) { - action = "---"; - } + client_id = `c${client_id}`; - if (!this.logger) { - return; - } + if (socket_id == null) { + socket_id = "---................."; + } - if (this.logger) this.logger.error(`${socket_id} ${session_id} ${client_id} ${action}`); - }, + if (action == null) { + action = "---"; + } - logWarningSessionClientSocketAction: function (session_id, client_id, socket_id, action) { - if (session_id == null) { - session_id = "---"; - } + if (!this.logger) { + return; + } - session_id = `s${session_id}`; + if (this.logger) + this.logger.error( + `${socket_id} ${session_id} ${client_id} ${action}` + ); + }, + + logWarningSessionClientSocketAction: function ( + session_id, + client_id, + socket_id, + action + ) { + if (session_id == null) { + session_id = "---"; + } - if (client_id == null) { - client_id = "---"; - } + session_id = `s${session_id}`; - client_id = `c${client_id}`; + if (client_id == null) { + client_id = "---"; + } - if (socket_id == null) { - socket_id = "---................."; - } + client_id = `c${client_id}`; - if (action == null) { - action = "---"; - } + if (socket_id == null) { + socket_id = "---................."; + } - if (!this.logger) { - return; - } + if (action == null) { + action = "---"; + } - if (this.logger) this.logger.warn(` ${socket_id} ${session_id} ${client_id} ${action}`); - }, - - // generate formatted path for session capture files - getCapturePath: function (session_id, start, type) { - return path.join(__dirname, config.capture.path, session_id.toString(), start.toString(), type); - }, + if (!this.logger) { + return; + } - start_recording: function (pool, session_id) {// TODO(rob): require client id and token - console.log(`start_recording called with pool: ${pool}, session: ${session_id}`); - let session = this.sessions.get(session_id); - if (!session) { - this.logErrorSessionClientSocketAction(session_id, null, null, `Tried to start recording, but session was null`); - return; - } + if (this.logger) + this.logger.warn( + ` ${socket_id} ${session_id} ${client_id} ${action}` + ); + }, + + // generate formatted path for session capture files + getCapturePath: function (session_id, start, type) { + return path.join( + __dirname, + config.capture.path, + session_id.toString(), + start.toString(), + type + ); + }, + + start_recording: function (pool, session_id) { + // TODO(rob): require client id and token + console.log( + `start_recording called with pool: ${pool}, session: ${session_id}` + ); + let session = this.sessions.get(session_id); + if (!session) { + this.logErrorSessionClientSocketAction( + session_id, + null, + null, + `Tried to start recording, but session was null` + ); + return; + } - if (session && !session.isRecording) { - session.isRecording = true; - session.recordingStart = Date.now(); - let path = this.getCapturePath(session_id, session.recordingStart, ''); - fs.mkdir(path, { recursive: true }, (err) => { - if(err) if (this.logger) this.logger.warn(`Error creating capture path: ${err}`); - }); - let capture_id = session_id+'_'+session.recordingStart; - session.capture_id = capture_id; - if (pool) { - pool.query( - "INSERT INTO captures(capture_id, session_id, start) VALUES(?, ?, ?)", [capture_id, session_id, session.recordingStart], - (err, res) => { - if (err != undefined) { - if (this.logger) this.logger.error(`Error writing recording start event to database: ${err} ${res}`); - } - } + if (session && !session.isRecording) { + session.isRecording = true; + session.recordingStart = Date.now(); + let path = this.getCapturePath(session_id, session.recordingStart, ""); + fs.mkdir(path, { recursive: true }, (err) => { + if (err) + if (this.logger) + this.logger.warn(`Error creating capture path: ${err}`); + }); + let capture_id = session_id + "_" + session.recordingStart; + session.capture_id = capture_id; + if (pool) { + pool.query( + "INSERT INTO captures(capture_id, session_id, start) VALUES(?, ?, ?)", + [capture_id, session_id, session.recordingStart], + (err, res) => { + if (err != undefined) { + if (this.logger) + this.logger.error( + `Error writing recording start event to database: ${err} ${res}` ); } + } + ); + } + + if (this.logger) this.logger.info(`Capture started: ${session_id}`); + } else if (session && session.isRecording) { + if (this.logger) + this.logger.warn( + `Requested session capture, but session is already recording: ${session_id}` + ); + } + }, + + // define end_recording event handler, use on socket event as well as on server cleanup for empty sessions + end_recording: function (pool, session_id) { + if (session_id) { + let session = this.sessions.get(session_id); + if (session && session.isRecording) { + session.isRecording = false; + if (this.logger) this.logger.info(`Capture ended: ${session_id}`); + // write out the buffers if not empty, but only up to where the cursor is + + // NOTE(rob): deprecated, use messages. + // let pos_writer = session.writers.pos; + // if (pos_writer.cursor > 0) { + // let path = this.getCapturePath(session_id, session.recordingStart, 'pos'); + // let wstream = fs.createWriteStream(path, { flags: 'a' }); + // wstream.write(pos_writer.buffer.slice(0, pos_writer.cursor)); + // wstream.close(); + // pos_writer.cursor = 0; + // } + // let int_writer = session.writers.int; + // if (int_writer.cursor > 0) { + // let path = this.getCapturePath(session_id, session.recordingStart, 'int'); + // let wstream = fs.createWriteStream(path, { flags: 'a' }); + // wstream.write(int_writer.buffer.slice(0, int_writer.cursor)); + // wstream.close(); + // int_writer.cursor = 0; + // } - if (this.logger) this.logger.info(`Capture started: ${session_id}`); - } else if (session && session.isRecording) { - if (this.logger) this.logger.warn(`Requested session capture, but session is already recording: ${session_id}`); - } - }, - - // define end_recording event handler, use on socket event as well as on server cleanup for empty sessions - end_recording: function (pool, session_id) { - if (session_id) { - let session = this.sessions.get(session_id); - if (session && session.isRecording) { - session.isRecording = false; - if (this.logger) this.logger.info(`Capture ended: ${session_id}`); - // write out the buffers if not empty, but only up to where the cursor is - - // NOTE(rob): deprecated, use messages. - // let pos_writer = session.writers.pos; - // if (pos_writer.cursor > 0) { - // let path = this.getCapturePath(session_id, session.recordingStart, 'pos'); - // let wstream = fs.createWriteStream(path, { flags: 'a' }); - // wstream.write(pos_writer.buffer.slice(0, pos_writer.cursor)); - // wstream.close(); - // pos_writer.cursor = 0; - // } - // let int_writer = session.writers.int; - // if (int_writer.cursor > 0) { - // let path = this.getCapturePath(session_id, session.recordingStart, 'int'); - // let wstream = fs.createWriteStream(path, { flags: 'a' }); - // wstream.write(int_writer.buffer.slice(0, int_writer.cursor)); - // wstream.close(); - // int_writer.cursor = 0; - // } - - // write out message buffer. - let path = this.getCapturePath(session_id, session.recordingStart, 'data'); - fs.writeFile(path, JSON.stringify(session.message_buffer), (e) => { if (e) {console.log(`Error writing message buffer: ${e}`);} }); - //TODO(Brandon): add success event here. Possibly notify Unity client. - // reset the buffer. - session.message_buffer = []; - - // write the capture end event to database - if (pool) { - let capture_id = session.capture_id; - pool.query( - "UPDATE captures SET end = ? WHERE capture_id = ?", [Date.now(), capture_id], - (err, res) => { - if (err != undefined) { - if (this.logger) this.logger.error(`Error writing recording end event to database: ${err} ${res}`); - } - } - ); - session.capture_id = null; - } - } else if (session && !session.isRecording) { - if (this.logger) this.logger.warn(`Requested to end session capture, but capture is already ended: ${session_id}`); - session.capture_id = null; - } else { - if (this.logger) this.logger.warn(`Error ending capture for session: ${session_id}`); + // write out message buffer. + let path = this.getCapturePath( + session_id, + session.recordingStart, + "data" + ); + fs.writeFile(path, JSON.stringify(session.message_buffer), (e) => { + if (e) { + console.log(`Error writing message buffer: ${e}`); + } + }); + //TODO(Brandon): add success event here. Possibly notify Unity client. + // reset the buffer. + session.message_buffer = []; + + // write the capture end event to database + if (pool) { + let capture_id = session.capture_id; + pool.query( + "UPDATE captures SET end = ? WHERE capture_id = ?", + [Date.now(), capture_id], + (err, res) => { + if (err != undefined) { + if (this.logger) + this.logger.error( + `Error writing recording end event to database: ${err} ${res}` + ); + } } - } - }, + ); + session.capture_id = null; + } + } else if (session && !session.isRecording) { + if (this.logger) + this.logger.warn( + `Requested to end session capture, but capture is already ended: ${session_id}` + ); + session.capture_id = null; + } else { + if (this.logger) + this.logger.warn(`Error ending capture for session: ${session_id}`); + } + } + }, + + record_message_data: function (data) { + if (data) { + let session = this.sessions.get(data.session_id); + + if (!session) { + this.logErrorSessionClientSocketAction( + data.session_id, + null, + null, + `Tried to record message data, but session was null` + ); + + return; + } + + // calculate a canonical session sequence number for this message from session start and message timestamp. + // NOTE(rob): investigate how we might timestamp incoming packets WHEN THEY ARE RECEIVED BY THE NETWORKING LAYER, ie. not + // when they are handled by the socket.io library. From a business logic perspective, the canonical order of events is based + // on when they arrive at the relay server, NOT when the client emits them. 8/3/2021 + data.seq = data.ts - session.recordingStart; + + data.capture_id = session.capture_id; // copy capture id session property and attach it to the message data. + + let session_id = data.session_id; + + let client_id = data.client_id; + + if (typeof data.message != `object`) { + try { + data.message = JSON.parse(data.message); + } catch (e) { + // if (this.logger) this.logger.warn(`Failed to parse message payload: ${message} ${e}`); + console.log(`Failed to parse message payload: ${data.message}; ${e}`); + + return; + } + } + + if (!session_id || !client_id) { + this.logErrorSessionClientSocketAction( + session_id, + null, + null, + `Tried to record message data. One of these properties is missing. session_id: ${session_id}, client_id: ${client_id}, message: ${data}` + ); + + return; + } + + if (session.message_buffer) { + // TODO(rob): find optimal buffer size + // if (session.message_buffer.length < MESSAGE_BUFFER_MAX_SIZE) { + // this.session.message_buffer.push(data) + // } else + + session.message_buffer.push(data); + + // DEBUG(rob): + // let mb_str = JSON.stringify(session.message_buffer); + // let bytes = new util.TextEncoder().encode(mb_str).length; + // console.log(`Session ${data.session_id} message buffer size: ${bytes} bytes`); + } + } else { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `message was null` + ); + } + }, + + handlePlayback: function (io, data) { + // TODO(rob): need to use playback object to track seq and group by playback_id, + // so users can request to pause playback, maybe rewind? + if (this.logger) this.logger.info(`Playback request: ${data.playback_id}`); + let client_id = data.client_id; + let session_id = data.session_id; + let playback_id = data.playback_id; + + let capture_id = null; + let start = null; + + if (client_id && session_id && playback_id) { + capture_id = playback_id.split("_")[0]; + start = playback_id.split("_")[1]; + // TODO(rob): check that this client has permission to playback this session + } else { + console.log("Invalid playback request:", data); + return; + } - record_message_data: function (data) { - if (data) { - let session = this.sessions.get(data.session_id); + // Everything looks good, getting ref to session. + let session = this.sessions.get(session_id); + + // playback sequence counter + let current_seq = 0; + // let audioStarted = false; + + // NOTE(rob): deprecated; playback data must use message system. + // check that all params are valid + // if (capture_id && start) { + // // TODO(rob): Mar 3 2021 -- audio playback on hold to focus on data. + // // build audio file manifest + // // if (this.logger) this.logger.info(`Buiding audio file manifest for capture replay: ${playback_id}`) + // // let audioManifest = []; + // // let baseAudioPath = this.getCapturePath(capture_id, start, 'audio'); + // // if(fs.existsSync(baseAudioPath)) { // TODO(rob): change this to async operation + // // let items = fs.readdirSync(baseAudioPath); // TODO(rob): change this to async operation + // // items.forEach(clientDir => { + // // let clientPath = path.join(baseAudioPath, clientDir) + // // let files = fs.readdirSync(clientPath) // TODO(rob): change this to async operation + // // files.forEach(file => { + // // let client_id = clientDir; + // // let seq = file.split('.')[0]; + // // let audioFilePath = path.join(clientPath, file); + // // let item = { + // // seq: seq, + // // client_id: client_id, + // // path: audioFilePath, + // // data: null + // // } + // // audioManifest.push(item); + // // }); + // // }); + // // } + + // // // emit audio manifest to connected clients + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioManifest', audioManifest); + + // // // stream all audio files for caching and playback by client + // // audioManifest.forEach((file) => { + // // fs.readFile(file.path, (err, data) => { + // // file.data = data; + // // if(err) if (this.logger) this.logger.error(`Error reading audio file: ${file.path}`); + // // // console.log('emitting audio packet:', file); + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioData', file); + // // }); + // // }); + + // // position streaming + // let capturePath = this.getCapturePath(capture_id, start, 'pos'); + // let stream = fs.createReadStream(capturePath, { highWaterMark: positionChunkSize() }); + + // // set actual playback start time + // let playbackStart = Date.now(); + + // // position data emit loop + // stream.on(KomodoReceiveEvents.data, function(chunk) { + // stream.pause(); + + // // start data buffer loop + // let buff = Buffer.from(chunk); + // let farr = new Float32Array(chunk.byteLength / 4); + // for (var i = 0; i < farr.length; i++) { + // farr[i] = buff.readFloatLE(i * 4); + // } + // var arr = Array.from(farr); + + // let timer = setInterval( () => { + // current_seq = Date.now() - playbackStart; + + // // console.log(`=== POS === current seq ${current_seq}; arr seq ${arr[POS_FIELDS-1]}`); + + // if (arr[POS_FIELDS-1] <= current_seq) { + // // alias client and entity id with prefix if entity type is not an asset + // if (arr[4] != 3) { + // arr[2] = 90000 + arr[2]; + // arr[3] = 90000 + arr[3]; + // } + // // if (!audioStarted) { + // // // HACK(rob): trigger clients to begin playing buffered audio + // // audioStarted = true; + // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.startPlaybackAudio'); + // // } + // io.to(session_id.toString()).emit(KomodoSendEvents.relayUpdate', arr); + // stream.resume(); + // clearInterval(timer); + // } + // }, 1); + // }); + + // stream.on(KomodoReceiveEvents.error, function(err) { + // if (this.logger) this.logger.error(`Error creating position playback stream for ${playback_id} ${start}: ${err}`); + // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); + // }); + + // stream.on(KomodoReceiveEvents.end, function() { + // if (this.logger) this.logger.info(`End of pos data for playback session: ${session_id}`); + // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); + // }); + + // // interaction streaming + // let ipath = this.getCapturePath(capture_id, start, 'int'); + // let istream = fs.createReadStream(ipath, { highWaterMark: interactionChunkSize() }); + + // istream.on(KomodoReceiveEvents.data, function(chunk) { + // istream.pause(); + + // let buff = Buffer.from(chunk); + // let farr = new Int32Array(chunk.byteLength / 4); + // for (var i = 0; i < farr.length; i++) { + // farr[i] = buff.readInt32LE(i * 4); + // } + // var arr = Array.from(farr); + + // let timer = setInterval( () => { + // // console.log(`=== INT === current seq ${current_seq}; arr seq ${arr[INT_FIELDS-1]}`); + + // if (arr[INT_FIELDS-1] <= current_seq) { + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionUpdate', arr); + // istream.resume(); + // clearInterval(timer); + // } + // }, 1); + + // }); + + // istream.on(KomodoReceiveEvents.error, function(err) { + // if (this.logger) this.logger.error(`Error creating interaction playback stream for session ${session_id}: ${err}`); + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionpPlaybackEnd'); + // }); + + // istream.on(KomodoReceiveEvents.end, function() { + // if (this.logger) this.logger.info(`End of int data for playback session: ${session_id}`); + // io.to(session_id.toString()).emit(KomodoSendEvents.interactionPlaybackEnd'); + // }); + // } + }, + + isValidRelayPacket: function (data) { + let session_id = data[1]; + + let client_id = data[2]; + + if (session_id && client_id) { + let session = this.sessions.get(session_id); + + if (!session) { + return; + } + + // check if the incoming packet is from a client who is valid for this session + + for (let i = 0; i < session.clients.length; i += 1) { + if (client_id == session.clients[i]) { + return true; + } + } + + return false; + } + }, - if (!session) { - this.logErrorSessionClientSocketAction(data.session_id, null, null, `Tried to record message data, but session was null`); - - return; - } + // NOTE(rob): DEPRECATED. 8/5/21. + // writeRecordedRelayData: function (data) { + // if (!data) { + // throw new ReferenceError ("data was null"); + // } - // calculate a canonical session sequence number for this message from session start and message timestamp. - // NOTE(rob): investigate how we might timestamp incoming packets WHEN THEY ARE RECEIVED BY THE NETWORKING LAYER, ie. not - // when they are handled by the socket.io library. From a business logic perspective, the canonical order of events is based - // on when they arrive at the relay server, NOT when the client emits them. 8/3/2021 - data.seq = data.ts - session.recordingStart; + // let session_id = data[1]; - data.capture_id = session.capture_id; // copy capture id session property and attach it to the message data. + // let session = this.sessions.get(session_id); - let session_id = data.session_id; + // if (!session) { + // throw new ReferenceError ("session was null"); + // } - let client_id = data.client_id; + // if (!session.isRecording) { + // return; + // } - if (typeof data.message != `object`) { - try { - data.message = JSON.parse(data.message); - } catch (e) { - // if (this.logger) this.logger.warn(`Failed to parse message payload: ${message} ${e}`); - console.log(`Failed to parse message payload: ${data.message}; ${e}`); + // // calculate and write session sequence number using client timestamp + // data[POS_FIELDS-1] = data[POS_FIELDS-1] - session.recordingStart; - return; - } - } + // // get reference to session writer (buffer and cursor) + // let writer = session.writers.pos; - if (!session_id || !client_id) { - this.logErrorSessionClientSocketAction(session_id, null, null, `Tried to record message data. One of these properties is missing. session_id: ${session_id}, client_id: ${client_id}, message: ${data}`); + // if (positionChunkSize() + writer.cursor > writer.buffer.byteLength) { + // // if buffer is full, dump to disk and reset the cursor + // let path = this.getCapturePath(session_id, session.recordingStart, 'pos'); - return; - } + // let wstream = fs.createWriteStream(path, { flags: 'a' }); - if (session.message_buffer) { - // TODO(rob): find optimal buffer size - // if (session.message_buffer.length < MESSAGE_BUFFER_MAX_SIZE) { - // this.session.message_buffer.push(data) - // } else + // wstream.write(writer.buffer.slice(0, writer.cursor)); - session.message_buffer.push(data); + // wstream.close(); - // DEBUG(rob): - // let mb_str = JSON.stringify(session.message_buffer); - // let bytes = new util.TextEncoder().encode(mb_str).length; - // console.log(`Session ${data.session_id} message buffer size: ${bytes} bytes`); - } - } else { - this.logErrorSessionClientSocketAction(null, null, null, `message was null`); - } - }, + // writer.cursor = 0; + // } - handlePlayback: function (io, data) { - // TODO(rob): need to use playback object to track seq and group by playback_id, - // so users can request to pause playback, maybe rewind? - if (this.logger) this.logger.info(`Playback request: ${data.playback_id}`); - let client_id = data.client_id; - let session_id = data.session_id; - let playback_id = data.playback_id; - - let capture_id = null; - let start = null; - - if (client_id && session_id && playback_id) { - capture_id = playback_id.split('_')[0]; - start = playback_id.split('_')[1]; - // TODO(rob): check that this client has permission to playback this session - } else { - console.log("Invalid playback request:", data); - return; - } + // for (let i = 0; i < data.length; i++) { + // writer.buffer.writeFloatLE(data[i], (i*POS_BYTES_PER_FIELD) + writer.cursor); + // } - // Everything looks good, getting ref to session. - let session = this.sessions.get(session_id); - - // playback sequence counter - let current_seq = 0; - // let audioStarted = false; - - // NOTE(rob): deprecated; playback data must use message system. - // check that all params are valid - // if (capture_id && start) { - // // TODO(rob): Mar 3 2021 -- audio playback on hold to focus on data. - // // build audio file manifest - // // if (this.logger) this.logger.info(`Buiding audio file manifest for capture replay: ${playback_id}`) - // // let audioManifest = []; - // // let baseAudioPath = this.getCapturePath(capture_id, start, 'audio'); - // // if(fs.existsSync(baseAudioPath)) { // TODO(rob): change this to async operation - // // let items = fs.readdirSync(baseAudioPath); // TODO(rob): change this to async operation - // // items.forEach(clientDir => { - // // let clientPath = path.join(baseAudioPath, clientDir) - // // let files = fs.readdirSync(clientPath) // TODO(rob): change this to async operation - // // files.forEach(file => { - // // let client_id = clientDir; - // // let seq = file.split('.')[0]; - // // let audioFilePath = path.join(clientPath, file); - // // let item = { - // // seq: seq, - // // client_id: client_id, - // // path: audioFilePath, - // // data: null - // // } - // // audioManifest.push(item); - // // }); - // // }); - // // } - - // // // emit audio manifest to connected clients - // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioManifest', audioManifest); - - // // // stream all audio files for caching and playback by client - // // audioManifest.forEach((file) => { - // // fs.readFile(file.path, (err, data) => { - // // file.data = data; - // // if(err) if (this.logger) this.logger.error(`Error reading audio file: ${file.path}`); - // // // console.log('emitting audio packet:', file); - // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.playbackAudioData', file); - // // }); - // // }); - - // // position streaming - // let capturePath = this.getCapturePath(capture_id, start, 'pos'); - // let stream = fs.createReadStream(capturePath, { highWaterMark: positionChunkSize() }); - - // // set actual playback start time - // let playbackStart = Date.now(); - - // // position data emit loop - // stream.on(KomodoReceiveEvents.data, function(chunk) { - // stream.pause(); - - // // start data buffer loop - // let buff = Buffer.from(chunk); - // let farr = new Float32Array(chunk.byteLength / 4); - // for (var i = 0; i < farr.length; i++) { - // farr[i] = buff.readFloatLE(i * 4); - // } - // var arr = Array.from(farr); - - // let timer = setInterval( () => { - // current_seq = Date.now() - playbackStart; - - // // console.log(`=== POS === current seq ${current_seq}; arr seq ${arr[POS_FIELDS-1]}`); - - // if (arr[POS_FIELDS-1] <= current_seq) { - // // alias client and entity id with prefix if entity type is not an asset - // if (arr[4] != 3) { - // arr[2] = 90000 + arr[2]; - // arr[3] = 90000 + arr[3]; - // } - // // if (!audioStarted) { - // // // HACK(rob): trigger clients to begin playing buffered audio - // // audioStarted = true; - // // io.of('chat').to(session_id.toString()).emit(KomodoSendEvents.startPlaybackAudio'); - // // } - // io.to(session_id.toString()).emit(KomodoSendEvents.relayUpdate', arr); - // stream.resume(); - // clearInterval(timer); - // } - // }, 1); - // }); - - // stream.on(KomodoReceiveEvents.error, function(err) { - // if (this.logger) this.logger.error(`Error creating position playback stream for ${playback_id} ${start}: ${err}`); - // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); - // }); - - // stream.on(KomodoReceiveEvents.end, function() { - // if (this.logger) this.logger.info(`End of pos data for playback session: ${session_id}`); - // io.to(session_id.toString()).emit(KomodoSendEvents.playbackEnd'); - // }); - - // // interaction streaming - // let ipath = this.getCapturePath(capture_id, start, 'int'); - // let istream = fs.createReadStream(ipath, { highWaterMark: interactionChunkSize() }); - - // istream.on(KomodoReceiveEvents.data, function(chunk) { - // istream.pause(); - - // let buff = Buffer.from(chunk); - // let farr = new Int32Array(chunk.byteLength / 4); - // for (var i = 0; i < farr.length; i++) { - // farr[i] = buff.readInt32LE(i * 4); - // } - // var arr = Array.from(farr); - - // let timer = setInterval( () => { - // // console.log(`=== INT === current seq ${current_seq}; arr seq ${arr[INT_FIELDS-1]}`); - - // if (arr[INT_FIELDS-1] <= current_seq) { - // io.to(session_id.toString()).emit(KomodoSendEvents.interactionUpdate', arr); - // istream.resume(); - // clearInterval(timer); - // } - // }, 1); - - // }); - - // istream.on(KomodoReceiveEvents.error, function(err) { - // if (this.logger) this.logger.error(`Error creating interaction playback stream for session ${session_id}: ${err}`); - // io.to(session_id.toString()).emit(KomodoSendEvents.interactionpPlaybackEnd'); - // }); - - // istream.on(KomodoReceiveEvents.end, function() { - // if (this.logger) this.logger.info(`End of int data for playback session: ${session_id}`); - // io.to(session_id.toString()).emit(KomodoSendEvents.interactionPlaybackEnd'); - // }); - // } - }, + // writer.cursor += positionChunkSize(); + // }, - isValidRelayPacket: function (data) { - let session_id = data[1]; + updateSessionState: function (data) { + if (!data || data.length < 5) { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `Tried to update session state, but data was null or not long enough` + ); - let client_id = data[2]; - - if (session_id && client_id) { - let session = this.sessions.get(session_id); + return; + } - if (!session) { - return; - } + let session_id = data[1]; - // check if the incoming packet is from a client who is valid for this session + let session = this.sessions.get(session_id); - for (let i = 0; i < session.clients.length; i += 1) { - if (client_id == session.clients[i]) { - return true; - } - } + if (!session) { + this.logErrorSessionClientSocketAction( + session_id, + null, + null, + `Tried to update session state, but there was no such session` + ); - return false; - } - }, + return; + } - // NOTE(rob): DEPRECATED. 8/5/21. - // writeRecordedRelayData: function (data) { - // if (!data) { - // throw new ReferenceError ("data was null"); - // } + // update session state with latest entity positions + let entity_type = data[4]; - // let session_id = data[1]; + if (entity_type == 3) { + let entity_id = data[3]; - // let session = this.sessions.get(session_id); + let i = session.entities.findIndex((e) => e.id == entity_id); - // if (!session) { - // throw new ReferenceError ("session was null"); - // } + if (i != -1) { + session.entities[i].latest = data; + } else { + let entity = { + id: entity_id, + latest: data, + render: true, + locked: false, + }; - // if (!session.isRecording) { - // return; - // } + session.entities.push(entity); + } + } + }, + + handleInteraction: function (socket, data) { + let session_id = data[1]; + let client_id = data[2]; + + if (session_id && client_id) { + // relay interaction events to all connected clients + socket + .to(session_id.toString()) + .emit(KomodoSendEvents.interactionUpdate, data); + + // do session state update if needed + let source_id = data[3]; + let target_id = data[4]; + let interaction_type = data[5]; + let session = this.sessions.get(session_id); + if (!session) return; + + // check if the incoming packet is from a client who is valid for this session + let joined = false; + for (let i = 0; i < session.clients.length; i++) { + if (client_id == session.clients[i]) { + joined = true; + break; + } + } + + if (!joined) return; + + // entity should be rendered + if (interaction_type == INTERACTION_RENDER) { + let i = session.entities.findIndex((e) => e.id == target_id); + if (i != -1) { + session.entities[i].render = true; + } else { + let entity = { + id: target_id, + latest: [], + render: true, + locked: false, + }; + session.entities.push(entity); + } + } + + // entity should stop being rendered + if (interaction_type == INTERACTION_RENDER_END) { + let i = session.entities.findIndex((e) => e.id == target_id); + if (i != -1) { + session.entities[i].render = false; + } else { + let entity = { + id: target_id, + latest: data, + render: false, + locked: false, + }; + session.entities.push(entity); + } + } + + // scene has changed + if (interaction_type == INTERACTION_SCENE_CHANGE) { + session.scene = target_id; + } + + // entity is locked + if (interaction_type == INTERACTION_LOCK) { + let i = session.entities.findIndex((e) => e.id == target_id); + if (i != -1) { + session.entities[i].locked = true; + } else { + let entity = { + id: target_id, + latest: [], + render: false, + locked: true, + }; + session.entities.push(entity); + } + } + + // entity is unlocked + if (interaction_type == INTERACTION_LOCK_END) { + let i = session.entities.findIndex((e) => e.id == target_id); + if (i != -1) { + session.entities[i].locked = false; + } else { + let entity = { + id: target_id, + latest: [], + render: false, + locked: false, + }; + session.entities.push(entity); + } + } + + // NOTE(rob): deprecated, use messages. + // write to file as binary data + // if (session.isRecording) { + // // calculate and write session sequence number + // data[INT_FIELDS-1] = data[INT_FIELDS-1] - session.recordingStart; + + // // get reference to session writer (buffer and cursor) + // let writer = session.writers.int; + + // if (interactionChunkSize() + writer.cursor > writer.buffer.byteLength) { + // // if buffer is full, dump to disk and reset the cursor + // let path = this.getCapturePath(session_id, session.recordingStart, 'int'); + // let wstream = fs.createWriteStream(path, { flags: 'a' }); + // wstream.write(writer.buffer.slice(0, writer.cursor)); + // wstream.close(); + // writer.cursor = 0; + // } + // for (let i = 0; i < data.length; i++) { + // writer.buffer.writeInt32LE(data[i], (i*INT_BYTES_PER_FIELD) + writer.cursor); + // } + // writer.cursor += interactionChunkSize(); + // } + } + }, + + handleState: function (socket, data) { + if (!socket) { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `tried to handle state, but socket was null` + ); + + return { session_id: -1, state: null }; + } - // // calculate and write session sequence number using client timestamp - // data[POS_FIELDS-1] = data[POS_FIELDS-1] - session.recordingStart; + if (!data) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + `tried to handle state, but data was null` + ); - // // get reference to session writer (buffer and cursor) - // let writer = session.writers.pos; + return { session_id: -1, state: null }; + } - // if (positionChunkSize() + writer.cursor > writer.buffer.byteLength) { - // // if buffer is full, dump to disk and reset the cursor - // let path = this.getCapturePath(session_id, session.recordingStart, 'pos'); + let session_id = data.session_id; - // let wstream = fs.createWriteStream(path, { flags: 'a' }); + let client_id = data.client_id; - // wstream.write(writer.buffer.slice(0, writer.cursor)); + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `State: ${JSON.stringify(data)}` + ); - // wstream.close(); + if (!session_id || !client_id) { + this.connectionAuthorizationErrorAction( + socket, + "You must provide a session ID and a client ID in the URL options." + ); - // writer.cursor = 0; - // } + return { session_id: -1, state: null }; + } - // for (let i = 0; i < data.length; i++) { - // writer.buffer.writeFloatLE(data[i], (i*POS_BYTES_PER_FIELD) + writer.cursor); - // } + let version = data.version; - // writer.cursor += positionChunkSize(); - // }, + let session = this.sessions.get(session_id); - updateSessionState: function (data) { - if (!data || data.length < 5) { - this.logErrorSessionClientSocketAction(null, null, null, `Tried to update session state, but data was null or not long enough`); - - return; - } + if (!session) { + this.stateErrorAction( + socket, + "The session was null, so no state could be found." + ); - let session_id = data[1]; + return { session_id: -1, state: null }; + } - let session = this.sessions.get(session_id); + let state = {}; - if (!session) { - this.logErrorSessionClientSocketAction(session_id, null, null, `Tried to update session state, but there was no such session`); + // check requested api version + if (version === 2) { + state = { + clients: session.clients, + entities: session.entities, + scene: session.scene, + isRecording: session.isRecording, + }; + } else { + // version 1 or no api version indicated - return; + let entities = []; + + let locked = []; + + for (let i = 0; i < session.entities.length; i++) { + entities.push(session.entities[i].id); + + if (session.entities[i].locked) { + locked.push(session.entities[i].id); } + } - // update session state with latest entity positions - let entity_type = data[4]; + state = { + clients: session.clients, + entities: entities, + locked: locked, + scene: session.scene, + isRecording: session.isRecording, + }; + } - if (entity_type == 3) { - let entity_id = data[3]; + return { session_id, state }; + }, - let i = session.entities.findIndex(e => e.id == entity_id); + // returns true on success and false on failure + addClientToSession: function (session, client_id, do_create_session) { + if (session == null && !do_create_session) { + this.logErrorSessionClientSocketAction( + null, + client_id, + null, + `tried to add client to session, but session was null and do_create_session was false` + ); - if (i != -1) { - session.entities[i].latest = data; - } else { - let entity = { - id: entity_id, - latest: data, - render: true, - locked: false - }; + return false; + } - session.entities.push(entity); - } - } - }, + if (session == null && do_create_session) { + session = this.createSession(); + } - handleInteraction: function (socket, data) { - let session_id = data[1]; - let client_id = data[2]; + if ( + session.clients == null || + typeof session.clients === "undefined" || + session.clients.length == 0 + ) { + session.clients = [client_id]; - if (session_id && client_id) { - // relay interaction events to all connected clients - socket.to(session_id.toString()).emit(KomodoSendEvents.interactionUpdate, data); - - // do session state update if needed - let source_id = data[3]; - let target_id = data[4]; - let interaction_type = data[5]; - let session = this.sessions.get(session_id); - if (!session) return; - - // check if the incoming packet is from a client who is valid for this session - let joined = false; - for (let i=0; i < session.clients.length; i++) { - if (client_id == session.clients[i]) { - joined = true; - break; - } - } + return true; + } - if (!joined) return; - - // entity should be rendered - if (interaction_type == INTERACTION_RENDER) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].render = true; - } else { - let entity = { - id: target_id, - latest: [], - render: true, - locked: false - }; - session.entities.push(entity); - } - } + session.clients.push(client_id); - // entity should stop being rendered - if (interaction_type == INTERACTION_RENDER_END) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].render = false; - } else { - let entity = { - id: target_id, - latest: data, - render: false, - locked: false - }; - session.entities.push(entity); - } - } + return true; + }, - // scene has changed - if (interaction_type == INTERACTION_SCENE_CHANGE) { - session.scene = target_id; - } + removeDuplicateClientsFromSession: function (session, client_id) { + if (session == null) { + this.logErrorSessionClientSocketAction( + null, + client_id, + null, + `tried to remove duplicate client from session, but session was null` + ); - // entity is locked - if (interaction_type == INTERACTION_LOCK) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].locked = true; - } else { - let entity = { - id: target_id, - latest: [], - render: false, - locked: true - }; - session.entities.push(entity); - } - } + return; + } - // entity is unlocked - if (interaction_type == INTERACTION_LOCK_END) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].locked = false; - } else { - let entity = { - id: target_id, - latest: [], - render: false, - locked: false - }; - session.entities.push(entity); - } - } + if (session.clients == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to remove duplicate client from session, but session.clients was null` + ); - // NOTE(rob): deprecated, use messages. - // write to file as binary data - // if (session.isRecording) { - // // calculate and write session sequence number - // data[INT_FIELDS-1] = data[INT_FIELDS-1] - session.recordingStart; - - // // get reference to session writer (buffer and cursor) - // let writer = session.writers.int; - - // if (interactionChunkSize() + writer.cursor > writer.buffer.byteLength) { - // // if buffer is full, dump to disk and reset the cursor - // let path = this.getCapturePath(session_id, session.recordingStart, 'int'); - // let wstream = fs.createWriteStream(path, { flags: 'a' }); - // wstream.write(writer.buffer.slice(0, writer.cursor)); - // wstream.close(); - // writer.cursor = 0; - // } - // for (let i = 0; i < data.length; i++) { - // writer.buffer.writeInt32LE(data[i], (i*INT_BYTES_PER_FIELD) + writer.cursor); - // } - // writer.cursor += interactionChunkSize(); - // } - } - }, + return; + } - handleState: function (socket, data) { - if(!socket) { - this.logErrorSessionClientSocketAction(null, null, null, `tried to handle state, but socket was null`); + if (session.clients.length == 0) { + return; + } - return { session_id: -1, state: null }; - } + const first_instance = session.clients.indexOf(client_id); - if (!data) { - this.logErrorSessionClientSocketAction(null, null, socket.id, `tried to handle state, but data was null`); - - return { session_id: -1, state: null }; - } - - let session_id = data.session_id; - - let client_id = data.client_id; - - this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, `State: ${JSON.stringify(data)}`); - - if (!session_id || !client_id) { - this.connectionAuthorizationErrorAction(socket, "You must provide a session ID and a client ID in the URL options."); + for (let i = 0; i < session.clients.length; i += 1) { + if (i != first_instance && session.clients[i] == client_id) { + session.clients.splice(i, 1); + } + } + }, + + removeClientFromSession: function (session, client_id) { + if (session == null) { + this.logErrorSessionClientSocketAction( + null, + client_id, + null, + `tried to remove client from session, but session was null` + ); + + return; + } - return { session_id: -1, state: null }; - } - - let version = data.version; + if (session.clients == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to remove client from session, but session.clients was null` + ); - let session = this.sessions.get(session_id); + return; + } - if (!session) { - this.stateErrorAction(socket, "The session was null, so no state could be found."); + let index = session.clients.indexOf(client_id); + + if ( + session.clients.length == 0 || + session.clients.indexOf(client_id) == -1 + ) { + //client_id is not in the array, so we don't need to remove it. + this.logWarningSessionClientSocketAction( + null, + null, + client_id, + `Tried removing client from session.clients, but it was not there. Proceeding anyways.` + ); + + return; + } - return { session_id: -1, state: null }; - } + session.clients.splice(index, 1); + }, - let state = {}; + addSocketToSession: function (session, socket, client_id) { + if (!session) { + this.logErrorSessionClientSocketAction( + null, + client_id, + socket.id, + `tried to add socket to session, but session was null` + ); - // check requested api version - if (version === 2) { - state = { - clients: session.clients, - entities: session.entities, - scene: session.scene, - isRecording: session.isRecording - }; - } else { // version 1 or no api version indicated + return; + } - let entities = []; + session.sockets[socket.id] = { client_id: client_id, socket: socket }; + }, + + // returns true iff socket was successfully joined to session + handleJoin: function ( + err, + socket, + session_id, + client_id, + do_bump_duplicates + ) { + if (!socket) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `tried to handle join, but socket was null` + ); + + return false; + } - let locked = []; + if (!this.joinSessionAction) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `in handleJoin, joinSessionAction callback was not provided. Proceeding anyways.` + ); + } - for (let i = 0; i < session.entities.length; i++) { - entities.push(session.entities[i].id); + if (err) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Error joining client to session: ${err}` + ); - if (session.entities[i].locked) { - locked.push(session.entities[i].id); - } - } + return false; + } - state = { - clients: session.clients, - entities: entities, - locked: locked, - scene: session.scene, - isRecording: session.isRecording - }; - } + let { success, session } = this.getSession(session_id); - return { session_id, state }; - }, + if (!success || !session) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + "session was null when adding socket to session. Creating a session for you." + ); - // returns true on success and false on failure - addClientToSession: function (session, client_id, do_create_session) { - if (session == null && !do_create_session) { - this.logErrorSessionClientSocketAction(null, client_id, null, `tried to add client to session, but session was null and do_create_session was false`); + session = this.createSession(session_id); + } - return false; - } + success = this.addClientToSession(session, client_id); - if (session == null && do_create_session) { - session = this.createSession(); - } + if (!success) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + `tried to handle join, but adding client to session failed.` + ); - if (session.clients == null || - typeof session.clients === "undefined" || - session.clients.length == 0) { - session.clients = [ client_id ]; + return; + } - return true; - } + this.bumpDuplicateSockets( + session, + client_id, + do_bump_duplicates, + socket.id + ); - session.clients.push(client_id); + if (do_bump_duplicates) { + this.removeDuplicateClientsFromSession(session, client_id); + } - return true; - }, + // socket to client mapping + this.addSocketToSession(session, socket, client_id); + + this.joinSessionAction(session_id, client_id); + + // socket successfully joined to session + return true; + }, + + //TODO rewrite this so that do_bump_duplicates and socket_id become ids_to_keep + bumpDuplicateSockets: function ( + session, + client_id, + do_bump_duplicates, + socket_id + ) { + if (session == null) { + this.logErrorSessionClientSocketAction( + null, + client_id, + socket_id, + `tried to bump duplicate sockets, but session was null` + ); + + return; + } - removeDuplicateClientsFromSession: function (session, client_id) { - if (session == null) { - this.logErrorSessionClientSocketAction(null, client_id, null, `tried to remove duplicate client from session, but session was null`); + let session_id = this.getSessionIdFromSession(session); - return; - } + if (this.bumpAction == null) { + this.logWarningSessionClientSocketAction( + session.id, + client_id, + socket_id, + `in bumpDuplicateSockets, bumpAction callback was not provided` + ); + } - if (session.clients == null) { - this.logErrorSessionClientSocketAction(session.id, client_id, null, `tried to remove duplicate client from session, but session.clients was null`); + let sockets; - return; - } + if (do_bump_duplicates) { + sockets = this.getSessionSocketsFromClientId( + session, + client_id, + socket_id + ); + } else { + sockets = this.getSessionSocketsFromClientId(session, client_id, null); + } - if (session.clients.length == 0) { - return; - } + let self = this; - const first_instance = session.clients.indexOf(client_id); + if (!sockets) { + this.logWarningSessionClientSocketAction( + session.id, + client_id, + socket_id, + `tried to bump duplicate sockets, but result of getSessionSocketsFromClientId was null. Proceeding anyways.` + ); + } - for (let i = 0; i < session.clients.length; i += 1) { - if (i != first_instance && session.clients[i] == client_id) { - session.clients.splice(i, 1); + sockets.forEach((socket) => { + self.bumpAction(session_id, socket); + + self.removeSocketFromSession(socket, session_id, client_id); + }); + }, + + writeEventToConnections: function (event, session_id, client_id) { + if (event && session_id && client_id) { + if (!this.pool) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + "pool was null" + ); + + return; + } + + if (this.pool) { + this.pool.query( + "INSERT INTO connections(timestamp, session_id, client_id, event) VALUES(?, ?, ?, ?)", + [Date.now(), session_id, client_id, event], + + (err, res) => { + if (err != undefined) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `Error writing ${event} event to database: ${err} ${res}` + ); } - } - }, + } + ); + } + } else { + this.logger.error( + `Failed to log event to database: ${event}, ${session_id}, ${client_id}` + ); + } + }, + + // returns session ID on success; returns -1 on failure + // TODO(Brandon): deprecate and remove 8/10/21 + getSessionIdFromSession: function (session) { + let result = -1; + + if (session == null || typeof session === "undefined") { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `tried to get session ID from session, but session was null or undefined` + ); + + return result; + } - removeClientFromSession: function (session, client_id) { - if (session == null) { - this.logErrorSessionClientSocketAction(null, client_id, null, `tried to remove client from session, but session was null`); + if (typeof session !== "object") { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `tried to get session ID from session, but session was not an object` + ); - return; - } + return result; + } - if (session.clients == null) { - this.logErrorSessionClientSocketAction(session.id, client_id, null, `tried to remove client from session, but session.clients was null`); + if (session.clients == null || typeof session.clients === "undefined") { + this.logErrorSessionClientSocketAction( + session.id, + null, + null, + `session.clients was null or undefined` + ); - return; - } + return result; + } - let index = session.clients.indexOf(client_id); + if (session.sockets == null || typeof session.sockets === "undefined") { + this.logErrorSessionClientSocketAction( + session.id, + null, + null, + `session.sockets was null or undefined` + ); - if (session.clients.length == 0 || - session.clients.indexOf(client_id) == -1) { - //client_id is not in the array, so we don't need to remove it. - this.logWarningSessionClientSocketAction(null, null, client_id, `Tried removing client from session.clients, but it was not there. Proceeding anyways.`); + return result; + } - return; - } + this.sessions.forEach((candidate_session, candidate_session_id) => { + if ( + candidate_session.clients == null || + typeof candidate_session.clients === "undefined" + ) { + return; // return from the inner function only. + } + + if ( + candidate_session.sockets == null || + typeof candidate_session.sockets === "undefined" + ) { + return; // return from the inner function only. + } + + if (candidate_session.sockets.size != session.sockets.size) { + return; // return from the inner function only. + } + + if (compareKeys(candidate_session.sockets, session.sockets)) { + result = candidate_session_id; + } + }); + + return result; + }, + + getSessionSocketsFromClientId: function ( + session, + client_id, + excluded_socket_id + ) { + if (session == null) { + this.logErrorSessionClientSocketAction( + null, + client_id, + null, + `tried to get session sockets from client ID, but session was null` + ); + + return null; + } - session.clients.splice(index, 1); - }, + if (session.sockets == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to get session sockets from client ID, but session.sockets was null` + ); - addSocketToSession: function (session, socket, client_id) { - if (!session) { - this.logErrorSessionClientSocketAction(null, client_id, socket.id, `tried to add socket to session, but session was null`); + return null; + } - return; - } + var result = []; - session.sockets[socket.id] = { client_id: client_id, socket: socket }; - }, + for (var candidate_socket_id in session.sockets) { + let isCorrectId = + session.sockets[candidate_socket_id].client_id == client_id; - // returns true iff socket was successfully joined to session - handleJoin: function (err, socket, session_id, client_id, do_bump_duplicates) { - if (!socket) { - this.logErrorSessionClientSocketAction(session_id, client_id, null, `tried to handle join, but socket was null`); - - return false; - } + let doExclude = + session.sockets[candidate_socket_id].socket.id == excluded_socket_id; - if (!this.joinSessionAction) { - this.logWarningSessionClientSocketAction(session_id, client_id, socket.id, `in handleJoin, joinSessionAction callback was not provided. Proceeding anyways.`); - } + if (isCorrectId && !doExclude) { + result.push(session.sockets[candidate_socket_id].socket); + } + } - if (err) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `Error joining client to session: ${err}`); + return result; + }, - return false; - } + isClientInSession: function (session_id, client_id) { + const numInstances = this.getNumClientInstancesForClient(session_id, client_id); - let { success, session } = this.getSession(session_id); + if (numInstances >= 1) { + return true; + } - if (!success || !session) { - this.logWarningSessionClientSocketAction(session_id, client_id, socket.id, "session was null when adding socket to session. Creating a session for you."); + return false; + }, - session = this.createSession(session_id); - } + // returns number of client instances of the same ID on success; returns -1 on failure; + getNumClientInstancesForClient: function (session_id, client_id) { + let session = this.sessions.get(session_id); - success = this.addClientToSession(session, client_id); + if (session == null || session.clients == null) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `Could not get number of client instances -- session was null or session.clients was null.` + ); - if (!success) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `tried to handle join, but adding client to session failed.`); - - return; - } + return -1; + } - this.bumpDuplicateSockets(session, client_id, do_bump_duplicates, socket.id); - - if (do_bump_duplicates) { - this.removeDuplicateClientsFromSession(session, client_id); - } + var count = 0; - // socket to client mapping - this.addSocketToSession(session, socket, client_id); + session.clients.forEach((value) => { + if (value == client_id) { + count += 1; + } + }); - this.joinSessionAction(session_id, client_id); + return count; + }, - // socket successfully joined to session - return true; - }, + // cleanup socket and client references in session state if reconnect fails + removeSocketFromSession: function (socket, session_id, client_id) { + if (!socket) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `tried removing socket from session, but socket was null` + ); - //TODO rewrite this so that do_bump_duplicates and socket_id become ids_to_keep - bumpDuplicateSockets: function (session, client_id, do_bump_duplicates, socket_id) { - if (session == null) { - this.logErrorSessionClientSocketAction(null, client_id, socket_id, `tried to bump duplicate sockets, but session was null`); + return; + } - return; - } + if (!this.disconnectAction) { + this.logWarningSessionClientSocketAction( + null, + client_id, + socket.id, + `in removeSocketFromSession, disconnectAction callback was not provided` + ); + } - let session_id = this.getSessionIdFromSession(session); + this.disconnectAction(socket, session_id, client_id); - if (this.bumpAction == null) { - this.logWarningSessionClientSocketAction(session.id, client_id, socket_id, `in bumpDuplicateSockets, bumpAction callback was not provided`); - } - - let sockets; - - if (do_bump_duplicates) { - sockets = this.getSessionSocketsFromClientId(session, client_id, socket_id); - } else { - sockets = this.getSessionSocketsFromClientId(session, client_id, null); - } + // clean up + let session = this.sessions.get(session_id); - let self = this; + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Could not find session when trying to remove a socket from it.` + ); - if (!sockets) { - this.logWarningSessionClientSocketAction(session.id, client_id, socket_id, `tried to bump duplicate sockets, but result of getSessionSocketsFromClientId was null. Proceeding anyways.`); - } - - sockets.forEach((socket) => { - self.bumpAction(session_id, socket); + return; + } - self.removeSocketFromSession(socket, session_id, client_id); - }); - }, + if (!(socket.id in session.sockets)) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + `tried removing socket from session.sockets, but it was not found.` + ); - writeEventToConnections: function (event, session_id, client_id) { - if (event && session_id && client_id) { - if (!this.pool) { - this.logErrorSessionClientSocketAction(session_id, client_id, null, "pool was null"); + return; + } - return; - } - - if (this.pool) { - this.pool.query( - "INSERT INTO connections(timestamp, session_id, client_id, event) VALUES(?, ?, ?, ?)", [Date.now(), session_id, client_id, event], - - (err, res) => { - if (err != undefined) { - this.logErrorSessionClientSocketAction(session_id, client_id, null, `Error writing ${event} event to database: ${err} ${res}`); - } - } - ); - } - } else { - this.logger.error(`Failed to log event to database: ${event}, ${session_id}, ${client_id}`); - } - }, + // remove socket->client mapping + delete session.sockets[socket.id]; - // returns session ID on success; returns -1 on failure - // TODO(Brandon): deprecate and remove 8/10/21 - getSessionIdFromSession: function (session) { - let result = -1; + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Removed client from session.` + ); - if (session == null || typeof session === "undefined") { - this.logErrorSessionClientSocketAction(null, null, null, `tried to get session ID from session, but session was null or undefined`); + this.removeClientFromSession(session, client_id); + }, - return result; - } + getNumClientInstances: function (session_id) { + let session = this.sessions.get(session_id); - if (typeof session !== "object") { - this.logErrorSessionClientSocketAction(null, null, null, `tried to get session ID from session, but session was not an object`); + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `tried to get number of clients for a session, but it was not found.` + ); - return result; - } + return -1; + } - if (session.clients == null || typeof session.clients === "undefined") { - this.logErrorSessionClientSocketAction(session.id, null, null, `session.clients was null or undefined`); + if (session.clients == null) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `the session's session.clients was null.` + ); - return result; - } + return -1; + } - if (session.sockets == null || typeof session.sockets === "undefined") { - this.logErrorSessionClientSocketAction(session.id, null, null, `session.sockets was null or undefined`); + return session.clients.length; + }, - return result; - } + try_to_end_recording: function (session_id) { + let session = this.sessions.get(session_id); - this.sessions.forEach((candidate_session, candidate_session_id) => { - if (candidate_session.clients == null || - typeof candidate_session.clients === "undefined") { - return; // return from the inner function only. - } - - if (candidate_session.sockets == null || - typeof candidate_session.sockets === "undefined") { - return; // return from the inner function only. - } + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `tried to end recording for session ${session_id}, but it was not found.` + ); - if (candidate_session.sockets.size != session.sockets.size) { - return; // return from the inner function only. - } + return; + } - if (compareKeys(candidate_session.sockets, session.sockets)) { - result = candidate_session_id; - } - }); + if (!session.isRecording) { + return; + } - return result; - }, - - getSessionSocketsFromClientId: function (session, client_id, excluded_socket_id) { - if (session == null) { - this.logErrorSessionClientSocketAction(null, client_id, null, `tried to get session sockets from client ID, but session was null`); + this.logInfoSessionClientSocketAction( + session_id, + null, + null, + `Stopping recording for empty session` + ); + + this.end_recording(session_id); + }, + + // clean up session from sessions map if empty, write + cleanUpSessionIfEmpty: function (session_id) { + if (this.getNumClientInstancesForClient(session_id) >= 0) { + // don't clean up if there are still clients in the session + return; + } - return null; - } + this.logInfoSessionClientSocketAction( + session_id, + null, + null, + `Ending empty session` + ); - if (session.sockets == null) { - this.logErrorSessionClientSocketAction(session.id, client_id, null, `tried to get session sockets from client ID, but session.sockets was null`); + this.try_to_end_recording(session_id); - return null; - } + this.sessions.delete(session_id); + }, - var result = []; + // if a session exists, return it. Otherwise, create one with default values, register it, and return it. + getOrCreateSession: function (session_id) { + let { success, session } = this.getSession(session_id); - for (var candidate_socket_id in session.sockets) { - let isCorrectId = session.sockets[candidate_socket_id].client_id == client_id; + if (success) { + return session; + } - let doExclude = (session.sockets[candidate_socket_id].socket.id == excluded_socket_id); + return this.createSession(session_id); + }, - if (isCorrectId && !doExclude) { - result.push(session.sockets[candidate_socket_id].socket); - } - } + getSession: function (session_id) { + let _session = this.sessions.get(session_id); - return result; - }, + if (_session != null && typeof _session != "undefined") { + return { + success: true, - // returns number of client instances of the same ID on success; returns -1 on failure; - getNumClientInstancesForClient: function (session_id, client_id) { - let session = this.sessions.get(session_id); + session: _session, + }; + } - if (session == null || session.clients == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, null, `Could not get number of client instances -- session was null or session.clients was null.`); + return { + success: false, + + session: null, + }; + }, + + initialize_recording_writers: function () {}, + + createSession: function (session_id) { + this.logInfoSessionClientSocketAction( + session_id, + null, + null, + `Creating session: ${session_id}` + ); + + this.sessions.set(session_id, { + id: session_id, + sockets: {}, // socket.id -> client_id + clients: [], + entities: [], + scene: null, + isRecording: false, + start: Date.now(), + recordingStart: 0, + seq: 0, + // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 + // writers: { + // pos: { + // buffer: Buffer.alloc(this.positionWriteBufferSize()), + // cursor: 0 + // }, + // int: { + // buffer: Buffer.alloc(this.interactionWriteBufferSize()), + // cursor: 0 + // } + // }, + message_buffer: [], + }); + + session = this.sessions.get(session_id); + + return session; + }, + + processReconnectionAttempt: function (err, socket, session_id, client_id) { + let success = this.handleJoin(err, socket, session_id, client_id, true); + + if (!success) { + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + "failed to reconnect" + ); + + this.removeSocketFromSession(socket, session_id, client_id); + + this.cleanUpSessionIfEmpty(session_id); + + return false; + } - return -1; - } + ////TODO does this need to be called here???? this.bumpOldSockets(session_id, client_id, socket.id); - var count = 0; + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + "successfully reconnected" + ); - session.clients.forEach((value) => { - if (value == client_id) { - count += 1; - } - }); + return true; + }, - return count; - }, + whoDisconnected: function (socket) { + for (var s in this.sessions) { + const session_id = s[0]; - // cleanup socket and client references in session state if reconnect fails - removeSocketFromSession: function (socket, session_id, client_id) { - if (!socket) { - this.logErrorSessionClientSocketAction(session_id, client_id, null, `tried removing socket from session, but socket was null`); + let session = s[1]; - return; - } + if (!(socket.id in session.sockets)) { + // This isn't the right session, so keep looking. + continue; + } - if (!this.disconnectAction) { - this.logWarningSessionClientSocketAction(null, client_id, socket.id, `in removeSocketFromSession, disconnectAction callback was not provided`); - } + // We found the right session. - this.disconnectAction(socket, session_id, client_id); + return { + session_id: session_id, - // clean up - let session = this.sessions.get(session_id); + client_id: client_id, + }; + } - if (!session) { - this.logWarningSessionClientSocketAction(session_id, client_id, socket.id, `Could not find session when trying to remove a socket from it.`); + return { + session_id: null, - return; - } + client_id: null, + }; + }, - if (!(socket.id in session.sockets)) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `tried removing socket from session.sockets, but it was not found.`); + // returns true if socket is still connected + handleDisconnect: function (socket, reason) { + if (!socket) { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `tried handling disconnect, but socket was null` + ); - return; - } + return false; + } - // remove socket->client mapping - delete session.sockets[socket.id]; + if (!this.reconnectAction) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + `in handleDisconnect, reconnectAction callback was not provided` + ); - this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, `Removed client from session.`); - - this.removeClientFromSession(session, client_id); - }, + return false; + } - getNumClientInstances: function (session_id) { - let session = this.sessions.get(session_id); + // Check disconnect event reason and handle + // see https://socket.io/docs/v2/server-api/index.html + + let knownReasons = { + // the disconnection was initiated by the server + "server namespace disconnect": { + doReconnect: false, + }, + // The socket was manually disconnected using socket.disconnect() + "client namespace disconnect": { + doReconnect: false, + }, + // The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) + "transport close": { + doReconnect: false, + }, + // The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) + "transport error": { + doReconnect: false, + }, + // The server did not send a PING within the pingInterval + pingTimeout range. + "ping timeout": { + doReconnect: true, + }, + }; + + let doReconnectOnUnknownReason = true; + + // find which session this socket is in + for (var s of this.sessions) { + let session_id = s[0]; + + let session = s[1]; + + if (!(socket.id in session.sockets)) { + // This isn't the right session, so keep looking. + continue; + } + + // We found the right session. + + let client_id = session.sockets[socket.id].client_id; + + if ( + (knownReasons.hasOwnProperty(reason) && + knownReasons[reason].doReconnect) || + doReconnectOnUnknownReason + ) { + return this.reconnectAction( + reason, + socket, + session_id, + client_id, + session + ); + } + + //Disconnect the socket + + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify( + session.clients + )}` + ); + + this.removeSocketFromSession(socket, session_id, client_id); + + this.cleanUpSessionIfEmpty(session_id); + + return false; // Don't continue to check other sessions. + } - if (!session) { - this.logWarningSessionClientSocketAction(session_id, null, null, `tried to get number of clients for a session, but it was not found.`); + //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. + this.logInfoSessionClientSocketAction( + null, + null, + socket.id, + `disconnected. Not found in sessions. Probably ok.)` + ); + }, + + createCapturesDirectory: function () { + if (!fs.existsSync(config.capture.path)) { + this.logInfoSessionClientSocketAction( + null, + null, + null, + `Creating directory for session captures: ${config.capture.path}` + ); + + fs.mkdirSync(config.capture.path); + } + }, + + getSessions: function () { + return this.sessions; + }, + + initGlobals: function () { + this.sessions = new Map(); + }, + +// Check if message payload is pre-parsed. + // mutates data.message + // returns data.message + parseMessageIfNeeded: function (data, session_id, client_id) { + // TODO(Brandon): evaluate whether to unpack here or keep as a string. + if (typeof data.message == `object`) { + //payload is already parsed. + return data.message; + } - return -1; - } + try { + // parse and replace message payload + data.message = JSON.parse(data.message); - if (session.clients == null) { - this.logWarningSessionClientSocketAction(session_id, null, null, `the session's session.clients was null.`); + return data.message; + } catch (e) { + this.logWarningSessionClientSocketAction(session_id, client_id, "n/a", + `Failed to parse 'interaction' message payload: ${data.message}; ` + ); - return -1; - } + return data.message; + } + }, - return session.clients.length; - }, + getEntityFromState: function (session, id) { + let i = session.entities.findIndex((candidateEntity) => candidateEntity.id == id); - try_to_end_recording: function (session_id) { - let session = this.sessions.get(session_id); + if (i == -1) { + return null; + } - if (!session) { - this.logWarningSessionClientSocketAction(session_id, null, null, `tried to end recording for session ${session_id}, but it was not found.`); + return session.entities[i]; + }, - return; - } + applyShowInteractionToState: function (session, target_id) { + let foundEntity = self.getEntityFromState(session, target_id); - if (!session.isRecording) { - return; - } + if (foundEntity == null) { + self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply show interaction to state: no entity with target_id ${target_id} found. Creating one.`); + + let entity = { + id: target_id, + latest: data.message, + render: true, + locked: false, + }; - this.logInfoSessionClientSocketAction(session_id, null, null, `Stopping recording for empty session`); + session.entities.push(entity); + + return; + } - this.end_recording(session_id); - }, + foundEntity.render = true; + }, - // clean up session from sessions map if empty, write - cleanUpSessionIfEmpty: function (session_id) { - if (this.getNumClientInstancesForClient(session_id) >= 0) { - // don't clean up if there are still clients in the session - return; - } + applyHideInteractionToState: function (session, target_id) { + let foundEntity = self.getEntityFromState(session, target_id); - this.logInfoSessionClientSocketAction(session_id, null, null, `Ending empty session`); + if (foundEntity == null) { + self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply hide interaction to state: no entity with target_id ${target_id} found. Creating one.`); + + let entity = { + id: target_id, + latest: [], + render: false, + locked: false, + }; - this.try_to_end_recording(session_id); + session.entities.push(entity); - this.sessions.delete(session_id); - }, + return; + } - // if a session exists, return it. Otherwise, create one with default values, register it, and return it. - getOrCreateSession: function (session_id) { - let { success, session } = this.getSession(session_id); + foundEntity.render = false; + }, - if (success) { - return session; - } + applyLockInteractionToState: function (session, target_id) { + let foundEntity = self.getEntityFromState(session, target_id); - return this.createSession(session_id); - }, + if (foundEntity == null) { + self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply lock interaction to state: no entity with target_id ${target_id} found. Creating one.`); + + let entity = { + id: target_id, + latest: [], // TODO(Brandon): investigate this. data.message? + render: true, + locked: true, + }; - getSession: function (session_id) { - let _session = this.sessions.get(session_id); + session.entities.push(entity); - if (_session != null && typeof _session != "undefined") { - return { - success: true, + return; + } - session: _session - }; - } + foundEntity.locked = true; + }, - return { - success: false, + applyUnlockInteractionToState: function (session, target_id) { + let foundEntity = self.getEntityFromState(session, target_id); - session: null + if (foundEntity == null) { + self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply unlock interaction to state: no entity with target_id ${target_id} found. Creating one.`); + + let entity = { + id: target_id, + latest: [], // TODO(Brandon): investigate this. data.message? + render: true, + locked: false, }; - }, - initialize_recording_writers: function () { - }, + session.entities.push(entity); - createSession: function (session_id) { - this.logInfoSessionClientSocketAction(session_id, null, null, `Creating session: ${session_id}`); - - this.sessions.set(session_id, { - id: session_id, - sockets: {}, // socket.id -> client_id - clients: [], - entities: [], - scene: null, - isRecording: false, - start: Date.now(), - recordingStart: 0, - seq: 0, - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // writers: { - // pos: { - // buffer: Buffer.alloc(this.positionWriteBufferSize()), - // cursor: 0 - // }, - // int: { - // buffer: Buffer.alloc(this.interactionWriteBufferSize()), - // cursor: 0 - // } - // }, - message_buffer: [] - }); + return; + } + + foundEntity.locked = false; + }, + + applyStartMoveInteractionToState: function (session, target_id) { + }, - session = this.sessions.get(session_id); + applyEndMoveInteractionToState: function (session, target_id) { + }, + + applyDrawToState: function () { + }, + + applyEraseToState: function () { + }, + + applyInteractionMessageToState: function (session, target_id, interaction_type) { + // entity should be rendered + if (interaction_type == INTERACTION_RENDER) { + self.applyShowInteractionToState(session, target_id); + } + + // entity should stop being rendered + if (interaction_type == INTERACTION_RENDER_END) { + self.applyHideInteractionToState(session, target_id); + } + + // scene has changed + if (interaction_type == INTERACTION_SCENE_CHANGE) { + session.scene = target_id; + } + + // entity is locked + if (interaction_type == INTERACTION_LOCK) { + self.applyLockInteractionToState(session, target_id); + } + + // entity is unlocked + if (interaction_type == INTERACTION_LOCK_END) { + self.applyUnlockInteractionToState(session, target_id); + } + }, + + applyObjectsSyncToState: function (session, data) { + let entity_id = data.message[KomodoMessages.sync.indices.entityId]; + + let foundEntity = self.getEntityFromState(session, entity_id); + + if (foundEntity == null) { + self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); - return session; - }, + let entity = { + id: entity_id, + latest: data.message, + render: true, + locked: false, + }; + + session.entities.push(entity); - processReconnectionAttempt: function (err, socket, session_id, client_id) { - let success = this.handleJoin(err, socket, session_id, client_id, true); + return; + } - if (!success) { - this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, 'failed to reconnect'); + foundEntity.latest = data.message; + }, - this.removeSocketFromSession(socket, session_id, client_id); + applySyncMessageToState: function (session, data) { + // update session state with latest entity positions - this.cleanUpSessionIfEmpty(session_id); + let entity_type = data.message[KomodoMessages.sync.indices.entityType]; - return false; - } + if (entity_type == SYNC_OBJECTS) { + self.applyObjectsSyncToState(session, data); + } + }, + + processMessage: function (data, socket) { + if (data == null) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + "tried to process message, but data was null" + ); + } + + let session_id = data.session_id; - ////TODO does this need to be called here???? this.bumpOldSockets(session_id, client_id, socket.id); + if (!session_id) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + "tried to process message, but session_id was null" + ); - this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, 'successfully reconnected'); + return; + } - return true; - }, + let client_id = data.client_id; - whoDisconnected: function (socket) { - for (var s in this.sessions) { - const session_id = s[0]; + if (!client_id) { + this.logErrorSessionClientSocketAction( + session_id, + null, + socket.id, + "tried to process message, but client_id was null" + ); - let session = s[1]; + return; + } - if (!(socket.id in session.sockets)) { - // This isn't the right session, so keep looking. - continue; - } + let type = data.type; - // We found the right session. + if (!type) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + "tried to process message, but type was null" + ); - return { - session_id: session_id, + return; + } - client_id: client_id - }; - } + let session = self.sessions.get(session_id); - return { - session_id: null, + if (!session) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + "tried to process message, but session was not found" + ); - client_id: null - }; - }, + return; + } + + if (!data.message) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + "tried to process message, but data.message was null" + ); + + return; + } + + // `message` here will be in the legacy packed-array format. + + // NOTE(rob): the following code is copypasta from the old interactionUpdate handler. 7/21/2021 - // returns true if socket is still connected - handleDisconnect: function (socket, reason) { - if (!socket) { - this.logErrorSessionClientSocketAction(null, null, null, `tried handling disconnect, but socket was null`); + // check if the incoming packet is from a client who is valid for this session + if (!self.isClientInSession(session_id, client_id)) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but client was not in session."); + + return; + } + + if (!data.message.length) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but data.message.length was 0."); + + return; + } + + // relay the message + self.messageAction(socket, session_id, data); - return false; + data.message = self.parseMessageIfNeeded(data, session_id, client_id); + + // get reference to session and parse message payload for state updates, if needed. + if (type == KomodoMessages.interaction.type) { + if (data.message.length != KomodoMessages.interaction.minLength) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: data.message.length was incorrect"); + + return; } + + let source_id = data.message[KomodoMessages.interaction.indices.sourceId]; - if (!this.reconnectAction) { - this.logErrorSessionClientSocketAction(null, null, socket.id, `in handleDisconnect, reconnectAction callback was not provided`); + if (source_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: source_id was null"); + } + + let target_id = data.message[KomodoMessages.interaction.indices.targetId]; - return false; + if (target_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: target_id was null"); } - // Check disconnect event reason and handle - // see https://socket.io/docs/v2/server-api/index.html - - let knownReasons = { - // the disconnection was initiated by the server - "server namespace disconnect": { - doReconnect: false, - }, - // The socket was manually disconnected using socket.disconnect() - "client namespace disconnect": { - doReconnect: false, - }, - // The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) - "transport close": { - doReconnect: false, - }, - // The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) - "transport error": { - doReconnect: false, - }, - // The server did not send a PING within the pingInterval + pingTimeout range. - "ping timeout": { - doReconnect: true, - }, - }; + let interaction_type = data.message[KomodoMessages.interaction.indices.interactionType]; - let doReconnectOnUnknownReason = true; + if (interaction_type == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: interaction_type was null"); + } - // find which session this socket is in - for (var s of this.sessions) { - let session_id = s[0]; + self.applyInteractionMessageToState(session, target_id, interaction_type); + } - let session = s[1]; + if (type == KomodoMessages.sync.type) { + self.applySyncMessageToState(e, id, latest, render, locked); + } - if (!(socket.id in session.sockets)) { - // This isn't the right session, so keep looking. - continue; - } + // data capture + if (session.isRecording) { + self.record_message_data(data); + } + }, + init: function (io, pool, logger) { + this.initGlobals(); - // We found the right session. - - let client_id = session.sockets[socket.id].client_id; + this.createCapturesDirectory(); - if ((knownReasons.hasOwnProperty(reason) && knownReasons[reason].doReconnect) || doReconnectOnUnknownReason) { - return this.reconnectAction(reason, socket, session_id, client_id, session); - } + if (logger == null) { + console.warn("No logger was found."); + } - //Disconnect the socket + this.logger = logger; + if (!this.logger) { + console.error("Failed to init logger. Exiting."); + process.exit(); + } - this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify(session.clients)}`); + this.logInfoSessionClientSocketAction( + "Session ID", + "Client ID", + "Socket ID", + "Message" + ); - this.removeSocketFromSession(socket, session_id, client_id); + if (pool == null) { + if (this.logger) this.logger.warn("No MySQL Pool was found."); + } - this.cleanUpSessionIfEmpty(session_id); + this.pool = pool; - return false; // Don't continue to check other sessions. - } + let self = this; - //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. - this.logInfoSessionClientSocketAction(null, null, socket.id, `disconnected. Not found in sessions. Probably ok.)`); - }, + if (io == null) { + if (this.logger) this.logger.warn("No SocketIO server was found."); + } - createCapturesDirectory: function () { - if (!fs.existsSync(config.capture.path)) { - this.logInfoSessionClientSocketAction(null, null, null, `Creating directory for session captures: ${config.capture.path}`); + this.connectionAuthorizationErrorAction = function (socket, message) { + socket.emit(KomodoSendEvents.connectionError, message); + }; - fs.mkdirSync(config.capture.path); - } - }, + this.messageAction = function (socket, session_id, data) { + socket.to(session_id.toString()).emit(KomodoSendEvents.message, data); + }; - getSessions: function () { - return this.sessions; - }, + this.bumpAction = function (session_id, socket) { + self.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `leaving session` + ); - initGlobals: function () { - this.sessions = new Map(); - }, + socket.leave(session_id.toString(), (err) => { + if (err) { + self.logErrorSessionClientSocketAction( + session_id, + null, + socket.id, + err + ); + + return; + } + }); + + self.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `Disconnecting: ...` + ); + + setTimeout(() => { + socket.disconnect(true); + + self.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `Disconnecting: Done.` + ); + }, 500); // delay half a second and then bump the old socket + }; + + this.joinSessionAction = function (session_id, client_id) { + io.to(session_id.toString()).emit(KomodoSendEvents.joined, client_id); + }; + + this.disconnectAction = function (socket, session_id, client_id) { + // notify and log event + socket + .to(session_id.toString()) + .emit(KomodoSendEvents.disconnected, client_id); + }; + + this.stateErrorAction = function (socket, message) { + socket.emit(KomodoSendEvents.stateError, message); + }; + + // returns true for successful reconnection + this.reconnectAction = function ( + reason, + socket, + session_id, + client_id, + session + ) { + self.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Client was disconnected; attempting to reconnect. Disconnect reason: , clients: ${JSON.stringify( + session.clients + )}` + ); + + socket.join(session_id.toString(), (err) => { + self.processReconnectionAttempt(err, socket, session_id, client_id); + }); + }; + + // main relay handler + io.on(KomodoReceiveEvents.connection, function (socket) { + self.logInfoSessionClientSocketAction( + null, + null, + socket.id, + `Session connection` + ); + + socket.on(KomodoReceiveEvents.sessionInfo, function (session_id) { + let session = self.sessions.get(session_id); - init: function (io, pool, logger) { - this.initGlobals(); + if (!session) { + self.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + `Requested session, but it does not exist.` + ); + + return; + } + + socket + .to(session_id.toString()) + .emit(KomodoSendEvents.sessionInfo, session); + }); + + //Note: "join" is our own event name, and should not be confused with socket.join. (It does not automatically listen for socket.join either.) + socket.on(KomodoReceiveEvents.join, function (data) { + let session_id = data[0]; + + let client_id = data[1]; + + self.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Asked to join` + ); + + if (!client_id || !session_id) { + self.connectionAuthorizationErrorAction( + socket, + "You must provide a client ID and a session ID in the URL options." + ); + + return; + } + + //TODO does this need to be called here???? self.bumpOldSockets(session_id, client_id, socket.id); + + // relay server joins connecting client to session room + socket.join(session_id.toString(), (err) => { + let success = self.handleJoin( + err, + socket, + session_id, + client_id, + true + ); + + if (success) { + // write join event to database + self.writeEventToConnections("connect", session_id, client_id); + } + }); + }); + + // When a client requests a state catch-up, send the current session state. Supports versioning. + socket.on(KomodoReceiveEvents.state, function (data) { + let { session_id, state } = self.handleState(socket, data); - this.createCapturesDirectory(); + if (session_id == -1 || !state) { + self.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + "state was null" + ); - if (logger == null) { - console.warn("No logger was found."); + return; } - this.logger = logger; - if (!this.logger) { - console.error("Failed to init logger. Exiting."); - process.exit(); + try { + // emit versioned state data + io.to(socket.id).emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. + } catch (err) { + this.logErrorSessionClientSocketAction( + session_id, + null, + socket.id, + err.message + ); } + }); + + socket.on(KomodoReceiveEvents.draw, function (data) { + let session_id = data[1]; + + let client_id = data[2]; - this.logInfoSessionClientSocketAction("Session ID", "Client ID", "Socket ID", "Message"); + if (!session_id) { + this.logErrorSessionClientSocketAction( + null, + client_id, + socket.id, + "tried to process draw event, but there was no session_id." + ); - if (pool == null) { - if (this.logger) this.logger.warn("No MySQL Pool was found."); + return; } - this.pool = pool; + if (!client_id) { + this.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + "tried to process draw event, but there was no client_id. Proceeding anyways." + ); - let self = this; + return; + } + + socket.to(session_id.toString()).emit(KomodoSendEvents.draw, data); + }); - if (io == null) { - if (this.logger) this.logger.warn("No SocketIO server was found."); + // general message relay + // TODO(rob): this is where all event data will eventually end up + // we will be doing compares on the data.type value for to-be-defined const values + // of the various interactions we care about, eg. grab, drop, start/end recording, etc. + // in order to update the session state accordingly. we will probably need to protect against + // garbage values that might be passed by devs who are overwriting reserved message events. + socket.on(KomodoReceiveEvents.message, function (data) { + if (socket == null) { + this.logErrorSessionClientSocketAction( + null, + null, + null, + "tried to process message, but socket was null" + ); } - this.connectionAuthorizationErrorAction = function (socket, message) { - socket.emit("connectionError", message); - }; + self.processMessage(data, socket); + }); - this.bumpAction = function (session_id, socket) { - self.logInfoSessionClientSocketAction(session_id, null, socket.id, `leaving session`); + // client position update handler + socket.on(KomodoReceiveEvents.update, function (data) { + if (!self.isValidRelayPacket(data)) { + return; + } - socket.leave(session_id.toString(), (err) => { - if (err) { - self.logErrorSessionClientSocketAction(session_id, null, socket.id, err); + let session_id = data[1]; - return; - } - }); + // relay packet if client is valid + socket + .to(session_id.toString()) + .emit(KomodoSendEvents.relayUpdate, data); - self.logInfoSessionClientSocketAction(session_id, null, socket.id, `Disconnecting: ...`); + // self.writeRecordedRelayData(data); NOTE(rob): DEPRECATED. 8/5/21. - setTimeout(() => { - socket.disconnect(true); + self.updateSessionState(data); + }); - self.logInfoSessionClientSocketAction(session_id, null, socket.id, `Disconnecting: Done.`); - }, 500); // delay half a second and then bump the old socket - }; + // handle interaction events + // see `INTERACTION_XXX` declarations for type values + socket.on(KomodoReceiveEvents.interact, function (data) { + self.handleInteraction(socket, data); + }); - this.joinSessionAction = function (session_id, client_id) { - io.to(session_id.toString()).emit(KomodoSendEvents.joined, client_id); - }; + // session capture handler + socket.on(KomodoReceiveEvents.start_recording, function (session_id) { + self.start_recording(pool, session_id); + }); - this.disconnectAction = function (socket, session_id, client_id) { - // notify and log event - socket.to(session_id.toString()).emit(KomodoSendEvents.disconnected, client_id); - }; + socket.on(KomodoReceiveEvents.end_recording, function (session_id) { + self.end_recording(pool, session_id); + }); - this.stateErrorAction = function (socket, message) { - socket.emit(KomodoSendEvents.stateError, message); - }; + socket.on(KomodoReceiveEvents.playback, function (data) { + self.handlePlayback(io, data); + }); - // returns true for successful reconnection - this.reconnectAction = function (reason, socket, session_id, client_id, session) { - self.logInfoSessionClientSocketAction(session_id, client_id, socket.id, `Client was disconnected; attempting to reconnect. Disconnect reason: ${reason}, clients: ${JSON.stringify(session.clients)}`); - - socket.join(session_id.toString(), (err) => { - self.processReconnectionAttempt(err, socket, session_id, client_id); - }); - }; + socket.on(SocketIOEvents.disconnect, function (reason) { + const { session_id, client_id } = self.whoDisconnected(socket); - // main relay handler - io.on(KomodoReceiveEvents.connection, function(socket) { - self.logInfoSessionClientSocketAction(null, null, socket.id, `Session connection`); - - socket.on(KomodoReceiveEvents.sessionInfo, function (session_id) { - let session = self.sessions.get(session_id); - - if (!session) { - self.logWarningSessionClientSocketAction(session_id, null, socket.id, `Requested session, but it does not exist.`); - - return; - } - - socket.to(session_id.toString()).emit(KomodoSendEvents.sessionInfo, session); - }); - - //Note: "join" is our own event name, and should not be confused with socket.join. (It does not automatically listen for socket.join either.) - socket.on(KomodoReceiveEvents.join, function(data) { - let session_id = data[0]; - - let client_id = data[1]; - - self.logInfoSessionClientSocketAction(session_id, client_id, socket.id, `Asked to join`); - - if (!client_id || !session_id) { - self.connectionAuthorizationErrorAction(socket, "You must provide a client ID and a session ID in the URL options."); - - return; - } - - //TODO does this need to be called here???? self.bumpOldSockets(session_id, client_id, socket.id); - - // relay server joins connecting client to session room - socket.join(session_id.toString(), (err) => { - let success = self.handleJoin(err, socket, session_id, client_id, true); - - if (success) { - // write join event to database - self.writeEventToConnections("connect", session_id, client_id); - } - }); - }); - - // When a client requests a state catch-up, send the current session state. Supports versioning. - socket.on(KomodoReceiveEvents.state, function(data) { - let { session_id, state } = self.handleState(socket, data); - - if (session_id == -1 || !state) { - self.logWarningSessionClientSocketAction(session_id, null, socket.id, "state was null"); - - return; - } - - try { - // emit versioned state data - io.to(socket.id).emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. - } catch (err) { - this.logErrorSessionClientSocketAction(session_id, null, socket.id, err.message); - } - }); - - socket.on(KomodoReceiveEvents.draw, function(data) { - let session_id = data[1]; - - let client_id = data[2]; - - if (session_id && client_id) { - socket.to(session_id.toString()).emit(KomodoSendEvents.draw, data); - } - }); - - // general message relay - // TODO(rob): this is where all event data will eventually end up - // we will be doing compares on the data.type value for to-be-defined const values - // of the various interactions we care about, eg. grab, drop, start/end recording, etc. - // in order to update the session state accordingly. we will probably need to protect against - // garbage values that might be passed by devs who are overwriting reserved message events. - socket.on(KomodoReceiveEvents.message, function(data) { - if (data) { - let session_id = data.session_id; - let client_id = data.client_id; - let type = data.type; - - if (session_id && client_id && type && data.message) { - // relay the message - socket.to(session_id.toString()).emit(KomodoSendEvents.message, data); - - // get reference to session and parse message payload for state updates, if needed. - let session = self.sessions.get(session_id); - if (session) { - if (type == "interaction") { - // `message` here will be in the legacy packed-array format. - - // NOTE(rob): the following code is copypasta from the old interactionUpdate handler. 7/21/2021 - - // check if the incoming packet is from a client who is valid for this session - let joined = false; - for (let i=0; i < session.clients.length; i++) { - if (client_id == session.clients[i]) { - joined = true; - break; - } - } - - if (!joined) return; - - if (!data.message.length) return; // no message payload, nothing to do. - - // Check if message payload is pre-parsed. - // TODO(Brandon): evaluate whether to unpack here or keep as a string. - if (typeof data.message != `object`) { - try { - // parse and replace message payload - data.message = JSON.parse(data.message); - } catch (e) { - console.log(`Failed to parse 'interaction' message payload: ${data.message}; ${e}`); - return; - } - } - - let source_id = data.message[3]; - let target_id = data.message[4]; - let interaction_type = data.message[5]; - - // entity should be rendered - if (interaction_type == INTERACTION_RENDER) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].render = true; - } else { - let entity = { - id: target_id, - latest: [], - render: true, - locked: false - }; - session.entities.push(entity); - } - } - - // entity should stop being rendered - if (interaction_type == INTERACTION_RENDER_END) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].render = false; - } else { - let entity = { - id: target_id, - latest: data.message, - render: false, - locked: false - }; - session.entities.push(entity); - } - } - - // scene has changed - if (interaction_type == INTERACTION_SCENE_CHANGE) { - session.scene = target_id; - } - - // entity is locked - if (interaction_type == INTERACTION_LOCK) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].locked = true; - } else { - let entity = { - id: target_id, - latest: [], - render: false, - locked: true - }; - session.entities.push(entity); - } - } - - // entity is unlocked - if (interaction_type == INTERACTION_LOCK_END) { - let i = session.entities.findIndex(e => e.id == target_id); - if (i != -1) { - session.entities[i].locked = false; - } else { - let entity = { - id: target_id, - latest: [], - render: false, - locked: false - }; - session.entities.push(entity); - } - } - } - - if (type == "sync") { - // update session state with latest entity positions - - if (!data.message.length) return; // no message payload, nothing to do. - - // Check if message payload is pre-parsed. - // TODO(Brandon): evaluate whether to unpack here or keep as a string. - if (typeof data.message != `object`) { - try { - data.message = JSON.parse(data.message); - } catch (e) { - console.log(`Failed to parse 'sync' message payload: ${data.message}; ${e}`); - return; - } - } - - let entity_type = data.message[4]; - - if (entity_type == 3) { - let entity_id = data.message[3]; - - let i = session.entities.findIndex(e => e.id == entity_id); - - if (i != -1) { - session.entities[i].latest = data.message; - } else { - let entity = { - id: entity_id, - latest: data.message, - render: true, - locked: false - }; - - session.entities.push(entity); - } - } - } - - // data capture - if (session.isRecording) { - self.record_message_data(data); - } - } - } - } - }); - - // client position update handler - socket.on(KomodoReceiveEvents.update, function(data) { - if (!self.isValidRelayPacket(data)) { - return; - } - - let session_id = data[1]; - - // relay packet if client is valid - socket.to(session_id.toString()).emit(KomodoSendEvents.relayUpdate, data); - - // self.writeRecordedRelayData(data); NOTE(rob): DEPRECATED. 8/5/21. - - self.updateSessionState(data); - }); - - // handle interaction events - // see `INTERACTION_XXX` declarations for type values - socket.on(KomodoReceiveEvents.interact, function(data) { - self.handleInteraction(socket, data); - }); - - // session capture handler - socket.on(KomodoReceiveEvents.start_recording, function (session_id) { - self.start_recording(pool, session_id); - }); - - socket.on(KomodoReceiveEvents.end_recording, function (session_id) { - self.end_recording(pool, session_id); - }); - - socket.on(KomodoReceiveEvents.playback, function(data) { - self.handlePlayback(io, data); - }); - - socket.on(SocketIOEvents.disconnect, function (reason) { - const { session_id, client_id } = self.whoDisconnected(socket); - - let didReconnect = self.handleDisconnect(socket, reason); - - if (didReconnect) { - // log reconnect event with timestamp to db - self.writeEventToConnections("reconnect", session_id, client_id); - return; - } - - // log reconnect event with timestamp to db - self.writeEventToConnections("disconnect", session_id, client_id); - }); - }); - } -}; \ No newline at end of file + let didReconnect = self.handleDisconnect(socket, reason); + + if (didReconnect) { + // log reconnect event with timestamp to db + self.writeEventToConnections("reconnect", session_id, client_id); + return; + } + + // log reconnect event with timestamp to db + self.writeEventToConnections("disconnect", session_id, client_id); + }); + }); + }, +}; From ca84dde2ef63be9cc60c67265e59860585f6431b Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 14 Oct 2021 00:36:49 -0500 Subject: [PATCH 05/24] squash with previous --- sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync.js b/sync.js index 7afdc4f..b8e3c03 100644 --- a/sync.js +++ b/sync.js @@ -1216,7 +1216,7 @@ module.exports = { }, writeEventToConnections: function (event, session_id, client_id) { - if (event && session_id && client_id) { + if (event && session_id && client_id) { //TODO(Brandon): support session_id = null and client_id = null if (!this.pool) { this.logErrorSessionClientSocketAction( session_id, @@ -1988,7 +1988,7 @@ module.exports = { self.applyObjectsSyncToState(session, data); } }, - + processMessage: function (data, socket) { if (data == null) { this.logErrorSessionClientSocketAction( From 182db1498b6334638eee5ebf6b9a8cdb30ae9713 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Fri, 15 Oct 2021 18:55:50 -0500 Subject: [PATCH 06/24] WIP: refactored adding and removing socket and client, refactored join and disconnect, fixed transport disconnect bug --- sync.js | 497 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 359 insertions(+), 138 deletions(-) diff --git a/sync.js b/sync.js index b8e3c03..05fc8bd 100644 --- a/sync.js +++ b/sync.js @@ -85,6 +85,7 @@ const SocketIOEvents = { const KomodoReceiveEvents = { join: "join", + leave: "leave", sessionInfo: "sessionInfo", state: "state", draw: "draw", @@ -100,6 +101,11 @@ const KomodoSendEvents = { connectionError: "connectionError", interactionUpdate: "interactionUpdate", joined: "joined", + failedToJoin: "failedToJoin", + successfullyJoined: "successfullyJoined", + left: "left", + failedToLeave: "failedToLeave", + successfullyLeft: "successfullyLeft", disconnected: "disconnected", sessionInfo: "sessionInfo", state: "state", @@ -128,6 +134,33 @@ const KomodoMessages = { } }; +// see https://socket.io/docs/v2/server-api/index.html + +const DisconnectKnownReasons = { + // the disconnection was initiated by the server + "server namespace disconnect": { + doReconnect: false, + }, + // The socket was manually disconnected using socket.disconnect() + "client namespace disconnect": { + doReconnect: false, + }, + // The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) + "transport close": { + doReconnect: true, + }, + // The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) + "transport error": { + doReconnect: true, + }, + // The server did not send a PING within the pingInterval + pingTimeout range. + "ping timeout": { + doReconnect: true, + }, +}; + +const doReconnectOnUnknownReason = true; + module.exports = { // NOTE(rob): deprecated. sessions must use message_buffer. // // write buffers are multiples of corresponding chunks @@ -1022,7 +1055,7 @@ module.exports = { `tried to remove client from session, but session was null` ); - return; + return false; } if (session.clients == null) { @@ -1033,7 +1066,7 @@ module.exports = { `tried to remove client from session, but session.clients was null` ); - return; + return false; } let index = session.clients.indexOf(client_id); @@ -1047,13 +1080,22 @@ module.exports = { null, null, client_id, - `Tried removing client from session.clients, but it was not there. Proceeding anyways.` + `Tried removing client from session.clients, but it was not there.` ); - return; + return false; } session.clients.splice(index, 1); + + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Removed client from session.` + ); + + return true; }, addSocketToSession: function (session, socket, client_id) { @@ -1072,42 +1114,52 @@ module.exports = { }, // returns true iff socket was successfully joined to session - handleJoin: function ( + makeSocketAndClientJoinSession: function ( err, socket, session_id, client_id, do_bump_duplicates ) { - if (!socket) { - this.logErrorSessionClientSocketAction( + var reason; + + if (!this.failedToJoinAction) { + this.logWarningSessionClientSocketAction( session_id, client_id, null, - `tried to handle join, but socket was null` + `in makeSocketAndClientJoinSession, failedToJoinAction callback was not provided. Proceeding anyways.` ); - - return false; } - if (!this.joinSessionAction) { - this.logWarningSessionClientSocketAction( + if (!socket) { + reason = `tried to handle join, but socket was null`; + + this.logErrorSessionClientSocketAction( session_id, client_id, - socket.id, - `in handleJoin, joinSessionAction callback was not provided. Proceeding anyways.` + null, + reason ); + + // don't call failedToJoinAction here because we don't have a socket. + + return; } if (err) { + reason = `Error joining client to session: ${err}`; + this.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - `Error joining client to session: ${err}` + reason ); - return false; + this.failedToJoinAction(session_id, reason); + + return; } let { success, session } = this.getSession(session_id); @@ -1126,12 +1178,16 @@ module.exports = { success = this.addClientToSession(session, client_id); if (!success) { + reason = `tried to handle join, but adding client to session failed.`; + this.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - `tried to handle join, but adding client to session failed.` + reason ); + + this.failedToJoinAction(session_id, reason); return; } @@ -1150,10 +1206,104 @@ module.exports = { // socket to client mapping this.addSocketToSession(session, socket, client_id); - this.joinSessionAction(session_id, client_id); + if (!self.successfullyJoinedAction) { + self.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `in makeSocketAndClientJoinSession, successfullyJoinedAction callback was not provided. Skipping.` + ); - // socket successfully joined to session - return true; + return; + } + + self.successfullyJoinedAction(session_id, client_id); + }, + + makeSocketAndClientLeaveSession: function (err, session_id, client_id, socket) { + var success; + var reason; + + if (!this.failedToLeaveAction) { + self.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `in makeSocketAndClientLeaveSession, failedToLeaveAction callback was not provided. Skipping.` + ); + + return; + } + + if (!socket) { + reason = `makeSocketAndClientLeaveSession: socket was null`; + + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + reason + ); + + // don't call failedToLeaveAction here because we don't have a socket. + + return; + } + + if (err) { + reason = `Error joining client to session: ${err}`; + + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + err + ); + + this.failedToLeaveAction(session_id, reason); + + return; + } + + success = this.removeSocketFromSession(socket, session_id); + + if (!success) { + reason = `removeSocketFromSession failed`; + + this.failedToLeaveAction(session_id, reason); + + return; + } + + success = this.removeClientFromSession(session, client_id); + + if (!success) { + reason = `removeClientFromSession failed`; + + this.failedToLeaveAction(session_id, reason); + + return; + } + + if (!self.successfullyLeftAction) { + self.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `in makeSocketAndClientLeaveSession, successfullyLeftAction callback was not provided. Skipping.` + ); + + return; + } + + self.successfullyLeftAction(session_id, client_id); + + self.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `Left.` + ); }, //TODO rewrite this so that do_bump_duplicates and socket_id become ids_to_keep @@ -1211,7 +1361,7 @@ module.exports = { sockets.forEach((socket) => { self.bumpAction(session_id, socket); - self.removeSocketFromSession(socket, session_id, client_id); + self.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); }); }, @@ -1328,6 +1478,54 @@ module.exports = { return result; }, + getClientIdFromSessionSocket: function (socket) { + if (session == null) { + this.logErrorSessionClientSocketAction( + null, + client_id, + null, + `tried to get client ID from session socket, but session was null` + ); + + return null; + } + + if (session.sockets == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to get client ID from session socket, but session.sockets was null` + ); + + return null; + } + + if (socket.id == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to get client ID from session socket, but socket.id was null` + ); + + return null; + } + + if (session.sockets[socket.id] == null) { + this.logErrorSessionClientSocketAction( + session.id, + client_id, + null, + `tried to get client ID from session socket, but session.sockets[socket.id] was null` + ); + + return null; + } + + return session.sockets[socket.id].client_id; + }, + getSessionSocketsFromClientId: function ( session, client_id, @@ -1382,6 +1580,34 @@ module.exports = { return false; }, + isSocketInSession: function (socket, session_id) { + if (!socket) { + this.logErrorSessionClientSocketAction( + session_id, + null, + null, + `isSocketInSession: socket was null` + ); + + return; + } + + let session = this.sessions.get(session_id); + + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + `isSocketInSession: could not find session` + ); + + return; + } + + return (socket.id in session.sockets); + }, + // returns number of client instances of the same ID on success; returns -1 on failure; getNumClientInstancesForClient: function (session_id, client_id) { let session = this.sessions.get(session_id); @@ -1408,64 +1634,61 @@ module.exports = { return count; }, - // cleanup socket and client references in session state if reconnect fails - removeSocketFromSession: function (socket, session_id, client_id) { - if (!socket) { - this.logErrorSessionClientSocketAction( + removeSocketFromSession: function (socket, session_id) { + let session = this.sessions.get(session_id); + + if (!session) { + this.logWarningSessionClientSocketAction( session_id, client_id, - null, - `tried removing socket from session, but socket was null` + socket.id, + `Could not find session when trying to remove a socket from it.` ); - return; + return false; } - if (!this.disconnectAction) { - this.logWarningSessionClientSocketAction( - null, + if (this.isSocketInSession(socket, session_id)) { + this.logErrorSessionClientSocketAction( + session_id, client_id, socket.id, - `in removeSocketFromSession, disconnectAction callback was not provided` + `tried removing socket from session.sockets, but it was not found.` ); + + return false; } - this.disconnectAction(socket, session_id, client_id); + delete session.sockets[socket.id]; - // clean up - let session = this.sessions.get(session_id); + return true; + }, - if (!session) { - this.logWarningSessionClientSocketAction( + // cleanup socket and client references in session state if reconnect fails + removeSocketAndClientFromSessionThenDisconnectSocket: function (socket, session_id, client_id) { + if (!socket) { + this.logErrorSessionClientSocketAction( session_id, client_id, - socket.id, - `Could not find session when trying to remove a socket from it.` + null, + `tried removing socket from session, but socket was null` ); return; } - if (!(socket.id in session.sockets)) { - this.logErrorSessionClientSocketAction( - session_id, + if (!this.disconnectAction) { + this.logWarningSessionClientSocketAction( + null, client_id, socket.id, - `tried removing socket from session.sockets, but it was not found.` + `in removeSocketAndClientFromSessionThenDisconnectSocket, disconnectAction callback was not provided` ); - - return; } - // remove socket->client mapping - delete session.sockets[socket.id]; + this.disconnectAction(socket, session_id, client_id); - this.logInfoSessionClientSocketAction( - session_id, - client_id, - socket.id, - `Removed client from session.` - ); + let session = this.sessions.get(session_id); this.removeClientFromSession(session, client_id); }, @@ -1614,7 +1837,7 @@ module.exports = { }, processReconnectionAttempt: function (err, socket, session_id, client_id) { - let success = this.handleJoin(err, socket, session_id, client_id, true); + let success = this.makeSocketAndClientJoinSession(err, socket, session_id, client_id, true); if (!success) { this.logInfoSessionClientSocketAction( @@ -1624,7 +1847,7 @@ module.exports = { "failed to reconnect" ); - this.removeSocketFromSession(socket, session_id, client_id); + this.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); this.cleanUpSessionIfEmpty(session_id); @@ -1649,7 +1872,7 @@ module.exports = { let session = s[1]; - if (!(socket.id in session.sockets)) { + if (!this.isSocketInSession(socket, session)) { // This isn't the right session, so keep looking. continue; } @@ -1659,7 +1882,7 @@ module.exports = { return { session_id: session_id, - client_id: client_id, + client_id: this.getClientIdFromSessionSocket(socket), }; } @@ -1670,7 +1893,7 @@ module.exports = { }; }, - // returns true if socket is still connected + // Returns true if socket is still connected handleDisconnect: function (socket, reason) { if (!socket) { this.logErrorSessionClientSocketAction( @@ -1695,87 +1918,52 @@ module.exports = { } // Check disconnect event reason and handle - // see https://socket.io/docs/v2/server-api/index.html - - let knownReasons = { - // the disconnection was initiated by the server - "server namespace disconnect": { - doReconnect: false, - }, - // The socket was manually disconnected using socket.disconnect() - "client namespace disconnect": { - doReconnect: false, - }, - // The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) - "transport close": { - doReconnect: false, - }, - // The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) - "transport error": { - doReconnect: false, - }, - // The server did not send a PING within the pingInterval + pingTimeout range. - "ping timeout": { - doReconnect: true, - }, - }; - - let doReconnectOnUnknownReason = true; - - // find which session this socket is in - for (var s of this.sessions) { - let session_id = s[0]; - - let session = s[1]; - - if (!(socket.id in session.sockets)) { - // This isn't the right session, so keep looking. - continue; - } - - // We found the right session. - - let client_id = session.sockets[socket.id].client_id; - - if ( - (knownReasons.hasOwnProperty(reason) && - knownReasons[reason].doReconnect) || - doReconnectOnUnknownReason - ) { - return this.reconnectAction( - reason, - socket, - session_id, - client_id, - session - ); - } - - //Disconnect the socket + + const { session_id, client_id } = self.whoDisconnected(socket); + if (session_id == null || client_id == null) { + //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. this.logInfoSessionClientSocketAction( - session_id, - client_id, + null, + null, socket.id, - `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify( - session.clients - )}` + `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction or removeSocketAndClientFromSessionThenDisconnectSocket.)` ); - this.removeSocketFromSession(socket, session_id, client_id); + return true; + } - this.cleanUpSessionIfEmpty(session_id); + if ( + (DisconnectKnownReasons.hasOwnProperty(reason) && + DisconnectKnownReasons[reason].doReconnect) || + doReconnectOnUnknownReason + ) { + // Try to reconnect the socket - return false; // Don't continue to check other sessions. + return this.reconnectAction( + reason, + socket, + session_id, + client_id, + session + ); } - //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. + // Disconnect the socket this.logInfoSessionClientSocketAction( - null, - null, + session_id, + client_id, socket.id, - `disconnected. Not found in sessions. Probably ok.)` + `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify( + session.clients + )}` ); + + this.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); + + this.cleanUpSessionIfEmpty(session_id); + + return false; }, createCapturesDirectory: function () { @@ -2205,9 +2393,45 @@ module.exports = { }; this.joinSessionAction = function (session_id, client_id) { + socket.join(session_id.toString(), (err) => { + self.makeSocketAndClientJoinSession( + err, + socket, + session_id, + client_id, + true + ); + }); + }; + + this.failedToJoinAction = function (session_id, reason) { + socket.emit(KomodoSendEvents.joinFailed, session_id, reason); + }; + + this.successfullyJoinedAction = function (session_id, client_id) { + // write join event to database + self.writeEventToConnections("connect", session_id, client_id); + + // tell other clients that a client joined io.to(session_id.toString()).emit(KomodoSendEvents.joined, client_id); }; + this.leaveSessionAction = function (session_id, client_id) { + socket.leave(session_id.toString(), (err) => { + this.makeSocketAndClientLeaveSession(err, session_id, client_id, socket); + }); + }; + + this.failedToLeaveAction = function (session_id, reason) { + // notify others the client has left + socket.emit(KomodoSendEvents.failedToLeave, session_id, reason); + }; + + this.successfullyLeftAction = function (session_id, client_id) { + // notify others the client has left + io.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); + }; + this.disconnectAction = function (socket, session_id, client_id) { // notify and log event socket @@ -2293,21 +2517,18 @@ module.exports = { //TODO does this need to be called here???? self.bumpOldSockets(session_id, client_id, socket.id); - // relay server joins connecting client to session room - socket.join(session_id.toString(), (err) => { - let success = self.handleJoin( - err, - socket, + if (!this.joinSessionAction) { + self.logErrorSessionClientSocketAction( session_id, client_id, - true + socket.id, + `in socket.on(${KomodoReceiveEvents.join}), joinSessionAction callback was not provided` ); - if (success) { - // write join event to database - self.writeEventToConnections("connect", session_id, client_id); - } - }); + return; + } + + this.joinSessionAction(session_id, client_id); }); // When a client requests a state catch-up, send the current session state. Supports versioning. From 2f7f8a39a94f9a7e937280628e7ec1389b0f8e95 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Fri, 15 Oct 2021 21:26:10 -0500 Subject: [PATCH 07/24] fix implementation error for removeSocketAndClientFromSessionThenDisconnectSocket, add unimplemented tests --- package.json | 2 +- sync.js | 139 ++++++++++++++++++++++++++++------------------ test/test-sync.js | 59 ++++++++++++++++++-- 3 files changed, 140 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 67ff87e..e99be69 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Relay server for Komodo VR client application", "main": "serve.js", "scripts": { - "test": "nyc mocha --exit" + "test": "nyc mocha --debug-brk --exit" }, "author": "Grainger IDEA Lab", "license": "NCSA", diff --git a/sync.js b/sync.js index 05fc8bd..0d18189 100644 --- a/sync.js +++ b/sync.js @@ -81,6 +81,8 @@ function compareKeys(a, b) { const SocketIOEvents = { disconnect: "disconnect", + disconnecting: "disconnecting", + error: "error" }; const KomodoReceiveEvents = { @@ -1088,10 +1090,12 @@ module.exports = { session.clients.splice(index, 1); + let session_id = this.getSessionIdFromSession(session); + this.logInfoSessionClientSocketAction( session_id, client_id, - socket.id, + null, `Removed client from session.` ); @@ -1144,7 +1148,7 @@ module.exports = { // don't call failedToJoinAction here because we don't have a socket. - return; + return false; } if (err) { @@ -1159,7 +1163,7 @@ module.exports = { this.failedToJoinAction(session_id, reason); - return; + return false; } let { success, session } = this.getSession(session_id); @@ -1189,7 +1193,7 @@ module.exports = { this.failedToJoinAction(session_id, reason); - return; + return false; } this.bumpDuplicateSockets( @@ -1206,18 +1210,20 @@ module.exports = { // socket to client mapping this.addSocketToSession(session, socket, client_id); - if (!self.successfullyJoinedAction) { - self.logWarningSessionClientSocketAction( + if (!this.successfullyJoinedAction) { + this.logWarningSessionClientSocketAction( session_id, client_id, socket.id, `in makeSocketAndClientJoinSession, successfullyJoinedAction callback was not provided. Skipping.` ); - return; + return true; } - self.successfullyJoinedAction(session_id, client_id); + this.successfullyJoinedAction(session_id, client_id); + + return true; }, makeSocketAndClientLeaveSession: function (err, session_id, client_id, socket) { @@ -1225,7 +1231,7 @@ module.exports = { var reason; if (!this.failedToLeaveAction) { - self.logWarningSessionClientSocketAction( + this.logWarningSessionClientSocketAction( session_id, client_id, socket.id, @@ -1285,8 +1291,8 @@ module.exports = { return; } - if (!self.successfullyLeftAction) { - self.logWarningSessionClientSocketAction( + if (!this.successfullyLeftAction) { + this.logWarningSessionClientSocketAction( session_id, client_id, socket.id, @@ -1296,9 +1302,9 @@ module.exports = { return; } - self.successfullyLeftAction(session_id, client_id); + this.successfullyLeftAction(session_id, client_id); - self.logInfoSessionClientSocketAction( + this.logInfoSessionClientSocketAction( session_id, null, socket.id, @@ -1328,7 +1334,7 @@ module.exports = { if (this.bumpAction == null) { this.logWarningSessionClientSocketAction( - session.id, + session_id, client_id, socket_id, `in bumpDuplicateSockets, bumpAction callback was not provided` @@ -1351,7 +1357,7 @@ module.exports = { if (!sockets) { this.logWarningSessionClientSocketAction( - session.id, + session_id, client_id, socket_id, `tried to bump duplicate sockets, but result of getSessionSocketsFromClientId was null. Proceeding anyways.` @@ -1580,16 +1586,21 @@ module.exports = { return false; }, + // Returns success = true if operation succeeded or false if socket or session with session_id were null. + // Returns isInSession if socket is in session.sockets. isSocketInSession: function (socket, session_id) { if (!socket) { - this.logErrorSessionClientSocketAction( + this.logWarningSessionClientSocketAction( session_id, null, null, - `isSocketInSession: socket was null` + `removeSocketFromSession: socket was null.` ); - return; + return { + success: false, + isInSession: null + }; } let session = this.sessions.get(session_id); @@ -1599,13 +1610,19 @@ module.exports = { session_id, null, socket.id, - `isSocketInSession: could not find session` + `Could not find session when trying to remove a socket from it.` ); - return; + return { + success: false, + isInSession: null + }; } - return (socket.id in session.sockets); + return { + success: true, + isInSession: (socket.id in session.sockets) + }; }, // returns number of client instances of the same ID on success; returns -1 on failure; @@ -1634,24 +1651,25 @@ module.exports = { return count; }, + // Return true iff removing the socket succeeded. removeSocketFromSession: function (socket, session_id) { - let session = this.sessions.get(session_id); + let { success, isInSession } = this.isSocketInSession(socket, session_id); - if (!session) { - this.logWarningSessionClientSocketAction( + if (!success || isInSession == null) { + this.logErrorSessionClientSocketAction( session_id, - client_id, + null, socket.id, - `Could not find session when trying to remove a socket from it.` + `tried removing socket from session.sockets, but there was an error.` ); return false; } - if (this.isSocketInSession(socket, session_id)) { - this.logErrorSessionClientSocketAction( + if (!isInSession) { + this.logWarningSessionClientSocketAction( session_id, - client_id, + null, socket.id, `tried removing socket from session.sockets, but it was not found.` ); @@ -1659,6 +1677,8 @@ module.exports = { return false; } + let session = this.sessions.get(session_id); + delete session.sockets[socket.id]; return true; @@ -1691,6 +1711,8 @@ module.exports = { let session = this.sessions.get(session_id); this.removeClientFromSession(session, client_id); + + this.removeSocketFromSession(socket, session_id); }, getNumClientInstances: function (session_id) { @@ -1872,13 +1894,14 @@ module.exports = { let session = s[1]; - if (!this.isSocketInSession(socket, session)) { + let { success, isInSession } = this.isSocketInSession(socket, session_id); + + if (!success || !isInSession) { // This isn't the right session, so keep looking. continue; } // We found the right session. - return { session_id: session_id, @@ -1919,7 +1942,7 @@ module.exports = { // Check disconnect event reason and handle - const { session_id, client_id } = self.whoDisconnected(socket); + const { session_id, client_id } = this.whoDisconnected(socket); if (session_id == null || client_id == null) { //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. @@ -2022,10 +2045,10 @@ module.exports = { }, applyShowInteractionToState: function (session, target_id) { - let foundEntity = self.getEntityFromState(session, target_id); + let foundEntity = this.getEntityFromState(session, target_id); if (foundEntity == null) { - self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply show interaction to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply show interaction to state: no entity with target_id ${target_id} found. Creating one.`); let entity = { id: target_id, @@ -2043,10 +2066,10 @@ module.exports = { }, applyHideInteractionToState: function (session, target_id) { - let foundEntity = self.getEntityFromState(session, target_id); + let foundEntity = this.getEntityFromState(session, target_id); if (foundEntity == null) { - self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply hide interaction to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply hide interaction to state: no entity with target_id ${target_id} found. Creating one.`); let entity = { id: target_id, @@ -2064,7 +2087,7 @@ module.exports = { }, applyLockInteractionToState: function (session, target_id) { - let foundEntity = self.getEntityFromState(session, target_id); + let foundEntity = this.getEntityFromState(session, target_id); if (foundEntity == null) { self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply lock interaction to state: no entity with target_id ${target_id} found. Creating one.`); @@ -2085,10 +2108,10 @@ module.exports = { }, applyUnlockInteractionToState: function (session, target_id) { - let foundEntity = self.getEntityFromState(session, target_id); + let foundEntity = this.getEntityFromState(session, target_id); if (foundEntity == null) { - self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply unlock interaction to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply unlock interaction to state: no entity with target_id ${target_id} found. Creating one.`); let entity = { id: target_id, @@ -2120,12 +2143,12 @@ module.exports = { applyInteractionMessageToState: function (session, target_id, interaction_type) { // entity should be rendered if (interaction_type == INTERACTION_RENDER) { - self.applyShowInteractionToState(session, target_id); + this.applyShowInteractionToState(session, target_id); } // entity should stop being rendered if (interaction_type == INTERACTION_RENDER_END) { - self.applyHideInteractionToState(session, target_id); + this.applyHideInteractionToState(session, target_id); } // scene has changed @@ -2135,12 +2158,12 @@ module.exports = { // entity is locked if (interaction_type == INTERACTION_LOCK) { - self.applyLockInteractionToState(session, target_id); + this.applyLockInteractionToState(session, target_id); } // entity is unlocked if (interaction_type == INTERACTION_LOCK_END) { - self.applyUnlockInteractionToState(session, target_id); + this.applyUnlockInteractionToState(session, target_id); } }, @@ -2150,7 +2173,7 @@ module.exports = { let foundEntity = self.getEntityFromState(session, entity_id); if (foundEntity == null) { - self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); let entity = { id: entity_id, @@ -2173,7 +2196,7 @@ module.exports = { let entity_type = data.message[KomodoMessages.sync.indices.entityType]; if (entity_type == SYNC_OBJECTS) { - self.applyObjectsSyncToState(session, data); + this.applyObjectsSyncToState(session, data); } }, @@ -2255,7 +2278,7 @@ module.exports = { // NOTE(rob): the following code is copypasta from the old interactionUpdate handler. 7/21/2021 // check if the incoming packet is from a client who is valid for this session - if (!self.isClientInSession(session_id, client_id)) { + if (!this.isClientInSession(session_id, client_id)) { this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but client was not in session."); return; @@ -2268,9 +2291,9 @@ module.exports = { } // relay the message - self.messageAction(socket, session_id, data); + this.messageAction(socket, session_id, data); - data.message = self.parseMessageIfNeeded(data, session_id, client_id); + data.message = this.parseMessageIfNeeded(data, session_id, client_id); // get reference to session and parse message payload for state updates, if needed. if (type == KomodoMessages.interaction.type) { @@ -2298,16 +2321,16 @@ module.exports = { this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: interaction_type was null"); } - self.applyInteractionMessageToState(session, target_id, interaction_type); + this.applyInteractionMessageToState(session, target_id, interaction_type); } if (type == KomodoMessages.sync.type) { - self.applySyncMessageToState(e, id, latest, render, locked); + this.applySyncMessageToState(e, id, latest, render, locked); } // data capture if (session.isRecording) { - self.record_message_data(data); + this.record_message_data(data); } }, init: function (io, pool, logger) { @@ -2405,7 +2428,7 @@ module.exports = { }; this.failedToJoinAction = function (session_id, reason) { - socket.emit(KomodoSendEvents.joinFailed, session_id, reason); + socket.emit(KomodoSendEvents.failedToJoin, session_id, reason); }; this.successfullyJoinedAction = function (session_id, client_id) { @@ -2418,7 +2441,7 @@ module.exports = { this.leaveSessionAction = function (session_id, client_id) { socket.leave(session_id.toString(), (err) => { - this.makeSocketAndClientLeaveSession(err, session_id, client_id, socket); + self.makeSocketAndClientLeaveSession(err, session_id, client_id, socket); }); }; @@ -2429,7 +2452,7 @@ module.exports = { this.successfullyLeftAction = function (session_id, client_id) { // notify others the client has left - io.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); + io.to(session_id.toString()).emit(KomodoSendEvents.successfullyLeft, client_id); }; this.disconnectAction = function (socket, session_id, client_id) { @@ -2460,6 +2483,7 @@ module.exports = { )}` ); + //TODO -- do we need to rejoin it manually? socket.join(session_id.toString(), (err) => { self.processReconnectionAttempt(err, socket, session_id, client_id); }); @@ -2659,6 +2683,13 @@ module.exports = { // log reconnect event with timestamp to db self.writeEventToConnections("disconnect", session_id, client_id); }); + + socket.on(SocketIOEvents.disconnecting, function (reason) { + }); + + socket.on(SocketIOEvents.error, function (err) { + self.logErrorSessionClientSocketAction(null, null, socket.id || "null", err); + }); }); }, }; diff --git a/test/test-sync.js b/test/test-sync.js index 82a441d..0ed68dc 100644 --- a/test/test-sync.js +++ b/test/test-sync.js @@ -69,6 +69,10 @@ describe("Sync Server: Sessions", function (done) { count.should.equal(1); + let sessionType = typeof singularEntry; + + sessionType.should.not.equal("undefined"); + singularEntry[0].should.equal(session_id); const expectedSession = { @@ -122,6 +126,10 @@ describe("Sync Server: Sessions", function (done) { success.should.equal(false); + let sessionType = typeof session; + + sessionType.should.not.equal("undefined"); + assert.strictEqual(session, null); }); @@ -139,6 +147,10 @@ describe("Sync Server: Sessions", function (done) { success.should.equal(true); + let sessionType = typeof session; + + sessionType.should.not.equal("undefined"); + assert(session !== null); session.should.eql(inputSession); @@ -357,6 +369,43 @@ describe("Sync Server: Clients and Sockets", function (done) { session.clients.length.should.equal(1); }); + it("should be able to remove a socket", function () { + let inputSession = { + clients: [CLIENT_ID, CLIENT_ID], + sockets: { } + }; + + inputSession.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + inputSession.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + syncServer.createSession(SESSION_ID); + + syncServer.sessions.set(SESSION_ID, inputSession); + + Object.keys(inputSession.sockets).length.should.equal(2); + + let removeSuccess = syncServer.removeSocketFromSession(DUMMY_SOCKET_A, SESSION_ID); + + removeSuccess.should.equal(true); + + let { success, session } = syncServer.getSession(SESSION_ID); + + Object.keys(session.sockets).length.should.equal(1); + }); + + it("should return true if it found a socket", function () { + throw Error("unimplemented"); + }); + + it("should return false if it couldn't find a socket", function () { + throw Error("unimplemented"); + }); + + it("should be able to remove a socket and client from a session then disconnect the socket", function () { + throw Error("unimplemented"); + }); + it("should return all session sockets for a given client ID", function () { syncServer.sessions = new Map (); @@ -436,7 +485,7 @@ describe("Sync Server: Integration", function (done) { }); it("should create a correct session object when a client joins", function () { - let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); success.should.equal(true); // we passed in err = null, so it should succeed. @@ -487,7 +536,7 @@ describe("Sync Server: Integration", function (done) { }); it("should create a correct clients array", function () { - let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); sessions = syncServer.getSessions(); @@ -505,7 +554,7 @@ describe("Sync Server: Integration", function (done) { }); it("should create a correct sockets object", function () { - let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); sessions = syncServer.getSessions(); @@ -539,9 +588,9 @@ describe("Sync Server: Integration", function (done) { client_id.should.equal(CLIENT_ID); }; - let success = syncServer.handleJoin(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); - success = syncServer.handleJoin(null, DUMMY_SOCKET_B, SESSION_ID, CLIENT_ID, true); + success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_B, SESSION_ID, CLIENT_ID, true); success.should.equal(true); // we passed in err = null, so it should succeed. From 4cf5d31c5a90b13720d82da887c0a3f8be1cb789 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Sat, 16 Oct 2021 18:31:23 -0500 Subject: [PATCH 08/24] split unit tests into multiple functions --- test/test-sync-clients-and-sockets.js | 326 ++++++++++++++ test/test-sync-integration.js | 183 ++++++++ test/test-sync-sessions.js | 158 +++++++ test/test-sync.js | 623 -------------------------- 4 files changed, 667 insertions(+), 623 deletions(-) create mode 100644 test/test-sync-clients-and-sockets.js create mode 100644 test/test-sync-integration.js create mode 100644 test/test-sync-sessions.js delete mode 100644 test/test-sync.js diff --git a/test/test-sync-clients-and-sockets.js b/test/test-sync-clients-and-sockets.js new file mode 100644 index 0000000..e5b71e1 --- /dev/null +++ b/test/test-sync-clients-and-sockets.js @@ -0,0 +1,326 @@ +/* jshint esversion: 6 */ + +// TODO: add test for getting state +// TODO: add test for connecting without valid credentials + +var assert = require("assert"); + +var should = require("should"); + +const { debug } = require("winston"); + +const syncServer = require("../sync"); + +const SESSION_ID = 123; + +const CLIENT_ID = 456; + +const DUMMY_SOCKET_A = { "dummy": "socketA", "id": "DEADBEEF" }; + +const DUMMY_SOCKET_B = { "dummy": "socketB", "id": "LIVEBEEF" }; + +const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; + +describe("Sync Server: Clients and Sockets", function (done) { + beforeEach(function () { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + throw Error("An unexpected bump occurred."); + }; + + syncServer.reconnectAction = function () { + throw Error("An unexpected reconnect occurred."); + }; + + syncServer.disconnectedAction = function () { + throw Error("An unexpected disconnect occurred."); + }; + + syncServer.requestToJoinSessionAction = function (session_id, client_id) { + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + }; + + syncServer.initGlobals(); + }); + + //TODO implement this if we ever keep a global list of clients + + /* + it("should have 0 clients on startup", function () { + }); + */ + + it("should append a valid client to an empty session", function () { + let sessions = new Map (); + + let session = { + clients: [ ] + }; + + sessions.set(SESSION_ID, session); + + syncServer.sessions = sessions; + + syncServer.addClientToSession(session, CLIENT_ID); + + let expectedClients = [ CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should create a session when appending a valid client to a null session, appropriately", function () { + let sessions = new Map (); + + syncServer.sessions = sessions; + + syncServer.addClientToSession(null, CLIENT_ID, true); + + let expectedClients = [ CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should return an error when appending a valid client to a null session, appropriately", function () { + let sessions = new Map (); + + syncServer.sessions = sessions; + + let success = syncServer.addClientToSession(null, CLIENT_ID, false); + + success.should.eql(false); + }); + + it("should append a duplicate client to a session", function () { + let sessions = new Map (); + + let session = { + clients: [ CLIENT_ID ] + }; + + sessions.set(SESSION_ID, session); + + syncServer.sessions = sessions; + + syncServer.addClientToSession(session, CLIENT_ID); + + let expectedClients = [ CLIENT_ID, CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should be able to bump one existing socket", function () { + let session = { + clients: [ CLIENT_ID, CLIENT_ID ], + sockets: { } + }; + + session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + session.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + syncServer.sessions.set(SESSION_ID, session); + + let outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_A.id, DUMMY_SOCKET_B.id ] ); + + let bumpCount = 0; + + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.equal(DUMMY_SOCKET_A); + + bumpCount += 1; + }; + + let disconnectCount = 0; + + syncServer.disconnectedAction = function (socket, session_id, client_id) { + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + + socket.should.equal(DUMMY_SOCKET_A); + + disconnectCount += 1; + }; + + syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_B.id); + + bumpCount.should.eql(1); + + disconnectCount.should.eql(1); + + outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_B.id ] ); + }); + + it("should be able to bump two existing sockets", function () { + let session = { + clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], + sockets: { } + }; + + session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + session.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + session.sockets[DUMMY_SOCKET_C.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }; + + syncServer.sessions.set(SESSION_ID, session); + + let outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_A.id, DUMMY_SOCKET_B.id, DUMMY_SOCKET_C.id ] ); + + let bumpCount = 0; + + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); + + bumpCount += 1; + }; + + let disconnectCount = 0; + + syncServer.disconnectedAction = function (socket, session_id, client_id) { + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + + socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); + + disconnectCount += 1; + }; + + syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_C.id); + + bumpCount.should.eql(2); + + disconnectCount.should.eql(2); + + outputSession = syncServer.sessions.get(SESSION_ID); + + Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_C.id ] ); + }); + + it("should reduce two duplicate clients to one client", function () { + syncServer.createSession(SESSION_ID); + + let { success, session } = syncServer.getSession(SESSION_ID); + + session.should.not.eql(null); + + session.clients.should.not.eql(null); + + session.clients.length.should.equal(0); + + syncServer.addClientToSession(session, CLIENT_ID); + + session.clients.length.should.equal(1); + + syncServer.addClientToSession(session, CLIENT_ID); + + session.clients.length.should.equal(2); + + syncServer.removeDuplicateClientsFromSession(session, CLIENT_ID); + + session.clients.length.should.equal(1); + }); + + it("should be able to remove a socket", function () { + let inputSession = { + clients: [CLIENT_ID, CLIENT_ID], + sockets: { } + }; + + inputSession.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + inputSession.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + syncServer.createSession(SESSION_ID); + + syncServer.sessions.set(SESSION_ID, inputSession); + + Object.keys(inputSession.sockets).length.should.equal(2); + + let removeSuccess = syncServer.removeSocketFromSession(DUMMY_SOCKET_A, SESSION_ID); + + removeSuccess.should.equal(true); + + let { success, session } = syncServer.getSession(SESSION_ID); + + Object.keys(session.sockets).length.should.equal(1); + }); + + it("should return true if it found a socket", function () { + throw Error("unimplemented"); + }); + + it("should return false if it couldn't find a socket", function () { + throw Error("unimplemented"); + }); + + it("should be able to remove a socket and client from a session then disconnect the socket", function () { + throw Error("unimplemented"); + }); + + it("should return all session sockets for a given client ID", function () { + syncServer.sessions = new Map (); + + let session = { + clients: [CLIENT_ID], + sockets: { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } + } + }; + + syncServer.sessions.set(SESSION_ID, session); + + let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); + + sockets.should.eql([ DUMMY_SOCKET_A ]); + + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); + }; + + syncServer.sessions.set(SESSION_ID, { + clients: [CLIENT_ID, CLIENT_ID], + sockets: { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, + socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B } + } + }); + + session = syncServer.sessions.get(SESSION_ID); + + sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); + + sockets.should.eql([ DUMMY_SOCKET_A, DUMMY_SOCKET_B ]); + }); + + it("should exclude a socket when requesting session sockets", function () { + let session = { + clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], + sockets: { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, + socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }, + socketC: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }, + } + }; + + syncServer.sessions.set(SESSION_ID, session); + + let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, DUMMY_SOCKET_C.id); + + sockets.should.eql( [ DUMMY_SOCKET_A, DUMMY_SOCKET_B ] ); + }); +}); diff --git a/test/test-sync-integration.js b/test/test-sync-integration.js new file mode 100644 index 0000000..3b74ad8 --- /dev/null +++ b/test/test-sync-integration.js @@ -0,0 +1,183 @@ +/* jshint esversion: 6 */ + +// TODO: add test for getting state +// TODO: add test for connecting without valid credentials + +var assert = require("assert"); + +var should = require("should"); + +const { debug } = require("winston"); + +const syncServer = require("../sync"); + +const SESSION_ID = 123; + +const CLIENT_ID = 456; + +const DUMMY_SOCKET_A = { "dummy": "socketA", "id": "DEADBEEF" }; + +const DUMMY_SOCKET_B = { "dummy": "socketB", "id": "LIVEBEEF" }; + +const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; + +describe("Sync Server: Integration", function (done) { + beforeEach(function () { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + throw Error("An unexpected bump occurred."); + }; + + syncServer.reconnectAction = function () { + throw Error("An unexpected reconnect occurred."); + }; + + syncServer.disconnectedAction = function () { + throw Error("An unexpected disconnect occurred."); + }; + + syncServer.requestToJoinSessionAction = function (session_id, client_id) { + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + }; + + syncServer.initGlobals(); + }); + + it("should create a correct session object when a client joins", function () { + let success = syncServer.addSocketAndClientToSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + + success.should.equal(true); // we passed in err = null, so it should succeed. + + sessions = syncServer.getSessions(); + + sessions.size.should.equal(1); + + let singularEntry; + + // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? + + for (let entry of sessions) { + singularEntry = entry; + } + + singularEntry[0].should.equal(SESSION_ID); + + //TODO - factor this out into a separate test? - it("should create a correct clients array" + + const expectedClients = [ CLIENT_ID ]; + + assert(singularEntry[1].clients != null); + + singularEntry[1].clients.length.should.equal(expectedClients.length); + + singularEntry[1].clients[0].should.equal(expectedClients[0]); + + //TODO - factor this out into a separate test? - it("should create a correct sockets object" + + const expectedSockets = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + let numSockets = Object.keys(singularEntry[1].sockets).length; + + numSockets.should.equal(1); + + singularEntry[1].sockets[DUMMY_SOCKET_A.id].should.eql(expectedSockets); + + // + + //this.addClientToSession(session, client_id); + + //this.bumpDuplicateSockets(session, client_id, do_bump_duplicates, socket.id); + + // socket to client mapping + //this.addSocketToSession(session, socket, client_id); + + //this.requestToJoinSessionAction(session_id, client_id); + }); + + it("should create a correct clients array", function () { + let success = syncServer.addSocketAndClientToSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + + sessions = syncServer.getSessions(); + + let singularEntry; + + for (let entry of sessions) { + singularEntry = entry; + } + + const expectedClients = [ CLIENT_ID ]; + + singularEntry[1].clients.length.should.equal(expectedClients.length); + + singularEntry[1].clients[0].should.equal(expectedClients[0]); + }); + + it("should create a correct sockets object", function () { + let success = syncServer.addSocketAndClientToSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + + sessions = syncServer.getSessions(); + + let singularEntry; + + for (let entry of sessions) { + singularEntry = entry; + } + + const socketA = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; + + let numSockets = Object.keys(singularEntry[1].sockets).length; + + numSockets.should.equal(1); + + singularEntry[1].sockets[DUMMY_SOCKET_A.id].should.eql(socketA); + }); + + it("should perform a bump properly", function () { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); + }; + + syncServer.disconnectedAction = function (socket, session_id, client_id) { + socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); + + session_id.should.equal(SESSION_ID); + + client_id.should.equal(CLIENT_ID); + }; + + let success = syncServer.addSocketAndClientToSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); + + success = syncServer.addSocketAndClientToSession(null, DUMMY_SOCKET_B, SESSION_ID, CLIENT_ID, true); + + success.should.equal(true); // we passed in err = null, so it should succeed. + + sessions = syncServer.getSessions(); + + // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? + + for (let entry of sessions) { + singularEntry = entry; + } + + const expectedClients = [ CLIENT_ID ]; + + singularEntry[1].clients.length.should.equal(expectedClients.length); + + singularEntry[1].clients[0].should.equal(expectedClients[0]); + + numSockets = Object.keys(singularEntry[1].sockets).length; + + numSockets.should.equal(1); + + const socketB = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + assert(singularEntry[1].sockets[DUMMY_SOCKET_B.id] != null); + + singularEntry[1].sockets[DUMMY_SOCKET_B.id].should.eql(socketB); + + assert(singularEntry[1].sockets[DUMMY_SOCKET_A.id] == null); + }); +}); \ No newline at end of file diff --git a/test/test-sync-sessions.js b/test/test-sync-sessions.js new file mode 100644 index 0000000..d179b35 --- /dev/null +++ b/test/test-sync-sessions.js @@ -0,0 +1,158 @@ +/* jshint esversion: 6 */ + +// TODO: add test for getting state +// TODO: add test for connecting without valid credentials + +var assert = require("assert"); + +var should = require("should"); + +const { debug } = require("winston"); + +const syncServer = require("../sync"); + +const SESSION_ID = 123; + +const CLIENT_ID = 456; + +const DUMMY_SOCKET_A = { "dummy": "socketA", "id": "DEADBEEF" }; + +const DUMMY_SOCKET_B = { "dummy": "socketB", "id": "LIVEBEEF" }; + +const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; + +describe("Sync Server: Sessions", function (done) { + beforeEach(function () { + syncServer.initGlobals(); + + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + throw Error("An unexpected bump occurred."); + }; + + syncServer.reconnectAction = function () { + throw Error("An unexpected reconnect occurred."); + }; + + syncServer.disconnectedAction = function () { + throw Error("An unexpected disconnect occurred."); + }; + }); + + it("should have 0 sessions on startup", function () { + let sessions = syncServer.getSessions(); + + sessions.size.should.equal(0); + }); + + it("should create one singular, correct sessions object", function () { + const session_id = 123; + + let sessions = syncServer.getSessions(); + + sessions.size.should.equal(0); + + syncServer.createSession(session_id); + + sessions = syncServer.getSessions(); + + let count = 0; + + let singularEntry; + + // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? + + for (let entry of sessions) { + count += 1; + + singularEntry = entry; + } + + count.should.equal(1); + + let sessionType = typeof singularEntry; + + sessionType.should.not.equal("undefined"); + + singularEntry[0].should.equal(session_id); + + const expectedSession = { + sockets: {}, // socket.id -> client_id + clients: [], + entities: [], + scene: null, + isRecording: false, + start: Date.now(), + recordingStart: 0, + seq: 0, + // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 + // writers: { + // pos: { + // buffer: Buffer.alloc(syncServer.positionWriteBufferSize()), + // cursor: 0 + // }, + // int: { + // buffer: Buffer.alloc(syncServer.interactionWriteBufferSize()), + // cursor: 0 + // } + // }, + message_buffer: [] + }; + + assert.deepStrictEqual(singularEntry[1].sockets, expectedSession.sockets); + + assert.deepStrictEqual(singularEntry[1].clients, expectedSession.clients); + + assert.deepStrictEqual(singularEntry[1].entities, expectedSession.entities); + + assert.deepStrictEqual(singularEntry[1].scene, expectedSession.scene); + + assert.deepStrictEqual(singularEntry[1].isRecording, expectedSession.isRecording); + + // Do not check start time for strict equality. + assert(Math.abs(singularEntry[1].start - expectedSession.start) < 1000); + + assert.deepStrictEqual(singularEntry[1].recordingStart, expectedSession.recordingStart); + + assert.deepStrictEqual(singularEntry[1].seq, expectedSession.seq); + + // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 + // assert.deepStrictEqual(singularEntry[1].writers, expectedSession.writers); + + assert.deepStrictEqual(singularEntry[1].message_buffer, expectedSession.message_buffer); + }); + + it("should return failure on getting a nonexistent session", function () { + let { success, session } = syncServer.getSession(SESSION_ID); + + success.should.equal(false); + + let sessionType = typeof session; + + sessionType.should.not.equal("undefined"); + + assert.strictEqual(session, null); + }); + + it("should return success for getting an existing session", function () { + let inputSession = { + clients: [ CLIENT_ID ], + sockets: { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } + } + }; + + syncServer.sessions.set(SESSION_ID, inputSession); + + let { success, session } = syncServer.getSession(SESSION_ID); + + success.should.equal(true); + + let sessionType = typeof session; + + sessionType.should.not.equal("undefined"); + + assert(session !== null); + + session.should.eql(inputSession); + }); +}); \ No newline at end of file diff --git a/test/test-sync.js b/test/test-sync.js deleted file mode 100644 index 0ed68dc..0000000 --- a/test/test-sync.js +++ /dev/null @@ -1,623 +0,0 @@ -/* jshint esversion: 6 */ - -// TODO: add test for getting state -// TODO: add test for connecting without valid credentials - -var assert = require("assert"); - -var should = require("should"); - -const { debug } = require("winston"); - -const syncServer = require("../sync"); - -const SESSION_ID = 123; - -const CLIENT_ID = 456; - -const DUMMY_SOCKET_A = { "dummy": "socketA", "id": "DEADBEEF" }; - -const DUMMY_SOCKET_B = { "dummy": "socketB", "id": "LIVEBEEF" }; - -const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; - -describe("Sync Server: Sessions", function (done) { - beforeEach(function () { - syncServer.initGlobals(); - - syncServer.bumpAction = function () { - throw Error("An unexpected bump occurred."); - }; - - syncServer.reconnectAction = function () { - throw Error("An unexpected reconnect occurred."); - }; - - syncServer.disconnectAction = function () { - throw Error("An unexpected disconnect occurred."); - }; - }); - - it("should have 0 sessions on startup", function () { - let sessions = syncServer.getSessions(); - - sessions.size.should.equal(0); - }); - - it("should create one singular, correct sessions object", function () { - const session_id = 123; - - let sessions = syncServer.getSessions(); - - sessions.size.should.equal(0); - - syncServer.createSession(session_id); - - sessions = syncServer.getSessions(); - - let count = 0; - - let singularEntry; - - // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? - - for (let entry of sessions) { - count += 1; - - singularEntry = entry; - } - - count.should.equal(1); - - let sessionType = typeof singularEntry; - - sessionType.should.not.equal("undefined"); - - singularEntry[0].should.equal(session_id); - - const expectedSession = { - sockets: {}, // socket.id -> client_id - clients: [], - entities: [], - scene: null, - isRecording: false, - start: Date.now(), - recordingStart: 0, - seq: 0, - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // writers: { - // pos: { - // buffer: Buffer.alloc(syncServer.positionWriteBufferSize()), - // cursor: 0 - // }, - // int: { - // buffer: Buffer.alloc(syncServer.interactionWriteBufferSize()), - // cursor: 0 - // } - // }, - message_buffer: [] - }; - - assert.deepStrictEqual(singularEntry[1].sockets, expectedSession.sockets); - - assert.deepStrictEqual(singularEntry[1].clients, expectedSession.clients); - - assert.deepStrictEqual(singularEntry[1].entities, expectedSession.entities); - - assert.deepStrictEqual(singularEntry[1].scene, expectedSession.scene); - - assert.deepStrictEqual(singularEntry[1].isRecording, expectedSession.isRecording); - - // Do not check start time for strict equality. - assert(Math.abs(singularEntry[1].start - expectedSession.start) < 1000); - - assert.deepStrictEqual(singularEntry[1].recordingStart, expectedSession.recordingStart); - - assert.deepStrictEqual(singularEntry[1].seq, expectedSession.seq); - - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // assert.deepStrictEqual(singularEntry[1].writers, expectedSession.writers); - - assert.deepStrictEqual(singularEntry[1].message_buffer, expectedSession.message_buffer); - }); - - it("should return failure on getting a nonexistent session", function () { - let { success, session } = syncServer.getSession(SESSION_ID); - - success.should.equal(false); - - let sessionType = typeof session; - - sessionType.should.not.equal("undefined"); - - assert.strictEqual(session, null); - }); - - it("should return success for getting an existing session", function () { - let inputSession = { - clients: [ CLIENT_ID ], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } - } - }; - - syncServer.sessions.set(SESSION_ID, inputSession); - - let { success, session } = syncServer.getSession(SESSION_ID); - - success.should.equal(true); - - let sessionType = typeof session; - - sessionType.should.not.equal("undefined"); - - assert(session !== null); - - session.should.eql(inputSession); - }); -}); - -describe("Sync Server: Clients and Sockets", function (done) { - beforeEach(function () { - syncServer.bumpAction = function () { - throw Error("An unexpected bump occurred."); - }; - - syncServer.reconnectAction = function () { - throw Error("An unexpected reconnect occurred."); - }; - - syncServer.disconnectAction = function () { - throw Error("An unexpected disconnect occurred."); - }; - - syncServer.joinSessionAction = function (session_id, client_id) { - session_id.should.equal(SESSION_ID); - - client_id.should.equal(CLIENT_ID); - }; - - syncServer.initGlobals(); - }); - - //TODO implement this if we ever keep a global list of clients - - /* - it("should have 0 clients on startup", function () { - }); - */ - - it("should append a valid client to an empty session", function () { - let sessions = new Map (); - - let session = { - clients: [ ] - }; - - sessions.set(SESSION_ID, session); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(session, CLIENT_ID); - - let expectedClients = [ CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - - it("should create a session when appending a valid client to a null session, appropriately", function () { - let sessions = new Map (); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(null, CLIENT_ID, true); - - let expectedClients = [ CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - - it("should return an error when appending a valid client to a null session, appropriately", function () { - let sessions = new Map (); - - syncServer.sessions = sessions; - - let success = syncServer.addClientToSession(null, CLIENT_ID, false); - - success.should.eql(false); - }); - - it("should append a duplicate client to a session", function () { - let sessions = new Map (); - - let session = { - clients: [ CLIENT_ID ] - }; - - sessions.set(SESSION_ID, session); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(session, CLIENT_ID); - - let expectedClients = [ CLIENT_ID, CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - - it("should be able to bump one existing socket", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID ], - sockets: { } - }; - - session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - session.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; - - syncServer.sessions.set(SESSION_ID, session); - - let outputSession = syncServer.sessions.get(SESSION_ID); - - Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_A.id, DUMMY_SOCKET_B.id ] ); - - let bumpCount = 0; - - syncServer.bumpAction = function (session_id, socket) { - session_id.should.equal(SESSION_ID); - - socket.should.equal(DUMMY_SOCKET_A); - - bumpCount += 1; - }; - - let disconnectCount = 0; - - syncServer.disconnectAction = function (socket, session_id, client_id) { - session_id.should.equal(SESSION_ID); - - client_id.should.equal(CLIENT_ID); - - socket.should.equal(DUMMY_SOCKET_A); - - disconnectCount += 1; - }; - - syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_B.id); - - bumpCount.should.eql(1); - - disconnectCount.should.eql(1); - - outputSession = syncServer.sessions.get(SESSION_ID); - - Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_B.id ] ); - }); - - it("should be able to bump two existing sockets", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], - sockets: { } - }; - - session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - session.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; - - session.sockets[DUMMY_SOCKET_C.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }; - - syncServer.sessions.set(SESSION_ID, session); - - let outputSession = syncServer.sessions.get(SESSION_ID); - - Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_A.id, DUMMY_SOCKET_B.id, DUMMY_SOCKET_C.id ] ); - - let bumpCount = 0; - - syncServer.bumpAction = function (session_id, socket) { - session_id.should.equal(SESSION_ID); - - socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); - - bumpCount += 1; - }; - - let disconnectCount = 0; - - syncServer.disconnectAction = function (socket, session_id, client_id) { - session_id.should.equal(SESSION_ID); - - client_id.should.equal(CLIENT_ID); - - socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); - - disconnectCount += 1; - }; - - syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_C.id); - - bumpCount.should.eql(2); - - disconnectCount.should.eql(2); - - outputSession = syncServer.sessions.get(SESSION_ID); - - Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_C.id ] ); - }); - - it("should reduce two duplicate clients to one client", function () { - syncServer.createSession(SESSION_ID); - - let { success, session } = syncServer.getSession(SESSION_ID); - - session.should.not.eql(null); - - session.clients.should.not.eql(null); - - session.clients.length.should.equal(0); - - syncServer.addClientToSession(session, CLIENT_ID); - - session.clients.length.should.equal(1); - - syncServer.addClientToSession(session, CLIENT_ID); - - session.clients.length.should.equal(2); - - syncServer.removeDuplicateClientsFromSession(session, CLIENT_ID); - - session.clients.length.should.equal(1); - }); - - it("should be able to remove a socket", function () { - let inputSession = { - clients: [CLIENT_ID, CLIENT_ID], - sockets: { } - }; - - inputSession.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - inputSession.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; - - syncServer.createSession(SESSION_ID); - - syncServer.sessions.set(SESSION_ID, inputSession); - - Object.keys(inputSession.sockets).length.should.equal(2); - - let removeSuccess = syncServer.removeSocketFromSession(DUMMY_SOCKET_A, SESSION_ID); - - removeSuccess.should.equal(true); - - let { success, session } = syncServer.getSession(SESSION_ID); - - Object.keys(session.sockets).length.should.equal(1); - }); - - it("should return true if it found a socket", function () { - throw Error("unimplemented"); - }); - - it("should return false if it couldn't find a socket", function () { - throw Error("unimplemented"); - }); - - it("should be able to remove a socket and client from a session then disconnect the socket", function () { - throw Error("unimplemented"); - }); - - it("should return all session sockets for a given client ID", function () { - syncServer.sessions = new Map (); - - let session = { - clients: [CLIENT_ID], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } - } - }; - - syncServer.sessions.set(SESSION_ID, session); - - let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); - - sockets.should.eql([ DUMMY_SOCKET_A ]); - - syncServer.bumpAction = function (session_id, socket) { - session_id.should.equal(SESSION_ID); - - socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); - }; - - syncServer.sessions.set(SESSION_ID, { - clients: [CLIENT_ID, CLIENT_ID], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, - socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B } - } - }); - - session = syncServer.sessions.get(SESSION_ID); - - sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); - - sockets.should.eql([ DUMMY_SOCKET_A, DUMMY_SOCKET_B ]); - }); - - it("should exclude a socket when requesting session sockets", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, - socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }, - socketC: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }, - } - }; - - syncServer.sessions.set(SESSION_ID, session); - - let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, DUMMY_SOCKET_C.id); - - sockets.should.eql( [ DUMMY_SOCKET_A, DUMMY_SOCKET_B ] ); - }); -}); - -describe("Sync Server: Integration", function (done) { - beforeEach(function () { - syncServer.bumpAction = function () { - throw Error("An unexpected bump occurred."); - }; - - syncServer.reconnectAction = function () { - throw Error("An unexpected reconnect occurred."); - }; - - syncServer.disconnectAction = function () { - throw Error("An unexpected disconnect occurred."); - }; - - syncServer.joinSessionAction = function (session_id, client_id) { - session_id.should.equal(SESSION_ID); - - client_id.should.equal(CLIENT_ID); - }; - - syncServer.initGlobals(); - }); - - it("should create a correct session object when a client joins", function () { - let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); - - success.should.equal(true); // we passed in err = null, so it should succeed. - - sessions = syncServer.getSessions(); - - sessions.size.should.equal(1); - - let singularEntry; - - // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? - - for (let entry of sessions) { - singularEntry = entry; - } - - singularEntry[0].should.equal(SESSION_ID); - - //TODO - factor this out into a separate test? - it("should create a correct clients array" - - const expectedClients = [ CLIENT_ID ]; - - assert(singularEntry[1].clients != null); - - singularEntry[1].clients.length.should.equal(expectedClients.length); - - singularEntry[1].clients[0].should.equal(expectedClients[0]); - - //TODO - factor this out into a separate test? - it("should create a correct sockets object" - - const expectedSockets = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - let numSockets = Object.keys(singularEntry[1].sockets).length; - - numSockets.should.equal(1); - - singularEntry[1].sockets[DUMMY_SOCKET_A.id].should.eql(expectedSockets); - - // - - //this.addClientToSession(session, client_id); - - //this.bumpDuplicateSockets(session, client_id, do_bump_duplicates, socket.id); - - // socket to client mapping - //this.addSocketToSession(session, socket, client_id); - - //this.joinSessionAction(session_id, client_id); - }); - - it("should create a correct clients array", function () { - let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); - - sessions = syncServer.getSessions(); - - let singularEntry; - - for (let entry of sessions) { - singularEntry = entry; - } - - const expectedClients = [ CLIENT_ID ]; - - singularEntry[1].clients.length.should.equal(expectedClients.length); - - singularEntry[1].clients[0].should.equal(expectedClients[0]); - }); - - it("should create a correct sockets object", function () { - let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); - - sessions = syncServer.getSessions(); - - let singularEntry; - - for (let entry of sessions) { - singularEntry = entry; - } - - const socketA = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - let numSockets = Object.keys(singularEntry[1].sockets).length; - - numSockets.should.equal(1); - - singularEntry[1].sockets[DUMMY_SOCKET_A.id].should.eql(socketA); - }); - - it("should perform a bump properly", function () { - syncServer.bumpAction = function (session_id, socket) { - session_id.should.equal(SESSION_ID); - - socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); - }; - - syncServer.disconnectAction = function (socket, session_id, client_id) { - socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); - - session_id.should.equal(SESSION_ID); - - client_id.should.equal(CLIENT_ID); - }; - - let success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_A, SESSION_ID, CLIENT_ID, true); - - success = syncServer.makeSocketAndClientJoinSession(null, DUMMY_SOCKET_B, SESSION_ID, CLIENT_ID, true); - - success.should.equal(true); // we passed in err = null, so it should succeed. - - sessions = syncServer.getSessions(); - - // TODO(Brandon) - are we supposed to dip into the syncServer.sessions variable directly like this? - - for (let entry of sessions) { - singularEntry = entry; - } - - const expectedClients = [ CLIENT_ID ]; - - singularEntry[1].clients.length.should.equal(expectedClients.length); - - singularEntry[1].clients[0].should.equal(expectedClients[0]); - - numSockets = Object.keys(singularEntry[1].sockets).length; - - numSockets.should.equal(1); - - const socketB = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; - - assert(singularEntry[1].sockets[DUMMY_SOCKET_B.id] != null); - - singularEntry[1].sockets[DUMMY_SOCKET_B.id].should.eql(socketB); - - assert(singularEntry[1].sockets[DUMMY_SOCKET_A.id] == null); - }); -}); \ No newline at end of file From 74093c4ab27e608d010e0848d225025521b3573e Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Sat, 16 Oct 2021 19:02:52 -0500 Subject: [PATCH 09/24] WIP: refactored joining, leaving, bumping; added bump send event; renamed things --- sync.js | 237 ++++++++++++++++---------- test/test-sync-clients-and-sockets.js | 8 +- test/test-sync-integration.js | 4 +- test/test-sync-sessions.js | 2 +- 4 files changed, 156 insertions(+), 95 deletions(-) diff --git a/sync.js b/sync.js index 0d18189..4e0e9e6 100644 --- a/sync.js +++ b/sync.js @@ -86,10 +86,10 @@ const SocketIOEvents = { }; const KomodoReceiveEvents = { - join: "join", + requestToJoinSession: "join", leave: "leave", sessionInfo: "sessionInfo", - state: "state", + requestOwnStateCatchup: "state", draw: "draw", message: "message", update: "update", @@ -102,7 +102,7 @@ const KomodoReceiveEvents = { const KomodoSendEvents = { connectionError: "connectionError", interactionUpdate: "interactionUpdate", - joined: "joined", + clientJoined: "joined", failedToJoin: "failedToJoin", successfullyJoined: "successfullyJoined", left: "left", @@ -114,6 +114,7 @@ const KomodoSendEvents = { draw: "draw", message: "message", relayUpdate: "relayUpdate", + notifyBump: "bump", }; const KomodoMessages = { @@ -1118,7 +1119,7 @@ module.exports = { }, // returns true iff socket was successfully joined to session - makeSocketAndClientJoinSession: function ( + addSocketAndClientToSession: function ( err, socket, session_id, @@ -1132,7 +1133,7 @@ module.exports = { session_id, client_id, null, - `in makeSocketAndClientJoinSession, failedToJoinAction callback was not provided. Proceeding anyways.` + `in addSocketAndClientToSession, failedToJoinAction callback was not provided. Proceeding anyways.` ); } @@ -1173,7 +1174,7 @@ module.exports = { session_id, client_id, socket.id, - "session was null when adding socket to session. Creating a session for you." + "session was null when making socket and client join session. Creating a session for you." ); session = this.createSession(session_id); @@ -1182,7 +1183,7 @@ module.exports = { success = this.addClientToSession(session, client_id); if (!success) { - reason = `tried to handle join, but adding client to session failed.`; + reason = `tried to make socket and client join session, but adding client to session failed.`; this.logErrorSessionClientSocketAction( session_id, @@ -1215,7 +1216,7 @@ module.exports = { session_id, client_id, socket.id, - `in makeSocketAndClientJoinSession, successfullyJoinedAction callback was not provided. Skipping.` + `in addSocketAndClientToSession, successfullyJoinedAction callback was not provided. Skipping.` ); return true; @@ -1226,7 +1227,22 @@ module.exports = { return true; }, - makeSocketAndClientLeaveSession: function (err, session_id, client_id, socket) { + makeSocketLeave: function (session_id, client_id) { + if (!this.requestToLeaveSessionAction) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + `in makeSocketLeave, requestToLeaveSessionAction callback was not provided. Skipping.` + ); + + return; + } + + this.requestToLeaveSessionAction(session_id, client_id); + }, + + tryToRemoveSocketAndClientFromSessionThenNotifyLeft: function (err, session_id, client_id, socket) { var success; var reason; @@ -1235,14 +1251,14 @@ module.exports = { session_id, client_id, socket.id, - `in makeSocketAndClientLeaveSession, failedToLeaveAction callback was not provided. Skipping.` + `in tryToRemoveSocketAndClientFromSessionThenNotifyLeft, failedToLeaveAction callback was not provided. Skipping.` ); return; } if (!socket) { - reason = `makeSocketAndClientLeaveSession: socket was null`; + reason = `tryToRemoveSocketAndClientFromSessionThenNotifyLeft: socket was null`; this.logErrorSessionClientSocketAction( session_id, @@ -1296,7 +1312,7 @@ module.exports = { session_id, client_id, socket.id, - `in makeSocketAndClientLeaveSession, successfullyLeftAction callback was not provided. Skipping.` + `in removeSocketAndClientFromSession, successfullyLeftAction callback was not provided. Skipping.` ); return; @@ -1306,12 +1322,38 @@ module.exports = { this.logInfoSessionClientSocketAction( session_id, - null, + client_id, socket.id, `Left.` ); }, + notifyBump: function (session_id, socket) { + if (this.notifyBumpAction == null) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket_id, + `notifyBumpAction callback was not provided` + ); + } + + this.notifyBumpAction(session_id, socket); + }, + + makeSocketLeaveSession: function (session_id, socket) { + if (this.makeSocketLeaveSessionAction == null) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket_id, + `makeSocketLeaveSessionAction callback was not provided` + ); + } + + this.makeSocketLeaveSessionAction(session_id, socket); + }, + //TODO rewrite this so that do_bump_duplicates and socket_id become ids_to_keep bumpDuplicateSockets: function ( session, @@ -1332,15 +1374,6 @@ module.exports = { let session_id = this.getSessionIdFromSession(session); - if (this.bumpAction == null) { - this.logWarningSessionClientSocketAction( - session_id, - client_id, - socket_id, - `in bumpDuplicateSockets, bumpAction callback was not provided` - ); - } - let sockets; if (do_bump_duplicates) { @@ -1365,9 +1398,13 @@ module.exports = { } sockets.forEach((socket) => { - self.bumpAction(session_id, socket); + self.removeSocketAndClientFromSession(socket, session_id, client_id); - self.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); + self.notifyBump(session_id, socket); + + self.makeSocketLeaveSession(session_id, socket); + + self.disconnectSocket(socket, session_id, client_id); }); }, @@ -1384,26 +1421,27 @@ module.exports = { return; } - if (this.pool) { - this.pool.query( - "INSERT INTO connections(timestamp, session_id, client_id, event) VALUES(?, ?, ?, ?)", - [Date.now(), session_id, client_id, event], + if (this.pool == null) { + this.logger.error( + `Failed to log event to database: ${event}, ${session_id}, ${client_id}: this.pool was null`); - (err, res) => { - if (err != undefined) { - this.logErrorSessionClientSocketAction( - session_id, - client_id, - null, - `Error writing ${event} event to database: ${err} ${res}` - ); - } - } - ); + return; } - } else { - this.logger.error( - `Failed to log event to database: ${event}, ${session_id}, ${client_id}` + + this.pool.query( + "INSERT INTO connections(timestamp, session_id, client_id, event) VALUES(?, ?, ?, ?)", + [Date.now(), session_id, client_id, event], + + (err, res) => { + if (err != undefined) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `Error writing ${event} event to database: ${err} ${res}` + ); + } + } ); } }, @@ -1684,8 +1722,7 @@ module.exports = { return true; }, - // cleanup socket and client references in session state if reconnect fails - removeSocketAndClientFromSessionThenDisconnectSocket: function (socket, session_id, client_id) { + disconnectSocket: function (socket, session_id, client_id) { if (!socket) { this.logErrorSessionClientSocketAction( session_id, @@ -1697,16 +1734,30 @@ module.exports = { return; } - if (!this.disconnectAction) { + if (!this.disconnectedAction) { this.logWarningSessionClientSocketAction( - null, + session_id, client_id, socket.id, - `in removeSocketAndClientFromSessionThenDisconnectSocket, disconnectAction callback was not provided` + `in disconnectSocket, disconnectedAction callback was not provided` ); } - this.disconnectAction(socket, session_id, client_id); + this.disconnectedAction(socket, session_id, client_id); + }, + + // cleanup socket and client references in session state if reconnect fails + removeSocketAndClientFromSession: function (socket, session_id, client_id) { + if (!socket) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `tried removing socket from session, but socket was null` + ); + + return; + } let session = this.sessions.get(session_id); @@ -1859,7 +1910,7 @@ module.exports = { }, processReconnectionAttempt: function (err, socket, session_id, client_id) { - let success = this.makeSocketAndClientJoinSession(err, socket, session_id, client_id, true); + let success = this.addSocketAndClientToSession(err, socket, session_id, client_id, true); if (!success) { this.logInfoSessionClientSocketAction( @@ -1869,7 +1920,9 @@ module.exports = { "failed to reconnect" ); - this.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); + this.tryToRemoveSocketAndClientFromSessionAndNotifyLeft(socket, session_id, client_id); + + this.disconnectSocket(socket, session_id, client_id); this.cleanUpSessionIfEmpty(session_id); @@ -1916,6 +1969,15 @@ module.exports = { }; }, + doTryReconnecting: function (reason) { + if (doReconnectOnUnknownReason) { + return true; + } + + return (DisconnectKnownReasons.hasOwnProperty(reason) && + DisconnectKnownReasons[reason].doReconnect); + }, + // Returns true if socket is still connected handleDisconnect: function (socket, reason) { if (!socket) { @@ -1941,7 +2003,6 @@ module.exports = { } // Check disconnect event reason and handle - const { session_id, client_id } = this.whoDisconnected(socket); if (session_id == null || client_id == null) { @@ -1950,19 +2011,14 @@ module.exports = { null, null, socket.id, - `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction or removeSocketAndClientFromSessionThenDisconnectSocket.)` + `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction, removeSocketAndClientFromSession, and/or DisconnectSocket.)` ); return true; } - if ( - (DisconnectKnownReasons.hasOwnProperty(reason) && - DisconnectKnownReasons[reason].doReconnect) || - doReconnectOnUnknownReason - ) { + if (this.doTryReconnecting(reason)) { // Try to reconnect the socket - return this.reconnectAction( reason, socket, @@ -1982,7 +2038,9 @@ module.exports = { )}` ); - this.removeSocketAndClientFromSessionThenDisconnectSocket(socket, session_id, client_id); + this.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(socket, session_id, client_id); + + this.disconnectSocket(socket, session_id, client_id); this.cleanUpSessionIfEmpty(session_id); @@ -2375,27 +2433,26 @@ module.exports = { socket.to(session_id.toString()).emit(KomodoSendEvents.message, data); }; - this.bumpAction = function (session_id, socket) { + this.notifyBumpAction = function (session_id, socket) { self.logInfoSessionClientSocketAction( session_id, null, socket.id, - `leaving session` + `Notifying about bump` ); - socket.leave(session_id.toString(), (err) => { - if (err) { - self.logErrorSessionClientSocketAction( - session_id, - null, - socket.id, - err - ); + // Let the client know it has been bumped + socket.emit(KomodoSendEvents.notifyBump, session_id); + }; - return; - } + this.makeSocketLeaveSessionAction = function (session_id, socket) { + socket.leave(session_id.toString(), (err) => { + this.failedToLeaveAction(session_id, `Failed to leave during bump: ${err}.`); }); + }; + // TODO(Brandon) -- review if this function is needed. I detached it 10/16/21. + this.disconnectSocketAfterDelayAction = function (session_id, socket) { self.logInfoSessionClientSocketAction( session_id, null, @@ -2415,9 +2472,9 @@ module.exports = { }, 500); // delay half a second and then bump the old socket }; - this.joinSessionAction = function (session_id, client_id) { + this.requestToJoinSessionAction = function (session_id, client_id) { socket.join(session_id.toString(), (err) => { - self.makeSocketAndClientJoinSession( + self.addSocketAndClientToSession( err, socket, session_id, @@ -2428,7 +2485,7 @@ module.exports = { }; this.failedToJoinAction = function (session_id, reason) { - socket.emit(KomodoSendEvents.failedToJoin, session_id, reason); + socket.emit(KomodoSendEvents.failedToJoin, session_id, reason); }; this.successfullyJoinedAction = function (session_id, client_id) { @@ -2436,27 +2493,32 @@ module.exports = { self.writeEventToConnections("connect", session_id, client_id); // tell other clients that a client joined - io.to(session_id.toString()).emit(KomodoSendEvents.joined, client_id); + io.to(session_id.toString()).emit(KomodoSendEvents.clientJoined, client_id); + + // tell the joining client that they successfully joined + socket.emit(KomodoSendEvents.successfullyJoined, session_id); }; - this.leaveSessionAction = function (session_id, client_id) { + this.requestToLeaveSessionAction = function (session_id, client_id) { socket.leave(session_id.toString(), (err) => { - self.makeSocketAndClientLeaveSession(err, session_id, client_id, socket); + self.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(err, session_id, client_id, socket); }); }; this.failedToLeaveAction = function (session_id, reason) { - // notify others the client has left socket.emit(KomodoSendEvents.failedToLeave, session_id, reason); }; this.successfullyLeftAction = function (session_id, client_id) { // notify others the client has left - io.to(session_id.toString()).emit(KomodoSendEvents.successfullyLeft, client_id); + io.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); + + // tell the leaving client that they successfully left + socket.emit(KomodoSendEvents.successfullyLeft, session_id); }; - this.disconnectAction = function (socket, session_id, client_id) { - // notify and log event + this.disconnectedAction = function (socket, session_id, client_id) { + // notify others the client has disconnected socket .to(session_id.toString()) .emit(KomodoSendEvents.disconnected, client_id); @@ -2517,8 +2579,7 @@ module.exports = { .emit(KomodoSendEvents.sessionInfo, session); }); - //Note: "join" is our own event name, and should not be confused with socket.join. (It does not automatically listen for socket.join either.) - socket.on(KomodoReceiveEvents.join, function (data) { + socket.on(KomodoReceiveEvents.requestToJoinSession, function (data) { let session_id = data[0]; let client_id = data[1]; @@ -2541,22 +2602,22 @@ module.exports = { //TODO does this need to be called here???? self.bumpOldSockets(session_id, client_id, socket.id); - if (!this.joinSessionAction) { + if (!this.requestToJoinSessionAction) { self.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - `in socket.on(${KomodoReceiveEvents.join}), joinSessionAction callback was not provided` + `in socket.on(${KomodoReceiveEvents.requestToJoinSession}), requestToJoinSessionAction callback was not provided` ); return; } - this.joinSessionAction(session_id, client_id); + this.requestToJoinSessionAction(session_id, client_id); }); // When a client requests a state catch-up, send the current session state. Supports versioning. - socket.on(KomodoReceiveEvents.state, function (data) { + socket.on(KomodoReceiveEvents.requestOwnStateCatchup, function (data) { let { session_id, state } = self.handleState(socket, data); if (session_id == -1 || !state) { @@ -2572,7 +2633,7 @@ module.exports = { try { // emit versioned state data - io.to(socket.id).emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. + socket.emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. } catch (err) { this.logErrorSessionClientSocketAction( session_id, diff --git a/test/test-sync-clients-and-sockets.js b/test/test-sync-clients-and-sockets.js index e5b71e1..1145ad7 100644 --- a/test/test-sync-clients-and-sockets.js +++ b/test/test-sync-clients-and-sockets.js @@ -23,7 +23,7 @@ const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; describe("Sync Server: Clients and Sockets", function (done) { beforeEach(function () { - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + syncServer.notifyBumpAction = function () { throw Error("An unexpected bump occurred."); }; @@ -127,7 +127,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.equal(DUMMY_SOCKET_A); @@ -178,7 +178,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); @@ -286,7 +286,7 @@ describe("Sync Server: Clients and Sockets", function (done) { sockets.should.eql([ DUMMY_SOCKET_A ]); - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); diff --git a/test/test-sync-integration.js b/test/test-sync-integration.js index 3b74ad8..68d301f 100644 --- a/test/test-sync-integration.js +++ b/test/test-sync-integration.js @@ -23,7 +23,7 @@ const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; describe("Sync Server: Integration", function (done) { beforeEach(function () { - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + syncServer.notifyBumpAction = function () { throw Error("An unexpected bump occurred."); }; @@ -134,7 +134,7 @@ describe("Sync Server: Integration", function (done) { }); it("should perform a bump properly", function () { - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); diff --git a/test/test-sync-sessions.js b/test/test-sync-sessions.js index d179b35..3d87432 100644 --- a/test/test-sync-sessions.js +++ b/test/test-sync-sessions.js @@ -25,7 +25,7 @@ describe("Sync Server: Sessions", function (done) { beforeEach(function () { syncServer.initGlobals(); - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + syncServer.notifyBumpAction = function () { throw Error("An unexpected bump occurred."); }; From c0d9db00e6c3807ea2f79e1e4f3f61ca10d8f57d Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Sun, 17 Oct 2021 16:20:14 -0500 Subject: [PATCH 10/24] WIP: refactored sync into sync and session --- session.js | 296 +++++++++++++++++ sync.js | 459 +++++--------------------- test/test-sync-clients-and-sockets.js | 18 +- test/test-sync-sessions.js | 2 +- 4 files changed, 383 insertions(+), 392 deletions(-) diff --git a/session.js b/session.js index e69de29..dabb999 100644 --- a/session.js +++ b/session.js @@ -0,0 +1,296 @@ +class Session { + constructor(session_id) { + this.id = session_id; + this.sockets = {}; // socket.id -> client_id + this.clients = []; + this.entities = []; + this.scene = null; + this.isRecording = false; + this.start = Date.now(); + this.recordingStart = 0; + this.seq = 0; + // NOTE(rob) = DEPRECATED; use message_buffer. 8/3/2021 + // writers = { + // pos = { + // buffer = Buffer.alloc(this.positionWriteBufferSize()); + // cursor = 0 + // }; + // int = { + // buffer = Buffer.alloc(this.interactionWriteBufferSize()); + // cursor = 0 + // } + // }; + this.message_buffer = []; + } + + getId() { + return this.id; + } + + getSockets() { + return this.sockets; + } + + addSocket(socket, client_id) { + this.sockets[socket.id] = { client_id: client_id, socket: socket }; + } + + // Returns success = true if operation succeeded or false if socket or session with session_id were null. + // Returns isInSession if socket is in this.sockets. + hasSocket(socket) { + if (!socket) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `hasSocket: socket was null.` + ); + + return { + success: false, + isInSession: null, + }; + } + + let session = this.sessions.get(session_id); + + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + `Could not find session when trying to remove a socket from it.` + ); + + return { + success: false, + isInSession: null, + }; + } + + return { + success: true, + isInSession: socket.id in this.sockets, + }; + } + + removeSocket(socket) { + let { success, isInSession } = this.hasSocket(socket); + + if (!success || isInSession == null) { + this.logErrorSessionClientSocketAction( + session_id, + null, + socket.id, + `tried removing socket from this.sockets, but there was an error.` + ); + + return false; + } + + if (!isInSession) { + this.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + `tried removing socket from this.sockets, but it was not found.` + ); + + return false; + } + + delete this.sockets[socket.id]; + + return true; + } + + getSocketsFromClientId(client_id, excluded_socket_id) { + if (this.sockets == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to get session sockets from client ID, but this.sockets was null` + ); + + return null; + } + + var result = []; + + for (var candidate_socket_id in this.sockets) { + let isCorrectId = + this.sockets[candidate_socket_id].client_id == client_id; + + let doExclude = + this.sockets[candidate_socket_id].socket.id == excluded_socket_id; + + if (isCorrectId && !doExclude) { + result.push(this.sockets[candidate_socket_id].socket); + } + } + + return result; + } + + getClients() { + return this.clients; + } + + addClient(client_id) { + if ( + this.clients == null || + typeof this.clients === "undefined" || + this.clients.length == 0 + ) { + this.clients = [client_id]; + + return true; + } + + this.clients.push(client_id); + + return true; + } + + removeClient(client_id) { + if (this.clients == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to remove client from session, but this.clients was null` + ); + + return false; + } + + let index = this.clients.indexOf(client_id); + + if (this.clients.length == 0 || this.clients.indexOf(client_id) == -1) { + //client_id is not in the array, so we don't need to remove it. + this.logWarningSessionClientSocketAction( + null, + null, + client_id, + `Tried removing client from this.clients, but it was not there.` + ); + + return false; + } + + this.clients.splice(index, 1); + } + + removeDuplicateClients(client_id) { + if (this.clients == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to remove duplicate client from session, but this.clients was null` + ); + + return; + } + + if (this.clients.length == 0) { + return; + } + + const first_instance = this.clients.indexOf(client_id); + + for (let i = 0; i < this.clients.length; i += 1) { + if (i != first_instance && this.clients[i] == client_id) { + this.clients.splice(i, 1); + } + } + } + + hasClient (client_id) { + const numInstances = this.getNumClientInstances(session_id, client_id); + + if (numInstances >= 1) { + return true; + } + + return false; + } + + getNumClientInstances(client_id) { + if (this.clients == null) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + null, + `Could not get number of client instances -- session was null or session.clients was null.` + ); + + return -1; + } + + var count = 0; + + this.clients.forEach((value) => { + if (value == client_id) { + count += 1; + } + }); + + return count; + } + + getTotalNumInstancesForAllClients () { + if (this.clients == null) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `the session's session.clients was null.` + ); + + return -1; + } + + return session.clients.length; + } + + getClientIdFromSocket(socket) { + if (this.sockets == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to get client ID from session socket, but this.sockets was null` + ); + + return null; + } + + if (socket.id == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to get client ID from session socket, but socket.id was null` + ); + + return null; + } + + if (this.sockets[socket.id] == null) { + this.logErrorSessionClientSocketAction( + this.id, + client_id, + null, + `tried to get client ID from session socket, but this.sockets[socket.id] was null` + ); + + return null; + } + + return this.sockets[socket.id].client_id; + } +} + +export default Session; diff --git a/sync.js b/sync.js index 4e0e9e6..132820e 100644 --- a/sync.js +++ b/sync.js @@ -42,6 +42,7 @@ const path = require("path"); const util = require("util"); const { syslog } = require("winston/lib/winston/config"); +const Session = require("./session"); // event data globals // NOTE(rob): deprecated. @@ -661,14 +662,7 @@ module.exports = { } // check if the incoming packet is from a client who is valid for this session - - for (let i = 0; i < session.clients.length; i += 1) { - if (client_id == session.clients[i]) { - return true; - } - } - - return false; + return session.hasClient(client_id); } }, @@ -784,16 +778,10 @@ module.exports = { if (!session) return; // check if the incoming packet is from a client who is valid for this session - let joined = false; - for (let i = 0; i < session.clients.length; i++) { - if (client_id == session.clients[i]) { - joined = true; - break; - } + if (!session.hasClient(client_id)) { + return; } - if (!joined) return; - // entity should be rendered if (interaction_type == INTERACTION_RENDER) { let i = session.entities.findIndex((e) => e.id == target_id); @@ -949,7 +937,7 @@ module.exports = { // check requested api version if (version === 2) { state = { - clients: session.clients, + clients: session.getClients(), entities: session.entities, scene: session.scene, isRecording: session.isRecording, @@ -970,7 +958,7 @@ module.exports = { } state = { - clients: session.clients, + clients: session.getClients(), entities: entities, locked: locked, scene: session.scene, @@ -982,7 +970,20 @@ module.exports = { }, // returns true on success and false on failure - addClientToSession: function (session, client_id, do_create_session) { + addClientToSession: function (session_id, client_id, do_create_session) { + let { success, session } = this.getSession(session_id); + + if (!success || !session) { + this.logWarningSessionClientSocketAction( + session_id, + client_id, + socket.id, + "session was null when making socket and client join session. Creating a session for you." + ); + + session = this.createSession(session_id); + } + if (session == null && !do_create_session) { this.logErrorSessionClientSocketAction( null, @@ -998,58 +999,42 @@ module.exports = { session = this.createSession(); } - if ( - session.clients == null || - typeof session.clients === "undefined" || - session.clients.length == 0 - ) { - session.clients = [client_id]; - - return true; - } - - session.clients.push(client_id); + session.addClient(client_id); return true; }, - removeDuplicateClientsFromSession: function (session, client_id) { - if (session == null) { + removeDuplicateClientsFromSession: function (session_id, client_id) { + let { success, session } = this.getSession(session_id); + + if (!success) { this.logErrorSessionClientSocketAction( null, client_id, null, - `tried to remove duplicate client from session, but session was null` + `tried to remove duplicate client from session, but failed to get session` ); return; } - if (session.clients == null) { + if (session == null) { this.logErrorSessionClientSocketAction( - session.id, + null, client_id, null, - `tried to remove duplicate client from session, but session.clients was null` + `tried to remove duplicate client from session, but session was null` ); return; } - if (session.clients.length == 0) { - return; - } - - const first_instance = session.clients.indexOf(client_id); - - for (let i = 0; i < session.clients.length; i += 1) { - if (i != first_instance && session.clients[i] == client_id) { - session.clients.splice(i, 1); - } - } + session.removeDuplicateClients(client_id); }, - removeClientFromSession: function (session, client_id) { + removeClientFromSession: function (session_id, client_id) { + let session = this.getSession(session_id); + if (session == null) { this.logErrorSessionClientSocketAction( null, @@ -1061,37 +1046,7 @@ module.exports = { return false; } - if (session.clients == null) { - this.logErrorSessionClientSocketAction( - session.id, - client_id, - null, - `tried to remove client from session, but session.clients was null` - ); - - return false; - } - - let index = session.clients.indexOf(client_id); - - if ( - session.clients.length == 0 || - session.clients.indexOf(client_id) == -1 - ) { - //client_id is not in the array, so we don't need to remove it. - this.logWarningSessionClientSocketAction( - null, - null, - client_id, - `Tried removing client from session.clients, but it was not there.` - ); - - return false; - } - - session.clients.splice(index, 1); - - let session_id = this.getSessionIdFromSession(session); + session.removeClient(client_id); this.logInfoSessionClientSocketAction( session_id, @@ -1103,21 +1058,6 @@ module.exports = { return true; }, - addSocketToSession: function (session, socket, client_id) { - if (!session) { - this.logErrorSessionClientSocketAction( - null, - client_id, - socket.id, - `tried to add socket to session, but session was null` - ); - - return; - } - - session.sockets[socket.id] = { client_id: client_id, socket: socket }; - }, - // returns true iff socket was successfully joined to session addSocketAndClientToSession: function ( err, @@ -1167,20 +1107,9 @@ module.exports = { return false; } - let { success, session } = this.getSession(session_id); - - if (!success || !session) { - this.logWarningSessionClientSocketAction( - session_id, - client_id, - socket.id, - "session was null when making socket and client join session. Creating a session for you." - ); + let session = this.getOrCreateSession(session_id); - session = this.createSession(session_id); - } - - success = this.addClientToSession(session, client_id); + success = session.addClient(client_id); if (!success) { reason = `tried to make socket and client join session, but adding client to session failed.`; @@ -1198,18 +1127,18 @@ module.exports = { } this.bumpDuplicateSockets( - session, + session_id, client_id, do_bump_duplicates, socket.id ); if (do_bump_duplicates) { - this.removeDuplicateClientsFromSession(session, client_id); + session.removeDuplicateClients(client_id); } // socket to client mapping - this.addSocketToSession(session, socket, client_id); + session.addSocket(socket, client_id); if (!this.successfullyJoinedAction) { this.logWarningSessionClientSocketAction( @@ -1287,7 +1216,7 @@ module.exports = { return; } - success = this.removeSocketFromSession(socket, session_id); + success = session.removeSocket(socket); if (!success) { reason = `removeSocketFromSession failed`; @@ -1297,10 +1226,10 @@ module.exports = { return; } - success = this.removeClientFromSession(session, client_id); + success = session.removeClient(client_id); if (!success) { - reason = `removeClientFromSession failed`; + reason = `session.removeClient failed`; this.failedToLeaveAction(session_id, reason); @@ -1356,11 +1285,13 @@ module.exports = { //TODO rewrite this so that do_bump_duplicates and socket_id become ids_to_keep bumpDuplicateSockets: function ( - session, + session_id, client_id, do_bump_duplicates, socket_id ) { + let session = this.getSession(session_id); + if (session == null) { this.logErrorSessionClientSocketAction( null, @@ -1372,18 +1303,15 @@ module.exports = { return; } - let session_id = this.getSessionIdFromSession(session); - let sockets; if (do_bump_duplicates) { - sockets = this.getSessionSocketsFromClientId( - session, + sockets = session.getSocketsFromClientId( client_id, socket_id ); } else { - sockets = this.getSessionSocketsFromClientId(session, client_id, null); + sockets = session.getSocketsFromClientId(client_id, null); } let self = this; @@ -1393,7 +1321,7 @@ module.exports = { session_id, client_id, socket_id, - `tried to bump duplicate sockets, but result of getSessionSocketsFromClientId was null. Proceeding anyways.` + `tried to bump duplicate sockets, but result of getSocketsFromClientId was null. Proceeding anyways.` ); } @@ -1446,83 +1374,9 @@ module.exports = { } }, - // returns session ID on success; returns -1 on failure - // TODO(Brandon): deprecate and remove 8/10/21 - getSessionIdFromSession: function (session) { - let result = -1; - - if (session == null || typeof session === "undefined") { - this.logErrorSessionClientSocketAction( - null, - null, - null, - `tried to get session ID from session, but session was null or undefined` - ); - - return result; - } - - if (typeof session !== "object") { - this.logErrorSessionClientSocketAction( - null, - null, - null, - `tried to get session ID from session, but session was not an object` - ); - - return result; - } - - if (session.clients == null || typeof session.clients === "undefined") { - this.logErrorSessionClientSocketAction( - session.id, - null, - null, - `session.clients was null or undefined` - ); - - return result; - } - - if (session.sockets == null || typeof session.sockets === "undefined") { - this.logErrorSessionClientSocketAction( - session.id, - null, - null, - `session.sockets was null or undefined` - ); - - return result; - } + getClientIdFromSessionSocket: function (session_id, socket) { + let session = this.getSession(session_id); - this.sessions.forEach((candidate_session, candidate_session_id) => { - if ( - candidate_session.clients == null || - typeof candidate_session.clients === "undefined" - ) { - return; // return from the inner function only. - } - - if ( - candidate_session.sockets == null || - typeof candidate_session.sockets === "undefined" - ) { - return; // return from the inner function only. - } - - if (candidate_session.sockets.size != session.sockets.size) { - return; // return from the inner function only. - } - - if (compareKeys(candidate_session.sockets, session.sockets)) { - result = candidate_session_id; - } - }); - - return result; - }, - - getClientIdFromSessionSocket: function (socket) { if (session == null) { this.logErrorSessionClientSocketAction( null, @@ -1533,48 +1387,15 @@ module.exports = { return null; } - - if (session.sockets == null) { - this.logErrorSessionClientSocketAction( - session.id, - client_id, - null, - `tried to get client ID from session socket, but session.sockets was null` - ); - - return null; - } - - if (socket.id == null) { - this.logErrorSessionClientSocketAction( - session.id, - client_id, - null, - `tried to get client ID from session socket, but socket.id was null` - ); - - return null; - } - - if (session.sockets[socket.id] == null) { - this.logErrorSessionClientSocketAction( - session.id, - client_id, - null, - `tried to get client ID from session socket, but session.sockets[socket.id] was null` - ); - - return null; - } - - return session.sockets[socket.id].client_id; }, getSessionSocketsFromClientId: function ( - session, + session_id, client_id, excluded_socket_id ) { + let session = this.getSession(session_id); + if (session == null) { this.logErrorSessionClientSocketAction( null, @@ -1586,140 +1407,49 @@ module.exports = { return null; } - if (session.sockets == null) { - this.logErrorSessionClientSocketAction( - session.id, - client_id, - null, - `tried to get session sockets from client ID, but session.sockets was null` - ); - - return null; - } - - var result = []; - - for (var candidate_socket_id in session.sockets) { - let isCorrectId = - session.sockets[candidate_socket_id].client_id == client_id; - - let doExclude = - session.sockets[candidate_socket_id].socket.id == excluded_socket_id; - - if (isCorrectId && !doExclude) { - result.push(session.sockets[candidate_socket_id].socket); - } - } - - return result; + return session.getSocketsFromClientId(client_id, excluded_socket_id); }, isClientInSession: function (session_id, client_id) { - const numInstances = this.getNumClientInstancesForClient(session_id, client_id); - - if (numInstances >= 1) { - return true; - } - - return false; - }, - - // Returns success = true if operation succeeded or false if socket or session with session_id were null. - // Returns isInSession if socket is in session.sockets. - isSocketInSession: function (socket, session_id) { - if (!socket) { - this.logWarningSessionClientSocketAction( - session_id, - null, - null, - `removeSocketFromSession: socket was null.` - ); - - return { - success: false, - isInSession: null - }; - } - - let session = this.sessions.get(session_id); - - if (!session) { - this.logWarningSessionClientSocketAction( - session_id, - null, - socket.id, - `Could not find session when trying to remove a socket from it.` - ); - - return { - success: false, - isInSession: null - }; - } + let session = this.getSession(session_id); - return { - success: true, - isInSession: (socket.id in session.sockets) - }; + return session.hasClient(client_id); }, // returns number of client instances of the same ID on success; returns -1 on failure; - getNumClientInstancesForClient: function (session_id, client_id) { - let session = this.sessions.get(session_id); + getNumClientInstancesForSession: function (session_id, client_id) { + let session = this.getSession(session_id); - if (session == null || session.clients == null) { + if (session == null) { this.logErrorSessionClientSocketAction( session_id, client_id, null, - `Could not get number of client instances -- session was null or session.clients was null.` + `Could not get number of client instances -- session was null` ); return -1; } - var count = 0; - - session.clients.forEach((value) => { - if (value == client_id) { - count += 1; - } - }); - - return count; + return session.getNumClientInstances(client_id); }, // Return true iff removing the socket succeeded. removeSocketFromSession: function (socket, session_id) { - let { success, isInSession } = this.isSocketInSession(socket, session_id); - - if (!success || isInSession == null) { - this.logErrorSessionClientSocketAction( - session_id, - null, - socket.id, - `tried removing socket from session.sockets, but there was an error.` - ); - - return false; - } + let session = this.sessions.get(session_id); - if (!isInSession) { + if (!session) { this.logWarningSessionClientSocketAction( session_id, null, - socket.id, - `tried removing socket from session.sockets, but it was not found.` + null, + `tried to removeSocketFromSession, but session was not found.` ); return false; } - let session = this.sessions.get(session_id); - - delete session.sockets[socket.id]; - - return true; + return session.removeSocket(socket); }, disconnectSocket: function (socket, session_id, client_id) { @@ -1761,12 +1491,12 @@ module.exports = { let session = this.sessions.get(session_id); - this.removeClientFromSession(session, client_id); + session.removeClient(client_id); - this.removeSocketFromSession(socket, session_id); + session.removeSocket(socket); }, - getNumClientInstances: function (session_id) { + getTotalNumInstancesForAllClientsForSession: function (session_id) { let session = this.sessions.get(session_id); if (!session) { @@ -1780,18 +1510,7 @@ module.exports = { return -1; } - if (session.clients == null) { - this.logWarningSessionClientSocketAction( - session_id, - null, - null, - `the session's session.clients was null.` - ); - - return -1; - } - - return session.clients.length; + return session.getTotalNumInstancesForAllClients(); }, try_to_end_recording: function (session_id) { @@ -1824,7 +1543,7 @@ module.exports = { // clean up session from sessions map if empty, write cleanUpSessionIfEmpty: function (session_id) { - if (this.getNumClientInstancesForClient(session_id) >= 0) { + if (this.getNumClientInstancesForSession(session_id) >= 0) { // don't clean up if there are still clients in the session return; } @@ -1880,29 +1599,7 @@ module.exports = { `Creating session: ${session_id}` ); - this.sessions.set(session_id, { - id: session_id, - sockets: {}, // socket.id -> client_id - clients: [], - entities: [], - scene: null, - isRecording: false, - start: Date.now(), - recordingStart: 0, - seq: 0, - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // writers: { - // pos: { - // buffer: Buffer.alloc(this.positionWriteBufferSize()), - // cursor: 0 - // }, - // int: { - // buffer: Buffer.alloc(this.interactionWriteBufferSize()), - // cursor: 0 - // } - // }, - message_buffer: [], - }); + this.sessions.set(session_id, new Session()); session = this.sessions.get(session_id); @@ -1947,7 +1644,7 @@ module.exports = { let session = s[1]; - let { success, isInSession } = this.isSocketInSession(socket, session_id); + let { success, isInSession } = session.hasSocket(socket); if (!success || !isInSession) { // This isn't the right session, so keep looking. @@ -1958,7 +1655,7 @@ module.exports = { return { session_id: session_id, - client_id: this.getClientIdFromSessionSocket(socket), + client_id: session.getClientIdFromSocket(socket), }; } @@ -2034,7 +1731,7 @@ module.exports = { client_id, socket.id, `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify( - session.clients + session.getClients() )}` ); @@ -2541,7 +2238,7 @@ module.exports = { client_id, socket.id, `Client was disconnected; attempting to reconnect. Disconnect reason: , clients: ${JSON.stringify( - session.clients + session.getClients() )}` ); @@ -2574,9 +2271,7 @@ module.exports = { return; } - socket - .to(session_id.toString()) - .emit(KomodoSendEvents.sessionInfo, session); + socket.to(session_id.toString()).emit(KomodoSendEvents.sessionInfo, session); }); socket.on(KomodoReceiveEvents.requestToJoinSession, function (data) { diff --git a/test/test-sync-clients-and-sockets.js b/test/test-sync-clients-and-sockets.js index 1145ad7..4a9e4e8 100644 --- a/test/test-sync-clients-and-sockets.js +++ b/test/test-sync-clients-and-sockets.js @@ -23,7 +23,7 @@ const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; describe("Sync Server: Clients and Sockets", function (done) { beforeEach(function () { - syncServer.notifyBumpAction = function () { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { throw Error("An unexpected bump occurred."); }; @@ -127,7 +127,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAction = function (session_id, socket) { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.equal(DUMMY_SOCKET_A); @@ -147,7 +147,7 @@ describe("Sync Server: Clients and Sockets", function (done) { disconnectCount += 1; }; - syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_B.id); + syncServer.bumpDuplicateSockets(SESSION_ID, CLIENT_ID, true, DUMMY_SOCKET_B.id); bumpCount.should.eql(1); @@ -178,7 +178,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAction = function (session_id, socket) { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); @@ -198,7 +198,7 @@ describe("Sync Server: Clients and Sockets", function (done) { disconnectCount += 1; }; - syncServer.bumpDuplicateSockets(session, CLIENT_ID, true, DUMMY_SOCKET_C.id); + syncServer.bumpDuplicateSockets(SESSION_ID, CLIENT_ID, true, DUMMY_SOCKET_C.id); bumpCount.should.eql(2); @@ -282,11 +282,11 @@ describe("Sync Server: Clients and Sockets", function (done) { syncServer.sessions.set(SESSION_ID, session); - let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); + let sockets = session.getSocketsFromClientId(CLIENT_ID, null); sockets.should.eql([ DUMMY_SOCKET_A ]); - syncServer.notifyBumpAction = function (session_id, socket) { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); @@ -302,7 +302,7 @@ describe("Sync Server: Clients and Sockets", function (done) { session = syncServer.sessions.get(SESSION_ID); - sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, null); + sockets = session.getSocketsFromClientId(CLIENT_ID, null); sockets.should.eql([ DUMMY_SOCKET_A, DUMMY_SOCKET_B ]); }); @@ -319,7 +319,7 @@ describe("Sync Server: Clients and Sockets", function (done) { syncServer.sessions.set(SESSION_ID, session); - let sockets = syncServer.getSessionSocketsFromClientId(session, CLIENT_ID, DUMMY_SOCKET_C.id); + let sockets = session.getSocketsFromClientId(CLIENT_ID, DUMMY_SOCKET_C.id); sockets.should.eql( [ DUMMY_SOCKET_A, DUMMY_SOCKET_B ] ); }); diff --git a/test/test-sync-sessions.js b/test/test-sync-sessions.js index 3d87432..d179b35 100644 --- a/test/test-sync-sessions.js +++ b/test/test-sync-sessions.js @@ -25,7 +25,7 @@ describe("Sync Server: Sessions", function (done) { beforeEach(function () { syncServer.initGlobals(); - syncServer.notifyBumpAction = function () { + syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { throw Error("An unexpected bump occurred."); }; From 6d6c1ceb57d261de41753808f459f61c6d188d40 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Sun, 17 Oct 2021 17:48:03 -0500 Subject: [PATCH 11/24] fixed bugs and tests for session refactor --- session.js | 69 +------- sync.js | 86 +++++---- test/test-session.js | 239 ++++++++++++++++++++++++++ test/test-sync-clients-and-sockets.js | 217 +++++------------------ test/test-sync-sessions.js | 47 +---- 5 files changed, 345 insertions(+), 313 deletions(-) create mode 100644 test/test-session.js diff --git a/session.js b/session.js index dabb999..1d86b8b 100644 --- a/session.js +++ b/session.js @@ -1,6 +1,6 @@ class Session { - constructor(session_id) { - this.id = session_id; + constructor(id) { + this.id = id; this.sockets = {}; // socket.id -> client_id this.clients = []; this.entities = []; @@ -35,67 +35,16 @@ class Session { this.sockets[socket.id] = { client_id: client_id, socket: socket }; } - // Returns success = true if operation succeeded or false if socket or session with session_id were null. + // Returns success = true if operation succeeded or false if socket or session with id were null. // Returns isInSession if socket is in this.sockets. hasSocket(socket) { - if (!socket) { - this.logWarningSessionClientSocketAction( - session_id, - null, - null, - `hasSocket: socket was null.` - ); - - return { - success: false, - isInSession: null, - }; - } - - let session = this.sessions.get(session_id); - - if (!session) { - this.logWarningSessionClientSocketAction( - session_id, - null, - socket.id, - `Could not find session when trying to remove a socket from it.` - ); - - return { - success: false, - isInSession: null, - }; - } - - return { - success: true, - isInSession: socket.id in this.sockets, - }; + return socket.id in this.sockets; } removeSocket(socket) { - let { success, isInSession } = this.hasSocket(socket); - - if (!success || isInSession == null) { - this.logErrorSessionClientSocketAction( - session_id, - null, - socket.id, - `tried removing socket from this.sockets, but there was an error.` - ); - - return false; - } + let isInSession = this.hasSocket(socket); if (!isInSession) { - this.logWarningSessionClientSocketAction( - session_id, - null, - socket.id, - `tried removing socket from this.sockets, but it was not found.` - ); - return false; } @@ -208,7 +157,7 @@ class Session { } hasClient (client_id) { - const numInstances = this.getNumClientInstances(session_id, client_id); + const numInstances = this.getNumClientInstances(client_id); if (numInstances >= 1) { return true; @@ -220,7 +169,7 @@ class Session { getNumClientInstances(client_id) { if (this.clients == null) { this.logErrorSessionClientSocketAction( - session_id, + this.id, client_id, null, `Could not get number of client instances -- session was null or session.clients was null.` @@ -243,7 +192,7 @@ class Session { getTotalNumInstancesForAllClients () { if (this.clients == null) { this.logWarningSessionClientSocketAction( - session_id, + this.id, null, null, `the session's session.clients was null.` @@ -293,4 +242,4 @@ class Session { } } -export default Session; +module.exports = Session; diff --git a/sync.js b/sync.js index 132820e..9bf7744 100644 --- a/sync.js +++ b/sync.js @@ -970,35 +970,20 @@ module.exports = { }, // returns true on success and false on failure - addClientToSession: function (session_id, client_id, do_create_session) { + addClientToSession: function (session_id, client_id) { let { success, session } = this.getSession(session_id); - if (!success || !session) { + if (!success) { this.logWarningSessionClientSocketAction( session_id, client_id, - socket.id, - "session was null when making socket and client join session. Creating a session for you." - ); - - session = this.createSession(session_id); - } - - if (session == null && !do_create_session) { - this.logErrorSessionClientSocketAction( - null, - client_id, null, - `tried to add client to session, but session was null and do_create_session was false` + `failed to get session when adding client to session. Not proceeding.` ); return false; } - if (session == null && do_create_session) { - session = this.createSession(); - } - session.addClient(client_id); return true; @@ -1033,9 +1018,9 @@ module.exports = { }, removeClientFromSession: function (session_id, client_id) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); - if (session == null) { + if ( !success || session == null ) { this.logErrorSessionClientSocketAction( null, client_id, @@ -1261,8 +1246,8 @@ module.exports = { if (this.notifyBumpAction == null) { this.logWarningSessionClientSocketAction( session_id, - client_id, - socket_id, + null, + socket.id, `notifyBumpAction callback was not provided` ); } @@ -1274,8 +1259,8 @@ module.exports = { if (this.makeSocketLeaveSessionAction == null) { this.logWarningSessionClientSocketAction( session_id, - client_id, - socket_id, + null, + socket.id, `makeSocketLeaveSessionAction callback was not provided` ); } @@ -1290,7 +1275,7 @@ module.exports = { do_bump_duplicates, socket_id ) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); if (session == null) { this.logErrorSessionClientSocketAction( @@ -1375,7 +1360,7 @@ module.exports = { }, getClientIdFromSessionSocket: function (session_id, socket) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); if (session == null) { this.logErrorSessionClientSocketAction( @@ -1394,7 +1379,7 @@ module.exports = { client_id, excluded_socket_id ) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); if (session == null) { this.logErrorSessionClientSocketAction( @@ -1411,14 +1396,14 @@ module.exports = { }, isClientInSession: function (session_id, client_id) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); return session.hasClient(client_id); }, // returns number of client instances of the same ID on success; returns -1 on failure; getNumClientInstancesForSession: function (session_id, client_id) { - let session = this.getSession(session_id); + let { success, session } = this.getSession(session_id); if (session == null) { this.logErrorSessionClientSocketAction( @@ -1607,7 +1592,9 @@ module.exports = { }, processReconnectionAttempt: function (err, socket, session_id, client_id) { - let success = this.addSocketAndClientToSession(err, socket, session_id, client_id, true); + this.getOrCreateSession(session_id); + + let success = this.addSocketAndClientToSession(err, socket, session_id, client_id); if (!success) { this.logInfoSessionClientSocketAction( @@ -1638,13 +1625,50 @@ module.exports = { return true; }, + isSocketInSession: function (session_id, socket) { + if (!socket) { + this.logWarningSessionClientSocketAction( + session_id, + null, + null, + `hasSocket: socket was null.` + ); + + return { + success: false, + isInSession: null, + }; + } + + let session = this.sessions.get(session_id); + + if (!session) { + this.logWarningSessionClientSocketAction( + session_id, + null, + socket.id, + `Could not find session when trying to remove a socket from it.` + ); + + return { + success: false, + isInSession: null, + }; + } + + return { + success: true, + isInSession: session.hasSocket(socket), + }; + }, + whoDisconnected: function (socket) { for (var s in this.sessions) { const session_id = s[0]; let session = s[1]; - let { success, isInSession } = session.hasSocket(socket); + let { success, isInSession } = isSocketInSession(session_id, socket); if (!success || !isInSession) { // This isn't the right session, so keep looking. diff --git a/test/test-session.js b/test/test-session.js new file mode 100644 index 0000000..74cd67f --- /dev/null +++ b/test/test-session.js @@ -0,0 +1,239 @@ +/* jshint esversion: 6 */ + +// TODO: add test for getting state +// TODO: add test for connecting without valid credentials + +var assert = require("assert"); + +var should = require("should"); + +const { debug } = require("winston"); + +const Session = require("../session"); + +const SESSION_ID = 123; + +const CLIENT_ID = 456; + +const DUMMY_SOCKET_A = { "dummy": "socketA", "id": "DEADBEEF" }; + +const DUMMY_SOCKET_B = { "dummy": "socketB", "id": "LIVEBEEF" }; + +const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; + +describe("Session", function (done) { + beforeEach(function () { + }); + + //TODO implement this if we ever keep a global list of clients + + /* + it("should have 0 clients on startup", function () { + }); + */ + + it("should create one singular, correct sessions object", function () { + const session_id = 123; + + let session = new Session(session_id); + + let sessionType = typeof session; + + sessionType.should.not.equal("undefined"); + + session.should.not.equal(null); + + const expectedSession = { + id: session_id, + sockets: {}, // socket.id -> client_id + clients: [], + entities: [], + scene: null, + isRecording: false, + start: Date.now(), + recordingStart: 0, + seq: 0, + // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 + // writers: { + // pos: { + // buffer: Buffer.alloc(syncServer.positionWriteBufferSize()), + // cursor: 0 + // }, + // int: { + // buffer: Buffer.alloc(syncServer.interactionWriteBufferSize()), + // cursor: 0 + // } + // }, + message_buffer: [] + }; + + session.id.should.equal(expectedSession.id); + + assert.deepStrictEqual(session.sockets, expectedSession.sockets); + + assert.deepStrictEqual(session.clients, expectedSession.clients); + + assert.deepStrictEqual(session.entities, expectedSession.entities); + + assert.deepStrictEqual(session.scene, expectedSession.scene); + + assert.deepStrictEqual(session.isRecording, expectedSession.isRecording); + + // Do not check start time for strict equality. + assert(Math.abs(session.start - expectedSession.start) < 1000); + + assert.deepStrictEqual(session.recordingStart, expectedSession.recordingStart); + + assert.deepStrictEqual(session.seq, expectedSession.seq); + + assert.deepStrictEqual(session.message_buffer, expectedSession.message_buffer); + }); + + it("should append a valid client to an empty session", function () { + let session = new Session(); + + session.addClient(CLIENT_ID); + + let expectedClients = [ CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should append a duplicate client to a session", function () { + let session = new Session(); + + session.addClient(CLIENT_ID); + + let expectedClients = [ CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + + session.addClient(CLIENT_ID); + + expectedClients = [ CLIENT_ID, CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should reduce two duplicate clients to one client", function () { + let session = new Session(); + + session.clients.length.should.equal(0); + + session.addClient(CLIENT_ID); + + session.clients.length.should.equal(1); + + session.addClient(CLIENT_ID); + + session.clients.length.should.equal(2); + + session.removeDuplicateClients(CLIENT_ID); + + session.clients.length.should.equal(1); + + let expectedClients = [ CLIENT_ID ]; + + session.clients.should.eql(expectedClients); + }); + + it("should return true if it found a socket", function () { + let session = new Session(); + + Object.keys(session.sockets).length.should.equal(0); + + session.addSocket(DUMMY_SOCKET_A, CLIENT_ID); + + let success = session.hasSocket(DUMMY_SOCKET_A); + + success.should.equal(true); + }); + + it("should return false if it couldn't find a socket", function () { + let session = new Session(); + + Object.keys(session.sockets).length.should.equal(0); + + session.addSocket(DUMMY_SOCKET_A, CLIENT_ID); + + let success = session.hasSocket(DUMMY_SOCKET_B); + + success.should.equal(false); + }); + + it("should be able to remove a socket", function () { + let session = new Session(); + + Object.keys(session.sockets).length.should.equal(0); + + session.addSocket(DUMMY_SOCKET_A, CLIENT_ID); + + Object.keys(session.sockets).length.should.equal(1); + + session.addSocket(DUMMY_SOCKET_B, CLIENT_ID); + + Object.keys(session.sockets).length.should.equal(2); + + let success = session.removeSocket(DUMMY_SOCKET_A); + + success.should.equal(true); + + Object.keys(session.sockets).length.should.equal(1); + + let expectedSockets = {}; + + expectedSockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; + + session.sockets.should.eql(expectedSockets); + }); + + it("should return all session sockets for a given client ID", function () { + let session = new Session(); + + session.clients = [CLIENT_ID]; + + session.sockets = { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } + }; + + let sockets = session.getSocketsFromClientId(CLIENT_ID, null); + + sockets.should.eql([ DUMMY_SOCKET_A ]); + + session.addSocket(DUMMY_SOCKET_B, CLIENT_ID); + + // syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + // session_id.should.equal(SESSION_ID); + + // socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); + // }; + + // syncServer.sessions.set(SESSION_ID, { + // clients: [CLIENT_ID, CLIENT_ID], + // sockets: { + // socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, + // socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B } + // } + // }); + + sockets = session.getSocketsFromClientId(CLIENT_ID, null); + + sockets.should.eql([ DUMMY_SOCKET_A, DUMMY_SOCKET_B ]); + }); + + it("should exclude a socket when requesting session sockets", function () { + let session = new Session(); + + session.clients = [ CLIENT_ID, CLIENT_ID, CLIENT_ID ]; + + session.sockets = { + socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, + socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }, + socketC: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }, + }; + + let sockets = session.getSocketsFromClientId(CLIENT_ID, DUMMY_SOCKET_C.id); + + sockets.should.eql( [ DUMMY_SOCKET_A, DUMMY_SOCKET_B ] ); + }); +}); diff --git a/test/test-sync-clients-and-sockets.js b/test/test-sync-clients-and-sockets.js index 4a9e4e8..542f3a5 100644 --- a/test/test-sync-clients-and-sockets.js +++ b/test/test-sync-clients-and-sockets.js @@ -10,6 +10,8 @@ var should = require("should"); const { debug } = require("winston"); const syncServer = require("../sync"); + +const Session = require("../session"); const SESSION_ID = 123; @@ -23,7 +25,7 @@ const DUMMY_SOCKET_C = { "dummy": "socketC", "id": "SCHRBEEF" }; describe("Sync Server: Clients and Sockets", function (done) { beforeEach(function () { - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function () { + syncServer.notifyBumpAction = function () { throw Error("An unexpected bump occurred."); }; @@ -51,69 +53,22 @@ describe("Sync Server: Clients and Sockets", function (done) { }); */ - it("should append a valid client to an empty session", function () { - let sessions = new Map (); - - let session = { - clients: [ ] - }; - - sessions.set(SESSION_ID, session); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(session, CLIENT_ID); - - let expectedClients = [ CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - - it("should create a session when appending a valid client to a null session, appropriately", function () { - let sessions = new Map (); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(null, CLIENT_ID, true); - - let expectedClients = [ CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - - it("should return an error when appending a valid client to a null session, appropriately", function () { + it("should return an error when appending a valid client to a null session", function () { let sessions = new Map (); syncServer.sessions = sessions; - let success = syncServer.addClientToSession(null, CLIENT_ID, false); + let success = syncServer.addClientToSession(null, CLIENT_ID); success.should.eql(false); }); - it("should append a duplicate client to a session", function () { - let sessions = new Map (); - - let session = { - clients: [ CLIENT_ID ] - }; - - sessions.set(SESSION_ID, session); - - syncServer.sessions = sessions; - - syncServer.addClientToSession(session, CLIENT_ID); - - let expectedClients = [ CLIENT_ID, CLIENT_ID ]; - - session.clients.should.eql(expectedClients); - }); - it("should be able to bump one existing socket", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID ], - sockets: { } - }; + let session = new Session(); + + session.clients = [ CLIENT_ID, CLIENT_ID ]; + + session.sockets = { }; session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; @@ -127,7 +82,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.equal(DUMMY_SOCKET_A); @@ -135,6 +90,16 @@ describe("Sync Server: Clients and Sockets", function (done) { bumpCount += 1; }; + let leaveCount = 0; + + syncServer.makeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.equal(DUMMY_SOCKET_A); + + leaveCount += 1; + }; + let disconnectCount = 0; syncServer.disconnectedAction = function (socket, session_id, client_id) { @@ -151,6 +116,8 @@ describe("Sync Server: Clients and Sockets", function (done) { bumpCount.should.eql(1); + leaveCount.should.eql(1); + disconnectCount.should.eql(1); outputSession = syncServer.sessions.get(SESSION_ID); @@ -159,10 +126,11 @@ describe("Sync Server: Clients and Sockets", function (done) { }); it("should be able to bump two existing sockets", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], - sockets: { } - }; + let session = new Session(); + + session.clients = [ CLIENT_ID, CLIENT_ID, CLIENT_ID ]; + + session.sockets = { }; session.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; @@ -178,7 +146,7 @@ describe("Sync Server: Clients and Sockets", function (done) { let bumpCount = 0; - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { + syncServer.notifyBumpAction = function (session_id, socket) { session_id.should.equal(SESSION_ID); socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); @@ -186,6 +154,16 @@ describe("Sync Server: Clients and Sockets", function (done) { bumpCount += 1; }; + let leaveCount = 0; + + syncServer.makeSocketLeaveSessionAction = function (session_id, socket) { + session_id.should.equal(SESSION_ID); + + socket.should.be.oneOf(DUMMY_SOCKET_A, DUMMY_SOCKET_B); + + leaveCount += 1; + }; + let disconnectCount = 0; syncServer.disconnectedAction = function (socket, session_id, client_id) { @@ -202,125 +180,12 @@ describe("Sync Server: Clients and Sockets", function (done) { bumpCount.should.eql(2); + leaveCount.should.eql(2); + disconnectCount.should.eql(2); outputSession = syncServer.sessions.get(SESSION_ID); Object.keys(outputSession.sockets).should.eql( [ DUMMY_SOCKET_C.id ] ); }); - - it("should reduce two duplicate clients to one client", function () { - syncServer.createSession(SESSION_ID); - - let { success, session } = syncServer.getSession(SESSION_ID); - - session.should.not.eql(null); - - session.clients.should.not.eql(null); - - session.clients.length.should.equal(0); - - syncServer.addClientToSession(session, CLIENT_ID); - - session.clients.length.should.equal(1); - - syncServer.addClientToSession(session, CLIENT_ID); - - session.clients.length.should.equal(2); - - syncServer.removeDuplicateClientsFromSession(session, CLIENT_ID); - - session.clients.length.should.equal(1); - }); - - it("should be able to remove a socket", function () { - let inputSession = { - clients: [CLIENT_ID, CLIENT_ID], - sockets: { } - }; - - inputSession.sockets[DUMMY_SOCKET_A.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }; - - inputSession.sockets[DUMMY_SOCKET_B.id] = { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }; - - syncServer.createSession(SESSION_ID); - - syncServer.sessions.set(SESSION_ID, inputSession); - - Object.keys(inputSession.sockets).length.should.equal(2); - - let removeSuccess = syncServer.removeSocketFromSession(DUMMY_SOCKET_A, SESSION_ID); - - removeSuccess.should.equal(true); - - let { success, session } = syncServer.getSession(SESSION_ID); - - Object.keys(session.sockets).length.should.equal(1); - }); - - it("should return true if it found a socket", function () { - throw Error("unimplemented"); - }); - - it("should return false if it couldn't find a socket", function () { - throw Error("unimplemented"); - }); - - it("should be able to remove a socket and client from a session then disconnect the socket", function () { - throw Error("unimplemented"); - }); - - it("should return all session sockets for a given client ID", function () { - syncServer.sessions = new Map (); - - let session = { - clients: [CLIENT_ID], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A } - } - }; - - syncServer.sessions.set(SESSION_ID, session); - - let sockets = session.getSocketsFromClientId(CLIENT_ID, null); - - sockets.should.eql([ DUMMY_SOCKET_A ]); - - syncServer.notifyBumpAndMakeSocketLeaveSessionAction = function (session_id, socket) { - session_id.should.equal(SESSION_ID); - - socket.should.eql( { dummy: "socketA", id: "DEADBEEF" } ); - }; - - syncServer.sessions.set(SESSION_ID, { - clients: [CLIENT_ID, CLIENT_ID], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, - socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B } - } - }); - - session = syncServer.sessions.get(SESSION_ID); - - sockets = session.getSocketsFromClientId(CLIENT_ID, null); - - sockets.should.eql([ DUMMY_SOCKET_A, DUMMY_SOCKET_B ]); - }); - - it("should exclude a socket when requesting session sockets", function () { - let session = { - clients: [ CLIENT_ID, CLIENT_ID, CLIENT_ID ], - sockets: { - socketA: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_A }, - socketB: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_B }, - socketC: { client_id: CLIENT_ID, socket: DUMMY_SOCKET_C }, - } - }; - - syncServer.sessions.set(SESSION_ID, session); - - let sockets = session.getSocketsFromClientId(CLIENT_ID, DUMMY_SOCKET_C.id); - - sockets.should.eql( [ DUMMY_SOCKET_A, DUMMY_SOCKET_B ] ); - }); }); diff --git a/test/test-sync-sessions.js b/test/test-sync-sessions.js index d179b35..cda00da 100644 --- a/test/test-sync-sessions.js +++ b/test/test-sync-sessions.js @@ -44,7 +44,7 @@ describe("Sync Server: Sessions", function (done) { sessions.size.should.equal(0); }); - it("should create one singular, correct sessions object", function () { + it("should create one singular sessions object", function () { const session_id = 123; let sessions = syncServer.getSessions(); @@ -74,51 +74,6 @@ describe("Sync Server: Sessions", function (done) { sessionType.should.not.equal("undefined"); singularEntry[0].should.equal(session_id); - - const expectedSession = { - sockets: {}, // socket.id -> client_id - clients: [], - entities: [], - scene: null, - isRecording: false, - start: Date.now(), - recordingStart: 0, - seq: 0, - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // writers: { - // pos: { - // buffer: Buffer.alloc(syncServer.positionWriteBufferSize()), - // cursor: 0 - // }, - // int: { - // buffer: Buffer.alloc(syncServer.interactionWriteBufferSize()), - // cursor: 0 - // } - // }, - message_buffer: [] - }; - - assert.deepStrictEqual(singularEntry[1].sockets, expectedSession.sockets); - - assert.deepStrictEqual(singularEntry[1].clients, expectedSession.clients); - - assert.deepStrictEqual(singularEntry[1].entities, expectedSession.entities); - - assert.deepStrictEqual(singularEntry[1].scene, expectedSession.scene); - - assert.deepStrictEqual(singularEntry[1].isRecording, expectedSession.isRecording); - - // Do not check start time for strict equality. - assert(Math.abs(singularEntry[1].start - expectedSession.start) < 1000); - - assert.deepStrictEqual(singularEntry[1].recordingStart, expectedSession.recordingStart); - - assert.deepStrictEqual(singularEntry[1].seq, expectedSession.seq); - - // NOTE(rob): DEPRECATED, use message_buffer. 8/3/2021 - // assert.deepStrictEqual(singularEntry[1].writers, expectedSession.writers); - - assert.deepStrictEqual(singularEntry[1].message_buffer, expectedSession.message_buffer); }); it("should return failure on getting a nonexistent session", function () { From f3c04179a36c263c423ec39f8052f292f3640fb3 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Sun, 17 Oct 2021 20:18:11 -0500 Subject: [PATCH 12/24] WIP: fixed some bugs with logging, action calls missing sockets --- serve.js | 6 +- session.js | 14 ---- sync.js | 134 +++++++++++++++++++++++----------- test/test-sync-integration.js | 2 +- 4 files changed, 95 insertions(+), 61 deletions(-) diff --git a/serve.js b/serve.js index 6920028..d21e3e9 100644 --- a/serve.js +++ b/serve.js @@ -76,12 +76,12 @@ if (config.db.host && config.db.host != "") { testQuery = pool.query(`SHOW TABLES;`, (err, res) => { if (err) { - if (logger) logger.error(err); + if (logger) logger.error(`Tried to connect to database: ${err}`); process.exit(); + } else { + if (logger) logger.info(`Database initialized with ${res.length} tables.`); } - - else { if (logger) logger.info(`Database initialized with ${res.length} tables.`); } }); if (logger) logger.info(`Database pool created: host: ${config.db.host}, database: ${config.db.database}.`); diff --git a/session.js b/session.js index 1d86b8b..956ce29 100644 --- a/session.js +++ b/session.js @@ -104,13 +104,6 @@ class Session { removeClient(client_id) { if (this.clients == null) { - this.logErrorSessionClientSocketAction( - this.id, - client_id, - null, - `tried to remove client from session, but this.clients was null` - ); - return false; } @@ -118,13 +111,6 @@ class Session { if (this.clients.length == 0 || this.clients.indexOf(client_id) == -1) { //client_id is not in the array, so we don't need to remove it. - this.logWarningSessionClientSocketAction( - null, - null, - client_id, - `Tried removing client from this.clients, but it was not there.` - ); - return false; } diff --git a/sync.js b/sync.js index 9bf7744..4aa3299 100644 --- a/sync.js +++ b/sync.js @@ -81,6 +81,7 @@ function compareKeys(a, b) { } const SocketIOEvents = { + connection: "connection", disconnect: "disconnect", disconnecting: "disconnecting", error: "error" @@ -116,6 +117,7 @@ const KomodoSendEvents = { message: "message", relayUpdate: "relayUpdate", notifyBump: "bump", + notifyNotInSession: "notifyNotInSession", }; const KomodoMessages = { @@ -1136,29 +1138,25 @@ module.exports = { return true; } - this.successfullyJoinedAction(session_id, client_id); + this.successfullyJoinedAction(session_id, client_id, socket); return true; }, - makeSocketLeave: function (session_id, client_id) { - if (!this.requestToLeaveSessionAction) { - this.logWarningSessionClientSocketAction( + tryToRemoveSocketAndClientFromSessionThenNotifyLeft: function (err, session_id, client_id, socket) { + var success; + var reason; + + if (err) { + this.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - `in makeSocketLeave, requestToLeaveSessionAction callback was not provided. Skipping.` + `in tryToRemoveSocketAndClientFromSessionThenNotifyLeft, ${err}` ); return; } - - this.requestToLeaveSessionAction(session_id, client_id); - }, - - tryToRemoveSocketAndClientFromSessionThenNotifyLeft: function (err, session_id, client_id, socket) { - var success; - var reason; if (!this.failedToLeaveAction) { this.logWarningSessionClientSocketAction( @@ -1196,7 +1194,7 @@ module.exports = { err ); - this.failedToLeaveAction(session_id, reason); + this.failedToLeaveAction(session_id, reason, socket); return; } @@ -1206,7 +1204,7 @@ module.exports = { if (!success) { reason = `removeSocketFromSession failed`; - this.failedToLeaveAction(session_id, reason); + this.failedToLeaveAction(session_id, reason, socket); return; } @@ -1216,7 +1214,7 @@ module.exports = { if (!success) { reason = `session.removeClient failed`; - this.failedToLeaveAction(session_id, reason); + this.failedToLeaveAction(session_id, reason, socket); return; } @@ -1232,7 +1230,7 @@ module.exports = { return; } - this.successfullyLeftAction(session_id, client_id); + this.successfullyLeftAction(session_id, client_id, socket); this.logInfoSessionClientSocketAction( session_id, @@ -1449,7 +1447,7 @@ module.exports = { return; } - if (!this.disconnectedAction) { + if (!this.disconnectAction) { this.logWarningSessionClientSocketAction( session_id, client_id, @@ -1458,7 +1456,7 @@ module.exports = { ); } - this.disconnectedAction(socket, session_id, client_id); + this.disconnectAction(socket, session_id, client_id); }, // cleanup socket and client references in session state if reconnect fails @@ -1592,6 +1590,13 @@ module.exports = { }, processReconnectionAttempt: function (err, socket, session_id, client_id) { + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + "Processing reconnection attempt." + ); + this.getOrCreateSession(session_id); let success = this.addSocketAndClientToSession(err, socket, session_id, client_id); @@ -1759,7 +1764,7 @@ module.exports = { )}` ); - this.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(socket, session_id, client_id); + this.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(null, session_id, client_id, socket); this.disconnectSocket(socket, session_id, client_id); @@ -2015,27 +2020,46 @@ module.exports = { return; } - let type = data.type; + let session = this.sessions.get(session_id); - if (!type) { + if (!session) { this.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - "tried to process message, but type was null" + "tried to process message, but session was not found. Creating session and proceeding." ); + session = this.createSession(session_id); //TODO(Brandon): review if we should + return; } - let session = self.sessions.get(session_id); + let isClientInSession = this.isClientInSession(session_id, client_id); - if (!session) { + let { success, isInSession } = this.isSocketInSession(session_id, socket); + + let isSocketInSession = isInSession; + + // check if the incoming packet is from a client who is valid for this session + if ( !isClientInSession || !success || !isSocketInSession ) { + this.logWarningSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but socket or client were not in session. Removing user."); + + this.notifyNotInSessionAction(socket); + + this.disconnectSocket(socket, session_id, client_id); + + this.removeSocketAndClientFromSession(socket, session_id, client_id); + } + + let type = data.type; + + if (!type) { this.logErrorSessionClientSocketAction( session_id, client_id, socket.id, - "tried to process message, but session was not found" + "tried to process message, but type was null" ); return; @@ -2057,11 +2081,6 @@ module.exports = { // NOTE(rob): the following code is copypasta from the old interactionUpdate handler. 7/21/2021 // check if the incoming packet is from a client who is valid for this session - if (!this.isClientInSession(session_id, client_id)) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but client was not in session."); - - return; - } if (!data.message.length) { this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but data.message.length was 0."); @@ -2104,7 +2123,7 @@ module.exports = { } if (type == KomodoMessages.sync.type) { - this.applySyncMessageToState(e, id, latest, render, locked); + this.applySyncMessageToState(session, data); } // data capture @@ -2166,9 +2185,21 @@ module.exports = { socket.emit(KomodoSendEvents.notifyBump, session_id); }; + this.notifyNotInSessionAction = function (socket) { + self.logInfoSessionClientSocketAction( + null, + null, + socket.id, + `Notifying not in session` + ); + + // Let the client know it has been bumped + socket.emit(KomodoSendEvents.notifyNotInSession); + }; + this.makeSocketLeaveSessionAction = function (session_id, socket) { socket.leave(session_id.toString(), (err) => { - this.failedToLeaveAction(session_id, `Failed to leave during bump: ${err}.`); + this.failedToLeaveAction(session_id, `Failed to leave during bump: ${err}.`, socket); }); }; @@ -2193,7 +2224,14 @@ module.exports = { }, 500); // delay half a second and then bump the old socket }; - this.requestToJoinSessionAction = function (session_id, client_id) { + this.requestToJoinSessionAction = function (session_id, client_id, socket) { + self.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `Processing request to join session.` + ); + socket.join(session_id.toString(), (err) => { self.addSocketAndClientToSession( err, @@ -2205,11 +2243,11 @@ module.exports = { }); }; - this.failedToJoinAction = function (session_id, reason) { + this.failedToJoinAction = function (session_id, reason, socket) { socket.emit(KomodoSendEvents.failedToJoin, session_id, reason); }; - this.successfullyJoinedAction = function (session_id, client_id) { + this.successfullyJoinedAction = function (session_id, client_id, socket) { // write join event to database self.writeEventToConnections("connect", session_id, client_id); @@ -2220,17 +2258,17 @@ module.exports = { socket.emit(KomodoSendEvents.successfullyJoined, session_id); }; - this.requestToLeaveSessionAction = function (session_id, client_id) { + this.requestToLeaveSessionAction = function (session_id, client_id, socket) { socket.leave(session_id.toString(), (err) => { self.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(err, session_id, client_id, socket); }); }; - this.failedToLeaveAction = function (session_id, reason) { + this.failedToLeaveAction = function (session_id, reason, socket) { socket.emit(KomodoSendEvents.failedToLeave, session_id, reason); }; - this.successfullyLeftAction = function (session_id, client_id) { + this.successfullyLeftAction = function (session_id, client_id, socket) { // notify others the client has left io.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); @@ -2238,7 +2276,10 @@ module.exports = { socket.emit(KomodoSendEvents.successfullyLeft, session_id); }; - this.disconnectedAction = function (socket, session_id, client_id) { + this.disconnectAction = function (socket, session_id, client_id) { + //disconnect the client + socket.disconnect(); + // notify others the client has disconnected socket .to(session_id.toString()) @@ -2272,8 +2313,15 @@ module.exports = { }); }; + self.logInfoSessionClientSocketAction( + null, + null, + null, + `Sync server started. Waiting for connections...` + ); + // main relay handler - io.on(KomodoReceiveEvents.connection, function (socket) { + io.on(SocketIOEvents.connection, function (socket) { self.logInfoSessionClientSocketAction( null, null, @@ -2321,7 +2369,7 @@ module.exports = { //TODO does this need to be called here???? self.bumpOldSockets(session_id, client_id, socket.id); - if (!this.requestToJoinSessionAction) { + if (!self.requestToJoinSessionAction) { self.logErrorSessionClientSocketAction( session_id, client_id, @@ -2332,7 +2380,7 @@ module.exports = { return; } - this.requestToJoinSessionAction(session_id, client_id); + self.requestToJoinSessionAction(session_id, client_id, socket); }); // When a client requests a state catch-up, send the current session state. Supports versioning. @@ -2401,7 +2449,7 @@ module.exports = { // garbage values that might be passed by devs who are overwriting reserved message events. socket.on(KomodoReceiveEvents.message, function (data) { if (socket == null) { - this.logErrorSessionClientSocketAction( + self.logErrorSessionClientSocketAction( null, null, null, diff --git a/test/test-sync-integration.js b/test/test-sync-integration.js index 68d301f..91b69c5 100644 --- a/test/test-sync-integration.js +++ b/test/test-sync-integration.js @@ -35,7 +35,7 @@ describe("Sync Server: Integration", function (done) { throw Error("An unexpected disconnect occurred."); }; - syncServer.requestToJoinSessionAction = function (session_id, client_id) { + syncServer.requestToJoinSessionAction = function (session_id, client_id, socket) { session_id.should.equal(SESSION_ID); client_id.should.equal(CLIENT_ID); From df01545ebe91aa84920f3f77f753c82dcc686662 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 18 Oct 2021 01:44:52 -0500 Subject: [PATCH 13/24] successfully finished previous WIPs; introduced socket-activity-monitor and socket-repair-center --- socket-activity-monitor.js | 60 ++++++++ socket-repair-center.js | 84 +++++++++++ sync.js | 288 ++++++++++++++++++++++++++++--------- 3 files changed, 363 insertions(+), 69 deletions(-) create mode 100644 socket-activity-monitor.js create mode 100644 socket-repair-center.js diff --git a/socket-activity-monitor.js b/socket-activity-monitor.js new file mode 100644 index 0000000..7453050 --- /dev/null +++ b/socket-activity-monitor.js @@ -0,0 +1,60 @@ +// University of Illinois/NCSA +// Open Source License +// http://otm.illinois.edu/disclose-protect/illinois-open-source-license + +// Copyright (c) 2020 Grainger Engineering Library Information Center. All rights reserved. + +// Developed by: IDEA Lab +// Grainger Engineering Library Information Center - University of Illinois Urbana-Champaign +// https://library.illinois.edu/enx + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal with +// the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to +// do so, subject to the following conditions: +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimers. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimers in the documentation +// and/or other materials provided with the distribution. +// * Neither the names of IDEA Lab, Grainger Engineering Library Information Center, +// nor the names of its contributors may be used to endorse or promote products +// derived from this Software without specific prior written permission. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +// SOFTWARE. + +/* jshint esversion: 6 */ + +require('./session.js'); + +class SocketActivityMonitor { + constructor() { + this.socketTimes = new Map(); // socket ID -> timestamp + } + + addOrUpdateTime(socketId) { + this.updateTime(socketId); + } + + updateTime(socketId) { + this.socketTimes.set(socketId, Date.now()); + } + + getDeltaTime(socketId) { + return Date.now() - this.socketTimes[socketId]; + } + + remove(socketId) { + this.socketTimes.delete(socketId); + } +} + +module.exports = SocketActivityMonitor; \ No newline at end of file diff --git a/socket-repair-center.js b/socket-repair-center.js new file mode 100644 index 0000000..759e692 --- /dev/null +++ b/socket-repair-center.js @@ -0,0 +1,84 @@ +// University of Illinois/NCSA +// Open Source License +// http://otm.illinois.edu/disclose-protect/illinois-open-source-license + +// Copyright (c) 2020 Grainger Engineering Library Information Center. All rights reserved. + +// Developed by: IDEA Lab +// Grainger Engineering Library Information Center - University of Illinois Urbana-Champaign +// https://library.illinois.edu/enx + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal with +// the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to +// do so, subject to the following conditions: +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimers. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimers in the documentation +// and/or other materials provided with the distribution. +// * Neither the names of IDEA Lab, Grainger Engineering Library Information Center, +// nor the names of its contributors may be used to endorse or promote products +// derived from this Software without specific prior written permission. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +// SOFTWARE. + +/* jshint esversion: 6 */ + +require('./session.js'); + +require('./socket-activity-monitor.js'); + +class SocketRepairCenter { + constructor(minRepairWaitTime, sessionManager, socketIOActionManager, socketActivityMonitor, logger) { + this.minRepairWaitTime = minRepairWaitTime; + + this.sessionManager = sessionManager; + + this.socketIOActionManager = socketIOActionManager; + + this.socketActivityMonitor = socketActivityMonitor; + + this.logger = logger; + + this.sockets = new Map(); // socket IDs -> sockets + } + + // Accept a new socket needing to be repaired. + add (socket) { + this.sockets.set(socket.id, socket); + } + + // Set the socket free, to be with the session manager. + remove (socket) { + this.sockets.delete(socket); + } + + hasSocket (socket) { + return this.sockets.has(socket.id); + } + + repairEligibleSockets () { + for (let [id, socket] of this.sockets.entries()) { + let deltaTime = this.socketActivityMonitor.getDeltaTime(id); + + if (deltaTime > minRepairWaitTime) { + this.logger.logInfoSessionClientSocketAction(session_id, client_id, id, `Repairing...`); + + this.sessionManager.repair(socket, session_id, client_id); + + this.remove(socket); + } + } + } +} + +module.exports = SocketRepairCenter; \ No newline at end of file diff --git a/sync.js b/sync.js index 4aa3299..3ce925a 100644 --- a/sync.js +++ b/sync.js @@ -41,9 +41,14 @@ const fs = require("fs"); const path = require("path"); const util = require("util"); + const { syslog } = require("winston/lib/winston/config"); + const Session = require("./session"); +const SocketRepairCenter = require("./socket-repair-center"); +const SocketActivityMonitor = require("./socket-activity-monitor"); + // event data globals // NOTE(rob): deprecated. // const POS_FIELDS = 14; @@ -117,7 +122,7 @@ const KomodoSendEvents = { message: "message", relayUpdate: "relayUpdate", notifyBump: "bump", - notifyNotInSession: "notifyNotInSession", + rejectUser: "rejectUser", }; const KomodoMessages = { @@ -1071,7 +1076,7 @@ module.exports = { session_id, client_id, null, - reason + `Failed to join: ${reason}` ); // don't call failedToJoinAction here because we don't have a socket. @@ -1086,7 +1091,7 @@ module.exports = { session_id, client_id, socket.id, - reason + `Failed to join: ${reason}` ); this.failedToJoinAction(session_id, reason); @@ -1105,7 +1110,7 @@ module.exports = { session_id, client_id, socket.id, - reason + `Failed to join: ${reason}` ); this.failedToJoinAction(session_id, reason); @@ -1138,6 +1143,13 @@ module.exports = { return true; } + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + "Successfully joined." + ); + this.successfullyJoinedAction(session_id, client_id, socket); return true; @@ -1737,7 +1749,7 @@ module.exports = { null, null, socket.id, - `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction, removeSocketAndClientFromSession, and/or DisconnectSocket.)` + `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction, removeSocketAndClientFromSession, and/or DisconnectSocket.` ); return true; @@ -1792,6 +1804,10 @@ module.exports = { initGlobals: function () { this.sessions = new Map(); + + this.socketActivityMonitor = new SocketActivityMonitor(); + + this.socketRepairCenter = new SocketRepairCenter(200, this, this, this, this); }, // Check if message payload is pre-parsed. @@ -1984,7 +2000,7 @@ module.exports = { } }, - processMessage: function (data, socket) { + getMetadataFromMessage: function (data) { if (data == null) { this.logErrorSessionClientSocketAction( null, @@ -1992,6 +2008,12 @@ module.exports = { socket.id, "tried to process message, but data was null" ); + + return { + success: false, + session_id: null, + client_id: null, + }; } let session_id = data.session_id; @@ -2004,7 +2026,11 @@ module.exports = { "tried to process message, but session_id was null" ); - return; + return { + success: false, + session_id: null, + client_id: data.client_id, + }; } let client_id = data.client_id; @@ -2017,44 +2043,198 @@ module.exports = { "tried to process message, but client_id was null" ); - return; + return { + success: false, + session_id: data.session_id, + client_id: null, + }; } - let session = this.sessions.get(session_id); + return { + success: true, + session_id: data.session_id, + client_id: data.client_id, + }; + }, - if (!session) { - this.logErrorSessionClientSocketAction( - session_id, + isSocketInRoom: function (socket, session_id) { + if (!socket) { + this.logInfoSessionClientSocketAction(session_id, client_id, socket.id, - "tried to process message, but session was not found. Creating session and proceeding." + "isSocketInRoom: socket was null." ); + } - session = this.createSession(session_id); //TODO(Brandon): review if we should + if (!socket.rooms) { + this.logInfoSessionClientSocketAction(session_id, + client_id, + socket.id, + "isSocketInRoom: socket.rooms was null." + ); + } - return; + let roomIds = Object.keys(socket.rooms); + + return roomIds.includes(session_id); + }, + + rejectUser: function (socket, reason) { + socketRepairCenter.set(socket.id, socket); + + if (!this.rejectUserAction) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + "in rejectUser, no rejectUserAction callback was provided." + ); + + return; + } + + this.rejectUserAction(socket, reason); + + this.disconnectSocket(socket, session_id, client_id); + + this.removeSocketAndClientFromSession(socket, session_id, client_id); + }, + + // Returns true iff we have previously cached a record for a user without a session. + isSocketInSocketRepairCenter: function (socket) { + return socket.id in this.socketRepairCenter; + }, + + applyMessageToState: function (data, type, message, session_id, client_id, socket) { + // get reference to session and parse message payload for state updates, if needed. + if (type == KomodoMessages.interaction.type) { + if (message.length < KomodoMessages.interaction.minLength) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: data.message.length was incorrect"); + + return; + } + + let source_id = message[KomodoMessages.interaction.indices.sourceId]; + + if (source_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: source_id was null"); + } + + let target_id = message[KomodoMessages.interaction.indices.targetId]; + + if (target_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: target_id was null"); + } + + let interaction_type = message[KomodoMessages.interaction.indices.interactionType]; + + if (interaction_type == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: interaction_type was null"); + } + + this.applyInteractionMessageToState(session, target_id, interaction_type); } - let isClientInSession = this.isClientInSession(session_id, client_id); + if (data.type == KomodoMessages.sync.type) { + this.applySyncMessageToState(session, data); + } + }, + + repair: function (socket, session_id, client_id) { + addClientToSessionIfNeeded(session_id, client_id); + + addSocketToSessionIfNeeded(socket, session_id); + + joinSocketToRoomIfNeeded(socket, session_id); + }, + + addClientToSessionIfNeeded: function (session_id, client_id) { + let session = this.sessionManager.getSession(session_id); + + if (!this.sessionManager.isClientInSession(session_id, client_id)) { + this.logger.logInfoSessionClientSocketAction(session_id, + client_id, + socket.id, + "User had a socket but client is not in session. Adding client and proceeding." + ); + + session.addClient(client_id); + + // TODO: consider doing this.rejectUserAction(socket, "User has a socket but client is not in session."); + } + }, + + // check if the incoming packet is from a client who is valid for this session + addSocketToSessionIfNeeded: function (socket, session_id) { + let socketResult = this.sessionManager.isSocketInSession(session_id, socket); + + let session = this.sessionManager.getSession(session_id); + + if (!socketResult.success || !socketResult.isInSession ) { + this.logger.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + "User had a socket but socket is not in session. Adding socket and proceeding." + ); + + session.addSocket(socket, client_id); + + // TODO: consider doing this.rejectUserAction(socket, "User has a socket but socket is not in session."); + } + }, - let { success, isInSession } = this.isSocketInSession(session_id, socket); + // Check if the socket is in a SocketIO room. + joinSocketToRoomIfNeeded: function (socket, session_id) { + if(!this.sessionManager.isSocketInRoom(socket, session_id)) { + this.logger.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + "User had a socket but socket is not joined to SocketIO room. Joining socket and proceeding." + ); - let isSocketInSession = isInSession; + this.socketIOActionManager.joinSocketToRoomAction(session_id, socket); - // check if the incoming packet is from a client who is valid for this session - if ( !isClientInSession || !success || !isSocketInSession ) { - this.logWarningSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but socket or client were not in session. Removing user."); + // TODO: consider doing this.rejectUserAction(socket, "User has a socket but socket is not in session."); + } + }, - this.notifyNotInSessionAction(socket); + processMessage: function (data, socket) { + this.socketActivityMonitor.updateTime(socket.id); - this.disconnectSocket(socket, session_id, client_id); + this.socketRepairCenter.repairEligibleSockets(); - this.removeSocketAndClientFromSession(socket, session_id, client_id); + // Don't process a message for a socket... + // * whose socket record isn't in the session + // * whose client record isn't in the session + // * who isn't joined to the (SocketIO) room + if (this.socketRepairCenter.hasSocket(socket)) { + return; } - let type = data.type; + let { success, session_id, client_id } = this.getMetadataFromMessage(data); - if (!type) { + if (!success) { + return; + } + + let session = this.sessions.get(session_id); + + if (!session) { + this.logErrorSessionClientSocketAction( + session_id, + client_id, + socket.id, + "tried to process message, but session was not found. Creating session and proceeding." + ); + + session = this.createSession(session_id); //TODO(Brandon): review if we should + + return; + } + + if (!data.type) { this.logErrorSessionClientSocketAction( session_id, client_id, @@ -2077,60 +2257,26 @@ module.exports = { } // `message` here will be in the legacy packed-array format. - - // NOTE(rob): the following code is copypasta from the old interactionUpdate handler. 7/21/2021 - - // check if the incoming packet is from a client who is valid for this session + + // relay the message + this.messageAction(socket, session_id, data); if (!data.message.length) { this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "tried to process message, but data.message.length was 0."); return; } - - // relay the message - this.messageAction(socket, session_id, data); data.message = this.parseMessageIfNeeded(data, session_id, client_id); - // get reference to session and parse message payload for state updates, if needed. - if (type == KomodoMessages.interaction.type) { - if (data.message.length != KomodoMessages.interaction.minLength) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: data.message.length was incorrect"); - - return; - } - - let source_id = data.message[KomodoMessages.interaction.indices.sourceId]; - - if (source_id == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: source_id was null"); - } - - let target_id = data.message[KomodoMessages.interaction.indices.targetId]; - - if (target_id == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: target_id was null"); - } - - let interaction_type = data.message[KomodoMessages.interaction.indices.interactionType]; - - if (interaction_type == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: interaction_type was null"); - } - - this.applyInteractionMessageToState(session, target_id, interaction_type); - } - - if (type == KomodoMessages.sync.type) { - this.applySyncMessageToState(session, data); - } + this.applyMessageToState(data, data.type, data.message, session_id, client_id, socket); // data capture if (session.isRecording) { this.record_message_data(data); } }, + init: function (io, pool, logger) { this.initGlobals(); @@ -2185,16 +2331,16 @@ module.exports = { socket.emit(KomodoSendEvents.notifyBump, session_id); }; - this.notifyNotInSessionAction = function (socket) { + this.rejectUserAction = function (socket, reason) { self.logInfoSessionClientSocketAction( null, null, socket.id, - `Notifying not in session` + `Rejecting` ); // Let the client know it has been bumped - socket.emit(KomodoSendEvents.notifyNotInSession); + socket.emit(KomodoSendEvents.rejectUser, reason); }; this.makeSocketLeaveSessionAction = function (session_id, socket) { @@ -2243,6 +2389,10 @@ module.exports = { }); }; + this.joinSocketToRoomAction = function (session_id, socket) { + socket.join(session_id.toString()); + }; + this.failedToJoinAction = function (session_id, reason, socket) { socket.emit(KomodoSendEvents.failedToJoin, session_id, reason); }; @@ -2313,7 +2463,7 @@ module.exports = { }); }; - self.logInfoSessionClientSocketAction( + this.logInfoSessionClientSocketAction( null, null, null, From c11137b4f7f04aeafd745881ed26dbd74b3940cb Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 18 Oct 2021 02:59:41 -0500 Subject: [PATCH 14/24] successfully implemented first version of socket repair center --- socket-activity-monitor.js | 2 +- socket-repair-center.js | 16 ++++-- sync.js | 109 ++++++++++++++++++++++++------------- 3 files changed, 83 insertions(+), 44 deletions(-) diff --git a/socket-activity-monitor.js b/socket-activity-monitor.js index 7453050..10a52ed 100644 --- a/socket-activity-monitor.js +++ b/socket-activity-monitor.js @@ -49,7 +49,7 @@ class SocketActivityMonitor { } getDeltaTime(socketId) { - return Date.now() - this.socketTimes[socketId]; + return Date.now() - this.socketTimes.get(socketId); } remove(socketId) { diff --git a/socket-repair-center.js b/socket-repair-center.js index 759e692..93c3b86 100644 --- a/socket-repair-center.js +++ b/socket-repair-center.js @@ -54,27 +54,35 @@ class SocketRepairCenter { // Accept a new socket needing to be repaired. add (socket) { + this.logger.logInfoSessionClientSocketAction(null, null, socket.id, `Added socket to repair center.`); + this.sockets.set(socket.id, socket); } // Set the socket free, to be with the session manager. remove (socket) { - this.sockets.delete(socket); + this.sockets.delete(socket.id); } hasSocket (socket) { return this.sockets.has(socket.id); } - repairEligibleSockets () { + repairSocketIfEligible (socket, session_id, client_id) { + // this.logger.logInfoSessionClientSocketAction(null, null, null, `deltaTime: ${this.sockets.size}`); + for (let [id, socket] of this.sockets.entries()) { let deltaTime = this.socketActivityMonitor.getDeltaTime(id); - if (deltaTime > minRepairWaitTime) { - this.logger.logInfoSessionClientSocketAction(session_id, client_id, id, `Repairing...`); + // this.logger.logInfoSessionClientSocketAction(null, null, id, `deltaTime: ${deltaTime}`); + + if (deltaTime > this.minRepairWaitTime) { + this.logger.logInfoSessionClientSocketAction(null, null, id, `Repair user: ...`); this.sessionManager.repair(socket, session_id, client_id); + this.socketActivityMonitor.updateTime(socket.id); + this.remove(socket); } } diff --git a/sync.js b/sync.js index 3ce925a..7ef7a35 100644 --- a/sync.js +++ b/sync.js @@ -1407,6 +1407,10 @@ module.exports = { isClientInSession: function (session_id, client_id) { let { success, session } = this.getSession(session_id); + + if (!success) { + return false; + } return session.hasClient(client_id); }, @@ -1594,7 +1598,7 @@ module.exports = { `Creating session: ${session_id}` ); - this.sessions.set(session_id, new Session()); + this.sessions.set(session_id, new Session(session_id)); session = this.sessions.get(session_id); @@ -1756,14 +1760,27 @@ module.exports = { } if (this.doTryReconnecting(reason)) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + `Trying to reconnect and rejoin user after ${reason}` + ); + // Try to reconnect the socket - return this.reconnectAction( + let success = this.reconnectAction( reason, socket, session_id, client_id, session ); + + if (!success) { + this.socketActivityMonitor.addOrUpdate(socket.id); + + this.socketRepairCenter.add(socket); + } } // Disconnect the socket @@ -1807,7 +1824,7 @@ module.exports = { this.socketActivityMonitor = new SocketActivityMonitor(); - this.socketRepairCenter = new SocketRepairCenter(200, this, this, this, this); + this.socketRepairCenter = new SocketRepairCenter(2000, this, this, this.socketActivityMonitor, this); }, // Check if message payload is pre-parsed. @@ -2141,21 +2158,22 @@ module.exports = { }, repair: function (socket, session_id, client_id) { - addClientToSessionIfNeeded(session_id, client_id); + let session = this.getOrCreateSession(session_id); - addSocketToSessionIfNeeded(socket, session_id); + this.addClientToSessionIfNeeded(socket, session, client_id); - joinSocketToRoomIfNeeded(socket, session_id); - }, + this.addSocketToSessionIfNeeded(socket, session, client_id); - addClientToSessionIfNeeded: function (session_id, client_id) { - let session = this.sessionManager.getSession(session_id); + this.joinSocketToRoomIfNeeded(socket, session); + }, - if (!this.sessionManager.isClientInSession(session_id, client_id)) { - this.logger.logInfoSessionClientSocketAction(session_id, - client_id, - socket.id, - "User had a socket but client is not in session. Adding client and proceeding." + addClientToSessionIfNeeded: function (socket, session, client_id) { + if (!session.hasClient(client_id)) { + this.logInfoSessionClientSocketAction( + session.getId(), + client_id, + socket.id, + " - Client is not in session. Adding client and proceeding." ); session.addClient(client_id); @@ -2165,17 +2183,15 @@ module.exports = { }, // check if the incoming packet is from a client who is valid for this session - addSocketToSessionIfNeeded: function (socket, session_id) { - let socketResult = this.sessionManager.isSocketInSession(session_id, socket); - - let session = this.sessionManager.getSession(session_id); + addSocketToSessionIfNeeded: function (socket, session, client_id) { + let socketResult = session.hasSocket(socket); - if (!socketResult.success || !socketResult.isInSession ) { - this.logger.logInfoSessionClientSocketAction( - session_id, + if (!socketResult) { + this.logInfoSessionClientSocketAction( + session.id, client_id, socket.id, - "User had a socket but socket is not in session. Adding socket and proceeding." + " - Socket is not in session. Adding socket and proceeding." ); session.addSocket(socket, client_id); @@ -2185,25 +2201,29 @@ module.exports = { }, // Check if the socket is in a SocketIO room. - joinSocketToRoomIfNeeded: function (socket, session_id) { - if(!this.sessionManager.isSocketInRoom(socket, session_id)) { - this.logger.logInfoSessionClientSocketAction( - session_id, - client_id, + joinSocketToRoomIfNeeded: function (socket, session) { + if(!this.isSocketInRoom(socket, session.getId())) { + this.logInfoSessionClientSocketAction( + session.getId(), + null, socket.id, - "User had a socket but socket is not joined to SocketIO room. Joining socket and proceeding." + " - Socket is not joined to SocketIO room. Joining socket and proceeding." ); - this.socketIOActionManager.joinSocketToRoomAction(session_id, socket); + this.joinSocketToRoomAction(session.getId(), socket); // TODO: consider doing this.rejectUserAction(socket, "User has a socket but socket is not in session."); } }, processMessage: function (data, socket) { - this.socketActivityMonitor.updateTime(socket.id); + let { success, session_id, client_id } = this.getMetadataFromMessage(data); - this.socketRepairCenter.repairEligibleSockets(); + if (!success) { + return; + } + + this.socketRepairCenter.repairSocketIfEligible(socket, session_id, client_id); // Don't process a message for a socket... // * whose socket record isn't in the session @@ -2212,12 +2232,8 @@ module.exports = { if (this.socketRepairCenter.hasSocket(socket)) { return; } - - let { success, session_id, client_id } = this.getMetadataFromMessage(data); - - if (!success) { - return; - } + + this.socketActivityMonitor.updateTime(socket.id); let session = this.sessions.get(session_id); @@ -2390,7 +2406,16 @@ module.exports = { }; this.joinSocketToRoomAction = function (session_id, socket) { - socket.join(session_id.toString()); + socket.join(session_id.toString(), (err) => { + if (err) { + self.logErrorSessionClientSocketAction( + session_id, + null, + socket.id, + `Tried to join socket to SocketIO room. Error: ${err}` + ); + } + }); }; this.failedToJoinAction = function (session_id, reason, socket) { @@ -2476,9 +2501,15 @@ module.exports = { null, null, socket.id, - `Session connection` + `Connected to main (sync) namespace` ); + self.socketRepairCenter.add(socket); + + self.socketActivityMonitor.updateTime(socket.id); + + // self.socketRepairCenter.repairSocketIfEligible(); + socket.on(KomodoReceiveEvents.sessionInfo, function (session_id) { let session = self.sessions.get(session_id); From cd51a5b6baf645ab516de315a00d60f784b2e905 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 18 Oct 2021 03:34:41 -0500 Subject: [PATCH 15/24] push state catch-up upon successful repair --- sync.js | 130 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/sync.js b/sync.js index 7ef7a35..2525594 100644 --- a/sync.js +++ b/sync.js @@ -73,6 +73,8 @@ const INTERACTION_LOCK_END = 9; const SYNC_OBJECTS = 3; +const STATE_VERSION = 2; + //TODO refactor this.sessions into instances of the Session object. // Courtesy of Casey Foster on Stack Overflow @@ -883,51 +885,7 @@ module.exports = { } }, - handleState: function (socket, data) { - if (!socket) { - this.logErrorSessionClientSocketAction( - null, - null, - null, - `tried to handle state, but socket was null` - ); - - return { session_id: -1, state: null }; - } - - if (!data) { - this.logErrorSessionClientSocketAction( - null, - null, - socket.id, - `tried to handle state, but data was null` - ); - - return { session_id: -1, state: null }; - } - - let session_id = data.session_id; - - let client_id = data.client_id; - - this.logInfoSessionClientSocketAction( - session_id, - client_id, - socket.id, - `State: ${JSON.stringify(data)}` - ); - - if (!session_id || !client_id) { - this.connectionAuthorizationErrorAction( - socket, - "You must provide a session ID and a client ID in the URL options." - ); - - return { session_id: -1, state: null }; - } - - let version = data.version; - + getState: function (session_id, version) { let session = this.sessions.get(session_id); if (!session) { @@ -973,7 +931,58 @@ module.exports = { }; } - return { session_id, state }; + return state; + }, + + handleStateCatchupRequest: function (socket, data) { + if (!socket) { + this.logErrorSessionClientSocketAction( + null, + null, + null, + `tried to handle state, but socket was null` + ); + + return { session_id: -1, state: null }; + } + + if (!data) { + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + `tried to handle state, but data was null` + ); + + return { session_id: -1, state: null }; + } + + let session_id = data.session_id; + + let client_id = data.client_id; + + let version = data.version; + + this.logInfoSessionClientSocketAction( + session_id, + client_id, + socket.id, + `State: ${JSON.stringify(data)}` + ); + + if (!session_id || !client_id) { + this.connectionAuthorizationErrorAction( + socket, + "You must provide a session ID and a client ID in the URL options." + ); + + return { session_id: -1, state: null }; + } + + return { + session_id: session_id, + state: this.getState(session_id, version) + }; }, // returns true on success and false on failure @@ -2165,6 +2174,28 @@ module.exports = { this.addSocketToSessionIfNeeded(socket, session, client_id); this.joinSocketToRoomIfNeeded(socket, session); + + let result = this.handleStateCatchupRequest( + socket, + { + session_id: session_id, + client_id: client_id, + version: STATE_VERSION + } + ); + + if (result.session_id == -1 || !result.state) { + this.logWarningSessionClientSocketAction( + result.session_id, + client_id, + socket.id, + "repair: state was null" + ); + + return; + } + + this.sendStateCatchUpAction(socket, result.state); }, addClientToSessionIfNeeded: function (socket, session, client_id) { @@ -2488,6 +2519,10 @@ module.exports = { }); }; + this.sendStateCatchUpAction = function (socket, state) { + socket.emit(KomodoSendEvents.state, state); + }; + this.logInfoSessionClientSocketAction( null, null, @@ -2566,7 +2601,7 @@ module.exports = { // When a client requests a state catch-up, send the current session state. Supports versioning. socket.on(KomodoReceiveEvents.requestOwnStateCatchup, function (data) { - let { session_id, state } = self.handleState(socket, data); + let { session_id, state } = self.handleStateCatchupRequest(socket, data); if (session_id == -1 || !state) { self.logWarningSessionClientSocketAction( @@ -2579,6 +2614,7 @@ module.exports = { return; } + //TODO -- refactor this so that sendStateCatchupAction gets called within handleStateCatchupRequest or something like that. try { // emit versioned state data socket.emit(KomodoSendEvents.state, state); // Behavior as of 10/7/21: Sends the state only to the client who requested it. From 3ea10ff1127dc4c51a058bec5ceab80c03baf1f5 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 18 Oct 2021 03:55:12 -0500 Subject: [PATCH 16/24] exclude own socket from successful join and successful leave events --- sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync.js b/sync.js index 2525594..f1e23b3 100644 --- a/sync.js +++ b/sync.js @@ -2458,7 +2458,7 @@ module.exports = { self.writeEventToConnections("connect", session_id, client_id); // tell other clients that a client joined - io.to(session_id.toString()).emit(KomodoSendEvents.clientJoined, client_id); + socket.to(session_id.toString()).emit(KomodoSendEvents.clientJoined, client_id); // tell the joining client that they successfully joined socket.emit(KomodoSendEvents.successfullyJoined, session_id); @@ -2476,7 +2476,7 @@ module.exports = { this.successfullyLeftAction = function (session_id, client_id, socket) { // notify others the client has left - io.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); + socket.to(session_id.toString()).emit(KomodoSendEvents.left, client_id); // tell the leaving client that they successfully left socket.emit(KomodoSendEvents.successfullyLeft, session_id); From caa51a6aea0d4d012321ec179a37eca44897b6dd Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 18 Oct 2021 15:17:43 -0500 Subject: [PATCH 17/24] fix bug with getState --- sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync.js b/sync.js index f1e23b3..db2b8a0 100644 --- a/sync.js +++ b/sync.js @@ -885,7 +885,7 @@ module.exports = { } }, - getState: function (session_id, version) { + getState: function (socket, session_id, version) { let session = this.sessions.get(session_id); if (!session) { @@ -981,7 +981,7 @@ module.exports = { return { session_id: session_id, - state: this.getState(session_id, version) + state: this.getState(socket, session_id, version) }; }, From b7e1cc85e3c8b0d6a8f906275ac20c488a4a6eb1 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Tue, 19 Oct 2021 02:14:06 -0500 Subject: [PATCH 18/24] breaking change: clients must connect to sync namespace; fix: socketRepairCenter only checks one socket now --- admin.js | 18 ++--- chat.js | 6 +- serve.js | 6 +- socket-repair-center.js | 21 +++--- sync.js | 143 ++++++++++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 56 deletions(-) diff --git a/admin.js b/admin.js index 6eeacc8..460a8ef 100644 --- a/admin.js +++ b/admin.js @@ -43,14 +43,16 @@ function JSONStringifyCircular(obj) { if (seen.has(value)) { return; } + seen.add(value); } + return value; }); } module.exports = { - init: function (io, syncServer, chatServer) { + init: function (io, logger, syncServer, chatServer) { var admin = io.of('/admin'); admin.use((socket, next) => { @@ -59,10 +61,11 @@ module.exports = { }); admin.on('connection', function(socket) { //TODO finish or remove. + // TODO(Brandon): log connection here + socket.emit("adminInfo", socket.id); socket.on('getAllSessions0', function() { - this.sessions = syncServer.sessions; socket.emit('receiveAllSessions0', JSONStringifyCircular(Array.from(sessions.entries()))); @@ -94,7 +97,6 @@ module.exports = { }); socket.on('sessionsWithDetails', function () { - this.sessions = syncServer.sessions; var result = fromEntries(this.sessions); @@ -103,17 +105,14 @@ module.exports = { }); socket.on('stateClientsSockets', function () { - this.sessions = syncServer.sessions; var stateClientsSockets = {}; this.sessions.forEach((session, session_id, map) => { - stateClientsSockets[session_id] = {}; stateClientsSockets[session_id].state = { - clients: session.clients, entities: session.entities, scene: session.scene, @@ -123,7 +122,6 @@ module.exports = { stateClientsSockets[session_id].clientsAndSockets = []; for (var socket_id in session.sockets) { - var client_id = session.sockets[socket_id].client_id; stateClientsSockets[session_id].clientsAndSockets.push(`${client_id} - ${socket_id}`); @@ -149,13 +147,11 @@ module.exports = { var sockets = io.of("/").sockets; for (var socket_id in sockets) { - let curSocketObj = sockets[socket_id]; socketsAndRooms[socket_id] = []; for (var room_id in curSocketObj.rooms) { - socketsAndRooms[socket_id].push(room_id); } } @@ -179,5 +175,9 @@ module.exports = { socket.emit('clients', JSON.stringify(sessionToClient)); }); }); + + logger.info(`Admin namespace is waiting for connections...`); + + return admin; } }; \ No newline at end of file diff --git a/chat.js b/chat.js index b74dc30..cc2c9c9 100644 --- a/chat.js +++ b/chat.js @@ -45,7 +45,7 @@ module.exports = { var chat = io.of('/chat'); chat.on('connection', function(socket) { - if (logger) logger.info(`Chat connection: ${socket.id}`); + // TODO(Brandon): log connection here // setup text chat relay socket.on('micText', function(data) { @@ -142,5 +142,9 @@ module.exports = { } }); }); + + logger.info(`Chat namespace is waiting for connections...`); + + return chat; } }; diff --git a/serve.js b/serve.js index d21e3e9..9e84d66 100644 --- a/serve.js +++ b/serve.js @@ -97,8 +97,8 @@ io.listen(PORT, { if (logger) logger.info(`Komodo relay is running on :${PORT}`); -syncServer.init(io, pool, logger); +var chatNamespace = chatServer.init(io, logger); -chatServer.init(io, logger); +var adminNamespace = adminServer.init(io, logger, syncServer, chatServer); -adminServer.init(io, syncServer, chatServer); \ No newline at end of file +syncServer.init(io, pool, logger, chatNamespace, adminNamespace); \ No newline at end of file diff --git a/socket-repair-center.js b/socket-repair-center.js index 93c3b86..5b26d44 100644 --- a/socket-repair-center.js +++ b/socket-repair-center.js @@ -69,22 +69,23 @@ class SocketRepairCenter { } repairSocketIfEligible (socket, session_id, client_id) { - // this.logger.logInfoSessionClientSocketAction(null, null, null, `deltaTime: ${this.sockets.size}`); + if (!this.hasSocket(socket)) { + return; + } - for (let [id, socket] of this.sockets.entries()) { - let deltaTime = this.socketActivityMonitor.getDeltaTime(id); + // this.logger.logInfoSessionClientSocketAction(null, null, null, `deltaTime: ${this.sockets.size}`); + let deltaTime = this.socketActivityMonitor.getDeltaTime(socket.id); - // this.logger.logInfoSessionClientSocketAction(null, null, id, `deltaTime: ${deltaTime}`); + // this.logger.logInfoSessionClientSocketAction(null, null, id, `deltaTime: ${deltaTime}`); - if (deltaTime > this.minRepairWaitTime) { - this.logger.logInfoSessionClientSocketAction(null, null, id, `Repair user: ...`); + if (deltaTime > this.minRepairWaitTime) { + this.logger.logInfoSessionClientSocketAction(null, null, socket.id, `Repair user: ...`); - this.sessionManager.repair(socket, session_id, client_id); + this.sessionManager.repair(socket, session_id, client_id); - this.socketActivityMonitor.updateTime(socket.id); + this.socketActivityMonitor.updateTime(socket.id); - this.remove(socket); - } + this.remove(socket); } } } diff --git a/sync.js b/sync.js index db2b8a0..5e92a1e 100644 --- a/sync.js +++ b/sync.js @@ -48,6 +48,7 @@ const Session = require("./session"); const SocketRepairCenter = require("./socket-repair-center"); const SocketActivityMonitor = require("./socket-activity-monitor"); +const chat = require("./chat"); // event data globals // NOTE(rob): deprecated. @@ -75,6 +76,8 @@ const SYNC_OBJECTS = 3; const STATE_VERSION = 2; +const SYNC_NAMESPACE = "/sync"; + //TODO refactor this.sessions into instances of the Session object. // Courtesy of Casey Foster on Stack Overflow @@ -967,7 +970,7 @@ module.exports = { session_id, client_id, socket.id, - `State: ${JSON.stringify(data)}` + `Received state catch-up request, version ${data.version}` ); if (!session_id || !client_id) { @@ -1655,6 +1658,44 @@ module.exports = { return true; }, + // Remove this function once we upgrade the server and client to use the "/sync" namespace + isInChatNamespace: function (socket) { + if (!socket) { + return false; + } + + if (!this.chatNamespace) { + return false; + } + + let connectedIds = Object.keys(this.chatNamespace.connected); + + if (connectedIds == null) { + return false; + } + + return connectedIds.includes(`${this.chatNamespace.name}#${socket.id}`); + }, + + // Remove this function once we upgrade the server and client to use the "/sync" namespace + isInAdminNamespace: function (socket) { + if (!socket) { + return false; + } + + if (!this.chatNamespace) { + return false; + } + + let connectedIds = Object.keys(this.adminNamespace.connected); + + if (connectedIds == null) { + return false; + } + + return connectedIds.includes(`${this.adminNamespace.name}#${socket.id}`); + }, + isSocketInSession: function (session_id, socket) { if (!socket) { this.logWarningSessionClientSocketAction( @@ -1729,6 +1770,9 @@ module.exports = { DisconnectKnownReasons[reason].doReconnect); }, + handleDisconnecting: function(socket, reason) { + }, + // Returns true if socket is still connected handleDisconnect: function (socket, reason) { if (!socket) { @@ -1742,6 +1786,13 @@ module.exports = { return false; } + this.logInfoSessionClientSocketAction( + null, + null, + socket.id, + `Disconnecting.` + ); + if (!this.reconnectAction) { this.logErrorSessionClientSocketAction( null, @@ -1756,13 +1807,25 @@ module.exports = { // Check disconnect event reason and handle const { session_id, client_id } = this.whoDisconnected(socket); - if (session_id == null || client_id == null) { + if (session_id == null) { //socket not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. this.logInfoSessionClientSocketAction( null, null, socket.id, - `disconnected. Not found in sessions. Probably ok. Skipping reconnectAction, removeSocketAndClientFromSession, and/or DisconnectSocket.` + `- session_id not found.` + ); + + return true; + } + + if (client_id == null) { + //client not found in our records. This will happen for komodo-unity versions v0.3.2 and below, which handle "sync" actions on the main server namespace. + this.logInfoSessionClientSocketAction( + null, + null, + socket.id, + `- client_id not found.` ); return true; @@ -1773,7 +1836,7 @@ module.exports = { null, null, socket.id, - `Trying to reconnect and rejoin user after ${reason}` + `- trying to reconnect and rejoin user. Reason: ${reason}` ); // Try to reconnect the socket @@ -1789,17 +1852,27 @@ module.exports = { this.socketActivityMonitor.addOrUpdate(socket.id); this.socketRepairCenter.add(socket); + + return false; } + + return true; } - // Disconnect the socket - this.logInfoSessionClientSocketAction( + this.logErrorSessionClientSocketAction( + null, + null, + socket.id, + `- not trying to reconnect user. Reason: ${reason}` + ); + + // Try to reconnect the socket + let success = this.reconnectAction( + reason, + socket, session_id, client_id, - socket.id, - `Client was disconnected, probably because an old socket was bumped. Reason: ${reason}, clients: ${JSON.stringify( - session.getClients() - )}` + session ); this.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(null, session_id, client_id, socket); @@ -2026,7 +2099,7 @@ module.exports = { } }, - getMetadataFromMessage: function (data) { + getMetadataFromMessage: function (data, socket) { if (data == null) { this.logErrorSessionClientSocketAction( null, @@ -2126,11 +2199,6 @@ module.exports = { this.removeSocketAndClientFromSession(socket, session_id, client_id); }, - // Returns true iff we have previously cached a record for a user without a session. - isSocketInSocketRepairCenter: function (socket) { - return socket.id in this.socketRepairCenter; - }, - applyMessageToState: function (data, type, message, session_id, client_id, socket) { // get reference to session and parse message payload for state updates, if needed. if (type == KomodoMessages.interaction.type) { @@ -2204,7 +2272,7 @@ module.exports = { session.getId(), client_id, socket.id, - " - Client is not in session. Adding client and proceeding." + "- Client is not in session. Adding client and proceeding." ); session.addClient(client_id); @@ -2222,7 +2290,7 @@ module.exports = { session.id, client_id, socket.id, - " - Socket is not in session. Adding socket and proceeding." + "- Socket is not in session. Adding socket and proceeding." ); session.addSocket(socket, client_id); @@ -2238,7 +2306,7 @@ module.exports = { session.getId(), null, socket.id, - " - Socket is not joined to SocketIO room. Joining socket and proceeding." + "- Socket is not joined to SocketIO room. Joining socket and proceeding." ); this.joinSocketToRoomAction(session.getId(), socket); @@ -2248,7 +2316,7 @@ module.exports = { }, processMessage: function (data, socket) { - let { success, session_id, client_id } = this.getMetadataFromMessage(data); + let { success, session_id, client_id } = this.getMetadataFromMessage(data, socket); if (!success) { return; @@ -2324,7 +2392,7 @@ module.exports = { } }, - init: function (io, pool, logger) { + init: function (io, pool, logger, chatNamespace, adminNamespace) { this.initGlobals(); this.createCapturesDirectory(); @@ -2334,15 +2402,28 @@ module.exports = { } this.logger = logger; + if (!this.logger) { console.error("Failed to init logger. Exiting."); process.exit(); } + if (chatNamespace == null) { + this.logger.warn("No chatNamespace was found."); + } + + this.chatNamespace = chatNamespace; + + if (adminNamespace == null) { + this.logger.warn("No adminNamespace was found."); + } + + this.adminNamespace = adminNamespace; + this.logInfoSessionClientSocketAction( - "Session ID", - "Client ID", - "Socket ID", + "Session", + "Client", + "Socket ID ", "Message" ); @@ -2523,20 +2604,13 @@ module.exports = { socket.emit(KomodoSendEvents.state, state); }; - this.logInfoSessionClientSocketAction( - null, - null, - null, - `Sync server started. Waiting for connections...` - ); - // main relay handler - io.on(SocketIOEvents.connection, function (socket) { + io.of(SYNC_NAMESPACE).on(SocketIOEvents.connection, function (socket) { self.logInfoSessionClientSocketAction( null, null, socket.id, - `Connected to main (sync) namespace` + `Connected to sync namespace` ); self.socketRepairCenter.add(socket); @@ -2730,11 +2804,14 @@ module.exports = { }); socket.on(SocketIOEvents.disconnecting, function (reason) { + self.handleDisconnecting(socket, reason); }); socket.on(SocketIOEvents.error, function (err) { self.logErrorSessionClientSocketAction(null, null, socket.id || "null", err); }); }); + + logger.info(`Sync namespace is waiting for connections...`); }, }; From 84a1e195588d7cf4609e7d1112809dee930b4693 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Tue, 19 Oct 2021 14:28:13 -0500 Subject: [PATCH 19/24] disconnect sockets whose messages are missing client_id or session_id --- sync.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sync.js b/sync.js index 5e92a1e..87ec7e4 100644 --- a/sync.js +++ b/sync.js @@ -2318,6 +2318,21 @@ module.exports = { processMessage: function (data, socket) { let { success, session_id, client_id } = this.getMetadataFromMessage(data, socket); + if (!client_id || !session_id) { + this.connectionAuthorizationErrorAction( + socket, + "You must provide a client ID and a session ID in the message metadata. Disconnecting" + ); + + this.disconnectAction( + socket, + session_id, + client_id + ); + + return; + } + if (!success) { return; } @@ -2567,6 +2582,14 @@ module.exports = { //disconnect the client socket.disconnect(); + if (!session_id) { + return; + } + + if (!client_id) { + return; + } + // notify others the client has disconnected socket .to(session_id.toString()) From d420436879426b8cd49a2dfbf6865476f0a5d811 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Tue, 19 Oct 2021 14:36:32 -0500 Subject: [PATCH 20/24] send server name upon connection --- sync.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sync.js b/sync.js index 87ec7e4..408079e 100644 --- a/sync.js +++ b/sync.js @@ -76,6 +76,8 @@ const SYNC_OBJECTS = 3; const STATE_VERSION = 2; +const SERVER_NAME = "Komodo Dev (IL)"; + const SYNC_NAMESPACE = "/sync"; //TODO refactor this.sessions into instances of the Session object. @@ -121,6 +123,7 @@ const KomodoSendEvents = { failedToLeave: "failedToLeave", successfullyLeft: "successfullyLeft", disconnected: "disconnected", + serverName: "serverName", sessionInfo: "sessionInfo", state: "state", draw: "draw", @@ -2629,6 +2632,8 @@ module.exports = { // main relay handler io.of(SYNC_NAMESPACE).on(SocketIOEvents.connection, function (socket) { + socket.emit(KomodoSendEvents.serverName, `${SERVER_NAME} + ${SYNC_NAMESPACE}`); + self.logInfoSessionClientSocketAction( null, null, From 8210e1d79d57e21133571bd625b733bb20825d7b Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 21 Oct 2021 05:43:17 -0500 Subject: [PATCH 21/24] attempt to fix state catch-up positions; use tab characters for logging --- sync.js | 157 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/sync.js b/sync.js index 408079e..8c541c8 100644 --- a/sync.js +++ b/sync.js @@ -49,6 +49,7 @@ const Session = require("./session"); const SocketRepairCenter = require("./socket-repair-center"); const SocketActivityMonitor = require("./socket-activity-monitor"); const chat = require("./chat"); +const { debug } = require("console"); // event data globals // NOTE(rob): deprecated. @@ -211,7 +212,7 @@ module.exports = { client_id = `c${client_id}`; if (socket_id == null) { - socket_id = "---................."; + socket_id = "---......................."; } if (action == null) { @@ -224,7 +225,7 @@ module.exports = { if (this.logger) this.logger.info( - ` ${socket_id} ${session_id} ${client_id} ${action}` + `${socket_id}\t${session_id}\t${client_id}\t${action}` ); }, @@ -247,7 +248,7 @@ module.exports = { client_id = `c${client_id}`; if (socket_id == null) { - socket_id = "---................."; + socket_id = "---......................."; } if (action == null) { @@ -260,7 +261,7 @@ module.exports = { if (this.logger) this.logger.error( - `${socket_id} ${session_id} ${client_id} ${action}` + `${socket_id}\t${session_id}\t${client_id}\t${action}` ); }, @@ -283,7 +284,7 @@ module.exports = { client_id = `c${client_id}`; if (socket_id == null) { - socket_id = "---................."; + socket_id = "---......................."; } if (action == null) { @@ -296,7 +297,7 @@ module.exports = { if (this.logger) this.logger.warn( - ` ${socket_id} ${session_id} ${client_id} ${action}` + `${socket_id}\t${session_id}\t${client_id}\t${action}` ); }, @@ -394,7 +395,7 @@ module.exports = { session_id, session.recordingStart, "data" - ); + ); // [capturesDirectoryHere]/[session_id_here]/[session.recordingStartHere]/data fs.writeFile(path, JSON.stringify(session.message_buffer), (e) => { if (e) { console.log(`Error writing message buffer: ${e}`); @@ -1954,7 +1955,7 @@ module.exports = { let entity = { id: target_id, - latest: data.message, + latest: {}, render: true, locked: false, }; @@ -1975,7 +1976,7 @@ module.exports = { let entity = { id: target_id, - latest: [], + latest: {}, render: false, locked: false, }; @@ -1996,7 +1997,7 @@ module.exports = { let entity = { id: target_id, - latest: [], // TODO(Brandon): investigate this. data.message? + latest: {}, // TODO(Brandon): investigate this. data.message? render: true, locked: true, }; @@ -2017,7 +2018,7 @@ module.exports = { let entity = { id: target_id, - latest: [], // TODO(Brandon): investigate this. data.message? + latest: {}, // TODO(Brandon): investigate this. data.message? render: true, locked: false, }; @@ -2069,8 +2070,8 @@ module.exports = { } }, - applyObjectsSyncToState: function (session, data) { - let entity_id = data.message[KomodoMessages.sync.indices.entityId]; + applyObjectsSyncPackedArrayToState: function (session, packedArray) { + let entity_id = packedArray[KomodoMessages.sync.indices.entityId]; let foundEntity = self.getEntityFromState(session, entity_id); @@ -2079,7 +2080,7 @@ module.exports = { let entity = { id: entity_id, - latest: data.message, + latest: packedArray, render: true, locked: false, }; @@ -2089,16 +2090,40 @@ module.exports = { return; } - foundEntity.latest = data.message; + foundEntity.latest = packedArray; }, - applySyncMessageToState: function (session, data) { - // update session state with latest entity positions + applyObjectsSyncToState: function (session, message) { + let foundEntity = self.getEntityFromState(session, entity_id); - let entity_type = data.message[KomodoMessages.sync.indices.entityType]; + if (foundEntity == null) { + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); + + let entity = { + id: entity_id, + latest: message, + render: true, + locked: false, + }; - if (entity_type == SYNC_OBJECTS) { - this.applyObjectsSyncToState(session, data); + session.entities.push(entity); + + return; + } + + foundEntity.latest = message; + }, + + applySyncMessageToState: function (session, message) { + if (message == null) { + //TODO: do something other than fail silently, which we need to do now + + return; + } + + // update session state with latest entity positions + if (message.entityType == SYNC_OBJECTS) { + this.applyObjectsSyncToState(session, message); } }, @@ -2202,38 +2227,62 @@ module.exports = { this.removeSocketAndClientFromSession(socket, session_id, client_id); }, - applyMessageToState: function (data, type, message, session_id, client_id, socket) { - // get reference to session and parse message payload for state updates, if needed. - if (type == KomodoMessages.interaction.type) { - if (message.length < KomodoMessages.interaction.minLength) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: data.message.length was incorrect"); - - return; - } - - let source_id = message[KomodoMessages.interaction.indices.sourceId]; + // currently unused. + applyInteractionPackedArrayToState: function (data, type, packedArray, session_id, client_id, socket) { + if (message.length < KomodoMessages.interaction.minLength) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "poorly formed interaction message: data.message.length was incorrect"); - if (source_id == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: source_id was null"); - } + return; + } - let target_id = message[KomodoMessages.interaction.indices.targetId]; + let source_id = message[KomodoMessages.interaction.indices.sourceId]; - if (target_id == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: target_id was null"); - } + if (source_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `poorly formed interaction message: ${JSON.stringify(message)}`); + } - let interaction_type = message[KomodoMessages.interaction.indices.interactionType]; + let target_id = message[KomodoMessages.interaction.indices.targetId]; - if (interaction_type == null) { - this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, "could not apply interaction message to state: interaction_type was null"); - } + if (target_id == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `poorly formed interaction message: ${message.toString()}`); + } + + let interaction_type = message[KomodoMessages.interaction.indices.interactionType]; + + if (interaction_type == null) { + this.logErrorSessionClientSocketAction(session_id, client_id, socket.id, `poorly formed interaction message: ${message.toString()}`); + } + + this.applyInteractionMessageToState(session, target_id, interaction_type); + }, + + // Not currently used. + applySyncPackedArrayToState: function(data, type, packedArray, session_id, client_id, socket) { + let entity_type = data.message[KomodoMessages.sync.indices.entityType]; + + if (entity_type == null) { + this.logErrorSessionClientSocketAction(null, null, null, JSON.stringify(data.message)); + } + + if (entity_type == SYNC_OBJECTS) { + this.applyObjectsSyncToState(session, data); + } + }, + + applyMessageToState: function (data, type, message, session, client_id, socket) { + if (message == null) { + //TODO: do something other than fail silently. - this.applyInteractionMessageToState(session, target_id, interaction_type); + return; + } + + // get reference to session and parse message payload for state updates, if needed. + if (type == KomodoMessages.interaction.type) { + this.applyInteractionMessageToState(session, message.targetEntity_id, message.interactionType); } if (data.type == KomodoMessages.sync.type) { - this.applySyncMessageToState(session, data); + this.applySyncMessageToState(session, message); } }, @@ -2265,6 +2314,13 @@ module.exports = { return; } + + this.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `Sending state catch-up: ${JSON.stringify(result.state)}` + ); this.sendStateCatchUpAction(socket, result.state); }, @@ -2402,7 +2458,9 @@ module.exports = { data.message = this.parseMessageIfNeeded(data, session_id, client_id); - this.applyMessageToState(data, data.type, data.message, session_id, client_id, socket); + //TODO remove this.logInfoSessionClientSocketAction(null, null, null, data.message); + + this.applyMessageToState(data, data.type, data.message, session, client_id, socket); // data capture if (session.isRecording) { @@ -2439,9 +2497,9 @@ module.exports = { this.adminNamespace = adminNamespace; this.logInfoSessionClientSocketAction( - "Session", + "Sess.", "Client", - "Socket ID ", + "Socket ID.................", "Message" ); @@ -2716,6 +2774,13 @@ module.exports = { return; } + self.logInfoSessionClientSocketAction( + session_id, + null, + socket.id, + `Sending state catch-up: ${JSON.stringify(state)}` + ); + //TODO -- refactor this so that sendStateCatchupAction gets called within handleStateCatchupRequest or something like that. try { // emit versioned state data From 57dd35ac7b005e13ce24caceb862fd89510959b6 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 21 Oct 2021 06:28:40 -0500 Subject: [PATCH 22/24] hotfix: pesky self bug --- sync.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sync.js b/sync.js index 8c541c8..23e0032 100644 --- a/sync.js +++ b/sync.js @@ -1993,7 +1993,7 @@ module.exports = { let foundEntity = this.getEntityFromState(session, target_id); if (foundEntity == null) { - self.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply lock interaction to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply lock interaction to state: no entity with target_id ${target_id} found. Creating one.`); let entity = { id: target_id, @@ -2073,7 +2073,7 @@ module.exports = { applyObjectsSyncPackedArrayToState: function (session, packedArray) { let entity_id = packedArray[KomodoMessages.sync.indices.entityId]; - let foundEntity = self.getEntityFromState(session, entity_id); + let foundEntity = this.getEntityFromState(session, entity_id); if (foundEntity == null) { this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); @@ -2094,7 +2094,7 @@ module.exports = { }, applyObjectsSyncToState: function (session, message) { - let foundEntity = self.getEntityFromState(session, entity_id); + let foundEntity = this.getEntityFromState(session, entity_id); if (foundEntity == null) { this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); @@ -2621,6 +2621,8 @@ module.exports = { socket.emit(KomodoSendEvents.successfullyJoined, session_id); }; + //TODO(Brandon) -- somewhere, handle request to leave and log to server output. + this.requestToLeaveSessionAction = function (session_id, client_id, socket) { socket.leave(session_id.toString(), (err) => { self.tryToRemoveSocketAndClientFromSessionThenNotifyLeft(err, session_id, client_id, socket); From cf2d74b05a82fbfdcdefcbb38b427abaa2ca83b7 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Thu, 21 Oct 2021 06:39:45 -0500 Subject: [PATCH 23/24] hotfix: repair object sync, which would crash server --- sync.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sync.js b/sync.js index 23e0032..6f3e1bf 100644 --- a/sync.js +++ b/sync.js @@ -2094,13 +2094,19 @@ module.exports = { }, applyObjectsSyncToState: function (session, message) { - let foundEntity = this.getEntityFromState(session, entity_id); + if (message == null) { + //TODO: do something other than fail silently, which we need to do now + + return; + } + + let foundEntity = this.getEntityFromState(session, message.entityId); if (foundEntity == null) { - this.logInfoSessionClientSocketAction("unk", "unk", "unk", `apply sync message to state: no entity with target_id ${target_id} found. Creating one.`); + this.logInfoSessionClientSocketAction(null, null, null, `Apply sync message to state: no entity with entityId ${message.entityId} found. Creating one.`); let entity = { - id: entity_id, + id: message.entityId, latest: message, render: true, locked: false, From 6d1db985dc8b2d786025f6fa3f1c759e62cd6b42 Mon Sep 17 00:00:00 2001 From: Brandon Dang Date: Mon, 29 Nov 2021 15:18:31 -0600 Subject: [PATCH 24/24] fix white space problems --- admin.js | 12 ++++++------ sync.js | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/admin.js b/admin.js index 460a8ef..3c96b1b 100644 --- a/admin.js +++ b/admin.js @@ -40,11 +40,11 @@ function JSONStringifyCircular(obj) { const seen = new WeakSet(); return JSON.stringify (obj, (key, value) => { if (typeof value === "object" && value !== null) { - if (seen.has(value)) { - return; - } + if (seen.has(value)) { + return; + } - seen.add(value); + seen.add(value); } return value; @@ -60,9 +60,9 @@ module.exports = { next(); }); - admin.on('connection', function(socket) { //TODO finish or remove. + admin.on('connection', function(socket) { + //TODO finish or remove. // TODO(Brandon): log connection here - socket.emit("adminInfo", socket.id); socket.on('getAllSessions0', function() { diff --git a/sync.js b/sync.js index 6f3e1bf..fe6a479 100644 --- a/sync.js +++ b/sync.js @@ -1208,7 +1208,6 @@ module.exports = { ); // don't call failedToLeaveAction here because we don't have a socket. - return; }