Fork me on GitHub
#clojurescript
<
2022-09-14
>
Chris McCormick01:09:30

Is anybody using GitHub actions to run their compiled cljs tests? I am trying to figure out the best way to set up a test runner that first compiles the code and then runs the tests with node.

Chris McCormick02:09:00

If anybody is looking for a solution in future I got GitHub actions CI running the shadow-cljs tests: https://github.com/chr15m/sitefox/blob/main/.github/workflows/tests.yml

John David Eriksen15:09:41

Any folks here who have experience with both use of JavaScript promises and also re-frame? With promises I was used to being able to chain several HTTP calls that depend on each other being completed in order using promises. For example, I might want to wait to fetch resources X, Y, and Z before using their return values to place an HTTP request to resource A. I might use Promise.all() to wait for resources X, Y, and Z to be fetched: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all While learning re-frame I saw that dispatch-sync could be used to dispatch events synchronously and I thought that perhaps I could use this function to replace my previous usage of promises. Turns out that the documentation generally discourages using dispatch-sync except in specific cases. How do you program your re-frame app so that you can enforce that certains groups HTTP calls complete in a specific sequence?

p-himik15:09:16

With promises, you don't wait, you never wait. You schedule work. "Do this, .then do that, .then something else." In re-frame, there's nothing built-in. But there are at least 2 libraries that provide an effect for HTTP requests. Using their code as an inspiration, you can easily write your own effect that support issuing multiple HTTP requests and then dispatching a new event once all those requests are completed. So it would be something like

(rf/reg-fx ::fetch-all ...)

(rf/reg-event-fx ::button-clicked
  (fn [_ _]
    {::fetch-all {:urls [url-x, url-y, url-z], :on-success [::-then-fetch-a], ...}}))

(rf/reg-event-fx ::-then-fetch-a
  (fn [_ [_ result-x result-y result-z]]
    {::fetch-all {:urls [url-a], ...}}))
As an alternative, you can use one of those 2 aforementioned libraries to just issue the requests, and the waiting for all 3 resources will be done at the re-frame level. But it's a messy solution where you have to count successful requests in your app-db.

👍 1
p-himik15:09:20

Also, there's #re-frame

Lone Ranger17:09:29

@U03SF5VUF6Y, in my opinion this is a bit of a limitation on the re-frame framework. I think the best you can do is stuff this into a reg-fx. I can be more specific if you like. Also you may want to consider taking advantage of some clojurescript constructs for concurrency instead of promises. I realize what you asked for was a re-frame specific answer -- this is not that. I have had some luck in attempting to get https://github.com/day8/re-frame-http-fx to do the job, but you will be forcing the issue to some extent.

👍 1
John David Eriksen18:09:34

Thanks p-himik and unbalanced. I will take a look at using reg-fx. I was hoping for something that would let me compose dispatch calls together in a way similar to how I am used to working with promises rather than having to create a specific handler for each case where I want to ensure that I can schedule some asynchronous work and then only schedule additional work once the previous set of tasks has completed.

p-himik18:09:38

Yeah, not possible since dispatch has no return value. You're supposed to compose actions from within (either by hard-coding stuff or by adjusting relevant event handlers so they accept follow-up event vectors), not from the outside (like it's done with promises). There is a way to compose actions from the outside by basically peeking into re-frame's machinery. But I wouldn't recommend even looking in that direction.

Lone Ranger18:09:38

@U03SF5VUF6Y the other part of the puzzle is using this library: https://github.com/day8/re-frame-async-flow-fx this will accomplish "wait until I've seen dispatches X, Y, and Z before performing action Foo. It's not perfect because you also need to specify X-fail|Y-fail|Z-fail and then Foo-fail . This also doesn't exactly easily cover usecases like pagination. However, it is a coherent framework for colating data from known, non-dynamic sources.

Lone Ranger18:09:15

And personally at this point is when I stopped using re-frame altogether and just switch to reagent + core.async.

p-himik18:09:27

That library is exactly what I was referring to in the "wouldn't recommend even looking in that direction". :D

p-himik18:09:03

By searching for its name in #re-frame you can find plenty of discussions and descriptions of good reasons not to use it.

John David Eriksen18:09:18

