Fork me on GitHub
#hyperfiddle
<
2023-03-16
>
J10:03:54

Hi guys! There is a reason to not pass the event here https://github.com/hyperfiddle/electric/blob/master/src/hyperfiddle/electric_ui4.cljc#L99? Because when a button is inside a form sometimes we want to preventDefault the event.

Geoffrey Gaillard10:03:08

This is a good question. We’ll discuss it. In the meantime, <button role="button">…</button> will not submit your form on click (default role for a form button is submit).

Dustin Getz12:03:03

The reasoning today (which can change) is: ui/input is a high level input that gives you the value not the event. ui/button is supposed to also be high level like that. Interacting with a dom/form would be a lower level concern in this worldview.

👍 2
markaddleman15:03:20

I’m curious about incorporating electric into a serverless architecture. I think the big question is how to externalize the server-side memoize caches to something like Redis? Of course, there are other issues like being at to associate a websocket with a stable client id but these are outside electric’s scope and have googleable solutions.

2
Dustin Getz15:03:16

You want server state to be durable across restarts? Memo state is stored in Missionary, so we'd likely move the missionary heap into something durable as you suggest or are you thinking about something else? Today the server runtime state can theoretically be reconstructed from the client runtime state, so this may not actually be an issue. We POC'ed this already, there's a blocker, it will land after the next missionary design lands

markaddleman15:03:13

The first one: I am thinking of durable server-side state across restarts. Separately, I have a question related to recreating server-side state from the client

markaddleman15:03:24

Unrelated to servlerless electric, I’m curious about the performance issues around a single client-side atom for its database and a server-side watch on that atom. Will electric stream all updates to the client-side atom? Or only those changes that server cares about? For example, suppose the atom contains

{:client/cursor-location "blah blah", :shared/key "key"}
The server contains functions like (get db :shared/key) but never reference :client/cursor-location

xificurC15:03:39

a server cannot watch a client atom

markaddleman15:03:05

oh, well that answers that 🙂

Dustin Getz15:03:15

only scope (e.g. lexical/dynamic/global/local) is streamed, as reified in the DAG

markaddleman15:03:08

ah, i think i got it. so, if the server could watch a client atom, then update to :client/cursor-location in the atom would trigger a recalc but since none of the server’s scope is changed, no application code would be executed

Dustin Getz15:03:27

any change to an atom will fire watches (this is how clojure atom watches work - see core/add-watch and core/remove-watch). what happens next depends on what your cursor does. From electric's perspective, at any point if a reactive value is the same as it was before, we skip the pointless recomputation and use the memoized value. So any work downstream of that would be skipped.

Dustin Getz15:03:37

does that answer your question?

markaddleman15:03:44

Now I’m curious why the server cannot watch a client-side atom.

Dustin Getz15:03:48

you can't move/serialize references

markaddleman15:03:39

I don’t follow but I think I should re-ground myself in the code before continuing my fanciful architecture ideas 🙂

Dustin Getz15:03:44

you can do this

#?(:cljs (def !x (atom {:deep {:thing 1}})))
(e/server 
  (let [x (e/client (e/watch !x))]
    (println x))) ; entire value was transferred

Dustin Getz15:03:18

but !x cannot transfer because it can't be serialized, and e/watch needs to attach directly to the reference, so it needs to be co-located

markaddleman15:03:42

I understand that part. But suppose the code was this:

#?(:cljs (def !x (atom {:client-only {:thing 1}
                        :shared {:something "blah"}})))
(e/server 
  (let [{shared :shared} (e/client (e/watch !x))]
    (println shared))) 

Dustin Getz15:03:21

e/watch returns a reactive value so that’s allowed

markaddleman15:03:24

If I understand correctly, if the client executes (swap! !x update :client-only f)then the bytes will flow across the websocket to the server and electric/missionary would run enough to determine that the server state hasn’t changed

Dustin Getz15:03:53

we aren’t that smart, the destructuring here happens on the server

markaddleman15:03:20

So, there is an opportunity for optimization but it is not currently implemented

markaddleman15:03:14

I imagine a poor man’s solution might be

#?(:cljs (def !x (atom {:client-only {:thing 1}
                        :shared {:something "blah"}})))
(e/server 
  (let [shared (e/client (e/watch-in !x [:shared]))]
    (println shared))) 

markaddleman15:03:42

(btw, I’m not criticizing, I’m just trying to understand the current state of the world)

Dustin Getz15:03:03

you should move the destructure to the client and stream the leaf

markaddleman15:03:21

ah, that makes sense

Dustin Getz15:03:27

sure we could optimize let, haven’t thought about it much

markaddleman15:03:20

I don’t have a strong opinion on it. The fact that I can move the destructure to the client gives me exactly what I was looking for

👍 2
Yab Mas15:03:58

I could use some feedback on the following. I'm trying to setup a project that uses js-modules and woud like to abstract the mounting of the program so I can call this from the init-functions of the modules with their root-component. I now have:

(defonce ^:private reactor nil)

(defonce ^:private current-app nil)

