From f500e93183544474ca7ba4b534f5fe2b7dba3210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my?= Date: Mon, 17 Feb 2025 19:34:58 +0100 Subject: [PATCH] Clojure sdk: Fixes, clojure snippets (#672) * Clojure sdk: Fixes, new website snippets Docs: corrected the readme's example Fix: changed return value in the jetty implementation Chore: Changed dependency coordinate of http-kit to a newer maven dependency Fix: using setTimeout in the redirect sugar function Fix: mistake in http-kit management in examples Docs: redirect example Feature: added another testing utility Docs: added missing snippets for the Datastar website * Fix: typo --- sdk/clojure/.gitignore | 7 +- sdk/clojure/CHANGELOG.md | 20 +++++- sdk/clojure/README.md | 3 +- sdk/clojure/adapter-http-kit/deps.edn | 2 +- .../datastar/clojure/adapter/ring/impl.clj | 6 +- sdk/clojure/doc/maintainers-guide.md | 13 ++++ .../datastar/clojure/adapter/test.clj | 43 +++++++++++- .../starfederation/datastar/clojure/api.clj | 5 +- sdk/clojure/src/dev/examples/redirect.clj | 66 +++++++++++++++++++ .../src/dev/examples/snippets/polling1.clj | 32 +++++++++ .../src/dev/examples/snippets/polling2.clj | 42 ++++++++++++ .../src/dev/examples/snippets/redirect1.clj | 28 ++++++++ .../src/dev/examples/snippets/redirect2.clj | 30 +++++++++ .../src/dev/examples/snippets/redirect3.clj | 29 ++++++++ sdk/clojure/src/dev/examples/utils.clj | 4 +- .../how_tos/polling_1.clojuresnippet | 20 ++++++ .../how_tos/polling_2.clojuresnippet | 28 ++++++++ .../how_tos/redirect_1.clojuresnippet | 16 +++++ .../how_tos/redirect_2.clojuresnippet | 17 +++++ .../how_tos/redirect_3.clojuresnippet | 16 +++++ 20 files changed, 415 insertions(+), 12 deletions(-) create mode 100644 sdk/clojure/src/dev/examples/redirect.clj create mode 100644 sdk/clojure/src/dev/examples/snippets/polling1.clj create mode 100644 sdk/clojure/src/dev/examples/snippets/polling2.clj create mode 100644 sdk/clojure/src/dev/examples/snippets/redirect1.clj create mode 100644 sdk/clojure/src/dev/examples/snippets/redirect2.clj create mode 100644 sdk/clojure/src/dev/examples/snippets/redirect3.clj create mode 100644 site/static/code_snippets/how_tos/polling_1.clojuresnippet create mode 100644 site/static/code_snippets/how_tos/polling_2.clojuresnippet create mode 100644 site/static/code_snippets/how_tos/redirect_1.clojuresnippet create mode 100644 site/static/code_snippets/how_tos/redirect_2.clojuresnippet create mode 100644 site/static/code_snippets/how_tos/redirect_3.clojuresnippet diff --git a/sdk/clojure/.gitignore b/sdk/clojure/.gitignore index d4cc8a969..5c8aa6ce0 100644 --- a/sdk/clojure/.gitignore +++ b/sdk/clojure/.gitignore @@ -5,7 +5,8 @@ !.clj-kondo/config.edn !.clj-kondo/hooks** - - - test-resources/test.config.edn +/.lsp-root +/.nfnl.fnl +/.nvim.fnl +/.nvim.lua diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md index 063f83690..8f83b4cfc 100644 --- a/sdk/clojure/CHANGELOG.md +++ b/sdk/clojure/CHANGELOG.md @@ -1,10 +1,28 @@ # Release notes for the Clojure SDK +## 2025-02-15 + +### Added + +- `starfederation.datastar.clojure.adapter.test/->sse-response`. This is a mock + for a SSE ring response that records the SSE events sent with it. +- Example snippets for the main site, ie, polling and redirection. These + examples are runnable from the development examples. +- Development example of the usage of the redirect sugar. + +### Fixed + +- Fixed the main readme example (wrong arity of `:on-close` callback using http-kit) +- The jetty adapter now returns a harmless value when sending an event. It used + to return the write buffer which shouldn't be used directly. +- The `starfederation.datastar.clojure.api/redirect!` helper function uses a js + timeout for redirection + ## 2025-02-03 ### Changed -- The ring adapter for the SKD is now a generic ring adapter. This adapter +- The ring adapter for the SDK is now a generic ring adapter. This adapter depends solely on the ring core protocols, the dependency to the ring jetty adapter has been removed. diff --git a/sdk/clojure/README.md b/sdk/clojure/README.md index 5e987b930..027704679 100644 --- a/sdk/clojure/README.md +++ b/sdk/clojure/README.md @@ -115,7 +115,7 @@ somewhere and use it later: (fn [sse-gen] (swap! !connections conj sse-gen)) :on-close - (fn [sse-gen] + (fn [sse-gen status] (swap! !connections disj sse-gen))})) @@ -184,6 +184,7 @@ Ring adapters are not made equals. Here are some the differences for the ones we ## TODO: +- Consider adding an option to adapter to be able to control the output stream (usefull to enable compression) - Streamlined release process (cutting releases and publish jar to a maven repo) - Consider uniformizing the adapters behavior on connection closing (throwing in all adapters?) - Review the etoin tests, there are some race conditions diff --git a/sdk/clojure/adapter-http-kit/deps.edn b/sdk/clojure/adapter-http-kit/deps.edn index 856ab638d..0e3a89375 100644 --- a/sdk/clojure/adapter-http-kit/deps.edn +++ b/sdk/clojure/adapter-http-kit/deps.edn @@ -1,4 +1,4 @@ ;; NOTE: Track the next release of http-kit to switch to maven dep {:paths ["src/main"] - :deps {http-kit/http-kit {:git/url "https://github.com/http-kit/http-kit" :git/sha "76b869fc34536ad0c43afa9a98d971a0fc32c644"}}} + :deps {http-kit/http-kit {:mvn/version "2.9.0-alpha2"}}} diff --git a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj b/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj index 623a68bb7..f1510e198 100644 --- a/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj +++ b/sdk/clojure/adapter-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj @@ -30,6 +30,7 @@ (doto ^BufferedWriter @writer (sse/write-event! event-type data-lines opts) (.flush)) + :sent (catch Exception e (throw (ex-info "Error sending SSE event" {:sse-gen this @@ -42,8 +43,9 @@ (u/lock! lock (when-let [^BufferedWriter w @writer] (.close w) - (when on-close - (on-close this))))) + (if on-close + (on-close this) + nil)))) Closeable (close [this] diff --git a/sdk/clojure/doc/maintainers-guide.md b/sdk/clojure/doc/maintainers-guide.md index e9fbf979d..f57d8c0f1 100644 --- a/sdk/clojure/doc/maintainers-guide.md +++ b/sdk/clojure/doc/maintainers-guide.md @@ -23,6 +23,19 @@ In the sdk code proper `sdk/clojure`: ## Test +### Running tests + +- `bb run test:all`: run all core tests and smoke tests with ring jetty and + Http-kit +- `bb run test:rj9a`: run all core tests and smoke tests with rj9a +- for the generic bash sdk tests + 1. go to `sdk/clojure/sdk-tests/` + 2. run `clojure -M -m starfederation.datastar.clojure.sdk-test.main` + 3. go to `sdk/test/` + 4. run `./test-all.sh localhost:8080` + +### webdriver config + - Tests resources contains a test.config.edn file. It contains a map whose keys are: - `:drivers`: [etaoin](https://github.com/clj-commons/etaoin) webdriver types to run - `:webdriver-opts`: a map of webdriver type to webriver specific options diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj index 4b93d72eb..7c8461108 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/adapter/test.clj @@ -1,7 +1,12 @@ (ns starfederation.datastar.clojure.adapter.test (:require [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.protocols :as p])) + [starfederation.datastar.clojure.protocols :as p] + [starfederation.datastar.clojure.utils :as u]) + (:import + [java.util.concurrent.locks ReentrantLock] + [java.io Closeable])) + (deftype ReturnMsgGen [] @@ -18,3 +23,39 @@ (defn ->sse-gen [& _] (->ReturnMsgGen)) + + + +(deftype RecordMsgGen [lock !rec !open?] + p/SSEGenerator + (send-event! [_ event-type data-lines opts] + (u/lock! lock + (vswap! !rec conj (-> (StringBuilder.) + (sse/write-event! event-type data-lines opts) + str)))) + + (close-sse! [_] + (u/lock! lock + (vreset! !open? false))) + + Closeable + (close [this] + (p/close-sse! this))) + +(java.util.ArrayList. 1) + +(defn ->sse-response + "Fake a sse-response, the events sent with sse-gen during the + `on-open` callback are recorded in a vector stored in an atom returned as the + body of the response." + [req {:keys [status headers on-open]}] + (let [!rec (volatile! []) + sse-gen (->RecordMsgGen (ReentrantLock.) + !rec + (volatile! true))] + (on-open sse-gen) + {:status (or status 200) + :headers (merge headers (sse/headers req)) + :body !rec})) + + diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj index 2286bd027..d9704078f 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj @@ -415,7 +415,10 @@ ([sse-gen url] (redirect! sse-gen url {})) ([sse-gen url opts] - (execute-script! sse-gen (str "window.location.href = \""url"\";") opts))) + (execute-script! sse-gen + (str "setTimeout(() => window.location.href =\"" url "\")") + opts))) + ;; ----------------------------------------------------------------------------- ;; Misc diff --git a/sdk/clojure/src/dev/examples/redirect.clj b/sdk/clojure/src/dev/examples/redirect.clj new file mode 100644 index 000000000..ba0bccdbc --- /dev/null +++ b/sdk/clojure/src/dev/examples/redirect.clj @@ -0,0 +1,66 @@ +(ns examples.redirect + (:require + [examples.common :as c] + [examples.utils :as u] + [dev.onionpancakes.chassis.core :refer [html]] + [reitit.ring :as rr] + [ring.util.response :as ruresp] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]])) + + +(def home-page + (html + (c/page-scaffold + [[:h1 "Test page"] + [:div.#indicator + [:button {:data-on-click (d*/sse-get "/redirect-me")} + "Start redirect"]]]))) + + +(defn home [_] + (ruresp/response home-page)) + + +(def guide-page + (html + (c/page-scaffold + [[:h1 "You have been redirected"] + [:a {:href "/" } "Home"]]))) + + +(defn guide [_] + (ruresp/response guide-page)) + + +(defn redirect-handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/redirect! sse "/guide") + (d*/close-sse! sse))})) + + + + +(def router (rr/router + [["/" {:handler home}] + ["/guide" {:handler guide}] + ["/redirect-me" {:handler redirect-handler}]])) + + +(def default-handler (rr/create-default-handler)) + + +(def handler + (rr/ring-handler router default-handler)) + + + +(comment + (u/reboot-hk-server! #'handler)) + + diff --git a/sdk/clojure/src/dev/examples/snippets/polling1.clj b/sdk/clojure/src/dev/examples/snippets/polling1.clj new file mode 100644 index 000000000..d7f6a20ab --- /dev/null +++ b/sdk/clojure/src/dev/examples/snippets/polling1.clj @@ -0,0 +1,32 @@ +(ns examples.snippets.polling1 + (:require + [dev.onionpancakes.chassis.core :refer [html]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) + + +(comment + (require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]])) + +(import + 'java.time.format.DateTimeFormatter + 'java.time.LocalDateTime) + +(def formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#time {:data-on-interval__duration.5s (d*/sse-get "/endpoint")} + (LocalDateTime/.format (LocalDateTime/now) formatter)])) + (d*/close-sse! sse))})) + +(comment + (handler {})) + + diff --git a/sdk/clojure/src/dev/examples/snippets/polling2.clj b/sdk/clojure/src/dev/examples/snippets/polling2.clj new file mode 100644 index 000000000..973223e06 --- /dev/null +++ b/sdk/clojure/src/dev/examples/snippets/polling2.clj @@ -0,0 +1,42 @@ +(ns examples.snippets.polling2 + (:require + [dev.onionpancakes.chassis.core :refer [html]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :as at :refer [->sse-response]])) + + +(comment + (require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]])) + +(import + 'java.time.format.DateTimeFormatter + 'java.time.LocalDateTime) + +(def date-time-formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) +(def seconds-formatter (DateTimeFormatter/ofPattern "ss")) + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (let [now (LocalDateTime/now) + current-time (LocalDateTime/.format now date-time-formatter) + seconds (LocalDateTime/.format now seconds-formatter) + duration (if (neg? (compare seconds "50")) + "5" + "1")] + (d*/merge-fragment! sse + (html [:div#time {(str "data-on-interval__duration." duration "s") + (d*/sse-get "/endpoint")} + current-time])) + + (d*/close-sse! sse)))})) + + +(comment + (handler {})) + + diff --git a/sdk/clojure/src/dev/examples/snippets/redirect1.clj b/sdk/clojure/src/dev/examples/snippets/redirect1.clj new file mode 100644 index 000000000..ac7e5a820 --- /dev/null +++ b/sdk/clojure/src/dev/examples/snippets/redirect1.clj @@ -0,0 +1,28 @@ +(ns examples.snippets.redirect1 + (:require + [dev.onionpancakes.chassis.core :refer [html]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) + + +(comment + (require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]])) + + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/execute-script! sse "window.location = \"/guide\"") + (d*/close-sse! sse))})) + +(comment + (handler {})) + + diff --git a/sdk/clojure/src/dev/examples/snippets/redirect2.clj b/sdk/clojure/src/dev/examples/snippets/redirect2.clj new file mode 100644 index 000000000..970d2aa78 --- /dev/null +++ b/sdk/clojure/src/dev/examples/snippets/redirect2.clj @@ -0,0 +1,30 @@ +(ns examples.snippets.redirect2 + (:require + [dev.onionpancakes.chassis.core :refer [html]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) + + +(comment + (require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]])) + + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/execute-script! sse + "setTimeout(() => window.location = \"/guide\"") + (d*/close-sse! sse))})) + + +(comment + (handler {})) + + diff --git a/sdk/clojure/src/dev/examples/snippets/redirect3.clj b/sdk/clojure/src/dev/examples/snippets/redirect3.clj new file mode 100644 index 000000000..4551db756 --- /dev/null +++ b/sdk/clojure/src/dev/examples/snippets/redirect3.clj @@ -0,0 +1,29 @@ +(ns examples.snippets.redirect3 + (:require + [dev.onionpancakes.chassis.core :refer [html]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) + + +(comment + (require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]])) + + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/redirect! sse "/guide") + (d*/close-sse! sse))})) + + +(comment + (handler {})) + + diff --git a/sdk/clojure/src/dev/examples/utils.clj b/sdk/clojure/src/dev/examples/utils.clj index 68458510d..915c57ade 100644 --- a/sdk/clojure/src/dev/examples/utils.clj +++ b/sdk/clojure/src/dev/examples/utils.clj @@ -43,8 +43,8 @@ (swap! !hk-server (fn [server] (when server - (http-kit-run! server)) - (http-kit-stop! handler + (http-kit-stop! server)) + (http-kit-run! handler {:port 8080 :legacy-return-value? false}))))) diff --git a/site/static/code_snippets/how_tos/polling_1.clojuresnippet b/site/static/code_snippets/how_tos/polling_1.clojuresnippet new file mode 100644 index 000000000..53eeab966 --- /dev/null +++ b/site/static/code_snippets/how_tos/polling_1.clojuresnippet @@ -0,0 +1,20 @@ +(require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]]) + '[some.hiccup.library :refer [html]]) + +(import + 'java.time.format.DateTimeFormatter + 'java.time.LocalDateTime) + +(def formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) + +(defn handle [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#time {:data-on-interval__duration.5s (d*/sse-get "/endpoint")} + (LocalDateTime/.format (LocalDateTime/now) formatter)])))})) + + (d*/close-sse! sse))})) diff --git a/site/static/code_snippets/how_tos/polling_2.clojuresnippet b/site/static/code_snippets/how_tos/polling_2.clojuresnippet new file mode 100644 index 000000000..2c7d6ff16 --- /dev/null +++ b/site/static/code_snippets/how_tos/polling_2.clojuresnippet @@ -0,0 +1,28 @@ +(require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]]) + '[some.hiccup.library :refer [html]]) + +(import + 'java.time.format.DateTimeFormatter + 'java.time.LocalDateTime) + +(def date-time-formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) +(def seconds-formatter (DateTimeFormatter/ofPattern "ss")) + +(defn handle [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (let [now (LocalDateTime/now) + current-time (LocalDateTime/.format now date-time-formatter) + seconds (LocalDateTime/.format now seconds-formatter) + duration (if (neg? (compare seconds "50")) + "5" + "1")] + (d*/merge-fragment! sse + (html [:div#time {(str "data-on-interval__duration." duration "s") + (d*/sse-get "/endpoint")} + current-time]))))})) + + (d*/close-sse! sse))})) diff --git a/site/static/code_snippets/how_tos/redirect_1.clojuresnippet b/site/static/code_snippets/how_tos/redirect_1.clojuresnippet new file mode 100644 index 000000000..a825c958b --- /dev/null +++ b/site/static/code_snippets/how_tos/redirect_1.clojuresnippet @@ -0,0 +1,16 @@ +(require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]]) + + +(defn handle [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/execute-script! sse "window.location = \"/guide\"") + (d*/close-sse! sse)})) + diff --git a/site/static/code_snippets/how_tos/redirect_2.clojuresnippet b/site/static/code_snippets/how_tos/redirect_2.clojuresnippet new file mode 100644 index 000000000..7ee600a3b --- /dev/null +++ b/site/static/code_snippets/how_tos/redirect_2.clojuresnippet @@ -0,0 +1,17 @@ +(require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]]) + + +(defn handle [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/execute-script! sse + "setTimeout(() => window.location = \"/guide\"") + (d*/close-sse! sse))})) + diff --git a/site/static/code_snippets/how_tos/redirect_3.clojuresnippet b/site/static/code_snippets/how_tos/redirect_3.clojuresnippet new file mode 100644 index 000000000..1211fca05 --- /dev/null +++ b/site/static/code_snippets/how_tos/redirect_3.clojuresnippet @@ -0,0 +1,16 @@ +(require + '[starfederation.datastar.clojure.api :as d*] + '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]] + '[some.hiccup.library :refer [html]]) + + +(defn handler [ring-request] + (->sse-response ring-request + {:on-open + (fn [sse] + (d*/merge-fragment! sse + (html [:div#indicator "Redirecting in 3 seconds..."])) + (Thread/sleep 3000) + (d*/redirect! sse "/guide") + (d*/close-sse! sse))})) +