Skip to content

Instantly share code, notes, and snippets.

@cmditch
Created June 24, 2020 16:19
Show Gist options
  • Save cmditch/4d8df9fdb8985c8b1a17a26a0b886a82 to your computer and use it in GitHub Desktop.
Save cmditch/4d8df9fdb8985c8b1a17a26a0b886a82 to your computer and use it in GitHub Desktop.
elm-debounce-throttler
module Data.Debouncer exposing
( Config
, Debouncer
, Msg
, cancel
, init
, poke
, update
)
import Dict exposing (Dict)
import Process
import Task
import Time exposing (Posix)
{-| Unique in that it can throttle and debounce at the same time, very similar
to what you see in Google Docs. For example, say you're typing in a note...
config =
{ config | debounce = 1000, throttle = 5000 }
This will save after one second of idleness, and will guaruntee a save every 5
seconds when you're actively typing.
If debounce == throttle it behaves like just a debouncer.
Also, a single debouncer can track the state of multiple entities,
if it makes sense they share the same Config.
-}
type Debouncer msg
= Debouncer (Config msg) (Dict String (Tracker msg))
type alias Config msg =
{ debounce : Int
, throttle : Int
, toSelf : Msg msg -> msg
}
type alias Tracker msg =
{ pokedAt : Posix
, initializedAt : Posix
, send : msg
}
type alias Id =
String
{-| Initialize a Debouncer
type Msg
= UpdateNote ...
| UpdatePerson
| SaveNotes
| SavePeople
| DebounceMsg Debouncer.Msg
myDebouncer =
Debounce.init
{ debounce = 1000
, throttle = 6000
, toSelf = DebounceMsg
}
-}
init : Config msg -> Debouncer msg
init config =
Debouncer config Dict.empty
{-| Start/Reset the debounce timer
update model msg =
case msg of
UpdateNote ... ->
( newModel
, Debouncer.poke "notes"
SaveNotes
model.debouncer
)
UpdatePerson ... ->
( newModel
, Debouncer.poke "people"
SavePeople
model.debouncer
)
You can also conditionally update the msg to send mid-debouncing:
sendMsg =
if noteSize < 1 MB then
SaveNotes
else
ShowError
pokeDebouncerCmd =
Debouncer.poke "notes"
sendMsg
model.debouncer
-}
poke : Id -> msg -> Debouncer msg -> Cmd msg
poke id send (Debouncer config _) =
Time.now
|> Task.perform (Msg (Poke send) id)
|> Cmd.map config.toSelf
{-| Cancel out an active debouncing
-}
cancel : Id -> Debouncer msg -> Debouncer msg
cancel id (Debouncer config trackers) =
Debouncer config <| Dict.remove id trackers
type Msg msg
= Msg (Op msg) Id Posix
type Op msg
= Check
| Poke msg
{-| TEA Boilerplate
update model msg =
case msg of
DebounceMsg msg ->
let
( newDebouncer, cmd ) =
Debouncer.update msg model.debouncer
in
( { model | debouncer = newDebouncer }
, Cmd.map HandleBacktalk cmd
)
-}
update : Msg msg -> Debouncer msg -> ( Debouncer msg, Cmd msg )
update (Msg op id time) (Debouncer config trackers) =
case op of
Poke send ->
let
toDebouncer tracker =
Debouncer config (Dict.insert id tracker trackers)
in
case Dict.get id trackers of
Nothing ->
-- Start the Check loop
toDebouncer
{ pokedAt = time
, initializedAt = time
, send = send
}
|> update (Msg Check id time)
-- Update pokedAt for existing Check loop
Just tracker ->
( toDebouncer { tracker | pokedAt = time, send = send }
, Cmd.none
)
Check ->
let
-- Start or continue the Check loop
debounce =
Process.sleep (clamp 1 1000 <| toFloat config.debounce)
|> Task.andThen (\_ -> Time.now)
|> Task.perform (Msg Check id)
|> Cmd.map config.toSelf
-- Reset the tracker when a msg is sent
sendAndReset send =
( Debouncer config (Dict.remove id trackers)
, Task.perform (\_ -> send) (Task.succeed ())
)
in
case Dict.get id trackers of
Nothing ->
( Debouncer config trackers
, Cmd.none
)
Just { pokedAt, initializedAt, send } ->
if diff time initializedAt >= config.throttle then
sendAndReset send
else if diff time pokedAt < config.debounce then
( Debouncer config trackers
, debounce
)
else
sendAndReset send
diff : Posix -> Posix -> Int
diff t1 t2 =
(Time.posixToMillis t1 - Time.posixToMillis t2)
|> abs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment