This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-03-06
Channels
- # babashka (60)
- # beginners (36)
- # clj-kondo (29)
- # clojure (91)
- # clojure-dev (18)
- # clojure-europe (12)
- # clojure-nl (1)
- # clojure-norway (11)
- # clojure-uk (5)
- # clojuredesign-podcast (8)
- # clojurescript (40)
- # core-typed (74)
- # data-science (8)
- # datomic (9)
- # emacs (22)
- # events (5)
- # fulcro (56)
- # gratitude (3)
- # hyperfiddle (11)
- # lsp (6)
- # malli (36)
- # meander (23)
- # off-topic (50)
- # polylith (4)
- # portal (10)
- # reitit (4)
- # schema (1)
- # shadow-cljs (66)
- # squint (3)
- # tools-deps (16)
in a uism is there a better way to check if a particular report has started than checking the state?
(get-in state-map ::uism/asm-id (report/report-ident StudentsReport))
On page load (and refresh) I noticed that the completion-fn in route-deferred get called twice. Is that expected or a bug? I was relying on it being called only once in my state machine,
That’s why it is supposed to be a side-effect free function. The docs say it can be called more than once. It’s part of the route search algorithm.
My understanding was that :will-enter can be called more than once but not the completion-fn
Docs improvement opportunity 🙂
hm…you are right that it doesn’t need to call the completion function more than once…
you mind narrowing that down? Like by using fulcro as a source dep and seeing why? I don’t think it is intentional
@U0CKQ19AQ looks like dr/change-route! is being called twice, and it is happening in fulcro-rad not fulcro
so it is called once by fulcro-rad/install-history
and then once by fulcro.dr/route-to!
and that second fulcro.dr/route-to! is actually called by rad/restore-route!
so I think the issue might be that install history purpose is to set up a listener, but the listener is actually triggered immediatly with these params:
:route [course content class edit 193] :params {:action edit, :id 193, :com.fulcrologic.rad.routing.history/direction :back}
so on first page load it is (wrongly?) assuming there's immediatly a "back" event that needs to be handles, to attemps to route-to there alreadymy proposition would be to add a guard when (gobj/getValueByKeys evt "state")
in the pop-state-listener
I’d have to re-wrap my head around the entire thing, which I don’t have time to do for a while. If you can spell out why you think that’ll work and be a non-breaking change, then that would be a big help
because what is happening is that it gets an event with a null
state on initialization, which is then treated as a "backward" event, because its not foward?
as (< event-uid current-uid)
(event-uid being part of state
which is nil`
I think it will be a non breaking change because it looks like the fact that there would be a initial event wasn't expected, and the pop-state-listener
is only handling forward or backward events, using the ordering of event-uid to determine which of the two to pick.
The first event however (at least on chrome), in neither a forward or backward event, and doesn't have an event-uid, so we can distinguish it and discard it when it comes to handling it in the pop-state-listener
it also seems like Firefox is not sending this initial event
its only Chrome
no rush on my end anyway I'm running from my fork
I originally considered “routing” to be an app-layer concern. I had enough requests that I build dynamic routing, but I do not claim that I did a perfect job here. The UI routing concern is just so involved. People want to hook too many things to it. I’m sort of working on a system with my new statechart library and fulcro integrations where I’ve got some ideas around application composure that’s more a system of statecharts as opposed to these hidden and standalone state machines. In any event, I generally recommend the following if you’re making anything of significant size/complexity: Treat dynamic routing as a feature for getting things on-screen. Use application layer logic (I’ve switched almost exclusively to statecharts) for the actual logic. This decouples the application from the routing.
> Treat dynamic routing as a feature for getting things on-screen. Use application layer logic (I’ve switched almost exclusively to statecharts) for the actual logic. This decouples the application from the routing. I only use will-enter to trigger a state-machine (and an augmented version for forms and reports that does the usual stuff they do + that state machine trigger). The only purpose of the state machine is to figure out the logical state of the routing to load what needs to be on screen. If I wasn't doing it on will-enter I guess I'd do it in a lifecycle hook?
So, here’s what I do. For example in my “signin/onboarding” system. I have to handle resuming session, resetting passwords, initial onboarding questions, etc. I make a state chart for that overall functionality. The state chart is better because it is hierarchical. Then I just make a “custom” node for the statechart graph with a function like this:
(defn rstate [{:keys [target params] :as state-props} & children]
(apply state (dissoc state-props :target :params)
(on-entry {}
(script {:expr
(fn [env data]
(log/info "Resolving target for route: " target)
(let [RouteTarget (scf/resolve-actor-class data target)]
(log/info "Routing to" (rc/component-name RouteTarget))
(rroute/route-to! @app-atom RouteTarget (or params {}))))}))
children))
then I can make nodes in my statechart that, when entered, put the right thing on the screen. All of the logic (loading, route selection, etc.) is now in ONE centralized place. The other thing I do is make co-located statecharts for components. That is a more advanced thing, but the idea is that instead of having the dynamic routing deal with anything, I make an routing/invocation node in my statechart that does the routing, then checks the component options for a statechart definition. If it finds one then it starts the statechart. Thus I end up with a cooperating parent/child statechart graph.The localized UI state machines on RAD are of course bw compatible with this because they use the will-enter and such.
Here’s istate
:
(defn istate
"A state that expects to have a `target` like rstate, but also expects that target to have a statechart
that should be invoked. The target actor must have a :statechart.definition/id that can be a constant or a `(fn [env data Target])`,
where the env and data come from the running system statechart. The result must be a statechart that is actually
registered in the statechart system. By default this state also routes to the target, but you can override that using
`:route? false`"
[{:keys [target route? params invoke-params finalize autoforward]
:or {route? true
invoke-params {}}
:as state-props} & children]
(apply state (dissoc state-props :target :params)
(ele/invoke (cond-> {:params (merge
{:fulcro/actors (fn [env data]
(let [Target (scf/resolve-actor-class data target)
actors (merge {:actor/component (scf/actor Target)} (?! (rc/component-options Target :statechart.fulcro/actors)))]
actors))}
invoke-params)
:autoforward (boolean autoforward)
:type :statechart
:srcexpr (fn [env data]
(enc/if-let [Target (scf/resolve-actor-class data target)
id (log/spy :info (?! (rc/component-options Target :statechart.definition/id) env data Target))]
(let [chart (?! (rc/component-options Target :statechart/definition) env data Target)]
(log/info "Registering stat chart during invoke")
(scf/register-statechart! @app-atom id chart)
id)
(log/error "The istate" (:id state-props) "is missing a target, or the target has no :statechart.definition/id")))}
finalize (assoc :finalize finalize)))
(on-entry {}
(script {:expr
(fn [env data]
(when route?
(let [RouteTarget (scf/resolve-actor-class data target)]
(rroute/route-to! @app-atom RouteTarget (or params {})))))}))
children))
and the overall statechart starts to look like this:
(def system-statechart
(statechart {}
(ele/data-model {:src {:fulcro/aliases
{:sign-up-mode? [:actor/choose-login-method :ui/sign-up-mode?]
:member/id [:actor/session :member/id]
:session-nonce [:actor/session :login/nonce]
:session-nonce-ready? [:actor/session :login/nonce-sent?]
:session-valid? [:actor/session :session/valid?]
:onboarding-fields [:actor/onboarding :ui/fields-to-fill]
:onboarding-field [:actor/onboarding :ui/current-field]
:email-login-error-message [:actor/email-login :ui/error-message]}}})
(state {:id :state/top-region}
(on :error.network :state/network-unavailable)
(transition {:event :error.http}
(script {:expr (fn [_ _]
[(toast (tr "Operation Failed") (tr "Sorry, there was an unexpected server error."))])}))
(on :event/session-resumed :state/initial)
(transition {:event :event/toast}
(script {:expr (fn [_ _ _ {:keys [title message]}] [(toast title message)])}))
(state {:id :state/initial}
(on-entry {}
(script {:expr (fn [_ _] [(fops/load :application/config nil {})])})
(script {:expr resume-session}))
(transition {:event :event/next
:target :choose/progress}))
(rstate {:id :state/network-unavailable
:target :actor/network-unavailable}
(on :event/retry :state/initial))
...
I’m using the above as the central overall logic for an entire Fulcro-based react-native app. Once I work out all the kinks I’ll probably add the bits to the fulcro integration part of the statecharts lib
The extra cool thing about this is that the application LOGIC is now completely separate from the UI. So, that same statechart can be used for the web version of the app, just by subbing out the “actors” with Dom-based instead of native-based components.
e.g.: Starting the statechart with the various actors in a native-specific entry point
(system/start! @app-atom
{:actor/network-unavailable ui.root/NetworkProblems
:actor/choose-login-method ui.login/ChooseLoginMethod
:actor/email-login ui.login/EmailLogin
:actor/image-picker ImagePicker
:actor/onboarding (scf/actor ui.onboarding/OnboardingProfile [:member/id :none])
:actor/session sessions/Session
...}
{:toast-mutation `toast
:clear-session `clear-session})
where my system/start! is just:
(defn start!
[fulcro-app actors mutations]
(scf/register-statechart! fulcro-app system-statechart-id system-statechart)
(scf/start! fulcro-app {:machine system-statechart-id
:session-id system-statechart-id
:data {:fulcro/mutations mutations
:fulcro/actors (enc/map-vals
(fn [a]
(if (or (qualified-keyword? a) (rc/component-class? a))
(scf/actor (rc/registry-key->class a))
a))
actors)}}))
I haven't had the time to look into statecharts properly yet, how do they compare to uism? do they superseed them completely and the use of uism in forms/reports could be replaced by statecharts?
Yes, they are more advanced, more generally useful (not tied to Fulcro, work in CLJC), can be used for long running things in back end (such as subscription expiration tracking), etc.
Based on the original work by Harel. There’s a SCXML standard that I based my implementation on
UISM is old-school state machines. Which are flat, and suffer from “state explosion” (number of nodes get large with any complexity).
state charts have hierarchy, include the idea of “invocations” of external processes, have a much more formalized set of constructs (on-enter, on-exit, eventless transitions, delayed events, etc.)
But my Fulcro integration stuff for them adds in Fulcro’s UISM concepts of actors and aliases to make them reusable in UI contexts…so UISM contributed some ideas there.
fulcro4 will replace state machines with statecharts then 😄
> UISM is old-school state machines. Which are flat, and suffer from “state explosion” (number of nodes get large with any complexity). that's great if statecharts improve on that, that's one of my main beef with the state machines but now I'm in a pickle because I have to double down on the state machines for some features and don't have the time to pick up on state charts for that
some time ago you mentioned that you implement > durable store and event queue using PostgreSQL is that how you manage > long running things in back end (such as subscription expiration tracking), etc. ? there's a way to express something like "trigger this event in a month if the statechart is still in that state"?
This is the standard (and alg) I’ve implemented: https://www.w3.org/TR/scxml/ Just pretend we use clj syntax instead of xml (easy enough). The element in question is send https://www.w3.org/TR/scxml/#send, which allows you to specify a literal delay or even a delay expression. So, say I’m doing a subscription expiration, I’d use a send element to schedule an event and calculate the delay based on the current date vs the expiration date.
this puts an event on the durable queue. Then I have a thread pool that delivers events to the statecharts (as long as two threads don’t try to talk to the same chart at the same time).
https://fulcrologic.github.io/statecharts/#_event_processing I’ve made it all very generic and pluggable so it can work well in CLJC. Thus the need to write your own (implement a protocol) event queue/processing.
> as long as two threads don’t try to talk to the same chart at the same time that sounds a bit tricky, I've implemented a task queue in progress a couple times using a skip lock but it was per task
If it's an SQL implementation, then you can use locking on the table. The easiest thing to do is to have a single thread responsible for the event queue, or a single machine. If it's a single machine, then it's trivial to make a local in memory tracking mechanism for which machine is actively being processed, so you can have a local thread pool
In a cluster of machines, I use the same kind of technique that atomic does for this. I use redis to coordinate which node is the current leader, and use a simple heartbeat mechanism to take over the leader role. This way you can have a cluster of machines where they all trying to become the leader, but only one wins.