From 56803d0151275739d0b49e29485962eb330bd071 Mon Sep 17 00:00:00 2001
From: Deluan <deluan@navidrome.org>
Date: Mon, 9 Nov 2020 15:13:32 -0500
Subject: [PATCH] Auto-reconnect to event stream after 20secs timeout

---
 server/events/sse.go               |  8 ++++----
 ui/src/App.js                      |  4 ++--
 ui/src/actions/activity.js         | 12 ------------
 ui/src/actions/index.js            |  2 +-
 ui/src/actions/serverEvents.js     | 12 ++++++++++++
 ui/src/eventStream.js              | 24 +++++++++++++++++++-----
 ui/src/reducers/activityReducer.js |  4 ++--
 7 files changed, 40 insertions(+), 26 deletions(-)
 delete mode 100644 ui/src/actions/activity.js
 create mode 100644 ui/src/actions/serverEvents.js

diff --git a/server/events/sse.go b/server/events/sse.go
index 7a31e9321..64897f7a0 100644
--- a/server/events/sse.go
+++ b/server/events/sse.go
@@ -17,7 +17,7 @@ type Broker interface {
 
 type broker struct {
 	// Events are pushed to this channel by the main events-gathering routine
-	Notifier chan []byte
+	notifier chan []byte
 
 	// New client connections
 	newClients chan chan []byte
@@ -32,7 +32,7 @@ type broker struct {
 func NewBroker() Broker {
 	// Instantiate a broker
 	broker := &broker{
-		Notifier:       make(chan []byte, 1),
+		notifier:       make(chan []byte, 1),
 		newClients:     make(chan chan []byte),
 		closingClients: make(chan chan []byte),
 		clients:        make(map[chan []byte]bool),
@@ -52,7 +52,7 @@ func (broker *broker) SendMessage(event Event) {
 	pkg.Name = event.EventName()
 	pkg.Event = event
 	data, _ := json.Marshal(pkg)
-	broker.Notifier <- data
+	broker.notifier <- data
 }
 
 func (broker *broker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -119,7 +119,7 @@ func (broker *broker) listen() {
 			// stop sending them messages.
 			delete(broker.clients, s)
 			log.Debug("Removed client", "numClients", len(broker.clients))
-		case event := <-broker.Notifier:
+		case event := <-broker.notifier:
 
 			// We got a new event from the outside!
 			// Send event to all connected clients
diff --git a/ui/src/App.js b/ui/src/App.js
index eb98ff480..57d4b32b7 100644
--- a/ui/src/App.js
+++ b/ui/src/App.js
@@ -27,7 +27,7 @@ import createAdminStore from './store/createAdminStore'
 import { i18nProvider } from './i18n'
 import config from './config'
 import { startEventStream } from './eventStream'
-import { updateScanStatus } from './actions'
+import { processEvent } from './actions'
 const history = createHashHistory()
 
 if (config.gaTrackingId) {
@@ -59,7 +59,7 @@ const App = () => (
 
 const Admin = (props) => {
   const dispatch = useDispatch()
-  startEventStream((data) => dispatch(updateScanStatus(data)))
+  startEventStream((data) => dispatch(processEvent(data)))
 
   return (
     <RAAdmin
diff --git a/ui/src/actions/activity.js b/ui/src/actions/activity.js
deleted file mode 100644
index b2c32fd45..000000000
--- a/ui/src/actions/activity.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export const ACTIVITY_SCAN_STATUS_UPD = 'ACTIVITY_SCAN_STATUS_UPD'
-
-const actionsMap = { scanStatus: ACTIVITY_SCAN_STATUS_UPD }
-
-export const updateScanStatus = (data) => {
-  let type = actionsMap[data.name]
-  if (!type) type = 'UNKNOWN'
-  return {
-    type,
-    data: data.data,
-  }
-}
diff --git a/ui/src/actions/index.js b/ui/src/actions/index.js
index 8a46b9ceb..0b161ad05 100644
--- a/ui/src/actions/index.js
+++ b/ui/src/actions/index.js
@@ -2,4 +2,4 @@ export * from './audioplayer'
 export * from './themes'
 export * from './albumView'
 export * from './dialogs'
-export * from './activity'
+export * from './serverEvents'
diff --git a/ui/src/actions/serverEvents.js b/ui/src/actions/serverEvents.js
new file mode 100644
index 000000000..07a1bf86d
--- /dev/null
+++ b/ui/src/actions/serverEvents.js
@@ -0,0 +1,12 @@
+export const EVENT_SCAN_STATUS = 'ACTIVITY_SCAN_STATUS_UPD'
+
+const actionsMap = { scanStatus: EVENT_SCAN_STATUS }
+
+export const processEvent = (data) => {
+  let type = actionsMap[data.name]
+  if (!type) type = 'EVENT_UNKNOWN'
+  return {
+    type,
+    data: data.data,
+  }
+}
diff --git a/ui/src/eventStream.js b/ui/src/eventStream.js
index add7d156c..28983b5d5 100644
--- a/ui/src/eventStream.js
+++ b/ui/src/eventStream.js
@@ -1,9 +1,9 @@
 import baseUrl from './utils/baseUrl'
 import throttle from 'lodash.throttle'
 
-// TODO https://stackoverflow.com/a/20060461
 let es = null
-let dispatchFunc = null
+let onMessageHandler = null
+let timeOut = null
 
 const getEventStream = () => {
   if (es === null) {
@@ -14,17 +14,31 @@ const getEventStream = () => {
   return es
 }
 
-export const startEventStream = (func) => {
+// Reestablish the event stream after 20 secs of inactivity
+const setTimeout = () => {
+  if (timeOut != null) {
+    window.clearTimeout(timeOut)
+  }
+  timeOut = window.setTimeout(() => {
+    es.close()
+    es = null
+    startEventStream(onMessageHandler)
+  }, 20000)
+}
+
+export const startEventStream = (messageHandler) => {
   const es = getEventStream()
-  dispatchFunc = func
+  onMessageHandler = messageHandler
   es.onmessage = throttle(
     (msg) => {
       const data = JSON.parse(msg.data)
       if (data.name !== 'keepAlive') {
-        dispatchFunc(data)
+        onMessageHandler(data)
       }
+      setTimeout() // Reset timeout on every received message
     },
     100,
     { trailing: true }
   )
+  setTimeout()
 }
diff --git a/ui/src/reducers/activityReducer.js b/ui/src/reducers/activityReducer.js
index 95b1aaff7..da19c194c 100644
--- a/ui/src/reducers/activityReducer.js
+++ b/ui/src/reducers/activityReducer.js
@@ -1,4 +1,4 @@
-import { ACTIVITY_SCAN_STATUS_UPD } from '../actions'
+import { EVENT_SCAN_STATUS } from '../actions'
 
 export const activityReducer = (
   previousState = {
@@ -8,7 +8,7 @@ export const activityReducer = (
 ) => {
   const { type, data } = payload
   switch (type) {
-    case ACTIVITY_SCAN_STATUS_UPD:
+    case EVENT_SCAN_STATUS:
       return { ...previousState, scanStatus: data }
     default:
       return previousState