├── LICENCE ├── README.md ├── codec_test.go ├── decoder.go ├── decoder_test.go ├── encoder.go ├── example_error_handling_stream_test.go ├── example_event_test.go ├── example_repository_test.go ├── interface.go ├── normalise.go ├── normalise_test.go ├── repository.go ├── server.go ├── server_test.go ├── stream.go └── stream_test.go /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2021 eventsource authors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/donovanhide/eventsource?status.svg)](http://godoc.org/github.com/donovanhide/eventsource) 2 | [![CircleCI](https://circleci.com/gh/donovanhide/eventsource.svg?style=svg)](https://circleci.com/gh/donovanhide/eventsource) 3 | 4 | 5 | # Eventsource 6 | 7 | Eventsource implements a [Go](http://golang.org/) implementation of client and server to allow streaming data one-way over a HTTP connection using the Server-Sent Events API http://dev.w3.org/html5/eventsource/ 8 | 9 | ## Installation 10 | 11 | go get github.com/donovanhide/eventsource 12 | 13 | ## Documentation 14 | 15 | * [Reference](http://godoc.org/github.com/donovanhide/eventsource) 16 | 17 | ## License 18 | 19 | Eventsource is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 20 | -------------------------------------------------------------------------------- /codec_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | type testEvent struct { 9 | id, event, data string 10 | } 11 | 12 | func (e *testEvent) Id() string { return e.id } 13 | func (e *testEvent) Event() string { return e.event } 14 | func (e *testEvent) Data() string { return e.data } 15 | 16 | var encoderTests = []struct { 17 | event *testEvent 18 | output string 19 | }{ 20 | {&testEvent{"1", "Add", "This is a test"}, "id: 1\nevent: Add\ndata: This is a test\n\n"}, 21 | {&testEvent{"", "", "This message, it\nhas two lines."}, "data: This message, it\ndata: has two lines.\n\n"}, 22 | } 23 | 24 | func TestRoundTrip(t *testing.T) { 25 | buf := new(bytes.Buffer) 26 | enc := NewEncoder(buf, false) 27 | dec := NewDecoder(buf) 28 | for _, tt := range encoderTests { 29 | want := tt.event 30 | if err := enc.Encode(want); err != nil { 31 | t.Fatal(err) 32 | } 33 | if buf.String() != tt.output { 34 | t.Errorf("Expected: %s Got: %s", tt.output, buf.String()) 35 | } 36 | ev, err := dec.Decode() 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if ev.Id() != want.Id() || ev.Event() != want.Event() || ev.Data() != want.Data() { 41 | t.Errorf("Expected: %s %s %s Got: %s %s %s", want.Id(), want.Event(), want.Data(), ev.Id(), ev.Event(), ev.Data()) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type publication struct { 11 | id, event, data string 12 | retry int64 13 | } 14 | 15 | func (s *publication) Id() string { return s.id } 16 | func (s *publication) Event() string { return s.event } 17 | func (s *publication) Data() string { return s.data } 18 | func (s *publication) Retry() int64 { return s.retry } 19 | 20 | // A Decoder is capable of reading Events from a stream. 21 | type Decoder struct { 22 | *bufio.Reader 23 | } 24 | 25 | // NewDecoder returns a new Decoder instance that reads events 26 | // with the given io.Reader. 27 | func NewDecoder(r io.Reader) *Decoder { 28 | dec := &Decoder{bufio.NewReader(newNormaliser(r))} 29 | return dec 30 | } 31 | 32 | // Decode reads the next Event from a stream (and will block until one 33 | // comes in). 34 | // Graceful disconnects (between events) are indicated by an io.EOF error. 35 | // Any error occuring mid-event is considered non-graceful and will 36 | // show up as some other error (most likely io.ErrUnexpectedEOF). 37 | func (dec *Decoder) Decode() (Event, error) { 38 | // peek ahead before we start a new event so we can return EOFs 39 | _, err := dec.Peek(1) 40 | if err == io.ErrUnexpectedEOF { 41 | err = io.EOF 42 | } 43 | if err != nil { 44 | return nil, err 45 | } 46 | pub := new(publication) 47 | inDecoding := false 48 | for { 49 | line, err := dec.ReadString('\n') 50 | if err != nil { 51 | return nil, err 52 | } 53 | if line == "\n" && inDecoding { 54 | // the empty line signals the end of an event 55 | break 56 | } else if line == "\n" && !inDecoding { 57 | // only a newline was sent, so we don't want to publish an empty event but try to read again 58 | continue 59 | } 60 | line = strings.TrimSuffix(line, "\n") 61 | if strings.HasPrefix(line, ":") { 62 | continue 63 | } 64 | sections := strings.SplitN(line, ":", 2) 65 | field, value := sections[0], "" 66 | if len(sections) == 2 { 67 | value = strings.TrimPrefix(sections[1], " ") 68 | } 69 | inDecoding = true 70 | switch field { 71 | case "event": 72 | pub.event = value 73 | case "data": 74 | pub.data += value + "\n" 75 | case "id": 76 | pub.id = value 77 | case "retry": 78 | pub.retry, _ = strconv.ParseInt(value, 10, 64) 79 | } 80 | } 81 | pub.data = strings.TrimSuffix(pub.data, "\n") 82 | return pub, nil 83 | } 84 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestDecode(t *testing.T) { 11 | tests := []struct { 12 | rawInput string 13 | wantedEvents []*publication 14 | }{ 15 | { 16 | rawInput: "event: eventName\ndata: {\"sample\":\"value\"}\n\n", 17 | wantedEvents: []*publication{{event: "eventName", data: "{\"sample\":\"value\"}"}}, 18 | }, 19 | { 20 | // the newlines should not be parsed as empty event 21 | rawInput: "\n\n\nevent: event1\n\n\n\n\nevent: event2\n\n", 22 | wantedEvents: []*publication{{event: "event1"}, {event: "event2"}}, 23 | }, 24 | } 25 | 26 | for _, test := range tests { 27 | decoder := NewDecoder(strings.NewReader(test.rawInput)) 28 | i := 0 29 | for { 30 | event, err := decoder.Decode() 31 | if err == io.EOF { 32 | break 33 | } 34 | if err != nil { 35 | t.Fatalf("Unexpected error on decoding event: %s", err) 36 | } 37 | 38 | if !reflect.DeepEqual(event, test.wantedEvents[i]) { 39 | t.Fatalf("Parsed event %+v does not equal wanted event %+v", event, test.wantedEvents[i]) 40 | } 41 | i++ 42 | } 43 | if i != len(test.wantedEvents) { 44 | t.Fatalf("Unexpected number of events: %d does not equal wanted: %d", i, len(test.wantedEvents)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | encFields = []struct { 12 | prefix string 13 | value func(Event) string 14 | }{ 15 | {"id: ", Event.Id}, 16 | {"event: ", Event.Event}, 17 | {"data: ", Event.Data}, 18 | } 19 | ) 20 | 21 | // An Encoder is capable of writing Events to a stream. Optionally 22 | // Events can be gzip compressed in this process. 23 | type Encoder struct { 24 | w io.Writer 25 | compressed bool 26 | } 27 | 28 | // NewEncoder returns an Encoder for a given io.Writer. 29 | // When compressed is set to true, a gzip writer will be 30 | // created. 31 | func NewEncoder(w io.Writer, compressed bool) *Encoder { 32 | if compressed { 33 | return &Encoder{w: gzip.NewWriter(w), compressed: true} 34 | } 35 | return &Encoder{w: w} 36 | } 37 | 38 | // Encode writes an event in the format specified by the 39 | // server-sent events protocol. 40 | func (enc *Encoder) Encode(ev Event) error { 41 | for _, field := range encFields { 42 | prefix, value := field.prefix, field.value(ev) 43 | if len(value) == 0 { 44 | continue 45 | } 46 | value = strings.Replace(value, "\n", "\n"+prefix, -1) 47 | if _, err := io.WriteString(enc.w, prefix+value+"\n"); err != nil { 48 | return fmt.Errorf("eventsource encode: %v", err) 49 | } 50 | } 51 | if _, err := io.WriteString(enc.w, "\n"); err != nil { 52 | return fmt.Errorf("eventsource encode: %v", err) 53 | } 54 | if enc.compressed { 55 | return enc.w.(*gzip.Writer).Flush() 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /example_error_handling_stream_test.go: -------------------------------------------------------------------------------- 1 | package eventsource_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/donovanhide/eventsource" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | func ExampleErrorHandlingStream() { 11 | listener, err := net.Listen("tcp", ":8080") 12 | if err != nil { 13 | return 14 | } 15 | defer listener.Close() 16 | http.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) { 17 | http.Error(w, "Something wrong.", 500) 18 | }) 19 | go http.Serve(listener, nil) 20 | 21 | _, err = eventsource.Subscribe("http://127.0.0.1:8080/stream", "") 22 | if err != nil { 23 | if serr, ok := err.(eventsource.SubscriptionError); ok { 24 | fmt.Printf("Status code: %d\n", serr.Code) 25 | fmt.Printf("Message: %s\n", serr.Message) 26 | } else { 27 | fmt.Println("failed to subscribe") 28 | } 29 | } 30 | 31 | // Output: 32 | // Status code: 500 33 | // Message: Something wrong. 34 | } 35 | -------------------------------------------------------------------------------- /example_event_test.go: -------------------------------------------------------------------------------- 1 | package eventsource_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/donovanhide/eventsource" 10 | ) 11 | 12 | type TimeEvent time.Time 13 | 14 | func (t TimeEvent) Id() string { return fmt.Sprint(time.Time(t).UnixNano()) } 15 | func (t TimeEvent) Event() string { return "Tick" } 16 | func (t TimeEvent) Data() string { return time.Time(t).String() } 17 | 18 | const ( 19 | TICK_COUNT = 5 20 | ) 21 | 22 | func TimePublisher(srv *eventsource.Server) { 23 | start := time.Date(2013, time.January, 1, 0, 0, 0, 0, time.UTC) 24 | ticker := time.NewTicker(time.Second) 25 | for i := 0; i < TICK_COUNT; i++ { 26 | <-ticker.C 27 | srv.Publish([]string{"time"}, TimeEvent(start)) 28 | start = start.Add(time.Second) 29 | } 30 | } 31 | 32 | func ExampleEvent() { 33 | srv := eventsource.NewServer() 34 | srv.Gzip = true 35 | defer srv.Close() 36 | l, err := net.Listen("tcp", ":8080") 37 | if err != nil { 38 | return 39 | } 40 | defer l.Close() 41 | http.HandleFunc("/time", srv.Handler("time")) 42 | go http.Serve(l, nil) 43 | go TimePublisher(srv) 44 | stream, err := eventsource.Subscribe("http://127.0.0.1:8080/time", "") 45 | if err != nil { 46 | return 47 | } 48 | for i := 0; i < TICK_COUNT; i++ { 49 | ev := <-stream.Events 50 | fmt.Println(ev.Id(), ev.Event(), ev.Data()) 51 | } 52 | 53 | // Output: 54 | // 1356998400000000000 Tick 2013-01-01 00:00:00 +0000 UTC 55 | // 1356998401000000000 Tick 2013-01-01 00:00:01 +0000 UTC 56 | // 1356998402000000000 Tick 2013-01-01 00:00:02 +0000 UTC 57 | // 1356998403000000000 Tick 2013-01-01 00:00:03 +0000 UTC 58 | // 1356998404000000000 Tick 2013-01-01 00:00:04 +0000 UTC 59 | } 60 | -------------------------------------------------------------------------------- /example_repository_test.go: -------------------------------------------------------------------------------- 1 | package eventsource_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/donovanhide/eventsource" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | type NewsArticle struct { 12 | id string 13 | Title, Content string 14 | } 15 | 16 | func (a *NewsArticle) Id() string { return a.id } 17 | func (a *NewsArticle) Event() string { return "News Article" } 18 | func (a *NewsArticle) Data() string { b, _ := json.Marshal(a); return string(b) } 19 | 20 | var articles = []NewsArticle{ 21 | {"2", "Governments struggle to control global price of gas", "Hot air...."}, 22 | {"1", "Tomorrow is another day", "And so is the day after."}, 23 | {"3", "News for news' sake", "Nothing has happened."}, 24 | } 25 | 26 | func buildRepo(srv *eventsource.Server) { 27 | repo := eventsource.NewSliceRepository() 28 | srv.Register("articles", repo) 29 | for i := range articles { 30 | repo.Add("articles", &articles[i]) 31 | srv.Publish([]string{"articles"}, &articles[i]) 32 | } 33 | } 34 | 35 | func ExampleRepository() { 36 | srv := eventsource.NewServer() 37 | defer srv.Close() 38 | http.HandleFunc("/articles", srv.Handler("articles")) 39 | l, err := net.Listen("tcp", ":8080") 40 | if err != nil { 41 | return 42 | } 43 | defer l.Close() 44 | go http.Serve(l, nil) 45 | stream, err := eventsource.Subscribe("http://127.0.0.1:8080/articles", "") 46 | if err != nil { 47 | return 48 | } 49 | go buildRepo(srv) 50 | // This will receive events in the order that they come 51 | for i := 0; i < 3; i++ { 52 | ev := <-stream.Events 53 | fmt.Println(ev.Id(), ev.Event(), ev.Data()) 54 | } 55 | stream, err = eventsource.Subscribe("http://127.0.0.1:8080/articles", "1") 56 | if err != nil { 57 | fmt.Println(err) 58 | return 59 | } 60 | // This will replay the events in order of id 61 | for i := 0; i < 3; i++ { 62 | ev := <-stream.Events 63 | fmt.Println(ev.Id(), ev.Event(), ev.Data()) 64 | } 65 | // Output: 66 | // 2 News Article {"Title":"Governments struggle to control global price of gas","Content":"Hot air...."} 67 | // 1 News Article {"Title":"Tomorrow is another day","Content":"And so is the day after."} 68 | // 3 News Article {"Title":"News for news' sake","Content":"Nothing has happened."} 69 | // 1 News Article {"Title":"Tomorrow is another day","Content":"And so is the day after."} 70 | // 2 News Article {"Title":"Governments struggle to control global price of gas","Content":"Hot air...."} 71 | // 3 News Article {"Title":"News for news' sake","Content":"Nothing has happened."} 72 | } 73 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | // Package eventsource implements a client and server to allow streaming data one-way over a HTTP connection 2 | // using the Server-Sent Events API http://dev.w3.org/html5/eventsource/ 3 | // 4 | // The client and server respect the Last-Event-ID header. 5 | // If the Repository interface is implemented on the server, events can be replayed in case of a network disconnection. 6 | package eventsource 7 | 8 | // Any event received by the client or sent by the server will implement this interface 9 | type Event interface { 10 | // Id is an identifier that can be used to allow a client to replay 11 | // missed Events by returning the Last-Event-Id header. 12 | // Return empty string if not required. 13 | Id() string 14 | // The name of the event. Return empty string if not required. 15 | Event() string 16 | // The payload of the event. 17 | Data() string 18 | } 19 | 20 | // If history is required, this interface will allow clients to reply previous events through the server. 21 | // Both methods can be called from different goroutines concurrently, so you must make sure they are go-routine safe. 22 | type Repository interface { 23 | // Gets the Events which should follow on from the specified channel and event id. 24 | Replay(channel, id string) chan Event 25 | } 26 | -------------------------------------------------------------------------------- /normalise.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // A reader which normalises line endings 8 | // "/r" and "/r/n" are converted to "/n" 9 | type normaliser struct { 10 | r io.Reader 11 | lastChar byte 12 | } 13 | 14 | func newNormaliser(r io.Reader) *normaliser { 15 | return &normaliser{r: r} 16 | } 17 | 18 | func (norm *normaliser) Read(p []byte) (n int, err error) { 19 | n, err = norm.r.Read(p) 20 | for i := 0; i < n; i++ { 21 | switch { 22 | case p[i] == '\n' && norm.lastChar == '\r': 23 | copy(p[i:n], p[i+1:]) 24 | norm.lastChar = p[i] 25 | n-- 26 | i-- 27 | case p[i] == '\r': 28 | norm.lastChar = p[i] 29 | p[i] = '\n' 30 | default: 31 | norm.lastChar = p[i] 32 | } 33 | } 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /normalise_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | inputFormat = "line1%sline2%sline3%s" 12 | endings = []string{"\n", "\r\n", "\r"} 13 | suffixes = []string{"\n", "\r\n", "\r", ""} 14 | descriptions = []string{"LF", "CRLF", "CR", "EOF"} 15 | expected = []string{"line1", "line2", "line3"} 16 | ) 17 | 18 | func Testnormaliser(t *testing.T) { 19 | for i, first := range endings { 20 | for j, second := range endings { 21 | for k, suffix := range suffixes { 22 | input := fmt.Sprintf(inputFormat, first, second, suffix) 23 | r := bufio.NewReader(newNormaliser(strings.NewReader(input))) 24 | for _, want := range expected { 25 | line, err := r.ReadString('\n') 26 | if err != nil && suffix != "" { 27 | t.Error("Unexpected error:", err) 28 | } 29 | line = strings.TrimSuffix(line, "\n") 30 | if line != want { 31 | expanded := fmt.Sprintf(inputFormat, descriptions[i], descriptions[j], descriptions[k]) 32 | t.Errorf(`Using %s Expected: "%s" Got: "%s"`, expanded, want, line) 33 | t.Log([]byte(line)) 34 | } 35 | } 36 | if _, err := r.ReadString('\n'); err == nil { 37 | t.Error("Expected EOF") 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | // Example repository that uses a slice as storage for past events. 9 | type SliceRepository struct { 10 | events map[string][]Event 11 | lock sync.RWMutex 12 | } 13 | 14 | func NewSliceRepository() *SliceRepository { 15 | return &SliceRepository{ 16 | events: make(map[string][]Event), 17 | } 18 | } 19 | 20 | func (repo SliceRepository) indexOfEvent(channel, id string) int { 21 | return sort.Search(len(repo.events[channel]), func(i int) bool { 22 | return repo.events[channel][i].Id() >= id 23 | }) 24 | } 25 | 26 | func (repo SliceRepository) Replay(channel, id string) (out chan Event) { 27 | out = make(chan Event) 28 | go func() { 29 | defer close(out) 30 | repo.lock.RLock() 31 | defer repo.lock.RUnlock() 32 | events := repo.events[channel][repo.indexOfEvent(channel, id):] 33 | for i := range events { 34 | out <- events[i] 35 | } 36 | }() 37 | return 38 | } 39 | 40 | func (repo *SliceRepository) Add(channel string, event Event) { 41 | repo.lock.Lock() 42 | defer repo.lock.Unlock() 43 | i := repo.indexOfEvent(channel, event.Id()) 44 | if i < len(repo.events[channel]) && repo.events[channel][i].Id() == event.Id() { 45 | repo.events[channel][i] = event 46 | } else { 47 | repo.events[channel] = append(repo.events[channel][:i], append([]Event{event}, repo.events[channel][i:]...)...) 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | type subscription struct { 11 | channel string 12 | lastEventId string 13 | out chan Event 14 | } 15 | 16 | type outbound struct { 17 | channels []string 18 | event Event 19 | } 20 | type registration struct { 21 | channel string 22 | repository Repository 23 | } 24 | 25 | type Server struct { 26 | AllowCORS bool // Enable all handlers to be accessible from any origin 27 | ReplayAll bool // Replay repository even if there's no Last-Event-Id specified 28 | BufferSize int // How many messages do we let the client get behind before disconnecting 29 | Gzip bool // Enable compression if client can accept it 30 | Logger *log.Logger // Logger is a logger that, when set, will be used for logging debug messages 31 | registrations chan *registration 32 | pub chan *outbound 33 | subs chan *subscription 34 | unregister chan *subscription 35 | quit chan bool 36 | isClosed bool 37 | isClosedMutex sync.RWMutex 38 | } 39 | 40 | // Create a new Server ready for handler creation and publishing events 41 | func NewServer() *Server { 42 | srv := &Server{ 43 | registrations: make(chan *registration), 44 | pub: make(chan *outbound), 45 | subs: make(chan *subscription), 46 | unregister: make(chan *subscription, 2), 47 | quit: make(chan bool), 48 | BufferSize: 128, 49 | } 50 | go srv.run() 51 | return srv 52 | } 53 | 54 | // Stop handling publishing 55 | func (srv *Server) Close() { 56 | srv.quit <- true 57 | srv.markServerClosed() 58 | } 59 | 60 | // Create a new handler for serving a specified channel 61 | func (srv *Server) Handler(channel string) http.HandlerFunc { 62 | return func(w http.ResponseWriter, req *http.Request) { 63 | h := w.Header() 64 | h.Set("Content-Type", "text/event-stream; charset=utf-8") 65 | h.Set("Cache-Control", "no-cache, no-store, must-revalidate") 66 | h.Set("Connection", "keep-alive") 67 | if srv.AllowCORS { 68 | h.Set("Access-Control-Allow-Origin", "*") 69 | } 70 | useGzip := srv.Gzip && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") 71 | if useGzip { 72 | h.Set("Content-Encoding", "gzip") 73 | } 74 | w.WriteHeader(http.StatusOK) 75 | 76 | // If the Handler is still active even though the server is closed, stop here. 77 | // Otherwise the Handler will block while publishing to srv.subs indefinitely. 78 | if srv.isServerClosed() { 79 | return 80 | } 81 | 82 | sub := &subscription{ 83 | channel: channel, 84 | lastEventId: req.Header.Get("Last-Event-ID"), 85 | out: make(chan Event, srv.BufferSize), 86 | } 87 | srv.subs <- sub 88 | flusher := w.(http.Flusher) 89 | notifier := w.(http.CloseNotifier) 90 | flusher.Flush() 91 | enc := NewEncoder(w, useGzip) 92 | for { 93 | select { 94 | case <-notifier.CloseNotify(): 95 | srv.unregister <- sub 96 | return 97 | case ev, ok := <-sub.out: 98 | if !ok { 99 | return 100 | } 101 | if err := enc.Encode(ev); err != nil { 102 | srv.unregister <- sub 103 | if srv.Logger != nil { 104 | srv.Logger.Println(err) 105 | } 106 | return 107 | } 108 | flusher.Flush() 109 | } 110 | } 111 | } 112 | } 113 | 114 | // Register the repository to be used for the specified channel 115 | func (srv *Server) Register(channel string, repo Repository) { 116 | srv.registrations <- ®istration{ 117 | channel: channel, 118 | repository: repo, 119 | } 120 | } 121 | 122 | // Publish an event with the specified id to one or more channels 123 | func (srv *Server) Publish(channels []string, ev Event) { 124 | srv.pub <- &outbound{ 125 | channels: channels, 126 | event: ev, 127 | } 128 | } 129 | 130 | func replay(repo Repository, sub *subscription) { 131 | for ev := range repo.Replay(sub.channel, sub.lastEventId) { 132 | sub.out <- ev 133 | } 134 | } 135 | 136 | func (srv *Server) run() { 137 | subs := make(map[string]map[*subscription]struct{}) 138 | repos := make(map[string]Repository) 139 | for { 140 | select { 141 | case reg := <-srv.registrations: 142 | repos[reg.channel] = reg.repository 143 | case sub := <-srv.unregister: 144 | delete(subs[sub.channel], sub) 145 | case pub := <-srv.pub: 146 | for _, c := range pub.channels { 147 | for s := range subs[c] { 148 | select { 149 | case s.out <- pub.event: 150 | default: 151 | srv.unregister <- s 152 | close(s.out) 153 | } 154 | 155 | } 156 | } 157 | case sub := <-srv.subs: 158 | if _, ok := subs[sub.channel]; !ok { 159 | subs[sub.channel] = make(map[*subscription]struct{}) 160 | } 161 | subs[sub.channel][sub] = struct{}{} 162 | if srv.ReplayAll || len(sub.lastEventId) > 0 { 163 | repo, ok := repos[sub.channel] 164 | if ok { 165 | go replay(repo, sub) 166 | } 167 | } 168 | case <-srv.quit: 169 | for _, sub := range subs { 170 | for s := range sub { 171 | close(s.out) 172 | } 173 | } 174 | return 175 | } 176 | } 177 | } 178 | 179 | func (srv *Server) isServerClosed() bool { 180 | srv.isClosedMutex.RLock() 181 | defer srv.isClosedMutex.RUnlock() 182 | return srv.isClosed 183 | } 184 | 185 | func (srv *Server) markServerClosed() { 186 | srv.isClosedMutex.Lock() 187 | defer srv.isClosedMutex.Unlock() 188 | srv.isClosed = true 189 | } 190 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewServerHandlerRespondsAfterClose(t *testing.T) { 11 | server := NewServer() 12 | httpServer := httptest.NewServer(server.Handler("test")) 13 | defer httpServer.Close() 14 | 15 | server.Close() 16 | responses := make(chan *http.Response) 17 | 18 | go func() { 19 | resp, err := http.Get(httpServer.URL) 20 | if err != nil { 21 | t.Fatalf("Unexpected error %s", err) 22 | } 23 | responses <- resp 24 | }() 25 | 26 | select { 27 | case resp := <-responses: 28 | if resp.StatusCode != 200 { 29 | t.Errorf("Received StatusCode %d, want 200", resp.StatusCode) 30 | } 31 | case <-time.After(250 * time.Millisecond): 32 | t.Errorf("Did not receive response in time") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Stream handles a connection for receiving Server Sent Events. 15 | // It will try and reconnect if the connection is lost, respecting both 16 | // received retry delays and event id's. 17 | type Stream struct { 18 | c *http.Client 19 | req *http.Request 20 | lastEventId string 21 | retry time.Duration 22 | // Events emits the events received by the stream 23 | Events chan Event 24 | // Errors emits any errors encountered while reading events from the stream. 25 | // It's mainly for informative purposes - the client isn't required to take any 26 | // action when an error is encountered. The stream will always attempt to continue, 27 | // even if that involves reconnecting to the server. 28 | Errors chan error 29 | // Logger is a logger that, when set, will be used for logging debug messages 30 | Logger *log.Logger 31 | // isClosed is a marker that the stream is/should be closed 32 | isClosed bool 33 | // isClosedMutex is a mutex protecting concurrent read/write access of isClosed 34 | isClosedMutex sync.RWMutex 35 | } 36 | 37 | type SubscriptionError struct { 38 | Code int 39 | Message string 40 | } 41 | 42 | func (e SubscriptionError) Error() string { 43 | return fmt.Sprintf("%d: %s", e.Code, e.Message) 44 | } 45 | 46 | // Subscribe to the Events emitted from the specified url. 47 | // If lastEventId is non-empty it will be sent to the server in case it can replay missed events. 48 | func Subscribe(url, lastEventId string) (*Stream, error) { 49 | req, err := http.NewRequest("GET", url, nil) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return SubscribeWithRequest(lastEventId, req) 54 | } 55 | 56 | // SubscribeWithRequest will take an http.Request to setup the stream, allowing custom headers 57 | // to be specified, authentication to be configured, etc. 58 | func SubscribeWithRequest(lastEventId string, request *http.Request) (*Stream, error) { 59 | return SubscribeWith(lastEventId, http.DefaultClient, request) 60 | } 61 | 62 | // SubscribeWith takes a http client and request providing customization over both headers and 63 | // control over the http client settings (timeouts, tls, etc) 64 | func SubscribeWith(lastEventId string, client *http.Client, request *http.Request) (*Stream, error) { 65 | stream := &Stream{ 66 | c: client, 67 | req: request, 68 | lastEventId: lastEventId, 69 | retry: time.Millisecond * 3000, 70 | Events: make(chan Event), 71 | Errors: make(chan error), 72 | } 73 | stream.c.CheckRedirect = checkRedirect 74 | 75 | r, err := stream.connect() 76 | if err != nil { 77 | return nil, err 78 | } 79 | go stream.stream(r) 80 | return stream, nil 81 | } 82 | 83 | // Close will close the stream. It is safe for concurrent access and can be called multiple times. 84 | func (stream *Stream) Close() { 85 | if stream.isStreamClosed() { 86 | return 87 | } 88 | 89 | stream.markStreamClosed() 90 | close(stream.Errors) 91 | close(stream.Events) 92 | } 93 | 94 | func (stream *Stream) isStreamClosed() bool { 95 | stream.isClosedMutex.RLock() 96 | defer stream.isClosedMutex.RUnlock() 97 | return stream.isClosed 98 | } 99 | 100 | func (stream *Stream) markStreamClosed() { 101 | stream.isClosedMutex.Lock() 102 | defer stream.isClosedMutex.Unlock() 103 | stream.isClosed = true 104 | } 105 | 106 | // Go's http package doesn't copy headers across when it encounters 107 | // redirects so we need to do that manually. 108 | func checkRedirect(req *http.Request, via []*http.Request) error { 109 | if len(via) >= 10 { 110 | return errors.New("stopped after 10 redirects") 111 | } 112 | for k, vv := range via[0].Header { 113 | for _, v := range vv { 114 | req.Header.Add(k, v) 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func (stream *Stream) connect() (r io.ReadCloser, err error) { 121 | var resp *http.Response 122 | stream.req.Header.Set("Cache-Control", "no-cache") 123 | stream.req.Header.Set("Accept", "text/event-stream") 124 | if len(stream.lastEventId) > 0 { 125 | stream.req.Header.Set("Last-Event-ID", stream.lastEventId) 126 | } 127 | if resp, err = stream.c.Do(stream.req); err != nil { 128 | return 129 | } 130 | if resp.StatusCode != 200 { 131 | message, _ := ioutil.ReadAll(resp.Body) 132 | err = SubscriptionError{ 133 | Code: resp.StatusCode, 134 | Message: string(message), 135 | } 136 | } 137 | r = resp.Body 138 | return 139 | } 140 | 141 | func (stream *Stream) stream(r io.ReadCloser) { 142 | defer r.Close() 143 | 144 | // receives events until an error is encountered 145 | stream.receiveEvents(r) 146 | 147 | // tries to reconnect and start the stream again 148 | stream.retryRestartStream() 149 | } 150 | 151 | func (stream *Stream) receiveEvents(r io.ReadCloser) { 152 | dec := NewDecoder(r) 153 | 154 | for { 155 | ev, err := dec.Decode() 156 | if stream.isStreamClosed() { 157 | return 158 | } 159 | if err != nil { 160 | stream.Errors <- err 161 | return 162 | } 163 | 164 | pub := ev.(*publication) 165 | if pub.Retry() > 0 { 166 | stream.retry = time.Duration(pub.Retry()) * time.Millisecond 167 | } 168 | if len(pub.Id()) > 0 { 169 | stream.lastEventId = pub.Id() 170 | } 171 | stream.Events <- ev 172 | } 173 | } 174 | 175 | func (stream *Stream) retryRestartStream() { 176 | backoff := stream.retry 177 | for { 178 | if stream.Logger != nil { 179 | stream.Logger.Printf("Reconnecting in %0.4f secs\n", backoff.Seconds()) 180 | } 181 | time.Sleep(backoff) 182 | if stream.isStreamClosed() { 183 | return 184 | } 185 | // NOTE: because of the defer we're opening the new connection 186 | // before closing the old one. Shouldn't be a problem in practice, 187 | // but something to be aware of. 188 | r, err := stream.connect() 189 | if err == nil { 190 | go stream.stream(r) 191 | return 192 | } 193 | stream.Errors <- err 194 | backoff *= 2 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package eventsource 2 | 3 | import ( 4 | "io" 5 | "net/http/httptest" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | const ( 12 | eventChannelName = "Test" 13 | timeToWaitForEvent = 100 * time.Millisecond 14 | ) 15 | 16 | func TestStreamSubscribeEventsChan(t *testing.T) { 17 | server := NewServer() 18 | httpServer := httptest.NewServer(server.Handler(eventChannelName)) 19 | // The server has to be closed before the httpServer is closed. 20 | // Otherwise the httpServer has still an open connection and it can not close. 21 | defer httpServer.Close() 22 | defer server.Close() 23 | 24 | stream := mustSubscribe(t, httpServer.URL, "") 25 | 26 | publishedEvent := &publication{id: "123"} 27 | server.Publish([]string{eventChannelName}, publishedEvent) 28 | 29 | select { 30 | case receivedEvent := <-stream.Events: 31 | if !reflect.DeepEqual(receivedEvent, publishedEvent) { 32 | t.Errorf("got event %+v, want %+v", receivedEvent, publishedEvent) 33 | } 34 | case <-time.After(timeToWaitForEvent): 35 | t.Error("Timed out waiting for event") 36 | } 37 | } 38 | 39 | func TestStreamSubscribeErrorsChan(t *testing.T) { 40 | server := NewServer() 41 | httpServer := httptest.NewServer(server.Handler(eventChannelName)) 42 | 43 | defer httpServer.Close() 44 | 45 | stream := mustSubscribe(t, httpServer.URL, "") 46 | server.Close() 47 | 48 | select { 49 | case err := <-stream.Errors: 50 | if err != io.EOF { 51 | t.Errorf("got error %+v, want %+v", err, io.EOF) 52 | } 53 | case <-time.After(timeToWaitForEvent): 54 | t.Error("Timed out waiting for error event") 55 | } 56 | } 57 | 58 | func TestStreamClose(t *testing.T) { 59 | server := NewServer() 60 | httpServer := httptest.NewServer(server.Handler(eventChannelName)) 61 | // The server has to be closed before the httpServer is closed. 62 | // Otherwise the httpServer has still an open connection and it can not close. 63 | defer httpServer.Close() 64 | defer server.Close() 65 | 66 | stream := mustSubscribe(t, httpServer.URL, "") 67 | stream.Close() 68 | // its safe to Close the stream multiple times 69 | stream.Close() 70 | 71 | select { 72 | case _, ok := <-stream.Events: 73 | if ok { 74 | t.Error("Expected stream.Events channel to be closed. Is still open.") 75 | } 76 | case <-time.After(timeToWaitForEvent): 77 | t.Error("Timed out waiting for stream.Events channel to close") 78 | } 79 | 80 | select { 81 | case _, ok := <-stream.Errors: 82 | if ok { 83 | t.Error("Expected stream.Errors channel to be closed. Is still open.") 84 | } 85 | case <-time.After(timeToWaitForEvent): 86 | t.Error("Timed out waiting for stream.Errors channel to close") 87 | } 88 | } 89 | 90 | func mustSubscribe(t *testing.T, url, lastEventId string) *Stream { 91 | stream, err := Subscribe(url, lastEventId) 92 | if err != nil { 93 | t.Fatalf("Failed to subscribe: %s", err) 94 | } 95 | return stream 96 | } 97 | --------------------------------------------------------------------------------