├── 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 |
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 |
10 | {{template "[[.]]" .Data}}
11 |
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 | ```
--------------------------------------------------------------------------------