diff --git a/README.md b/README.md index 4ba98d1..d6b6d63 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ project's deps are on the source roots and its native libs are loaded, so Each session keeps its own current namespace and runs evals on a dedicated serialized worker thread, so sessions are isolated and a long eval doesn't block other ops. `interrupt` aborts the running eval and the session keeps serving. +An eval's output reaches the client as it is printed, one `out` (or `err`) +message per flush, not all at once when the eval finishes. That needs jolt +0.8.13, the floor `deps.edn` declares. Leave `cider.nrepl/cider-middleware` out if you only want eval: the cider ops are what pull in orchard and compliment. diff --git a/deps.edn b/deps.edn index 27095a2..ed0307b 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,9 @@ {;; jolt.ffi/write takes the value BEFORE the offset as of jolt 0.8.0, and the ;; two spellings are both integers — an older runtime writes to the wrong ;; place rather than failing. Declare the floor so it refuses instead. - :jolt/min-version "0.8.0" + ;; 0.8.13: jolt.nrepl/evaluate's {:out f :err f} arity, which the session + ;; middleware streams eval output through. + :jolt/min-version "0.8.13" :paths ["src"] diff --git a/src/nrepl/middleware/session.clj b/src/nrepl/middleware/session.clj index 16058a1..c0797b5 100644 --- a/src/nrepl/middleware/session.clj +++ b/src/nrepl/middleware/session.clj @@ -41,8 +41,13 @@ ns-atom (:ns session) ;; the actual evaluation runs interruptibly: interrupt! on `token` aborts ;; it (throws {:jolt/interrupted true}), which the worker catches below. + ;; Output streams: evaluate calls :out / :err with each chunk as it is + ;; flushed, while the eval runs, as upstream's session writers do. :out + ;; then comes back empty. {:keys [value out ns err ex err-msg ex-class root-ex-class]} - (jolt.host/run-interruptible token (fn [] (server/evaluate code (or ns-str @ns-atom))))] + (jolt.host/run-interruptible token (fn [] (server/evaluate code (or ns-str @ns-atom) + {:out (fn [s] (reply {"out" s})) + :err (fn [s] (reply {"err" s}))})))] (reset! ns-atom ns) (when (seq out) (reply {"out" out})) (cond diff --git a/test/nrepl/middleware/session_test.clj b/test/nrepl/middleware/session_test.clj index a2d0134..f0e701c 100644 --- a/test/nrepl/middleware/session_test.clj +++ b/test/nrepl/middleware/session_test.clj @@ -64,3 +64,20 @@ c (h/combine (h/eval-code t "(map (fn [x] (println \"side\" x) x) [1 2])" :session s))] (is (= "side 1\nside 2\n" (:out c))) (is (= ["(1 2)"] (:value c))))))) + +(deftest eval-output-streams-as-it-is-flushed + (with-conn t + (let [s (h/new-session t) + rs (h/eval-code t "(println \"first\") (Thread/sleep 200) (println \"second\") :ok" :session s)] + (testing "each println is its own out message, not one batch at the end" + (is (= ["first\n" "second\n"] (keep :out rs)))) + (testing "the output comes before the value" + (is (< (.indexOf ^java.util.List (vec rs) (first (filter :out rs))) + (.indexOf ^java.util.List (vec rs) (first (filter :value rs))))))))) + +(deftest eval-err-output-reaches-the-client + (with-conn t + (let [s (h/new-session t) + rs (h/eval-code t "(binding [*out* *err*] (println \"warned\")) :ok" :session s)] + (is (= ["warned\n"] (keep :err rs))) + (is (= [":ok"] (keep :value rs))))))