Documentation
¶
Overview ¶
Package throttle rate-limits how often a function may run, invoking it at most once per a fixed wait duration. It is a stdlib-only Go port of lodash's throttle (https://lodash.com/docs/#throttle), matching that function's leading/trailing edge behavior along with its cancel and flush controls; the same leading/trailing model is shared by the npm "throttle-debounce" package. Throttling is used to tame high-frequency events — scroll and resize handlers, keystrokes, progress callbacks, log flushing — so expensive work runs at a bounded rate instead of on every event.
A Throttler is created with New(wait, fn, opts...). Calling its Call method stands in for "the event fired"; the Throttler decides whether to run fn now, schedule it for the end of the current window, or ignore it because fn already ran recently. Regardless of how many times Call is invoked inside one wait window, fn runs at most once for the leading edge and at most once for the trailing edge of that window. This is distinct from debouncing, which resets its timer on every call and only fires after activity stops; a throttle guarantees steady progress during a sustained burst.
Leading and trailing behavior is configurable and both default to enabled, exactly as in lodash. With leading enabled, the first Call in an idle period invokes fn immediately. With trailing enabled, if one or more further Calls arrive during the wait window, fn is invoked once more when the window elapses, so the most recent activity is not lost. Disabling leading via WithLeading(false) delays the first invocation to the trailing edge; disabling trailing via WithTrailing(false) drops the follow-up call. If both are disabled fn never runs. When calls are spaced farther apart than wait, each one is treated as a fresh leading edge and invokes fn immediately.
Cancel discards any pending trailing invocation and resets the Throttler to an idle state, so the next Call is again treated as a leading edge. Flush immediately invokes a pending trailing call (if trailing is enabled and one is scheduled) rather than waiting for the timer, and then clears the pending state so no double invocation occurs. Pending reports whether a timer is currently scheduled. These three methods mirror lodash's cancel, flush, and pending helpers.
The clock is injectable via WithClock, which supplies a Clock implementation providing Now and AfterFunc plus a Timer with Stop. The default clock uses time.Now and time.AfterFunc, so real timers fire fn on their own goroutines; a fake clock can be advanced manually to make tests and examples fully deterministic without sleeping. A Throttler serializes its own state with an internal mutex, so Call, Cancel, Flush, and Pending are safe to invoke from multiple goroutines; the wrapped fn must itself be safe for the concurrency it may see.
Example ¶
Example demonstrates the leading-and-trailing behavior of a throttle using an injected fake clock so the timing is deterministic. The first Call fires the wrapped function immediately on the leading edge, taking the counter to 1. A second Call inside the same wait window does not fire again but schedules a trailing invocation. Advancing the clock past the wait duration fires that trailing call, taking the counter to 2. This shows that no matter how many times Call is invoked in one window, the function runs at most once for the leading edge and once for the trailing edge.
package main
import (
"fmt"
"time"
"github.com/malcolmston/express/throttle"
)
// fakeClock is a manually advanced Clock so the example is fully deterministic
// and does not sleep on real time.
type fakeClock struct {
now time.Time
timers []*fakeTimer
}
type fakeTimer struct {
at time.Time
fn func()
stopped bool
}
func (t *fakeTimer) Stop() bool {
if t.stopped {
return false
}
t.stopped = true
return true
}
func (c *fakeClock) Now() time.Time { return c.now }
func (c *fakeClock) AfterFunc(d time.Duration, fn func()) throttle.Timer {
t := &fakeTimer{at: c.now.Add(d), fn: fn}
c.timers = append(c.timers, t)
return t
}
// Advance moves the clock forward and fires any timers that are now due.
func (c *fakeClock) Advance(d time.Duration) {
c.now = c.now.Add(d)
for _, t := range c.timers {
if !t.stopped && !t.at.After(c.now) {
t.stopped = true
t.fn()
}
}
}
// Example demonstrates the leading-and-trailing behavior of a throttle using an
// injected fake clock so the timing is deterministic. The first Call fires the
// wrapped function immediately on the leading edge, taking the counter to 1. A
// second Call inside the same wait window does not fire again but schedules a
// trailing invocation. Advancing the clock past the wait duration fires that
// trailing call, taking the counter to 2. This shows that no matter how many
// times Call is invoked in one window, the function runs at most once for the
// leading edge and once for the trailing edge.
func main() {
clock := &fakeClock{now: time.Unix(0, 0)}
var count int
th := throttle.New(100*time.Millisecond, func() { count++ }, throttle.WithClock(clock))
th.Call() // leading edge fires immediately
th.Call() // within the window: schedules a trailing call
fmt.Println(count)
clock.Advance(100 * time.Millisecond) // trailing call fires
fmt.Println(count)
}
Output: 1 2
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// AfterFunc waits for the duration to elapse and then calls fn in its own
// goroutine. It returns a Timer that can be used to cancel the call.
AfterFunc(d time.Duration, fn func()) Timer
}
Clock abstracts time so that throttling can be tested with a fake clock.
type Option ¶
type Option func(*Throttler)
Option configures a Throttler.
func WithLeading ¶
WithLeading enables or disables invoking on the leading edge of the timeout. It is enabled by default.
func WithTrailing ¶
WithTrailing enables or disables invoking on the trailing edge of the timeout. It is enabled by default.
type Throttler ¶
type Throttler struct {
// contains filtered or unexported fields
}
Throttler is a throttled wrapper around a function.
func New ¶
New creates a Throttler that invokes fn at most once per wait. By default it invokes on both the leading and trailing edges.
func (*Throttler) Call ¶
func (t *Throttler) Call()
Call invokes the throttled behavior. It fires at most once per wait.
func (*Throttler) Cancel ¶
func (t *Throttler) Cancel()
Cancel discards any pending trailing invocation.