github.com/haraldrudell/parl@v0.4.176/debouncer.go (about) 1 /* 2 © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "sync/atomic" 10 "time" 11 12 "github.com/haraldrudell/parl/ptime" 13 ) 14 15 const ( 16 // disables the debounce time 17 // - debounce time holds incoming items until 18 // debounce time elapses with no additional items 19 // - when disabled max delay defaults to1 s and 20 // items are sent when maxDelay reached 21 NoDebounceTime time.Duration = 0 22 // disables debouncer max delay function 23 // - when debounce timer holds items, those items 24 // are sent when age reaches maxDelay 25 // - when debounce time disabled, defaults to 1 s. 26 // otherwise no default 27 NoDebounceMaxDelay time.Duration = 0 28 // maxDelay when debounce-time disabled 29 defaultDebouncerMaxDelay = time.Second 30 ) 31 32 // Debouncer debounces event stream values 33 // - T values are received from the in channel 34 // - Once d time has elapsed with no further incoming Ts, 35 // a slice of read T values are provided to the sender function 36 // - errFn receives any panics in the threads, expected none 37 // - sender and errFn functions must be thread-safe. 38 // - Debouncer is shutdown gracefully by closing the input channel or 39 // immediately by invoking the Shutdown method 40 // - — 41 // - two threads are launched per debouncer 42 type Debouncer[T any] struct { 43 in *debouncerIn[T] // input thread 44 out *debouncerOut[T] // output thread 45 isShutdown *Awaitable // shutdown control 46 } 47 48 // debouncerIn implements the debouncer input-thread 49 type debouncerIn[T any] struct { 50 // from where incoming values for debouncing are read 51 inputCh <-chan T 52 // non-blocking unbound buffer to output thread 53 buffer NBChan[T] 54 // how long time must pass between two consecutive 55 // incoming values in order to submit to output channel 56 debounceInterval time.Duration 57 // how input-thread orders output thread to send 58 // on expired debounce period 59 debounceTimer *time.Timer 60 // is maxDelay timer is used 61 useMaxDelay bool 62 // when input thread receives a a value and max delay timer is not running, 63 // max delay timer is started. 64 // - max delay timer then runs until output thread resets it. 65 maxDelayRunning atomic.Bool 66 // how input-thread orders output thread to send 67 // on expired maxDelay period 68 maxDelayTimer ptime.ThreadSafeTimer 69 // how input thread receives shutdown 70 isShutdown *Awaitable 71 // how input thread emits an unforeseen panic 72 errFn AddError 73 // awaitable indicating input thread exit 74 inputExit Awaitable 75 } 76 77 // debouncerIn implements the debouncer input-thread 78 type debouncerOut[T any] struct { 79 // non-blocking unbound buffer from input thread 80 buffer *NBChan[T] 81 // send trigger based on debounce time expired 82 debounceC <-chan time.Time 83 // is maxDelay timer is used 84 useMaxDelay bool 85 // when input thread receives a a value and max delay timer is not running, 86 // max delay timer is started. 87 // - max delay timer then runs until output thread resets it. 88 maxDelayRunning *atomic.Bool 89 // maxDelayTimer timer expiring when output thread should send 90 maxDelayTimer *ptime.ThreadSafeTimer 91 // indicates that input thread exited 92 isInputExit AwaitableCh 93 // the output function receiving slices of values 94 sender func([]T) 95 // how output thread receives shutdown 96 isShutdown *Awaitable 97 // how output thread emits an unforeseen panic 98 errFn AddError 99 // awaitable indicating output thread exit 100 outputExit Awaitable 101 } 102 103 // NewDebouncer returns a channel debouncer 104 // - values incoming faster than debounceInterval are aggregated 105 // into slices 106 // - values are not kept waiting longer than maxDelay 107 // - debounceInterval is only used if > 0 ns 108 // - if debounceInterval is not used and maxDelay is 0, 109 // maxDelay defaults to 1 s to avoid a hanging debouncer 110 // - sender should not be long-running or blocking 111 // - inputCh sender errFn cannot be nil 112 // - close of input channel or Shutdown is required to release resources 113 // - errFn should not receive any errors but will receive possible runtime panics 114 // - — 115 // - NewDebouncer launches two threads prior to return 116 func NewDebouncer[T any]( 117 debounceInterval, maxDelay time.Duration, 118 inputCh <-chan T, 119 sender func([]T), 120 errFn AddError, 121 ) (debouncer *Debouncer[T]) { 122 if inputCh == nil { 123 panic(NilError("inputCh")) 124 } else if sender == nil { 125 panic(NilError("sender")) 126 } else if errFn == nil { 127 panic(NilError("errFn")) 128 } 129 130 var isShutdown Awaitable 131 132 // debounce timer expiring when output thread should send 133 var debounceTimer = time.NewTimer(time.Second) 134 // get timer ready for reset 135 debounceTimer.Stop() 136 if len(debounceTimer.C) > 0 { 137 <-debounceTimer.C 138 } 139 140 // 1 s default for maxDelay 141 if debounceInterval <= 0 && maxDelay <= 0 { 142 maxDelay = defaultDebouncerMaxDelay 143 } 144 145 in := debouncerIn[T]{ 146 inputCh: inputCh, 147 debounceInterval: debounceInterval, 148 debounceTimer: debounceTimer, 149 useMaxDelay: maxDelay > 0, 150 maxDelayTimer: *ptime.NewThreadSafeTimer(maxDelay), 151 isShutdown: &isShutdown, 152 errFn: errFn, 153 } 154 // get timer ready for reset 155 in.maxDelayTimer.Stop() 156 if len(in.maxDelayTimer.C) > 0 { 157 <-in.maxDelayTimer.C 158 } 159 out := debouncerOut[T]{ 160 buffer: &in.buffer, 161 debounceC: debounceTimer.C, 162 useMaxDelay: in.useMaxDelay, 163 maxDelayRunning: &in.maxDelayRunning, 164 maxDelayTimer: &in.maxDelayTimer, 165 isInputExit: in.inputExit.Ch(), 166 sender: sender, 167 isShutdown: &isShutdown, 168 errFn: errFn, 169 } 170 171 go out.outputThread() 172 go in.inputThread() 173 174 return &Debouncer[T]{ 175 in: &in, 176 out: &out, 177 isShutdown: &isShutdown, 178 } 179 } 180 181 // Shutdown shuts down the debouncer 182 // - Shutdown does not return until resources have been released 183 // - buffered values are discarded and input channle is not read to end 184 func (d *Debouncer[T]) Shutdown() { 185 d.isShutdown.Close() 186 d.Wait() 187 } 188 189 // Wait blocks until the debouncer exits 190 // - the debouncer exits from input channel closing or Shutdown 191 func (d *Debouncer[T]) Wait() { 192 <-d.in.inputExit.Ch() 193 <-d.out.outputExit.Ch() 194 } 195 196 // inputThread debounces the input channel until it closes or Shutdown 197 func (d *debouncerIn[T]) inputThread() { 198 defer d.inputExit.Close() 199 defer Recover(func() DA { return A() }, nil, OnError(d.errFn)) 200 defer d.maxDelayTimer.Stop() 201 defer d.debounceTimer.Stop() 202 defer d.buffer.Close() // close of buffer causes output thread to eventually exit 203 204 // debounce timer was started 205 var debounceTimerRunning bool 206 207 // read input channel and save values to unbound buffer 208 for { 209 210 // wait for value or shutdown 211 var value T 212 var hasValue bool 213 select { 214 case value, hasValue = <-d.inputCh: 215 if hasValue { 216 break // a value was received 217 } 218 return // the input channel closed return 219 case <-d.isShutdown.Ch(): 220 return // shutdown received return 221 } 222 223 // put read value in unbound buffer 224 d.buffer.Send(value) 225 226 // a value was received. If max delay is used and not running, 227 // start it 228 if d.useMaxDelay && d.maxDelayRunning.CompareAndSwap(false, true) { 229 d.maxDelayTimer.Reset(0) 230 } 231 232 // if debounce timer is used, 233 // start or extend debounce timer 234 if d.debounceInterval > 0 { 235 if debounceTimerRunning { 236 // get debounceTimer ready for reset 237 d.debounceTimer.Stop() 238 select { 239 case <-d.debounceTimer.C: 240 default: 241 } 242 } else { 243 debounceTimerRunning = true 244 } 245 // Reset should be invoked only on: 246 // - stopped or expired timers 247 // - with drained channels 248 d.debounceTimer.Reset(d.debounceInterval) 249 } 250 } 251 } 252 253 // outputThread copies the unbound buffer to sender whenever 254 // a timer expires 255 func (d *debouncerOut[T]) outputThread() { 256 defer d.isShutdown.Close() // shutdown input thread if running 257 defer d.outputExit.Close() 258 defer Recover(func() DA { return A() }, nil, OnError(d.errFn)) 259 260 // while buffer is not closed and emptied, wait for: 261 // - debounce timer expired triggering send, 262 // - maxDelay timer expired triggering send, 263 // - input thread exiting or 264 // - shutdown causing exit 265 for !d.buffer.IsClosed() { 266 select { 267 // input thread starts and extends the debounce timer as 268 // values are received 269 // - if it expires due to long time between incoming values, 270 // it triggers a send here 271 case <-d.debounceC: 272 // input thread starts the max delay timer upon receining a value 273 // and it is not running 274 // - if it expires prior to debounce timer, it triggers a send here 275 case <-d.maxDelayTimer.C: // send due to max Delay reached 276 case <-d.isInputExit: // input thread did exit 277 case <-d.isShutdown.Ch(): 278 return // shutdown received 279 } 280 281 // sending values, so reset max delay timer 282 if d.useMaxDelay && d.maxDelayRunning.Load() { 283 d.maxDelayTimer.Stop() 284 d.maxDelayRunning.Store(false) 285 } 286 287 // send any values 288 if values := d.buffer.Get(); len(values) > 0 { 289 d.sender(values) 290 } 291 } 292 }