package main import ( "fmt" "html/template" "log" "net/http" ) type handler = func(http.ResponseWriter, *http.Request) func generateHandler( file string, handler func(http.ResponseWriter, *http.Request), data func(http.ResponseWriter, *http.Request) any, methods []string, ) handler { var tmpl *template.Template if file != "" { tmpl = template.Must(template.ParseFiles(fmt.Sprintf("templates/%s", file))) } return func(w http.ResponseWriter, r *http.Request) { for _, method := range methods { if method == r.Method { goto ok } } w.WriteHeader(http.StatusMethodNotAllowed) return ok: handler(w, r) if tmpl != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data(w, r)) } } } func generateSseHandler(handler handler) handler { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.WriteHeader(http.StatusOK) handler(w, r) } } type client = chan []event type clientSet struct { clients map[client]bool } func newClientSet() clientSet { return clientSet{make(map[client]bool)} } func (clients clientSet) broadcast(events ...event) { for client := range clients.clients { client <- events } } func (clients clientSet) playerCountEvent(offset int) event { playerCount := len(clients.clients) + offset var data string if playerCount == 0 { data = "No players online." } else { data = fmt.Sprintf("%d player%s online.", playerCount, map[bool]string{true: "", false: "s"}[playerCount == 1]) } return event{eventPlayerCount, data} } func (clients clientSet) broadcastPlayerCount(offset int) { clients.broadcast(clients.playerCountEvent(offset)) } func (clients clientSet) broadcastWord(word string) { clients.broadcast( event{eventWord, fmt.Sprintf("
%s
", word)}, event{eventNavigate, fmt.Sprintf("https://jisho.org/search/%s", word)}, ) } func (clients clientSet) connect(client client) { // Channels are blocking. // Client initialization is waiting for connect() to complete. // If we try to braodcast to it before connect() is done, // the channel handling logic wouldn't have started yet, // and broadcast() would be freeze. // Therefore, we must broadcast an offset player count first, // then actually add the client. clients.broadcastPlayerCount(1) clients.clients[client] = true } func (clients clientSet) disconnect(client client) { delete(clients.clients, client) clients.broadcastPlayerCount(0) } var clients = newClientSet() type event struct { eventName string data string } func (event event) encode() string { return fmt.Sprintf("event: %s\ndata: %s\n\n", event.eventName, event.data) } const eventWord = "word" const eventPlayerCount = "players" const eventNavigate = "navigate" func generateWordBroadcaster() handler { return generateSseHandler(func(w http.ResponseWriter, r *http.Request) { sendEvent := func(events ...event) { for _, event := range events { fmt.Fprint(w, event.encode()) } w.(http.Flusher).Flush() } sendEvent(clients.playerCountEvent(1)) ctx := r.Context() channel := make(client) clients.connect(channel) outer: for { select { case <-ctx.Done(): clients.disconnect(channel) break outer case events := <-channel: sendEvent(events...) } } }) } func generateWordHandler() handler { return generateHandler( "", func(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(0) clients.broadcastWord(r.FormValue("word")) }, nil, []string{http.MethodPost}, ) } func main() { http.Handle("/", http.FileServer(http.Dir("static"))) http.HandleFunc("/api/events", generateWordBroadcaster()) http.HandleFunc("/api/submit", generateWordHandler()) log.Fatal(http.ListenAndServe(":3333", nil)) }