Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/variables/go-versions.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
latest=1.22
penultimate=1.21
min=1.17
latest=1.24
penultimate=1.23
min=1.23
6 changes: 5 additions & 1 deletion .github/workflows/go-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ jobs:
- name: Set Go Version Matrices
id: set-matrix
run: |
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\",\"${{ steps.set-env.outputs.min }}\"]" >> $GITHUB_OUTPUT
if [ "${{ steps.set-env.outputs.penultimate }}" == "${{ steps.set-env.outputs.min }}" ]; then
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\"]" >> $GITHUB_OUTPUT
else
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\",\"${{ steps.set-env.outputs.min }}\"]" >> $GITHUB_OUTPUT
fi
46 changes: 24 additions & 22 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
version: "2"
run:
deadline: 120s
tests: false

linters:
enable:
- bodyclose
- dupl
- errcheck
- exportloopref
- goconst
- gochecknoglobals
- gochecknoinits
- goconst
- gocritic
- gocyclo
- godox
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- nolintlint
- prealloc
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
fast: false

linters-settings:
gofmt:
simplify: false
goimports:
local-prefixes: github.com/launchdarkly

exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues:
exclude-use-default: false
max-same-issues: 1000
formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: false
goimports:
local-prefixes:
- gopkg.in/launchdarkly
- github.com/launchdarkly
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GOLANGCI_LINT_VERSION=v1.60.1
GOLANGCI_LINT_VERSION=v1.64.5

LINTER=./bin/golangci-lint
LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION)
Expand Down
2 changes: 1 addition & 1 deletion contract-tests/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/launchdarkly/eventsource/contract-tests

go 1.17
go 1.23

replace github.com/launchdarkly/eventsource => ../

Expand Down
11 changes: 2 additions & 9 deletions contract-tests/go.sum
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
github.com/launchdarkly/go-test-helpers/v3 v3.1.0 h1:E3bxJMzMoA+cJSF3xxtk2/chr1zshl1ZWa0/oR+8bvg=
github.com/launchdarkly/go-test-helpers/v3 v3.1.0/go.mod h1:Ake5+hZFS/DmIGKx/cizhn5W9pGA7pplcR7xCxWiLIo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 changes: 25 additions & 0 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package eventsource
import (
"bufio"
"io"
"net/http"
"strconv"
"strings"
"time"
)

type publication struct {
id, event, data, lastEventID string
headers http.Header
retry int64
}

Expand All @@ -22,12 +24,21 @@ func (s *publication) Retry() int64 { return s.retry }
// LastEventID is from a separate interface, EventWithLastID
func (s *publication) LastEventID() string { return s.lastEventID }

// Headers is from a separate interface, EventWithHeaders
func (s *publication) Headers() http.Header {
if s.headers == nil {
return nil
}
return s.headers.Clone()
}

// A Decoder is capable of reading Events from a stream.
type Decoder struct {
linesCh <-chan string
errorCh <-chan error
readTimeout time.Duration
lastEventID string
headers http.Header
}

// DecoderOption is a common interface for optional configuration parameters that can be
Expand All @@ -48,6 +59,12 @@ func (o lastEventIDDecoderOption) apply(d *Decoder) {
d.lastEventID = string(o)
}

type headersDecoderOption http.Header

func (o headersDecoderOption) apply(d *Decoder) {
d.headers = http.Header(o)
}

// DecoderOptionReadTimeout returns an option that sets the read timeout interval for a
// Decoder when the Decoder is created. If the Decoder does not receive new data within this
// length of time, it will return an error. By default, there is no read timeout.
Expand All @@ -62,6 +79,13 @@ func DecoderOptionLastEventID(lastEventID string) DecoderOption {
return lastEventIDDecoderOption(lastEventID)
}

// DecoderOptionHeaders returns an option that sets the Headers property for a
// Decoder when the Decoder is created. This allows access to the HTTP response
// headers for the event.
func DecoderOptionHeaders(headers http.Header) DecoderOption {
return headersDecoderOption(headers)
}

// NewDecoder returns a new Decoder instance that reads events with the given io.Reader.
func NewDecoder(r io.Reader) *Decoder {
bufReader := bufio.NewReader(newNormaliser(r))
Expand Down Expand Up @@ -152,6 +176,7 @@ ReadLoop:
}
pub.data = strings.TrimSuffix(pub.data, "\n")
pub.lastEventID = dec.lastEventID
pub.headers = dec.headers
return pub, nil
}

Expand Down
69 changes: 51 additions & 18 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eventsource

