aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-04-25 20:21:03 -0400
committerGalen Guyer <galen@galenguyer.com>2022-04-25 20:21:03 -0400
commit6014afab0fabad2b7213fee67a1dc28257ba48e4 (patch)
treeaf3f0d137584b7aa882083a9fedbd7130bb94823
parentdefd715b7de572edcd61a23ba693fdb7ff3a7e2f (diff)
use server sent events for live poll updates without page refresh
also just use the material theme directly because holy moly themeswitcher is having a bad time
-rw-r--r--main.go19
-rw-r--r--sse/broker.go130
-rw-r--r--templates/create.tmpl3
-rw-r--r--templates/index.tmpl7
-rw-r--r--templates/poll.tmpl3
-rw-r--r--templates/result.tmpl28
-rw-r--r--templates/unauthorized.tmpl3
7 files changed, 182 insertions, 11 deletions
diff --git a/main.go b/main.go
index 7efc1c4..26866d7 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "encoding/json"
"net/http"
"os"
"sort"
@@ -8,6 +9,7 @@ import (
csh_auth "github.com/computersciencehouse/csh-auth"
"github.com/computersciencehouse/vote/database"
+ "github.com/computersciencehouse/vote/sse"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson/primitive"
)
@@ -16,6 +18,7 @@ func main() {
r := gin.Default()
r.StaticFS("/static", http.Dir("static"))
r.LoadHTMLGlob("templates/*")
+ broker := sse.NewBroker()
csh := csh_auth.CSHAuth{}
csh.Init(
@@ -188,6 +191,18 @@ func main() {
}
database.CastVote(vote)
+ if poll, err := database.GetPoll(c.Param("id")); err == nil {
+ if results, err := poll.GetResult(); err == nil {
+ if bytes, err := json.Marshal(results); err == nil {
+ broker.Notifier <- sse.NotificationEvent{
+ EventName: poll.Id,
+ Payload: string(bytes),
+ }
+ }
+
+ }
+ }
+
c.Redirect(302, "/results/"+poll.Id)
}))
@@ -244,6 +259,10 @@ func main() {
c.Redirect(302, "/results/"+poll.Id)
}))
+ r.GET("/stream/:topic", broker.ServeHTTP)
+
+ go broker.Listen()
+
r.Run()
}
diff --git a/sse/broker.go b/sse/broker.go
new file mode 100644
index 0000000..464b333
--- /dev/null
+++ b/sse/broker.go
@@ -0,0 +1,130 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Ismael Celis and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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 AUTHORS 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 IN THE SOFTWARE.
+*/
+
+package sse
+
+import (
+ "io"
+ "log"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+const patience time.Duration = time.Second * 1
+
+type (
+ NotificationEvent struct {
+ EventName string
+ Payload interface{}
+ }
+
+ NotifierChan chan NotificationEvent
+
+ Broker struct {
+
+ // Events are pushed to this channel by the main events-gathering routine
+ Notifier NotifierChan
+
+ // New client connections
+ newClients chan NotifierChan
+
+ // Closed client connections
+ closingClients chan NotifierChan
+
+ // Client connections registry
+ clients map[NotifierChan]struct{}
+ }
+)
+
+func NewBroker() (broker *Broker) {
+ // Instantiate a broker
+ return &Broker{
+ Notifier: make(NotifierChan, 1),
+ newClients: make(chan NotifierChan),
+ closingClients: make(chan NotifierChan),
+ clients: make(map[NotifierChan]struct{}),
+ }
+}
+
+func (broker *Broker) ServeHTTP(c *gin.Context) {
+ eventName := c.Param("topic")
+
+ // Each connection registers its own message channel with the Broker's connections registry
+ messageChan := make(NotifierChan)
+
+ // Signal the broker that we have a new connection
+ broker.newClients <- messageChan
+
+ // Remove this client from the map of connected clients
+ // when this handler exits.
+ defer func() {
+ broker.closingClients <- messageChan
+ }()
+
+ c.Stream(func(w io.Writer) bool {
+ // Emit Server Sent Events compatible
+ event := <-messageChan
+
+ switch eventName {
+ case event.EventName:
+ c.SSEvent(event.EventName, event.Payload)
+ }
+
+ // Flush the data immediately instead of buffering it for later.
+ c.Writer.Flush()
+
+ return true
+ })
+}
+
+// Listen for new notifications and redistribute them to clients
+func (broker *Broker) Listen() {
+ for {
+ select {
+ case s := <-broker.newClients:
+
+ // A new client has connected.
+ // Register their message channel
+ broker.clients[s] = struct{}{}
+ log.Printf("Client added. %d registered clients", len(broker.clients))
+ case s := <-broker.closingClients:
+
+ // A client has dettached and we want to
+ // stop sending them messages.
+ delete(broker.clients, s)
+ log.Printf("Removed client. %d registered clients", len(broker.clients))
+ case event := <-broker.Notifier:
+
+ // We got a new event from the outside!
+ // Send event to all connected clients
+ for clientMessageChan := range broker.clients {
+ select {
+ case clientMessageChan <- event:
+ case <-time.After(patience):
+ log.Print("Skipping client.")
+ }
+ }
+ }
+ }
+}
diff --git a/templates/create.tmpl b/templates/create.tmpl
index 788384c..e0b6ba3 100644
--- a/templates/create.tmpl
+++ b/templates/create.tmpl
@@ -2,7 +2,8 @@
<html lang="en">
<head>
<title>CSH Vote</title>
- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" />
+ <!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
+ <link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
diff --git a/templates/index.tmpl b/templates/index.tmpl
index 37ab0b5..3b09eb7 100644
--- a/templates/index.tmpl
+++ b/templates/index.tmpl
@@ -2,11 +2,8 @@
<html lang="en">
<head>
<title>CSH Vote</title>
- <link
- rel="stylesheet"
- href="https://themeswitcher.csh.rit.edu/api/get"
- media="screen"
- />
+ <!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
+ <link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" />
<style>
ul {
list-style: none;
diff --git a/templates/poll.tmpl b/templates/poll.tmpl
index 4d1a7c1..1d8f2ac 100644
--- a/templates/poll.tmpl
+++ b/templates/poll.tmpl
@@ -2,7 +2,8 @@
<html lang="en">
<head>
<title>CSH Vote</title>
- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" />
+ <!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
+ <link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
diff --git a/templates/result.tmpl b/templates/result.tmpl
index 67b93a0..bebd9ae 100644
--- a/templates/result.tmpl
+++ b/templates/result.tmpl
@@ -2,7 +2,8 @@
<html lang="en">
<head>
<title>CSH Vote</title>
- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" />
+ <!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
+ <link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
@@ -27,9 +28,11 @@
<br />
<br />
- <div>
+ <div id="results">
{{ range $option, $count := .Results }}
- <div style="font-size: 1.25rem; line-height: 1.25;">{{ $option }}: {{ $count }}</div>
+ <div id="{{ $option }}" style="font-size: 1.25rem; line-height: 1.25">
+ {{ $option }}: {{ $count }}
+ </div>
<br />
{{ end }}
</div>
@@ -42,5 +45,24 @@
</form>
{{ end }}
</div>
+ <script>
+ let eventSource = new EventSource("/stream/{{ .Id }}");
+
+ eventSource.addEventListener("{{ .Id }}", function (event) {
+ let data = JSON.parse(event.data);
+ for (let option in data) {
+ let count = data[option];
+ let element = document.getElementById(option);
+ if (element == null) {
+ let newElement = document.createElement("div");
+ newElement.id = option;
+ newElement.style = "font-size: 1.25rem; line-height: 1.25";
+ newElement.innerText = option + ": " + count;
+ document.getElementById("results").appendChild(newElement);
+ }
+ element.innerText = option + ": " + count;
+ }
+ });
+ </script>
</body>
</html>
diff --git a/templates/unauthorized.tmpl b/templates/unauthorized.tmpl
index e00076a..545d297 100644
--- a/templates/unauthorized.tmpl
+++ b/templates/unauthorized.tmpl
@@ -2,7 +2,8 @@
<html lang="en">
<head>
<title>CSH Vote</title>
- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" />
+ <!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
+ <link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" />
<style>
#lockdown {
width: 20%;