Fork me on GitHub
#clojurescript
<
2022-10-13
>
Daniel Gerson09:10:15

Hi all. I was trying to wrap my head using promesa flavoured promises to represent something of a standard but contrived failover example. Apologies for the zoo and lions, but it's more memorable than db calls. It's not obvious from the User Guide how to structure something like this. I thought it would be good to post it publicly for comment, to help anyone in the same bind, and have for future reference. More in 🧵

Daniel Gerson09:10:25

The use case is: - Visit the zoo api call - Check all cat cages are closed (a series of async calls) - If lion cage open call lion tamer (async) - propagate error up to visit zoo call and return zoo closed for maintenance, logging appropriately. https://github.com/dmg46664/problems/blob/main/04_promesa_error_handling/promesa_zoo.cljs

Lone Ranger11:10:13

Interesting, this is helpful. Thank you!

gratitude 1
Daniel Gerson12:10:11

@U3BALC2HH No problem! I'm also looking for constructive criticism if any :-)

Lone Ranger12:10:34

Ok. Well for starters, is this server/nodejs code or client code?

Lone Ranger12:10:51

I'm just trying to wrap my head around the http-style-response maps

Lone Ranger12:10:19

The reason is because the calling context is a bit unclear to me -- unless you are plugging this into a framework that is expecting this sort of arrangement, the data returned from the p/resolved is effectively a wasted no-op. (If this is server code, ignore the following) You don't seem (I could be wrong) to have an async escape hatch. Generally speaking, I tend to expect to see a series of async calls end with some side-effect performed, typically one that updates one or more parts of the application state or triggers an observer or pub/sub chain that results in a different view rendered for the user. I don't see that here. As a consequence of this, I do not think that the error propagation you are looking for is going to propagate in the way you are looking for it to propagate. Typically in a client-side application I would expect an error to result in changing some part of local state to indicate an error occurred, and in changing the state (assuming this were a React flavored app) the view would render the error to the user. I'm know this is about lions and tigers but I'm not seeing any pseudocode to that effect -- so while I think your code is valid, without that side-effect/state-mutation escape hatch, I don't think this is going to serve to propagate the error state in the way you're going for.

Daniel Gerson12:10:48

Apologies, some context: This code is meant to help me reason about http/lambda aws calls, spawning dynamodb promises. Dynamodb gives back http status responses, or errors (not sure of the mix). However, the layered example is generic and so could happen both on the client or server. The http status results simply indicate where this started from. Hope that helps. I just saw your long response now, so will digest it later in the day.

Lone Ranger12:10:20

Whoops, just saw the nbb reference at the top -- my comments may be totally invalid. Since this was in #C03S1L9DN I assumed it was for a client app, my bad, probably

Daniel Gerson12:10:17

No problem. It wasn't obvious to me that I could say throw errors in p/catch and that is the way to bubble up the problem. Hence I needed to flesh an example out to have a mind model going forward.

Lone Ranger12:10:29

Yeah, the error will propagate but only in the async context

👍 1
skylize14:10:48

Promesa follows more or less the same model as JS promises, where errors bypass anything later in the chain, until somebody catches it. The catch can either return a value, which will proceed to the next item in the chain or re-throw, which again bypasses anything that fails to catch.

(-> (p/rejected (ex-info "error" {}))
    (p/then (fn [_] "I will never be called"))
    (p/catch (fn [error]
               (print (ex-message error))
               (throw error)))
    (p/then (fn [_] "I will never be called"))
    (p/then (fn [_] "I will never be called"))
    (p/catch (fn [error]
               (str "resolved: " (ex-message error))))
    (p/then print))

error
resolved: error
#<Promise[~]>

Lone Ranger14:10:57

Nice. And am I right that unless caught, the error won’t percolate to the main thread, right?

👀 1
skylize16:10:10

I've only used Promesa a little. But so far, I see no sign of exceptions escaping the promise context. E.g. If you remove everything after the throw in my example above, nothing blows up. That part is quite different from using native Promises in JS, where Errors/rejections bubble out the end as Uncaught (in promise).

👀 1
Daniel Gerson19:11:46

So now that I've learned more today, the alternative feedback I was searching here was https://www.youtube.com/watch?v=bDN898hu_wQ

Hankstenberg14:10:15

