From 9615d4d449e4bd9bb363cfcea46880ff0d3b6557 Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 08:16:26 +0200 Subject: [PATCH 1/9] feat: implement permanent broadcast message functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add SendPermanentBroadCastMessage to continuously send updates - refactor BroadcastMessage to use a shared Message struct - update pomodoro logic to modify broadcast.Message directly - adjust client command handling to use broadcast.Clients map - enhance ServerMessage struct with "Ongoing" and "Paused" fields 🤖 --- README.md | 17 ++++---- cmd/server/main.go | 2 + internal/broadcast/broadcast.go | 40 +++++++++++++------ internal/pomodoro/pomodoro.go | 50 ++++++++++++------------ internal/pomodoro/timer.go | 17 ++------ internal/websocket/client_commands.go | 7 ++-- internal/websocket/handle_connections.go | 5 +-- pkg/models/server.go | 2 + 8 files changed, 75 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 5f80e98..a7c41f3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ This section describes the WebSocket commands that clients can send to the Pomod Both client commands (sent to the server) and server messages (sent to the client) use the JSON format. The structure of these messages are described in the sections below. - ### Client Commands Clients communicate with the server by sending JSON-encoded commands. Each command must include a `command` field specifying the action to perform. Here are the available commands: @@ -51,19 +50,23 @@ Example: ### Server Messages -The server sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. +The server periodically (every second) sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. These messages contain the following fields: - mode: Indicates the current phase of the Pomodoro session ("Work", "ShortBreak", "LongBreak", or "none" if no session is running). - session: The current session number (e.g., 1 for the first work session). - total_sessions: The total number of sessions for the current Pomodoro cycle (e.g., 4 if the cycle consists of 4 work/break sessions). - time_left: The remaining time for the current mode, in seconds (e.g., 900 for 15 minutes). +- ongoing: Wether a pomodoro is currently ongoing +- paused: Wether the timer is paused -| Message Type | Sent by Server | Example | -| --------------------- | ------------------------------------------------ | --------------------------------------------------- | -| Welcome Message | Upon initial client connection | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | -| Session Update | Periodically during a session | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900}` | -| Session End/Reset | After session completes or is reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | +| Message Type | Example | +| --------------------- | --------------------------------------------------- | +| Welcome Message | `{"mode": "none", "session":0, "total_sessions":0, "time_left":0, "ongoing": false, "paused": false}` | +| Session Running | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900, "ongoing": true, "paused": false}` | +| Session Running | `{"mode": "ShortBreak", "session": 2, "total_sessions": 4, "time_left": 50, "ongoing": true, "paused": false}` | +| Session Paused | `{"mode": "Work", "session": 2, "total_sessions": 4, "time_left": 456, "ongoing": true, "paused": true}` | +| Session End/Reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0, "ongoing": false, "paused": false}` | ## Testing diff --git a/cmd/server/main.go b/cmd/server/main.go index 2105b39..bf3f949 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package server import ( "flag" "fmt" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "log" @@ -23,6 +24,7 @@ func Start() { listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort) http.HandleFunc("/ws", websocket.HandleConnections) + go broadcast.SendPermanentBroadCastMessage() log.Printf("Pomodoro WebSocket server started on %s\n", listen) err := http.ListenAndServe(listen, nil) diff --git a/internal/broadcast/broadcast.go b/internal/broadcast/broadcast.go index 1be04ce..bd93d06 100644 --- a/internal/broadcast/broadcast.go +++ b/internal/broadcast/broadcast.go @@ -5,23 +5,37 @@ import ( "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" + "time" ) -// BroadcastMessage sends a message to all connected WebSocket clients. -func BroadcastMessage(clients map[*websocket.Conn]*models.Client, message models.ServerMessage) { - // Marshal the message into JSON format - jsonMessage, err := json.Marshal(message) - if err != nil { - log.Printf("Error marshalling message: %v", err) - return - } +// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct +var Clients = make(map[*websocket.Conn]*models.Client) +var Message = models.ServerMessage{ + Mode: "none", + Session: 0, + TotalSession: 0, + TimeLeft: 0, + Ongoing: false, + Paused: false, +} - // Iterate over all connected clients and broadcast the message - for _, client := range clients { - err := client.SendMessage(websocket.TextMessage, jsonMessage) +// BroadcastMessage sends a message to all connected WebSocket clients. +func SendPermanentBroadCastMessage() { + for { + // Marshal the message into JSON format + jsonMessage, err := json.Marshal(Message) if err != nil { - log.Printf("Error broadcasting to client: %v", err) - // The client is responsible for closing itself on error + log.Printf("Error marshalling message: %v", err) + return } + // Iterate over all connected clients and broadcast the message + for _, client := range Clients { + err := client.SendMessage(websocket.TextMessage, jsonMessage) + if err != nil { + log.Printf("Error broadcasting to client: %v", err) + // The client is responsible for closing itself on error + } + } + time.Sleep(time.Second) } } diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 3124b6c..21b2fa2 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -7,9 +7,6 @@ import ( "sync" ) -var pomodoroOngoing bool -var pomodoroPaused bool - var pomodoroResetChannel = make(chan bool, 1) var pomodoroPauseChannel = make(chan bool, 1) var pomodoroResumeChannel = make(chan bool, 1) @@ -19,52 +16,53 @@ var mu sync.Mutex // to synchronize access to shared state // RunPomodoro iterates the Pomodoro work/break sessions. func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTomatoPomodoroConfig) { mu.Lock() - pomodoroOngoing = true - pomodoroPaused = false + broadcast.Message.Ongoing = true + broadcast.Message.Paused = false mu.Unlock() + broadcast.Message.TotalSession = config.Sessions + for session := 1; session <= config.Sessions; session++ { - if !startTimer(clients, config.Work, "Work", session, config.Sessions) { + broadcast.Message.Session = session + if !startTimer(clients, config.Work, "Work") { break } if session == config.Sessions { - if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) { + if !startTimer(clients, config.LongBreak, "LongBreak") { break } } else { - if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) { + if !startTimer(clients, config.ShortBreak, "ShortBreak") { break } } } mu.Lock() - pomodoroOngoing = false + broadcast.Message.Ongoing = false mu.Unlock() } // ResetPomodoro resets the running Pomodoro timer. func ResetPomodoro(clients map[*websocket.Conn]*models.Client) { - mu.Lock() - pomodoroOngoing = false // Reset the running state - pomodoroPaused = false // Reset the paused state - mu.Unlock() - - // Broadcast the reset message to all clients - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: "none", - Session: 0, - TotalSession: 0, - TimeLeft: 0, - }) - // Send a reset signal to stop any running timers pomodoroResetChannel <- true + + mu.Lock() + broadcast.Message.Ongoing = false + broadcast.Message.Paused = false + mu.Unlock() + + // Reset message + broadcast.Message.Mode = "none" + broadcast.Message.Session = 0 + broadcast.Message.TotalSession = 0 + broadcast.Message.TimeLeft = 0 } func PausePomodoro() { mu.Lock() - pomodoroPaused = true + broadcast.Message.Paused = true mu.Unlock() pomodoroPauseChannel <- true @@ -72,7 +70,7 @@ func PausePomodoro() { func ResumePomodoro() { mu.Lock() - pomodoroPaused = false + broadcast.Message.Paused = false mu.Unlock() pomodoroResumeChannel <- true } @@ -80,11 +78,11 @@ func ResumePomodoro() { func IsPomodoroOngoing() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return pomodoroOngoing + return broadcast.Message.Ongoing } func IsPomodoroPaused() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return pomodoroPaused + return broadcast.Message.Paused } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index f23d286..272b989 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -8,7 +8,7 @@ import ( ) // startTimer runs the countdown and broadcasts every second. -func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string, session int, totalSessions int) bool { +func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string) bool { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: @@ -20,12 +20,8 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int default: // Broadcast the current state to all clients if !IsPomodoroPaused() { - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: mode, - Session: session, - TotalSession: totalSessions, - TimeLeft: remainingSeconds, - }) + broadcast.Message.Mode = mode + broadcast.Message.TimeLeft = remainingSeconds time.Sleep(time.Second) remainingSeconds-- } @@ -33,12 +29,7 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int } // Final broadcast when time reaches zero - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: mode, - Session: session, - TotalSession: totalSessions, - TimeLeft: 0, - }) + broadcast.Message.TimeLeft = remainingSeconds return true } diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index 1005006..b1c6f88 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/json" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" @@ -24,7 +25,7 @@ func handleClientCommands(ws *websocket.Conn) { _, message, err := ws.ReadMessage() if err != nil { log.Printf("Client disconnected: %v", err) - delete(Clients, ws) + delete(broadcast.Clients, ws) break } @@ -42,11 +43,11 @@ func handleClientCommands(ws *websocket.Conn) { if clientCommand.Config != unsetPomodoroConfig { pomodoroConfig = clientCommand.Config } - go pomodoro.RunPomodoro(Clients, pomodoroConfig) // Start the timer with the list of clients + go pomodoro.RunPomodoro(broadcast.Clients, pomodoroConfig) // Start the timer with the list of clients } case "stop": if pomodoro.IsPomodoroOngoing() { - pomodoro.ResetPomodoro(Clients) // Reset Pomodoro + pomodoro.ResetPomodoro(broadcast.Clients) // Reset Pomodoro } case "pause": if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { diff --git a/internal/websocket/handle_connections.go b/internal/websocket/handle_connections.go index be49918..cbf0f04 100644 --- a/internal/websocket/handle_connections.go +++ b/internal/websocket/handle_connections.go @@ -1,6 +1,7 @@ package websocket import ( + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" @@ -8,8 +9,6 @@ import ( "sync" ) -// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct -var Clients = make(map[*websocket.Conn]*models.Client) var mu sync.Mutex // Mutex to protect access to the Clients map // Upgrader to upgrade HTTP requests to WebSocket connections @@ -29,7 +28,7 @@ func HandleConnections(w http.ResponseWriter, r *http.Request) { // Register the new client mu.Lock() - Clients[ws] = &models.Client{ + broadcast.Clients[ws] = &models.Client{ Conn: ws, // Store the WebSocket connection } mu.Unlock() diff --git a/pkg/models/server.go b/pkg/models/server.go index b4de1c2..7ebe4c3 100644 --- a/pkg/models/server.go +++ b/pkg/models/server.go @@ -6,4 +6,6 @@ type ServerMessage struct { Session int `json:"session"` // Current session number TotalSession int `json:"total_sessions"` // Total number of sessions TimeLeft int `json:"time_left"` // Remaining time in seconds + Ongoing bool `json:"ongoing"` // Ongoing pomodoro + Paused bool `json:"paused"` // Is timer paused } From 234f3c17dc07d370d560bcaf767c32b52c7e02db Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 08:39:39 +0200 Subject: [PATCH 2/9] feat: simplify pomodoro functions by removing client parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove clients parameter from RunPomodoro function - update startTimer to no longer require clients parameter - modify ResetPomodoro to eliminate clients parameter - adjust client_commands to reflect changes in function signatures 🤖 --- internal/pomodoro/pomodoro.go | 11 +++++------ internal/pomodoro/timer.go | 4 +--- internal/websocket/client_commands.go | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 21b2fa2..a72319c 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -3,7 +3,6 @@ package pomodoro import ( "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/pkg/models" - "github.com/gorilla/websocket" "sync" ) @@ -14,7 +13,7 @@ var pomodoroResumeChannel = make(chan bool, 1) var mu sync.Mutex // to synchronize access to shared state // RunPomodoro iterates the Pomodoro work/break sessions. -func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTomatoPomodoroConfig) { +func RunPomodoro(config models.GoTomatoPomodoroConfig) { mu.Lock() broadcast.Message.Ongoing = true broadcast.Message.Paused = false @@ -24,15 +23,15 @@ func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTom for session := 1; session <= config.Sessions; session++ { broadcast.Message.Session = session - if !startTimer(clients, config.Work, "Work") { + if !startTimer(config.Work, "Work") { break } if session == config.Sessions { - if !startTimer(clients, config.LongBreak, "LongBreak") { + if !startTimer(config.LongBreak, "LongBreak") { break } } else { - if !startTimer(clients, config.ShortBreak, "ShortBreak") { + if !startTimer(config.ShortBreak, "ShortBreak") { break } } @@ -44,7 +43,7 @@ func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTom } // ResetPomodoro resets the running Pomodoro timer. -func ResetPomodoro(clients map[*websocket.Conn]*models.Client) { +func ResetPomodoro() { // Send a reset signal to stop any running timers pomodoroResetChannel <- true diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index 272b989..30466c6 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -2,13 +2,11 @@ package pomodoro import ( "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" - "git.smsvc.net/pomodoro/GoTomato/pkg/models" - "github.com/gorilla/websocket" "time" ) // startTimer runs the countdown and broadcasts every second. -func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string) bool { +func startTimer(remainingSeconds int, mode string) bool { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index b1c6f88..8952fd0 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -43,11 +43,11 @@ func handleClientCommands(ws *websocket.Conn) { if clientCommand.Config != unsetPomodoroConfig { pomodoroConfig = clientCommand.Config } - go pomodoro.RunPomodoro(broadcast.Clients, pomodoroConfig) // Start the timer with the list of clients + go pomodoro.RunPomodoro(pomodoroConfig) // Start the timer with the list of clients } case "stop": if pomodoro.IsPomodoroOngoing() { - pomodoro.ResetPomodoro(broadcast.Clients) // Reset Pomodoro + pomodoro.ResetPomodoro() // Reset Pomodoro } case "pause": if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { From aab6896c7d86bfe2c4ac7ad3b42b277bdfb4cf60 Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 09:20:58 +0200 Subject: [PATCH 3/9] refactor: move broadcast.Message to shared.Message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create a new shared state file to manage message state - replace broadcast.Message with shared.Message in pomodoro logic - update websocket client handling to use shared.Clients - remove unnecessary broadcast imports from various files - ensure consistent message handling across the application 🤖 --- internal/broadcast/broadcast.go | 18 +++---------- internal/pomodoro/pomodoro.go | 32 ++++++++++++------------ internal/pomodoro/timer.go | 12 ++++----- internal/shared/state.go | 14 +++++++++++ internal/websocket/client_commands.go | 3 +-- internal/websocket/handle_connections.go | 5 ++-- 6 files changed, 44 insertions(+), 40 deletions(-) create mode 100644 internal/shared/state.go diff --git a/internal/broadcast/broadcast.go b/internal/broadcast/broadcast.go index bd93d06..ac23e7e 100644 --- a/internal/broadcast/broadcast.go +++ b/internal/broadcast/broadcast.go @@ -2,34 +2,24 @@ package broadcast import ( "encoding/json" - "git.smsvc.net/pomodoro/GoTomato/pkg/models" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" + ws "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "github.com/gorilla/websocket" "log" "time" ) -// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct -var Clients = make(map[*websocket.Conn]*models.Client) -var Message = models.ServerMessage{ - Mode: "none", - Session: 0, - TotalSession: 0, - TimeLeft: 0, - Ongoing: false, - Paused: false, -} - // BroadcastMessage sends a message to all connected WebSocket clients. func SendPermanentBroadCastMessage() { for { // Marshal the message into JSON format - jsonMessage, err := json.Marshal(Message) + jsonMessage, err := json.Marshal(shared.Message) if err != nil { log.Printf("Error marshalling message: %v", err) return } // Iterate over all connected clients and broadcast the message - for _, client := range Clients { + for _, client := range ws.Clients { err := client.SendMessage(websocket.TextMessage, jsonMessage) if err != nil { log.Printf("Error broadcasting to client: %v", err) diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index a72319c..b748d0a 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -1,7 +1,7 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "sync" ) @@ -15,14 +15,14 @@ var mu sync.Mutex // to synchronize access to shared state // RunPomodoro iterates the Pomodoro work/break sessions. func RunPomodoro(config models.GoTomatoPomodoroConfig) { mu.Lock() - broadcast.Message.Ongoing = true - broadcast.Message.Paused = false + shared.Message.Ongoing = true + shared.Message.Paused = false mu.Unlock() - broadcast.Message.TotalSession = config.Sessions + shared.Message.TotalSession = config.Sessions for session := 1; session <= config.Sessions; session++ { - broadcast.Message.Session = session + shared.Message.Session = session if !startTimer(config.Work, "Work") { break } @@ -38,7 +38,7 @@ func RunPomodoro(config models.GoTomatoPomodoroConfig) { } mu.Lock() - broadcast.Message.Ongoing = false + shared.Message.Ongoing = false mu.Unlock() } @@ -48,20 +48,20 @@ func ResetPomodoro() { pomodoroResetChannel <- true mu.Lock() - broadcast.Message.Ongoing = false - broadcast.Message.Paused = false + shared.Message.Ongoing = false + shared.Message.Paused = false mu.Unlock() // Reset message - broadcast.Message.Mode = "none" - broadcast.Message.Session = 0 - broadcast.Message.TotalSession = 0 - broadcast.Message.TimeLeft = 0 + shared.Message.Mode = "none" + shared.Message.Session = 0 + shared.Message.TotalSession = 0 + shared.Message.TimeLeft = 0 } func PausePomodoro() { mu.Lock() - broadcast.Message.Paused = true + shared.Message.Paused = true mu.Unlock() pomodoroPauseChannel <- true @@ -69,7 +69,7 @@ func PausePomodoro() { func ResumePomodoro() { mu.Lock() - broadcast.Message.Paused = false + shared.Message.Paused = false mu.Unlock() pomodoroResumeChannel <- true } @@ -77,11 +77,11 @@ func ResumePomodoro() { func IsPomodoroOngoing() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return broadcast.Message.Ongoing + return shared.Message.Ongoing } func IsPomodoroPaused() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return broadcast.Message.Paused + return shared.Message.Paused } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index 30466c6..1c1f360 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -1,11 +1,11 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "time" ) -// startTimer runs the countdown and broadcasts every second. +// startTimer runs the countdown and shared. every second. func startTimer(remainingSeconds int, mode string) bool { for remainingSeconds > 0 { select { @@ -18,16 +18,16 @@ func startTimer(remainingSeconds int, mode string) bool { default: // Broadcast the current state to all clients if !IsPomodoroPaused() { - broadcast.Message.Mode = mode - broadcast.Message.TimeLeft = remainingSeconds + shared.Message.Mode = mode + shared.Message.TimeLeft = remainingSeconds time.Sleep(time.Second) remainingSeconds-- } } } - // Final broadcast when time reaches zero - broadcast.Message.TimeLeft = remainingSeconds + // Final shared.when time reaches zero + shared.Message.TimeLeft = remainingSeconds return true } diff --git a/internal/shared/state.go b/internal/shared/state.go new file mode 100644 index 0000000..26ff2d6 --- /dev/null +++ b/internal/shared/state.go @@ -0,0 +1,14 @@ +package shared + +import ( + "git.smsvc.net/pomodoro/GoTomato/pkg/models" +) + +var Message = models.ServerMessage{ + Mode: "none", + Session: 0, + TotalSession: 0, + TimeLeft: 0, + Ongoing: false, + Paused: false, +} diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index 8952fd0..df4fdc3 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -2,7 +2,6 @@ package websocket import ( "encoding/json" - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" @@ -25,7 +24,7 @@ func handleClientCommands(ws *websocket.Conn) { _, message, err := ws.ReadMessage() if err != nil { log.Printf("Client disconnected: %v", err) - delete(broadcast.Clients, ws) + delete(Clients, ws) break } diff --git a/internal/websocket/handle_connections.go b/internal/websocket/handle_connections.go index cbf0f04..be49918 100644 --- a/internal/websocket/handle_connections.go +++ b/internal/websocket/handle_connections.go @@ -1,7 +1,6 @@ package websocket import ( - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" @@ -9,6 +8,8 @@ import ( "sync" ) +// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct +var Clients = make(map[*websocket.Conn]*models.Client) var mu sync.Mutex // Mutex to protect access to the Clients map // Upgrader to upgrade HTTP requests to WebSocket connections @@ -28,7 +29,7 @@ func HandleConnections(w http.ResponseWriter, r *http.Request) { // Register the new client mu.Lock() - broadcast.Clients[ws] = &models.Client{ + Clients[ws] = &models.Client{ Conn: ws, // Store the WebSocket connection } mu.Unlock() From 6ffd9f1e382852ba5a9b42c686088510c5bacf8c Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 09:24:20 +0200 Subject: [PATCH 4/9] refactor: move broadcast package to websocket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update import paths to reflect the new package name - change function call to use the new websocket package - adjust client iteration to use the renamed Clients variable 🤖 --- cmd/server/main.go | 3 +-- internal/{broadcast => websocket}/broadcast.go | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) rename internal/{broadcast => websocket}/broadcast.go (86%) diff --git a/cmd/server/main.go b/cmd/server/main.go index bf3f949..b1d9db8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,7 +3,6 @@ package server import ( "flag" "fmt" - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "log" @@ -24,7 +23,7 @@ func Start() { listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort) http.HandleFunc("/ws", websocket.HandleConnections) - go broadcast.SendPermanentBroadCastMessage() + go websocket.SendPermanentBroadCastMessage() log.Printf("Pomodoro WebSocket server started on %s\n", listen) err := http.ListenAndServe(listen, nil) diff --git a/internal/broadcast/broadcast.go b/internal/websocket/broadcast.go similarity index 86% rename from internal/broadcast/broadcast.go rename to internal/websocket/broadcast.go index ac23e7e..728726a 100644 --- a/internal/broadcast/broadcast.go +++ b/internal/websocket/broadcast.go @@ -1,9 +1,8 @@ -package broadcast +package websocket import ( "encoding/json" "git.smsvc.net/pomodoro/GoTomato/internal/shared" - ws "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "github.com/gorilla/websocket" "log" "time" @@ -19,7 +18,7 @@ func SendPermanentBroadCastMessage() { return } // Iterate over all connected clients and broadcast the message - for _, client := range ws.Clients { + for _, client := range Clients { err := client.SendMessage(websocket.TextMessage, jsonMessage) if err != nil { log.Printf("Error broadcasting to client: %v", err) From 03ab627729d44a49515b7ee7662054684ff21234 Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 09:34:36 +0200 Subject: [PATCH 5/9] feat: simplify timer function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove "mode" parameter from startTimer function - update shared.Message.Mode for each pomodoro state - update comments 🤖 --- internal/pomodoro/pomodoro.go | 9 ++++++--- internal/pomodoro/timer.go | 7 +++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index b748d0a..c5e927e 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -23,15 +23,18 @@ func RunPomodoro(config models.GoTomatoPomodoroConfig) { for session := 1; session <= config.Sessions; session++ { shared.Message.Session = session - if !startTimer(config.Work, "Work") { + shared.Message.Mode = "Work" + if !startTimer(config.Work) { break } if session == config.Sessions { - if !startTimer(config.LongBreak, "LongBreak") { + shared.Message.Mode = "LongBreak" + if !startTimer(config.LongBreak) { break } } else { - if !startTimer(config.ShortBreak, "ShortBreak") { + shared.Message.Mode = "ShortBreak" + if !startTimer(config.ShortBreak) { break } } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index 1c1f360..4b4c86e 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -5,8 +5,8 @@ import ( "time" ) -// startTimer runs the countdown and shared. every second. -func startTimer(remainingSeconds int, mode string) bool { +// runs the countdown and updates shared.Message.TimeLeft every second. +func startTimer(remainingSeconds int) bool { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: @@ -18,7 +18,6 @@ func startTimer(remainingSeconds int, mode string) bool { default: // Broadcast the current state to all clients if !IsPomodoroPaused() { - shared.Message.Mode = mode shared.Message.TimeLeft = remainingSeconds time.Sleep(time.Second) remainingSeconds-- @@ -26,7 +25,7 @@ func startTimer(remainingSeconds int, mode string) bool { } } - // Final shared.when time reaches zero + // Final shared.Message when time reaches zero shared.Message.TimeLeft = remainingSeconds return true From 28342058aa7d8ee5c2f4b743a175d9f732eb06ce Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 09:37:07 +0200 Subject: [PATCH 6/9] doc: update comments --- internal/websocket/broadcast.go | 2 +- internal/websocket/client_commands.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/websocket/broadcast.go b/internal/websocket/broadcast.go index 728726a..02b7f0a 100644 --- a/internal/websocket/broadcast.go +++ b/internal/websocket/broadcast.go @@ -8,7 +8,7 @@ import ( "time" ) -// BroadcastMessage sends a message to all connected WebSocket clients. +// sends continous messages to all connected WebSocket clients. func SendPermanentBroadCastMessage() { for { // Marshal the message into JSON format diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index df4fdc3..17560aa 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -10,7 +10,7 @@ import ( var unsetPomodoroConfig models.GoTomatoPomodoroConfig // used to check if client passed a config json -// handleClientCommands listens for commands from WebSocket clients and dispatches to the timer. +// handleClientCommands listens for commands from WebSocket clients func handleClientCommands(ws *websocket.Conn) { for { var clientCommand models.ClientCommand From a9d145ee713b2b3511fc5e7c61064c8ca7c96359 Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 09:46:16 +0200 Subject: [PATCH 7/9] feat: create shared config defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add shared configuration defaults for server and pomodoro 🤖 --- cmd/server/main.go | 5 +++-- internal/shared/configDefaults.go | 20 ++++++++++++++++++++ internal/websocket/client_commands.go | 12 +++--------- 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 internal/shared/configDefaults.go diff --git a/cmd/server/main.go b/cmd/server/main.go index b1d9db8..9ce6580 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package server import ( "flag" "fmt" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "log" @@ -11,8 +12,8 @@ import ( func Start() { // Define CLI flags for ListenAddress and ListenPort - listenAddress := flag.String("listenAddress", "0.0.0.0", "IP address to listen on") - listenPort := flag.Int("listenPort", 8080, "Port to listen on") + listenAddress := flag.String("listenAddress", shared.DefaultServerConfig.ListenAddress, "IP address to listen on") + listenPort := flag.Int("listenPort", shared.DefaultServerConfig.ListenPort, "Port to listen on") flag.Parse() serverConfig := models.GoTomatoServerConfig{ diff --git a/internal/shared/configDefaults.go b/internal/shared/configDefaults.go new file mode 100644 index 0000000..ba4be6c --- /dev/null +++ b/internal/shared/configDefaults.go @@ -0,0 +1,20 @@ +package shared + +import ( + "git.smsvc.net/pomodoro/GoTomato/pkg/models" +) + +var DefaultServerConfig = models.GoTomatoServerConfig{ + ListenAddress: "0.0.0.0", + ListenPort: 8080, +} + +var DefaultPomodoroConfig = models.GoTomatoPomodoroConfig{ + Work: 25 * 60, + ShortBreak: 5 * 60, + LongBreak: 15 * 60, + Sessions: 4, +} + +// used to check if client passed a config json +var UnsetPomodoroConfig models.GoTomatoPomodoroConfig diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index 17560aa..e1ff8b0 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -3,23 +3,17 @@ package websocket import ( "encoding/json" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" ) -var unsetPomodoroConfig models.GoTomatoPomodoroConfig // used to check if client passed a config json - // handleClientCommands listens for commands from WebSocket clients func handleClientCommands(ws *websocket.Conn) { for { var clientCommand models.ClientCommand - var pomodoroConfig = models.GoTomatoPomodoroConfig{ - Work: 25 * 60, - ShortBreak: 5 * 60, - LongBreak: 15 * 60, - Sessions: 4, - } + var pomodoroConfig = shared.DefaultPomodoroConfig _, message, err := ws.ReadMessage() if err != nil { @@ -39,7 +33,7 @@ func handleClientCommands(ws *websocket.Conn) { switch clientCommand.Command { case "start": if !pomodoro.IsPomodoroOngoing() { - if clientCommand.Config != unsetPomodoroConfig { + if clientCommand.Config != shared.UnsetPomodoroConfig { pomodoroConfig = clientCommand.Config } go pomodoro.RunPomodoro(pomodoroConfig) // Start the timer with the list of clients From b7d03aa1d8f35e095ce4f9c48ae01e8dd2477e5f Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 13:14:26 +0200 Subject: [PATCH 8/9] break: empty Message.Mode when no pomodoro ongoing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change "none" to an empty string for the mode field - update README to reflect the new mode representation - ensure consistency across session end/reset and welcome messages 🤖 --- README.md | 6 +++--- internal/pomodoro/pomodoro.go | 2 +- internal/shared/state.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a7c41f3..955b001 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Example: The server periodically (every second) sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. These messages contain the following fields: -- mode: Indicates the current phase of the Pomodoro session ("Work", "ShortBreak", "LongBreak", or "none" if no session is running). +- mode: Indicates the current phase of the Pomodoro session ("Work", "ShortBreak", "LongBreak", or empty if no session is running). - session: The current session number (e.g., 1 for the first work session). - total_sessions: The total number of sessions for the current Pomodoro cycle (e.g., 4 if the cycle consists of 4 work/break sessions). - time_left: The remaining time for the current mode, in seconds (e.g., 900 for 15 minutes). @@ -62,11 +62,11 @@ These messages contain the following fields: | Message Type | Example | | --------------------- | --------------------------------------------------- | -| Welcome Message | `{"mode": "none", "session":0, "total_sessions":0, "time_left":0, "ongoing": false, "paused": false}` | +| Welcome Message | `{"mode": "", "session":0, "total_sessions":0, "time_left":0, "ongoing": false, "paused": false}` | | Session Running | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900, "ongoing": true, "paused": false}` | | Session Running | `{"mode": "ShortBreak", "session": 2, "total_sessions": 4, "time_left": 50, "ongoing": true, "paused": false}` | | Session Paused | `{"mode": "Work", "session": 2, "total_sessions": 4, "time_left": 456, "ongoing": true, "paused": true}` | -| Session End/Reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0, "ongoing": false, "paused": false}` | +| Session End/Reset | `{"mode": "", "session": 0, "total_sessions": 0, "time_left": 0, "ongoing": false, "paused": false}` | ## Testing diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index c5e927e..2c69b65 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -56,7 +56,7 @@ func ResetPomodoro() { mu.Unlock() // Reset message - shared.Message.Mode = "none" + shared.Message.Mode = "" shared.Message.Session = 0 shared.Message.TotalSession = 0 shared.Message.TimeLeft = 0 diff --git a/internal/shared/state.go b/internal/shared/state.go index 26ff2d6..3c0ddec 100644 --- a/internal/shared/state.go +++ b/internal/shared/state.go @@ -5,7 +5,7 @@ import ( ) var Message = models.ServerMessage{ - Mode: "none", + Mode: "", Session: 0, TotalSession: 0, TimeLeft: 0, From bf2685a055aecb7626f009e0a8f4e5c37bd8897c Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Mon, 21 Oct 2024 13:42:32 +0200 Subject: [PATCH 9/9] fix: send correct server message on pomodoro end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replace manual state reset with a dedicated ResetToDefault function - remove locking mechanism during state updates 🤖 --- internal/pomodoro/pomodoro.go | 16 ++-------------- internal/shared/state.go | 18 +++++++++++------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 2c69b65..702d0f7 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -40,26 +40,14 @@ func RunPomodoro(config models.GoTomatoPomodoroConfig) { } } - mu.Lock() - shared.Message.Ongoing = false - mu.Unlock() + shared.Message = shared.ResetToDefault() } // ResetPomodoro resets the running Pomodoro timer. func ResetPomodoro() { // Send a reset signal to stop any running timers pomodoroResetChannel <- true - - mu.Lock() - shared.Message.Ongoing = false - shared.Message.Paused = false - mu.Unlock() - - // Reset message - shared.Message.Mode = "" - shared.Message.Session = 0 - shared.Message.TotalSession = 0 - shared.Message.TimeLeft = 0 + shared.Message = shared.ResetToDefault() } func PausePomodoro() { diff --git a/internal/shared/state.go b/internal/shared/state.go index 3c0ddec..87e8759 100644 --- a/internal/shared/state.go +++ b/internal/shared/state.go @@ -4,11 +4,15 @@ import ( "git.smsvc.net/pomodoro/GoTomato/pkg/models" ) -var Message = models.ServerMessage{ - Mode: "", - Session: 0, - TotalSession: 0, - TimeLeft: 0, - Ongoing: false, - Paused: false, +var Message = ResetToDefault() + +func ResetToDefault() models.ServerMessage { + return models.ServerMessage{ + Mode: "", + Session: 0, + TotalSession: 0, + TimeLeft: 0, + Ongoing: false, + Paused: false, + } }