Fork me on GitHub
#hyperfiddle
<
2024-03-14
>
simonkatz11:03:38

My app has a glitch. I have a list of data items, a search string input and a search status display (blank input / found / not found). The search status has a background color to indicate the state, and it briefly but noticeably flashes a wrong color when the user changes the search string from empty to “1” (for example). I have a minimal repro — see details in my in-thread reply.

simonkatz11:03:06

If the input is an integer between 1 and 100, that’s regarded as :found. An empty input is :blank, and the other alternative is :not-found. When the user changes the input from an empty string to “1”, I get the following output (with an unwanted :not-found that is causing my glitch):

---------
    :not-found "1"
    search-str = "1"
    :found "1"

simonkatz11:03:58

I can make the glitch go away if I do any of the following: • move the body of FoundStatus to the call site • wrap the call of FoundStatus in (e/server ...) • remove the (e/client ...) wrapping of the call of FoundStatus.

simonkatz11:03:11

In my actual app I haven’t found a simple workaround that works, and in any case there’s the possibility that when I refactor I would re-introduce the glitch.

simonkatz11:03:33

I guess this is a bug, but is there something I can do to work around this? Maybe a way of saying to only pass on a reactive value if it’s had the same value for a certain amount of time?

Dustin Getz12:03:42

I think this is what we call the "when true" bug, I will need to look more closely to be certain

thanks3 1
simonkatz16:03:29

I said: > In my actual app I haven’t found a simple workaround that works FWIW, that’s not true. I’ve wrapped the call of FoundStatus in (e/server ...) and all is good.

Dustin Getz16:03:43

one trick that often helps, is this idiom: (case found? (FoundStatus. search-str found?)) – the case forces found? to resolve (not be Pending) before evaluating the body. I'll need to dig into our issue tracker to remember the exact details of the bug

Dustin Getz16:03:14

to be clear, if the bug is what i think it is, it should just be fixed, and i dont know if the (case x ...) pattern has real world use once Electric is perfected. This particular bug won't be fixed until Electric v3, it was a deep issue

simonkatz16:03:37

Thanks for the suggestion and additional details. I’ve tried (case found? (FoundStatus. search-str found?)), but it makes no difference. 😞

👀 1
awb9914:03:48

I think hyperfiddle could benefit by having an options-ui. The idea by options-ui is to declarative specify which options should be collected from the user and which choices are allowed.

Dustin Getz15:03:25

this sounds a bit like HFQL, which is a powerful hypermedia DSL described on the new website - https://www.hyperfiddle.net/

1
Dustin Getz15:03:31

in fact the reason we built Electric was to make HFQL possible

awb9917:03:52

Is there a repo for this?

Dustin Getz17:03:10

not public yet

awb9919:03:31

I am interested to see it!

awb9914:03:05

The benfit of that is that one can have different ui elements (textbox; combobox; date-picker; button; user defined elements) and so this options then can be used to make requests to clj or just to render ui differently.

awb9914:03:44

Another idea how electric could become easier to use is to bring in Tlthe concept of extensions. An extension is something that adds features to electric without having to do any configuration. The idea is that once you add an extension to the classpath by having it included as a dependency then it will automatically add it to the cljs build. This makes it a lot easier to reuse frontend ui components. An example is here: https://github.com/pink-gorilla/ui-highcharts/blob/master/resources/ext/ui-highcharts.edn

👀 1
awb9914:03:49

So having a resource that matches #"ext/.*/.edn" it woudl read the content: {:name "ui-highcharts" :cljs-namespace [ui.highcharts]} and in this case it would add ui.highcharts to the shadow build.

Kurt Harriger17:03:25

does anyone have css hot reloading working?

Dustin Getz17:03:51

.css artifacts hot reload via shadow which i believe works out of the box in the starter app, what are the specifics of your setup?

Kurt Harriger17:03:59

I was just trying to throw css into the index page and see if it would hot reload but doesn’t seem to be. I just forked the starter app and tried similarly and its not working.. I’ll push a branch really qucik

mgxm17:03:37

try to change :http-root to :watch-dir

Dustin Getz17:03:51

you're right, the starter app may be broken, i'll log a ticket

Kurt Harriger17:03:52

starter app didn’t include any css so not really broken but an example would help

👍 1
Kurt Harriger17:03:06

changing to watch-dir seems to have fixed it

Kurt Harriger17:03:21

I’ll send PR