(defn ^:private mount-current-app []
  (set! reactor (current-app
                  #(js/console.log "Reactor success:" %)
                  #(js/console.error "Reactor failure:" %))))

(defn initialize-app [app]
  (set! current-app app)
  (mount-current-app))

(defn ^:dev/after-load start! []
  (assert (nil? reactor) "reactor already running")
  (mount-current-app))

(defn ^:dev/before-load stop! []
  (when reactor (reactor))                                  
  (set! reactor nil))
and then for each module a file with something like:
(defn ^:export init! []
  (current-app/initialize-app
    (e/boot
      (binding [dom/node js/document.body]
        (hello-world/HelloWorld.)))))
They're all cljs files. This seems to work fine at first. But I noticed hot-reload is now broken. As in, it still reloads, I see the stop! and start! logs but it mounts the old-program. I'm also wondering if it's possible to abstract this last bit of duplication as well, the call to e/boot, so I can just pass the root-component. I tried, but the results of e/defn seem to be nil when passed around like that in normal cljs, not sure if that suprises me or not, still trying to get my head around the basics. Any hints appreciated 🙏

2
Dustin Getz15:03:32

try lifting out the e/boot to a def

Yab Mas15:03:44

Doesn't change anything... So still works, but doesn't properly update on hot-reload

Yab Mas15:03:55

(def root
  (e/boot
    (binding [dom/node js/document.body]
      (hello-world/HelloWorld.))))

(defn ^:export init! []
  (current-app/initialize-app root))

Yab Mas15:03:37

Thought you might have meant e/def but that just causes an error

xificurC20:03:19

the fact that it mounts the old program suggest your changes might not be getting picked up for some reason

Yab Mas07:03:24

Yes, I've got ^:dev/always. Actually on both the current-app ns and the init ns as I wasn't sure what was necessary . I just now tried removing it from either one of them, but it all gives the same result. I also see in my browser logs that they all got reloaded. That is: hello-world, current-app and init. One thing that stands out is that the order is: hello-world > current-app > init, while I would expect hello-world > init > current-app. Im not sure if the printing order is guaranteed to be the reloading order, but this seems to be consistent on all reloads. If this had anything to do with the observed behaviour I would actually expect the old update to show up when I trigger another reload with a new change, but it doesnt... it just always loads the first version. Complete refresh obviously updates to the latest version.

Yab Mas08:03:55

I'm also still very interested in the second part of my question. Not so much because I necessarily need to pass electric-components around in cljs file, Im fine with having it refactored to this point, Im just trying to understand what is/isnt possible and why. In my cljs-repl eval of HelloWorld gives nil, so thats inline with the errors I get when Im trying to pass it around as a value. Macroexpanding also result in nil. Eval of (e/boot ...) gives me an object, so it makes sense Im able to pas that around in cljs-land. Macroexpanding it gives... well, something that doesnt really help my understanding 🙂 I guess its cljs side of the reactive-runtime. Not sure how to integrate my findings into actual understanding.

xificurC08:03:13

what do you mean by "eval HelloWorld"?

xificurC08:03:23

could you provide a repo with repro steps to further investigate your issues?

Yab Mas08:03:21

sure, I'll clean up a bit and sent you the repo with some instructions, thanks!

👍 2
Yab Mas08:03:57

I just meant evaling the var and checking the result in the repl. Another way to put it (def x hello-world/HelloWorld) whats the value of x in a cljs file, nil apparently

xificurC09:03:13

ok, understand. The reason for that is e/def doesn't actually do anything 🙂 It stores the code as metadata on the var. The electric analyzer will look at that metadata and compile your program

Yab Mas09:03:31

I see. When I eval (meta #'hello-world/HelloWorld) I find your meta-data in the result. I'll have to think about the implication for a bit, but this definitely gives me a better understanding, thanks

Yab Mas10:03:23

https://github.com/avisi-apps/tech-testing-ground/tree/master/prototypes/electric. Start repl, function to start shadow-watch is in dev > user. Run mount/start in src > server > server. Locahost:3000 now shows hello world. Changing something in src > components > hello-world and saving triggers reload but doesnt update whats shown in the browser.

Geoffrey Gaillard15:03:38

Thank you for the repo. It helps a lot. We are swamped today, sorry, we will look into it first thing on Monday morning 🤞

👍 2
Geoffrey Gaillard09:03:33

I understand you want multiple js modules with one electric program per module. Shadow's :init-fn runs once when the module is loaded, but not during hot code reload. As a result, the init! functions in main-page and current-app won't be called on hot code reload. You can either: • invert the dependency between current-app and main-page or item-view • add ^:dev/after-load to one of your init! functions.

Yab Mas12:03:22

Ok, makes sense. I think it's actually connected to the second part of my initial post. I tried to copy this structure from the way we do it in our fulcro apps, but there I'm able to pass the component as an arg to inititialize-app and changes to it get picked-up on hot-reload. As I'm unable to pass electric-components around like that and have to boot them first I got this situation. After learning last friday that for electric-components it's about the var/meta-data and not the value, I tried sending it as a symbol with the intent of finding the corresponding var from wihtin the other ns but this doesnt seem possible in cljs (I might be wrong about that). I guess it should be possible to extract the relevant meta-data, pass that as an arg and than reconstruct a var in the other ns which can be passed to e/boot, but it feels a bit contrived so havn't tried (and I now actually think it wouldnt solve the reload problem either). As for the reload-problem itself. The first option probably works but breaks the code structure, so I went with the second and it works fine, thanks for looking into it.

J17:03:27

Hi! Is this code valid:

(dom/on "click" (e/fn [e]
                  (.preventDefault e)
                  (try
                    (e/server
                      (e/offload
                        #(let [foo "foo"]
                          (prn "BAZ")
                          (Thread/sleep 10000)
                          (prn "FOO")
                          foo)))
                     (catch Pending _
                       (swap! !state assoc :status :submited)
                       (e/on-unmount #(swap! !state assoc :status :idle))))
I never see the FOO print. Like if the Thread/sleep was ignored. I don’t understand why.

2
Dustin Getz17:03:19

hmmm I dont know, and can reproduce

Dustin Getz17:03:22

still thinking

Dustin Getz17:03:43

In case you're stuck, just don't silence the Pending exception inside the callback and it will work

Dustin Getz17:03:35

dom/on is listening for Pending to know when the callback has successfully run on the server

J17:03:05

What do you mean by don't silence the Pending exception?

Dustin Getz17:03:20

you're catching it (i.e. not rethrowing it)

Dustin Getz19:03:56

You can use this pattern to hook the loading state

Dustin Getz19:03:52

(case (e/server ...) X) is the interesting part, the case here only runs the body X when the pending state is resolved

Dustin Getz19:03:07

Does that make sense?

Dustin Getz19:03:57

We are still thinking about this usage of case and how to do this better

xificurC19:03:03

in this case you could also just use e/on-unmount in the body

J10:03:19

hummm it’s an unusal case ^^. The try/catch is more explicit.

xificurC10:03:34

in this case the try/catch is not needed at all. When the event handler finishes it is unmounted, so you can use on-unmount

J10:03:30

Ooh ok I see!

Can17:03:14

hello! How can I move that button to left button? I checked some CSS tutorials but everything didn't work that's what I tried until now.

xificurC19:03:21

can you share your code?

Can20:03:59

of course

Can20:03:01

(e/defn SliderApp []
        (e/client
          (let [!state (atom {:in "" :v 0 :v-state 0 :placeholder "Write a number please..."})]
            (let [in (get (e/watch !state) :in) v (get (e/watch !state) :v)]
              (dom/div (dom/props {:style {:display     :grid
                                           :width       "40em"
                                           :grid-gap    "0.5em"
                                           :align-items :center}})
                       (dom/h1 (dom/text "Slider  Example")
                               (dom/props {:style {:grid-row 1 :align-items :center
                                                   }}))

                       (ui4/range v (e/fn [newv] ((swap! !state assoc :v newv)))
                                  (dom/props {:min 0, :max 100, :style {:grid-row 2}}))


                       (ui4/input in (e/fn [v] (swap! !state assoc :in v))
                                  (dom/props {:placeholder (get (e/watch !state) :placeholder)
                                              :style       {:background-color (get (e/watch !state) :bg-color2)
                                                            :width            "47em" :height "1em" :align-items :center :grid-row 3}})
                                  (dom/on "keydown" (e/fn [enter]
                                                          (when (= "Enter" (.-key enter))
                                                            (when-some [givenValue (contrib.str/empty->nil (-> enter .-target .-value))]
                                                              (swap! !state assoc :v givenValue)
                                                              (set! (.-value dom/node)
                                                                    )))))
                                  (dom/on "keyup" (e/fn [keyup]
                                                        (when-some [givenValue (contrib.str/empty->nil (-> keyup .-target .-value))]
                                                          (swap! !state assoc :v-state givenValue)
                                                          )))
                                  )

                       (dom/button (dom/on "click" (e/fn [click] (swap! !state assoc :v (get (e/watch !state) :v-state))))
                                   (dom/text "Insert Num!!!")
                                   (dom/props {:style {:grid-row 4 :width "15em" :height "2em"
                                                       :grid-gap "10em" :align-items :auto
                                                       }})
                                   )

                       (dom/button (dom/on "click" (e/fn [click] (swap! !state assoc :v "")
                                                         (swap! !state assoc :in "")))
                                   (dom/text "Reset!!!")
                                   (dom/props {:style {:grid-row 4 :width "15em" :height "2em"
                                                       :grid-gap "1em" :align-items :auto
                                                       }})
                                   )

                       (dom/h1 (dom/p (dom/text "result is: " v))
                               (dom/props {:style {:grid-row 5 :align-items :center
                                                   }}))
                       )
              )
            )
          )
        )

xificurC08:03:36

your issue probably stems from bad CSS / grid usage

xificurC09:03:13

from some quick fiddling it seems you putting 2 items on a row creates 2 grid columns. I don't think grids give you much here, but the minimal change to put the buttons together is to wrap them in a div

denik18:03:10

Getting this often when typing fast using a keydown listener (dom/on "keydown" ….)

2
Dustin Getz19:03:46

this is harmless, we're dealing with it

👍 2