Cool, taking a look at core.async now.

Lone Ranger19:09:09

If you end up going down that route, Eric Normand has several really friendly video courses on it. That being said -- core.async is not as friendly to debug as promises. I will tell you that the punchline is, 1. do some async work with core.async 2. update your re-frame.db/app-db or some reagent/atom with the result of the core.async work. 3. your page will update accordingly. Updating the page state is your "exit plan" from core.async, otherwise you'll be like, how do I ever get out of this? Because one you start down the core.async pathway it seems like everything needs to be a core.async function and you don't really want that. want to work in sync-land as much as possible. /opinion

James Amberger19:09:41

Good thread. Can it be moved/linked to #re-frame?

John David Eriksen21:09:08

@U032EFN0T33, a link to this thread has been shared to #re-frame Thanks!

Lone Ranger17:09:30

Curious if anyone has figured out a good solution for dynamic runtime dependency injection in ClojureScript that allows for extensible polymorphic behavior. Consider this scenario:

(defn save-doc! [doc]
   (if (implements? proto.doc-manager/IDoc doc)
      (proto.doc-manager/-save-doc! doc)
      (throw (js/Error. (str "Doesn't support IDoc: " doc)))))
where
(defprotocol IDoc 
  (-save-doc! [this] [manager parent]))
and each doc can implement
(defrecord SomeDoc [doc-data]
  IDoc
  (-save-doc! [this]  
       ...))
buuuuttt... this behavior should be different if it's in a web worker or the main thread. So the classic way to do this is
(defrecord SomeDoc [doc-data save-doc-manager]
   IDoc 
   (-save-doc! [this]
      (-save-doc! save-doc-manager doc-data)))
then at runtime based on feature detection you can reify the correct save-doc-manager. However, extrapolated, this amount of code repetition for various doc types gets absolutely obnoxious when you have lots of different attributes. In Clojure, the extend function does wonders because you can do this programmatically. However, in ClojureScript we don't have this available so it's pretty tricky, even with macros. Curious if anyone has a clean solution for this?

dnolen18:09:45

the problem is that there are no interfaces in JavaScript so covering a bunch of types is not really practical.

dnolen18:09:26

if you have a bunch of docs types, one trick that might work is to set the prototype on a record w/ no methods.

dnolen18:09:00

then you could modify the prototype at runtime and all those types would get the change you need

Lone Ranger18:09:22

No interfaces in javascript :scrunched-up-face: whatttt

dnolen18:09:24

another trick would be specify! which allows you to alter instances

😮 1
dnolen18:09:54

not possible in Clojure, but possible in ClojureScript

Lone Ranger18:09:56

yeah taking a second to wrap my head around that technique

Lone Ranger18:09:05

Modifying the prototype doesn't allow you to get "abstract base class"-like behavior, though, right? Only modify the signatures? Still no "default" behavior, I'm assuming?

dnolen18:09:39

if you’re just looking for default behavior you can use extend-type default then in the implementation check what you need to check and call the right implementation

Lone Ranger18:09:07

(defprotocol IFoo
  (foo [this] [this that]))

(let [foo-do (reify IFoo (foo [this] "this"))]
  (println (foo foo-do))
  (println (foo (specify! foo-do IFoo (foo [this] "that"))))
  (println (foo foo-do)))
this is the idea right? and I did end up going the extend-type route in the current implementation, it's still a little messy.
(defmacro extend-types [types & specs]
  (let [impls (parse-impls specs)]
    (list* 'do
           (map
            (fn [protocol]
              (let [spec       (get impls protocol)
                    next-specs (list* 'extend-protocol protocol
                                      (mapcat
                                       (fn [type]
                                         (conj spec type))
                                       types))]
                next-specs))
            (keys impls)))))
and then using that to extend a lot of the behavior. Not perfect but does save some boiler-plate.

Lone Ranger18:09:15

I actually considered doing something insane like loading up all the code into a self-hosted interpreter in a webworker and shipping over extend-type code literals, getting back the resulting code as a string, and then dynamically loading it into the calling thread via script but at that point I assumed I was starting to lose my mind

dnolen18:09:17

not exactly

dnolen18:09:04

(extend-type default IFoo (foo … (if (satisfies IDoc? x) (specify! (.-prototype x) IDefault …) …)))

