mirror of
https://gitlab.com/pulsechaincom/erigon-pulse.git
synced 2024-12-22 11:41:19 +00:00
98c57e75c0
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
83 lines
1.6 KiB
Go
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
|
|
}
|