diff options
-rw-r--r-- | config.go | 10 | ||||
-rw-r--r-- | docs/cca.scfg.example | 4 | ||||
-rw-r--r-- | err.go | 1 | ||||
-rw-r--r-- | frontend/student.js | 29 | ||||
-rw-r--r-- | frontend/style.css | 11 | ||||
-rw-r--r-- | state.go | 4 | ||||
-rw-r--r-- | tmpl/student.html | 24 | ||||
-rw-r--r-- | wsc.go | 41 | ||||
-rw-r--r-- | wsm.go | 30 | ||||
-rw-r--r-- | wsx.go | 2 |
10 files changed, 135 insertions, 21 deletions
@@ -64,6 +64,7 @@ var configWithPointers struct { Expr *int `scfg:"expr"` } `scfg:"auth"` Perf struct { + SendQ *int `scfg:"sendq"` MessageArgumentsCap *int `scfg:"msg_args_cap"` MessageBytesCap *int `scfg:"msg_bytes_cap"` ReadHeaderTimeout *int `scfg:"read_header_timeout"` @@ -99,6 +100,7 @@ var config struct { Expr int } Perf struct { + SendQ int MessageArgumentsCap int MessageBytesCap int ReadHeaderTimeout int @@ -254,6 +256,14 @@ func fetchConfig(path string) (retErr error) { } config.Auth.Expr = *(configWithPointers.Auth.Expr) + if configWithPointers.Perf.SendQ == nil { + return fmt.Errorf( + "%w: perf.sendq", + errMissingConfigValue, + ) + } + config.Perf.SendQ = *(configWithPointers.Perf.SendQ) + if configWithPointers.Perf.MessageArgumentsCap == nil { return fmt.Errorf( "%w: perf.msg_args_cap", diff --git a/docs/cca.scfg.example b/docs/cca.scfg.example index 6280162..39c0b23 100644 --- a/docs/cca.scfg.example +++ b/docs/cca.scfg.example @@ -95,4 +95,8 @@ perf { # choose the course? Setting this to true may provide a better # user experience but would have a major performance impact. propagate_immediate true + + # How long should the send queue be, for messages sequentially + # propagated through a queue, rather than usems? + senq 10 } @@ -46,4 +46,5 @@ var ( errCannotReceiveMessage = errors.New("cannot receive message") errNoSuchCourse = errors.New("no such course") errInvalidState = errors.New("invalid state") + errWebSocketWrite = errors.New("error writing to websocket") ) diff --git a/frontend/student.js b/frontend/student.js index af5d6ad..ef47724 100644 --- a/frontend/student.js +++ b/frontend/student.js @@ -36,6 +36,7 @@ document.addEventListener("DOMContentLoaded", () => { */ socket.addEventListener("open", function() { + let gstate = 0 let _handleMessage = event => { let msg = new String(event?.data) @@ -78,9 +79,11 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById( `tick${ courseIDs[i] }` ).checked = true - document.getElementById( - `tick${ courseIDs[i] }` - ).disabled = false + if (gstate === 1) { + document.getElementById( + `tick${ courseIDs[i] }` + ).disabled = false + } } } break @@ -102,7 +105,7 @@ document.addEventListener("DOMContentLoaded", () => { !(document.getElementById(`tick${ mar[1] }`).checked) ) { document.getElementById(`tick${ mar[1] }`).disabled = true - } else { + } else if (gstate === 1) { document.getElementById(`tick${ mar[1] }`).disabled = false } break @@ -130,6 +133,24 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById(`tick${ mar[1] }`). indeterminate = false break + case "STOP": + gstate = 0 + document.getElementById("stateindicator").textContent = "disabled" + document.getElementById("confirmbutton").disabled = true + document.querySelectorAll(".coursecheckbox").forEach(c => { + c.disabled = true + }) + break + case "START": + gstate = 1 + document.querySelectorAll(".courseitem").forEach(c => { + if (c.querySelector(".selected-number").textContent !== c.querySelector(".max-number").textContent || c.querySelector(".coursecheckbox").checked) { + c.querySelector(".coursecheckbox").disabled = false + } + }) + document.getElementById("confirmbutton").disabled = false + document.getElementById("stateindicator").textContent = "enabled" + break default: alert(`Invalid command ${ mar[0] } received from socket. Something is wrong.`) } diff --git a/frontend/style.css b/frontend/style.css index 6edbf60..78be469 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -337,6 +337,17 @@ table.table-of-courses { width: 100%; } +:disabled { + background: repeating-linear-gradient( + 135deg, + grey, + grey 5px, + dimgrey 5px, + dimgrey 10px + ); +} + + /* * .need-connection is the content that should actually display when we are * connected via WebSocket. The JavaScript would change display from none to @@ -87,9 +87,9 @@ func setState(ctx context.Context, newState uint32) error { return false }) case 1: - /* TODO: Send message to all connections saying "stop" */ + propagate("STOP") case 2: - /* TODO: Send message to all connections saying "start" */ + propagate("START") default: return errInvalidState } diff --git a/tmpl/student.html b/tmpl/student.html index f9580cc..8699196 100644 --- a/tmpl/student.html +++ b/tmpl/student.html @@ -95,6 +95,9 @@ You logged in on another session. </li> <li> + CCA staff disabled the student portal. + </li> + <li> The network is over-saturated and connections cannot be maintained. </li> <li> @@ -113,7 +116,7 @@ </p> </div> <div class="need-connection"> - <div id="alreadyopen" class="reading-width"> + <div class="reading-width"> <table class="table-of-courses"> <thead> <tr> @@ -136,15 +139,15 @@ <tr><th colspan="7">{{ .Name }}</th></tr> {{- range .Courses }} <tr class="courseitem" id="course{{.ID}}" data-group="{{.Group}}"> - <th style="font-weight: normal" scope="row"> - <input aria-label="Enroll in course" class="coursecheckbox" type="checkbox" id="tick{{.ID}}" name="tick{{.ID}}" value="tick{{.ID}}" data-group="{{.Group}}" {{ if ge .Selected .Max }}disabled{{ end }} ></input> + <th style="font-weight: normal;" scope="row"> + <input aria-label="Enroll in course" class="coursecheckbox" type="checkbox" id="tick{{.ID}}" name="tick{{.ID}}" value="tick{{.ID}}" data-group="{{.Group}}" disabled ></input> <span id="coursestatus{{.ID}}"></span> </th> <td> - <span id="selected{{.ID}}">{{.Selected}}</span> + <span class="selected-number" id="selected{{.ID}}">{{.Selected}}</span> </td> <td> - <span id="max{{.ID}}">{{.Max}}</span> + <span class="max-number" id="max{{.ID}}">{{.Max}}</span> </td> <td>{{.Title}}</td> <td id="type{{.ID}}">{{.Type}}</td> @@ -159,9 +162,10 @@ <td class="th-like" colspan="7"> <div class="flex-justify"> <div class="left"> + Course selections are <span id="stateindicator">disabled</span> </div> <div class="right"> - <button id="confirmbutton" class="btn-primary btn">Confirm</button> + <button id="confirmbutton" class="btn-primary btn" disabled>Confirm</button> </div> </div> </td> @@ -169,14 +173,6 @@ </tfoot> </table> </div> - <!-- - This should be handled in the JavaScript sometime later. - <div id="notopenyet" class="message-box"> - <p> - CCA selections are not currently open. - </p> - </div> - --> </div> </div> <script src="static/student.js"></script> @@ -24,6 +24,7 @@ import ( "context" "errors" "fmt" + "log" "sync" "sync/atomic" "time" @@ -53,6 +54,10 @@ func handleConn( session string, userID string, ) (retErr error) { + send := make(chan string, config.Perf.SendQ) + chanPool.Store(userID, &send) + defer chanPool.CompareAndDelete(userID, &send) + reportError := makeReportError(ctx, c) newCtx, newCancel := context.WithCancel(ctx) @@ -235,6 +240,21 @@ func handleConn( errContextCancelled, newCtx.Err(), ) + case sendText := <-send: + select { + case <-newCtx.Done(): + return fmt.Errorf( + "%w: %w", + errContextCancelled, + newCtx.Err(), + ) + default: + } + + err := writeText(newCtx, c, sendText) + if err != nil { + return err + } case courseID := <-usemParent: select { case <-newCtx.Done(): @@ -326,3 +346,24 @@ func handleConn( } var cancelPool sync.Map /* string, *context.CancelFunc */ + +var chanPool sync.Map /* string, *chan string */ + +func propagate(msg string) { + chanPool.Range(func(_userID, _ch interface{}) bool { + ch, ok := _ch.(*chan string) + if !ok { + panic("chanPool has non-\"*chan string\" key") + } + select { + case *ch <- msg: + default: + userID, ok := _userID.(string) + if !ok { + panic("chanPool has non-string key") + } + log.Println("WARNING: SendQ exceeded for " + userID) + } + return true + }) +} @@ -67,6 +67,12 @@ func messageHello( return reportError("error collecting choices") } + if atomic.LoadUint32(&state) == 2 { + err = writeText(ctx, c, "START") + if err != nil { + return fmt.Errorf("%w: %w", errCannotSend, err) + } + } err = writeText(ctx, c, "HI :"+strings.Join(courseIDs, ",")) if err != nil { return fmt.Errorf("%w: %w", errCannotSend, err) @@ -86,6 +92,18 @@ func messageChooseCourse( ) error { _ = session + if atomic.LoadUint32(&state) != 2 { + err := writeText(ctx, c, "E :Course selections are not open") + if err != nil { + return fmt.Errorf( + "%w: %w", + errCannotSend, + err, + ) + } + return nil + } + select { case <-ctx.Done(): return fmt.Errorf( @@ -263,6 +281,18 @@ func messageUnchooseCourse( ) error { _ = session + if atomic.LoadUint32(&state) != 2 { + err := writeText(ctx, c, "E :Course selections are not open") + if err != nil { + return fmt.Errorf( + "%w: %w", + errCannotSend, + err, + ) + } + return nil + } + select { case <-ctx.Done(): return fmt.Errorf( @@ -30,7 +30,7 @@ import ( func writeText(ctx context.Context, c *websocket.Conn, msg string) error { err := c.Write(ctx, websocket.MessageText, []byte(msg)) if err != nil { - return fmt.Errorf("error writing to connection: %w", err) + return fmt.Errorf("%w: %w", errWebSocketWrite, err) } return nil } |