(ns browser-server
;;original name: sci.nrepl.browser-server
;;original file https://github.com/babashka/sci.nrepl/blob/main/src/sci/nrepl/browser_server.clj

  (:require
   [bencode.core :as bencode]
   [clojure.edn :as edn]
   [clojure.string :as str]
   [org.httpkit.server :as httpkit])
  (:import
   [java.io PushbackInputStream EOFException BufferedOutputStream]
   [java.net ServerSocket]))
(set! *warn-on-reflection* true)
true
(defn- update-when [m k f]
  (if-let [v (get m k)]
    (assoc m k (f v))
    m))
#'browser-server/update-when
(defn- coerce-bencode [x]
  (if (bytes? x)
    (String. ^bytes x)
    x))
#'browser-server/coerce-bencode
(defn- read-bencode [in]
  (try (let [msg (bencode/read-bencode in)
             msg (zipmap (map keyword (keys msg))
                         (map coerce-bencode (vals msg)))]
         msg)
       (catch Exception e
         #_(def e e)
         (throw e))))
#'browser-server/read-bencode
(defonce ^:private !last-ctx
  (volatile! nil))
(defn send-response [{:keys [out id session response]
                      :or {out (:out @!last-ctx)}}]
  (let [response (cond-> response
                   id (assoc :id id)
                   session (assoc :session session))]
    (bencode/write-bencode out response)
    (.flush ^java.io.OutputStream out)))
(defn- handle-clone [ctx]
  (let [id (str (java.util.UUID/randomUUID))]
    (send-response (assoc ctx
                          :response {"new-session" id "status" ["done"]}))))
#'browser-server/handle-clone
(defonce nrepl-channel (atom nil))
(defn- response-handler [message]
  (let [{:as msg :keys [id session]} (edn/read-string message)]
    (send-response {:id id
                    :session session
                    :response (dissoc msg :id :session)})))
#'browser-server/response-handler
(defn- websocket-send! [msg]
  (when-let [chan @nrepl-channel]
    (httpkit/send! chan (str msg))))
#'browser-server/websocket-send!
(defn- handle-eval [{:as ctx :keys [msg session id send-fn] :or {send-fn websocket-send!}}]
  (vreset! !last-ctx ctx)
  (let [code (get msg :code)]
    (if (or (str/includes? code "clojure.main/repl-requires")
            (str/includes? code "System/getProperty"))
      (do
        (send-response (assoc ctx :response {"value" "nil"}))
        (send-response (assoc ctx :response {"status" ["done"]})))
      (send-fn {:op :eval
                :code code
                :id id
                :session session}))))
#'browser-server/handle-eval
(defn- handle-load-file [ctx]
  (let [msg (get ctx :msg)
        code (get msg :file)
        msg (assoc msg :code code)]
    (handle-eval (assoc ctx :msg msg))))
#'browser-server/handle-load-file
(defn- handle-complete [{:keys [id session msg send-fn] :or {send-fn websocket-send!}}]
  (send-fn {:op :complete
            :id id
            :session session
            :symbol (get msg :symbol)
            :prefix (get msg :prefix)
            :ns (get msg :ns)}))
#'browser-server/handle-complete
(defn- generically-handle-on-server [{:keys [id op session msg send-fn] :or {send-fn websocket-send!}}]
  (send-fn (merge msg
                  {:op op
                   :id id
                   :session session})))
#'browser-server/generically-handle-on-server
(defn- handle-describe [ctx]
  (vreset! !last-ctx ctx)
  (generically-handle-on-server (assoc ctx :op :describe)))
#'browser-server/handle-describe
(defn- session-loop [in out {:keys [opts]}]
  (loop []
    (when-let [msg (try
                     (let [msg (read-bencode in)]
                       msg)
                     (catch EOFException _
                       (when-not (:quiet opts)
                         (println "Client closed connection."))))]
      (let [ctx (cond-> {:out out :msg msg}
                  (:send-fn opts)
                  (assoc :send-fn (:send-fn opts)))
            id (get msg :id)
            session (get msg :session)
            ctx (assoc ctx :id id :session session)
            op (keyword (get msg :op))]
        (case op
          :clone (handle-clone ctx)
          :eval (handle-eval ctx)
          :describe (handle-describe ctx)
          :load-file (handle-load-file ctx)
          :complete (handle-complete ctx)
          (generically-handle-on-server (assoc ctx :op op))))
      (recur))))
#'browser-server/session-loop
(defn- listen [^ServerSocket listener {:as opts}]
  (println (str "nREPL server started on port " (:port opts) "..."))
  (let [client-socket (.accept listener)
        in (.getInputStream client-socket)
        in (PushbackInputStream. in)
        out (.getOutputStream client-socket)
        out (BufferedOutputStream. out)]
    (future
      (session-loop in out {:opts opts}))
    (recur listener opts)))
#'browser-server/listen
(defonce !socket (atom nil))
(defn start-nrepl-server! [{:keys [port] :as opts}]
  (let [port (or port 1339)
        inet-address (java.net.InetAddress/getByName "localhost")
        socket (new ServerSocket port 0 inet-address)
        _ (reset! !socket socket)]
    (future (listen socket opts))))
(defn stop-nrepl-server! []
  (when-let [socket @!socket]
    (.close ^ServerSocket socket)
    (reset! !socket nil)))
(defn- create-channel [req]
  (httpkit/as-channel req
                      {:on-open (fn [ch]
                                  (reset! nrepl-channel ch))
                       :on-close (fn [_ch _reason] (prn :close))
                       :on-receive
                       (fn [_ch message]
                         (prn :msg message)
                         (response-handler message))}))
#'browser-server/create-channel
(defn- app [{:as req}]
  (when (:websocket? req)
    (case (:uri req)
      "/_nrepl"
      (create-channel req))))
#'browser-server/app
(defonce ^:private !server
  (atom nil))
(defn halt! []
  (when-let [{:keys [port stop-fn]} @!server]
    (stop-fn)
    (println (str "Webserver running on " port ", stopped."))
    (reset! !server nil)))
(defn start-websocket-server! [{:keys [port]}]
  (let [port (or port 1340)]
    (halt!)
    (try
      (reset! !server {:port port :stop-fn (httpkit/run-server #'app {:port port})})
      (println (str "Websocket server started on " port "..."))
      (catch Exception #_java.net.BindException e ;; TODO, add BindException to bb, done for 0.8.3
             (println "Port " port " not available, server not started!")
             (println (.getMessage e))))))
(defn start!
  [{:keys [nrepl-port websocket-port]
    :or {nrepl-port 1339
         websocket-port 1340}}]
  (start-nrepl-server! {:port nrepl-port})
  (start-websocket-server! {:port websocket-port}))
source: src/mentat_collective/emmy/browser_server.clj