erigon-pulse/erigon-lib/sse/encoder.go
a 98c57e75c0
[caplin] event source server (#8865)
eventsource is required for the validator api. this implements the
eventsource sink/server handler

the implementation is based off of this document:
https://html.spec.whatwg.org/multipage/server-sent-events.html

note that this is a building block for the full eventsource server.
there still needs to be work done

prysm has their own custom solution based off of protobuf/grpc:
https://hackmd.io/@prysmaticlabs/eventstream-api using that would be not
good

existing eventsource implementations for golang are not good for our
situation. options are:

1. https://github.com/r3labs/sse - has most stars - this is the best
contender, since it uses []byte and not string, but it allocates and
copies extra times in the server (because of use of fprintf) and makes
an incorrect assumption about Last-Event-ID needing to be a number (i
can't find this in the specification).
2. https://github.com/antage/eventsource -requires full buffers, copies
many times, does not provide abstraction for headers. relatively
unmaintained
3. https://github.com/donovanhide/eventsource - missing functionality
around sending ids, requires full buffers, etc
4. https://github.com/bernerdschaefer/eventsource - 10 years old,
unmaintained.

additionally, implemetations other than r3labs/sse are very incorrect
because they do not split up the data field correctly when newlines are
sent. (parsers by specification will fail to encode messages sent by
most of these implementations that have newlines, as i understand it).
the implementation by r3labs/sse is also incorrect because it does not
respect \r

finally, all these implementations have very heavy implementation of the
server, which we do not need since we will use fixed sequence ids.
r3labs/sse for instance hijacks the entire handler and ties that to the
server, losing a lot of flexiblity in how we implement our server
 
for the beacon api, we need to stream: 

```head, block, attestation, voluntary_exit, bls_to_execution_change, finalized_checkpoint, chain_reorg, contribution_and_proof, light_client_finality_update, light_client_optimistic_update, payload_attributes```
 
some of these are rather big json payloads, and the ability to simultaneously stream them from io.Readers instead of making a full copy of the payload every time we wish to rebroadcast it will save a lot of heap size for  both resource constrained environments and serving at scale.  

the protocol itself is relatively simple, there are just a few gotchas
2023-11-30 22:21:51 +01:00

83 lines
1.6 KiB
Go

package sse
import "io"
// Packet represents an event to send
// the order in this struct is the order that they will be sent.
type Packet struct {
// as a special case, an empty value of event will not write an event header
Event string
// additional headers to be added.
// using the reserved headers event, header, data, id is undefined behavior
// note that this is the canonical way to send the "retry" header
Header map[string]string
// the io.Reader to source the data from
Data io.Reader
// whether or not to send an id, and if so, what id to send
// a nil id means to not send an id.
// empty string means to simply send the string "id\n"
// otherwise, the id is sent as is
// id is always sent at the end of the packet
ID *string
}
func ID(x string) *string {
return &x
}
// Encoder works at a higher level than the encoder.
// it works on the packet level.
type Encoder struct {
wr *Writer
firstWriteDone bool
}
func NewEncoder(w io.Writer) *Encoder {
wr := NewWriter(w)
return &Encoder{
wr: wr,
}
}
func (e *Encoder) Encode(p *Packet) error {
if e.firstWriteDone {
err := e.wr.Next()
if err != nil {
return err
}
}
e.firstWriteDone = true
if len(p.Event) > 0 {
if err := e.wr.Header("event", p.Event); err != nil {
return err
}
}
if p.Header != nil {
for k, v := range p.Header {
if err := e.wr.Header(k, v); err != nil {
return err
}
}
}
if p.Data != nil {
if err := e.wr.WriteData(p.Data); err != nil {
return err
}
}
err := e.wr.Flush()
if err != nil {
return err
}
if p.ID != nil {
if err := e.wr.Header("id", *p.ID); err != nil {
return err
}
}
return nil
}