Andrew Wilcox05:03:21

Thank you for this!

chaos18:03:37

Hi, in the below program, there is a reactive input that starts at directory c:/tmp/ which has 5 files as returned from the server. There are two println forms that print the current dir and cnt of files to both server and client respectively the both print c:/tmp/ and 5 count of files at startup as expected (the c:/tmp/ directory has 5 files in it). When the user appends the letter e to the input field (i.e. c:/tmp/e ) the server looks at that c:/tmp/e directory and returns 0 files (the directory is empty). So the expected output of the program when this happens should be c:/tmp/e and 0 files for both server and client, though on the client side I'm getting a print out of c:/tmp/e dir and 5 files first, and then the expected c:/tmp/e and 0 files. I was expecting the client println form to only fire once with c:/tmp/e and 0.Why is that so?

(ns electric-starter-app.main
  (:require [hyperfiddle.electric :as e]
            [hyperfiddle.electric-dom2 :as dom]
            [hyperfiddle.electric-ui4 :as ui]))

;; Saving this file will automatically recompile and update in your browser

#?(:clj
   (defn dir-list [dir]
     (.listFiles ( dir))))

(e/defn Main [ring-request]
  (e/client
    (binding [dom/node js/document.body]
      (let [!dir (atom "c:/tmp/")
            dir (e/watch !dir)]
      (ui/input dir (e/fn [v] (reset! !dir v))
                (dom/props {:type "search"}))
      (e/server
       (let [files (dir-list dir)
             cnt (count files)]
         (println :dir dir :files cnt)
         (e/client
          (println :dir dir :files cnt))))))))

;; server output
;; :dir c:/tmp/ :files 5
;; :dir c:/tmp/e :files 0

;; client output
;; shadow-cljs: #3 ready! 
;; :dir c:/tmp/ :files 5
;; :dir c:/tmp/e :files 5    ;; didn't expect this
;; :dir c:/tmp/e :files 0
more details at https://github.com/hyperfiddle/electric/issues/68

Dustin Getz18:03:52

What seems to be happening here is you expect the dir watch to be transactional with the cnt derived value

Dustin Getz18:03:54

the dir watch is on the client, and cnt is on the server, so we're seeing latency, but you reasonably expect (println :dir dir :files cnt) to only run after all parameters are available

chaos18:03:20

right, I was expecting the same semantics as if I was reading a plain clojure without the distiction of client/server (apart from the atom reactiveness)

chaos19:03:22

or put it perhaps differently, I was expecting the value of dir and cnt to be synchronized, given that cnt is dependent of dir

Dustin Getz19:03:56

I need to confirm with the team (Electric v3 is sharpening a bunch of stuff). Broadly the consistency model is such that client-internal propagations result in consistent states as guaranteed by missionary, and same for server-internal propagations, but for distributed propagations there are some tradeoffs, but it does IIUC seem like the Pending cnt and the dir should be known simultaneously and therefore the println should not ever see that inconsistent state

Dustin Getz19:03:19

Let us discuss internally and get back to you

👍 1
henrik10:03:37

See what would happen if you enforce a synchonization point before the print:

#?(:clj (defn search
          [dir]
          (let [files (dir-list dir)]
            {:dir  dir
             :files files
             :cnt   (count files)})))


(e/defn Main [ring-request]
  (e/client
    (binding [dom/node js/document.body]
      (let [!dir (atom "c:/tmp/")
            dir  (e/watch !dir)]
        (ui/input dir (e/fn [v] (reset! !dir v))
          (dom/props {:type "search"}))
        (e/server
          (let [{:keys [dir cnt]} (search dir)]
            (println :dir dir :files cnt)
            (e/client
              (println :dir dir :files cnt))))))))
In this case, I think you should see consistent output, since the Clojure function would ensure that the count changes as the dir changes, rather than independently of each other.

Dustin Getz16:03:07

@U012BL6D9GE I spoke with the team. We believe this is exactly a known and previously resolved issue we call the "distributed glitch", which is: when cnt should be pending because it is derived from dir via the network. Electric v2 already accounts for this circumstance and therefore the behavior you observe is unexpected and we have logged it as a bug.

chaos20:03:44

