github.com/diamondburned/arikawa/v2@v2.1.0/utils/wsutil/heart.go (about) 1 package wsutil 2 3 import ( 4 "context" 5 "time" 6 7 "github.com/pkg/errors" 8 9 "github.com/diamondburned/arikawa/v2/internal/heart" 10 ) 11 12 type errBrokenConnection struct { 13 underneath error 14 } 15 16 // Error formats the broken connection error with the message "explicit 17 // connection break." 18 func (err errBrokenConnection) Error() string { 19 return "explicit connection break: " + err.underneath.Error() 20 } 21 22 // Unwrap returns the underlying error. 23 func (err errBrokenConnection) Unwrap() error { 24 return err.underneath 25 } 26 27 // ErrBrokenConnection marks the given error as a broken connection error. This 28 // error will cause the pacemaker loop to break and return the error. The error, 29 // when stringified, will say "explicit connection break." 30 func ErrBrokenConnection(err error) error { 31 return errBrokenConnection{underneath: err} 32 } 33 34 // IsBrokenConnection returns true if the error is a broken connection error. 35 func IsBrokenConnection(err error) bool { 36 var broken *errBrokenConnection 37 return errors.As(err, &broken) 38 } 39 40 // TODO API 41 type EventLoopHandler interface { 42 EventHandler 43 HeartbeatCtx(context.Context) error 44 } 45 46 // PacemakerLoop provides an event loop with a pacemaker. A zero-value instance 47 // is a valid instance only when RunAsync is called first. 48 type PacemakerLoop struct { 49 heart.Pacemaker 50 Extras ExtraHandlers 51 ErrorLog func(error) 52 53 events <-chan Event 54 control chan func() 55 handler func(*OP) error 56 } 57 58 func (p *PacemakerLoop) errorLog(err error) { 59 if p.ErrorLog == nil { 60 WSDebug("Uncaught error:", err) 61 return 62 } 63 64 p.ErrorLog(err) 65 } 66 67 // Pace calls the pacemaker's Pace function. 68 func (p *PacemakerLoop) Pace(ctx context.Context) error { 69 return p.Pacemaker.PaceCtx(ctx) 70 } 71 72 // StartBeating asynchronously starts the pacemaker loop. 73 func (p *PacemakerLoop) StartBeating(pace time.Duration, evl EventLoopHandler, exit func(error)) { 74 WSDebug("Starting the pacemaker loop.") 75 76 p.Pacemaker = heart.NewPacemaker(pace, evl.HeartbeatCtx) 77 p.control = make(chan func()) 78 p.handler = evl.HandleOP 79 p.events = nil // block forever 80 81 go func() { exit(p.startLoop()) }() 82 } 83 84 // Stop signals the pacemaker to stop. It does not wait for the pacer to stop. 85 // The pacer will call the given callback with a nil error. 86 func (p *PacemakerLoop) Stop() { 87 close(p.control) 88 } 89 90 // SetEventChannel sets the event channel inside the event loop. There is no 91 // guarantee that the channel is set when the function returns. This function is 92 // concurrently safe. 93 func (p *PacemakerLoop) SetEventChannel(evCh <-chan Event) { 94 p.control <- func() { p.events = evCh } 95 } 96 97 // SetPace (re)sets the pace duration. As with SetEventChannel, there is no 98 // guarantee that the pacer is reset when the function returns. This function is 99 // concurrently safe. 100 func (p *PacemakerLoop) SetPace(pace time.Duration) { 101 p.control <- func() { p.Pacemaker.SetPace(pace) } 102 } 103 104 func (p *PacemakerLoop) startLoop() error { 105 defer WSDebug("Pacemaker loop has exited.") 106 defer p.Pacemaker.StopTicker() 107 108 for { 109 select { 110 case <-p.Pacemaker.Ticks: 111 if err := p.Pacemaker.Pace(); err != nil { 112 return errors.Wrap(err, "pace failed, reconnecting") 113 } 114 115 case fn, ok := <-p.control: 116 if !ok { // Intentional stop at p.Close(). 117 WSDebug("Pacemaker intentionally stopped using p.control.") 118 return nil 119 } 120 121 fn() 122 123 case ev, ok := <-p.events: 124 if !ok { 125 WSDebug("Events channel closed, stopping pacemaker.") 126 return nil 127 } 128 129 if ev.Error != nil { 130 return errors.Wrap(ev.Error, "event returned error") 131 } 132 133 o, err := DecodeOP(ev) 134 if err != nil { 135 return errors.Wrap(err, "failed to decode OP") 136 } 137 138 // Check the events before handling. 139 p.Extras.Check(o) 140 141 // Handle the event 142 if err := p.handler(o); err != nil { 143 if IsBrokenConnection(err) { 144 return errors.Wrap(err, "handler failed") 145 } 146 147 p.errorLog(err) 148 } 149 } 150 } 151 }