import (
"io"
"net/http"
"strings"
"testing"

Expand Down Expand Up @@ -57,6 +58,13 @@ func requireLastEventID(t *testing.T, event Event) string {
return eventWithID.LastEventID()
}

func requireHeaders(t *testing.T, event Event) http.Header {
// necessary because we can't yet add Headers to the basic Event interface; see EventWithHeaders
eventWithHeaders, ok := event.(EventWithHeaders)
require.True(t, ok, "event should have implemented EventWithHeaders")
return eventWithHeaders.Headers()
}

func TestDecoderTracksLastEventID(t *testing.T) {
t.Run("uses last ID that is passed in options", func(t *testing.T) {
inputData := "data: abc\n\n"
Expand Down Expand Up @@ -96,8 +104,8 @@ func TestDecoderTracksLastEventID(t *testing.T) {
assert.Equal(t, "def", requireLastEventID(t, event3))
})

t.Run("last ID persists if not overridden", func(t *testing.T) {
inputData := "id: abc\ndata: first\n\ndata: second\n\nid: def\ndata:third\n\n"
t.Run("last ID can be overridden with empty string", func(t *testing.T) {
inputData := "id: abc\ndata: first\n\nid: \ndata: second\n\n"
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionLastEventID("my-id"))

event1, err := decoder.Decode()
Expand All @@ -112,32 +120,57 @@ func TestDecoderTracksLastEventID(t *testing.T) {

assert.Equal(t, "second", event2.Data())
assert.Equal(t, "", event2.Id())
assert.Equal(t, "abc", requireLastEventID(t, event2))
assert.Equal(t, "", requireLastEventID(t, event2))
})
}

event3, err := decoder.Decode()
func TestDecoderTracksHeaders(t *testing.T) {
t.Run("event headers is nil if not provided in options", func(t *testing.T) {
inputData := "data: abc\n\n"

decoder := NewDecoderWithOptions(strings.NewReader(inputData))

event, err := decoder.Decode()
require.NoError(t, err)

assert.Equal(t, "third", event3.Data())
assert.Equal(t, "def", event3.Id())
assert.Equal(t, "def", requireLastEventID(t, event3))
assert.Equal(t, "abc", event.Data())
assert.Equal(t, "", event.Id())
assert.Nil(t, requireHeaders(t, event))
})

t.Run("last ID can be overridden with empty string", func(t *testing.T) {
inputData := "id: abc\ndata: first\n\nid: \ndata: second\n\n"
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionLastEventID("my-id"))
t.Run("uses headers that are passed in options", func(t *testing.T) {
inputData := "data: abc\n\n"
headers := http.Header{
"X-Ld-Envid": {"env-id"},
}

event1, err := decoder.Decode()
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionHeaders(headers))

event, err := decoder.Decode()
require.NoError(t, err)

assert.Equal(t, "first", event1.Data())
assert.Equal(t, "abc", event1.Id())
assert.Equal(t, "abc", requireLastEventID(t, event1))
assert.Equal(t, "abc", event.Data())
assert.Equal(t, "", event.Id())
assert.Equal(t, headers, requireHeaders(t, event))
})

event2, err := decoder.Decode()
t.Run("event headers are immutable", func(t *testing.T) {
inputData := "data: abc\n\n"
headers := http.Header{
"X-Ld-Envid": {"env-id"},
}

decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionHeaders(headers))

event, err := decoder.Decode()
require.NoError(t, err)

assert.Equal(t, "second", event2.Data())
assert.Equal(t, "", event2.Id())
assert.Equal(t, "", requireLastEventID(t, event2))
eventHeaders := requireHeaders(t, event)
assert.Equal(t, "abc", event.Data())
assert.Equal(t, "", event.Id())
assert.Equal(t, headers, eventHeaders)

eventHeaders.Add("New-Header", "new-value")
assert.NotContains(t, headers, "New-Header")
})
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/launchdarkly/eventsource

go 1.17
go 1.23

require (
github.com/launchdarkly/go-test-helpers/v2 v2.2.0
github.com/launchdarkly/go-test-helpers/v3 v3.1.0
github.com/stretchr/testify v1.6.0
)

Expand Down
7 changes: 2 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
github.com/launchdarkly/go-test-helpers/v3 v3.1.0 h1:E3bxJMzMoA+cJSF3xxtk2/chr1zshl1ZWa0/oR+8bvg=
github.com/launchdarkly/go-test-helpers/v3 v3.1.0/go.mod h1:Ake5+hZFS/DmIGKx/cizhn5W9pGA7pplcR7xCxWiLIo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 changes: 15 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// If the Repository interface is implemented on the server, events can be replayed in case of a network disconnection.
package eventsource

import "net/http"

// Event is the interface for any event received by the client or sent by the server.
type Event interface {
// Id is an identifier that can be used to allow a client to replay
Expand Down Expand Up @@ -32,6 +34,19 @@ type EventWithLastID interface {
LastEventID() string
}

// EventWithHeaders is an additional interface for an event received by the client,
// allowing access to the HTTP response headers.
//
// This is defined as a separate interface for backward compatibility, since this
// feature was added after the Event interface had been defined and adding a method
// to Event would break existing implementations. All events returned by Stream do
// implement this interface, and in a future major version the Event type will be
// changed to always include this field.
type EventWithHeaders interface {
// Headers provides access to the HTTP response headers for the event.
Headers() http.Header
}

// Repository is an interface to be used with Server.Register() allowing clients to replay previous events
// through the server, if history is required.
type Repository interface {
Expand Down
Loading
Loading