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  }