├── LICENSE ├── examples ├── hotwire │ ├── main.go │ ├── message │ │ └── message.go │ └── templates │ │ ├── base.temp.html │ │ └── messages.temp.html └── timestamp │ ├── index.temp.html │ └── main.go ├── go.mod ├── go.sum ├── internal └── util │ └── util.go ├── pkg └── turbo │ ├── client.go │ ├── hub.go │ ├── stream.go │ ├── turbo.go │ └── update.go └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Amit Mittal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/hotwire/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | 8 | "github.com/akmittal/turbo-go/examples/hotwire/message" 9 | "github.com/akmittal/turbo-go/pkg/turbo" 10 | "github.com/go-chi/chi" 11 | ) 12 | 13 | var messages []message.Message 14 | 15 | func main() { 16 | 17 | mux := chi.NewMux() 18 | mux.Get("/", getIndex) 19 | 20 | hub := turbo.NewHub() 21 | msgChan := make(chan interface{}) 22 | mux.Post("/send", func(rw http.ResponseWriter, req *http.Request) { 23 | sendMessage(msgChan, hub, rw, req) 24 | }) 25 | go hub.Run() 26 | mux.Get("/socket", func(rw http.ResponseWriter, req *http.Request) { 27 | getSocket(msgChan, hub, rw, req) 28 | }) 29 | http.ListenAndServe(":8000", mux) 30 | } 31 | func getIndex(rw http.ResponseWriter, req *http.Request) { 32 | temp, err := template.ParseFiles("examples/hotwire/templates/messages.temp.html", "examples/hotwire/templates/base.temp.html") 33 | if err != nil { 34 | http.Error(rw, "Error", 400) 35 | } 36 | temp.Execute(rw, messages) 37 | } 38 | func sendMessage(msgChan chan interface{}, hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) { 39 | if err := req.ParseForm(); err != nil { 40 | http.Error(rw, err.Error(), 401) 41 | return 42 | } 43 | 44 | // fmt.Fprintf(rw, "Post from website! r.PostFrom = %v\n", req.PostForm) 45 | var msg message.Message 46 | msg.Text = req.FormValue("message") 47 | 48 | messages = append(messages, msg) 49 | go func() { 50 | msgChan <- msg 51 | }() 52 | fmt.Fprintf(rw, "%s", "Done") 53 | 54 | } 55 | 56 | func getSocket(msgChan chan interface{}, hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) { 57 | temp, _ := template.ParseFiles("examples/hotwire/templates/messages.temp.html") 58 | messageTemp := temp.Lookup("message") 59 | 60 | appendMessage := turbo.Stream{ 61 | Action: turbo.APPEND, 62 | Template: messageTemp, 63 | Target: "messages", 64 | Data: msgChan, 65 | } 66 | 67 | appendMessage.Stream(hub, rw, req) 68 | } 69 | -------------------------------------------------------------------------------- /examples/hotwire/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type Message struct { 4 | Text string 5 | } 6 | 7 | func New(text string) Message { 8 | return Message{text} 9 | } 10 | -------------------------------------------------------------------------------- /examples/hotwire/templates/base.temp.html: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current time 10 | 11 | 12 | 13 |
14 | {{template "main" .}} 15 |
16 | 17 | 18 | 23 | 24 | 25 | {{end}} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/hotwire/templates/messages.temp.html: -------------------------------------------------------------------------------- 1 | {{template "base" .}} {{define "title"}}Home{{end}} {{define "main"}} 2 |

Messages

3 | 4 |
{{range .}} 5 | {{template "message" .}} 6 | 7 | {{end}}
8 |
9 | 10 | 11 |
12 | 13 | {{end}} 14 | {{define "message"}} 15 |
{{.Text}}
16 | {{end}} 17 | 18 | -------------------------------------------------------------------------------- /examples/timestamp/index.temp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Current time 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /examples/timestamp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/akmittal/turbo-go/pkg/turbo" 9 | "github.com/go-chi/chi" 10 | ) 11 | 12 | const temp = `

{{.}}

` 13 | 14 | func main() { 15 | 16 | mux := chi.NewMux() 17 | mux.Get("/", getIndex) 18 | 19 | hub := turbo.NewHub() 20 | go hub.Run() 21 | mux.Get("/socket", func(rw http.ResponseWriter, req *http.Request) { 22 | getSocket(hub, rw, req) 23 | }) 24 | http.ListenAndServe(":8000", mux) 25 | } 26 | func getIndex(rw http.ResponseWriter, req *http.Request) { 27 | temp, _ := template.ParseFiles("examples/timestamp/index.temp.html") 28 | temp.Execute(rw, nil) 29 | } 30 | 31 | func getSocket(hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) { 32 | 33 | parsed, err := template.New("main").Parse(temp) 34 | if err != nil { 35 | http.Error(rw, "Error", 500) 36 | } 37 | 38 | tempChan := make(chan interface{}) 39 | appendMessage := turbo.Stream{ 40 | Action: turbo.UPDATE, 41 | Template: parsed, 42 | Target: "currenttime", 43 | Data: tempChan, 44 | } 45 | 46 | go sendMessages(tempChan) 47 | appendMessage.Stream(hub, rw, req) 48 | 49 | } 50 | func sendMessages(data chan interface{}) { 51 | for { 52 | data <- time.Now().Format("January 02, 2006 15:04:05") 53 | time.Sleep(time.Second) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/akmittal/turbo-go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-chi/chi v1.5.1 // indirect 7 | github.com/gorilla/websocket v1.4.2 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= 2 | github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | ) 7 | 8 | const temp = ` 9 | 12 | ` 13 | 14 | var parsed *template.Template 15 | 16 | func init() { 17 | parsed, _ = template.New("text").Delims("[[", "]]").Parse(temp) 18 | 19 | } 20 | 21 | func WrapTemplateInTurbo(name string) (string, error) { 22 | 23 | var buf bytes.Buffer 24 | 25 | err := parsed.Execute(&buf, name) 26 | return buf.String(), err 27 | } 28 | -------------------------------------------------------------------------------- /pkg/turbo/client.go: -------------------------------------------------------------------------------- 1 | package turbo 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | const ( 10 | // Time allowed to write a message to the peer. 11 | writeWait = 10 * time.Second 12 | 13 | // Time allowed to read the next pong message from the peer. 14 | pongWait = 60 * time.Second 15 | 16 | // Send pings to peer with this period. Must be less than pongWait. 17 | pingPeriod = (pongWait * 9) / 10 18 | 19 | // Maximum message size allowed from peer. 20 | maxMessageSize = 512 21 | ) 22 | 23 | var ( 24 | newline = []byte{'\n'} 25 | space = []byte{' '} 26 | ) 27 | 28 | // Client is a middleman between the websocket connection and the hub. 29 | type Client struct { 30 | hub *Hub 31 | 32 | // The websocket connection. 33 | conn *websocket.Conn 34 | 35 | // Buffered channel of outbound messages. 36 | send chan []byte 37 | } 38 | 39 | func (c *Client) writePump() { 40 | ticker := time.NewTicker(pingPeriod) 41 | defer func() { 42 | ticker.Stop() 43 | c.conn.Close() 44 | }() 45 | for { 46 | select { 47 | case message, ok := <-c.send: 48 | 49 | if !ok { 50 | // The hub closed the channel. 51 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 52 | return 53 | } 54 | 55 | err := c.conn.WriteMessage(1, message) 56 | if err != nil { 57 | return 58 | } 59 | 60 | case <-ticker.C: 61 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 62 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 63 | return 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/turbo/hub.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package turbo 6 | 7 | // Hub maintains the set of active clients and broadcasts messages to the 8 | // clients. 9 | type Hub struct { 10 | // Registered clients. 11 | clients map[*Client]bool 12 | 13 | // Inbound messages from the clients. 14 | broadcast chan []byte 15 | 16 | // Register requests from the clients. 17 | register chan *Client 18 | 19 | // Unregister requests from clients. 20 | unregister chan *Client 21 | } 22 | 23 | //NewHub Create a new hub, Hub keep track of all connected clients 24 | func NewHub() *Hub { 25 | return &Hub{ 26 | broadcast: make(chan []byte), 27 | register: make(chan *Client), 28 | unregister: make(chan *Client), 29 | clients: make(map[*Client]bool), 30 | } 31 | } 32 | 33 | //Run Start the Hub, Running HUb broadcast messages to all connected hubs 34 | func (h *Hub) Run() { 35 | for { 36 | select { 37 | case client := <-h.register: 38 | 39 | h.clients[client] = true 40 | case client := <-h.unregister: 41 | 42 | if _, ok := h.clients[client]; ok { 43 | delete(h.clients, client) 44 | close(client.send) 45 | } 46 | case message := <-h.broadcast: 47 | 48 | for client := range h.clients { 49 | select { 50 | case client.send <- message: 51 | 52 | default: 53 | close(client.send) 54 | delete(h.clients, client) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/turbo/stream.go: -------------------------------------------------------------------------------- 1 | package turbo 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/akmittal/turbo-go/internal/util" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var upgrader = websocket.Upgrader{} // use default options 13 | // Stream Create a new Turbo stream with Action and Data channel. 14 | type Stream struct { 15 | Action Action 16 | Template *template.Template 17 | Target string 18 | Data chan interface{} 19 | } 20 | 21 | // Stream start streaming messages to all hub clients 22 | func (s *Stream) Stream(hub *Hub, rw http.ResponseWriter, req *http.Request) { 23 | 24 | var turboTemplate, err = util.WrapTemplateInTurbo(s.Template.Name()) 25 | if err != nil { 26 | http.Error(rw, "Error", 500) 27 | } 28 | conn, err := upgrader.Upgrade(rw, req, nil) 29 | if err != nil { 30 | log.Println(err) 31 | return 32 | } 33 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} 34 | hub.register <- client 35 | 36 | temp, err := s.Template.New("userTemplate").Parse(turboTemplate) 37 | go client.writePump() 38 | 39 | if err != nil { 40 | http.Error(rw, "Error parsing template", 500) 41 | } 42 | for datum := range s.Data { 43 | 44 | turbo := Turbo{ 45 | Action: s.Action, 46 | Template: temp, 47 | Target: s.Target, 48 | Data: datum, 49 | } 50 | turbo.sendSocket(hub) 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/turbo/turbo.go: -------------------------------------------------------------------------------- 1 | package turbo 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "net/http" 7 | ) 8 | 9 | // Turbo Action type, Value can be APPEND|PREPEND|REPLACE|UPDATE|REMOVE 10 | type Action string 11 | 12 | const ( 13 | APPEND Action = "append" 14 | PREPEND Action = "prepend" 15 | REPLACE Action = "replace" 16 | UPDATE Action = "update" 17 | REMOVE Action = "remove" 18 | ) 19 | 20 | var parsedTemp *template.Template 21 | 22 | //Turbo Create a new Turbo update 23 | type Turbo struct { 24 | Action Action 25 | Template *template.Template 26 | Target string 27 | Data interface{} 28 | } 29 | 30 | func (h *Turbo) SetHeader(rw http.ResponseWriter) { 31 | rw.Header().Add("Content-type", "text/vnd.turbo-stream.html") 32 | } 33 | 34 | //Send sends turbo template as HTTP response 35 | func (h *Turbo) Send(rw http.ResponseWriter) { 36 | rw.Header().Add("Content-type", "text/vnd.turbo-stream.html") 37 | h.Template.Execute(rw, h) 38 | } 39 | func (h *Turbo) sendSocket(hub *Hub) { 40 | var buf bytes.Buffer 41 | h.Template.Execute(&buf, h) 42 | hub.broadcast <- buf.Bytes() 43 | } 44 | -------------------------------------------------------------------------------- /pkg/turbo/update.go: -------------------------------------------------------------------------------- 1 | package turbo 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | 7 | "github.com/akmittal/turbo-go/internal/util" 8 | ) 9 | 10 | type Update struct { 11 | Action Action 12 | Template *template.Template 13 | Target string 14 | Data interface{} 15 | } 16 | 17 | func (u *Update) Send(rw http.ResponseWriter, req *http.Request) { 18 | var turboTemplate, err = util.WrapTemplateInTurbo(u.Template.Name()) 19 | 20 | parsed, err := u.Template.New("userTemplate").Parse(turboTemplate) 21 | if err != nil { 22 | http.Error(rw, "Error parsing template", 500) 23 | } 24 | 25 | turbo := Turbo{ 26 | Action: u.Action, 27 | Template: parsed, 28 | Target: u.Target, 29 | Data: u.Data, 30 | } 31 | turbo.Send(rw) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Turbo-go 2 | 3 | Build hotwire applications using Go 4 | 5 | 6 | 7 | ## Example 8 | Examples are in [examples](http://github.com/akmittal/turbo-go/tree/master/examples) directory 9 | 10 | ## Install turbo-go 11 | ``` text 12 | go get github.com/akmittal/turbo-go 13 | ``` 14 | 15 | ## API 16 | ```github.com/akmittal/turbo-go/pkg/turbo``` 17 | 18 | Send single template update 19 | ``` go 20 | messageTemp, err := template.New("message").parse(`
{{.}}
`) 21 | data := time.Now() 22 | turbo := turbo.Turbo{ 23 | Action: turbo.APPEND, // Action can be UPDATE, APPEND, PREPEND, REPLACE, REMOVE 24 | Template: messageTemp, 25 | Target: "messages", 26 | Data: data, 27 | } 28 | ``` 29 | 30 | Send stream of templates 31 | 32 | ``` go 33 | func main(){ 34 | // Create hub 35 | hub := turbo.NewHub() 36 | go hub.Run() 37 | mux.Get("/socket", func(rw http.ResponseWriter, req *http.Request) { 38 | getSocket(msgChan, hub, rw, req) 39 | }) 40 | } 41 | 42 | func getSocket(msgChan chan interface{}, hub *turbo.Hub, rw http.ResponseWriter, req *http.Request) { 43 | temp, _ := template.ParseFiles("templates/messages.temp.html") 44 | messageTemp := temp.Lookup("message") 45 | 46 | appendMessage := turbo.Stream{ 47 | Action: turbo.APPEND, 48 | Template: messageTemp, 49 | Target: "messages", 50 | Data: msgChan, 51 | } 52 | 53 | appendMessage.Stream(hub, rw, req) 54 | } 55 | 56 | 57 | ``` --------------------------------------------------------------------------------