Fork me on GitHub
#beginners
<
2024-02-26
>
Tommi Martin11:02:52

Hello there, quick question. If you use jetty, ring websocket, overtone.at-at and an atom something like this: • Create a handler for jetty that is dedicated for websockets. • Inside the handler when a websocket is called. store the websocket into an atom (conj into vector) and start a 3 loop with overtone at at. • Every 3 seconds, read the websockets from the atom and publish a message into each socket. While the code technically works. When implemented like this the message is only published to the first socket, all other sockets are in closed or closing state according to websocket/closed? It seems that this happens either because of the conjoin or at read time but I can figure out why? The goal of the code is to publish the same information from to multiple sockets at once. Am I designing my code wrong or not taking into account something basic when working with sockets and atoms? Example code in a thread (excluding the jetty setup)

Tommi Martin11:02:50

(ns app.handlers
  (:require
   [jsonista.core :as j]
   [overtone.at-at :as at]
   [ring.websocket :as ws])

(def websockets (atom []))
(def interval-running (atom false))

(defn publish-update
  []
  (doseq [socket @websockets]
    (when (ws/open? socket) ; only the first entry in the vector returns true?
      (ws/send socket (j/write-value-as-string {:msg "stuff"})))))

(defn create-interval
  []
  (when (= false @interval-running)
    (at/every 3000 publish-update overtone-pool :initial-delay 3000)
    (reset! interval-running true)))

(defn websocket [request]
  (assert (ws/upgrade-request? request))
  {::ws/listener
   {:on-open
    (fn [socket]
      (prn "New socket:" (ws/open? socket)) 
      (swap! websockets conj socket)
      (prn "socket conjoined:" (ws/open? socket)) ;; returns true for every connection.
      (create-interval) 
      (ws/send socket (j/write-value-as-string {:msg "Starting updates"})))
    :on-message
    (fn [socket message]
      (if (= "exit" message)
        (ws/close socket)))}})

sheluchin21:02:29

It seems a bit awkward that only a single :main-opts is taken when using multiple deps.edn aliases. How do people get around this when activating multiple aliases that require :main-opts?

Noah Bogart21:02:10

what would it mean to run multiple -main functions?

sheluchin21:02:55

Is that how you think of it? I thought of it more like passing multiple arguments to your one -main fn.

Noah Bogart21:02:12

you have to specify the namespace with -m, so merging them would mean you're specifying multiple namespaces

Noah Bogart21:02:05

if it was like :main-ns noah/some-ns :main-opts ["arg1" "arg2"] then i could see it working to combine the :main-opts. but as it is, you're doing both, you're specifying the target namesapce to run the -main of, and you're supplying a base set of arguments

sheluchin21:02:12

Ah, I see. I did not notice that every :main-opts includes a --main/`-m`. The https://clojure.org/reference/deps_edn#aliases_mainopts does not go to that depth intentionally, I guess.

Noah Bogart21:02:51

oh interesting, it's not required? i'm not sure how you'd run something without specifying it, tho

seancorfield21:02:32

It accepts any clojure.main options. That includes -e, -i, -m, and -r etc.

👍 1
seancorfield21:02:20

But you can only combine some of those -- and anything that follows -m some.ns is treated as a command-line arg for some.ns/-main.

seancorfield21:02:21

@UPWHQK562 What is your specific use case here? i.e., what :main-opts would you want to combine?

sheluchin22:02:54

I ran into problems because I have a global deps.edn alias for Portal (with nREPL middleware added in :main-opts) but a project I'm working on uses shadow-cljs and I think a conflict happened there. The explanation I found for it is here https://clojurians.slack.com/archives/C6N245JGG/p1671011984378119?thread_ts=1671006295.467309&amp;cid=C6N245JGG but I'm not sure I fully understand how to use aliases now.

seancorfield22:02:39

Aliases always "combine" but the rules depend on what specifically is in the alias. :main-opts is the odd-one-out as it is "last one wins" but the rest either concatenate or merge. So if Shadow is starting its own nREPL server, you somehow need to tell it what additional arguments need to be passed to nREPL (no idea how to do that -- I do not use Shadow). The general guidance is to try to avoid combining :main-opts and dependencies (`:extra-paths`, :extra-deps) by using separate aliases, so you might have :test:runner where :test provides the paths/deps and :runner provides the :main-opts -- that lets you specify :test in a situation where some tooling adds its own main opts (as Shadow does). But that doesn't help you when you need to pass additional command-line arguments to some -main function via an alias (such as middleware for nREPL). There's no good solution for that.

sheluchin22:02:03

I have not encountered that piece of guidance. That's good to know and I will make a note of it. So aliases that tell the program what it needs to run should separate from the aliases that tell the program how it needs to run. Does that sound like a correct take on it? Then a concrete example might be that your running your tests is a completely different function from running your application for actual usage.

👍 1
sheluchin22:02:10

but looking at https://github.com/practicalli/clojure-cli-config/blob/66d7d37aa38080e8f02dc977aab55342f5aabdb5/deps.edn it looks like many or even most of the aliases that use :main-opts also specify extra dependencies.

seancorfield22:02:57

Well, the :test:runner example is the one I like to use since you might want clojure -M:test:nrepl to start an nREPL server with your tests and their dependencies on the classpath or clojure -M:test:runner to actually run the tests.

seancorfield22:02:29

If you never use the dependencies except when you need :main-opts then there's no need to separate them. :nrepl is probably a reasonable example: you're very unlikely to ever want the nREPL deps on your classpath unless you are starting an nREPL server, i.e., using :main-opts for it. But :portal can provide your deps without :main-opts -- the problematic issue is when you want to specify (nREPL) middleware for Portal -- but that's a general issue with nREPL and middleware. It's why my :dev/repl alias looks to see what's on the classpath when its :main-opts are invoked and it then adds whatever middleware it thinks is available: https://github.com/seancorfield/dot-clojure/blob/develop/deps.edn#L52-L62 and https://github.com/seancorfield/dot-clojure/blob/develop/src/org/corfield/dev/repl.clj#L122-L132

sheluchin22:02:32

Okay, that took a bit of thinking. Your start-repl fn looks quite sophisticated and I'll probably shy away from using something similar for now, but it's a good case to understand the problem. If my understanding is right, the root of the issue (and it's a fairly common one) is that tooling which leverages nREPL often needs to inject tool-specific middleware, and that can only be done through :main-opts. It's a problem without a clean solution, because you may want to use multiple tools that interface with nREPL, but you can't just use multiple aliases for that, since their need to include a custom middleware always clashes.

seancorfield22:02:28

Yup, that's an accurate summary.

sheluchin23:02:31

Is it mostly isolated to dev tooling?

sheluchin23:02:37

In any case, thanks very much for the help on this topic, @U04V70XH6 and @UEENNMX0T.

👍 1
seancorfield23:02:24

I think it mostly affects dev tooling, yes. I wonder if there's an argument in favor of having some sort of :main-args that would concatenate and be applied to the end of :main-opts? I'll post on http://ask.clojure.org

seancorfield23:02:46

Hmm, that wouldn't solve this problem since --middleware expects a single vector of symbols so you couldn't combine aliases to produce it even if there was a separate :main-args CLI option. For nREPL middleware, I think this would require changes to nREPL itself...