clojurescript

pez 2025-01-30T23:09:06.458029Z

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?

phronmophobic 2025-01-30T23:35:19.857119Z

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.

pez 2025-01-30T23:38:32.596209Z

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…

phronmophobic 2025-01-30T23:40:23.824429Z

if you reevaluate the def, you need to also re-evaluate the functions that use the atom.

phronmophobic 2025-01-30T23:40:59.161069Z

typically, I would have a rich comment block to reset the atom. I don't think you would typically want to create new instances.

phronmophobic 2025-01-30T23:41:48.634099Z

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

pez 2025-01-30T23:41:52.844149Z

Totally agree. It is just that in the funny state things are evaluating the def makes the debouncer wake up.

phronmophobic 2025-01-30T23:43:18.982459Z

That sounds odd. Can you call js/setTimeout directly?

pez 2025-01-30T23:45:03.974509Z

It is odd like few odd things I have ever seen.

2025-01-30T23:45:47.476169Z

Why are there parens around @!debounced

❓ 1
2025-01-30T23:46:22.907249Z

in cancel-timeout

2025-01-30T23:46:38.585889Z

oh

pez 2025-01-30T23:46:58.442869Z

How do you mean calling js/setTimeout directly, @smith.adriane?

phronmophobic 2025-01-30T23:47:00.702929Z

I think the parens are correct. maps are functions of their keys.

phronmophobic 2025-01-30T23:48:13.306469Z

> 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?

pez 2025-01-30T23:48:22.609389Z

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.

phronmophobic 2025-01-30T23:49:47.812129Z

Do you have the diff? It might be easier to analyze the diff rather than the whole setup.

pez 2025-01-30T23:50:02.782389Z

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.

pez 2025-01-30T23:53:48.134789Z

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.

pez 2025-01-30T23:57:39.462409Z

I’ll push the whole thing and try to give some context.

pez 2025-01-31T09:28:50.069399Z

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! ❤️ 🙏

pez 2025-01-31T10:51:52.691009Z

Saving it as a gist here: https://gist.github.com/PEZ/785208524dca981cd6c238959ae74782

Vishal Gautam 2025-01-31T15:27:53.809099Z

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

pez 2025-01-31T15:35:26.338349Z

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!.

phronmophobic 2025-01-31T17:00:43.795919Z

@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.

2025-01-31T17:03:07.858829Z

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?!

phronmophobic 2025-01-31T17:07:25.762389Z

> 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.

phronmophobic 2025-01-31T17:09:31.929679Z

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.

phronmophobic 2025-01-31T17:14:54.978009Z

> 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.

phronmophobic 2025-01-31T17:19:36.836179Z

I personally discourage usage of agents. There are usually other, better options.

pez 2025-01-31T17:20:10.294619Z

That is so awesome, @smith.adriane. Thanks! I’ll update the gist later when I have some more time.

pez 2025-01-31T17:21:19.449849Z

I am most often vigilant against side effects, dunno why I dropped my guard this time.

Chris McCormick 2025-01-30T05:43:48.027649Z

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.

Roman Liutikov 2025-01-30T06:46:15.818109Z

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

👍 1
Chris McCormick 2025-01-30T07:17:34.788949Z

Are you dispatching it manually on the server or using some library?

Chris McCormick 2025-01-30T07:17:56.156779Z

I mean dispatching to server side functions, or you handle everything in one receiver function?

Roman Liutikov 2025-01-30T07:25:21.969449Z

It’s really simple, not a library, just dispatch by method name, a multimethod works.

👍 1