Hi all, I'm trying to put a function invocation into a map for later execution in clojurescript. So something like:

({:id :task :eval '(println "test")}
In Clojure I could eval the value of :eval later, is there some way to do this in Clojurescript too?

p-himik14:10:00

Will you ever be serializing that map?

Hankstenberg14:10:44

Good question. Yes, possibly.

Lone Ranger14:10:16

#sci could do this too

🙌 1
Lone Ranger14:10:37

With some caveats and bookkeeping

Hankstenberg14:10:28

Yea, but I'm looking to run this in the browser.

Lone Ranger14:10:27

That’s the best part

Lone Ranger14:10:31

it works in the browser

Lone Ranger14:10:37

Even with :advanced compilation

Hankstenberg14:10:36

Wow indeed. 😮

Hankstenberg15:10:50

So this works for anything that can be run by sci, correct?

Hankstenberg15:10:07

Not for functions for the surrounding js namespace.

p-himik15:10:27

If you don't really need to serialize it, then just store a function as a function:

{:id :task, :eval #(println "test")}
If you do need to serialize it, then there are two approaches: • Come up with a fixed registry of functions, assign each function an ID, and use that ID instead of the function - you'd have to make a run-time lookup in that registry • Use some soft of self-hosting - either with CLJS itself or with the aforementioned sci, or maybe with something else

1
Hankstenberg15:10:26

Yes, I'm going with the first approach as I write this. I even called it "registry" too.

Lone Ranger15:10:45

You can load in functions from the surrounding namespace

Lone Ranger15:10:56

Just need to specify the execution context

Lone Ranger15:10:05

Can also whitelist/blacklist functions

Lone Ranger15:10:12

It’s pretty sweet

Hankstenberg16:10:05

Wow.. but it can not execute anything JavaScript, right? But still, super impressive, I already have a use case for that!

Lone Ranger17:10:38

Can totally execute javascript if you want 😅

Lone Ranger17:10:20

The only thing it CAN'T do super well is use macros defined OUTSIDE of sci

Lone Ranger17:10:42

there are workarounds but loading entire libraries is not always possible

Lone Ranger17:10:06

notably, that makes core.async difficult. However, there are a lot of workarounds for that

Hankstenberg07:10:36

Wow, I didn't know sci was that cool tbh. I think I have a great use case for it in mind!

Lone Ranger17:10:01

Does anyone have any thoughts on best practices for making an open source clojurescript library designed to wrap an existing javascript npm library? Specifically, I don't like the idea of bundling the npm dependency with the library itself -- or perhaps it could be, but only as a feature flag. It could introduce quite a lot of bloat and prevent folks from using their preferred tools. However, it's unclear to me what best practices would be, syntactically and structurally, for referring to a user provided external dependency. For instance: One option:

(ns something.core
  (:require mynpmdep))
Another option:
(ns something.core)
(def mynpmdep js/mynpmdep)
Another option:
(ns something.core
  (:require [mynpmdep.core :refer [mynpmdep]))
;; `mynpmdep` is the actual javascript object
Any thoughts?

Lone Ranger17:10:01

Talking to myself here, #1 seems the cleanest, but that would require users to understand, effectively, how to package a cljsjs dependency unless there's already an existing cljsjs package. :thinking_face:

p-himik18:10:32

Two things: • Just don't use cljsjs, don't rely on it - it's pretty much a thing of the past with the current tools, at least the way I see it • This particular problem is discussed every 2-3 months, should be easy to find descriptions of solutions The most reasonable that I remember and gravitate towards myself is just specifying in the README how your library should be used, including telling the user to install a specific NPM package.

Lone Ranger18:10:16

Interesting. :thinking_face: I'll do some searches

dvingo18:10:00

Yea, would recommend just specifying in the README which packages to install from npm. I do that for two styling libs: https://github.com/dvingo/cljs-styled-components https://github.com/dvingo/cljs-emotion

dvingo18:10:02

in the library code you just include it from npm ["@emotion/styled" :as styled*] if a user goes to use the lib they will get a compile error if the library isn't present

Lone Ranger18:10:23

But that also means then that user needs to compile it and can't have it be loaded via script tag, yes?

Lone Ranger18:10:59

Part of what's frustrating to me is I'm getting super annoyed at the gigantic size of these clojurescript builds

p-himik18:10:44

Including it via a script tag has its downsides. And to deal with a build size, you should be using module splitting.

Lone Ranger18:10:07

Agreed, but AFAIK module splitting doesn't do much to deal with the npm deps, which is where the bulk of the size is

p-himik19:10:44

If you load your NPM deps via a script tag, you will be loading the same exact total size.

p-himik19:10:11

Well, not exact exact, but, as the docs say, "comparable (or often better)": https://shadow-cljs.github.io/docs/UsersGuide.html#js-provider

Lone Ranger20:10:42

Agreed -- but there are options for splitting up the npm deps bundle and loading the relevant javascript on demand if you are willing to forgo baking them in with your clojurescript

Lone Ranger20:10:29

There's no reason to burden users with all that unnecessary code over the wire before the page even loads

p-himik20:10:47

> there are options for splitting up the npm deps bundle and loading the relevant javascript on demand That's exactly what module splitting does in CLJS.

p-himik20:10:03

I'm not talking about namespaces here, I'm talking about modules - a completely different thing.

Lone Ranger21:10:47

No, I know what you're saying. What I'm saying is that the clojurescript code takes up a fraction of the bundle size -- the npm deps take up the majority. Code splitting the clojurescript is also a good idea but it doesn't do anything to reduce the size of the npm-bundle. Even when you module split, all the npm deps go in the cljs_base.js artifact. And there's no way I know of to subdivide the npm deps out of the cljs_base.js artifact. So the result is that the majority (over 95%, in my case) of the size still lives in the cljs_base.js artifact, even if I'm careful to split out as much as possible into modules.

Lone Ranger21:10:53

You can code split out the npm deps, but you still have to bake them all in during compilation, so there's no point in splitting out npm deps.

Lone Ranger21:10:40

(I would love to be wrong about this, btw)

p-himik21:10:59

Why would an NPM dep go to the base artifact if only one of the CLJS modules uses that dep? I just tried it - split a test app into 3 modules: base, main, ui. Only the ui module depends on React. And in the end, only the ui module contains it - the base one doesn't contain it.

p-himik21:10:11

So it seems to me that it works pretty much as intended.

Lone Ranger21:10:58

only the UI module contains the React code?

Lone Ranger21:10:03

How did you verify that?

p-himik21:10:57

grep react *. :)

Lone Ranger21:10:06

interesting :thinking_face:

p-himik21:10:48

The trick is that only the ui module should depend on React. If some other module depends on it as well, React will be put into the base module.

p-himik22:10:27

Just as per the docs: > The :base module declared an empty :entries [] vector which is a convenience to say that it should extract all the namespaces that both of the other modules share (eg. cljs.core in this case).

Lone Ranger22:10:18

Interesting. So that should in turn apply if I'm using reagent? So for instance, if you were to have your ui module depend on Reagent, and Reagent depends on React, React should still only show up in the UI code?

p-himik22:10:31

So if you have e.g. modules A, B, C, and base, where only A and B depend upon some NPM library N, then don't just require it there - instead, add a third module, AB-common, and make it an intermediate dependency of A and B.

p-himik22:10:52

> [...] React should still only show up in the UI code? Yes. That's exactly what happened in my case - I used React via Reagent.

p-himik22:10:21

As soon as I added "react" import to the main module, React has moved to the base module. But Reagent has stayed in the ui module.

Lone Ranger22:10:05

I wonder if I was just code splitting wrong when I tested this or if I finally found the one thing that I can't get figwheel to do properly that shadow is doing properly

p-himik22:10:23

In other words, dependencies move to the closest common module. If only one module depends on something - that dependency will be in that module. If two modules depend on something - it will be in the closest "parent" of the two.

Lone Ranger22:10:25

I've managed to avoid switching tooling for like 5 years

p-himik22:10:42

Can't answer to that question, I haven't used Figwheel for around the same 5 years or so.

Lone Ranger22:10:03

Ok so I just want to verify

Lone Ranger22:10:10

you had a big index.bundle.js

Lone Ranger22:10:14

and then you module split as per the docs

Lone Ranger22:10:26

and the compiler intelligently moved the code to the correct places

Lone Ranger22:10:38

or did you do your npm deps some other way

Lone Ranger22:10:48

did you use webpack to make the npm deps?

p-himik22:10:09

NPM deps are used in the most regular way you'd use them with shadow-cljs. I've never used webpack in my whole career. :)

p-himik22:10:28

Just follow the module splitting guide in the shadow-cljs documentation. It just works.

Lone Ranger22:10:22

I think that's the major difference

p-himik22:10:38

Also, shadow-cljs has build reports. On this pic you can clearly see what goes where.

Lone Ranger22:10:00

I was thinking it would be virtually impossible to split up a webpack bundle, my brain was doing summersaults thinking about how one would parse index.bundle.js and assign dependencies to clojurescript dependencies

Lone Ranger22:10:15

so that means that shadow is integrating the npm build step instead of webpack

Lone Ranger18:10:30

So I was unable to replicate those results

Lone Ranger19:10:12

Also I'm noticing in the build report you displayed, react is going in the :base:

Lone Ranger19:10:21

@U05224H0W can you possibly weigh in on this?

thheller19:10:29

you can click the + in front of react and hover of the files

thheller19:10:48

it'll tell you what required it, and thats usually the clue to how it got there

thheller19:10:54

can't say anything else without seeing code/build config

Lone Ranger19:10:17

ok, that did. I was able to replicate the result. Dang, thanks. You saved me tons of work.

p-himik20:10:13

Ah, I missed that module splitting is a bit different from code splitting. But the module splitting guide worked for me just as well.

jpmonettas19:10:18

Is there a way of getting a stack trace with source mapping?

(try (throw (js/Error. "Dummy"))
       (catch js/Error e
         (js/console.error e)
         (tap> (.split (.-stack e) "\n"))))
If I do that I see that the console adds the mappings, but my tap contains just the js stack trace. Is there a way of using the source mapping functionality from the runtime?

p-himik19:10:52

You'd have to use some third-party package. A quick search shows https://github.com/mozilla/source-map/ and https://github.com/JaneaSystems/convert-sourcemap-stacktrace but there are probably others.

jpmonettas19:10:38

I see, but it will require the cljs code to know the location of a source map also

jpmonettas19:10:19

it would be nice if the browser provided that functionality, I mean, it can automatically do it for the console.error

Lone Ranger16:10:06

yeah. the stack traces in cljs are slightly better than "segfault, core dumped"... but not much

Lone Ranger16:10:17

especially when using core.async 😓

Lone Ranger16:10:28

but I do love me some core.async

schadocalex08:10:15

Don’t catch the error (or throw it again) and you’ll be able to see the stack trace in the browser debugger with source map

jpmonettas11:10:56

@U01V2D5ALKX I can already see the error stacktrace source mapped on the browser console. What I'm trying to accomplish is to obtain it as data

👍 1
Lone Ranger13:10:52

Do you control the error @U0739PUFQ? Could you throw ex-info instead of js/Error? much easier to capture the error data that way

jpmonettas13:10:58

@U3BALC2HH what is the difference? What I'm trying to do with that piece of code is just to get a stack trace as data, the throw is catched immediately on the wrapping expression

Lone Ranger13:10:34

Ok my bad, I didn't know wanted the stack trace specifically, I thought you just wanted data X_d from the exception, in which case you could (throw (ex-info some-message some-hashmap)) and then

(catch :default e
  (do-something-with (ex-data e)))

jpmonettas13:10:59

yeah, my question was how to obtain the current stack trace source mapped

schadocalex13:10:04

Sorry I misread your question. I think this is not a clojure issue, and it seems the only solution is to use libraries, there's no native API for that. I think https://github.com/stacktracejs/stacktrace.js would fit your needs, but there's others.

jpmonettas13:10:14

hey np yeah, but that requires to create a request and hit some service exposing the source mapping, I was just asking if the browser provided that info some how, given that it is already using it for the console.log

schadocalex14:10:40

This is directly the browser that overrides JS outputs, the JS engine doesn't have the source map. The browser do itself the required source maps requests. Maybe with inline source mapping you can avoid additional requests.

schadocalex14:10:41

I mean the browser don't magically get the source map, if there are external source maps it also "requires to create a request and hit some service exposing the source mapping"

Lone Ranger14:10:13

If this is an internal tool, it might be possible to use a browser extension to capture this, since it gives more access to browser facilities outside normal js facilities