I’m pulling my hair… I have created this debouncer that accepts en event-handler/dispatcher and some data to call the event-handler with, debounced.
;; Adapted from:
(ns pez.debounce)
(def !debounced-events (atom {}))
(defn cancel-timeout! [id]
(js/clearTimeout (:timeout (@!debounced-events id)))
(swap! !debounced-events dissoc id))
(defn dispatch-debounce! [{:keys [id type actions timeout event-handler]
:or {type :dispatch}}]
(case type
:dispatch (let [timeout-id (js/setTimeout (fn []
(swap! !debounced-events dissoc id)
(event-handler nil actions))
timeout)]
(cancel-timeout! id)
(swap! !debounced-events assoc id
{:timeout timeout-id
:dispatch actions}))
:cancel (cancel-timeout! id)
:flush (let [flush-actions (get-in @!debounced-events [id :dispatch])]
(cancel-timeout! id)
(event-handler nil flush-actions))
(js/console.warn "dispatch-debounce!: ignoring bad :dispatch-debounce action:" type "id:" id)))
It never calls the event-handler. In fact never callse the function passed to js/setTimout . Unless I evaluate the !debounced-events atom in the repl. Then it all starts to work exactly as I intend. I am clearly doing something wrong, but what?There are a few recommendations I would suggest to narrow down the issue:
• There are a few "read, then write" operations on the atom. You can usually replace them with swap-vals!. Eg. update the atom and then perform the action based on the change that occurred.
• Use defonce for the !debounced-events. In a dev environment, it's easy to accidently create a new atom that is only used by some subset of cancel-timeout! , dispatch-debounce! and your repl calls.
Thanks! I’ll see what swap-vals! can do for me here. Yes, it should be defomce, but atm def is my way of seeing the debouncer working…
if you reevaluate the def, you need to also re-evaluate the functions that use the atom.
typically, I would have a rich comment block to reset the atom. I don't think you would typically want to create new instances.
If you don't have a reason to write it yourself, I would consider looking for existing implementations, https://cloogle.phronemophobic.com/doc-search.html?q=debounce+an+operation
Totally agree. It is just that in the funny state things are evaluating the def makes the debouncer wake up.
That sounds odd. Can you call js/setTimeout directly?
It is odd like few odd things I have ever seen.
Why are there parens around @!debounced
in cancel-timeout
oh
How do you mean calling js/setTimeout directly, @smith.adriane?
I think the parens are correct. maps are functions of their keys.
> In fact never callse the function passed to js/setTimout .
I'm trying to figure out if there's something odd about your environment or some error in your code. If you have a top level call to js/setTimeout, does the function get called?
The debouncer works fine in another project I have. And I am pretty sure quite a few re-frame apps are using it, or variants of it. My change was to pass in the dispatcher/event-handler.
Do you have the diff? It might be easier to analyze the diff rather than the whole setup.
I haven’t tried. But I can run this in the repl:
(debounce/dispatch-debounce! {:id :foo
:type :dispatch
:actions [[:ax/assoc :foo :bar]]
:timeout 2000
:event-handler event-handler})
And it does what I expect.So, maybe that means that the debouncer is fine, but that I use it from a place where it won’t work. I am using it from the draw function of a quil sketch.
I’ll push the whole thing and try to give some context.
So, I realized that what I actually want is a throttle. The way I am using the debouncer it isn’t supposed to ever fire. facepalm . Why it started to behave like a throttle when I reevaluated the atom is beyond me, and also it doesn’t much matter. 😃 I now have a throttle like so:
(ns pez.throttle)
(defonce !throttles (atom {}))
(defn dispatch! [{:keys [id timeout thunk]}]
(let [maybe-schedule (fn [current-throttles]
(if (contains? current-throttles id)
current-throttles
(assoc current-throttles id
{:timeout (js/setTimeout
(fn []
(swap! !throttles dissoc id))
timeout)})))
[previous-throttles _] (swap-vals! !throttles maybe-schedule)]
(when-not (contains? previous-throttles id)
(thunk))))
It works great. However, I also now see that I have probably thought about my original problem the wrong way, and I won’t be needing this throttle. I will keep the learnings, though. Especially getting reminded about swap-vals! was a wake-up call for me. Thanks, @smith.adriane! ❤️ 🙏Saving it as a gist here: https://gist.github.com/PEZ/785208524dca981cd6c238959ae74782
i am sorry but why are you trying to reinvent the wheel where there are many packages that you can easily use it. isnt that the whole point of clojurescript aka leverage js libraries... just use this: https://github.com/xnimorz/use-debounce
A reason not to use that particular one is that I am not using React. 😃 (And that’s because someone decided to reinvent a wheel, see #replicant) But I was not intentionally intending to re-invent a wheel, I was using a solution that people use for re-frame effects. It has served me well before, but now I was trying to use it for something it was not intended to and I started down a route of trying to fix it. None of the thousands of debounce functions already out there would have worked. It was late at night, is a possible excuse. For the throttle I decided to roll my own because it was a good exercise and an opportunity to practice using swap-vals!.
@pez It doesn't really matter for js since everything is single-threaded, but it's good practice to avoid side effects in swap functions since they can be retried. One trick is to wrap the effect in a delay and deref it after the swap:
(ns pez.throttle)
(defonce !throttles (atom {}))
(defn dispatch! [{::keys [id timeout thunk]}]
(let [maybe-schedule (fn [current-throttles]
(if (contains? current-throttles id)
current-throttles
(assoc current-throttles id
{:timeout
;; no side effects
(delay
(js/setTimeout
(fn []
(swap! !throttles dissoc id))
timeout))})))
[previous-throttles new-throttles] (swap-vals! !throttles maybe-schedule)]
(when-not (contains? previous-throttles id)
;; start timeout
(-> new-throttles (get id) :timeout deref)
(thunk))))
I haven't tested this code, but it hopefully gives the right idea.True-ish, in Clojure a swap! can be retried and the orthodox solution is to fire off an agent. The agent will execute only if the transaction commits. However, in ClojureScript an atom will not retry; there is no other thread to be messing with it; therefore agents do not exist; which could be interpreted as license to not avoid side effects?!
> which could be interpreted as license to not avoid side effects?! Just because you can use a function with side effects doesn't mean you should. Pure functions are great because they are easier to reason about. Another benefit is that it's usually not too much work to change a cljs file to cljc and write code that works for jvm and js runtimes.
The semantics of atom is fundamentally about CAS which automatically retries for you. It's usually better to program against semantics rather than implementation details. For example, there are javascript runtimes with multi-threading, even if they are generally uncommon.
> The agent will execute only if the transaction commits.
I know this is true for refs, but I can't find any docs that say if this is true for atoms. I didn't think it was.
I personally discourage usage of agents. There are usually other, better options.
That is so awesome, @smith.adriane. Thanks! I’ll update the gist later when I have some more time.
I am most often vigilant against side effects, dunno why I dropped my guard this time.
In the https://state-of-clojurescript.com/ there was an answer "RPC" to the question "How do you sync data between client and server?" I'm curious to hear what libraries or techniques people are using to do RPC between client and server.
For me it’s JSON RPC or any variation of it, a single endpoint that takes a payload that includes a command, that’s it
Are you dispatching it manually on the server or using some library?
I mean dispatching to server side functions, or you handle everything in one receiver function?
It’s really simple, not a library, just dispatch by method name, a multimethod works.