Skip to content
Open
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
258 changes: 136 additions & 122 deletions sse-encoder.go
Original file line number Diff line number Diff line change
@@ -1,122 +1,136 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package sse

import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)

// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/

const ContentType = "text/event-stream;charset=utf-8"

var (
contentType = []string{ContentType}
noCache = []string{"no-cache"}
)

var fieldReplacer = strings.NewReplacer(
"\n", "\\n",
"\r", "\\r",
)

var dataReplacer = strings.NewReplacer(
"\n", "\ndata:",
"\r", "\\r",
)

type Event struct {
Event string
Id string //nolint:staticcheck // ST1003: public API field Id kept for backward compatibility
Retry uint
Data any
}

func Encode(writer io.Writer, event Event) error {
w := checkWriter(writer)
writeID(w, event.Id)
writeEvent(w, event.Event)
writeRetry(w, event.Retry)
return writeData(w, event.Data)
}

func writeID(w stringWriter, id string) {
if len(id) > 0 {
_, _ = w.WriteString("id:")
_, _ = fieldReplacer.WriteString(w, id)
_, _ = w.WriteString("\n")
}
}

func writeEvent(w stringWriter, event string) {
if len(event) > 0 {
_, _ = w.WriteString("event:")
_, _ = fieldReplacer.WriteString(w, event)
_, _ = w.WriteString("\n")
}
}

func writeRetry(w stringWriter, retry uint) {
if retry > 0 {
_, _ = w.WriteString("retry:")
_, _ = w.WriteString(strconv.FormatUint(uint64(retry), 10))
_, _ = w.WriteString("\n")
}
}

func writeData(w stringWriter, data any) error {
_, _ = w.WriteString("data:")

bData, ok := data.([]byte)
if ok {
_, _ = dataReplacer.WriteString(w, string(bData))
_, _ = w.WriteString("\n\n")
return nil
}

switch kindOfData(data) { //nolint:exhaustive // remaining kinds need no special encoding
case reflect.Struct, reflect.Slice, reflect.Map:
err := json.NewEncoder(w).Encode(data)
if err != nil {
return err
}
_, _ = w.WriteString("\n")
default:
_, _ = dataReplacer.WriteString(w, fmt.Sprint(data))
_, _ = w.WriteString("\n\n")
}
return nil
}

func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}

func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType

if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
}

func kindOfData(data any) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Pointer {
valueType = value.Elem().Kind()
}
return valueType
}
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package sse

import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)

// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/

const ContentType = "text/event-stream;charset=utf-8"

var (
contentType = []string{ContentType}
noCache = []string{"no-cache"}
)

var fieldReplacer = strings.NewReplacer(
"\n", "\\n",
"\r", "\\r",
)

var dataReplacer = strings.NewReplacer(
"\n", "\ndata:",
"\r", "\\r",
)

type Event struct {
Event string
Id string //nolint:staticcheck // ST1003: public API field Id kept for backward compatibility
Retry uint
Data any
}

func Encode(writer io.Writer, event Event) error {
w := checkWriter(writer)
writeID(w, event.Id)
writeEvent(w, event.Event)
writeRetry(w, event.Retry)
return writeData(w, event.Data)
}

func writeID(w stringWriter, id string) {
if len(id) > 0 {
_, _ = w.WriteString("id:")
_, _ = fieldReplacer.WriteString(w, id)
_, _ = w.WriteString("\n")
}
}

func writeEvent(w stringWriter, event string) {
if len(event) > 0 {
_, _ = w.WriteString("event:")
_, _ = fieldReplacer.WriteString(w, event)
_, _ = w.WriteString("\n")
}
}

func writeRetry(w stringWriter, retry uint) {
if retry > 0 {
_, _ = w.WriteString("retry:")
_, _ = w.WriteString(strconv.FormatUint(uint64(retry), 10))
_, _ = w.WriteString("\n")
}
}

func writeData(w stringWriter, data any) error {
_, _ = w.WriteString("data:")

// A heartbeat often sets only Event. nil must not reach reflect.Value.Kind.
if data == nil {
_, _ = w.WriteString("\n\n")
return nil
}

bData, ok := data.([]byte)
if ok {
_, _ = dataReplacer.WriteString(w, string(bData))
_, _ = w.WriteString("\n\n")
return nil
}

switch kindOfData(data) { //nolint:exhaustive // remaining kinds need no special encoding
case reflect.Invalid:
_, _ = w.WriteString("\n\n")
case reflect.Struct, reflect.Slice, reflect.Map:
err := json.NewEncoder(w).Encode(data)
if err != nil {
return err
}
_, _ = w.WriteString("\n")
default:
_, _ = dataReplacer.WriteString(w, fmt.Sprint(data))
_, _ = w.WriteString("\n\n")
}
return nil
}

func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}

func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType

if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
}

func kindOfData(data any) reflect.Kind {
value := reflect.ValueOf(data)
if !value.IsValid() {
return reflect.Invalid
}
valueType := value.Kind()
if valueType == reflect.Pointer {
if value.IsNil() {
return reflect.Invalid
}
valueType = value.Elem().Kind()
}
return valueType
}
Loading