├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── consumer.go ├── doc.go ├── eventsource.go ├── eventsource_test.go ├── examples ├── greeter.go └── public │ └── index.html └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Sublime Text 2 files 2 | /*.sublime-* 3 | 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1 5 | - 1.2 6 | - 1.3 7 | - tip 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2014 Anton Ageev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eventsource 2 | 3 | [](https://travis-ci.org/antage/eventsource) 4 | 5 | _eventsource_ provides server-sent events for net/http server. 6 | 7 | ## Usage 8 | 9 | ### SSE with default options 10 | 11 | ``` go 12 | package main 13 | 14 | import ( 15 | "gopkg.in/antage/eventsource.v1" 16 | "log" 17 | "net/http" 18 | "strconv" 19 | "time" 20 | ) 21 | 22 | func main() { 23 | es := eventsource.New(nil, nil) 24 | defer es.Close() 25 | http.Handle("/events", es) 26 | go func() { 27 | id := 1 28 | for { 29 | es.SendEventMessage("tick", "tick-event", strconv.Itoa(id)) 30 | id++ 31 | time.Sleep(2 * time.Second) 32 | } 33 | }() 34 | log.Fatal(http.ListenAndServe(":8080", nil)) 35 | } 36 | ``` 37 | 38 | ### SSE with custom options 39 | 40 | ``` go 41 | package main 42 | 43 | import ( 44 | "gopkg.in/antage/eventsource.v1" 45 | "log" 46 | "net/http" 47 | "strconv" 48 | "time" 49 | ) 50 | 51 | func main() { 52 | es := eventsource.New( 53 | &eventsource.Settings{ 54 | Timeout: 5 * time.Second, 55 | CloseOnTimeout: false, 56 | IdleTimeout: 30 * time.Minute, 57 | }, nil) 58 | es.SendRetryMessage(3 * time.Second) 59 | defer es.Close() 60 | http.Handle("/events", es) 61 | go func() { 62 | id := 1 63 | for { 64 | es.SendEventMessage("tick", "tick-event", strconv.Itoa(id)) 65 | id++ 66 | time.Sleep(2 * time.Second) 67 | } 68 | }() 69 | log.Fatal(http.ListenAndServe(":8080", nil)) 70 | } 71 | ``` 72 | 73 | ### SSE with custom HTTP headers 74 | 75 | ``` go 76 | package main 77 | 78 | import ( 79 | "gopkg.in/antage/eventsource.v1" 80 | "log" 81 | "net/http" 82 | "strconv" 83 | "time" 84 | ) 85 | 86 | func main() { 87 | es := eventsource.New( 88 | eventsource.DefaultSettings(), 89 | func(req *http.Request) [][]byte { 90 | return [][]byte{ 91 | []byte("X-Accel-Buffering: no"), 92 | []byte("Access-Control-Allow-Origin: *"), 93 | } 94 | }, 95 | ) 96 | defer es.Close() 97 | http.Handle("/events", es) 98 | go func() { 99 | id := 1 100 | for { 101 | es.SendEventMessage("tick", "tick-event", strconv.Itoa(id)) 102 | id++ 103 | time.Sleep(2 * time.Second) 104 | } 105 | }() 106 | log.Fatal(http.ListenAndServe(":8080", nil)) 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "net" 7 | "net/http" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type consumer struct { 13 | conn io.WriteCloser 14 | es *eventSource 15 | in chan []byte 16 | staled bool 17 | } 18 | 19 | type gzipConn struct { 20 | net.Conn 21 | *gzip.Writer 22 | } 23 | 24 | func (gc gzipConn) Write(b []byte) (int, error) { 25 | n, err := gc.Writer.Write(b) 26 | if err != nil { 27 | return n, err 28 | } 29 | 30 | return n, gc.Writer.Flush() 31 | } 32 | 33 | func (gc gzipConn) Close() error { 34 | err := gc.Writer.Close() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return gc.Conn.Close() 40 | } 41 | 42 | func newConsumer(resp http.ResponseWriter, req *http.Request, es *eventSource) (*consumer, error) { 43 | conn, _, err := resp.(http.Hijacker).Hijack() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | consumer := &consumer{ 49 | conn: conn, 50 | es: es, 51 | in: make(chan []byte, 10), 52 | staled: false, 53 | } 54 | 55 | _, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n")) 56 | if err != nil { 57 | conn.Close() 58 | return nil, err 59 | } 60 | 61 | _, err = conn.Write([]byte("Vary: Accept-Encoding\r\n")) 62 | if err != nil { 63 | conn.Close() 64 | return nil, err 65 | } 66 | 67 | if es.gzip && (req == nil || strings.Contains(req.Header.Get("Accept-Encoding"), "gzip")) { 68 | _, err = conn.Write([]byte("Content-Encoding: gzip\r\n")) 69 | if err != nil { 70 | conn.Close() 71 | return nil, err 72 | } 73 | 74 | consumer.conn = gzipConn{conn, gzip.NewWriter(conn)} 75 | } 76 | 77 | if es.customHeadersFunc != nil { 78 | for _, header := range es.customHeadersFunc(req) { 79 | _, err = conn.Write(header) 80 | if err != nil { 81 | conn.Close() 82 | return nil, err 83 | } 84 | _, err = conn.Write([]byte("\r\n")) 85 | if err != nil { 86 | conn.Close() 87 | return nil, err 88 | } 89 | } 90 | } 91 | 92 | _, err = conn.Write([]byte("\r\n")) 93 | if err != nil { 94 | conn.Close() 95 | return nil, err 96 | } 97 | 98 | go func() { 99 | idleTimer := time.NewTimer(es.idleTimeout) 100 | defer idleTimer.Stop() 101 | for { 102 | select { 103 | case message, open := <-consumer.in: 104 | if !open { 105 | consumer.conn.Close() 106 | return 107 | } 108 | conn.SetWriteDeadline(time.Now().Add(consumer.es.timeout)) 109 | _, err := consumer.conn.Write(message) 110 | if err != nil { 111 | netErr, ok := err.(net.Error) 112 | if !ok || !netErr.Timeout() || consumer.es.closeOnTimeout { 113 | consumer.staled = true 114 | consumer.conn.Close() 115 | consumer.es.staled <- consumer 116 | return 117 | } 118 | } 119 | idleTimer.Reset(es.idleTimeout) 120 | case <-idleTimer.C: 121 | consumer.conn.Close() 122 | consumer.es.staled <- consumer 123 | return 124 | } 125 | } 126 | }() 127 | 128 | return consumer, nil 129 | } 130 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package http provides server-sent events for net/http server. 3 | 4 | Example: 5 | 6 | package main 7 | 8 | import ( 9 | "gopkg.in/antage/eventsource.v1" 10 | "log" 11 | "net/http" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | es := eventsource.New(nil, nil) 18 | defer es.Close() 19 | http.Handle("/events", es) 20 | go func() { 21 | id := 1 22 | for { 23 | es.SendEventMessage("tick", "tick-event", strconv.Itoa(id)) 24 | id++ 25 | time.Sleep(2 * time.Second) 26 | } 27 | }() 28 | log.Fatal(http.ListenAndServe(":8080", nil)) 29 | } 30 | */ 31 | package eventsource 32 | -------------------------------------------------------------------------------- /eventsource.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type eventMessage struct { 15 | id string 16 | event string 17 | data string 18 | } 19 | 20 | type retryMessage struct { 21 | retry time.Duration 22 | } 23 | 24 | type eventSource struct { 25 | customHeadersFunc func(*http.Request) [][]byte 26 | 27 | sink chan message 28 | staled chan *consumer 29 | add chan *consumer 30 | close chan bool 31 | idleTimeout time.Duration 32 | retry time.Duration 33 | timeout time.Duration 34 | closeOnTimeout bool 35 | gzip bool 36 | 37 | consumersLock sync.RWMutex 38 | consumers *list.List 39 | } 40 | 41 | type Settings struct { 42 | // SetTimeout sets the write timeout for individual messages. The 43 | // default is 2 seconds. 44 | Timeout time.Duration 45 | 46 | // CloseOnTimeout sets whether a write timeout should close the 47 | // connection or just drop the message. 48 | // 49 | // If the connection gets closed on a timeout, it's the client's 50 | // responsibility to re-establish a connection. If the connection 51 | // doesn't get closed, messages might get sent to a potentially dead 52 | // client. 53 | // 54 | // The default is true. 55 | CloseOnTimeout bool 56 | 57 | // Sets the timeout for an idle connection. The default is 30 minutes. 58 | IdleTimeout time.Duration 59 | 60 | // Gzip sets whether to use gzip Content-Encoding for clients which 61 | // support it. 62 | // 63 | // The default is false. 64 | Gzip bool 65 | } 66 | 67 | func DefaultSettings() *Settings { 68 | return &Settings{ 69 | Timeout: 2 * time.Second, 70 | CloseOnTimeout: true, 71 | IdleTimeout: 30 * time.Minute, 72 | Gzip: false, 73 | } 74 | } 75 | 76 | // EventSource interface provides methods for sending messages and closing all connections. 77 | type EventSource interface { 78 | // it should implement ServerHTTP method 79 | http.Handler 80 | 81 | // send message to all consumers 82 | SendEventMessage(data, event, id string) 83 | 84 | // send retry message to all consumers 85 | SendRetryMessage(duration time.Duration) 86 | 87 | // consumers count 88 | ConsumersCount() int 89 | 90 | // close and clear all consumers 91 | Close() 92 | } 93 | 94 | type message interface { 95 | // The message to be sent to clients 96 | prepareMessage() []byte 97 | } 98 | 99 | func (m *eventMessage) prepareMessage() []byte { 100 | var data bytes.Buffer 101 | if len(m.id) > 0 { 102 | data.WriteString(fmt.Sprintf("id: %s\n", strings.Replace(m.id, "\n", "", -1))) 103 | } 104 | if len(m.event) > 0 { 105 | data.WriteString(fmt.Sprintf("event: %s\n", strings.Replace(m.event, "\n", "", -1))) 106 | } 107 | if len(m.data) > 0 { 108 | lines := strings.Split(m.data, "\n") 109 | for _, line := range lines { 110 | data.WriteString(fmt.Sprintf("data: %s\n", line)) 111 | } 112 | } 113 | data.WriteString("\n") 114 | return data.Bytes() 115 | } 116 | 117 | func controlProcess(es *eventSource) { 118 | for { 119 | select { 120 | case em := <-es.sink: 121 | message := em.prepareMessage() 122 | func() { 123 | es.consumersLock.RLock() 124 | defer es.consumersLock.RUnlock() 125 | 126 | for e := es.consumers.Front(); e != nil; e = e.Next() { 127 | c := e.Value.(*consumer) 128 | 129 | // Only send this message if the consumer isn't staled 130 | if !c.staled { 131 | select { 132 | case c.in <- message: 133 | default: 134 | } 135 | } 136 | } 137 | }() 138 | case <-es.close: 139 | close(es.sink) 140 | close(es.add) 141 | close(es.staled) 142 | close(es.close) 143 | 144 | func() { 145 | es.consumersLock.RLock() 146 | defer es.consumersLock.RUnlock() 147 | 148 | for e := es.consumers.Front(); e != nil; e = e.Next() { 149 | c := e.Value.(*consumer) 150 | close(c.in) 151 | } 152 | }() 153 | 154 | es.consumersLock.Lock() 155 | defer es.consumersLock.Unlock() 156 | 157 | es.consumers.Init() 158 | return 159 | case c := <-es.add: 160 | func() { 161 | es.consumersLock.Lock() 162 | defer es.consumersLock.Unlock() 163 | 164 | es.consumers.PushBack(c) 165 | }() 166 | case c := <-es.staled: 167 | toRemoveEls := make([]*list.Element, 0, 1) 168 | func() { 169 | es.consumersLock.RLock() 170 | defer es.consumersLock.RUnlock() 171 | 172 | for e := es.consumers.Front(); e != nil; e = e.Next() { 173 | if e.Value.(*consumer) == c { 174 | toRemoveEls = append(toRemoveEls, e) 175 | } 176 | } 177 | }() 178 | func() { 179 | es.consumersLock.Lock() 180 | defer es.consumersLock.Unlock() 181 | 182 | for _, e := range toRemoveEls { 183 | es.consumers.Remove(e) 184 | } 185 | }() 186 | close(c.in) 187 | } 188 | } 189 | } 190 | 191 | // New creates new EventSource instance. 192 | func New(settings *Settings, customHeadersFunc func(*http.Request) [][]byte) EventSource { 193 | if settings == nil { 194 | settings = DefaultSettings() 195 | } 196 | 197 | es := new(eventSource) 198 | es.customHeadersFunc = customHeadersFunc 199 | es.sink = make(chan message, 1) 200 | es.close = make(chan bool) 201 | es.staled = make(chan *consumer, 1) 202 | es.add = make(chan *consumer) 203 | es.consumers = list.New() 204 | es.timeout = settings.Timeout 205 | es.idleTimeout = settings.IdleTimeout 206 | es.closeOnTimeout = settings.CloseOnTimeout 207 | es.gzip = settings.Gzip 208 | go controlProcess(es) 209 | return es 210 | } 211 | 212 | func (es *eventSource) Close() { 213 | es.close <- true 214 | } 215 | 216 | // ServeHTTP implements http.Handler interface. 217 | func (es *eventSource) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 218 | cons, err := newConsumer(resp, req, es) 219 | if err != nil { 220 | log.Print("Can't create connection to a consumer: ", err) 221 | return 222 | } 223 | es.add <- cons 224 | } 225 | 226 | func (es *eventSource) sendMessage(m message) { 227 | es.sink <- m 228 | } 229 | 230 | func (es *eventSource) SendEventMessage(data, event, id string) { 231 | em := &eventMessage{id, event, data} 232 | es.sendMessage(em) 233 | } 234 | 235 | func (m *retryMessage) prepareMessage() []byte { 236 | return []byte(fmt.Sprintf("retry: %d\n\n", m.retry/time.Millisecond)) 237 | } 238 | 239 | func (es *eventSource) SendRetryMessage(t time.Duration) { 240 | es.sendMessage(&retryMessage{t}) 241 | } 242 | 243 | func (es *eventSource) ConsumersCount() int { 244 | es.consumersLock.RLock() 245 | defer es.consumersLock.RUnlock() 246 | 247 | return es.consumers.Len() 248 | } 249 | -------------------------------------------------------------------------------- /eventsource_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type testEnv struct { 14 | eventSource EventSource 15 | server *httptest.Server 16 | } 17 | 18 | func setup(t *testing.T) *testEnv { 19 | t.Log("Setup testing environment") 20 | e := new(testEnv) 21 | e.eventSource = New(nil, nil) 22 | e.server = httptest.NewServer(e.eventSource) 23 | return e 24 | } 25 | 26 | func setupWithHeaders(t *testing.T, headers [][]byte) *testEnv { 27 | t.Log("Setup testing environment") 28 | e := new(testEnv) 29 | e.eventSource = New( 30 | nil, 31 | func(*http.Request) [][]byte { 32 | return headers 33 | }, 34 | ) 35 | e.server = httptest.NewServer(e.eventSource) 36 | return e 37 | } 38 | 39 | func setupWithCustomSettings(t *testing.T, settings *Settings) *testEnv { 40 | t.Log("Setup testing environment") 41 | e := new(testEnv) 42 | e.eventSource = New( 43 | settings, 44 | nil, 45 | ) 46 | e.server = httptest.NewServer(e.eventSource) 47 | return e 48 | } 49 | 50 | func teardown(t *testing.T, e *testEnv) { 51 | t.Log("Teardown testing environment") 52 | e.eventSource.Close() 53 | e.server.Close() 54 | } 55 | 56 | func checkError(t *testing.T, e error) { 57 | if e != nil { 58 | t.Error(e) 59 | } 60 | } 61 | 62 | func read(t *testing.T, c net.Conn) []byte { 63 | resp := make([]byte, 1024) 64 | _, err := c.Read(resp) 65 | if err != nil && err != io.EOF { 66 | t.Error(err) 67 | } 68 | return resp 69 | } 70 | 71 | func startEventStream(t *testing.T, e *testEnv) (net.Conn, []byte) { 72 | url := e.server.URL 73 | t.Log("open connection") 74 | conn, err := net.Dial("tcp", strings.Replace(url, "http://", "", 1)) 75 | checkError(t, err) 76 | t.Log("send GET request to the connection") 77 | _, err = conn.Write([]byte("GET / HTTP/1.1\n\n")) 78 | checkError(t, err) 79 | 80 | resp := read(t, conn) 81 | t.Logf("got response: \n%s", resp) 82 | return conn, resp 83 | } 84 | 85 | func expectResponse(t *testing.T, c net.Conn, expecting string) { 86 | time.Sleep(100 * time.Millisecond) 87 | 88 | resp := read(t, c) 89 | if !strings.Contains(string(resp), expecting) { 90 | t.Errorf("expected:\n%s\ngot:\n%s\n", expecting, resp) 91 | } 92 | } 93 | 94 | func TestConnection(t *testing.T) { 95 | e := setup(t) 96 | defer teardown(t, e) 97 | 98 | conn, resp := startEventStream(t, e) 99 | defer conn.Close() 100 | 101 | if !strings.Contains(string(resp), "HTTP/1.1 200 OK\r\n") { 102 | t.Error("the response has no HTTP status") 103 | } 104 | 105 | if !strings.Contains(string(resp), "Content-Type: text/event-stream\r\n") { 106 | t.Error("the response has no Content-Type header with value 'text/event-stream'") 107 | } 108 | } 109 | 110 | func TestConnectionWithCustomHeaders(t *testing.T) { 111 | e := setupWithHeaders(t, [][]byte{ 112 | []byte("X-Accel-Buffering: no"), 113 | []byte("Access-Control-Allow-Origin: *"), 114 | }) 115 | defer teardown(t, e) 116 | 117 | conn, resp := startEventStream(t, e) 118 | defer conn.Close() 119 | 120 | if !strings.Contains(string(resp), "HTTP/1.1 200 OK\r\n") { 121 | t.Error("the response has no HTTP status") 122 | } 123 | 124 | if !strings.Contains(string(resp), "Content-Type: text/event-stream\r\n") { 125 | t.Error("the response has no Content-Type header with value 'text/event-stream'") 126 | } 127 | 128 | if !strings.Contains(string(resp), "X-Accel-Buffering: no\r\n") { 129 | t.Error("the response has no X-Accel-Buffering header with value 'no'") 130 | } 131 | 132 | if !strings.Contains(string(resp), "Access-Control-Allow-Origin: *\r\n") { 133 | t.Error("the response has no Access-Control-Allow-Origin header with value '*'") 134 | } 135 | } 136 | 137 | func TestRetryMessageSending(t *testing.T) { 138 | e := setup(t) 139 | defer teardown(t, e) 140 | 141 | conn, _ := startEventStream(t, e) 142 | defer conn.Close() 143 | 144 | t.Log("send retry message") 145 | e.eventSource.SendRetryMessage(3 * time.Second) 146 | expectResponse(t, conn, "retry: 3000\n\n") 147 | } 148 | 149 | func TestEventMessageSending(t *testing.T) { 150 | e := setup(t) 151 | defer teardown(t, e) 152 | 153 | conn, _ := startEventStream(t, e) 154 | defer conn.Close() 155 | 156 | t.Log("send message 'test'") 157 | e.eventSource.SendEventMessage("test", "", "") 158 | expectResponse(t, conn, "data: test\n\n") 159 | 160 | t.Log("send message 'test' with id '1'") 161 | e.eventSource.SendEventMessage("test", "", "1") 162 | expectResponse(t, conn, "id: 1\ndata: test\n\n") 163 | 164 | t.Log("send message 'test' with id '1\n1'") 165 | e.eventSource.SendEventMessage("test", "", "1\n1") 166 | expectResponse(t, conn, "id: 11\ndata: test\n\n") 167 | 168 | t.Log("send message 'test' with event type 'notification'") 169 | e.eventSource.SendEventMessage("test", "notification", "") 170 | expectResponse(t, conn, "event: notification\ndata: test\n\n") 171 | 172 | t.Log("send message 'test' with event type 'notification\n2'") 173 | e.eventSource.SendEventMessage("test", "notification\n2", "") 174 | expectResponse(t, conn, "event: notification2\ndata: test\n\n") 175 | 176 | t.Log("send message 'test\ntest2\ntest3\n'") 177 | e.eventSource.SendEventMessage("test\ntest2\ntest3\n", "", "") 178 | expectResponse(t, conn, "data: test\ndata: test2\ndata: test3\ndata: \n\n") 179 | } 180 | 181 | func TestStalledMessages(t *testing.T) { 182 | e := setup(t) 183 | defer teardown(t, e) 184 | 185 | conn, _ := startEventStream(t, e) 186 | conn2, _ := startEventStream(t, e) 187 | 188 | t.Log("send message 'test' to both connections") 189 | e.eventSource.SendEventMessage("test", "", "") 190 | expectResponse(t, conn, "data: test\n\n") 191 | expectResponse(t, conn2, "data: test\n\n") 192 | 193 | conn.Close() 194 | conn2.Close() 195 | 196 | t.Log("send message with no open connections") 197 | e.eventSource.SendEventMessage("test", "", "1") 198 | 199 | connNew, _ := startEventStream(t, e) 200 | 201 | t.Log("send a message to new connection") 202 | e.eventSource.SendEventMessage("test", "", "1\n1") 203 | expectResponse(t, connNew, "id: 11\ndata: test\n\n") 204 | 205 | for i := 0; i < 10; i++ { 206 | t.Log("sending additional message ", i) 207 | e.eventSource.SendEventMessage("test", "", "") 208 | expectResponse(t, connNew, "data: test\n\n") 209 | } 210 | } 211 | 212 | func TestIdleTimeout(t *testing.T) { 213 | settings := DefaultSettings() 214 | settings.IdleTimeout = 500 * time.Millisecond 215 | e := setupWithCustomSettings(t, settings) 216 | defer teardown(t, e) 217 | 218 | startEventStream(t, e) 219 | 220 | ccount := e.eventSource.ConsumersCount() 221 | if ccount != 1 { 222 | t.Fatalf("Expected 1 customer but got %d", ccount) 223 | } 224 | 225 | <-time.After(1000 * time.Millisecond) 226 | 227 | ccount = e.eventSource.ConsumersCount() 228 | if ccount != 0 { 229 | t.Fatalf("Expected 0 customer but got %d", ccount) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /examples/greeter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/antage/eventsource.v1" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | es := eventsource.New(nil, nil) 12 | defer es.Close() 13 | http.Handle("/", http.FileServer(http.Dir("./public"))) 14 | http.Handle("/events", es) 15 | go func() { 16 | for { 17 | es.SendEventMessage("hello", "", "") 18 | log.Printf("Hello has been sent (consumers: %d)", es.ConsumersCount()) 19 | time.Sleep(2 * time.Second) 20 | } 21 | }() 22 | log.Print("Open URL http://localhost:8080/ in your browser.") 23 | err := http.ListenAndServe(":8080", nil) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |