summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.go10
-rw-r--r--docs/cca.scfg.example4
-rw-r--r--err.go1
-rw-r--r--frontend/student.js29
-rw-r--r--frontend/style.css11
-rw-r--r--state.go4
-rw-r--r--tmpl/student.html24
-rw-r--r--wsc.go41
-rw-r--r--wsm.go30
-rw-r--r--wsx.go2
10 files changed, 135 insertions, 21 deletions
diff --git a/config.go b/config.go
index 47b69f9..e991516 100644
--- a/config.go
+++ b/config.go
@@ -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
}
diff --git a/err.go b/err.go
index 859c567..4597b3f 100644
--- a/err.go
+++ b/err.go
@@ -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
diff --git a/state.go b/state.go
index 1a7ede6..e4c468b 100644
--- a/state.go
+++ b/state.go
@@ -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>
diff --git a/wsc.go b/wsc.go
index 0bb2c7f..2f00302 100644
--- a/wsc.go
+++ b/wsc.go
@@ -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
+ })
+}
diff --git a/wsm.go b/wsm.go
index 4a63c9a..01cf36a 100644
--- a/wsm.go
+++ b/wsm.go
@@ -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(
diff --git a/wsx.go b/wsx.go
index fbac598..d27b72f 100644
--- a/wsx.go
+++ b/wsx.go
@@ -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
}