Fork me on GitHub
#hyperfiddle
<
2023-08-01
>
frozenlock00:08:59

In the chat example, is there a particular reason why each msg is sent individually instead of using msgs directly in the client?

(e/server
          (e/for-by identity [msg (reverse msgs)] ; chat renders bottom up
            (e/client
              (dom/li (dom/style {:visibility (if (nil? msg)
                                                "hidden" "visible")})
                (dom/text msg)))))
Was it to show the interaction of nested client/server?

Dustin Getz01:08:57

i don’t understand the question?

frozenlock01:08:37

e/for-by is under server, but couldn't the whole thing be done on the client? Something like this:

(e/defn Chat []
  (e/client
    (try
      (dom/ul
        (e/for-by identity [msg (reverse msgs)] ; chat renders bottom up
                  (dom/li (dom/style {:visibility (if (nil? msg)
                                                    "hidden" "visible")})
                          (dom/text msg))))

      (dom/input
        (dom/props {:placeholder "Type a message"})
        (dom/on "keydown" (e/fn [e]
                            (when (= "Enter" (.-key e))
                              (when-some [v (empty->nil (.. e -target -value))]
                                (e/server (swap! !msgs #(cons v (take 9 %))))
                                (set! (.-value dom/node) ""))))))
      (catch Pending e
        (dom/style {:background-color "yellow"})))))
msgs is already defined on line 9. I don't quite get why this needs another server interaction for the for-by loop.

xificurC07:08:33

This is a common pattern. Since msgs is a server value you want to diff it on the server. e/for-by diffs and maintains the list incrementally. Doing the whole thing on the client will compile but you'll be re-sending the whole list of msgs on every change

👍 6
frozenlock11:08:11

I see. And will all the li elements be updated because their value changed, or will it only add the new msg and shift the others positions in the list?

xificurC11:08:36

good question. Updates are fine grained, so if a new value comes in it's the only one that's going to create

frozenlock11:08:48

So the sever will only send the info for the new msg and the old one that was discarded?

frozenlock00:08:25

For reference, I'm talking about this example: https://electric.hyperfiddle.net/user.demo-chat!Chat

joshcho02:08:28

I have code to hide the element until loaded like this:

(dom/div
  (try
    (dom/text (e/server (get-text)))
    (catch Pending e
      (dom/props {:class "hidden"}))))
I have two questions: 1. How do I instead show a different element until loaded, like a loading symbol? 2. I just want this to only wait for load only on mount. Is that possible? Essentially, the try/catch block would only exist when it mounts.

xificurC07:08:10

To mount A xor B you need a conditional, e.g.

(if-some [txt (try (e/server (get-text)) (catch Pending _))]
  (dom/div ...)
  (Spinner.))
If (get-text) returns Pending only on mount then you're done. Otherwise a quick&dirty way would be to wrap the above code with an atom to differentiate between first and subsequent Pendings
(let [!mounting (atom true)]
  ...)

Dustin Getz11:08:56

@U6D1FU8F2 i recommend not trying to battle with Pending if possible, just let the page stream in for now

👍 2
Dustin Getz11:08:57

I say this because there are some aspects of Pending that we are internally a bit confused about (e.g. does Pending always match what you'd want to be "Loading" or is Pending sometimes seen at times that you would not consider to be Loading – we aren't quite sure)

Dustin Getz11:08:08

If you need to do this it's fine, just stay in touch so we can understand your use case

❤️ 2
henrik14:08:38

I’ve run into cases where I’ve found it a bit hard to get things to happen in a desired order wrt rendering. For example, in a list, I’ve found that it’s hard to fix the race condition of items being added to the top of the list vs. scrolling to the currently highlighted item. If the added items pushes the highlighted item below the fold, scrolling to the highlighted item will pretty much never happen, since the data that triggers the scrolling event (i.e.: the list content changed) is available before the DOM elements are mounted on screen, rather than happening in the “same transaction” so to speak. With Pending, it seems that Pending -> Data available -> DOM is rendered happens in three discrete steps as well, so it’s hard to go directly from spinner to the desired DOM content without passing an undesired empty DOM state. This specific case I’m thinking of goes via for-by though, so maybe it’s related to the eagerness you mentioned previously @U09K620SG. I’m thinking it might be possible to fix both by sending back dom/node as a signal that everything is mounted and visible, but I haven’t tested yet. If it works, the second step will be to figure out how to do it so that it doesn’t result in a mess of spaghetti.

👀 2
Dustin Getz14:08:20

Is this demonstrated in the project repo you sent me?

henrik14:08:19

Not quite, I removed the code when I couldn’t get it to work. I’ll add it back and ping you.

Dustin Getz14:08:45

Please do, thanks. We are planning some changes here so I do not recommend you add hacks

👍 2
Dustin Getz14:08:08

and the e/for latency issue is involved

Dustin Getz14:08:52

transactional dom writes is being considered

henrik14:08:53

The DOM is very untransactional in its natural state. A lot of effort has gone into React to hide this fact, AFAIK.

Dustin Getz14:08:26

we understand

henrik15:08:25

Yeah, not to say that the solution that React delivers isn’t without its problems either. They chose to set the granularity at the component level, so it’ll provide some transactionality at the level of the component. The fact that Electric is more granular than that is a major strength in most cases, I would say.

Dustin Getz15:08:55

react is also limited by the reconcilier which makes assumptions

Dustin Getz15:08:29

i'm not the engineer who did the research so far so i dont want to comment too much, but we did do a discovery spike to see what other frameworks are doing

joshcho16:08:20

Thank you @U09K620SG. A lot of drive in this project makes me trust it more.

🙂 2
joshcho16:08:08

In terms of lists from e/watch, how does Electric handle rerendering? Suppose the watched variable goes from (a b c) to (a d b c e b). If there is a for loop over the variable, how does render/rerender happen?

Dustin Getz16:08:25

yes, search for e/for-by

👍 2
joshcho16:08:08

Really interesting. So a reactive database like https://github.com/mhuebert/re-db is not strictly necessary? Electric just takes care of the diffs in db watch naturally, and stops propagation when a higher node in the s-expr tree is equivalent.

joshcho16:08:33

I found myself being confused (then pleasantly surprised) when my console logs weren't being printed when I thought the "component" should re-render. It takes some time to get used to the fine-grained nature of things, and how things sort of "just work" at the compiler level.

joshcho16:08:48

But I guess re-db can be useful since with something like (e/server (some-query db)) will re-run every time !conn updates, whereas a reactive database can prevent that from happening. But at least upstream, this won't trigger an update if (some-query db) hasn't changed.

Dustin Getz19:08:22

Yeah, it's great to have reactivity/streaming inside the data layer, and if you do have a streaming data layer, Electric can speak that natively

Dustin Getz19:08:44

Unfortunately, knowing when to refresh your queries (for a data layer that is not streaming) is a hard problem, and it's not one that Electric solves out of the box

Dustin Getz19:08:50

Most databases, e.g. Postgres and Datomic, provide a notification API so that you can understand what changed and use that information to know which queries to recheck – for many apps a simple heuristic will do. We can also help you throttle various queries as not all of your app needs to be realtime – in a scaled situation likely only certain restricted components, like a chat component, would be realtime, and that would be backed by a message queue or document store that offers efficient subscription

❤️ 2
joshcho00:08:18

Thanks @U09K620SG, makes a lot of sense.

braai engineer07:08:42

Multiple Electric builds per project - possible? E.g. Shadow-cljs can have different builds defined for when you need a lightweight release for customers and a heavy build for admins / managers. Multiple "user.cljs"? Or do I need multiple Java runtimes for this?

Geoffrey Gaillard09:08:39

No need for multiple java runtimes. It should work exactly how you described it.

frozenlock11:08:58

There's some black magic happening in https://electric.hyperfiddle.net/user.tutorial-backpressure!Backpressure How does it work? (int s) is compiled into a flow? > • Electric is a Clojure to https://github.com/leonoel/missionary compiler; it compiles lifts each Clojure form into a Missionary continuous flow. >

xificurC11:08:59

yes, (int s) compiles roughly to (m/latest int s)

Dustin Getz13:08:26

That pseudo-transformation excludes the client/server network management

Dustin Getz13:08:45

the Electric compiler uses a real Clojure/Script analyser to transform clojure/script syntax into "DAG bytecode" for a "DAG virtual machine"

Dustin Getz13:08:23

Electric is basically the JVM but for network

Dustin Getz13:08:45

Don't be scared, this is why we waited 2.5 years to release anything publicly

Dustin Getz13:08:55

it is robust

👍 2
Garrett Hopper15:08:15

How similar would you say this is to the SSA/VM macro logic of core.logic or https://github.com/leonoel/cloroutine/blob/master/src/cloroutine/impl.cljc?

xificurC15:08:25

Leo wrote electric, missionary and cloroutine. With that said the electric VM is a VM of its own suited to solve the problems electric is meant to solve (a reactive network-transparent language)

Garrett Hopper15:08:01

That's good to know. I knew he wrote Missionary and Cloroutine, but I hadn't realized he was directly involved with Electric as well. 🙂

Dustin Getz15:08:48

It's not SSA but cloroutine does implement a clojure/script analyzer and electric does too so they are similar in that aspect

👍 2
Dustin Getz15:08:38

fwiw cloroutine is not really on the stack in Electric, cloroutine is used to implement the missionary m/ap and m/cp syntax sugar macros but those are not essential to the Electric compiler transform

2
frozenlock16:08:56

I weep for those not already familiar with Clojure and the cljs ecosystem. Learning all of this at once would be challenging to say the least. I've read the Missionary tutorial twice and it's still not sticking. 😒

Garrett Hopper16:08:44

FWIW, I don't think that new users in the future will need to know internal details like this. Writing "Electric code" is straightforward. It's just for those that need to be convinced of the correctness of the internals that need to dig into these details. 🙂

👍 2
frozenlock16:08:13

Hopefully. It's not yet clear to me yet what can be omitted.

Dustin Getz16:08:24

Missionary docs need dramatic improvement, that's not on you

Dustin Getz16:08:31

Leo is a walking computer science textbook

Dustin Getz16:08:03

Electric also needs streamlining, we will do another pass this fall

frozenlock11:08:29

Hmm... I have some pretty involved algorithm in a .cljc file. If I use electric, does it mean it will automagically become reactive? :thinking_face:

frozenlock11:08:13

Say it processes a coll of 1000 elements. Will it be able to re-process only the nth element if its incoming value changed?

xificurC11:08:59

e/for-by can. Note that dropping a heavy algorithm as-is into electric will degrade its performance. But if you use e/for-by in the right place(s) it will be incremental

frozenlock11:08:35

Very interesting

Dustin Getz12:08:25

by "heavy alg" we mean, say, code designed for linear execution, like a database query engine

Dustin Getz12:08:34

Compare to reactive/async execution which is IO bound

Dustin Getz12:08:56

use Clojure for linear code, use Electric for reactive code

frozenlock12:08:54

I'm transforming timeseries data with a bunch of transducers. Each transducer adds its own field to the the timeseries, potentially using values from a previous transducers. For example:

{:t 1, :v 100}
{:t 2, :v 150}

;; After transducers 'plus5'

{:t 1, :v 100, :plus5 105}
{:t 2, :v 150, :plus5 155}
Would you say it's possible/desirable to leverage reactions?

Dustin Getz13:08:40

I would use Missionary if your algorithm is concurrent

Dustin Getz13:08:53

Electric includes network management which is not free

Dustin Getz13:08:21

Processing timeseries data sounds compute bound not IO bound

joshcho16:08:07

@U09FL65DK Can you elaborate on e/for-by? What's the difference (rendering-wise) with e/for?

frozenlock16:08:31

My understanding is that for-by identifies the results with a 'key', similar to how it's done with components in Reagent. Assuming (for [x coll] ...), with current value of x being {:id 1 :msg "hello"} : In Reagent, we'd do something like this: ^{:key (:id x)} [:div (:msg x)]]

👍 4
Dustin Getz16:08:05

(e/for ...) is just (e/for-by identity ...)

Dustin Getz16:08:59

honestly we'll probably remove e/for , the only benefit is that the early tutorial code is slightly cuter (can delay the explanation)

joshcho16:08:35

That makes a lot of sense. In terms of internals, what happens if there are two elements with the same key?

leonoel07:08:51

> In terms of internals, what happens if there are two elements with the same key? The diff algorithm will choose the closest, according to position in sequence

frozenlock12:08:31

https://electric.hyperfiddle.net/user.demo-chat-extended!ChatExtended Line 50: do is used, but a non-last component appears in the final UI. ("Authenticated as") Does it mean that components are registered as a side-effect?

xificurC12:08:08

dom/div and friends mount a new DOM node as a side effect

✔️ 2
Dustin Getz12:08:47

this is what I mean by effectful rendering in the London talk

Dustin Getz12:08:17

the effects are supervised by a functional effect system which provides strong guarantees wrt effectful evaluation that imperative languages do not

Dustin Getz12:08:43

• cancellation • concurrency and backpressure combinators • composability / referential transparency – coordinate large scale effect fabrics • glitch-free event propagation • correct error handling by default (process supervision) • resource lifecycle (RAII) – strong resource cleanup guarantees

Dustin Getz12:08:58

See the talk for more discussion of this

frozenlock13:08:00

I already watched your talk, but a bunch of stuff flew over my head because I lacked the knowledge/context to appreciate it.

frozenlock13:08:15

I'll make sure to re-watch it once I'm more versed in the subject.

🙂 2
Dustin Getz13:08:49

Ah ok! TY for the feedback

frozenlock12:08:14

(defonce conn ; state survives reload
This is on the server side. I would expect the ns to be evaluated only once, yet the comment mentions something about surviving reloads.

xificurC12:08:22

electric code needs to stay in sync on the server and client side, which means the whole ns is often reloaded

✔️ 2
Dustin Getz12:08:24

electric uses a custom hot code reloading strategy: we hacked shadow compilation so that whenever a cljc namespace with electric code in it is recompiled, the server namespace is simultaneously reloaded

Dustin Getz12:08:54

this prevents spooky undefined behavior when a fresh client DAG connects to a stale server DAG

frozenlock12:08:59

Makes sense, it's just unexpected when coming from... well, the rest of the Clojure world. 😉

telekid12:08:02

Dustin, I would love to read a blog post similar to https://tonsky.me/blog/datascript-internals/.

👀 2
telekid22:08:52

Yeah, great doc

joshcho17:08:16

One feedback regarding the tutorial is that it will be nice if I can see the code and the writing at the same time. The writing references the elements of the code, but if I scroll down, I can't see the code. One possible (not great) solution is to have scrollable writing horizontally next to the code/result, but then the result window will be constrained horizontally. My viewport is a 1440p monitor. Also, it would be really neat if hovering over the symbols highlighted that which is in the source code. Is the tutorial itself hosted somewhere? It'd be nice to play around with it.

👀 2
Dustin Getz19:08:11

does two windows solve your problem?

👍 2
Dustin Getz19:08:13

Here is the tutorial repo, is that what you're looking for? https://github.com/hyperfiddle/electric-examples-app

👍 2
joshcho00:08:44

Yes, tyty.

Garrett Hopper17:08:41

Does Electric's reactivity model in CLJS depend on JS object reference equality? I have a function which gets run with an input JS array and returns the same array with some items added/removed. It looks like none of the downstream dependencies of this variable get re-run. (I'm guessing because of work-skipping assuming the JS array hasn't changed since it's the same object?) (It works as expected if I generate a new JS array instead, although that's not an option in my use case unfortunately.)

Garrett Hopper17:08:09

E.g.

(defn update-js-array [_ js-array]
  (.push js-array #js {})
  js-array)
(e/defn Example [input]
  (let [js-array (array)
        updated (update-js-array input js-array)]
    (js/console.log (count updated) updated input)))
Whenever the input changes, the (count updated) will always log 1, whereas the actual updated array will be logged which shows its actual size has changed.

Dustin Getz19:08:43

Yes, the reactive evaluation uses clojure.core/= to decide if it can skip work

Dustin Getz19:08:00

a dumb hack could be to wrap the returned array in a unique wrapper object

Garrett Hopper19:08:37

JS wrapper object you mean? Otherwise it seems like it'd be the same issue

Garrett Hopper19:08:17

Gotcha; I hadn't considered that. I only tried a CLJ wrapper. 🙂 Good enough for me without dropping down to some sort of missionary flow workaround 🤷

Dustin Getz19:08:45

you can also cachebust with a counter (assuming you control all reference writers)

Garrett Hopper19:08:00

That's kind of how I'm doing it now, though having to keep the JS array and its associated cachebuster together and used together everywhere isn't quite as clean as just wrapping in a new JS object.

Dustin Getz19:08:18

(defn ->Object "allocate a process-unique identity that cannot collide" []
  #?(:clj (Object.) :cljs (js/Object.)))

👀 4
Dustin Getz19:08:29

{(->Object) ref} i think would work, you just have to remember to pass around the composite and remember to unbox only from Clojure

frozenlock20:08:33

What happens when you use defn instead of e/defn ? The function won't be reactive?

Dustin Getz21:08:12

Yes you get Clojure semantics not Electric. Same if you use clojure.core/fn inside an Electric function – you get clojure semantics inside the clojure function

Dustin Getz21:08:40

electric functions can call clojure functions; clojure functions cannot call electric functions

Dustin Getz21:08:02

magic network only works in electric functions

frozenlock21:08:51

Oh I see, so I can't use e/server inside a defn ?

Garrett Hopper22:08:28

Yes, you can 🙂 (F. v) just desugars to (new F v); doesn't matter as long as F is bound to an Electric function

👍 2
frozenlock22:08:15

Great, thanks