« get me outta code hell

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:
Diffstat (limited to 'socket.js')
-rw-r--r--socket.js157
1 files changed, 110 insertions, 47 deletions
diff --git a/socket.js b/socket.js
index bc35c76..a40dc97 100644
--- a/socket.js
+++ b/socket.js
@@ -40,6 +40,7 @@ const {
 
 const {
   getTimeStringsFromSec,
+  parseWithoutPrototype,
   silenceEvents
 } = require('./general-util')
 
@@ -67,7 +68,7 @@ function serializePartySource(item) {
   }
 }
 
-function deserializePartySource(source) {
+function deserializePartySource(source, parent = null) {
   // Reconstruct a party source into the ordinary group/track format.
 
   const recursive = source => {
@@ -81,9 +82,16 @@ function deserializePartySource(source) {
   }
 
   const top = recursive(source)
-  return (isGroup(top)
+
+  const item = (isGroup(top)
     ? updateGroupFormat(top)
     : updateTrackFormat(top))
+
+  if (parent) {
+    item[parentSymbol] = parent
+  }
+
+  return item
 }
 
 function serializeCommandToData(command) {
@@ -94,7 +102,11 @@ function serializeCommandToData(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)
+  return parseWithoutPrototype(data)
+}
+
+function namePartySources(nickname) {
+  return `Party Sources - ${nickname}`
 }
 
 function isItemRef(ref) {
@@ -144,8 +156,15 @@ function validateCommand(command) {
   switch (command.sender) {
     case 'server':
       switch (command.code) {
-        case 'initialize-backend':
-          return typeof command.backend === 'object'
+        case 'initialize-party':
+          return (
+            typeof command.backend === 'object' &&
+            typeof command.socketInfo === 'object' &&
+            Object.values(command.socketInfo).every(info => (
+              typeof info.nickname === 'string' &&
+              Array.isArray(info.sharedSources)
+            ))
+          )
         case 'set-socket-id':
           return typeof command.socketId === 'string'
       }
@@ -290,23 +309,36 @@ function makeSocketServer() {
   const server = new net.Server()
   const socketMap = Object.create(null)
 
+  // Keeps track of details to share with newly joining sockets for
+  // synchronization.
+  const socketInfoMap = Object.create(null)
+
   server.canonicalBackend = null
 
   // <variable> -> queue player id -> array: socket
-  const readyToResume = {}
-  const donePlaying = {}
+  const readyToResume = Object.create(null)
+  const donePlaying = Object.create(null)
 
   server.on('connection', socket => {
     const socketId = shortid.generate()
 
-    socketMap[socketId] = socket
+    const socketInfo = {
+      hasAnnouncedJoin: false,
+      nickname: DEFAULT_NICKNAME,
 
-    let hasAnnouncedJoin = false
-    let nickname = DEFAULT_NICKNAME
+      // Unlike in client code, this isn't an array of actual playlist items;
+      // rather, it's the intermediary format used when transferring between
+      // client and server.
+      sharedSources: []
+    }
+
+    socketMap[socketId] = socket
+    socketInfoMap[socketId] = socketInfo
 
     socket.on('close', () => {
       if (socketId in socketMap) {
         delete socketMap[socketId]
+        delete socketInfoMap[socketId]
       }
     })
 
@@ -322,7 +354,7 @@ function makeSocketServer() {
 
       command.sender = 'client'
       command.senderSocketId = socketId
-      command.senderNickname = nickname
+      command.senderNickname = socketInfo.nickname
 
       if (!validateCommand(command)) {
         return
@@ -331,7 +363,7 @@ function makeSocketServer() {
       // If the socket hasn't announced its joining yet, it only has access to
       // a few commands.
 
-      if (!hasAnnouncedJoin) {
+      if (!socketInfo.hasAnnouncedJoin) {
         if (![
           'announce-join',
           'set-nickname'
@@ -414,22 +446,30 @@ function makeSocketServer() {
       // Also attach the old nickname for display in log messages.
 
       if (command.code === 'set-nickname') {
-        command.oldNickname = nickname
-        command.senderNickname = nickname
-        nickname = command.nickname
+        command.oldNickname = socketInfo.nickname
+        command.senderNickname = socketInfo.nickname
+        socketInfo.nickname = command.nickname
+      }
+
+      // If it's a 'share-with-party' command, keep track of the item being
+      // shared, so we can synchronize newly joining sockets with it.
+
+      if (command.code === 'share-with-party') {
+        const { sharedSources } = socketInfoMap[socketId]
+        sharedSources.push(command.item)
       }
 
       // If it's an 'announce-join' command, mark the variable for this!
 
       if (command.code === 'announce-join') {
-        hasAnnouncedJoin = true;
+        socketInfo.hasAnnouncedJoin = true;
       }
 
       // If the socket hasn't announced its joining yet, don't relay the
       // command. (Since hasAnnouncedJoin gets set above, 'announce-join'
-      // will meet this condition.)
+      // will pass this condition.)
 
-      if (!hasAnnouncedJoin) {
+      if (!socketInfo.hasAnnouncedJoin) {
         return
       }
 
@@ -458,8 +498,9 @@ function makeSocketServer() {
 
     socket.write(serializeCommandToData({
       sender: 'server',
-      code: 'initialize-backend',
-      backend: savedBackend
+      code: 'initialize-party',
+      backend: savedBackend,
+      socketInfo: socketInfoMap
     }) + '\n')
   })
 
@@ -509,16 +550,16 @@ function attachBackendToSocketClient(backend, client) {
 
   let hasAnnouncedJoin = false
 
-  const partyGrouplike = {
-    name: `Party Sources - ${client.nickname}`,
+  const sharedSources = {
+    name: namePartySources(client.nickname),
     isPartySources: true,
     items: []
   }
 
-  const partyGrouplikeMap = Object.create(null)
+  const socketInfoMap = Object.create(null)
 
   const getPlaylistSources = () =>
-    partyGrouplike.items.map(item => item[originalSymbol])
+    sharedSources.items.map(item => item[originalSymbol])
 
   backend.setHasAnnouncedJoin(false)
   backend.setAlwaysStartPaused(true)
@@ -556,7 +597,7 @@ function attachBackendToSocketClient(backend, client) {
       case 'distribute-queue':
         actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}`
         break
-      case 'initialize-backend':
+      case 'initialize-party':
         return
       case 'play':
         actionmsg = `started playing ${itemToMessage(command.track)}`
@@ -647,10 +688,31 @@ function attachBackendToSocketClient(backend, client) {
         switch (command.code) {
           case 'set-socket-id':
             client.socketId = command.socketId
-            partyGrouplikeMap[command.socketId] = partyGrouplike
-            backend.loadPartyGrouplike(client.socketId, partyGrouplike)
+            socketInfoMap[command.socketId] = {
+              nickname: client.nickname,
+              sharedSources
+            }
+            backend.loadSharedSources(command.socketId, sharedSources)
             return
-          case 'initialize-backend':
+          case 'initialize-party':
+            for (const [ socketId, info ] of Object.entries(command.socketInfo)) {
+              const nickname = info.nickname
+
+              const sharedSources = {
+                name: namePartySources(nickname),
+                isPartySources: true
+              }
+
+              sharedSources.items = info.sharedSources.map(
+                item => deserializePartySource(item, sharedSources))
+
+              socketInfoMap[socketId] = {
+                nickname,
+                sharedSources
+              }
+
+              backend.loadSharedSources(socketId, sharedSources)
+            }
             await restoreBackend(backend, command.backend)
             backend.on('playing', QP => {
               QP.once('received time data', () => {
@@ -668,13 +730,16 @@ function attachBackendToSocketClient(backend, client) {
 
         switch (command.code) {
           case 'announce-join': {
-            const partyGrouplike = {
-              name: `Party Sources - ${command.senderNickname}`,
+            const sharedSources = {
+              name: namePartySources(command.senderNickname),
               isPartySources: true,
               items: []
             }
-            partyGrouplikeMap[command.senderSocketId] = partyGrouplike
-            backend.loadPartyGrouplike(command.senderSocketId, partyGrouplike)
+            socketInfoMap[command.senderSocketId] = {
+              nickname: command.senderNickname,
+              sharedSources
+            }
+            backend.loadSharedSources(command.senderSocketId, sharedSources)
             return
           }
           case 'clear-queue':
@@ -733,11 +798,10 @@ function attachBackendToSocketClient(backend, client) {
             if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time))
             return
           case 'set-nickname': {
-            const partyGrouplike = partyGrouplikeMap[command.senderSocketId]
-            if (partyGrouplike) {
-              partyGrouplike.name = `Party Sources - ${command.senderNickname}`
-              backend.partyGrouplikeUpdated(client.socketId, partyGrouplike)
-            }
+            const info = socketInfoMap[command.senderSocketId]
+            info.nickname = command.senderNickname
+            info.sharedSources.name = namePartySources(command.senderNickname)
+            backend.sharedSourcesUpdated(client.socketId, info.sharedSources)
             return
           }
           case 'set-pause': {
@@ -755,11 +819,10 @@ function attachBackendToSocketClient(backend, client) {
             return
           }
           case 'share-with-party': {
-            const deserialized = deserializePartySource(command.item)
-            const partyGrouplike = partyGrouplikeMap[command.senderSocketId]
-            deserialized[parentSymbol] = partyGrouplike
-            partyGrouplike.items.push(deserialized)
-            backend.partyGrouplikeUpdated(command.senderSocketId, partyGrouplike)
+            const { sharedSources } = socketInfoMap[command.senderSocketId]
+            const deserialized = deserializePartySource(command.item, sharedSources)
+            sharedSources.items.push(deserialized)
+            backend.sharedSourcesUpdated(command.senderSocketId, sharedSources)
             return
           }
           case 'stop-playing':
@@ -861,7 +924,7 @@ function attachBackendToSocketClient(backend, client) {
 
   backend.on('set party nickname', nickname => {
     let oldNickname = client.nickname
-    partyGrouplike.name = `Party Sources - ${nickname}`
+    sharedSources.name = namePartySources(nickname)
     client.nickname = nickname
     client.sendCommand({code: 'set-nickname', nickname, oldNickname})
   })
@@ -898,15 +961,15 @@ function attachBackendToSocketClient(backend, client) {
   })
 
   backend.on('share with party', item => {
-    if (partyGrouplike.items.every(x => x[originalSymbol] !== item)) {
+    if (sharedSources.items.every(x => x[originalSymbol] !== item)) {
       const serialized = serializePartySource(item)
       const deserialized = deserializePartySource(serialized)
 
-      deserialized[parentSymbol] = partyGrouplike
+      deserialized[parentSymbol] = sharedSources
       deserialized[originalSymbol] = item
 
-      partyGrouplike.items.push(deserialized)
-      backend.partyGrouplikeUpdated(client.socketId, partyGrouplike)
+      sharedSources.items.push(deserialized)
+      backend.sharedSourcesUpdated(client.socketId, sharedSources)
 
       updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())