Fork me on GitHub
#hyperfiddle
<
2023-02-19
>
Jungwoo Kim14:02:07

Q) What does prefix ! mean in some examples…? Here’s one of the examples.

(defonce !state #?(:clj (atom (list)) :cljs nil))
https://github.com/hyperfiddle/electric/blob/master/src-docs/user/demo_4_chat.cljc

Jungwoo Kim14:02:56

What I mean is it has specific meaning like idiomatic clojure styles? something like this? https://clojure.org/guides/weird_characters#_symbol_unsafe_operations

xificurC14:02:21

A convention marking stateful variables

🙏 3
Jungwoo Kim14:02:19

Thanks! I’m familiar with (def ^:dynamic *state* …) though.

Dustin Getz14:02:16

In Electric, a common pattern is needing to separate a reference from the reactive value derived from the reference: https://github.com/hyperfiddle/electric/blob/eaa9f61b5856ff7b1317e6e1a85bd0181dc2d513/src-docs/user/demo_3_system_properties.cljc#L21

Dustin Getz14:02:52

so you need distinct names for the two aspects of the state. The !x is a stable value - the identity needed to alter the state, and x is the ever-changing value of the state itself

👍 7
Dustin Getz14:02:52

If you try to combine them, it causes reactive code that depends only on !x but not x to recompute more often than necessary, sometimes far more often

Dustin Getz14:02:25

Wrt earmuff notation, Clojure does something really unfortunate: (e/def *x*) results in Warning: *x* not declared dynamic and thus is not dynamically rebindable, but its name suggests otherwise. Please either indicate ^:dynamic *x* or change the name.

Dustin Getz14:02:58

so we can't use the earmuff notation really for anything; it's a shame

😂 2
Dustin Getz14:02:31

You might ask, why not just pass around !x and call (p/watch !x) locally? That causes problems, it introduces "FRP glitches" (observable inconsistent states), because it results in multiple subscriptions to the same reference and clojure updates the subscriptions one at a time, causing your program to see a series of glitched states that eventually converge

👍 6
Jungwoo Kim14:02:06

Thank you for your detailed explanation. interesting. I’m getting more understands of states.

🙂 2
Dustin Getz14:02:25

Any of us are happy to do a video call if you would like, we can accelerate your learning curve, would you like to do one?

Jungwoo Kim23:02:10

Cool! I’d like it!

👀 2
Dustin Getz15:02:06

@U02SKL96W1J great - I will DM you in the next few days about doing the video call, i see you are in Asia TZ

🙏 2
JAtkins14:02:20

Morning 👋 I'm missing some subtlety right now. I've got a simple, but it's failing. Any time I click a button, I get this error in the js console. I'm not seeing any obvious difference between this example and the demo-2-toggle example though, so I'm not exactly sure what to look at for incorrect code.

👀 2
JAtkins14:02:37

ha, I've always wondered if slack supported syntax highlighting. TIL

xificurC14:02:04

You mixed ui4 with old ui code

xificurC14:02:34

(ui/button (e/fn [] ..))

Dustin Getz14:02:42

::ui/click-event is not a thing anymore

JAtkins14:02:57

ah. I think I copied from a dom/button example I found

Dustin Getz14:02:03

where, i will delete it

JAtkins14:02:55

there are a few. The one I saw was demo_bubbles.cljc

Dustin Getz14:02:20

oh, scratch folder is a lot of old stuff

Dustin Getz14:02:28

we'll need to do some surgery on the repo

Dustin Getz14:02:49

sorry for the confusion

xificurC14:02:29

Undefined continuous flow happens when something that is not an electric function is called as such. In this case ui/button called a map

JAtkins14:02:24

So, new ~V! is the culprit sorta? (electric-ui4/button)

2
JAtkins14:02:29

Will (e/fn [] (e/server (new Object))) cause trouble? (trying now)

JAtkins15:02:01

Sorry, I just worked out the confusion. I thought to get that error it was looking at (new {::ui/on-click ...}) and complaining specifically that the map was not an electric fn. Not the case, it's just complaining that new is not called on an object. I was overthinking the problem

Dustin Getz15:02:19

oh, the confusion may be what new means here

Dustin Getz15:02:35

Electric uses new to call electric functions

Dustin Getz15:02:55

We also capitalize them (like react components)

Dustin Getz15:02:12

The syntax sugar works –

(e/defn Teeshirt-orders-view [x] ...)
(Teeshirt-orders-view. x)
(new Teeshirt-orders-view x)

Dustin Getz15:02:21

does that resolve the confusion?

JAtkins15:02:45

Yep, I was just reading the error wrong

JAtkins15:02:50

thankfully 🙂

🙂 2
xificurC15:02:08

I think you had it right, JAtkins, ui/button's first argument should be an electric function and internally it called new on your map

J15:02:13

Hi guys! Does`e/defn` support multi arity?

Dustin Getz15:02:22

not yet sorry

J15:02:36

No problem ^^

JAtkins15:02:53

Another question - whenever I click the button it sets off an infinite loop and calls change-op! repeatedly. But, if I change to no args for change-op! , the loop is avoided. Here's the modification to make it work:

(defn change-op!
  [#_curr-op]
  (let [curr-op (::op @!state)]
    (println "updating-op - curr-op" curr-op)
    (swap! !state assoc ::op (-> ops (set) (disj curr-op) (seq) (rand-nth)))))

(e/defn View
  [{::keys [op]}]
  (e/client
   (dom/p (dom/text (str "Op is " (name op))))
   (ui/button (e/fn [] (e/server (e/discard (change-op!))))
              (dom/text "Change op!"))))

👀 2
Dustin Getz15:02:26

which snippet is which?

JAtkins15:02:12

The complete snippit has the failing code, the one with change-op! no args works as expected

👀 2
Dustin Getz15:02:04

ok i understand the issue

Dustin Getz15:02:59

Can you read curr-op from the swap's provided prev value, rather than sampling the reference?

Dustin Getz15:02:55

swap! !state update ::op ...

JAtkins15:02:42

Yeah, that works. I'm just confused why the loop happens in case I run across it again 🙂

JAtkins15:02:54

*rather how can I avoid it

Dustin Getz15:02:11

let me think how best to explain

JAtkins15:02:28

cool no rush. I'm heading out - got church rn

👍 2
JAtkins15:02:40

will be back in the afternoon

👍 2
Dustin Getz15:02:30

First, this is a known issue and we know how to fix it 1. change-op! will be called when any of its parameters change – per Electric semantics 2. e/server will throw pending until the remote result is known – per Electric semantics 3. e/fn is actually a DAG value or "piece of DAG"; it has a mount/unmount lifecycle – per Electric semantics 4. ui/button mounts the piece of DAG on click and then monitors for pending exceptions in order to unmount the piece of DAG when the pending state resolves The loop is caused by latency; the swap! on the server -> the curr-op parameter to update on the server -> change-op! 's parameter has updated, so change-op! will be called again per (#1). And due to latency, the button doesn't unmount the piece of DAG (#4) until after (#1) has re-entered. You shouldn't have to understand this, i will try to escalate the fix this week.

JAtkins17:02:35

Ah, this makes a lot of sense. Thanks!

xificurC17:02:47

(let [!x (atom 1)]
  (reset! !x (inc (e/watch !x))))

xificurC17:02:57

Can you see the cycle?

JAtkins22:02:29

Actually I’m back to confused. Why is the body of e/fn invoked more than once? I get that e/fn is a dag value, but I’m not sure why it’s contents are evaled

xificurC22:02:10

Is this a question about the snippet above? Because there are no e/fns there

JAtkins23:02:09

I agree that snippet has a loop. Makes perfect sense. The confusion is how (or why) the body of the on click handler is replaced and called again. If it’s a bug that makes sense

JAtkins23:02:00

The e/fn handler being a dag value that’s replaced also makes sense, the confusion is “why is it called again”

JAtkins23:02:35

I could be missing something here. It’s k inda guaranteed I am actually :)

xificurC23:02:21

Your (change-op! op) has the same cycle

xificurC23:02:19

It changes op so it reruns, in a loop

JAtkins23:02:21

The change op in the dag replaces the string rep in the dom, and recreates the click handler, if I understand right

JAtkins23:02:32

I’m stuck on a phone. I’ll try and make a picture shortly

xificurC23:02:34

Not exactly, the click handler just never finishes running the e/fn because it loops

xificurC23:02:43

As Dustin said we might be able to fix this case, just be wary of cycles like this for now

👍 1
J18:02:56

Another question ^^. I try to send a message to the back part when the tab is closed. I use:

(dom/on "beforeunload" (e/fn [_] (e/server (prn "Foo") nil)))
But looks like the listener is not activated. I tried with the click event and it works.

👀 1
1
xificurC18:02:09

IIUC it should be mounted on the body

J18:02:50

I use it like this:

(e/defn Room [room-id]
  (e/client
    (dom/on "beforeunload" (e/fn [_]...))
    (dom/div ...)))

Dustin Getz18:02:08

(binding [dom/node js/window]
  (dom/on "beforeunload" (e/fn [_] (e/server (prn "unload") nil))))

Dustin Getz18:02:39

(tested, works for me)

J18:02:09

Thanks guys!

Dustin Getz18:02:37

cleaned up: (dom/on js/window "beforeunload" (e/fn [_] (e/server (prn "unload2") nil)))

Dustin Getz18:02:43

there's another arity

Dustin Getz18:02:01

The presence demo is detecting the disconnection from the server side

Dustin Getz18:02:16

Let me know if you want an explanation of m/observe; it hooks flow construction and destruction lifecycle

J18:02:18

I saw it (m/observe) in the code of the html5 router but on client side. Yes, I would like a little explanation about m/observe.

👀 1
Dustin Getz19:02:46

m/observe is meant to manage a resource - it lets you subscribe to a foreign reference (say a Clojure atom) and then release the resource when the flow terminates

Dustin Getz19:02:29

(This is at the Missionary level so it's very low level)

Dustin Getz19:02:40

here, the point is that m/observe lets you detect the flow's cancellation signal and perform a side effect in response to it

Dustin Getz19:02:21

here I modified the toggle demo, to demonstrate using m/observe to hook the flow lifecycle from Electric:

Dustin Getz19:02:39

the electric if will switch between the two objects, unmounting one and mounting the other

Dustin Getz19:02:41

finally, in Electric we use new to join a foreign missionary flow value

Dustin Getz19:02:36

the parallels between objects and flows are on purpose, there is a very deep connection here

Dustin Getz19:02:39

you should almost always think of e/defn as defining a reactive function, and it absolutely is a function

Dustin Getz19:02:03

you can also think of e/defn as defining an object, with a resource lifecycle and state

Dustin Getz19:02:29

this is why we capitalize electric function names and call them with new

Dustin Getz19:02:41

(same as React.js components)

xificurC19:02:07

Not public API though, the ui4 namespace is still in flux

J20:02:56

Thanks guys! Why m/reductions is called with the m/observe?

Dustin Getz20:02:12

m/observe builds a discrete flow, and electric clojure signals are continuous flows. The most important difference is that continuous flows must always have a value, so we use (m/reductions {} nil) to add an initial value, making it compatible with Electric reactive values which can never be undefined

👍 1
Dustin Getz20:02:54

{} here is pronounced "discard"; it is a function that discards the first parameter and returns the second. (Try it at the REPL and convince yourself it is true)

Dustin Getz20:02:27

When using discard as a reducing function over a flow, it throws away the accumulator and returns the most recent value

Dustin Getz20:02:53

Alternative implementation that avoids (m/reductions {} nil), perhaps this one is better

👍 1
vincent18:02:53

gm everyone ☀️ which examples should i read if i want to build a realtime multiplayer commenting and posting app

vincent18:02:46

in case anyone has a strong suggestion, if not i'll rummage 😅

nooga20:02:38

I'm trying to wrap my head around this: When the app gets compiled into the client and server parts it must be that the client part is instantiated many times across users' browsers. What happens to the server part then? I imagine there is only one DAG on the server but there can be multiple flows going through at the same time. 0-cost Multiplayer would suggest that these flows can merge neatly, but how can they be kept separate?

Dustin Getz20:02:06

Good question; on the server each websocket session gets its own instance of the server app. But all the sessions share memory space since they are on the same server

👍 1