Fork me on GitHub
#clojurescript
<
2021-08-04
>
Nik04:08:38

Hey. does clojurescript has any way to work with AsyncIterable inteface in JS. The JS way is to use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of. For normal async/await or promise, I found - https://clojurescript.org/guides/promise-interop

Karol Wójcik05:08:02

Isn’t it something you can iterate over and just treat it like a regular promise? Cljs does not support async/await

emccue05:08:03

I don't think so natively, but -

emccue05:08:04

you should be able to make a macro that handles it

emccue05:08:26

(async-for [thing async-iterable]
  (inc thing))

emccue05:08:05

can expand to

emccue05:08:28

(let [async-iterator ((aget async-iterable (.- js/Symbol asyncIterator)))
      handle-next    (fn handle-next [value]
                       (let [next (.next async-iterator)]
                         (.then next
                                (fn [res]
                                  (if (.- res done)
                                    value
                                    (handle-next (conj value (.- res value))))))))]

  (handle-next []))

emccue05:08:36

is my first stab at it

emccue05:08:55

you could also throw in some handling of (reduced) or make the point to do the side effects

emccue05:08:30

(defmacro async-for [binding async-iterable & body]
  `(let [async-iterator# ((aget ~async-iterable (.- js/Symbol asyncIterator)))
         handle-next#    (fn handle-next# [value#]
                           (let [next# (.next async-iterator#)]
                             (.then next#
                                    (fn [res#]
                                      (if (.- res# done)
                                        value#
                                        (handle-next (conj value# 
                                                           (let [~binding (.- res# value)]
                                                             ~@body))))))))]
                                                             
     (handle-next# [])))

Nik08:08:18

@U3JH98J4R I should have clarified that I'm looking to consume AsyncIterable. I tried with (defn ipfs-stat [ipfsPath] (go (<p! (.ls (:ipfs @app-state) ipfsPath)))) Thanks for the code sample though, it going in my rosetta notes for js and cljs 😉

Nik08:08:50

but it returns error in browser console Uncaught TypeError: p.then is not a function at Object.cljs$core$async$interop$p__GT_c [as p__GT_c] (interop.cljs:19) at switch__30353__auto__ (core.cljs:20) at eval (core.cljs:19) at Function.ipfs_browser$core$ipfs_stat_$statemachine__30354__auto____1 [as cljs$core$IFn$_invoke$arity$1] (core.cljs:19) at Object.cljs$core$async$impl$ioc_helpers$run_state_machine [as run_state_machine] (ioc_helpers.cljs:43) at Object.cljs$core$async$impl$ioc_helpers$run_state_machine_wrapped [as run_state_machine_wrapped] (ioc_helpers.cljs:47) at eval (core.cljs:19) at cljs$core$async$impl$dispatch$process_messages (dispatch.cljs:27) at MessagePort.channel.port1.onmessage (nexttick.js:214) @UJ1339K2B Can you please give an short example.

Nik08:08:55

FYI, I'm using IPFS API, here are https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md#ipfslsipfspath on the ls method with example in Js

Nik08:08:27

I'm getting the feeling I probably not using <p! right it comes from this require statement - [cljs.core.async.interop :refer-macros [<p!]]

Nik08:08:57

Most of my searches lead to examples of Channels in Clj/Cljs

mauricio.szabo21:08:31

I think that for async...of does not translate to anything - it's just new syntax. I don't think there's a way to make it work with CLJS yet... Maybe use the (js* "javascript-code-here") to somehow handle this problem?

Nik03:08:14

@U3Y18N0UC using .next seems to working, it returns promise one by one. thanks for the js* trick, I didn't know something like this also exists

emccue04:08:11

@nikwarke I posted a more "complete" code sample with explanation on your ask.clojure question https://ask.clojure.org/index.php/10896/how-to-work-with-asynciterable-interface-in-cljs

emccue04:08:24

you might have seen, but just calling your attention to it

mauricio.szabo13:08:58

@nikwarke yes, the .next works but only for asyncIterators. There's also asyncIterable (notice the able in the end) and to make that work, we have to use the js* macro. @U3JH98J4R unfortunately, the code you posted on ask.clojure will not work - that was my first attempt. aget only works for string arguments - but you can replace that code with (js* "~{}[Symbol.asyncIterator]" async-iterable) and then it works 🙂

emccue13:08:37

gross. but okay

mauricio.szabo13:08:13

Yeah, I agree 😅

emccue13:08:49

i updated the snippet, since that might be what someone finds on a google

emccue13:08:24

im also pretty sure what i wrote would eventually blow the stack, but idk a better way

Nik00:08:08

Hmm, while using .next I started getting another error. It was related to parsing a JS object to cljs and I wasn't sure who is responsible, cljs or ipfs api. I started reading code from ipfs side. I'll give it try with *js as well

dnolen14:08:46

@nikwarke there's no explicit support for it - but I think the idiomatic thing to do would be to turn into a streaming channel

Nik14:08:01

@dnolen I think it does convert to channel. I tried to run following code (go (let [val (<! (.ls (:ipfs @app-state) addr))] (println val))) which returned - #object[cljs.core.async.impl.channels.ManyToManyChannel]

Nik14:08:47

Here are the docs for the 'ls' method of IPFS api that I'm using - https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md#ipfslsipfspath

dnolen14:08:27

that's not going to work as is - sorry can't chime in how to do this at the moment

Nik14:08:30

I just started with clojure so feeling a bit out of depth right now 😅

Nik15:08:07

oh okay. no problem, thanks

raspasov15:08:37

@nikwarke can you call .next on the result of (.ls (:ipfs @app-state) addr) ?

raspasov15:08:37

(.next (.ls (:ipfs @app-state) addr)) - does that work, aka return a JS promise?

raspasov15:08:40

I haven’t used the IPFS library or AsyncIterators, so this is a blind guess.

Nik15:08:37

@U287L02DT Aha! yes, it returns - #object[Promise [object Promise]]

raspasov15:08:58

Cool, so you can process that AsyncIterator via a ClojureScript (loop [] (recur ...)) + some Promise interop. Not the prettiest thing, but it seems like it should work.

raspasov15:08:15

Basically, keep calling (.next …) until the iterator is exhausted. For every promise you get, I would put the Promise return value onto a core.async channel (or just for debugging, you can swap! to an atom - not a good approach in general)

Nik15:08:49

yeah, thanks a lot. I'm building a pet project so ugly is fine for now 🙂

👌 2
mauricio.szabo21:08:05

@dnolen while this solution works for AsyncIterable, it does not work for AsyncIterator. So, there's no way to consume this, for example, in ClojureScript:

(js* "myAsyncIterable = {
    async* [Symbol.asyncIterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
};
")
Do you have any idea on how to make this work? Is it something that'll need to be added to the ClojureScript compiler?

mauricio.szabo22:08:01

Ok, I found a way :). Will try to summarize in a gist so other people can use it too

dnolen14:08:56

instead futzing around w/ macros

dnolen14:08:21

then you can use all normal ClojureScript iteration patterns as well as core.async streaming channel ops

dnolen14:08:55

not saying it's particular obvious how to do or anything

dnolen14:08:36

that said, would be yet another nice community contribution - I helped guide the other JS interop stuff - happy to guide this one too

dnolen14:08:25

I would say the difficulty is pretty low - provided you understand the JS and CLJS sides of the problem

Proctor15:08:47

We are seeing some seemingly odd behavior using def where we are memoizing a more expensive function for caching in our Lambda (using ShadowCLJS), and doing a with-redef When testing with the Spy library to mock it, we get the error:

#error {:message "Promise error", :data {:error :promise-error}, :cause {:status :unexpected-failure, :body #object[TypeError TypeError: user_auth_profile.employee_record.update.build_client is not a function]}}
Looking in deeper, it also happens if we do a with-meta as well, but if we do a let over the def we can do the with-redef fine. I have tried to create a simpler version of the flow to try to reproduce it, but the simple example doesn’t reproduce like the other one, which I will paste in the thread…

Proctor15:08:48

The sample src (although this works, but the structure is the same)

(def build-client (memoize identity))

(defn- process
  [event]
  (async-utils/go?
    (let [client (build-client {:some :settings})]
      (assoc event :client client))))

(defn lambda-handler
  [event]
  (let [p (p/promise)]
    (async/go
      (try
        (->> (let [res (async/<! (process event))]
               {:success res})
             (p/resolve! p))
        (catch :default e
          (p/reject! p e))))
    p))

Proctor15:08:42

the sample test:

(deftest test-lambda-handler
  (testing "It adds the client to the event"
    (async
      done
      (go
        (println "calling simple repro test with build-client as spy ")
        (let [build-client (spy/mock (fn [_config] {:double-impl :results}))]
          (with-redefs [er-update/build-client     build-client]
            (let [res (core.async.interop/p->c (er-update/lambda-handler {:test :event}))]
              (is ((complement nil?) res)))

            (done)))))))

Proctor15:08:47

still playing with it, and we have some work-arounds, but it has piqued curiosity on if there are any complier tricks or something that may account for the differences where a MetaFn cannot be called, but a Function can be called

Proctor15:08:17

and that in the scenario when it fails, e.g. (build-client {}) if I change it to be an apply, e.g. (apply build-client {}) the apply works

lilactown15:08:53

it sounds like a MetaFn is being passed to a promise's .then

lilactown15:08:52

since you haven't posted the non-working code it's hard to diagnose why that is occuring

Proctor16:08:18

ooohhh… that sounds like it might be reasonable…. will have to play with that

lilactown16:08:23

it sounds like there are two issues: • with-redefs isn't behaving the way you assume in async code • an error is occurring when passing a MetaFn to a promise

lilactown16:08:04

> but if we do a `let` over the `def` we can do the `with-redef` fine. what does this mean?

Proctor16:08:44

the first fails:

(def build-client (memoize identity))
but if I do a let to capture the memoize and then delegate to using a function, it works:
(let [build-client* (memoize identity)]
  (defn build-client
    [config]
    (build-client* config)))

Proctor16:08:30

so it made me think there was something about the def itself, but that might be a wrong hypothesis

Proctor16:08:50

and there are other mocked out functions (defined with defn) that are in the with-redefs block that work, where the item setup as a def doesn’t

Proctor16:08:59

if I do a with-redef on the var and give it a Function instead of a MetaFn it works as well

lilactown16:08:29

can you reduce it down to something that does not use with-redefs at all?

Proctor16:08:32

which is what was leading me to think there might be some nuance around the def when not augmented in the output of defn macro transformation

Proctor16:08:47

lemme check on that

lilactown16:08:01

it sounds like the issue is memoize creates a metafn (not sure) and you're trying to use it with a promise

lilactown16:08:11

that is separate from your issue with with-redefs

lilactown16:08:48

with-redefs is a massive footgun if you're doing anything async. I would not recommend it

Proctor16:08:38

memoize is not the metafn, but using the Spy library on it makes it so

Proctor16:08:52

could also do it using a with-meta

Proctor16:08:23

testing out removing the with-redef by giving a dumb implementation that we don’t have to mock out now though

Proctor16:08:37

and seeing if making it a meta-fn breaks it

lilactown16:08:48

based on your example code (That does work as you said) I cannot see what changes you would need to make to break it in the way you're seeing

lilactown16:08:24

the error sounds like it's something akin to

Promise.then(build_client)
but that doesn't seem to occur in your code

Proctor16:08:35

so: • taking out the with-redefs and giving an identity in the source (which we don’t use because the part that consumes it is mocked out as well), works • wrapping the call with with-meta in the source (e.g. ((with-meta build-client {:meta :data}) config} and no with-redefs works • passing a spy/mock for build-client by using with-redefs and having that wrapped in the with-meta call above works • removing the with-meta around the invocation works • changing it to (apply build-client config) works regardless of MetaFn or not, as well as with-redefs

Proctor16:08:24

we have ways to work around this, but the nuance here is what hooked me… 🙂

Nico16:08:40

Hello there, I'm a reagent beginner and I'm stuck on a (seemingly) simple problem in form fields handling. I don't want to use reagent/forms for now, in order to understand how the "vanilla" system works. Here is the thing: I want to bind an atom to a checkbox (easy, isn't it ?:) But when I click my checkbox, the state doesn't change. I wrapped up the atom in the following component for the demo : (defn my-checkbox [] (let [state (r/atom true)] [:input {:type "checkbox" :checked @state :on-change (fn [e] (let [v (-> e .-target .-value)] reset! state (not v)))}])) I tried to trace the value of v, it's correct. Could you please guide me to the right path? :) Thanks a lot !

p-himik16:08:34

Replace let with reagent.core/with-let, according to your import of reagent.core. Wrap reset! and its arguments in (). And finally, definitely go through Reagent examples and documentation - there's not too much of it, and it explains a lot. Also, there's #reagent

Nico16:08:56

Oh thank you very much I'll give a try tonight ! Sorry about the wrong topic 😕

isak16:08:02

One other problem - you need to use the checked property, not the value for that HTML control. So (.. e -target -checked) is the value you want to reset! your reagent atom to.

Nico09:08:21

Hi, so after some head scratching here is a working component

(defn my-checkbox []
  (let [state (r/atom true)]
    (fn []
      [:input {:type      "checkbox"
               :checked   @state
               :on-change (fn [e]
                            (let [v (-> e .-target .-checked)]
                              (reset! state v)))}])))
@U2FRKM4TW I tried the with-let without success, I'll investigate further 🙂 @U08JKUHA9 the checked property was important, thank you ! And the two other points were to • Return a function instead of the hiccup stuff so that the atom is not reinitialized at component refresh • reset to (-> e .-target .-checked) instead of its negation because the checked property was already toggled in this handler ! Thank you so much to you both :)

3
p-himik09:08:17

So does it not work if you write it like this?

(defn my-checkbox []
  (r/with-let [state (r/atom true)]
    [:input {:type      "checkbox"
             :checked   @state
             :on-change (fn [^js e]
                          (let [v (-> e .-target .-checked)]
                            (reset! state v)))}]))

Nico09:08:54

Outch it does -_- Let me hide somewhere for a little time please 😕

😄 3
Ronny Li17:08:07

Hi everyone, I just watched a https://m.youtube.com/watch?v=3HxVMGaiZbc from David Nolen and had a couple questions: 1. He mentions that all components are in Javascript and they get called in CLJS with the appropriate props. How does he do that? 2. At roughly 29:00 we can see a bit of their code and I noticed that their view function returns another function that accepts props. What is the benefit of this pattern? 3. Also at 29:00 we see that they seem to pass around an increasingly large props map by merging new keys into it. What's the benefit of that pattern? Thanks in advance! This was a great talk. I can't believe their CLJS codebase is only 1500 LOC...

athomasoriginal17:08:19

i’m not part of their team, but assuming Reagent is what they’re using: 1. You can use JS packages inside of CLJS (interop). So, they likely write React components, make it available to their project as a library and just use them directly. Think material-ui. see https://github.com/reagent-project/reagent/blob/master/doc/InteropWithReact.md 2. That appears to be a form-2 reagent component. 3. This can be done for many reasons, but often it’s chosen for convenience (they’re chosen might be for a different reason though!) Again, i’m not speaking for them, just how I interpret the code.

dgb2317:08:56

On 3: I think I use this or a similar pattern sometimes, not just in frontend code. If you have some process that can be described as a pipeline (might be tree-like), then you can either encode it like so: A -> B -> C Where each arrow is a full transformation to a different data structure. However you can encode it like so too: (A) -> (AB) -> (ABC) Here, information is not lost between transformations, because they just add things. The parens imply that you wrap the data structure into something. Destructuring, pattern matching, multimethods or other forms of dispatch can then be used to narrow down what any particular function cares about. Note that (ABC) might make the most sense if A, B and C are structurally equivalent, so you can combine them more deeply, it’s not necessary but it would bring unique advantages. (ABC) might also be a denormalized structure, meaning parts of it are derived from other parts. What’s the advantage of this? * It’s for example nice if you didn’t quite figure out what your modules/functions need to know. * It’s great for observing and debugging. * The stuff that consumes your transformations in the end might have special dispatch rules much later in the development to account for “special cases”. * Metadata might be a better solution for this in some cases.

🙏 2
Ronny Li18:08:02

Thank you @U6GNVEWQG! 1. Ah I guess I could export the JSX components and use them in CLJS like [:> FlipMove ... ] 2. Oops of course 🙂 3. They mention that all their components can be simulated in Storybook so I guess that means they rely heavily on props. Does that mean their entire UI re-renders constantly whenever anything in the props changes?

athomasoriginal18:08:41

> so I guess that means they rely heavily on props Their components are, as David notes at one point “static”. In the react world, they use the word “dumb components”. This is a good thing 🙂 You will strive to make as many components as possible “static” > Does that mean their entire UI re-renders constantly whenever anything in the props changes? I can’t speak for vouchio, but I doubt they would have excessive re-renders. In general, components don’t re-render unless props change. You get to control when props change and which components are listening so you can control when re-renders happen.

Ronny Li18:08:32

Thanks everyone, I'm going to try to incorporate some of these ideas :)

athomasoriginal18:08:34

Honestly, their approach is pretty great and I use a similar approach in my apps as well. The only difference is that I write my components in CLJS. Now, keep in mind, if your building a team of JS devs and moving them to CLJS, Nolen’s approach seems like a good one.

👍 2
Ronny Li20:08:48

@U6GNVEWQG on closer inspection, is it a form-2 component if the outer function and inner function don't share the same arguments? Or am I just extremely ignorant about form-2 components?

athomasoriginal22:08:20

I would have to double check, but all my comments are based on the assumption they are using Reagent, but who knows, maybe not. Nolen said it’s just an HOC so maybe this isn’t. I wouldn’t get too hung up on it though 😉

👍 3
dnolen17:08:13

@ronny463 1. interop w/ ClojureScript just works 2. just higher order stuff 3. the underlying component just does a lot of stuff (manages many different nested views) - we don't care about the details but event handlers for the various things must be setup

🙏 4
Ronny Li18:08:57

For 3. do you find that a lot of unaffected parts of your UI are re-rendering because a small part of the props map is updating?

Ronny Li18:08:37

Can I also ask where you subscribe to the reframe events if the components rely entirely on props? I guess that's done when the app is initialized and the results are passed down as props?

dnolen18:08:01

3. might be true, but no performance issues for us

👍 2
dnolen18:08:23

props are for the pure components, business logic state changes are handled by re-frame

🙏 2
dnolen17:08:02

note it would be a lot more code but by pushing all the components, styling etc. to JS/Storybook - then nothing is left for the ClojureScript except business logic

dnolen17:08:24

which is what you want - nothing is obscured

👍 2
Ronny Li18:08:27

I also like that it allows you to bring on JS developers and have them be productive right away

dnolen18:08:37

yes that was part of the goal

Drew Verlee21:08:23

What are some storybook like libraries in the cljs ecosystem? I know of dynadoc, devcards, nubanks worspaces, any others people have seen or used?

👀 2
isak22:08:17

@dnolen with that approach, is needing to convert back and forth from js to clojure data types (like maps, vectors, keywords) constantly a problem?

dnolen23:08:21

reagent mostly handles it - but not the nested case - you can use react context to solve this though, so the component can be compatible with JS or CLJS usage

3
lilactown23:08:20

it's also easy enough to just pass in JS values if your components are dumb enough

3