glasserc / elm-debouncer / Debouncer.Internal

The purpose of this module is to expose the "guts" of the logic so that it can be tested. It is not in the "exposed-modules" of the elm.json file, so it's not directly accessible to clients of the package.

Testing just the things exposed to clients of the package would be awkward, since it would require asynchronous tests. Here, we don't do anything actually asynchronous:

The user-facing documentation for these types is in Debouncer.Basic and Debouncer.Messages, since those are the exposed modules.


type alias Accumulator i o =
i -> Maybe o -> Maybe o

A type which represents a function which incorporates an input into the output so far.


type Config i o
    = Config (ConfigRecord i o)

An opaque type representing the configuration for a debouncer.


type alias ConfigRecord i o =
{ emitWhenUnsettled : Maybe Milliseconds
, emitWhileUnsettled : Maybe Milliseconds
, settleWhenQuietFor : Maybe Milliseconds
, accumulator : Accumulator i o 
}

Each of the items is a Maybe, since you don't necessarily want to automatically emit at all for one kind of event or another.

If all three are Nothing, then the debouncer will never automatically emit -- you would have to tell it to emit at some point, based on some external event.


type Debouncer i o
    = Debouncer (Config i o) (State o)

Our basic type. Pairs a configuration with some state that changes as inputs are received and outputs are provided.


type alias Milliseconds =
Basics.Int

A useful reminder of what our intervals mean.


type Msg i
    = InputProvidedAt i Milliseconds
    | ManualCancel
    | ManualSettle
    | ManualEmitAt Milliseconds
    | Check Milliseconds

Docs docs docs.


type State o
    = Settled
    | Unsettled (UnsettledState o)

Docs docs docs.


type alias UnsettledState o =
{ unsettledAt : Milliseconds
, lastInputProvidedAt : Milliseconds
, lastEmittedAt : Maybe Milliseconds
, output : Maybe o 
}

output needs to be a Maybe because we may have emitted while unsettled, due to emitWhileUnsettled. And, we could arrive at settleWhenQuietFor without any further input, so we don't necessarily emit anything when we become settled.

accumulateWith : Accumulator i o -> Config a b -> Config i o

Note that this changes the type of the Config to match the type of the acummulator provided.

addInputs : number -> Maybe number -> Maybe number

Accumulates by taking a sum of the inputs.

allInputs : i -> Maybe (List i) -> Maybe (List i)

Accumulates all inputs in a list.

Note that the output list will have the most recent input first.

appendInputToOutput : appendable -> Maybe appendable -> Maybe appendable

Accumulates by appending the input to the output.

appendOutputToInput : appendable -> Maybe appendable -> Maybe appendable

Accumulates by appending the output to the input.

cancel : Debouncer i o -> Debouncer i o

Cancellation is one way we can directly modify a debouncer. Basically, we just forget our state. There may be future Check ... messages coming, but they will consider what to do based solely on our state at that time. So, forgetting our state is sufficient to "cancel" any pending output.

debounce : Milliseconds -> Config i i

A starting point for a configuration which debounces -- that is, which will emit once quiet for the time you specify.

So, debounce (2 * Time.second) is equivalent to

manual
    |> settleWhenQuietFor (Just (2 * Time.second))

If you also want to emit using the first input, then you can use emitWhenSettled. For instance, the following configuration would emit the first input immediately when becoming unsettled, and then emit any subsequent input once the debouncer was quiet for 2 seconds.

debounce (2 * Time.second)
    |> emitWhenUnsettled (Just 0)

emitFirstInput : Basics.Bool -> Config i o -> Config i o

Modify a Config by controlling whether to emit the first input when becoming unsettled.

emitFirstInput True config is equivalent to

emitWhenUnsettled (Just 0) config

emitFirstInput False config is equivalent to

emitWhenUnsettled Nothing config

For more complex cases, where you want to emit the first input on a different interval than others, but not immediately, you can use emitWhenSettled directly.

emitWhenUnsettled : Maybe Milliseconds -> Config i o -> Config i o

Docs docs docs.

emitWhileUnsettled : Maybe Milliseconds -> Config i o -> Config i o

Docs docs docs.

firstInput : i -> Maybe i -> Maybe i

Accumulates by just keeping the first input.

fromSeconds : Basics.Float -> Milliseconds

A convenience when you'd rather think in seconds than milliseconds.

fromSeconds 0.5 --> 500

lastInput : i -> Maybe i -> Maybe i

Accumulates by just keeping the last input.

manual : Config i i

A starting point for a "manual" configuration.

By default, it:

So, without more, you would need to tell this debouncer when to emit something -- it would never happen automatically.

To change any of those parameters, use the various functions that alter a Config (i.e. settleWhenQuietFor, emitWhenUnsettled, emitWhileUnsettled).

By default, the output type is the same as the input type. However, you can change that by using the accumulateWith function to provide a different accumulator.

nothingIfNegative : Maybe number -> Maybe number

Docs docs docs.

sanitizeConfig : Config i o -> Config i o

Sanitize the config to simplify some of the logic.

settleWhenQuietFor : Maybe Milliseconds -> Config i o -> Config i o

Docs docs docs.

throttle : Milliseconds -> Config i i

A starting point for a configuration which throttles -- that is, which will emit the first input immediately, and then accmulate and emit no more often than the specified interval.

So, throttle (2 * Time.second) is equivalent to

manual
    |> emitWhileUnsettled (Just (2 * Time.second))
    |> emitWhenUnsettled (Just 0)

toDebouncer : Config i o -> Debouncer i o

Docs docs docs.

update : Msg i -> Debouncer i o -> ( Debouncer i o, List Milliseconds, Maybe o )

The second return parameter is not a Cmd, but instead a list of intervals at which we ought to check whether to emit something. This assists with testability (since we can test whether that list is correct). The caller is responsible for actually turning that list into commands.