nbb

2024-09-05T09:09:27.173089Z

So I’m looking to use babashka and nbb together. In short one of the projects I’m working on requires me to use some node packages so nbb is perfect. However, the rest of the project is in babashka (with some java dependencies like datalevin). Is there a way to combine the two? I know we can shell out to the nbb scripts. However, the node packages api has a large surface area so that would be quite cumbersome. Ideally, I want to be able to call nbb functions (doesn’t have to be the same process) from babashka/clojure. The other option I’m looking into is graalvm’s polyglot mode (but that would be quite a bit more work). Any ideas?

borkdude 2024-09-05T09:12:15.490039Z

> node packages api has a large surface area Not sure what you mean here... I haven't actually tried this myself. GraalVM polyglot could be interesting, but I think the JS package there can't be very node-specific

2024-09-05T09:14:18.753379Z

As in there are several functions that we’d want to interleave with our other code. So it’s not trivial to write a single script to call, it would have to be multiple. If that makes sense?

borkdude 2024-09-05T09:15:06.720299Z

Perhaps you could do it via nREPL?

2024-09-05T09:15:28.842369Z

For now we can get buy. I’ve just been blown away with how magical nbb is in terms of making node actually usable. 🤣

borkdude 2024-09-05T09:15:35.802779Z

:)

2024-09-05T09:16:15.689109Z

Oh, that’s interesting, so start an nrepl server in nbb process and then call out to that, from babashka/clojure?

2024-09-05T09:16:31.257879Z

That’s exactly the mad science sort of answer I was looking for.

2024-09-05T09:16:51.008309Z

Where would be the best place to start?

borkdude 2024-09-05T09:17:23.023389Z

what you could do is either: Start nbb first with an nREPL server and then connect to it from the JVM to execute the stuff you want, or vice versa

2024-09-05T09:17:57.933799Z

That’s actually brilliant.

borkdude 2024-09-05T09:18:09.394449Z

I guess you could also spin up nbb's nrepl server from the JVM as a shell process

🤯 1
borkdude 2024-09-05T09:19:06.172849Z

you don't even need the nREPL protocol, you can do it via stdin/stdout via this API: https://github.com/babashka/nbb/blob/main/doc/api.md#nbbrepl

2024-09-05T09:21:00.704189Z

Thats even better, the exact sort thing I do in emacs all the time. Don’t know how I missed it.

2024-09-05T09:21:26.584059Z

I guess elisp has always been “the silly place”

2024-09-05T09:22:12.117249Z

Thank you!

borkdude 2024-09-05T09:23:15.721039Z

No problem, let me know if it works out or not

2024-09-05T09:24:22.875529Z

I’ll defo do a write up blog post if it does. 🎉

🎉 1
2024-09-05T13:13:02.775969Z

So here’s what I have so far, half working. A cljs nbb file.

(ns core
  (:require [nbb.repl :refer [repl]]))

(repl)
And in clojure land.
(ns app.nbb
  (:require [clojure.java.process :as p]
            [ :as io]))
            
(def nbb-process
  (p/start "nbb" "resources/nbb/core.cljs"))
  
(def stdin (-> nbb-process p/stdin))
(def ^java.io.Writer writer (io/writer stdin))

(def stdout (-> nbb-process p/stdout))
(def ^java.io.Reader reader (io/reader stdout))

