Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debouncing functionality #14

Open
DylanArnold opened this issue Apr 20, 2016 · 2 comments
Open

Add debouncing functionality #14

DylanArnold opened this issue Apr 20, 2016 · 2 comments

Comments

@DylanArnold
Copy link
Contributor

DylanArnold commented Apr 20, 2016

Quick and dirty rushed implementation. Could be useful for some ideas.

EDIT: Original gist deleted but I dug up an old commit which should work.

import java.util.Date

import diode.ActionResult.NoChange
import diode.data.AsyncAction
import diode.{ActionProcessor, ActionResult, Dispatcher}

class DebounceProcessor[M <: AnyRef] extends ActionProcessor[M] {
  var cleanup = Seq[(Long, AnyRef)]()

  var actionRefLastSent = Map[AnyRef, Long]()
  val delta = 1000

  def clean(now: Long): Unit = {
    cleanup = cleanup.dropWhile { case (itemTime, ref) =>
      actionRefLastSent.get(ref) match {
        case None => {
          // This action has already been cleaned. Drop current item.
          true
        }
        case Some(lastSentTime) => {
          if (itemTime < lastSentTime) {
            // This item has been superseeded by a new action
            true
          }
          else {
            if (lastSentTime + delta <= now) {
              actionRefLastSent -= ref
              true
            }
            else {
              // Item hasn't expired yet
              false
            }
          }
        }
      }
    }
  }


  override def process(dispatch: Dispatcher, action: Any, next: (Any) => ActionResult[M], currentModel: M): ActionResult[M] = {
    val now = new Date().getTime

    clean(now)

    action match {
      case a: AsyncAction[_, _] => {
        if (actionRefLastSent.contains(a)) {
          NoChange
        }
        else {
          actionRefLastSent += a -> now
          cleanup :+= now -> a

          next(action)
        }
      }
      case _ => {
        next(action)
      }
    }
  }
}
@DylanArnold DylanArnold changed the title Add debouncing functionallity Add debouncing functionality Apr 20, 2016
@DylanArnold
Copy link
Contributor Author

DylanArnold commented May 29, 2016

I've been thinking about a different solution to this by the way.

Option 1

If different fetchers had unique IDs, dispatched actions from a fetcher could go into some internal state such as:

var activeFetches = Set[FetcherId]()

When a fetch starts, add it to the set and perform the effect.

If another fetch gets triggered while this one is in progress (already in set), (discard it / debounce).

When the effect completes, remove the id from the set.

It's basically a lock during a fetch.

Option 2

Another possibility I've been thinking about, might be something like storing the AsyncActions that have triggered Effects until the future completes. Something like that is probably simpler, and similar I guess to the debouncing implementation in a way. Some method to dispatch the Effect would also store the action temporarily (in model or somewhere else?). The difference is that this would happen in the ActionHandler, not an ActionProcessor. The key improvement would be that this is the point you have access to the specific future, so you know when it is done and can "bounce" any duplicate actions in the action handler. This does away with any debounce interval.

Option 2.5

Similar to option 2 but I realise that maybe you can use an ActionProcessor. Maybe you'd just need some extra notification that an Effect/Action has completed. The processor would still store the Action when an Effect is first triggered (taking a lock), when the Effect is completed something would need to trigger some other Action (ReleaseLock(originalAction) ??) that the processor intercepts to remove the stored action.

Something like the above should be better than debouncing because what is a good interval to debounce an action within? I've found it's a bit latency dependent. If your pots take longer than the debounce interval to resolve then the fetch action can get re-triggered while your model is still reaching a 'steady state'.

I realise this may be out of scope for Diode, just throwing it out there, and I expect there is more to it for a complete solution. I think I'll eventually play with something like this though.

Edit: I actually still hit cases where the debounce interval isn't high enough, which results in a cascading flood of requests, which in turn pushes the resolution time up to the point where "it's not going to finish in a reasonable amount of time". I definitely don't trust debouncing as a strategy to solve this problem enough so will probably at the very least implement something like the above.

@math85360
Copy link

Hi !

I can't access to the gist but I need also debouncing (but not the same case I think) to parse input in Javascript but it's too costly to parse input for each key.

My naive code below :

case class CancelDebounce(handle: SetTimeoutHandle, ifCanceled: () => Unit) {
  def apply() {
    ifCanceled()
    clearTimeout(handle)
  }
}

case class DebouncedException() extends Exception("Debounced")

class DebounceJS extends RunAfter {
  // OK only in JS
  private var activeTimeout: Option[CancelDebounce] = None

  override def runAfter[A](delay: FiniteDuration)(f: => A) = {
    val p = Promise[A]()
    val oldTimeout = activeTimeout
    activeTimeout = None
    oldTimeout.foreach(_())
    activeTimeout = Some(CancelDebounce(
      setTimeout(delay)({
        activeTimeout = None
        p.success(f)
      }),
      () => () /*p.failure(DebouncedException())*/ ))
    // With p.failure, the error will be received by handleError(msg: String) 
    // so we can't catch this error properly
    // Need to change handleError(msg:String) to a handleError(action:Action, error: Throwable)
    // Without p.failure, the promise will never be completed
    // Maybe a problem with any long-time application
    // Can cause memory leaks with lot of debouncing usage ??
    p.future
  }
}

Now I can use it with :

// I use object because DebounceJS must be persistent as a singleton
object DebounceInput extends DebounceJS

// Instead of the implicit runAfterJS, I use my DebounceJS instance
Effect.action(ActionToDispatch()).after(300.millis)(DebounceInput)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants