Fork me on GitHub
#fulcro
<
2021-11-11
>
cjsauer00:11:18

Hm…but then the {:expr/operands '…} recursion “target” is wrong…it’ll try to recurse on the BranchNode instead of the top-level ExpressionNode

cjsauer00:11:35

Eliminating this weirdness at the data level is probably the best bet

hadils01:11:16

I am running into a problem in my Fulcro app (front-end plain, back-end RAD). I route to a stateful component and I need to assign unique tempids. The problem is that the dynamic router can call :will-enter more than once. I would like to bundle the tempids with the mutation that initializes the stateful component, but I don’t know of a way to do this so that tempids get assigned exactly once.

tony.kay05:11:58

tempids should be generated by the caller, not the callee. You should send it as a route parameter.

tony.kay05:11:11

(like extra param, not part of the path)

hadils01:11:11

I call my mutation inside the :will-enter function. Is this incorrect?

tony.kay05:11:29

only if you're using route-deferred with a lambda

Jakub Holý (HolyJak)10:11:50

As mentioned under https://fulcro-community.github.io/guides/tutorial-advanced-minimalist-fulcro/#_the_bare_essentials_of_fulcro_routing: > WARNING: will-enter can be called multiple times as part of the route resolution algorithm and MUST NOT side-effect in its body. Use dr/route-deferred and do any necessary side-effects in the completion function passed to it, which is guaranteed to be called only once.

hadils13:11:20

I am using dr/route-deferred. I am trying to get my arms around an expanding number of input parameters.

Jakub Holý (HolyJak)20:11:31

In that case the mutation should only be triggered once (provided you transact it inside its callback)??

cjsauer15:11:01

Okay I think I’ve finally found the root problem. I have a component like this now (I got rid of the data weirdness, branches and leaves are the same entity type now):

(defsc Expression [_this {:expr/keys [operator operands]}]
  {:ident :expr/id
   :query (fn [] [:expr/id :expr/operator {:expr/operands '...}])})
The catch is, this Expression component lives below a union component called Statement. Basically users can create many different statement types, one of which is this recursive expression thing. Because of this, the {:expr/operands '…} join is automatically targeting the Statement component as its target of recursion. I was expecting it to use “this” component, the Expression, as the target. The union component obviously doesn’t find the :expr/id key in the props map, and errors with Union components must have ident. Are unions just implicitly the target of recursion?

tony.kay16:11:29

So unions are a bit fiddly. They are really mean to address the common case of heterogeneous lists of things, and since there is really only one thing in the database for a given resolution, but two components (the union and the child) there is a problem dealing with recursive things. Should the recursion be the union or child? Unions ended up being a relatively lightly used feature, and recursion on unions even less so. As a result this question never got more attention. It is perfectly valid to want either. But the system is coded to assume you probably want the way it is used, because UI recursion is usually a tree of the top-level heterogeneous thing.

cjsauer16:11:59

Ah I just posted the solution at the same time as your response. Makes sense. It’s definitely a sharp edge for the strange among us that wander into this zone haha. A “dummy” union seems to work okay.

tony.kay16:11:48

Yeah, I see that. I typically avoid unions in this kind of scenario and just make a component that queries for all the possible things the nodes might have, and generalize to a :node/id for normalization, and a type field for switching up rendering within the component.

tony.kay16:11:00

A multimethod is good for the latter in rendering

cjsauer16:11:10

I’ve actually noticed other EQL processing libraries stumble over this same hurdle. It seems to be a gap in EQL in general. The '... primitive is a bit too underpowered.

cjsauer16:11:00

Hm avoiding unions might work better…I could try that as well. Just one big query and then use the operator to key on.

tony.kay16:11:49

It's too rarely used and has an easy workaround. I serioiusly doubt we'll ever make it better, due to the difficulty of improving it (Pathom, Fulcro, EQL lib would all have to change to support the solution, it would have to be bw compat, etc.), and the fact that there are easy workarounds that are "good enough"

tony.kay16:11:53

yeah...the important bit is normalization/denormalization. Don't need the union for that. A larger query (within reason) is also not so bad

cjsauer16:11:10

Yea definitely. Would have to be EQL2 at this stage of the game.

cjsauer16:11:17

Do you think it’s worthwhile that I open a PR for a docs improvement surrounding recursive unions?

cjsauer16:11:59

Maybe just the code sample of using a “dummy union” to customize the recursion target.

tony.kay16:11:04

On Fulcro dev Guide? Sure, improvements to docs are always welcome

👍 1
tony.kay16:11:14

I'm not sure what your "dummy union" is trying to do

tony.kay16:11:21

and why you're calling it that

cjsauer16:11:16

It’s a union with only a single entity type, so “dummy” as in “it will never actually switch on anything”. It’s literally just to force the union-seen var to the value I want when using '…

tony.kay16:11:21

That is not an intentional design element, so documenting it probably isn't a great idea...are you saying when you do that example, that the recursion then targets Expression, or are you saying the union looks so much like Expression now that it doesn't matter?

tony.kay16:11:39

yeah...that seems pretty pointless

tony.kay16:11:50

oh...so you're nesting a union IN a union?

cjsauer16:11:01

I have to do this or my app won’t boot lol

tony.kay16:11:23

I would avoid unions altogether for this. It's way too complicated for the non-benefit you're getting

tony.kay16:11:06

I mean, it's your code...but a nested unoin....whew...actually a little surprised that doesn't confuse some internal implementation of something

tony.kay16:11:27

I don't think any of us ever thought to design-for or test such a use-case

cjsauer16:11:27

I suppose my only worry with a generalized :node/id is the speed of lookups…I’ll have to sprinkle filter expressions throughout the code to pinpoint the entities I need. Or maintain a separate index…I expect there to be a LOT of these nodes…

tony.kay16:11:33

speed of lookups?

cjsauer16:11:11

Sorry that was vague. Things like “get all expression of type X”. Those become scans rather than just a quick get.

tony.kay16:11:11

if each real node has a :node/type field, which is a keyword, I'm not sure how the resulting implementation could be any slower

tony.kay16:11:11

Well, try your nested union

tony.kay16:11:24

if it ends up working ok, then great

tony.kay16:11:48

you can always load/normalize using alternate components if there's some glitch using those with I/O: A single component with a morphing ident and all attributes in the query would probably function for load.

cjsauer16:11:58

It seems to be normalizing exactly how I’d expect. And I don’t expect to go any deeper than this. I’ll just leave a comment that it’s “only lightly nested” 🙂

cjsauer16:11:49

My loads are interesting. This tree of data is actually coming from a language source file (my own language), and so I’m not actually loading the tree over the network. I load the source file, and then analyze locally, and then transact that into app-db. I’m using Fulcro to render the AST more or less.

cjsauer16:11:30

This odd use-case is likely why I’m running into the rare recursive union problem. Recursive sum types are pretty common in language dev.

cjsauer16:11:34

> A single component with a morphing ident and all attributes in the query would probably function for load. Will keep this in mind, thanks

tony.kay16:11:16

Yeah, I've worked with more than one person trying to leverage EQL against these kinds of things. The core target audience of EQL/Fulcro is applications for the web that are largely business-oriented. So, cases like this just don't come up much and are not a terribly high priority. That said, there are a number of ways to solve the problem your working on. The "single component" solution will actually work to give you what you want, I think.

(defsc Node [t p]
  {:query [:statement/id :expression/id :operator/id :operation/id :etc {:node/children ...}]
   :ident (fn [_ props] based on real ID in props, like you had before)}
  (render-node p) ; multimethod to do proper render

tony.kay17:11:29

On load, this will normalize things into the proper tables and put the right idents in place...and denorm will work just fine. You'll get should component update optimizations.

cjsauer17:11:47

Oh interesting…for some reason I assumed doing so would cause me to lose my separate tables for each statement type…but that’s not the case because the :ident can still be dynamic? It’s the query-as-a-map that makes something a union yea?

cjsauer17:11:06

Well that’s very nice then. I’ll try that out.

tony.kay17:11:12

yeah, way simpler

cjsauer17:11:54

And then within render I can still use case to render the correct component

tony.kay17:11:05

right, or a multimethod like I suggested

tony.kay17:11:19

dispatch function could just detect the ID type

cjsauer17:11:54

Got it. That’s really nice. Very much appreciate the direction 🙏

tony.kay17:11:59

(defmulti render-node (fn [node] first key with an id name))
(defmethod render-node :expression/id ...)

tony.kay17:11:18

If you don't need "narrowing" of the selection on denormalization, and don't load these over the network (where you'd possibly want the resolution), then you could use a much simpler query as well: ['* {:node/children '...}]

cjsauer17:11:22

I expect new nodes in the future so that’s really awesome.

tony.kay17:11:04

It's also perfectly find to use an expression for a query as long as you're not stealing a query from another node in a normalization sense.

tony.kay17:11:46

so: :query (fn [] (into [{:node/children '...}] (concat expression-elements statement-elements etc)) is also fine

tony.kay17:11:12

though I'd probably write that expression in a def outside of the component, so you're not running it every time a get-query happens

cjsauer17:11:19

That’s a good tip. I’ve gotten into trouble before trying to be clever and return (comp/get-query SomethingElse) and it’s no bueno. Metadata is easy to drop or muddle…

tony.kay17:11:48

right, but in this case it's just keywords and the ... symbol

tony.kay17:11:07

I think the Dev Doc contribution is probably more along the lines of this suggestion....don't use unions for this 😄

cjsauer17:11:28

Yea 100% haha.

tony.kay17:11:38

A formalization of this particular discussion would be helpful to others I would think

cjsauer17:11:51

It definitely would’ve saved me a day or two.

cjsauer17:11:24

So just to be sure, is using comp/get-query in this “uber node” not going to work? Like this:

{:ident (fn [] (statement-ident props))
   :query (fn []
            (into [:statement/idx]
              (concat 
                (comp/get-query One)
                (comp/get-query Two)
                (comp/get-query Three)))}

cjsauer17:11:59

Is that what you meant by “stealing”?

cjsauer17:11:23

Ah okay it worked (first try!). My understanding is that concat will lose the metadata on the nested calls to get-query, which in this case is indeed what I want.

tony.kay18:11:59

it will lose the metadata...I would not bother writing defsc for One Two Three...I'd just do (def One [:statement/blah :etc])

tony.kay18:11:18

you don't want the metadata in this case, because the containing component is the only one that matters

👍 1
cjsauer16:11:46

This is what ended up working ☝️ I had to create a dummy union to force the recursion to begin at the proper point. Update for anyone reading, see above thread. TLDR: don’t nest unions 🙂

👍 1
Mark Wardle18:11:32

Hello all. I have been building a re-frame front-end but while it’s easy to make rapid progress, I’m seeing the complexity in managing synchronisation to my backend, and lots of implicit dependencies. It works, but I can see I’m building on sand. So… I already have pathom on the backend, so I have been experimenting with fulcro, obviously! What’s the best way to send authenticated user / tokens together with the EQL in a fulcro-http-remote set-up? Apologies for such a beginner question! At the moment I manually manage both client and server, so have control in configuring the pathom boundary interface on the server, and ensuring the right HTTP header is set in my re-frame http action, explicit but very verbose. I’m using com.fulcrologic.fulcro.server.api-middleware/wrap-api on the server in my new set-up. Thank you for any advice!

tony.kay18:11:07

The client has a middleware system. You use that to augment request/responses as needed.

tony.kay18:11:08

for an example of middleware that modifies outgoing headers

tony.kay18:11:03

easiest thing to do when the header has to change over time is to put that info into an atom that can be modified when needed, and just have the middleware set it when it's available

Mark Wardle18:11:11

That's really helpful. Thank you very much. I was looking to try to get data from the main APP database but it felt wrong to be using functions clearly designed for REPL usage… just popping into an atom sounds like a great idea. Thank you again.

tony.kay18:11:32

yeah, the app database is the target of UI/IO activity of Fulcro itself, but there's nothing wrong with using other language features to implement things outside of the realm of Fulcro's app state database, such as http header handling.

tony.kay18:11:55

and there's nothing wrong with having the result-action of a mutation modify some external atom that is used for such a purpose.

tony.kay18:11:25

In this particular case, the externality is necessary because otherwise you've got a loop in requires: the app requires the networking, which requires the app.

Mark Wardle18:11:52

Got it. That makes sense. Having already got a backend that speaks pathom, writing components feels very easy now I understand the basics. I'm watching your tutorial videos again so I'll get there eventually! Thank you.

tony.kay18:11:16

another way that I've done that is also perfectly acceptable is to make a ns that contains an empty atom for the fulcro app itself, and have your client ns reset that atom to the app value. Then the client AND the networking can refer to the app atom...and it can all be in the app

tony.kay18:11:56

Then the network middleware can use (app/current-state @SPA) to get the normalized database and pull out what it needs.

tony.kay18:11:36

(ns app.application) (defonce SPA (atom nil)))
(ns app.client 
  (require [app.application :refer [SPA]]
           [app.remotes :as r]))

(reset! app/SPA (fulcro-app {:remotes {:remote r/my-remote}}))
(ns app.remotes
  (:require 
    [app.application :refer [SPA]])) 

(defn add-header-middleware [...] ... use @SPA ....)

(def my-remote ...)

Mark Wardle19:11:25

That looks like a great strategy. And on the server I simply need to add a wrap to authenticate as I never want to be processing EQL server side without an authenticated user. I can then stuff the creds into the environment and it'll all work. Phew! Thank you for your advice.

Mark Wardle07:11:46

Thanks! I will have a look.

Mark Wardle07:11:56

(I hadn’t seen that before)

Mark Wardle21:11:29

Thanks Jakub - that’s a useful resource. It’s good to have the same concepts explained in different ways. Of course, my issue was adding middleware into both the server and the client in order to magically handle credentials/auth. Thanks again.

Jakub Holý (HolyJak)21:11:38

Yes, sadly it does not help with this particular problem at all 🙂

Mark Wardle16:11:00

Jakub - thank you very much for pointing out this resource - while it didn’t help for that specific situation, it actually has helped a lot with everything else! Thanks again

🙏 1
❤️ 1
mgxm23:11:04

Trying to think in a way to interop an js promise API within state machine events, anyone tried before? it's possible? since the event expect an env

hadils00:11:17

Hi! I handled the js promise API by creating an async remote that has promise code as resolvers…that was the best way I could figure out @U013STZ1PJ5

mgxm02:11:51

thanks @UGNMGFJG3, it's a good ideia