2024 Aug 6 by Dustin Getz - https://twitter.com/dustingetz

The biggest remaining barrier to adopting Electric Clojure today is that it requires users to write too many macros. Some users have been willing to accept and set aside this issue while we deal with it but many cannot, they see writing macros as an insurmountable problem and that the technology is brittle or a hack.

So, today we are thrilled to finally say: Electric v3 is coming, and it fixes this! The root of the problem (that v3 solves) is what we call undesired transfer: when an Electric program implies a network topology that is different than the one that the programmer wants.

The easiest way to understand the new semantics is to understand what’s wrong with the old, so let’s start there.

Example of undesired transfer in Electric v2

Consider this (broken) Electric v2 code:

#?(:clj (def !conn (d/create-conn ...))) ; database on server

(e/defn RenderView [search db]
  (e/client
    (js/console.log "querying with filter: " search)
    (dom/div (dom/text (e/server (query-database search db))))))

(e/client
  (let [db (e/server (e/watch !conn))
        !search (atom "") search (e/watch !search)]
    (dom/input (dom/on! "keydown" (fn [e] (reset! !search (-> e .-target .-value)))))
    (RenderView. search db)
    #_(RenderView. search (e/server (e/watch !conn))
    )))

Undesired transfer in Electric v2 happens in two ways:

  1. let is too eager, transferring all values to the site of the let. This let is client-sited, so Electric attempts to transfer the result of (e/server (e/watch !conn)) to the client to be bound to the name db, and fails to serialize db in the process because it is an unserializable reference type.
  2. Electric v2 functions transfer all of their arguments to the call site before calling the function. For example, this call (RenderView. search (e/server (e/watch !conn))) is client-sited, therefore Electric attempts to first transfer the result of (e/watch !conn) (a database reference) to the client before booting RenderView with all client side args, and again the expr (being a database reference) will fail to serialize.

We can work around the issues by refactoring the program with these rules in mind:

(e/server
  (let [db (e/watch !conn)]
    (e/client 
      (let [!search (atom "") search (e/watch !search)]
        (dom/input (dom/on! "keydown" (fn [e] (reset! !search (-> e .-target .-value)))))
        (e/server (RenderView. search db))))))