From 503a37ba4d7550f9c2ed1602e589a0142a20d10d Mon Sep 17 00:00:00 2001 From: Florrie Date: Fri, 10 Jul 2020 11:28:34 -0300 Subject: basic working backend save/restore & socket server Backend save/restore code (living in serialized-backend.js) has been well tested and shouldn't need much change going forward. Now we get to begin working on the actual synchronized-over-socket-server commands! --- socket.js | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 socket.js (limited to 'socket.js') diff --git a/socket.js b/socket.js new file mode 100644 index 0000000..5bfe80a --- /dev/null +++ b/socket.js @@ -0,0 +1,183 @@ +// Tools for hosting an MTUI party over a socket server. Comparable in idea to +// telnet.js, but for interfacing over commands rather than hosting all client +// UIs on one server. The intent of the code in this file is to allow clients +// to connect and interface with each other, while still running all processes +// involved in mtui on their own machines -- so mtui will download and play +// music using each connected machine's own internet connection and speakers. + +// TODO: Option to display listing items which aren't available on all +// connected devices. + +'use strict' // single quotes & no semicolons time babey + +const EventEmitter = require('events') +const net = require('net') + +const { + saveBackend, + restoreBackend, + updateRestoredTracksUsingPlaylists +} = require('./serialized-backend') + +function serializeCommandToData(command) { + // Turn a command into a string/buffer that can be sent over a socket. + return JSON.stringify(command) +} + +function deserializeDataToCommand(data) { + // Turn data received from a socket into a command that can be processed as + // an action to apply to the mtui backend. + return JSON.parse(data) +} + +function validateCommand(command) { + // TODO: Could be used to validate "against" a backend, but for now it just + // checks data types. + + if (typeof command !== 'object') { + return false + } + + if (!['server', 'client'].includes(command.sender)) { + return false + } + + switch (command.sender) { + case 'server': + switch (command.code) { + case 'initialize-backend': + return typeof command.backend === 'object' + } + break + case 'client': + break + } + + return false +} + +function makeSocketServer() { + // The socket server has two functions: to maintain a "canonical" backend + // and synchronize newly connected clients with the relevent data in this + // backend, and to receive command data from clients and relay this to + // other clients. + // + // makeSocketServer doesn't actually start the server listening on a port; + // that's the responsibility of the caller (use server.listen()). + + const server = new net.Server() + const sockets = [] + + server.canonicalBackend = null + + function receivedData(data) { + // Parse data as a command and validate it. If invalid, drop this data. + + let command + try { + command = deserializeDataToCommand(data) + } catch (error) { + return + } + + if (!validateCommand(command)) { + return + } + + // Relay the data to client sockets. + + for (const socket of sockets) { + socket.write(command) + } + } + + server.on('connection', socket => { + sockets.push(socket) + + socket.on('close', () => { + if (sockets.includes(socket)) { + sockets.splice(sockets.indexOf(socket), 1) + } + }) + + socket.on('data', receivedData) + + socket.write(JSON.stringify({ + sender: 'server', + code: 'initialize-backend', + backend: saveBackend(server.canonicalBackend) + })) + }) + + return server +} + +function makeSocketClient() { + // The socket client connects to a server and sends/receives commands to/from + // that server. This doesn't actually connect the socket to a port/host; that + // is the caller's responsibility (use client.socket.connect()). + + const client = new EventEmitter() + client.socket = new net.Socket() + + client.sendCommand = function(command) { + const data = serializeCommandToData(command) + client.socket.write(data) + } + + client.socket.on('data', data => { + // Same sort of "guarding" deserialization/validation as in the server + // code, because it's possible the client and server backends mismatch. + + let command + try { + command = deserializeDataToCommand(data) + } catch (error) { + return + } + + if (!validateCommand(command)) { + return + } + + client.emit('command', command) + }) + + return client +} + +function attachBackendToSocketClient(backend, client, { + getPlaylistSources +}) { + // All actual logic for instances of the mtui backend interacting with each + // other through commands lives here. + + client.on('command', async command => { + switch (command.sender) { + case 'server': + switch (command.code) { + case 'initialize-backend': + await restoreBackend(backend, command.backend) + // TODO: does this need to be called here? + updateRestoredTracksUsingPlaylists(backend, getPlaylistSources()) + break + } + break + } + }) +} + +function attachSocketServerToBackend(server, backend) { + // Unlike the function for attaching a backend to follow commands from a + // client (attachBackendToSocketClient), this function is minimalistic. + // It just sets the associated "canonical" backend. Actual logic for + // de/serialization lives in serialized-backend.js. + server.canonicalBackend = backend +} + +Object.assign(module.exports, { + makeSocketServer, + makeSocketClient, + attachBackendToSocketClient, + attachSocketServerToBackend +}) -- cgit 1.3.0-6-gf8a5