├── .travis.yml ├── LICENSE ├── README.md ├── sse-decoder.go ├── sse-decoder_test.go ├── sse-encoder.go ├── sse_test.go └── writer.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.3 5 | - 1.4 6 | - tip 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Martínez-Almeida 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Server-Sent Events [![GoDoc](https://godoc.org/github.com/manucorporat/sse?status.svg)](https://godoc.org/github.com/manucorporat/sse) [![Build Status](https://travis-ci.org/manucorporat/sse.svg)](https://travis-ci.org/manucorporat/sse) 2 | 3 | Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. The Server-Sent Events EventSource API is [standardized as part of HTML5[1] by the W3C](http://www.w3.org/TR/2009/WD-eventsource-20091029/). 4 | 5 | - [Real world demostration using Gin](http://sse.getgin.io/) 6 | - [Read this great SSE introduction by the HTML5Rocks guys](http://www.html5rocks.com/en/tutorials/eventsource/basics/) 7 | - [Browser support](http://caniuse.com/#feat=eventsource) 8 | 9 | ##Sample code 10 | 11 | ```go 12 | import "github.com/manucorporat/sse" 13 | 14 | func httpHandler(w http.ResponseWriter, req *http.Request) { 15 | // data can be a primitive like a string, an integer or a float 16 | sse.Encode(w, sse.Event{ 17 | Event: "message", 18 | Data: "some data\nmore data", 19 | }) 20 | 21 | // also a complex type, like a map, a struct or a slice 22 | sse.Encode(w, sse.Event{ 23 | Id: "124", 24 | Event: "message", 25 | Data: map[string]interface{}{ 26 | "user": "manu", 27 | "date": time.Now().Unix(), 28 | "content": "hi!", 29 | }, 30 | }) 31 | } 32 | ``` 33 | ``` 34 | event: message 35 | data: some data\\nmore data 36 | 37 | id: 124 38 | event: message 39 | data: {"content":"hi!","date":1431540810,"user":"manu"} 40 | 41 | ``` 42 | 43 | ##Content-Type 44 | 45 | ```go 46 | fmt.Println(sse.ContentType) 47 | ``` 48 | ``` 49 | text/event-stream 50 | ``` 51 | 52 | ##Decoding support 53 | 54 | There is a client-side implementation of SSE coming soon. -------------------------------------------------------------------------------- /sse-decoder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sse 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "io/ioutil" 11 | ) 12 | 13 | type decoder struct { 14 | events []Event 15 | } 16 | 17 | func Decode(r io.Reader) ([]Event, error) { 18 | var dec decoder 19 | return dec.decode(r) 20 | } 21 | 22 | func (d *decoder) dispatchEvent(event Event, data string) { 23 | dataLength := len(data) 24 | if dataLength > 0 { 25 | //If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer. 26 | data = data[:dataLength-1] 27 | dataLength-- 28 | } 29 | if dataLength == 0 && event.Event == "" { 30 | return 31 | } 32 | if event.Event == "" { 33 | event.Event = "message" 34 | } 35 | event.Data = data 36 | d.events = append(d.events, event) 37 | } 38 | 39 | func (d *decoder) decode(r io.Reader) ([]Event, error) { 40 | buf, err := ioutil.ReadAll(r) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | var currentEvent Event 46 | var dataBuffer *bytes.Buffer = new(bytes.Buffer) 47 | // TODO (and unit tests) 48 | // Lines must be separated by either a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair, 49 | // a single U+000A LINE FEED (LF) character, 50 | // or a single U+000D CARRIAGE RETURN (CR) character. 51 | lines := bytes.Split(buf, []byte{'\n'}) 52 | for _, line := range lines { 53 | if len(line) == 0 { 54 | // If the line is empty (a blank line). Dispatch the event. 55 | d.dispatchEvent(currentEvent, dataBuffer.String()) 56 | 57 | // reset current event and data buffer 58 | currentEvent = Event{} 59 | dataBuffer.Reset() 60 | continue 61 | } 62 | if line[0] == byte(':') { 63 | // If the line starts with a U+003A COLON character (:), ignore the line. 64 | continue 65 | } 66 | 67 | var field, value []byte 68 | colonIndex := bytes.IndexRune(line, ':') 69 | if colonIndex != -1 { 70 | // If the line contains a U+003A COLON character character (:) 71 | // Collect the characters on the line before the first U+003A COLON character (:), 72 | // and let field be that string. 73 | field = line[:colonIndex] 74 | // Collect the characters on the line after the first U+003A COLON character (:), 75 | // and let value be that string. 76 | value = line[colonIndex+1:] 77 | // If value starts with a single U+0020 SPACE character, remove it from value. 78 | if len(value) > 0 && value[0] == ' ' { 79 | value = value[1:] 80 | } 81 | } else { 82 | // Otherwise, the string is not empty but does not contain a U+003A COLON character character (:) 83 | // Use the whole line as the field name, and the empty string as the field value. 84 | field = line 85 | value = []byte{} 86 | } 87 | // The steps to process the field given a field name and a field value depend on the field name, 88 | // as given in the following list. Field names must be compared literally, 89 | // with no case folding performed. 90 | switch string(field) { 91 | case "event": 92 | // Set the event name buffer to field value. 93 | currentEvent.Event = string(value) 94 | case "id": 95 | // Set the event stream's last event ID to the field value. 96 | currentEvent.Id = string(value) 97 | case "retry": 98 | // If the field value consists of only characters in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9), 99 | // then interpret the field value as an integer in base ten, and set the event stream's reconnection time to that integer. 100 | // Otherwise, ignore the field. 101 | currentEvent.Id = string(value) 102 | case "data": 103 | // Append the field value to the data buffer, 104 | dataBuffer.Write(value) 105 | // then append a single U+000A LINE FEED (LF) character to the data buffer. 106 | dataBuffer.WriteString("\n") 107 | default: 108 | //Otherwise. The field is ignored. 109 | continue 110 | } 111 | } 112 | // Once the end of the file is reached, the user agent must dispatch the event one final time. 113 | d.dispatchEvent(currentEvent, dataBuffer.String()) 114 | 115 | return d.events, nil 116 | } 117 | -------------------------------------------------------------------------------- /sse-decoder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sse 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestDecodeSingle1(t *testing.T) { 15 | events, err := Decode(bytes.NewBufferString( 16 | `data: this is a text 17 | event: message 18 | fake: 19 | id: 123456789010 20 | : we can append data 21 | : and multiple comments should not break it 22 | data: a very nice one`)) 23 | 24 | assert.NoError(t, err) 25 | assert.Len(t, events, 1) 26 | assert.Equal(t, events[0].Event, "message") 27 | assert.Equal(t, events[0].Id, "123456789010") 28 | } 29 | 30 | func TestDecodeSingle2(t *testing.T) { 31 | events, err := Decode(bytes.NewBufferString( 32 | `: starting with a comment 33 | fake: 34 | 35 | data:this is a \ntext 36 | event:a message\n\n 37 | fake 38 | :and multiple comments\n should not break it\n\n 39 | id:1234567890\n10 40 | :we can append data 41 | data:a very nice one\n! 42 | 43 | 44 | `)) 45 | assert.NoError(t, err) 46 | assert.Len(t, events, 1) 47 | assert.Equal(t, events[0].Event, "a message\\n\\n") 48 | assert.Equal(t, events[0].Id, "1234567890\\n10") 49 | } 50 | 51 | func TestDecodeSingle3(t *testing.T) { 52 | events, err := Decode(bytes.NewBufferString( 53 | ` 54 | id:123456ABCabc789010 55 | event: message123 56 | : we can append data 57 | data:this is a text 58 | data: a very nice one 59 | data: 60 | data 61 | : ending with a comment`)) 62 | 63 | assert.NoError(t, err) 64 | assert.Len(t, events, 1) 65 | assert.Equal(t, events[0].Event, "message123") 66 | assert.Equal(t, events[0].Id, "123456ABCabc789010") 67 | } 68 | 69 | func TestDecodeMulti1(t *testing.T) { 70 | events, err := Decode(bytes.NewBufferString( 71 | ` 72 | id: 73 | event: weird event 74 | data:this is a text 75 | :data: this should NOT APER 76 | data: second line 77 | 78 | : a comment 79 | event: message 80 | id:123 81 | data:this is a text 82 | :data: this should NOT APER 83 | data: second line 84 | 85 | 86 | : a comment 87 | event: message 88 | id:123 89 | data:this is a text 90 | data: second line 91 | 92 | :hola 93 | 94 | data 95 | 96 | event: 97 | 98 | id`)) 99 | assert.NoError(t, err) 100 | assert.Len(t, events, 3) 101 | assert.Equal(t, events[0].Event, "weird event") 102 | assert.Equal(t, events[0].Id, "") 103 | } 104 | 105 | func TestDecodeW3C(t *testing.T) { 106 | events, err := Decode(bytes.NewBufferString( 107 | `data 108 | 109 | data 110 | data 111 | 112 | data: 113 | `)) 114 | assert.NoError(t, err) 115 | assert.Len(t, events, 1) 116 | } 117 | -------------------------------------------------------------------------------- /sse-encoder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sse 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "reflect" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | // Server-Sent Events 18 | // W3C Working Draft 29 October 2009 19 | // http://www.w3.org/TR/2009/WD-eventsource-20091029/ 20 | 21 | const ContentType = "text/event-stream" 22 | 23 | var contentType = []string{ContentType} 24 | var noCache = []string{"no-cache"} 25 | 26 | var fieldReplacer = strings.NewReplacer( 27 | "\n", "\\n", 28 | "\r", "\\r") 29 | 30 | var dataReplacer = strings.NewReplacer( 31 | "\n", "\ndata:", 32 | "\r", "\\r") 33 | 34 | type Event struct { 35 | Event string 36 | Id string 37 | Retry uint 38 | Data interface{} 39 | } 40 | 41 | func Encode(writer io.Writer, event Event) error { 42 | w := checkWriter(writer) 43 | writeId(w, event.Id) 44 | writeEvent(w, event.Event) 45 | writeRetry(w, event.Retry) 46 | return writeData(w, event.Data) 47 | } 48 | 49 | func writeId(w stringWriter, id string) { 50 | if len(id) > 0 { 51 | w.WriteString("id:") 52 | fieldReplacer.WriteString(w, id) 53 | w.WriteString("\n") 54 | } 55 | } 56 | 57 | func writeEvent(w stringWriter, event string) { 58 | if len(event) > 0 { 59 | w.WriteString("event:") 60 | fieldReplacer.WriteString(w, event) 61 | w.WriteString("\n") 62 | } 63 | } 64 | 65 | func writeRetry(w stringWriter, retry uint) { 66 | if retry > 0 { 67 | w.WriteString("retry:") 68 | w.WriteString(strconv.FormatUint(uint64(retry), 10)) 69 | w.WriteString("\n") 70 | } 71 | } 72 | 73 | func writeData(w stringWriter, data interface{}) error { 74 | w.WriteString("data:") 75 | switch kindOfData(data) { 76 | case reflect.Struct, reflect.Slice, reflect.Map: 77 | err := json.NewEncoder(w).Encode(data) 78 | if err != nil { 79 | return err 80 | } 81 | w.WriteString("\n") 82 | default: 83 | dataReplacer.WriteString(w, fmt.Sprint(data)) 84 | w.WriteString("\n\n") 85 | } 86 | return nil 87 | } 88 | 89 | func (r Event) Render(w http.ResponseWriter) error { 90 | header := w.Header() 91 | header["Content-Type"] = contentType 92 | 93 | if _, exist := header["Cache-Control"]; !exist { 94 | header["Cache-Control"] = noCache 95 | } 96 | return Encode(w, r) 97 | } 98 | 99 | func kindOfData(data interface{}) reflect.Kind { 100 | value := reflect.ValueOf(data) 101 | valueType := value.Kind() 102 | if valueType == reflect.Ptr { 103 | valueType = value.Elem().Kind() 104 | } 105 | return valueType 106 | } 107 | -------------------------------------------------------------------------------- /sse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sse 6 | 7 | import ( 8 | "bytes" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestEncodeOnlyData(t *testing.T) { 16 | w := new(bytes.Buffer) 17 | event := Event{ 18 | Data: "junk\n\njk\nid:fake", 19 | } 20 | err := Encode(w, event) 21 | assert.NoError(t, err) 22 | assert.Equal(t, w.String(), 23 | `data:junk 24 | data: 25 | data:jk 26 | data:id:fake 27 | 28 | `) 29 | 30 | decoded, _ := Decode(w) 31 | assert.Equal(t, decoded, []Event{event}) 32 | } 33 | 34 | func TestEncodeWithEvent(t *testing.T) { 35 | w := new(bytes.Buffer) 36 | event := Event{ 37 | Event: "t\n:<>\r\test", 38 | Data: "junk\n\njk\nid:fake", 39 | } 40 | err := Encode(w, event) 41 | assert.NoError(t, err) 42 | assert.Equal(t, w.String(), 43 | `event:t\n:<>\r est 44 | data:junk 45 | data: 46 | data:jk 47 | data:id:fake 48 | 49 | `) 50 | 51 | decoded, _ := Decode(w) 52 | assert.Equal(t, decoded, []Event{event}) 53 | } 54 | 55 | func TestEncodeWithId(t *testing.T) { 56 | w := new(bytes.Buffer) 57 | err := Encode(w, Event{ 58 | Id: "t\n:<>\r\test", 59 | Data: "junk\n\njk\nid:fa\rke", 60 | }) 61 | assert.NoError(t, err) 62 | assert.Equal(t, w.String(), 63 | `id:t\n:<>\r est 64 | data:junk 65 | data: 66 | data:jk 67 | data:id:fa\rke 68 | 69 | `) 70 | } 71 | 72 | func TestEncodeWithRetry(t *testing.T) { 73 | w := new(bytes.Buffer) 74 | err := Encode(w, Event{ 75 | Retry: 11, 76 | Data: "junk\n\njk\nid:fake\n", 77 | }) 78 | assert.NoError(t, err) 79 | assert.Equal(t, w.String(), 80 | `retry:11 81 | data:junk 82 | data: 83 | data:jk 84 | data:id:fake 85 | data: 86 | 87 | `) 88 | } 89 | 90 | func TestEncodeWithEverything(t *testing.T) { 91 | w := new(bytes.Buffer) 92 | err := Encode(w, Event{ 93 | Event: "abc", 94 | Id: "12345", 95 | Retry: 10, 96 | Data: "some data", 97 | }) 98 | assert.NoError(t, err) 99 | assert.Equal(t, w.String(), "id:12345\nevent:abc\nretry:10\ndata:some data\n\n") 100 | } 101 | 102 | func TestEncodeMap(t *testing.T) { 103 | w := new(bytes.Buffer) 104 | err := Encode(w, Event{ 105 | Event: "a map", 106 | Data: map[string]interface{}{ 107 | "foo": "b\n\rar", 108 | "bar": "id: 2", 109 | }, 110 | }) 111 | assert.NoError(t, err) 112 | assert.Equal(t, w.String(), "event:a map\ndata:{\"bar\":\"id: 2\",\"foo\":\"b\\n\\rar\"}\n\n") 113 | } 114 | 115 | func TestEncodeSlice(t *testing.T) { 116 | w := new(bytes.Buffer) 117 | err := Encode(w, Event{ 118 | Event: "a slice", 119 | Data: []interface{}{1, "text", map[string]interface{}{"foo": "bar"}}, 120 | }) 121 | assert.NoError(t, err) 122 | assert.Equal(t, w.String(), "event:a slice\ndata:[1,\"text\",{\"foo\":\"bar\"}]\n\n") 123 | } 124 | 125 | func TestEncodeStruct(t *testing.T) { 126 | myStruct := struct { 127 | A int 128 | B string `json:"value"` 129 | }{1, "number"} 130 | 131 | w := new(bytes.Buffer) 132 | err := Encode(w, Event{ 133 | Event: "a struct", 134 | Data: myStruct, 135 | }) 136 | assert.NoError(t, err) 137 | assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n") 138 | 139 | w.Reset() 140 | err = Encode(w, Event{ 141 | Event: "a struct", 142 | Data: &myStruct, 143 | }) 144 | assert.NoError(t, err) 145 | assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n") 146 | } 147 | 148 | func TestEncodeInteger(t *testing.T) { 149 | w := new(bytes.Buffer) 150 | err := Encode(w, Event{ 151 | Event: "an integer", 152 | Data: 1, 153 | }) 154 | assert.NoError(t, err) 155 | assert.Equal(t, w.String(), "event:an integer\ndata:1\n\n") 156 | } 157 | 158 | func TestEncodeFloat(t *testing.T) { 159 | w := new(bytes.Buffer) 160 | err := Encode(w, Event{ 161 | Event: "Float", 162 | Data: 1.5, 163 | }) 164 | assert.NoError(t, err) 165 | assert.Equal(t, w.String(), "event:Float\ndata:1.5\n\n") 166 | } 167 | 168 | func TestEncodeStream(t *testing.T) { 169 | w := new(bytes.Buffer) 170 | 171 | Encode(w, Event{ 172 | Event: "float", 173 | Data: 1.5, 174 | }) 175 | 176 | Encode(w, Event{ 177 | Id: "123", 178 | Data: map[string]interface{}{"foo": "bar", "bar": "foo"}, 179 | }) 180 | 181 | Encode(w, Event{ 182 | Id: "124", 183 | Event: "chat", 184 | Data: "hi! dude", 185 | }) 186 | assert.Equal(t, w.String(), "event:float\ndata:1.5\n\nid:123\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\nid:124\nevent:chat\ndata:hi! dude\n\n") 187 | } 188 | 189 | func TestRenderSSE(t *testing.T) { 190 | w := httptest.NewRecorder() 191 | 192 | err := (Event{ 193 | Event: "msg", 194 | Data: "hi! how are you?", 195 | }).Render(w) 196 | 197 | assert.NoError(t, err) 198 | assert.Equal(t, w.Body.String(), "event:msg\ndata:hi! how are you?\n\n") 199 | assert.Equal(t, w.Header().Get("Content-Type"), "text/event-stream") 200 | assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache") 201 | } 202 | 203 | func BenchmarkResponseWriter(b *testing.B) { 204 | w := httptest.NewRecorder() 205 | b.ResetTimer() 206 | b.ReportAllocs() 207 | for i := 0; i < b.N; i++ { 208 | (Event{ 209 | Event: "new_message", 210 | Data: "hi! how are you? I am fine. this is a long stupid message!!!", 211 | }).Render(w) 212 | } 213 | } 214 | 215 | func BenchmarkFullSSE(b *testing.B) { 216 | buf := new(bytes.Buffer) 217 | b.ResetTimer() 218 | b.ReportAllocs() 219 | for i := 0; i < b.N; i++ { 220 | Encode(buf, Event{ 221 | Event: "new_message", 222 | Id: "13435", 223 | Retry: 10, 224 | Data: "hi! how are you? I am fine. this is a long stupid message!!!", 225 | }) 226 | buf.Reset() 227 | } 228 | } 229 | 230 | func BenchmarkNoRetrySSE(b *testing.B) { 231 | buf := new(bytes.Buffer) 232 | b.ResetTimer() 233 | b.ReportAllocs() 234 | for i := 0; i < b.N; i++ { 235 | Encode(buf, Event{ 236 | Event: "new_message", 237 | Id: "13435", 238 | Data: "hi! how are you? I am fine. this is a long stupid message!!!", 239 | }) 240 | buf.Reset() 241 | } 242 | } 243 | 244 | func BenchmarkSimpleSSE(b *testing.B) { 245 | buf := new(bytes.Buffer) 246 | b.ResetTimer() 247 | b.ReportAllocs() 248 | for i := 0; i < b.N; i++ { 249 | Encode(buf, Event{ 250 | Event: "new_message", 251 | Data: "hi! how are you? I am fine. this is a long stupid message!!!", 252 | }) 253 | buf.Reset() 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import "io" 4 | 5 | type stringWriter interface { 6 | io.Writer 7 | WriteString(string) (int, error) 8 | } 9 | 10 | type stringWrapper struct { 11 | io.Writer 12 | } 13 | 14 | func (w stringWrapper) WriteString(str string) (int, error) { 15 | return w.Writer.Write([]byte(str)) 16 | } 17 | 18 | func checkWriter(writer io.Writer) stringWriter { 19 | if w, ok := writer.(stringWriter); ok { 20 | return w 21 | } else { 22 | return stringWrapper{writer} 23 | } 24 | } 25 | --------------------------------------------------------------------------------