dnolen18:09:18

you have to know a little bit about JavaScript but something like this should work

Lone Ranger18:09:37

ohhhhhhh very clever

dnolen18:09:57

right the first time some protocol get called you fix up anybody you care about

dnolen18:09:02

because JS is dynamic this can work

Lone Ranger19:09:45

Okay so, if I were to draw this out into a more complete example, something like this (?):

(defprotocol IDoc
  (save-doc! [this]))

(defrecord Doc [])

(defmulti specify-runtime-behavior! :runtime)

(defmethod specify-runtime-behavior! :main
  [runtime-opts]
  (specify! (.-prototype Doc)
            IDoc
            (save-doc! [this] "sent to webworker!")))


(defmethod specify-runtime-behavior! :webworker
  [runtime-opts]
  (specify! (.-prototype Doc)
            IDoc
            (save-doc! [this] "saved to database!")))

(defn -main []
  (let [runtime-opts (detect-features!)]
    (specify-runtime-behavior! runtime-opts)))
Wasn't totally following the "configure on first call" technique but I'm loving the specify->protocol technique.

dnolen19:09:12

yeap that works too - much simpler

Lone Ranger19:09:02

word, thanks!

Lone Ranger18:09:37

(and I'm pretty sure that's by design because I know inheritance is the 👿 and everything)

p-himik18:09:37

Could you please use a 🧵 ? The yesterday's discussion went for well over an hour and spans multiple screens - hard to participate in channels where it happens.

👍 1
Ben Lieberman18:09:45

I'm doing the React tutorial and I suspect I'm doing something dumb with repeatedly but I cannot get these click handlers to work

(defn square []
  (let [state (r/atom "")]
    [:button {:class-name "square"
              :on-click #(reset! state "X")}
     @state]))

(defn board []
  (let [status (r/atom "Next player: X")
        render-square (fn [] (square))]
    [:div
     [:div {:class-name "status"} @status]
     (doall (map (fn [_] [:div {:class-name "board-row"}
                    (repeatedly 3 render-square)]) (range 1 4)))]))

p-himik18:09:44

Don't define ratoms in form-1 components.

p-himik18:09:17

Otherwise, your state gets erased on every re-render of those components. Use a form-2 or -3 component, or reagent.core/with-let.

p-himik18:09:26

Also, there's #reagent

CarnunMP18:09:47

Hi again! To follow up on my recent post (and hopefully to provide more context/useful information this time 😛 )... It's no longer possible to develop Chrome extensions in ClojureScript (suitable for publishing). At least, the wonderful shadow-cljs + https://github.com/binaryage/chromex/tree/master/examples/shadow combo no longer works, as the https://github.com/thheller/shadow-cljs/blob/49fb078b834e64f63122e3a8ad3ddcd1f4485969/src/main/shadow/build/targets/chrome_extension.clj build code that https://github.com/binaryage/chromex/blob/27609a7025466d8b9bfbb28baa061ea8f51fb807/examples/shadow/shadow-cljs.edn#L4 generates Manifest V2 extensions only (is my understanding?), and https://developer.chrome.com/docs/extensions/mv2/publish_app/. Fair enough... as Chrome extensions were always a pretty niche use case and building/packaging them is an undocumented feature of shadow. Still, it would be amazing to be able to continue developing Chrome extensions in cljs! At https://nette.io/ we've been working on an extension that gives you fancy bookmarking, keeps track of research trails as you surf the web, lets you capture media, etc—it’s pretty cool 😉—integrated with the main Nette app, so porting it to plain JavaScript (say) would be a not entirely trivial thing. We’re now looking into publishing to the Web Store, so we're trying to tackle the migration from V2 -> V3. None of us are browser extension experts — so we'd be really grateful for help from people that know a bit more about: • Chrome extensions in general, and • shadow('s building of them) in particular! I put together a minimal, rather hacky example of kinda sorta migrating the https://github.com/binaryage/chromex/tree/master/examples/shadow using a build hook to patch the generated manifest.json... plus a few more tweaks. The README lists the changes made and why, as well as a few questions that came up along the way. (And each change is also implicitly a question: 'Is there a better way to do this?' 😛 ) You can find it here if you're interested: https://github.com/nette-io/hacky-chromex-shadow-manifest-v2-to-v3-migration. It seems to us that https://github.com/thheller/shadow-cljs/blob/49fb078b834e64f63122e3a8ad3ddcd1f4485969/src/main/shadow/build/targets/chrome_extension.clj is where the relevant magic happens? In which case a patch there would mostly solve things, maybe. Unless we've missed something?

CarnunMP18:09:43

Cc: @U051V5LLP, you might be interested in these, uh, developments since the last thread. :))

