diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-04-25 20:21:03 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-04-25 20:21:03 -0400 |
commit | 6014afab0fabad2b7213fee67a1dc28257ba48e4 (patch) | |
tree | af3f0d137584b7aa882083a9fedbd7130bb94823 | |
parent | defd715b7de572edcd61a23ba693fdb7ff3a7e2f (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.go | 19 | ||||
-rw-r--r-- | sse/broker.go | 130 | ||||
-rw-r--r-- | templates/create.tmpl | 3 | ||||
-rw-r--r-- | templates/index.tmpl | 7 | ||||
-rw-r--r-- | templates/poll.tmpl | 3 | ||||
-rw-r--r-- | templates/result.tmpl | 28 | ||||
-rw-r--r-- | templates/unauthorized.tmpl | 3 |
7 files changed, 182 insertions, 11 deletions
@@ -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%; |