github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/client/eventstream/eventstream.go (about) 1 // Package eventstream implements a convenience client for the Events method 2 // of the Tendermint RPC service, allowing clients to observe a resumable 3 // stream of events matching a query. 4 package eventstream 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "github.com/ari-anchor/sei-tendermint/rpc/coretypes" 13 ) 14 15 // Client is the subset of the RPC client interface consumed by Stream. 16 type Client interface { 17 Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error) 18 } 19 20 // ErrStopRunning is returned by a Run callback to signal that no more events 21 // are wanted and that Run should return. 22 var ErrStopRunning = errors.New("stop accepting events") 23 24 // A Stream cpatures the state of a streaming event subscription. 25 type Stream struct { 26 filter *coretypes.EventFilter // the query being streamed 27 batchSize int // request batch size 28 newestSeen string // from the latest item matching our query 29 waitTime time.Duration // the long-polling interval 30 client Client 31 } 32 33 // New constructs a new stream for the given query and options. 34 // If opts == nil, the stream uses default values as described by 35 // StreamOptions. This function will panic if cli == nil. 36 func New(cli Client, query string, opts *StreamOptions) *Stream { 37 if cli == nil { 38 panic("eventstream: nil client") 39 } 40 return &Stream{ 41 filter: &coretypes.EventFilter{Query: query}, 42 batchSize: opts.batchSize(), 43 newestSeen: opts.resumeFrom(), 44 waitTime: opts.waitTime(), 45 client: cli, 46 } 47 } 48 49 // Run polls the service for events matching the query, and calls accept for 50 // each such event. Run handles pagination transparently, and delivers events 51 // to accept in order of publication. 52 // 53 // Run continues until ctx ends or accept reports an error. If accept returns 54 // ErrStopRunning, Run returns nil; otherwise Run returns the error reported by 55 // accept or ctx. Run also returns an error if the server reports an error 56 // from the Events method. 57 // 58 // If the stream falls behind the event log on the server, Run will stop and 59 // report an error of concrete type *MissedItemsError. Call Reset to reset the 60 // stream to the head of the log, and call Run again to resume. 61 func (s *Stream) Run(ctx context.Context, accept func(*coretypes.EventItem) error) error { 62 for { 63 items, err := s.fetchPages(ctx) 64 if err != nil { 65 return err 66 } 67 68 // Deliver events from the current batch to the receiver. We visit the 69 // batch in reverse order so the receiver sees them in forward order. 70 for i := len(items) - 1; i >= 0; i-- { 71 if err := ctx.Err(); err != nil { 72 return err 73 } 74 75 itm := items[i] 76 err := accept(itm) 77 if itm.Cursor > s.newestSeen { 78 s.newestSeen = itm.Cursor // update the latest delivered 79 } 80 if errors.Is(err, ErrStopRunning) { 81 return nil 82 } else if err != nil { 83 return err 84 } 85 } 86 } 87 } 88 89 // Reset updates the stream's current cursor position to the head of the log. 90 // This method may safely be called only when Run is not executing. 91 func (s *Stream) Reset() { s.newestSeen = "" } 92 93 // fetchPages fetches the next batch of matching results. If there are multiple 94 // pages, all the matching pages are retrieved. An error is reported if the 95 // current scan position falls out of the event log window. 96 func (s *Stream) fetchPages(ctx context.Context) ([]*coretypes.EventItem, error) { 97 var pageCursor string // if non-empty, page through items before this 98 var items []*coretypes.EventItem 99 100 // Fetch the next paginated batch of matching responses. 101 for { 102 rsp, err := s.client.Events(ctx, &coretypes.RequestEvents{ 103 Filter: s.filter, 104 MaxItems: s.batchSize, 105 After: s.newestSeen, 106 Before: pageCursor, 107 WaitTime: s.waitTime, 108 }) 109 if err != nil { 110 return nil, err 111 } 112 113 // If the oldest item in the log is newer than our most recent item, 114 // it means we might have missed some events matching our query. 115 if s.newestSeen != "" && s.newestSeen < rsp.Oldest { 116 return nil, &MissedItemsError{ 117 Query: s.filter.Query, 118 NewestSeen: s.newestSeen, 119 OldestPresent: rsp.Oldest, 120 } 121 } 122 items = append(items, rsp.Items...) 123 124 if rsp.More { 125 // There are more results matching this request, leave the baseline 126 // where it is and set the page cursor so that subsequent requests 127 // will get the next chunk. 128 pageCursor = items[len(items)-1].Cursor 129 } else if len(items) != 0 { 130 // We got everything matching so far. 131 return items, nil 132 } 133 } 134 } 135 136 // StreamOptions are optional settings for a Stream value. A nil *StreamOptions 137 // is ready for use and provides default values as described. 138 type StreamOptions struct { 139 // How many items to request per call to the service. The stream may pin 140 // this value to a minimum default batch size. 141 BatchSize int 142 143 // If set, resume streaming from this cursor. Typically this is set to the 144 // cursor of the most recently-received matching value. If empty, streaming 145 // begins at the head of the log (the default). 146 ResumeFrom string 147 148 // Specifies the long poll interval. The stream may pin this value to a 149 // minimum default poll interval. 150 WaitTime time.Duration 151 } 152 153 func (o *StreamOptions) batchSize() int { 154 const minBatchSize = 16 155 if o == nil || o.BatchSize < minBatchSize { 156 return minBatchSize 157 } 158 return o.BatchSize 159 } 160 161 func (o *StreamOptions) resumeFrom() string { 162 if o == nil { 163 return "" 164 } 165 return o.ResumeFrom 166 } 167 168 func (o *StreamOptions) waitTime() time.Duration { 169 const minWaitTime = 5 * time.Second 170 if o == nil || o.WaitTime < minWaitTime { 171 return minWaitTime 172 } 173 return o.WaitTime 174 } 175 176 // MissedItemsError is an error that indicates the stream missed (lost) some 177 // number of events matching the specified query. 178 type MissedItemsError struct { 179 // The cursor of the newest matching item the stream has observed. 180 NewestSeen string 181 182 // The oldest cursor in the log at the point the miss was detected. 183 // Any matching events between NewestSeen and OldestPresent are lost. 184 OldestPresent string 185 186 // The active query. 187 Query string 188 } 189 190 // Error satisfies the error interface. 191 func (e *MissedItemsError) Error() string { 192 return fmt.Sprintf("missed events matching %q between %q and %q", e.Query, e.NewestSeen, e.OldestPresent) 193 }