I’m experimenting with a debouncer interface (full snippet available here):
newtype Debouncer :: Type -> Type
newtype Debouncer a = Debouncer
{ ref :: AVar (Aff Unit)
, delayMillis :: Milliseconds
}
type Task :: Type -> Type
type Task a =
{ action :: Aff a
, callback :: a -> Effect Unit
}
new :: forall a. Milliseconds -> Effect (Debouncer a)
new delayMillis = do
ref <- AVar.new $ pure unit
pure $ Debouncer { ref, delayMillis }
run :: forall a. Debouncer a -> Task a -> Effect Unit
run (Debouncer { ref, delayMillis }) { action, callback } = do
Aff.runAff_ (handler callback) do
-- 1. Kill the previous task instance if it's still queued.
Bind.join $ AffAVar.take ref
-- 2. Fork the current task instance.
fiber <- Aff.forkAff do
Aff.delay delayMillis
action
-- 3. Store an effect that, when called, will kill the current task instance.
AffAVar.put (Aff.killFiber killedByDebouncer fiber) ref
-- 4. Wait until the task completes or was killed.
Aff.joinFiber fiber
This seems to be working well from initial testing, but I’m curious if it could use more safety. Specifically, is it possible for the outer Aff
within run
to be somehow killed externally after the AVar
has been taken from (1) but before the new kill action can be put into the AVar
(3)?
I want to avoid the possibility of the AVar
being taken from but then never refilled, causing future calls to run
to block indefinitely. I see that a bracket
operation does exist in purescript-aff
, but I’m not sure if it’s necessary/warranted for this use case where the Aff
computations in run
aren’t exposed (minus the user-specified action
).
This code is targeting the browser and the specific use case is hitting a backend on form input changes.