(.write writer (str '(+ 3 4 5)))
What’s killing me is reading from streams as I have to detect the end of the response. Any tips?

borkdude 2024-09-05T13:15:08.705919Z

if only nbb had a prepl, this would be easier :) but perhaps as a workaround you write a special value and read it back

borkdude 2024-09-05T13:15:12.720659Z

as a delimiter

borkdude 2024-09-05T13:15:24.398609Z

nrepl is also more structured

borkdude 2024-09-05T13:16:27.570719Z

you could try to start the nbb nrepl server and then read values using this: https://github.com/babashka/nrepl-client

2024-09-05T13:18:04.390479Z

haha, yeah. Good shout with the special character. It’s more the pain of java low level read api. Can’t use spit/slurp as they close the connection, working out termination etc. maybe nrepl + nrepl client is the right level of abstraction, I’ll check it out.

2024-09-05T13:18:27.175699Z

Still amazed it kinda works.

borkdude 2024-09-05T13:19:14.235949Z

you can do something like this:

(binding [*in* stdout] (read-line))
and call read-line until you encounter the special value

👀 1
borkdude 2024-09-05T13:28:38.635509Z

Somehow I don't see any input here:

(ns app.nbb
  (:require [babashka.process :as p]
            [ :as io]))

(def nbb-process
  (p/process {:err :inherit} "npx" "nbb" "repl"))

(def stdin (-> nbb-process :in))
(def ^java.io.Writer writer (io/writer stdin))

(def stdout (-> nbb-process :out))
(def ^java.io.Reader reader (io/reader stdout))

(.write writer (str '(+ 3 4 5) "\n"))
(.flush writer)

(future
  (binding [*in* stdout]
    (while
        (when-let [line (read-line)]
          (println :> )
          line))))

@(promise)
but when I add :out :inherit I do see the REPL output...

👀 1
2024-09-05T13:36:01.536359Z

Yeah so I’m running into the same issue, where I can see the evaluation is happening with :out :inherit but I can’t seem to read normally as in it just blocks.

2024-09-05T13:36:47.216739Z

Assumed the blocking was the reader trying to read and not having anything to read (and not using a termination character).

borkdude 2024-09-05T13:37:39.929009Z

This program works (at least in bb):

(ns app.nbb
  (:require [babashka.process :as p]
            [ :as io]))

(def nbb-process
  (p/process {:err :inherit} "npx" "nbb" "repl"))

(def stdin (-> nbb-process :in))
(def ^java.io.Writer writer (io/writer stdin))

(def stdout (-> nbb-process :out))
(def ^java.io.Reader reader (io/reader stdout))

(.write writer (str '(+ 3 4 5) "\n"))
(.flush writer)

(.close writer)

(while
    (when-let [line (.readLine reader)]
      (println :> line)
      line))

👀 1
2024-09-05T13:39:02.559269Z

Works in clojure land too!

borkdude 2024-09-05T13:39:33.180689Z

something like this will make it more easily readable:

(.write writer (str ::begin "," '(+ 3 4 5) "," ::end "\n"))

2024-09-05T13:40:30.105479Z

but runs into the issue I was having before in that it closes the stream

2024-09-05T13:40:47.023299Z

which seems to kill the process

borkdude 2024-09-05T13:41:10.306339Z

This is because .close writer , I did that on purpose

borkdude 2024-09-05T13:41:17.365339Z

once you're finished with nbb, closing the reader kills the REPL

borkdude 2024-09-05T13:41:46.181679Z

but not closing it leaves the REPL open

2024-09-05T13:42:43.853269Z

right so now I just need to stop reading when :app.nbb/end is read so as not to block

2024-09-05T13:43:29.095179Z

(str ::begin "," '(+ 3 4 5) "," ::end "\n") <- this is a nice trick

borkdude 2024-09-05T13:43:38.391009Z

I'm not 100% sure what happens

borkdude 2024-09-05T13:43:49.013779Z

it seems node crashes with Error: write EPIPE sometimes

2024-09-05T13:45:48.819359Z

haven’t seen that crash yet 🤞

borkdude 2024-09-05T13:46:34.517109Z

This seems to kinda work in my case:

(defn read-val []
  (when-let [line (.readLine reader)]
    (println :> line)
    line))

(read-val) ;; repl greeting

(.write writer (str ::begin "," '(+ 3 4 5) "," ::end "\n"))
(.flush writer)

(read-val) ;; app begin
(read-val) ;; 12
(read-val) ;; app end

(.write writer (str ::begin "," '(assoc {:a 1} :b 2) "," ::end "\n"))
(.flush writer)

(read-val) ;; app begin
(read-val) ;; {:a 1 :b 2}
(read-val) ;; app end

borkdude 2024-09-05T13:47:03.084339Z

it seems kinda clunky. nrepl is probably more robust

2024-09-05T13:47:18.354229Z

Yeah, does feel like just re-inventing nrepl

borkdude 2024-09-05T13:47:34.358739Z

yeah or prepl which is also suited for this, but not implemented yet in nbb

2024-09-05T13:48:43.250179Z

I’ll play around with an nrepl version and see how that goes. Thanks for all the help!

👍 1
2024-09-05T13:59:16.982089Z

(def nbb-process
  (p/start "nbb" "resources/nbb/core.cljs"))

(def stdin (-> nbb-process p/stdin))
(def ^java.io.Writer writer (io/writer stdin))

(def stdout (-> nbb-process p/stdout))
(def ^java.io.Reader reader (io/reader stdout))
  
(defn execute [sexp]
  (.write writer (str ::begin "," sexp "," ::end "\n"))
  (.flush writer)
  (while
      (when-let [line (.readLine reader)]
        (when-not (re-find #":app.nbb/end" line)
          (println :> line)
          line))))

(execute
  '(ns core
     (:require
      ["@metaplex-foundation/umi-bundle-defaults" :refer [createUmi]]
      ["@metaplex-foundation/umi" :as umi]
      ["@metaplex-foundation/mpl-bubblegum" :as gum]
      [promesa.core :as p]
      [cljs-bean.core :refer [->clj]])))

(execute
  '(def umi-conn
     (-> (createUmi "")
       (.use (gum/mplBubblegum)))))
Not doing anything with the response. But this all works, can require and use packages that are in the resource/nbb/project.json. Pretty cool. prepl always returns a map right? That would make this sort of thing trivial.

borkdude 2024-09-05T14:22:21.172939Z

yup

2024-09-05T15:00:06.167809Z

Here’s the nrepl version works a treat package json:

{
  "dependencies": {
    "@metaplex-foundation/mpl-bubblegum": "^4.2.0",
    "@metaplex-foundation/umi": "^0.9.2",
    "@metaplex-foundation/umi-bundle-defaults": "^0.9.2",
    "@solana/web3.js": "^1.95.2"
  }
}
core.cljs:
(ns core
  (:require [nbb.nrepl-server :as nrepl]))

(nrepl/start-server! {:port 1337})
clojure land:
(ns app.nbb
  (:require [clojure.java.process :as p]
            [nrepl.core :as nrepl]))

(def nbb-nrepl
  (p/start "nbb" "resources/nbb/core.cljs"))

(defn execute [sexp]
  (with-open [conn (nrepl/connect :port 1337)]
    (->> (-> (nrepl/client conn 1000)
           (nrepl/message {:op   "eval"
                           :code (str sexp)}))
      (map nrepl/read-response-value)
      nrepl/combine-responses)))

(comment
  (try
    ;; Works but throws an exception as it can't read the response:
    ;; "Could not read response value: #object[Is core]"
    (execute
      '(ns core
         (:require
          ["@metaplex-foundation/umi-bundle-defaults" :refer [createUmi]]
          ["@metaplex-foundation/umi" :as umi]
          ["@metaplex-foundation/mpl-bubblegum" :as gum])))
    (catch Throwable _))

  (execute
    '(def umi-conn
       (-> (createUmi "")
         (.use (gum/mplBubblegum)))))

  ;; {:id "a73c3698-8d00-416d-9d21-42be56c0be7c",
  ;; :ns "core",
  ;; :value [#'core/umi-conn],
  ;; :status #{"done"}}
  )
  

2024-09-05T15:00:23.390299Z

Thanks again for all the help!

borkdude 2024-09-05T15:00:51.218529Z

awesome

borkdude 2024-09-05T15:01:15.454819Z

why not just start nrepl from the command line without the script? not that it matters

2024-09-05T15:01:43.334449Z

So I was having issues picking up the package json

borkdude 2024-09-05T15:02:11.828029Z

ah

2024-09-05T15:02:14.745079Z

it seemed to run in the wrong directory, and even passing :dir option in didn’t seem to fix it

borkdude 2024-09-05T15:02:21.180209Z

weird

2024-09-05T15:02:47.441369Z

yeah, probably just me not using the process api properly.

borkdude 2024-09-05T15:02:58.066869Z

looking forward to the blog post when you have time some day ;)

borkdude 2024-09-05T15:05:06.660669Z

you could probably re-use the nrepl client over multiple calls, but cool that you have it working

2024-09-05T15:05:24.444379Z

Yeah, got a backlog already, need to put one out on back pressure for virtual threads. Turns out infinite threads can be a bit of a pain when you have a slow consumer, who knew? Thank you for nbb! The library I never thought i’d need. The space I’m currently working in is just very auto generated JS SDK heavy and nbb means I can stay in my clojure happy place.

❤️ 1
2024-09-05T15:05:45.381529Z

Ahh yes that’s a good point.