Created
June 24, 2020 16:19
-
-
Save cmditch/4d8df9fdb8985c8b1a17a26a0b886a82 to your computer and use it in GitHub Desktop.
elm-debounce-throttler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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