CarnunMP18:09:20

And of course, cc: @U05224H0W and @U08E3BBST! 🙂

thheller06:09:57

if you want changes in shadow-cljs please take this to a shadow-cljs issue. and ideally just make a list of changes that need to happen. I can easily add them, I just don't have the time to figure out what those changes are.

👍 1
thheller06:09:38

if you are considering writing a patch please create a new target as a copy of the old chrome-extension. this is likely going to break stuff for v2, just in case anyone is still using that

👍 1
craftybones06:09:28

Is #scittle an option?

CarnunMP08:09:20

Hmm, alrighty! Cheers @U05224H0W... a list of changes or a patch/new target incoming. Once we've worked through all the details of our own migration. :))

CarnunMP08:09:08

> Is #scittle an option? Say more @U8VE0UBBR... :thinking_face: My experience with Scittle is pretty minimal. Can you require cljs libs outside of the https://babashka.org/scittle/?

craftybones05:09:12

@U018D6NKRA4 - no, I'm afraid not. I missed that this was your requirement

CarnunMP12:09:11

Ah, no worries @U8VE0UBBR :))

Lone Ranger19:09:39

Does anyone understand the trick being employed when this happens?

(ns foo.core
   (:require some-npm-dep))

;; how can a namespace also be an object??
(println (some-npm-dep. "new object"))

dnolen19:09:22

it’s to satisfy a JS pattern

Lone Ranger19:09:37

This ties into the code splitting conversation from yesterday -- basically I'm trying to replicate that behavior with a clojurescript namespace. I've moved all of my npm deps into external dependencies and setup externs appropriately. Now I'm loading, let's say, react, via

// src/js/components/react-deps.js
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Holdable, defineHold } from 'react-touch';
window.ReactTouch = {};
window.ReactTouch.Holdable = Holdable;
window.ReactTouch.defineHold = defineHold;
which gets compiled via webpack and loaded in index.html via
<script src="js/compiled/dist/react_deps.bundle.js" type="text/javascript"></script>
I've then provided
(ns cljsjs.react)

(def react js/React))
which allows reagent to be satisfied when it asks for react here: https://github.com/reagent-project/reagent/blob/master/src/reagent/core.cljs#L4 However, the behavior does not quite line up as it does with true cljsjs files, since react is not an object. When I tried to do the same trick with pouchdb,
Use of undeclared Var app.lib.data.pouch/pouchdb                                                                                                                                                             
                                                                                                                                                                                                                   
   6     pouchdb ))                                                                                                                                                                                                
   7                                                                                                                                                                                                               
   8                                                                                                                                                                                                               
   9                                                                                                                                                                                                               
  10  (defn make-local-pouch []                                                                                                                                                                                    
  11    (pouchdb. "local" #js {:skip_setup true                                                                                                                                                                    
        ^---                                                                                                                                                                                                       
  12                           :adapter    "memory"}))                                                                                                                                                             
  13                                                                                                                                                                                                               
  14  #_(.. pouchdb                                                                                                                                                                                                
  15        (plugin (.. js/window -pmemory))                                                                                                                                                                       
  16        (plugin (.. js/window -auth -default))         
(again, the point of all this rigamaroll is trying to get the cljs_base.js size appropriately small using the appropriate tools to split out the npm deps) Because this almost lets me get the same compiler benefits as compiling the npm deps directly in advanced compilation -- I just need to figure out how to get a clojurescript namespace to behave as an object and I will not have to change any of my existing code in order to satisfy the new build requirements (yay!). Of course if I do have to make some code changes that's not the end of the world.

Lone Ranger20:09:28

also I swear this is my last question

Lone Ranger20:09:31

for at least a month

lol 1