« get me outta code hell

basic working backend save/restore & socket server - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/socket.js
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2020-07-10 11:28:34 -0300
committerFlorrie <towerofnix@gmail.com>2020-07-10 11:28:34 -0300
commit503a37ba4d7550f9c2ed1602e589a0142a20d10d (patch)
treebadb7a7638672c14ddea4f4c28a6c232f1c4a0f4 /socket.js
parentc625099a05f4e4dd8b998a9469f016d649982b50 (diff)
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!
Diffstat (limited to 'socket.js')
-rw-r--r--socket.js183
1 files changed, 183 insertions, 0 deletions
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
+})