This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-02-19
Channels
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.cljcWhat I mean is it has specific meaning like idiomatic clojure styles? something like this? https://clojure.org/guides/weird_characters#_symbol_unsafe_operations
Thanks! I’m familiar with (def ^:dynamic *state* …)
though.
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
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
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
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.
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
Thank you for your detailed explanation. interesting. I’m getting more understands of states.
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?
@U02SKL96W1J great - I will DM you in the next few days about doing the video call, i see you are in Asia TZ
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.
::ui/click-event
is not a thing anymore
where, i will delete it
oh, scratch folder is a lot of old stuff
we'll need to do some surgery on the repo
sorry for the confusion
Undefined continuous flow happens when something that is not an electric function is called as such. In this case ui/button called a map
I'm lost; the right way to call ui4/button is https://github.com/hyperfiddle/electric/blob/eaa9f61b5856ff7b1317e6e1a85bd0181dc2d513/src-docs/user/demo_2_toggle.cljc#L19
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
oh, the confusion may be what new
means here
Electric uses new
to call electric functions
We also capitalize them (like react components)
The syntax sugar works –
(e/defn Teeshirt-orders-view [x] ...)
(Teeshirt-orders-view. x)
(new Teeshirt-orders-view x)
does that resolve the confusion?
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
not yet sorry
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!"))))
which snippet is which?
The complete snippit has the failing code, the one with change-op!
no args works as expected
ok i understand the issue
Can you read curr-op
from the swap's provided prev value, rather than sampling the reference?
swap! !state update ::op ...
Yeah, that works. I'm just confused why the loop happens in case I run across it again 🙂
it's subtle
let me think how best to explain
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.
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
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
The e/fn handler being a dag value that’s replaced also makes sense, the confusion is “why is it called again”
The change op in the dag replaces the string rep in the dom, and recreates the click handler, if I understand right
Not exactly, the click handler just never finishes running the e/fn because it loops
As Dustin said we might be able to fix this case, just be wary of cycles like this for now
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.I use it like this:
(e/defn Room [room-id]
(e/client
(dom/on "beforeunload" (e/fn [_]...))
(dom/div ...)))
(binding [dom/node js/window]
(dom/on "beforeunload" (e/fn [_] (e/server (prn "unload") nil))))
(tested, works for me)
cleaned up: (dom/on js/window "beforeunload" (e/fn [_] (e/server (prn "unload2") nil)))
there's another arity
The presence demo is detecting the disconnection from the server side
Let me know if you want an explanation of m/observe; it hooks flow construction and destruction lifecycle
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.
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
(This is at the Missionary level so it's very low level)
here, the point is that m/observe lets you detect the flow's cancellation signal and perform a side effect in response to it
here I modified the toggle demo, to demonstrate using m/observe to hook the flow lifecycle from Electric:
the electric if
will switch between the two objects, unmounting one and mounting the other
finally, in Electric we use new
to join a foreign missionary flow value
the parallels between objects and flows are on purpose, there is a very deep connection here
you should almost always think of e/defn
as defining a reactive function, and it absolutely is a function
you can also think of e/defn
as defining an object, with a resource lifecycle and state
it is both
this is why we capitalize electric function names and call them with new
(same as React.js components)
There are also https://github.com/hyperfiddle/electric/blob/1c6c3891cbf13123fef8d33e6555d300f0dac134/src/hyperfiddle/electric_ui4.cljc#L122-L123 I once used to cover up the interop
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
{}
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)
When using discard as a reducing function over a flow, it throws away the accumulator and returns the most recent value
Alternative implementation that avoids (m/reductions {} nil), perhaps this one is better
gm everyone ☀️ which examples should i read if i want to build a realtime multiplayer commenting and posting app
https://github.com/hyperfiddle/electric/blob/master/src-docs/user/demo_4_chat_extended.cljc shows presence, you'll need that
https://github.com/hyperfiddle/electric/blob/master/src-docs/user/demo_5_todomvc.cljc has nontrivial database queries, loading states, and interesting ux
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?
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
Datomic browser demo released: https://github.com/hyperfiddle/electric-datomic-browser