Thanks @U09K620SG for looking into this, and I can confirm @U06B8J0AJ workaround should also work (I've tried something similar in my code at the time). Just trying to set my expectations on the bindings resolution across boundaries, because it caught me by surprise. In a even more simplified example below, I was similarly expecting the program to be read from top to bottom like a "normal" clojure program, with electric taking care of efficiently transporting the values under the hood as need. If a reactive change happens at the higher form, the flow continues synchronously to the inner forms where the variables are affected. The program starts by sleeping for 100 ms amount of time on the server, and then reports on it from both sides. When the button is pressed, the sleep amount is increased by 1, and the reactive process is triggered once more. There's an extra reporting on the client, that says that ms is 101 but the server-msg is slept for 100 ms, which breaks my expectations. If I am not to reason about the program as one that the flow of information is sequential and bindings dependencies are maintained in the flow across the boundaries, how should I expect the reactive update part to behave? Am I responsible forcing synchronization of the bindings across boundaries? I would have thought electric has all the information required to do so in the graph but it might as well be a hard problem to solve automatically, given the nature of the setup with n-clients communicating with a single server.

(ns electric-starter-app.main
  (:require [hyperfiddle.electric :as e]
            [hyperfiddle.electric-dom2 :as dom]
            [hyperfiddle.electric-ui4 :as ui]))

;; Saving this file will automatically recompile and update in your browser

#?(:clj
   (defn sleep [ms]
     (Thread/sleep ms)
     [:slept-for-ms ms]))

(e/defn Main [ring-request]
  (e/client
    (binding [dom/node js/document.body]
      (let [!ms (atom 100)
            ms (e/watch !ms)]
        (ui/button (e/fn []
                     (println :client :sleep-increase)
                     (swap! !ms inc))
                   (dom/text "inc sleep"))
        (println :client :sleeping-for ms)
      (e/server
       (let [server-msg (sleep ms)]
         (println :server :sleep-for-ms ms :server-msg server-msg)
         (e/client
          (println :client :sleep-for-ms ms :server-msg server-msg))))))))

;; client output
;; => Connecting... 
;; => shadow-cljs: #7 ready!
;; => Connected. 
;; => :client :sleeping-for 100 
;; => :client :sleep-for-ms 100 :server-msg [:slept-for-ms 100]
;; button pressed
;; => :client :sleep-increase 
;; => :client :sleep-for-ms 101 :server-msg [:slept-for-ms 100]  ;; I wasn't expecting this to trigger
;; => :client :sleeping-for 101 
;; => :client :sleep-for-ms 101 :server-msg [:slept-for-ms 101]

;; server output
;; => :server :sleep-for-ms 100 :server-msg [:slept-for-ms 100]
;; button pressed
;; => :server :sleep-for-ms 101 :server-msg [:slept-for-ms 101]

Dustin Getz20:03:23

I don't understand your question, that is a lot to unpack

Dustin Getz20:03:30

Electric maximizes concurrency, it is not sequential/imperative evaluation

Dustin Getz20:03:51

the line you commented looks like the same glitch as your original report, it is a bug

Dustin Getz20:03:46

the sleep function is also wrong because it is synchronous blocking, it needs to be an async missionary sleep

chaos20:03:52

Yeah, sorry not being clear. I'm just trying to understand how to reason about the program. My expectation was that I can read the program the "same" as a clojure program, and that is sequentially, in which case that line that I mentioned above shouldn't have happened. If this is also due to the "glitch" then I am covered, the program can be reasoned by me as if it was sequeantal then and this is how I expect it to run. I thought the "glitch" described earlier, was when trying to send two bindings across from the server, in this case is only one.

chaos20:03:15

The use of sleep is just for demonstration purposes, I just wanted to have a a server funtion that will take "some" time to complete (i.e. doing I/O), and sleep was the easiest to use to make my point

chaos20:03:19

what I mean by sequental evaluation in the presence of reactivity is how the various forms are triggered. So when the program starts, I expect the following "sequential" evaluation of bindings: !ms->ms->(println :client :sleeping-for ms)->(sleep ms)->server-msg->server println->client println, which does happen I would have expected the same flow to happen after the button is pressed, but I am getting the extra print out. And thus the question. Though now you have said that this is the same bug as before, I'm covered.

chaos20:03:36

Btw, is there a way for me to track the issue, maybe as a github issue, or shall I periodically look at master checkins?

Dustin Getz20:03:52

That sounds right, the evaluation order should exactly match the dataflow DAG, effects run when one or more inputs change and you should never see inconsistent states

🙌 1
Dustin Getz20:03:18

With respect to the issue, we've added it to our internal tracker and will look when we have time

👍 1