├── examples ├── hello-world │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── dev │ │ │ └── user.clj │ │ └── main │ │ │ └── example │ │ │ ├── utils.clj │ │ │ ├── main.clj │ │ │ ├── server.clj │ │ │ └── core.clj │ ├── deps.edn │ └── resources │ │ └── public │ │ └── hello-world.html └── hello-httpkit │ ├── README.md │ ├── deps.edn │ ├── resources │ └── hello-world.html │ └── src │ └── hello_httpkit.clj ├── libraries ├── sdk │ ├── build.clj │ ├── resources │ │ └── clj-kondo.exports │ │ │ └── starfederation.datastar.clojure │ │ │ └── sdk │ │ │ └── config.edn │ ├── deps.edn │ ├── src │ │ └── main │ │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ ├── protocols.clj │ │ │ ├── api │ │ │ ├── common.clj │ │ │ ├── signals.clj │ │ │ └── scripts.clj │ │ │ ├── adapter │ │ │ └── test.cljc │ │ │ ├── utils.clj │ │ │ └── consts.clj │ └── README.md ├── sdk-brotli │ ├── build.clj │ ├── deps.edn │ ├── README.md │ └── src │ │ └── main │ │ └── starfederation │ │ └── datastar │ │ └── clojure │ │ └── brotli.clj ├── sdk-http-kit │ ├── build.clj │ ├── deps.edn │ ├── README.md │ └── src │ │ └── main │ │ └── starfederation │ │ └── datastar │ │ └── clojure │ │ └── adapter │ │ ├── http_kit.clj │ │ └── http_kit │ │ └── impl.cljc ├── sdk-ring │ ├── build.clj │ ├── deps.edn │ ├── README.md │ └── src │ │ └── main │ │ └── starfederation │ │ └── datastar │ │ └── clojure │ │ └── adapter │ │ ├── ring.clj │ │ └── ring │ │ └── impl.clj ├── sdk-malli-schemas │ ├── build.clj │ ├── src │ │ └── main │ │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ ├── api │ │ │ ├── scripts_schemas.clj │ │ │ ├── signals_schemas.clj │ │ │ ├── sse_schemas.clj │ │ │ ├── elements_schemas.clj │ │ │ └── common_schemas.clj │ │ │ ├── api_schemas.clj │ │ │ └── adapter │ │ │ └── common_schemas.clj │ ├── deps.edn │ └── README.md ├── sdk-ring-malli-schemas │ ├── build.clj │ ├── src │ │ └── main │ │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── adapter │ │ │ └── ring_schemas.clj │ ├── README.md │ └── deps.edn ├── sdk-http-kit-malli-schemas │ ├── build.clj │ ├── src │ │ └── main │ │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── adapter │ │ │ ├── http_kit2_schemas.clj │ │ │ └── http_kit_schemas.clj │ ├── deps.edn │ └── README.md └── build_stub.clj ├── src ├── dev │ ├── examples │ │ ├── form_behavior │ │ │ └── style.css │ │ ├── forms │ │ │ ├── common.clj │ │ │ ├── core.clj │ │ │ ├── html.clj │ │ │ └── datastar.clj │ │ ├── common.clj │ │ ├── animation_gzip │ │ │ ├── broadcast.clj │ │ │ ├── style.css │ │ │ ├── state.clj │ │ │ ├── handlers.clj │ │ │ └── animation.clj │ │ ├── snippets │ │ │ ├── redirect3.clj │ │ │ ├── redirect1.clj │ │ │ ├── redirect2.clj │ │ │ ├── polling1.clj │ │ │ ├── polling2.clj │ │ │ └── load_more.clj │ │ ├── malli.clj │ │ ├── scripts.clj │ │ ├── http_kit_disconnect.clj │ │ ├── broadcast_http_kit.clj │ │ ├── redirect.clj │ │ ├── snippets.clj │ │ ├── jetty_disconnect.clj │ │ ├── multiple_fragments.clj │ │ ├── broadcast_ring.clj │ │ ├── faulty_event.clj │ │ ├── remove_fragments.clj │ │ ├── data_dsl.clj │ │ ├── animation_gzip.clj │ │ ├── http_kit2 │ │ │ └── animation.clj │ │ ├── utils.clj │ │ └── tiny_gzip.clj │ ├── bench │ │ └── split.clj │ └── user.clj ├── bb-example │ ├── bb.edn │ └── src │ │ └── main │ │ └── bb_example │ │ ├── core.clj │ │ ├── animation │ │ ├── broadcast.clj │ │ ├── style.css │ │ ├── state.clj │ │ ├── handlers.clj │ │ └── core.clj │ │ ├── common.clj │ │ ├── broadcast.clj │ │ └── animation.clj ├── test │ ├── adapter-common │ │ └── test │ │ │ ├── examples │ │ │ ├── common.clj │ │ │ ├── form.clj │ │ │ └── counter.clj │ │ │ └── utils.clj │ ├── adapter-ring │ │ ├── test │ │ │ └── examples │ │ │ │ └── ring_handler.clj │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── adapter │ │ │ └── ring │ │ │ └── impl_test.clj │ ├── adapter-http-kit │ │ ├── test │ │ │ ├── examples │ │ │ │ ├── http_kit_handler.clj │ │ │ │ └── http_kit_handler2.clj │ │ │ ├── http_kit_test.clj │ │ │ └── http_kit2_test.clj │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── adapter │ │ │ └── http_kit │ │ │ └── impl_test.clj │ ├── malli-schemas │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── api_schemas_test.clj │ ├── brotli │ │ └── starfederation │ │ │ └── datastar │ │ │ └── clojure │ │ │ └── brotli_test.clj │ ├── adapter-ring-jetty │ │ └── test │ │ │ └── ring_jetty_test.clj │ └── adapter-rj9a │ │ └── test │ │ └── rj9a_test.clj └── bb │ └── tasks │ └── cljdoc.clj ├── .gitignore ├── .clj-kondo ├── config.edn └── hooks │ └── test_hooks.clj ├── sdk-tests ├── src │ ├── dev │ │ └── user.clj │ └── main │ │ └── starfederation │ │ └── datastar │ │ └── clojure │ │ └── sdk_test │ │ └── main.clj ├── README.md └── deps.edn ├── doc ├── Libraries.md └── cljdoc.edn ├── LICENSE.md └── .github └── workflows └── release-sdk.yml /examples/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | .nrepl-port 2 | .cpcache 3 | -------------------------------------------------------------------------------- /libraries/sdk/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-brotli/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-ring/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-ring-malli-schemas/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit-malli-schemas/build.clj: -------------------------------------------------------------------------------- 1 | (ns build) 2 | (load-file "../build_stub.clj") 3 | -------------------------------------------------------------------------------- /src/dev/examples/form_behavior/style.css: -------------------------------------------------------------------------------- 1 | #buttons { 2 | display: flex; 3 | gap: 10px; 4 | } 5 | -------------------------------------------------------------------------------- /libraries/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {starfederation.datastar.clojure.utils/def-clone clojure.core/def}} 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .nrepl-port 3 | .lsp 4 | .clj-kondo/** 5 | !.clj-kondo/config.edn 6 | !.clj-kondo/hooks** 7 | **/target 8 | test-resources/test.config.edn 9 | /.lsp-root 10 | /.nfnl.fnl 11 | /.nvim.fnl 12 | /.nvim.lua 13 | 14 | .cljdoc-preview 15 | .lazy.fnl 16 | .lazy.lua 17 | -------------------------------------------------------------------------------- /examples/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello world example 2 | 3 | ## Running the example 4 | 5 | - repl: 6 | 7 | ```bash 8 | clojure -M:repl -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" 9 | ``` 10 | 11 | - main: 12 | 13 | ```bash 14 | clojure -M -m example.main 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/hello-httpkit/README.md: -------------------------------------------------------------------------------- 1 | # A Datastar + http-kit starter 2 | 3 | ## Running the example 4 | 5 | - repl: 6 | 7 | ```bash 8 | clojure -M:repl -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" 9 | ``` 10 | 11 | - main: 12 | 13 | ```bash 14 | clojure -M -m hello-httpkit 15 | ``` 16 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {fr.jeremyschoffen.datastar.utils/defroutes clojure.core/def 2 | starfederation.datastar.clojure.utils/transient-> clojure.core/->} 3 | :hooks 4 | {:analyze-call 5 | {test.utils/with-server hooks.test-hooks/with-server}} 6 | :linters {:redundant-ignore {:exclude [:clojure-lsp/unused-public-var]}}} 7 | -------------------------------------------------------------------------------- /examples/hello-world/src/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clj-reload.core :as reload])) 4 | 5 | 6 | (alter-var-root #'*warn-on-reflection* (constantly true)) 7 | 8 | 9 | (reload/init 10 | {:no-reload ['user]}) 11 | 12 | 13 | (defn reload! [] 14 | (reload/reload)) 15 | 16 | 17 | (comment 18 | (reload!) 19 | *e) 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/hello-world/src/main/example/utils.clj: -------------------------------------------------------------------------------- 1 | (ns example.utils 2 | (:require 3 | [charred.api :as charred] 4 | [starfederation.datastar.clojure.api :as d*])) 5 | 6 | 7 | (def ^:private bufSize 1024) 8 | (def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) 9 | 10 | (defn get-signals [req] 11 | (-> req d*/get-signals read-json)) 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/hello-world/src/main/example/main.clj: -------------------------------------------------------------------------------- 1 | (ns example.main 2 | (:require 3 | [example.core :as c] 4 | [example.server :as server])) 5 | 6 | 7 | (defn -main [& _] 8 | (let [server (server/start! c/handler)] 9 | (.addShutdownHook (Runtime/getRuntime) 10 | (Thread. (fn [] 11 | (server/stop! server) 12 | (shutdown-agents)))))) 13 | -------------------------------------------------------------------------------- /src/bb-example/bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main" 2 | "../../libraries/sdk/src/main" 3 | "../../libraries/sdk-http-kit/src/main/" 4 | "../../libraries/sdk-http-kit-malli-schemas/src/main/" 5 | "../../libraries/sdk-malli-schemas/src/main"] 6 | :deps {metosin/malli {:mvn/version "0.17.0"} 7 | org.clojars.askonomm/ruuter {:mvn/version "1.3.5"} 8 | ring/ring-core {:mvn/version "1.14.2"}}} 9 | -------------------------------------------------------------------------------- /sdk-tests/src/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clj-reload.core :as reload])) 4 | 5 | 6 | (alter-var-root #'*warn-on-reflection* (constantly true)) 7 | 8 | ;(rcf/enable!) 9 | 10 | 11 | (reload/init 12 | {:no-reload ['user]}) 13 | 14 | 15 | (defn reload! [] 16 | (reload/reload)) 17 | 18 | 19 | 20 | 21 | (defn clear-terminal! [] 22 | (binding [*out* (java.io.PrintWriter. System/out)] 23 | (print "\033c") 24 | (flush))) 25 | 26 | 27 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.scripts-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.api.common-schemas :as cs] 5 | [starfederation.datastar.clojure.api.scripts])) 6 | 7 | (m/=> starfederation.datastar.clojure.api.scripts/->script-tag 8 | [:-> cs/script-content-schema cs/execute-script-options-schemas :string]) 9 | 10 | -------------------------------------------------------------------------------- /libraries/sdk-ring-malli-schemas/src/main/starfederation/datastar/clojure/adapter/ring_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.ring-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.adapter.ring] 5 | [starfederation.datastar.clojure.adapter.common-schemas :as cs])) 6 | 7 | 8 | (m/=> starfederation.datastar.clojure.adapter.ring/->sse-response 9 | [:-> :map cs/->sse-response-options-schema :any]) 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.core 2 | (:require 3 | [org.httpkit.server :as hk])) 4 | 5 | 6 | 7 | (defonce !server (atom nil)) 8 | 9 | (defn stop! [] 10 | (when-let [server @!server] 11 | (hk/server-stop! server) 12 | (reset! !server nil))) 13 | 14 | (defn start! [handler] 15 | (stop!) 16 | (reset! !server (hk/run-server handler {:port 8080 17 | :legacy-return-value? false}))) 18 | 19 | -------------------------------------------------------------------------------- /src/dev/examples/forms/common.clj: -------------------------------------------------------------------------------- 1 | (ns examples.forms.common 2 | (:require 3 | [dev.onionpancakes.chassis.compiler :as hc])) 4 | 5 | 6 | (defn result-area [res-from-signalsl res-from-form] 7 | (hc/compile 8 | [:div {:id "form-result"} 9 | [:span "From signals: " [:span {:data-text "$input1"}]] 10 | [:br] 11 | [:span "from backend signals: " [:span res-from-signalsl]] 12 | [:br] 13 | [:span "from backend form: " [:span res-from-form]]])) 14 | 15 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.signals-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.api.common-schemas :as cs] 5 | [starfederation.datastar.clojure.api.signals])) 6 | 7 | 8 | (m/=> starfederation.datastar.clojure.api.signals/->patch-signals 9 | [:-> cs/signals-schema cs/patch-signals-options-schemas cs/data-lines-schema]) 10 | 11 | -------------------------------------------------------------------------------- /src/dev/examples/common.clj: -------------------------------------------------------------------------------- 1 | (ns examples.common 2 | (:require 3 | [dev.onionpancakes.chassis.core :as h] 4 | [dev.onionpancakes.chassis.compiler :as hc] 5 | [starfederation.datastar.clojure.api :as d*])) 6 | 7 | 8 | (defn page-scaffold [body] 9 | (hc/compile 10 | [[h/doctype-html5] 11 | [:html 12 | [:head 13 | [:meta {:charset "UTF-8"}] 14 | [:script {:type "module" 15 | :src d*/CDN-url}]] 16 | [:body body]]])) 17 | 18 | -------------------------------------------------------------------------------- /libraries/sdk/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main" "resources"] 2 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" 3 | :git/sha "deedd62"} 4 | slipset/deps-deploy {:mvn/version "0.2.2"}} 5 | :ns-default build} 6 | :neil {:project {:name dev.data-star.clojure/sdk 7 | :version "1.0.0-RC5" 8 | :description "Datastar SDK for Clojure"}}}} 9 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api/sse_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.sse-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.api.common-schemas :as cs] 5 | [starfederation.datastar.clojure.api.sse])) 6 | 7 | 8 | (m/=> starfederation.datastar.clojure.api.sse/send-event! 9 | [:function 10 | [:-> cs/sse-gen-schema cs/event-type-schema cs/data-lines-schema :any] 11 | [:-> cs/sse-gen-schema cs/event-type-schema cs/data-lines-schema cs/sse-options-schema :any]]) 12 | -------------------------------------------------------------------------------- /.clj-kondo/hooks/test_hooks.clj: -------------------------------------------------------------------------------- 1 | (ns hooks.test-hooks 2 | (:require 3 | [clj-kondo.hooks-api :as api])) 4 | 5 | 6 | (defn with-server [{:keys [node] :as exp}] 7 | (let [[s-name handler opts & body] (-> node :children rest) 8 | underscore (api/token-node '_) 9 | new-children (list* 10 | (api/token-node 'let) 11 | (api/vector-node 12 | [s-name handler 13 | underscore opts]) 14 | body) 15 | new-node (assoc node :children new-children)] 16 | (assoc exp :node new-node))) 17 | 18 | -------------------------------------------------------------------------------- /sdk-tests/README.md: -------------------------------------------------------------------------------- 1 | # SDK tests 2 | 3 | This is where the code for the [generic tests](/sdk/test) lives. 4 | 5 | ## Running the test app 6 | 7 | - repl: 8 | 9 | ```bash 10 | clojure -M:repl -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" 11 | 12 | ``` 13 | 14 | - main: 15 | 16 | ```bash 17 | clojure -M -m starfederation.datastar.clojure.sdk-test.main 18 | ``` 19 | 20 | - start go binary running the tests 21 | 22 | ```bash 23 | go run github.com/starfederation/datastar/sdk/tests/cmd/datastar-sdk-tests@latest 24 | ``` 25 | 26 | More information here: [SDKs' common tests](https://github.com/starfederation/datastar/tree/develop/sdk/tests). 27 | -------------------------------------------------------------------------------- /libraries/sdk-ring-malli-schemas/README.md: -------------------------------------------------------------------------------- 1 | # Malli schemas for the SDK 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/ring-malli-schemas.svg)](https://clojars.org/dev.data-star.clojure/ring-malli-schemas) 8 | 9 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/malli-schemas)](https://cljdoc.org/d/dev.data-star.clojure/malli-schemas/CURRENT) 10 | 11 | ## Overview 12 | 13 | This library provides Malli schemas for the Ring adapter APIs. 14 | 15 | Notable schema namespaces: 16 | 17 | - `starfederation.datastar.clojure.adapter.ring-schemas` 18 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 3 | metosin/malli {:mvn/version "0.19.1"}} 4 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" 5 | :git/sha "deedd62"} 6 | slipset/deps-deploy {:mvn/version "0.2.2"}} 7 | :ns-default build} 8 | :neil {:project {:name dev.data-star.clojure/malli-schemas 9 | :version "1.0.0-RC5" 10 | :description "Malli schemas for the Datastar SDK"}}}} 11 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.elements-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.api.common-schemas :as cs] 5 | [starfederation.datastar.clojure.api.elements])) 6 | 7 | 8 | (m/=> starfederation.datastar.clojure.api.elements/->patch-elements 9 | [:-> cs/elements-schema cs/patch-element-options-schemas cs/data-lines-schema]) 10 | 11 | 12 | (m/=> starfederation.datastar.clojure.api.elements/->patch-elements-seq 13 | [:-> cs/elements-seq-schema cs/patch-element-options-schemas cs/data-lines-schema]) 14 | 15 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 3 | http-kit/http-kit {:mvn/version "2.9.0-beta2"}} 4 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" 5 | :git/sha "deedd62"} 6 | slipset/deps-deploy {:mvn/version "0.2.2"}} 7 | :ns-default build} 8 | :neil {:project {:name dev.data-star.clojure/http-kit 9 | :version "1.0.0-RC5" 10 | :description "http-kit adapter for the Datastar SDK"}}}} 11 | -------------------------------------------------------------------------------- /libraries/sdk-ring/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 3 | org.ring-clojure/ring-core-protocols {:mvn/version "1.15.0"}} 4 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" 5 | :git/sha "deedd62"} 6 | slipset/deps-deploy {:mvn/version "0.2.2"}} 7 | :ns-default build} 8 | :neil {:project {:name dev.data-star.clojure/ring 9 | :version "1.0.0-RC5" 10 | :description "Ring adapter for the Datastar SDK"}}}} 11 | -------------------------------------------------------------------------------- /src/dev/bench/split.clj: -------------------------------------------------------------------------------- 1 | (ns bench.split 2 | (:require 3 | [clojure.string :as string] 4 | [starfederation.datastar.clojure.api.common :as c])) 5 | 6 | 7 | (defn old-datalines [data-lines! prefix text] 8 | (reduce 9 | (fn [acc part] 10 | (conj! acc (str prefix part))) 11 | data-lines! 12 | (string/split-lines text))) 13 | 14 | 15 | (def input "hello there\n wold !\r\n How are \ryou \ntoday") 16 | (def input-big (apply str (repeat 100 input))) 17 | 18 | (defn bench [f input] 19 | (println "---------------------------------------") 20 | (dotimes [_ 20] 21 | (time 22 | (dotimes [_ 10000] 23 | (f (transient []) "elements " input))))) 24 | 25 | 26 | (comment 27 | (bench old-datalines input-big) 28 | (bench c/add-data-lines! input-big)) 29 | -------------------------------------------------------------------------------- /examples/hello-httpkit/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.cnuernber/charred {:mvn/version "1.034"} 3 | dev.data-star.clojure/http-kit {:local/root "../../libraries/sdk-http-kit" 4 | :exclusions [dev.data-star.clojure/sdk]} 5 | dev.data-star.clojure/sdk {:local/root "../../libraries/sdk"} 6 | dev.onionpancakes/chassis {:mvn/version "1.0.365"} 7 | http-kit/http-kit {:mvn/version "2.8.1"} 8 | metosin/reitit {:mvn/version "0.7.2"}} 9 | :aliases 10 | {:repl {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 11 | nrepl/nrepl {:mvn/version "1.3.0"} 12 | cider/cider-nrepl {:mvn/version "0.50.2"}}}}} 13 | -------------------------------------------------------------------------------- /libraries/sdk-brotli/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.20.0"} 3 | dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 4 | io.netty/netty-buffer {:mvn/version "4.2.6.Final"}} 5 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" 6 | :git/sha "deedd62"} 7 | slipset/deps-deploy {:mvn/version "0.2.2"}} 8 | :ns-default build} 9 | :neil {:project {:name dev.data-star.clojure/brotli 10 | :description "Brotli compression helpers for the Datastar SDK" 11 | :version "1.0.0-RC5"}}}} 12 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit-malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.http-kit2-schemas 2 | (:require 3 | [malli.core :as m] 4 | [malli.util :as mu] 5 | [starfederation.datastar.clojure.adapter.common :as ac] 6 | [starfederation.datastar.clojure.adapter.http-kit2] 7 | [starfederation.datastar.clojure.adapter.common-schemas :as cs])) 8 | 9 | 10 | (def options-schema 11 | (mu/update-in cs/->sse-response-options-schema 12 | [ac/write-profile] 13 | (fn [x] 14 | (mu/optional-keys x [ac/wrap-output-stream])))) 15 | 16 | 17 | (m/=> starfederation.datastar.clojure.adapter.http-kit2/->sse-response 18 | [:-> :map options-schema :any]) 19 | 20 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit-malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.http-kit-schemas 2 | (:require 3 | [malli.core :as m] 4 | [malli.util :as mu] 5 | [starfederation.datastar.clojure.adapter.common :as ac] 6 | [starfederation.datastar.clojure.adapter.http-kit] 7 | [starfederation.datastar.clojure.adapter.common-schemas :as cs])) 8 | 9 | 10 | (def options-schema 11 | (mu/update-in cs/->sse-response-options-schema 12 | [ac/write-profile] 13 | (fn [x] 14 | (mu/optional-keys x [ac/wrap-output-stream])))) 15 | 16 | 17 | (m/=> starfederation.datastar.clojure.adapter.http-kit/->sse-response 18 | [:-> :map options-schema :any]) 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /doc/Libraries.md: -------------------------------------------------------------------------------- 1 | # Libraries 2 | 3 | ## [Core SDK](/libraries/sdk/README.md) 4 | 5 | Core SDK implementing the generic parts of the Datastar ADR. 6 | 7 | ## [Http-kit adapter](/libraries/sdk-http-kit/README.md) 8 | 9 | SSE-gen implementation and ring API for Http-kit 10 | 11 | ## [Ring](/libraries/sdk-ring/README.md) 12 | 13 | SSE-gen implementation and ring API for ring compliant adapters 14 | 15 | ## [Brotli](/libraries/sdk-brotli/README.md) 16 | 17 | Write profiles for brotli 18 | 19 | ## Malli Schemas 20 | 21 | We provide libraries containing Malli schemas for the different libraries 22 | 23 | - [Core SDK schemas](/libraries/sdk-malli-schemas/README.md) 24 | - [Http-kit adapter schemas](/libraries/sdk-http-kit-malli-schemas/README.md) 25 | - [Ring adapter schemas](/libraries/sdk-ring-malli-schemas/README.md) 26 | -------------------------------------------------------------------------------- /libraries/sdk-ring-malli-schemas/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {metosin/malli {:mvn/version "0.17.0"} 3 | dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 4 | dev.data-star.clojure/ring {:mvn/version "1.0.0-RC5"} 5 | dev.data-star.clojure/malli-schemas {:mvn/version "1.0.0-RC5"}} 6 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" 7 | :git/sha "e405aac"} 8 | slipset/deps-deploy {:mvn/version "0.2.2"}} 9 | :ns-default build} 10 | :neil {:project {:name dev.data-star.clojure/ring-malli-schemas 11 | :version "1.0.0-RC5" 12 | :description "Malli schemas for the Datastar SDK"}}}} 13 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip/broadcast.clj: -------------------------------------------------------------------------------- 1 | (ns examples.animation-gzip.broadcast 2 | (:require 3 | [examples.animation-gzip.rendering :as rendering] 4 | [examples.animation-gzip.state :as state] 5 | [starfederation.datastar.clojure.api :as d*])) 6 | 7 | 8 | (defn send-frame! [sse frame] 9 | (try 10 | (d*/patch-elements! sse frame) 11 | (catch Exception e 12 | (println e)))) 13 | 14 | 15 | (defn broadcast-new-frame! [frame] 16 | (let [sses @state/!conns] 17 | (doseq [sse sses] 18 | (send-frame! sse frame)))) 19 | 20 | 21 | (defn install-watch! [] 22 | (add-watch state/!state ::watch 23 | (fn [_k _ref old new] 24 | (when-not (identical? old new) 25 | (let [frame (rendering/render-content new)] 26 | (broadcast-new-frame! frame)))))) 27 | 28 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit-malli-schemas/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC5"} 3 | dev.data-star.clojure/http-kit {:mvn/version "1.0.0-RC5"} 4 | dev.data-star.clojure/malli-schemas {:mvn/version "1.0.0-RC5"} 5 | metosin/malli {:mvn/version "0.17.0"}} 6 | :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" 7 | :git/sha "e405aac"} 8 | slipset/deps-deploy {:mvn/version "0.2.2"}} 9 | :ns-default build} 10 | :neil {:project {:name dev.data-star.clojure/http-kit-malli-schemas 11 | :version "1.0.0-RC5" 12 | :description "Malli schemas for the Datastar SDK"}}}} 13 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation/broadcast.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.animation.broadcast 2 | (:require 3 | [bb-example.animation.rendering :as rendering] 4 | [bb-example.animation.state :as state] 5 | [starfederation.datastar.clojure.api :as d*])) 6 | 7 | 8 | (defn send-frame! [sse frame] 9 | (try 10 | (d*/patch-elements! sse frame) 11 | (catch Exception e 12 | (println e)))) 13 | 14 | 15 | (defn broadcast-new-frame! [frame] 16 | (let [sses @state/!conns] 17 | (doseq [sse sses] 18 | (send-frame! sse frame)))) 19 | 20 | 21 | (defn install-watch! [] 22 | (add-watch state/!state ::watch 23 | (fn [_k _ref old new] 24 | (when-not (identical? old new) 25 | (let [frame (rendering/render-content new)] 26 | (broadcast-new-frame! frame)))))) 27 | 28 | -------------------------------------------------------------------------------- /examples/hello-world/src/main/example/server.clj: -------------------------------------------------------------------------------- 1 | (ns example.server 2 | (:require 3 | [example.core :as c] 4 | [ring.adapter.jetty :as jetty]) 5 | (:import 6 | org.eclipse.jetty.server.Server)) 7 | 8 | 9 | (defonce !jetty-server (atom nil)) 10 | 11 | 12 | (defn start! [handler & {:as opts}] 13 | (let [opts (merge {:port 8080 :join? false} 14 | opts)] 15 | (println "Starting server on port:" (:port opts)) 16 | (jetty/run-jetty handler opts))) 17 | 18 | 19 | (defn stop! [server] 20 | (println "Stopping server") 21 | (println server) 22 | (.stop ^Server server)) 23 | 24 | 25 | (defn reboot-jetty-server! [handler & {:as opts}] 26 | (swap! !jetty-server 27 | (fn [server] 28 | (when server 29 | (stop! server)) 30 | (start! handler opts)))) 31 | 32 | (comment 33 | (reboot-jetty-server! #'c/handler)) 34 | -------------------------------------------------------------------------------- /sdk-tests/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | 3 | :deps {datastar/sdk {:local/root "../libraries/sdk/"} 4 | datastar/ring {:local/root "../libraries/sdk-ring/" 5 | :exclusions [dev.data-star.clojure/sdk]} 6 | ring/ring-jetty-adapter {:mvn/version "1.13.0"} 7 | metosin/reitit {:mvn/version "0.7.2"} 8 | com.cnuernber/charred {:mvn/version "1.034"} 9 | dev.onionpancakes/chassis {:mvn/version "1.0.365"}} 10 | 11 | 12 | :aliases 13 | {:repl {:extra-paths ["src/dev"] 14 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 15 | nrepl/nrepl {:mvn/version "1.3.0"} 16 | cider/cider-nrepl {:mvn/version "0.50.2"} 17 | io.github.tonsky/clj-reload {:mvn/version "0.7.1"}}}}} 18 | 19 | -------------------------------------------------------------------------------- /examples/hello-world/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main" "resources"] 2 | 3 | :deps {dev.data-star.clojure/sdk {:local/root "../../libraries/sdk/"} 4 | dev.data-star.clojure/ring {:local/root "../../libraries/sdk-ring/" 5 | :exclusions [dev.data-star.clojure/sdk]} 6 | ring/ring-jetty-adapter {:mvn/version "1.13.0"} 7 | metosin/reitit {:mvn/version "0.7.2"} 8 | dev.onionpancakes/chassis {:mvn/version "1.0.365"} 9 | com.cnuernber/charred {:mvn/version "1.034"}} 10 | 11 | :aliases 12 | {:repl {:extra-paths ["src/dev"] 13 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 14 | nrepl/nrepl {:mvn/version "1.3.0"} 15 | cider/cider-nrepl {:mvn/version "0.50.2"} 16 | io.github.tonsky/clj-reload {:mvn/version "0.7.1"}}}}} 17 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit-malli-schemas/README.md: -------------------------------------------------------------------------------- 1 | # Malli schemas for the SDK 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/http-kit-malli-schemas.svg)](https://clojars.org/dev.data-star.clojure/http-kit-malli-schemas) 8 | 9 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/malli-schemas)](https://cljdoc.org/d/dev.data-star.clojure/malli-schemas/CURRENT) 10 | 11 | ## Overview 12 | 13 | This library provides Malli schemas for Http-kit adapter APIs. 14 | 15 | Require the namespaces for which you want schema and/or instrumentation. Then 16 | use malli's instrumentation facilities. 17 | 18 | Notable schema namespaces: 19 | 20 | - `starfederation.datastar.clojure.adapter.http-kit-schemas` for the 21 | original http-kit adapter 22 | - `starfederation.datastar.clojure.adapter.http-kit2-schemas` for the new 23 | http-kit adapter API 24 | -------------------------------------------------------------------------------- /src/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.repl.deps :as crdeps] 4 | [clojure+.hashp :as hashp] 5 | [clj-reload.core :as reload] 6 | [malli.dev :as mdev])) 7 | 8 | 9 | (alter-var-root #'*warn-on-reflection* (constantly true)) 10 | 11 | 12 | (hashp/install!) 13 | 14 | (reload/init 15 | {:no-reload ['user]}) 16 | 17 | 18 | (defn reload! [] 19 | (reload/reload)) 20 | 21 | 22 | (defn clear-terminal! [] 23 | (binding [*out* (java.io.PrintWriter. System/out)] 24 | (print "\033c") 25 | (flush))) 26 | 27 | 28 | (defmacro force-out [& body] 29 | `(binding [*out* (java.io.OutputStreamWriter. System/out)] 30 | ~@body)) 31 | 32 | 33 | (comment 34 | (mdev/start! {:exception true}) 35 | (mdev/stop!) 36 | (reload!) 37 | *e 38 | (crdeps/sync-deps) 39 | 40 | (-> (System/getProperties) 41 | keys 42 | sort) 43 | 44 | (require '[clojure.tools.build.api :as b]) 45 | b/write-pom 46 | b/copy-dir) 47 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/README.md: -------------------------------------------------------------------------------- 1 | # Malli schemas for the SDK 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/malli-schemas.svg)](https://clojars.org/dev.data-star.clojure/malli-schemas) 8 | 9 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/malli-schemas)](https://cljdoc.org/d/dev.data-star.clojure/malli-schemas/CURRENT) 10 | 11 | ## Overview 12 | 13 | This library provides Malli schemas for the core SDK. 14 | 15 | Require the namespaces for which you want schema and/or instrumentation. Then 16 | use malli's instrumentation facilities. 17 | 18 | Notable schema namespaces: 19 | 20 | - `starfederation.datastar.clojure.api-schemas` for the general d\* API 21 | - `starfederation.datastar.clojure.api.*-schemas` for more specific code 22 | underlying the main API 23 | - `starfederation.datastar.clojure.adapter.common-schemas` for the common 24 | adapter machinery (write profiles) 25 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/redirect3.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.redirect3 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 7 | [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) 8 | 9 | 10 | #_{:clj-kondo/ignore true} 11 | (comment 12 | (require 13 | '[starfederation.datastar.clojure.api :as d*] 14 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 15 | '[some.hiccup.library :refer [html]])) 16 | 17 | 18 | (defn handler [ring-request] 19 | (->sse-response ring-request 20 | {on-open 21 | (fn [sse] 22 | (d*/patch-elements! sse 23 | (html [:div#indicator "Redirecting in 3 seconds..."])) 24 | (Thread/sleep 3000) 25 | (d*/redirect! sse "/guide") 26 | (d*/close-sse! sse))})) 27 | 28 | 29 | (comment 30 | (handler {})) 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/dev/examples/malli.clj: -------------------------------------------------------------------------------- 1 | (ns examples.malli 2 | (:require 3 | [malli.core :as m] 4 | [malli.instrument :as mi] 5 | [malli.dev :as mdev] 6 | [starfederation.datastar.clojure.adapter.test :as at] 7 | [starfederation.datastar.clojure.api :as d*] 8 | [starfederation.datastar.clojure.api.elements :as e] 9 | [starfederation.datastar.clojure.api.elements-schemas] 10 | [starfederation.datastar.clojure.api.common :as c])) 11 | 12 | ;; Testing how instrumentation works and how it's activated 13 | (comment 14 | m/-instrument 15 | (mi/instrument!) 16 | (mi/unstrument!) 17 | (mdev/start!) 18 | (mdev/start! {:exception true}) 19 | (mdev/stop!)) 20 | 21 | (comment 22 | (d*/patch-elements! {} "frag") 23 | (d*/patch-elements! (at/->sse-gen) "frag") 24 | (e/->patch-elements "f" {c/retry-duration :a}) 25 | (e/->patch-elements "f" {c/retry-duration 1022})) 26 | 27 | 28 | (comment 29 | #_{:clj-kondo/ignore true} 30 | (user/reload!) 31 | :help 32 | :dbg 33 | :rec 34 | :stop 35 | :debug) 36 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/redirect1.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.redirect1 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.test :refer [->sse-response]] 7 | [starfederation.datastar.clojure.adapter.common :refer [on-open]])) 8 | 9 | 10 | #_{:clj-kondo/ignore true} 11 | (comment 12 | (require 13 | '[starfederation.datastar.clojure.api :as d*] 14 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 15 | '[some.hiccup.library :refer [html]])) 16 | 17 | 18 | (defn handler [ring-request] 19 | (->sse-response ring-request 20 | {on-open 21 | (fn [sse] 22 | (d*/patch-elements! sse 23 | (html [:div#indicator "Redirecting in 3 seconds..."])) 24 | (Thread/sleep 3000) 25 | (d*/execute-script! sse "window.location = \"/guide\"") 26 | (d*/close-sse! sse))})) 27 | 28 | (comment 29 | (handler {})) 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © Star Federation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip/style.css: -------------------------------------------------------------------------------- 1 | #main-content { 2 | width: 70%; 3 | display: flex; 4 | align-items: flex-start; 5 | justify-content: space-between; 6 | flex-direction: row; 7 | } 8 | 9 | #right-pane { 10 | width: auto; 11 | } 12 | 13 | #controls { 14 | list-style: none; 15 | display: flex; 16 | flex-direction: row; 17 | } 18 | 19 | .h-list { 20 | list-style: none; 21 | display: flex; 22 | flex-direction: row; 23 | gap: 5px; 24 | } 25 | 26 | h2 { 27 | width: 50%; 28 | } 29 | 30 | .pseudo-canvas { 31 | display: grid; 32 | gap: 0; 33 | border: 1px dotted black; 34 | } 35 | 36 | .pseudo-pixel { 37 | min-width: 10px; 38 | width: 10px; 39 | height: 10px; 40 | min-height: 10px; 41 | padding: 0; 42 | margin: 0; 43 | border: 0; 44 | } 45 | 46 | .stack { 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .stack > * + * { 52 | margin-block-start: 1rem; 53 | } 54 | 55 | .center { 56 | box-sizing: content-box; 57 | margin-inline: auto; 58 | max-inline-size: var(--measure); 59 | } 60 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation/style.css: -------------------------------------------------------------------------------- 1 | #main-content { 2 | width: 70%; 3 | display: flex; 4 | align-items: flex-start; 5 | justify-content: space-between; 6 | flex-direction: row; 7 | } 8 | 9 | #right-pane { 10 | width: auto; 11 | } 12 | 13 | #controls { 14 | list-style: none; 15 | display: flex; 16 | flex-direction: row; 17 | } 18 | 19 | .h-list { 20 | list-style: none; 21 | display: flex; 22 | flex-direction: row; 23 | gap: 5px; 24 | } 25 | 26 | h2 { 27 | width: 50%; 28 | } 29 | 30 | .pseudo-canvas { 31 | display: grid; 32 | gap: 0; 33 | border: 1px dotted black; 34 | } 35 | 36 | .pseudo-pixel { 37 | min-width: 10px; 38 | width: 10px; 39 | height: 10px; 40 | min-height: 10px; 41 | padding: 0; 42 | margin: 0; 43 | border: 0; 44 | } 45 | 46 | .stack { 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .stack > * + * { 52 | margin-block-start: 1rem; 53 | } 54 | 55 | .center { 56 | box-sizing: content-box; 57 | margin-inline: auto; 58 | max-inline-size: var(--measure); 59 | } 60 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/redirect2.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.redirect2 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 7 | [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) 8 | 9 | 10 | #_{:clj-kondo/ignore true} 11 | (comment 12 | (require 13 | '[starfederation.datastar.clojure.api :as d*] 14 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 15 | '[some.hiccup.library :refer [html]])) 16 | 17 | 18 | (defn handler [ring-request] 19 | (->sse-response ring-request 20 | {on-open 21 | (fn [sse] 22 | (d*/patch-elements! sse 23 | (html [:div#indicator "Redirecting in 3 seconds..."])) 24 | (Thread/sleep 3000) 25 | (d*/execute-script! sse 26 | "setTimeout(() => window.location = \"/guide\"") 27 | (d*/close-sse! sse))})) 28 | 29 | 30 | (comment 31 | (handler {})) 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/polling1.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.polling1 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 7 | [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) 8 | 9 | 10 | #_{:clj-kondo/ignore true} 11 | (comment 12 | (require 13 | '[starfederation.datastar.clojure.api :as d*] 14 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 15 | '[some.hiccup.library :refer [html]])) 16 | 17 | (import 18 | 'java.time.format.DateTimeFormatter 19 | 'java.time.LocalDateTime) 20 | 21 | (def formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) 22 | 23 | (defn handler [ring-request] 24 | (->sse-response ring-request 25 | {on-open 26 | (fn [sse] 27 | (d*/patch-elements! sse 28 | (html [:div#time {:data-on:interval__duration.5s (d*/sse-get "/endpoint")} 29 | (LocalDateTime/.format (LocalDateTime/now) formatter)])) 30 | (d*/close-sse! sse))})) 31 | 32 | (comment 33 | (handler {})) 34 | 35 | 36 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc/languages ["clj"] 2 | :cljdoc.doc/tree 3 | [["Readme" {:file "README.md"}] 4 | ["Changelog" {:file "CHANGELOG.md"}] 5 | 6 | ["SDK docs" 7 | ["Using Datastar" {:file "doc/Using-datastar.md"}] 8 | ["A tour of the API" {:file "doc/Api.md"}] 9 | ["SDK Libraries" {:file "doc/Libraries.md"} 10 | ["Core SDK" {:file "libraries/sdk/README.md"}] 11 | ["Http-kit adapter" {:file "libraries/sdk-http-kit/README.md"}] 12 | ["Ring adapter" {:file "libraries/sdk-ring/README.md"}] 13 | ["Core Malli Schemas" {:file "libraries/sdk-malli-schemas/README.md"}] 14 | ["Http-kit Malli Schemas" {:file "libraries/sdk-http-kit-malli-schemas/README.md"}] 15 | ["Ring Malli Schemas" {:file "libraries/sdk-ring-malli-schemas/README.md"}] 16 | ["Brotli" {:file "libraries/sdk-brotli/README.md"}]] 17 | ["Write profiles" {:file "doc/Write-profiles.md"}]] 18 | 19 | ["Internals and advanced topics" 20 | ["SSE design notes" {:file "doc/SSE-design-notes.md"}] 21 | ["Maintainer's guide" {:file "doc/maintainers-guide.md"}] 22 | ["Adapter implemation guide" {:file "doc/implementing-adapters.md"}]]]} 23 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.protocols) 2 | 3 | 4 | (defprotocol SSEGenerator 5 | (send-event! [this event-type data-lines opts] "Send sse event.") 6 | (get-lock [this] "Access to the lock used in the generator.") 7 | (close-sse! [this] "Close connection.") 8 | (sse-gen? [this] "Test wheter a value is a SSEGenerator.")) 9 | 10 | 11 | (defn throw-not-implemented [type method] 12 | (throw (ex-info (str "Type " type " is not a SSEGenerator.") {:type type :method method}))) 13 | 14 | 15 | (extend-protocol SSEGenerator 16 | nil 17 | (sse-gen? [_] false) 18 | 19 | (send-event! [_this _event-type _data-lines _opts] 20 | (throw-not-implemented nil :send-event!)) 21 | 22 | (get-lock [_this] 23 | (throw-not-implemented nil :get-lock)) 24 | 25 | (close-sse! [_this] 26 | (throw-not-implemented nil :close-sse!)) 27 | 28 | 29 | Object 30 | (sse-gen? [_] false) 31 | 32 | (send-event! [_this _event-type _data-lines _opts] 33 | (throw-not-implemented Object :send-event!)) 34 | 35 | (get-lock [_this] 36 | (throw-not-implemented Object :get-lock)) 37 | 38 | (close-sse! [_this] 39 | (throw-not-implemented Object :close-sse!))) 40 | 41 | -------------------------------------------------------------------------------- /src/dev/examples/forms/core.clj: -------------------------------------------------------------------------------- 1 | (ns examples.forms.core 2 | (:require 3 | [examples.common :as c] 4 | [examples.forms.html :as efh] 5 | [examples.forms.datastar :as efd*] 6 | [dev.onionpancakes.chassis.core :as h] 7 | [examples.utils :as u] 8 | [ring.util.response :as rur] 9 | [reitit.ring :as rr])) 10 | 11 | ;; We test here several ways to manage forms, whether the plain HTML way 12 | ;; or using D* 13 | 14 | (def home 15 | (h/html 16 | (c/page-scaffold 17 | [[:h1 "Forms Forms Forms"] 18 | [:ul 19 | [:li [:a {:href "/html/get"} "html GET example"]] 20 | [:li [:a {:href "/html/post"}"html POST example"]] 21 | [:li [:a {:href "/datastar/get"}"html GET example"]] 22 | [:li [:a {:href "/datastar/post"}"html POST example"]]]]))) 23 | 24 | (def router 25 | (rr/router 26 | [["/" {:handler (constantly (rur/response home))}] 27 | ["" efh/routes] 28 | ["" efd*/routes] 29 | c/datastar-route])) 30 | 31 | 32 | 33 | (def handler 34 | (rr/ring-handler router (rr/create-default-handler))) 35 | 36 | (defn after-ns-reload [] 37 | (println "rebooting server") 38 | (u/reboot-hk-server! #'handler)) 39 | 40 | (comment 41 | (u/reboot-hk-server! #'handler)) 42 | -------------------------------------------------------------------------------- /sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/main.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.sdk-test.main 2 | (:require 3 | [ring.adapter.jetty :as jetty] 4 | [starfederation.datastar.clojure.sdk-test.core :as c]) 5 | (:import 6 | org.eclipse.jetty.server.Server)) 7 | 8 | 9 | 10 | (defonce !jetty-server (atom nil)) 11 | 12 | 13 | (def default-server-opts {:port 7331 14 | :join? false}) 15 | 16 | (defn start! [handler & {:as opts}] 17 | (let [opts (merge default-server-opts opts)] 18 | (println "Starting server port" (:port opts)) 19 | (jetty/run-jetty handler opts))) 20 | 21 | 22 | (defn stop! [server] 23 | (println "Stopping server") 24 | (println server) 25 | (.stop ^Server server)) 26 | 27 | 28 | (defn reboot-jetty-server! [handler & {:as opts}] 29 | (swap! !jetty-server 30 | (fn [server] 31 | (when server 32 | (stop! server)) 33 | (start! handler opts)))) 34 | 35 | 36 | (comment 37 | (reboot-jetty-server! #'c/handler)) 38 | 39 | (defn -main [& _] 40 | (let [server (start! c/handler)] 41 | (.addShutdownHook (Runtime/getRuntime) 42 | (Thread. (fn [] 43 | (stop! server) 44 | (shutdown-agents)))))) 45 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/common.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.common 2 | (:require 3 | [cheshire.core :as json] 4 | [clojure.java.io :as io] 5 | [hiccup2.core :as h] 6 | [starfederation.datastar.clojure.api :as d*] 7 | [starfederation.datastar.clojure.consts :as consts])) 8 | 9 | (defn parse-signals [signals] 10 | (if (string? signals) 11 | (json/parse-string signals) 12 | (-> signals 13 | io/reader 14 | json/parse-stream))) 15 | 16 | 17 | (defn get-signals [req] 18 | (some-> req d*/get-signals parse-signals)) 19 | 20 | 21 | 22 | (defn GET [path handler] 23 | {:path path 24 | :method :get 25 | :response handler}) 26 | 27 | 28 | (defn POST [path handler] 29 | {:path path 30 | :method :post 31 | :response handler}) 32 | 33 | 34 | 35 | 36 | (defn response [body] 37 | {:status 200 38 | :body body}) 39 | 40 | 41 | 42 | (def cdn-url 43 | (str "https://cdn.jsdelivr.net/gh/starfederation/datastar@" 44 | consts/version 45 | "/bundles/datastar.js")) 46 | 47 | 48 | 49 | (defn page-scaffold [body] 50 | (h/html 51 | (h/raw "") 52 | [:html 53 | [:head 54 | [:meta {:charset "UTF-8"}] 55 | [:script {:type "module" 56 | :src cdn-url}]] 57 | [:body body]])) 58 | -------------------------------------------------------------------------------- /src/test/adapter-common/test/examples/common.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.common 2 | (:require 3 | [dev.onionpancakes.chassis.core :as h] 4 | [dev.onionpancakes.chassis.compiler :as hc] 5 | [ring.middleware.params :as rmp] 6 | [ring.middleware.multipart-params :as rmpp] 7 | [reitit.ring :as rr] 8 | [starfederation.datastar.clojure.api :as d*])) 9 | 10 | 11 | 12 | (defn script [type src] 13 | [:script {:type type :src src}]) 14 | 15 | 16 | (def datastar 17 | (script "module" d*/CDN-url)) 18 | 19 | 20 | (defn scaffold [content & {:as _}] 21 | (hc/compile 22 | [h/doctype-html5 23 | [:html 24 | [:head 25 | [:meta {:charset "UTF-8"}] 26 | datastar] 27 | [:body content]]])) 28 | 29 | 30 | ;; ----------------------------------------------------------------------------- 31 | ;; Common Handler stuff 32 | ;; ----------------------------------------------------------------------------- 33 | (def wrap-params 34 | {:name ::wrap-params 35 | :description "Ring param extraction middleware." 36 | :wrap rmp/wrap-params}) 37 | 38 | 39 | (def wrap-mpparams 40 | {:name ::wrap-multipart-params 41 | :description "Ring multipart param extraction middleware." 42 | :wrap rmpp/wrap-multipart-params}) 43 | 44 | (def global-middleware 45 | [[wrap-params]]) 46 | 47 | 48 | (def default-handler 49 | (rr/create-default-handler)) 50 | 51 | 52 | -------------------------------------------------------------------------------- /libraries/sdk-brotli/README.md: -------------------------------------------------------------------------------- 1 | # Datastar Brotli write profile 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/brotli.svg)](https://clojars.org/dev.data-star.clojure/brotli) 8 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/brotli)](https://cljdoc.org/d/dev.data-star.clojure/brotli/CURRENT) 9 | 10 | This library contains some utilities to work with Brotli. 11 | 12 | Credits to [Anders](https://andersmurphy.com/) and his work on [Hyperlith](https://github.com/andersmurphy/hyperlith) 13 | from which this library takes it's code. 14 | 15 | > [!IMPORTANT] 16 | > At this moment only Http-kit is supported. 17 | 18 | ## Overview 19 | 20 | This library provides Brotli write profiles you can use like this: 21 | 22 | ```clojure 23 | (require 24 | '[starfederation.datastar.clojure.api :as d*] 25 | '[starfederation.datastar.clojure.adapter.Http-kit :as hk-gen] 26 | '[starfederation.datastar.clojure.brotli :as brotli]) 27 | 28 | (defn handler [req] 29 | (hk-gen/->sse-response 30 | {hk-gen/write-profile (brotli/->brotli-profile) 31 | hk-gen/on-open 32 | (fn [sse] 33 | (d*/with-open-sse sse 34 | (do ...)) 35 | ``` 36 | 37 | See docstrings in the `starfederation.datastar.clojure.brotli` namespace for 38 | more information. 39 | -------------------------------------------------------------------------------- /src/dev/examples/scripts.clj: -------------------------------------------------------------------------------- 1 | (ns examples.scripts 2 | (:require 3 | [examples.common :as c] 4 | [examples.utils :as u] 5 | [dev.onionpancakes.chassis.core :as h] 6 | [reitit.ring :as rr] 7 | [reitit.ring.middleware.parameters :as reitit-params] 8 | [ring.util.response :as ruresp] 9 | [starfederation.datastar.clojure.api :as d*] 10 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) 11 | 12 | ;; Sending scripts and playing with auto-remove 13 | 14 | (def page 15 | (h/html 16 | (c/page-scaffold 17 | [[:h1 "Test page"] 18 | [:button {:data-on:click (d*/sse-get "/endpoint")} 19 | "Say hello!"]]))) 20 | 21 | 22 | (defn home [_] 23 | (ruresp/response page)) 24 | 25 | 26 | (defn endpoint [req] 27 | (hk-gen/->sse-response req 28 | {hk-gen/on-open 29 | (fn [sse] 30 | (d*/with-open-sse sse 31 | (d*/execute-script! sse 32 | "console.log('hello')" 33 | {d*/auto-remove false})))})) 34 | 35 | 36 | (def router (rr/router 37 | [["/" {:handler home}] 38 | ["/endpoint" {:handler endpoint}]])) 39 | 40 | 41 | (def default-handler (rr/create-default-handler)) 42 | 43 | 44 | (def handler 45 | (rr/ring-handler router 46 | default-handler 47 | {:middleware [reitit-params/parameters-middleware]})) 48 | 49 | 50 | (comment 51 | (u/reboot-hk-server! #'handler)) 52 | -------------------------------------------------------------------------------- /src/dev/examples/http_kit_disconnect.clj: -------------------------------------------------------------------------------- 1 | (ns examples.http-kit-disconnect 2 | (:require 3 | [examples.utils :as u] 4 | [reitit.ring :as rr] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open on-close]])) 7 | 8 | 9 | ;; This is a small experiment to determine the behaviour of 10 | ;; Http-kit in the face of the client disconnecting 11 | ;; Http-kit somehow detects closed connections on it's own 12 | 13 | (def !conn (atom nil)) 14 | 15 | (defn long-connection [req] 16 | (->sse-response req 17 | {on-open 18 | (fn [sse] 19 | (reset! !conn sse) 20 | (d*/console-log! sse "'connected'")) 21 | on-close 22 | (fn on-close [_ status-code] 23 | (println "-----------------") 24 | (println "Connection closed status: " status-code) 25 | (println "-----------------"))})) 26 | 27 | 28 | (def routes 29 | [["/persistent" {:handler long-connection}]]) 30 | 31 | 32 | (def router 33 | (rr/router routes)) 34 | 35 | 36 | (def default-handler (rr/create-default-handler)) 37 | 38 | 39 | (def handler 40 | (rr/ring-handler router 41 | default-handler)) 42 | 43 | (defn send-tiny-event! [] 44 | (d*/console-log! @!conn "'toto'")) 45 | 46 | 47 | ;; curl -vv http://localhost:8080/persistent 48 | (comment 49 | (-> !conn deref d*/close-sse!) 50 | (send-tiny-event!) 51 | (d*/console-log! @!conn "'toto'") 52 | 53 | (u/clear-terminal!) 54 | (u/reboot-hk-server! #'handler)) 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/dev/examples/broadcast_http_kit.clj: -------------------------------------------------------------------------------- 1 | (ns examples.broadcast-http-kit 2 | (:require 3 | [examples.utils :as u] 4 | [reitit.ring :as rr] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open on-close]])) 7 | 8 | 9 | ;; Tiny setup for that allows broadcasting events to several curl processes 10 | 11 | (defonce !conns (atom #{})) 12 | 13 | (defn long-connection [req] 14 | (->sse-response req 15 | {on-open 16 | (fn [sse] 17 | (swap! !conns conj sse) 18 | (d*/console-log! sse "'connected'")) 19 | on-close 20 | (fn on-close [sse status-code] 21 | (swap! !conns disj sse) 22 | (println "Connection closed status: " status-code) 23 | (println "remove connection from pool"))})) 24 | 25 | 26 | (def routes 27 | [["/persistent" {:handler long-connection}]]) 28 | 29 | 30 | (def router 31 | (rr/router routes)) 32 | 33 | 34 | (def default-handler (rr/create-default-handler)) 35 | 36 | 37 | (def handler 38 | (rr/ring-handler router 39 | default-handler)) 40 | 41 | (defn broadcast-number! [n] 42 | (doseq [conn @!conns] 43 | (try 44 | (d*/console-log! conn (str "n: " n)) 45 | (catch Exception e 46 | (println "Error: " e))))) 47 | 48 | 49 | 50 | ;; open several clients: 51 | ;; curl -vv http://localhost:8080/persistent 52 | (comment 53 | (-> !conns deref first d*/close-sse!) 54 | (broadcast-number! (rand-int 25)) 55 | (u/clear-terminal!) 56 | (u/reboot-hk-server! #'handler)) 57 | 58 | -------------------------------------------------------------------------------- /libraries/sdk/README.md: -------------------------------------------------------------------------------- 1 | # Generic Clojure SDK for Datastar 2 | 3 | ## Installation 4 | 5 | The SDK provided adapter libraries already depend on this library. However, 6 | if you want to [develop your own SSEGenerator](/doc/implementing-adapters.md) 7 | you'll need to depend on: 8 | 9 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/sdk.svg)](https://clojars.org/dev.data-star.clojure/sdk) 10 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/sdk)](https://cljdoc.org/d/dev.data-star.clojure/sdk/CURRENT) 11 | 12 | ## Overview 13 | 14 | Datastar SDKs in each language follow an 15 | [Architecture Decision Record](https://github.com/starfederation/datastar/blob/develop/sdk/ADR.md) 16 | and the Clojure SDK is no exception. This ADR describes a general mechanism to 17 | manage SSE streams called a ServerSentEventGenerator and functions using it to 18 | send SSE event formatted the way the Datastar expect them in the browser. 19 | 20 | This library is a generic implementation of the ADR. It contains the code for: 21 | 22 | - building blocks to manage SSE streams 23 | - a clojure protocol `starfederation.datastar.clojure.protocols/SSEGenerator` 24 | allowing for the implementation of SSE generators for different Ring adapters. 25 | - functions based on the protocol for working with SSE generators. 26 | - helpers allowing to send Javascript scripts to run in the browser 27 | - a generic mechanism called [write profiles](/doc/Write-profiles.md) to manage 28 | the buffering behavior and compression of SSE streams 29 | - several write profiles providing gzip compression 30 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/polling2.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.polling2 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 7 | [starfederation.datastar.clojure.adapter.test :as at :refer [->sse-response]])) 8 | 9 | #_{:clj-kondo/ignore true} 10 | (comment 11 | (require 12 | '[starfederation.datastar.clojure.api :as d*] 13 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 14 | '[some.hiccup.library :refer [html]])) 15 | 16 | (import 17 | 'java.time.format.DateTimeFormatter 18 | 'java.time.LocalDateTime) 19 | 20 | (def date-time-formatter (DateTimeFormatter/ofPattern "YYYY-MM-DD HH:mm:ss")) 21 | (def seconds-formatter (DateTimeFormatter/ofPattern "ss")) 22 | 23 | (defn handler [ring-request] 24 | (->sse-response ring-request 25 | {on-open 26 | (fn [sse] 27 | (let [now (LocalDateTime/now) 28 | current-time (LocalDateTime/.format now date-time-formatter) 29 | seconds (LocalDateTime/.format now seconds-formatter) 30 | duration (if (neg? (compare seconds "50")) 31 | "5" 32 | "1")] 33 | (d*/patch-elements! sse 34 | (html [:div#time {(str "data-on:interval__duration." duration "s") 35 | (d*/sse-get "/endpoint")} 36 | current-time])) 37 | 38 | (d*/close-sse! sse)))})) 39 | 40 | 41 | (comment 42 | (handler {})) 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/hello-world/src/main/example/core.clj: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.string :as string] 5 | [dev.onionpancakes.chassis.compiler :as hc] 6 | [dev.onionpancakes.chassis.core :as h] 7 | [example.utils :as u] 8 | [reitit.ring.middleware.parameters :as rmparams] 9 | [reitit.ring :as rr] 10 | [ring.util.response :as ruresp] 11 | [starfederation.datastar.clojure.api :as d*] 12 | [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]])) 13 | 14 | 15 | (def home-page 16 | (-> (io/resource "public/hello-world.html") 17 | slurp 18 | (string/split-lines) 19 | (->> (drop 3) 20 | (apply str)))) 21 | 22 | 23 | (defn home [_] 24 | (-> home-page 25 | (ruresp/response) 26 | (ruresp/content-type "text/html"))) 27 | 28 | 29 | (def message "Hello, world!") 30 | 31 | (def msg-count (count message)) 32 | 33 | 34 | (defn ->frag [i] 35 | (h/html 36 | (hc/compile 37 | [:div {:id "message"} 38 | (subs message 0 (inc i))]))) 39 | 40 | 41 | 42 | (defn hello-world [request] 43 | (let [d (-> request u/get-signals (get "delay") int)] 44 | (->sse-response request 45 | {on-open 46 | (fn [sse] 47 | (d*/with-open-sse sse 48 | (dotimes [i msg-count] 49 | (d*/patch-elements! sse (->frag i)) 50 | (Thread/sleep d))))}))) 51 | 52 | 53 | (def routes 54 | [["/" {:handler home}] 55 | ["/hello-world" {:handler hello-world 56 | :middleware [rmparams/parameters-middleware]}]]) 57 | 58 | (def router (rr/router routes)) 59 | 60 | (def handler (rr/ring-handler router)) 61 | 62 | -------------------------------------------------------------------------------- /src/dev/examples/redirect.clj: -------------------------------------------------------------------------------- 1 | (ns examples.redirect 2 | (:require 3 | [examples.common :as c] 4 | [examples.utils :as u] 5 | [dev.onionpancakes.chassis.core :refer [html]] 6 | [reitit.ring :as rr] 7 | [ring.util.response :as ruresp] 8 | [starfederation.datastar.clojure.api :as d*] 9 | [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) 10 | 11 | ;; Redirection example 12 | 13 | (def home-page 14 | (html 15 | (c/page-scaffold 16 | [[:h1 "Test page"] 17 | [:div.#indicator 18 | [:button {:data-on:click (d*/sse-get "/redirect-me")} 19 | "Start redirect"]]]))) 20 | 21 | 22 | (defn home [_] 23 | (ruresp/response home-page)) 24 | 25 | 26 | (def guide-page 27 | (html 28 | (c/page-scaffold 29 | [[:h1 "You have been redirected"] 30 | [:a {:href "/" } "Home"]]))) 31 | 32 | 33 | (defn guide [_] 34 | (ruresp/response guide-page)) 35 | 36 | 37 | (defn redirect-handler [ring-request] 38 | (->sse-response ring-request 39 | {on-open 40 | (fn [sse] 41 | (d*/patch-elements! sse 42 | (html [:div#indicator "Redirecting in 3 seconds..."])) 43 | (Thread/sleep 3000) 44 | (d*/redirect! sse "/guide") 45 | (d*/close-sse! sse))})) 46 | 47 | 48 | 49 | 50 | (def router (rr/router 51 | [["/" {:handler home}] 52 | ["/guide" {:handler guide}] 53 | ["/redirect-me" {:handler redirect-handler}]])) 54 | 55 | 56 | (def default-handler (rr/create-default-handler)) 57 | 58 | 59 | (def handler 60 | (rr/ring-handler router default-handler)) 61 | 62 | 63 | 64 | (comment 65 | (u/reboot-hk-server! #'handler)) 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/dev/examples/forms/html.clj: -------------------------------------------------------------------------------- 1 | (ns examples.forms.html 2 | (:require 3 | [examples.common :as c] 4 | [dev.onionpancakes.chassis.core :as h] 5 | [examples.utils :as u] 6 | [ring.util.response :as rur] 7 | [reitit.ring.middleware.parameters :as params])) 8 | 9 | 10 | 11 | (defn page-get [result] 12 | (h/html 13 | (c/page-scaffold 14 | [:div 15 | [:h2 "Html GET form"] 16 | [:form {:action "" :method "GET"} 17 | [:input {:type "text" 18 | :id "input1" 19 | :name "input1" 20 | :data-bind:input1 true}] 21 | [:button "submit"]] 22 | [:div result]]))) 23 | 24 | (defn get-home [req] 25 | (let [v (get-in req [:params "input1"])] 26 | (u/clear-terminal!) 27 | (u/?req req) 28 | (println "got here " v) 29 | (rur/response (page-get v)))) 30 | 31 | 32 | (defn page-post [result] 33 | (h/html 34 | (c/page-scaffold 35 | [:div 36 | [:h2 "Html POST form !!!!"] 37 | [:form {:action "" :method "POST"} 38 | [:input {:type "text" 39 | :id "input1" 40 | :name "input1" 41 | :data-bind:input1 true}] 42 | [:button "submit"]] 43 | [:div result]]))) 44 | 45 | 46 | (defn post-home [req] 47 | (let [v (get-in req [:params "input1"])] 48 | (u/clear-terminal!) 49 | (u/?req req) 50 | (println "got here " v) 51 | (rur/response (page-post v)))) 52 | 53 | 54 | 55 | (def routes 56 | ["/html" 57 | ["/get" {:handler #'get-home 58 | :middleware [params/parameters-middleware]}] 59 | ["/post" {:handler #'post-home 60 | :middleware [params/parameters-middleware]}]]) 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/broadcast.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.broadcast 2 | (:require 3 | [bb-example.core :as core] 4 | [org.httpkit.server :as hk] 5 | [ruuter.core :as ruuter] 6 | [starfederation.datastar.clojure.api :as d*] 7 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen :refer [->sse-response on-open on-close]] 8 | 9 | [starfederation.datastar.clojure.api-schemas] 10 | [starfederation.datastar.clojure.adapter.http-kit-schemas])) 11 | 12 | 13 | ;; Tiny setup for that allows broadcasting events to several curl processes 14 | 15 | (defonce !conns (atom #{})) 16 | 17 | (defn long-connection [req] 18 | (->sse-response req 19 | {on-open 20 | (fn [sse] 21 | (swap! !conns conj sse) 22 | (d*/console-log! sse "'connected'")) 23 | on-close 24 | (fn on-close [sse status-code] 25 | (swap! !conns disj sse) 26 | (println "Connection closed status: " status-code) 27 | (println "remove connection from pool"))})) 28 | 29 | 30 | (def routes 31 | [{:path"/" 32 | :method :get 33 | :response long-connection}]) 34 | 35 | 36 | 37 | (defn handler [req] 38 | (ruuter/route routes req)) 39 | 40 | (defn broadcast-number! [n] 41 | (doseq [conn @!conns] 42 | (try 43 | (d*/console-log! conn (str "n: " n)) 44 | (catch Exception e 45 | (println "Error: " e))))) 46 | 47 | 48 | ;; open several clients: 49 | ;; curl -vv http://localhost:8080/persistent 50 | (comment 51 | (core/start! #'handler) 52 | !conns 53 | (some-> !conns deref first d*/close-sse!) 54 | (broadcast-number! (rand-int 25))) 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/dev/examples/snippets.clj: -------------------------------------------------------------------------------- 1 | (ns examples.snippets 2 | (:require 3 | [starfederation.datastar.clojure.api :as d*] 4 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 5 | [starfederation.datastar.clojure.adapter.test :as at :refer [->sse-response]])) 6 | 7 | 8 | ;; Snippets used in the website docs 9 | 10 | (def sse (at/->sse-gen)) 11 | 12 | ;; multiple_events 13 | (d*/patch-elements! sse "
...
") 14 | (d*/patch-elements! sse "
...
") 15 | (d*/patch-signals! sse "{answer: '...'}") 16 | (d*/patch-signals! sse "{prize: '...'}") 17 | 18 | ;; setup 19 | #_{:clj-kondo/ignore true} 20 | (comment 21 | (require 22 | '[starfederation.datastar.clojure.api :as d*] 23 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) 24 | 25 | 26 | (defn handler [request] 27 | (->sse-response request 28 | {on-open 29 | (fn [sse] 30 | (d*/patch-elements! sse 31 | "
What do you put in a toaster?
") 32 | 33 | (d*/patch-signals! sse "{response: '', answer: 'bread'}"))})) 34 | 35 | (comment 36 | (handler {})) 37 | 38 | ;; multiple_events going deeper 39 | #_{:clj-kondo/ignore true} 40 | (comment 41 | (require 42 | '[starfederation.datastar.clojure.api :as d*] 43 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) 44 | 45 | 46 | #_{:clj-kondo/ignore true} 47 | (defn handler [request] 48 | (->sse-response request 49 | {on-open 50 | (fn [sse] 51 | (d*/patch-elements! sse "
Hello, world!
") 52 | (d*/patch-signals! sse "{foo: {bar: 1}}") 53 | (d*/execute-script! sse "console.log('Success!')"))})) 54 | -------------------------------------------------------------------------------- /src/test/adapter-ring/test/examples/ring_handler.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.ring-handler 2 | (:require 3 | [test.examples.common :as common] 4 | [test.examples.counter :as counter] 5 | [test.examples.form :as form] 6 | [starfederation.datastar.clojure.adapter.ring :as jetty-gen] 7 | [reitit.ring :as rr])) 8 | 9 | ;; ----------------------------------------------------------------------------- 10 | ;; counters 11 | ;; ----------------------------------------------------------------------------- 12 | (def update-signal (counter/->update-signal jetty-gen/->sse-response)) 13 | 14 | (defn increment 15 | ([req] 16 | (update-signal req inc)) 17 | ([req respond _] 18 | (respond (update-signal req inc)))) 19 | 20 | 21 | (defn decrement 22 | ([req] 23 | (update-signal req dec)) 24 | ([req respond _] 25 | (respond (update-signal req dec)))) 26 | 27 | 28 | (def counter-routes 29 | ["/counters/" 30 | ["" {:handler #'counter/counters}] 31 | ["increment/:id" #'increment] 32 | ["decrement/:id" #'decrement]]) 33 | 34 | 35 | ;; ----------------------------------------------------------------------------- 36 | ;; Form 37 | ;; ----------------------------------------------------------------------------- 38 | (def endpoint (form/->endpoint jetty-gen/->sse-response)) 39 | 40 | 41 | (def form-routes 42 | ["/form" 43 | ["" {:handler #'form/form}] 44 | ["/endpoint" {:middleware [common/wrap-mpparams] 45 | :handler #'endpoint}]]) 46 | 47 | 48 | (def router 49 | (rr/router 50 | [counter-routes 51 | form-routes])) 52 | 53 | 54 | (def handler 55 | (rr/ring-handler router 56 | common/default-handler 57 | {:middleware common/global-middleware})) 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/adapter-http-kit/test/examples/http_kit_handler.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.http-kit-handler 2 | (:require 3 | [test.examples.common :as common] 4 | [test.examples.counter :as counters] 5 | [test.examples.form :as form] 6 | [reitit.ring :as rr] 7 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) 8 | 9 | 10 | ;; ----------------------------------------------------------------------------- 11 | ;; Counters 12 | ;; ----------------------------------------------------------------------------- 13 | (def update-signal (counters/->update-signal hk-gen/->sse-response)) 14 | 15 | 16 | (defn increment 17 | ([req] 18 | (update-signal req inc)) 19 | ([req respond _raise] 20 | (respond (increment req)))) 21 | 22 | 23 | (defn decrement 24 | ([req] 25 | (update-signal req dec)) 26 | ([req respond _raise] 27 | (respond (decrement req)))) 28 | 29 | 30 | (def counter-routes 31 | ["/counters/" 32 | ["" {:handler #'counters/counters}] 33 | ["increment/:id" #'increment] 34 | ["decrement/:id" #'decrement]]) 35 | 36 | 37 | 38 | ;; ----------------------------------------------------------------------------- 39 | ;; Form 40 | ;; ----------------------------------------------------------------------------- 41 | (def endpoint (form/->endpoint hk-gen/->sse-response)) 42 | 43 | 44 | (def form-routes 45 | ["/form" 46 | ["" {:handler #'form/form}] 47 | ["/endpoint" {:middleware [common/wrap-mpparams] 48 | :handler #'endpoint}]]) 49 | 50 | 51 | 52 | 53 | (def router 54 | (rr/router 55 | [counter-routes 56 | form-routes])) 57 | 58 | 59 | (def handler 60 | (rr/ring-handler router 61 | common/default-handler 62 | {:middleware common/global-middleware})) 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/test/adapter-http-kit/test/examples/http_kit_handler2.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.http-kit-handler2 2 | (:require 3 | [test.examples.common :as common] 4 | [test.examples.counter :as counters] 5 | [test.examples.form :as form] 6 | [reitit.ring :as rr] 7 | [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) 8 | 9 | 10 | ;; ----------------------------------------------------------------------------- 11 | ;; Counters 12 | ;; ----------------------------------------------------------------------------- 13 | (def update-signal (counters/->update-signal hk-gen/->sse-response)) 14 | 15 | 16 | (defn increment 17 | ([req] 18 | (update-signal req inc)) 19 | ([req respond _raise] 20 | (respond (increment req)))) 21 | 22 | 23 | (defn decrement 24 | ([req] 25 | (update-signal req dec)) 26 | ([req respond _raise] 27 | (respond (decrement req)))) 28 | 29 | 30 | (def counter-routes 31 | ["/counters/" 32 | ["" {:handler #'counters/counters}] 33 | ["increment/:id" #'increment] 34 | ["decrement/:id" #'decrement]]) 35 | 36 | 37 | 38 | ;; ----------------------------------------------------------------------------- 39 | ;; Form 40 | ;; ----------------------------------------------------------------------------- 41 | (def endpoint (form/->endpoint hk-gen/->sse-response)) 42 | 43 | 44 | (def form-routes 45 | ["/form" 46 | ["" {:handler #'form/form}] 47 | ["/endpoint" {:middleware [common/wrap-mpparams] 48 | :handler #'endpoint}]]) 49 | 50 | 51 | 52 | 53 | (def router 54 | (rr/router 55 | [counter-routes 56 | form-routes])) 57 | 58 | 59 | (def handler 60 | (rr/ring-handler router 61 | common/default-handler 62 | {:middleware (into [[hk-gen/start-responding-middleware]] common/global-middleware)})) 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/test/adapter-common/test/utils.clj: -------------------------------------------------------------------------------- 1 | (ns test.utils 2 | (:require 3 | [charred.api :as charred]) 4 | (:import 5 | java.io.StringWriter 6 | java.net.ServerSocket)) 7 | 8 | ;; ----------------------------------------------------------------------------- 9 | ;; JSON helpers 10 | ;; ----------------------------------------------------------------------------- 11 | (def ^:private bufSize 1024) 12 | (def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) 13 | 14 | (def ^:private write-json* (charred/write-json-fn {})) 15 | 16 | (defn- write-json [s] 17 | (let [out (StringWriter.)] 18 | (write-json* out s) 19 | (.toString out))) 20 | 21 | (comment 22 | (-> {"1" 2} 23 | (write-json) 24 | (read-json)) 25 | := {"1" 2}) 26 | 27 | 28 | ;; ----------------------------------------------------------------------------- 29 | ;; Http servers helpers 30 | ;; ----------------------------------------------------------------------------- 31 | (defn free-port! [] 32 | (with-open [socket (ServerSocket. 0)] 33 | (.getLocalPort socket))) 34 | 35 | 36 | (defn url [port path] 37 | (format "http://localhost:%s/%s" port path)) 38 | 39 | 40 | 41 | (defn sanitize-opts [opts] 42 | (-> opts 43 | (update :port #(or % (free-port!))) 44 | (dissoc :start! :stop!))) 45 | 46 | 47 | (defmacro with-server 48 | "Setup a server 49 | 50 | Opts: 51 | - `:start!`: mandatory 52 | - `:stop!`: mandatory " 53 | [server-name handler opts & body] 54 | `(let [opts# ~opts 55 | {start!# :start! 56 | stop!# :stop!} opts# 57 | sanitized-opts# (sanitize-opts opts#) 58 | ~server-name (start!# ~handler sanitized-opts#)] 59 | (try 60 | ~@body 61 | (finally 62 | (stop!# ~server-name))))) 63 | 64 | 65 | (comment 66 | (macroexpand-1 67 | '(with-server serv h {:port 123456} 68 | (do1) 69 | (do2)))) 70 | 71 | -------------------------------------------------------------------------------- /src/dev/examples/jetty_disconnect.clj: -------------------------------------------------------------------------------- 1 | (ns examples.jetty-disconnect 2 | (:require 3 | [examples.utils :as u] 4 | [reitit.ring :as rr] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open on-close]])) 7 | 8 | 9 | ;; This is a small experiment to determine the behaviour of 10 | ;; ring jetty in the face of the client disconnecting 11 | 12 | 13 | ;; 2 tiny events to detect a lost connection or 1 big event 14 | ;; Jetty internal buffer has an impact 15 | 16 | (def !conn (atom nil)) 17 | 18 | (def big-message 19 | (let [b (StringBuilder.)] 20 | (doseq [i (range 10000)] 21 | (doto ^StringBuilder b 22 | (.append (str "-------------" i "-----------------\n")))) 23 | (str b))) 24 | 25 | 26 | (defn long-connection [req respond raise] 27 | (try 28 | (respond 29 | (->sse-response req 30 | {on-open 31 | (fn [sse] 32 | (reset! !conn sse) 33 | (d*/console-log! sse "'connected'")) 34 | on-close 35 | (fn [_sse] 36 | (println "Connection lost detected") 37 | (reset! !conn nil))})) 38 | (catch Exception e 39 | (raise e)))) 40 | 41 | 42 | (def routes 43 | [["/persistent" {:handler long-connection}]]) 44 | 45 | 46 | (def router 47 | (rr/router routes)) 48 | 49 | 50 | (def default-handler (rr/create-default-handler)) 51 | 52 | 53 | (def handler 54 | (rr/ring-handler router 55 | default-handler)) 56 | 57 | (defn send-tiny-event! [] 58 | (d*/console-log! @!conn "'toto'")) 59 | 60 | 61 | (defn send-big-event! [] 62 | (d*/patch-elements! @!conn big-message)) 63 | 64 | ;; curl -vv http://localhost:8081/persistent 65 | (comment 66 | (-> !conn deref d*/close-sse!) 67 | (send-tiny-event!) 68 | (send-big-event!) 69 | 70 | (u/clear-terminal!) 71 | (u/reboot-jetty-server! #'handler {:async? true})) 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/hello-httpkit/resources/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Datastar SDK Demo 5 | 6 | 7 | 8 | 9 |
10 |
11 |

12 | Datastar SDK Demo 13 |

14 | Rocket 15 |
16 |

17 | SSE events will be streamed from the backend to the frontend. 18 |

19 |
20 | 23 | 24 |
25 | 28 |
29 |
30 |
Hello, world!
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/hello-world/resources/public/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Datastar SDK Demo 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Datastar SDK Demo 15 |

16 | Rocket 17 |
18 |

19 | SSE events will be streamed from the backend to the frontend. 20 |

21 |
22 | 25 | 26 |
27 | 30 |
31 |
32 |
Hello, world!
33 |
34 | 35 | -------------------------------------------------------------------------------- /src/dev/examples/snippets/load_more.clj: -------------------------------------------------------------------------------- 1 | #_{:clj-kondo/ignore true} 2 | (ns examples.snippets.load-more 3 | (:require 4 | [dev.onionpancakes.chassis.core :refer [html]] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :refer [on-open]] 7 | [starfederation.datastar.clojure.adapter.test :refer [->sse-response]] 8 | [charred.api :as charred])) 9 | 10 | 11 | (def ^:private bufSize 1024) 12 | (def read-json-str (charred/parse-json-fn {:async? false :bufsize bufSize})) 13 | 14 | (def write-json-str charred/write-json-str) 15 | 16 | 17 | 18 | #_{:clj-kondo/ignore true} 19 | (comment 20 | (require 21 | '[charred.api :as charred] 22 | '[starfederation.datastar.clojure.api :as d*] 23 | '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 24 | '[some.hiccup.library :refer [html]] 25 | '[some.json.library :refer [read-json-str write-json-str]])) 26 | 27 | 28 | (def max-offset 5) 29 | 30 | (defn handler [ring-request] 31 | (->sse-response ring-request 32 | {on-open 33 | (fn [sse] 34 | (let [d*-signals (-> ring-request d*/get-signals read-json-str) 35 | offset (get d*-signals "offset") 36 | limit 1 37 | new-offset (+ offset limit)] 38 | 39 | (d*/patch-elements! sse 40 | (html [:div "Item " new-offset]) 41 | {d*/selector "#list" 42 | d*/patch-mode d*/pm-append}) 43 | 44 | (if (< new-offset max-offset) 45 | (d*/patch-signals! sse (write-json-str {"offset" new-offset})) 46 | (d*/remove-element! sse "#load-more")) 47 | 48 | (d*/close-sse! sse)))})) 49 | 50 | 51 | (comment 52 | (handler {:request-method :get :query-params {"datastar" "{\"offset\": 1}"}}) 53 | (handler {:request-method :get :query-params {"datastar" "{\"offset\": 2}"}}) 54 | (handler {:request-method :get :query-params {"datastar" "{\"offset\": 3}"}}) 55 | (handler {:request-method :get :query-params {"datastar" "{\"offset\": 4}"}})) 56 | 57 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/api/common.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.common 2 | (:require 3 | [clojure.string :as string])) 4 | 5 | ;; ----------------------------------------------------------------------------- 6 | ;; Option names 7 | ;; ----------------------------------------------------------------------------- 8 | 9 | ;; SSE Options 10 | (def id :d*.sse/id) 11 | (def retry-duration :d*.sse/retry-duration) 12 | 13 | ;; Merge fragment opts 14 | (def selector :d*.elements/selector) 15 | (def patch-mode :d*.elements/patch-mode) 16 | (def use-view-transition :d*.elements/use-view-transition) 17 | 18 | ;;Signals opts 19 | (def only-if-missing :d*.signals/only-if-missing) 20 | 21 | 22 | ;; Script opts 23 | (def auto-remove :d*.scripts/auto-remove) 24 | (def attributes :d*.scripts/attributes) 25 | 26 | 27 | 28 | ;; ----------------------------------------------------------------------------- 29 | ;; Data lines construction helpers 30 | ;; ----------------------------------------------------------------------------- 31 | (defn add-opt-line! 32 | "Add an option `v` line to the transient `data-lines!` vector. 33 | 34 | Args: 35 | - `data-lines!`: a transient vector of data-lines that will be written in a sse 36 | event 37 | - `prefix`: The Datastar specific preffix for that line 38 | - `v`: the value for that line 39 | " 40 | [data-lines! prefix v] 41 | (conj! data-lines! (str prefix v))) 42 | 43 | 44 | (defn add-data-lines! 45 | "Add several data-lines to the `data-lines!` transient vector." 46 | [data-lines! prefix ^String text] 47 | (let [stream (.lines text) 48 | i (.iterator stream)] 49 | (loop [acc data-lines!] 50 | (if (.hasNext i) 51 | (recur (conj! acc (str prefix (.next i)))) 52 | acc)))) 53 | 54 | (defn add-boolean-option? 55 | "Utility used to test whether an boolean option should result in a sse event 56 | data-line. Returns true if `val` a boolean and isn't the `default-val`." 57 | [default-val val] 58 | (and 59 | (boolean? val) 60 | (not= val default-val))) 61 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation/state.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.animation.state 2 | (:require 3 | [bb-example.animation.core :as animation])) 4 | 5 | ;; ----------------------------------------------------------------------------- 6 | ;; Animation state 7 | ;; ----------------------------------------------------------------------------- 8 | (defonce !state (atom animation/starting-state)) 9 | 10 | 11 | (defn reset-state! [] 12 | (reset! !state animation/starting-state)) 13 | 14 | (defn resize! [x y] 15 | (swap! !state animation/resize x y)) 16 | 17 | (defn add-ping! 18 | ([pos] 19 | (swap! !state animation/add-ping pos)) 20 | ([pos duration speed] 21 | (swap! !state animation/add-ping pos duration speed))) 22 | 23 | 24 | (defn add-random-pings! [] 25 | (swap! !state animation/add-random-pings 10)) 26 | 27 | 28 | (defn step-state! [] 29 | (swap! !state animation/step-state)) 30 | 31 | 32 | (defn- try-claiming [current-state id] 33 | (compare-and-set! 34 | !state 35 | current-state 36 | (animation/start-animating current-state id))) 37 | 38 | 39 | (defn- claim-animator-job! [id] 40 | (let [current-state @!state] 41 | (if (:animator current-state) 42 | :already-claimed 43 | (if (try-claiming current-state id) 44 | :claimed 45 | (recur id))))) 46 | 47 | 48 | (defn start-animating! [] 49 | (let [id (random-uuid) 50 | res (claim-animator-job! id)] 51 | (when (= :claimed res) 52 | (future 53 | (loop [] 54 | (let [state @!state] 55 | (when (:animator state) 56 | (step-state!) 57 | (Thread/sleep (long (:animation-tick state))) 58 | (recur)))))))) 59 | 60 | 61 | (defn stop-animating! [] 62 | (swap! !state animation/stop-animating)) 63 | 64 | 65 | ;; ----------------------------------------------------------------------------- 66 | ;; SSE connections state 67 | ;; ----------------------------------------------------------------------------- 68 | (defonce !conns (atom #{})) 69 | 70 | 71 | (defn add-conn! [sse] 72 | (swap! !conns conj sse)) 73 | 74 | 75 | (defn remove-conn! [sse] 76 | (swap! !conns disj sse)) 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip/state.clj: -------------------------------------------------------------------------------- 1 | (ns examples.animation-gzip.state 2 | (:require 3 | [examples.animation-gzip.animation :as animation])) 4 | 5 | ;; ----------------------------------------------------------------------------- 6 | ;; Animation state 7 | ;; ----------------------------------------------------------------------------- 8 | (defonce !state (atom animation/starting-state)) 9 | 10 | 11 | (defn reset-state! [] 12 | (reset! !state animation/starting-state)) 13 | 14 | (defn resize! [x y] 15 | (swap! !state animation/resize x y)) 16 | 17 | (defn add-ping! 18 | ([pos] 19 | (swap! !state animation/add-ping pos)) 20 | ([pos duration speed] 21 | (swap! !state animation/add-ping pos duration speed))) 22 | 23 | 24 | (defn add-random-pings! [] 25 | (swap! !state animation/add-random-pings 10)) 26 | 27 | 28 | (defn step-state! [] 29 | (swap! !state animation/step-state)) 30 | 31 | 32 | (defn- try-claiming [current-state id] 33 | (compare-and-set! 34 | !state 35 | current-state 36 | (animation/start-animating current-state id))) 37 | 38 | 39 | (defn- claim-animator-job! [id] 40 | (let [current-state @!state] 41 | (if (:animator current-state) 42 | :already-claimed 43 | (if (try-claiming current-state id) 44 | :claimed 45 | (recur id))))) 46 | 47 | 48 | (defn start-animating! [] 49 | (let [id (random-uuid) 50 | res (claim-animator-job! id)] 51 | (when (= :claimed res) 52 | (future 53 | (loop [] 54 | (let [state @!state] 55 | (when (:animator state) 56 | (step-state!) 57 | (Thread/sleep (long (:animation-tick state))) 58 | (recur)))))))) 59 | 60 | 61 | (defn stop-animating! [] 62 | (swap! !state animation/stop-animating)) 63 | 64 | 65 | ;; ----------------------------------------------------------------------------- 66 | ;; SSE connections state 67 | ;; ----------------------------------------------------------------------------- 68 | (defonce !conns (atom #{})) 69 | 70 | 71 | (defn add-conn! [sse] 72 | (swap! !conns conj sse)) 73 | 74 | 75 | (defn remove-conn! [sse] 76 | (swap! !conns disj sse)) 77 | 78 | 79 | -------------------------------------------------------------------------------- /libraries/sdk-ring/README.md: -------------------------------------------------------------------------------- 1 | # Datastar ring adapter 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/ring.svg)](https://clojars.org/dev.data-star.clojure/ring) 8 | 9 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/ring)](https://cljdoc.org/d/dev.data-star.clojure/ring/CURRENT) 10 | 11 | This library already depends on the core SDK lib. 12 | 13 | ## Overview 14 | 15 | Datastar SDK adapter for [ring](https://github.com/ring-clojure/ring). It is currently 16 | tested only with 17 | [ring-jetty-adapter](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter). 18 | 19 | This SDK adapter is based on the `ring.core.protocols/StreamableResponseBody` protocol. 20 | Any ring adapter using this protocol should work with this library. 21 | 22 | ## Specific behavior 23 | 24 | ### Detecting a closed connection 25 | 26 | With the [ring-jetty-adapter](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter), 27 | sending events on a closed connection will fail at some point throwing an 28 | `IOException`. By default the SSE-Gen will catch this exception, close itself 29 | then call the `on-close` callback. 30 | 31 | > [!Note] 32 | > At this moment, when using the ring adapter and Jetty, our SSE-Gen needs 33 | > to send 2 small events or 1 big event to detect a closed connection. 34 | > There must be some buffering happening independent of our implementation. 35 | 36 | ### SSE connection lifetime 37 | 38 | |Api| connection lifetime| 39 | |-|--| 40 | |Ring sync| same as the thread carrying the initial response| 41 | |Ring async| alive until the client or the server closes it| 42 | 43 | > [!IMPORTANT] 44 | > This is standard behavior as specified in the Ring spec. It implies that you 45 | > can't keep a SSE connection opened beyond the lifetime of the thread making 46 | > the initial response when using the synchronous API. 47 | 48 | In other words, the [barebones broadcast example](https://cljdoc.org/d/dev.data-star.clojure/sdk/CURRENT/doc/sdk-docs/using-datastar#barebones-broadcast) 49 | from the docs will work with the ring asynchronous API, not the synchronous one 50 | when using this library. 51 | -------------------------------------------------------------------------------- /src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.ring.impl-test 2 | (:require 3 | [lazytest.core :as lt :refer [defdescribe it expect]] 4 | [ring.core.protocols :as p] 5 | [starfederation.datastar.clojure.adapter.common :as ac] 6 | [starfederation.datastar.clojure.adapter.ring.impl :as impl] 7 | [starfederation.datastar.clojure.adapter.test :as at] 8 | [starfederation.datastar.clojure.api :as d*] 9 | [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) 10 | (:import 11 | [java.io ByteArrayOutputStream])) 12 | 13 | 14 | ;; ----------------------------------------------------------------------------- 15 | ;; Basic sending of a SSE event without any server 16 | ;; ----------------------------------------------------------------------------- 17 | (def expected-event-result 18 | (d*/patch-elements! (at/->sse-gen) "msg")) 19 | 20 | (defn send-SSE-event [response] 21 | (let [response (assoc-in response [::impl/opts ac/on-open] (fn [_sse])) 22 | baos (ByteArrayOutputStream.)] 23 | (with-open [sse-gen (impl/->sse-gen) 24 | baos baos] 25 | (p/write-body-to-stream sse-gen response baos) 26 | (d*/patch-elements! sse-gen "msg" {})) 27 | 28 | (expect 29 | (= (read-bytes baos (::impl/opts response)) 30 | expected-event-result)))) 31 | 32 | (defdescribe simple-test 33 | (it "can send events with a using temp buffers" 34 | (send-SSE-event {})) 35 | 36 | (it "can send events with a using a persistent buffered reader" 37 | (send-SSE-event {::impl/opts {ac/write-profile ac/buffered-writer-profile}})) 38 | 39 | (it "can send gziped events with a using temp buffers" 40 | (send-SSE-event {::impl/opts {ac/write-profile ac/gzip-profile 41 | :gzip? true}})) 42 | 43 | (it "can send gziped events with a using a persistent buffered reader" 44 | (send-SSE-event {::impl/opts {ac/write-profile ac/gzip-buffered-writer-profile 45 | :gzip? true}}))) 46 | 47 | 48 | 49 | 50 | (comment 51 | (user/reload!) 52 | (require '[lazytest.repl :as ltr]) 53 | (ltr/run-test-var #'simple-test)) 54 | -------------------------------------------------------------------------------- /src/bb/tasks/cljdoc.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.cljdoc 2 | (:require 3 | [babashka.fs :as fs] 4 | [babashka.tasks :as t] 5 | [clojure.string :as string] 6 | [tasks.build :as build])) 7 | 8 | 9 | (def cljdoc-dir ".cljdoc-preview") 10 | 11 | (defn home-dir [] (str (fs/home))) 12 | (defn cwd [] (str (fs/cwd))) 13 | 14 | (defn git-rev [] 15 | (-> (t/shell {:out :string} "git" "rev-parse" "HEAD") 16 | :out 17 | string/trim)) 18 | 19 | 20 | (defn try-docker-cmd [cmd] 21 | (try 22 | (t/shell {:out :string} cmd "--help") 23 | cmd 24 | (catch Exception _ nil))) 25 | 26 | 27 | (def docker-cmd 28 | (or (try-docker-cmd "docker") 29 | (try-docker-cmd "podman"))) 30 | 31 | 32 | (defn start-server! [] 33 | (fs/create-dirs cljdoc-dir) 34 | 35 | (t/shell 36 | docker-cmd "run" 37 | "--rm" 38 | "--publish" "8000:8000" 39 | "--volume" (str (home-dir) "/.m2:/root/.m2") 40 | "--volume" "./.cljdoc-preview:/app/data" 41 | "--platform" "linux/amd64" 42 | "cljdoc/cljdoc")) 43 | 44 | 45 | (def libs 46 | #{:sdk 47 | :brotli 48 | :http-kit 49 | :http-kit-malli-schemas 50 | :malli-schemas 51 | :ring 52 | :ring-malli-schemas}) 53 | 54 | (defn lib-arg->kw [lib] 55 | (cond-> lib 56 | (and (string? lib) (string/starts-with? lib ":")) 57 | (subs 1) 58 | 59 | true 60 | keyword)) 61 | 62 | 63 | (defn ingest! [lib & {:keys [version] 64 | :or {version (build/current-version)}}] 65 | (let [lib (lib-arg->kw lib)] 66 | (if (contains? libs lib) 67 | (t/shell 68 | docker-cmd "run" 69 | "--rm" 70 | "--volume" (str (home-dir) "/.m2:/root/.m2") 71 | "--volume" (str (cwd) ":/repo-to-import") 72 | "--volume" "./.cljdoc-preview:/app/data" 73 | "--platform" "linux/amd64" 74 | "--entrypoint" "clojure" 75 | "cljdoc/cljdoc" "-Sforce" "-M:cli" "ingest" 76 | "--project" (str "dev.data-star.clojure/" (name lib)) 77 | "--version" (str version) 78 | "--git" "/repo-to-import" 79 | "--rev" (git-rev)) 80 | (println "Can't ingest " lib ", unrecognized")))) 81 | 82 | 83 | (defn clean! [] 84 | (fs/delete-tree cljdoc-dir)) 85 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/api/signals.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.signals 2 | (:require 3 | [clojure.string :as string] 4 | [starfederation.datastar.clojure.api.common :as common] 5 | [starfederation.datastar.clojure.api.sse :as sse] 6 | [starfederation.datastar.clojure.consts :as consts] 7 | [starfederation.datastar.clojure.utils :as u])) 8 | 9 | 10 | ;; ----------------------------------------------------------------------------- 11 | ;; Merge signal 12 | ;; ----------------------------------------------------------------------------- 13 | (defn add-only-if-missing? [v] 14 | (common/add-boolean-option? consts/default-patch-signals-only-if-missing v)) 15 | 16 | (defn ->patch-signals [signals opts] 17 | (let [oim (common/only-if-missing opts)] 18 | (u/transient-> [] 19 | (cond-> 20 | (and oim (add-only-if-missing? oim)) 21 | (common/add-opt-line! consts/only-if-missing-dataline-literal oim) 22 | 23 | (u/not-empty-string? signals) 24 | (common/add-data-lines! consts/signals-dataline-literal signals))))) 25 | 26 | 27 | 28 | (comment 29 | (= (->patch-signals "{'some': \n 'json'}" {}) 30 | ["signals {'some': " 31 | "signals 'json'}"])) 32 | 33 | (defn patch-signals! [sse-gen signals-content opts] 34 | (try 35 | (sse/send-event! sse-gen 36 | consts/event-type-patch-signals 37 | (->patch-signals signals-content opts) 38 | opts) 39 | (catch Exception e 40 | (throw (ex-info "Failed to send merge signals" 41 | {:signals signals-content} 42 | e))))) 43 | 44 | 45 | ;; ----------------------------------------------------------------------------- 46 | ;; Read signals 47 | ;; ----------------------------------------------------------------------------- 48 | (defn get-signals 49 | "Returns the signals json string. You need to use some middleware 50 | that adds the :query-params key to the request for this function 51 | to work properly. 52 | 53 | (Bring your own json parsing)" 54 | [request] 55 | (if (= :get (:request-method request)) 56 | (get-in request [:query-params consts/datastar-key]) 57 | (:body request))) 58 | -------------------------------------------------------------------------------- /src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api-schemas-test 2 | (:require 3 | [lazytest.core :as lt :refer [defdescribe describe expect it]] 4 | [malli.instrument :as mi] 5 | [starfederation.datastar.clojure.adapter.test :as at] 6 | [starfederation.datastar.clojure.api :as d*] 7 | [starfederation.datastar.clojure.api.elements :as elements] 8 | [starfederation.datastar.clojure.api-schemas])) 9 | 10 | 11 | (def with-malli 12 | (lt/around [f] 13 | (mi/instrument!) 14 | (f) 15 | (mi/unstrument!))) 16 | 17 | 18 | (def sse-gen (at/->sse-gen)) 19 | 20 | 21 | (defn get-exception [thunk] 22 | (try 23 | (thunk) 24 | nil 25 | (catch Exception e 26 | e))) 27 | 28 | 29 | (defn get-exception-msg [thunk] 30 | (-> thunk 31 | (get-exception) 32 | ex-message)) 33 | 34 | 35 | (def malli-error-msg 36 | ":malli.core/invalid-input") 37 | 38 | 39 | (def dumy-script "console.log('hello')") 40 | 41 | (def thunk-wrong-script-type #(d*/execute-script! sse-gen :test)) 42 | (def thunk-wrong-option-type #(d*/execute-script! sse-gen dumy-script {d*/auto-remove :test})) 43 | 44 | 45 | (defdescribe test-malli-schemas 46 | (describe "without malli" 47 | (it "error can go through" 48 | (expect (= (thunk-wrong-script-type) 49 | "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")) 50 | (expect (= (thunk-wrong-option-type) 51 | "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")))) 52 | 53 | (describe "with malli" 54 | {:context [with-malli]} 55 | (it "types are checked" 56 | (let [msg1 (get-exception-msg thunk-wrong-script-type) 57 | msg2 (get-exception-msg thunk-wrong-option-type)] 58 | (expect (= msg1 malli-error-msg)) 59 | (expect (= msg2 malli-error-msg))))) 60 | 61 | (describe "Schemas not required" 62 | (it "doesn't trigger instrumentation" 63 | (expect (= (elements/->patch-elements "" {d*/retry-duration :test}) 64 | []))))) 65 | 66 | (comment 67 | (require '[lazytest.repl :as ltr]) 68 | (ltr/run-test-var #'test-malli-schemas)) 69 | 70 | -------------------------------------------------------------------------------- /src/dev/examples/multiple_fragments.clj: -------------------------------------------------------------------------------- 1 | (ns examples.multiple-fragments 2 | (:require 3 | [examples.common :as c] 4 | [examples.utils :as u] 5 | [dev.onionpancakes.chassis.core :as h] 6 | [reitit.ring :as rr] 7 | [reitit.ring.middleware.parameters :as reitit-params] 8 | [ring.util.response :as ruresp] 9 | [starfederation.datastar.clojure.api :as d*] 10 | [starfederation.datastar.clojure.adapter.common :as ac] 11 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] 12 | [starfederation.datastar.clojure.adapter.ring :as ring-gen])) 13 | 14 | 15 | ;; Testing the sending of multiple fragments at once 16 | 17 | (defn res [id val] 18 | [:span {:id id} val]) 19 | 20 | 21 | (def page 22 | (h/html 23 | (c/page-scaffold 24 | [[:h1 "Test page"] 25 | [:input {:type "text" :data-bind:input true}] 26 | [:button {:data-on:click (d*/sse-get "/endpoint")} "Send input"] 27 | [:br] 28 | [:span {:data-text "$input"}] 29 | [:br] 30 | [:div "res: " (res "res-1" "")] 31 | [:div "duplicate res: " (res "res-2" "")]]))) 32 | 33 | 34 | (defn home [_] 35 | (ruresp/response page)) 36 | 37 | 38 | (defn ->elements [input-val] 39 | [(h/html (res "res-1" input-val)) 40 | (h/html (res "res-2" input-val))]) 41 | 42 | 43 | (defn ->endpoint[->sse-response] 44 | (fn [req] 45 | (let [signals (u/get-signals req) 46 | input-val (get signals "input")] 47 | (->sse-response req 48 | {ac/on-open 49 | (fn [sse] 50 | (d*/with-open-sse sse 51 | (d*/patch-elements-seq! sse (->elements input-val))))})))) 52 | 53 | 54 | (defn ->router [->sse-response] 55 | (rr/router 56 | [["/" {:handler home}] 57 | ["/endpoint" {:handler (->endpoint ->sse-response)}] 58 | c/datastar-route])) 59 | 60 | 61 | (def default-handler (rr/create-default-handler)) 62 | 63 | 64 | (defn ->handler [->sse-response] 65 | (rr/ring-handler (->router ->sse-response) 66 | default-handler 67 | {:middleware [reitit-params/parameters-middleware]})) 68 | 69 | 70 | 71 | (def handler-hk (->handler hk-gen/->sse-response)) 72 | (def handler-ring (->handler ring-gen/->sse-response)) 73 | 74 | 75 | (comment 76 | :dbg 77 | :rec 78 | (u/clear-terminal!) 79 | (u/reboot-hk-server! handler-hk) 80 | (u/reboot-rj9a-server! #'handler-ring)) 81 | 82 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.animation 2 | (:require 3 | [bb-example.animation.broadcast :as broadcast] 4 | [bb-example.animation.handlers :as handlers] 5 | [bb-example.animation.rendering :as rendering] 6 | [bb-example.animation.state :as state] 7 | [bb-example.common :as c] 8 | [bb-example.core :as core] 9 | [ruuter.core :as ruuter] 10 | [ring.middleware.params :as r-params] 11 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] 12 | 13 | [starfederation.datastar.clojure.adapter.http-kit-schemas] 14 | [starfederation.datastar.clojure.api-schemas])) 15 | 16 | ;; This example let's use play with fat updates and compression 17 | ;; to get an idea of the gains compression can help use achieve 18 | ;; in terms of network usage. 19 | (broadcast/install-watch!) 20 | 21 | (def routes 22 | [(c/GET "/" handlers/home-handler) 23 | (c/GET "/ping/:id" (r-params/wrap-params handlers/ping-handler)) 24 | (c/GET "/random-10" handlers/random-pings-handler) 25 | (c/GET "/reset" handlers/reset-handler) 26 | (c/GET "/step1" handlers/step-handler) 27 | (c/GET "/play" handlers/play-handler) 28 | (c/GET "/pause" handlers/pause-handler) 29 | (c/GET "/refresh" handlers/refresh-handler) 30 | (c/POST "/resize" (r-params/wrap-params handlers/resize-handler)) 31 | (c/GET "/updates" (handlers/->updates-handler 32 | {hk-gen/write-profile hk-gen/gzip-profile}))]) 33 | 34 | (defn handler [req] 35 | (ruuter/route routes req)) 36 | 37 | 38 | (defn clear-terminal! [] 39 | (binding [*out* (java.io.PrintWriter. System/out)] 40 | (print "\033c") 41 | (flush))) 42 | 43 | 44 | 45 | (comment 46 | (clear-terminal!) 47 | (core/start! #'handler) 48 | *e 49 | state/!state 50 | state/!conns 51 | (reset! state/!conns #{}) 52 | 53 | (-> state/!state 54 | deref 55 | rendering/page) 56 | (state/resize! 10 10) 57 | (state/resize! 20 20) 58 | (state/resize! 25 25) 59 | (state/resize! 30 30) 60 | (state/resize! 50 50) 61 | (state/reset-state!) 62 | (state/add-random-pings!) 63 | (state/step-state!) 64 | (state/start-animating!)) 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/dev/examples/broadcast_ring.clj: -------------------------------------------------------------------------------- 1 | (ns examples.broadcast-ring 2 | (:require 3 | [clojure.string :as string] 4 | [examples.utils :as u] 5 | [reitit.ring :as rr] 6 | [starfederation.datastar.clojure.api :as d*] 7 | [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open on-close]])) 8 | 9 | 10 | ;; Tiny setup for that allows broadcasting events to several curl processes 11 | 12 | (defonce !conns (atom #{})) 13 | 14 | 15 | (defn long-connection 16 | ([req respond _raise] 17 | (respond 18 | (->sse-response req 19 | {on-open 20 | (fn [sse] 21 | (swap! !conns conj sse) 22 | (try 23 | (d*/console-log! sse "'connected with jetty!'") 24 | (catch Exception _ 25 | (d*/close-sse! sse)))) 26 | on-close 27 | (fn on-close [sse] 28 | (swap! !conns disj sse) 29 | (println "Removed connection from pool"))})))) 30 | 31 | 32 | (def routes 33 | [["/persistent" {:handler long-connection}]]) 34 | 35 | 36 | (def router 37 | (rr/router routes)) 38 | 39 | 40 | (def default-handler (rr/create-default-handler)) 41 | 42 | 43 | (def handler 44 | (rr/ring-handler router 45 | default-handler)) 46 | 47 | (defn broadcast-number! [n] 48 | (doseq [conn @!conns] 49 | (try 50 | (d*/console-log! conn (str "n: " n)) 51 | (catch Exception _ 52 | (d*/close-sse! conn) 53 | (println "closing connection"))))) 54 | 55 | 56 | (defn broadcast-lines! [n] 57 | (doseq [conn @!conns] 58 | (try 59 | (d*/patch-elements! conn (->> (range 0 n) 60 | (map (fn [x] 61 | (str "-----------------" x "-------------"))) 62 | (string/join "\n"))) 63 | (catch Exception _ 64 | (d*/close-sse! conn) 65 | (println "closing connection"))))) 66 | 67 | 68 | 69 | ;; open several clients: 70 | ;; curl -vv http://localhost:8081/persistent 71 | (comment 72 | (-> !conns deref first d*/close-sse!) 73 | (reset! !conns #{}) 74 | (broadcast-number! (rand-int 25)) 75 | (broadcast-lines! 1000) 76 | (u/clear-terminal!) 77 | (u/reboot-jetty-server! #'handler {:async? true :output-buffer-size 64}) 78 | (u/reboot-jetty-server! #'handler {:async? true}) 79 | 80 | (u/reboot-rj9a-server! #'handler {:async? true})) 81 | 82 | -------------------------------------------------------------------------------- /src/test/adapter-common/test/examples/form.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.form 2 | (:require 3 | [test.examples.common :as common] 4 | [dev.onionpancakes.chassis.core :as h] 5 | [dev.onionpancakes.chassis.compiler :as hc] 6 | [ring.middleware.multipart-params] 7 | [ring.util.response :as rur] 8 | [starfederation.datastar.clojure.adapter.common :as ac] 9 | [starfederation.datastar.clojure.api :as d*])) 10 | 11 | 12 | ;; ----------------------------------------------------------------------------- 13 | ;; Views 14 | ;; ----------------------------------------------------------------------------- 15 | (def input-id :input1) 16 | (def get-button-id :get-form) 17 | (def post-button-id :post-form) 18 | (def form-result-id :form-result) 19 | 20 | 21 | (defn form-get [url] 22 | (d*/sse-get url "{contentType: 'form'}")) 23 | 24 | 25 | (defn form-post [url] 26 | (d*/sse-post url "{contentType: 'form'}")) 27 | 28 | 29 | (defn result-area [res] 30 | (hc/compile 31 | [:span {:id form-result-id} res])) 32 | 33 | (defn form-page [] 34 | (common/scaffold 35 | (hc/compile 36 | [:div 37 | [:h2 "Form page"] 38 | [:form {:action ""} 39 | [:h3 "D* post form"] 40 | [:label {:for input-id} "Enter text"] 41 | [:br] 42 | 43 | [:input {:type "text" 44 | :id input-id 45 | :name input-id}] 46 | [:br] 47 | [:button {:id get-button-id 48 | :data-on:click (form-get "/form/endpoint")} 49 | "Form get"] 50 | 51 | [:br] 52 | [:button {:id post-button-id 53 | :data-on:click (form-post "/form/endpoint")} 54 | "Form post"] 55 | 56 | 57 | [:br] 58 | 59 | (result-area "")]]))) 60 | 61 | 62 | (def page (h/html (form-page))) 63 | 64 | 65 | (defn form 66 | ([_] 67 | (rur/response page)) 68 | ([req respond _] 69 | (respond (form req)))) 70 | 71 | 72 | (defn process-endpoint [request ->sse-response] 73 | (let [input-val (get-in request [:params (name input-id)])] 74 | (->sse-response request 75 | {ac/on-open 76 | (fn [sse-gen] 77 | (d*/with-open-sse sse-gen 78 | (d*/patch-elements! sse-gen (h/html (result-area input-val)))))}))) 79 | 80 | 81 | (defn ->endpoint [->sse-response] 82 | (fn endpoint 83 | ([request] 84 | (process-endpoint request ->sse-response)) 85 | ([request respond _raise] 86 | (respond (endpoint request))))) 87 | 88 | -------------------------------------------------------------------------------- /src/dev/examples/faulty_event.clj: -------------------------------------------------------------------------------- 1 | (ns examples.faulty-event 2 | (:require 3 | [clojure.pprint :as pp] 4 | [examples.utils :as u] 5 | [reitit.ring :as rr] 6 | [starfederation.datastar.clojure.api :as d*] 7 | [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]])) 8 | 9 | ;; Testing several ways exception might be caught when using a ring adapter 10 | 11 | (defn faulty-event 12 | ([req] 13 | (->sse-response req 14 | {on-open 15 | (fn [sse] 16 | (d*/with-open-sse sse 17 | (try 18 | (d*/console-log! sse 19 | "dummy val" 20 | {d*/retry-duration :faulty-value}) 21 | (catch Exception _ 22 | (println "Caught the faulty event when sending in sync mode")))))})) 23 | ([req respond raise] 24 | (respond 25 | (->sse-response req 26 | {on-open 27 | (fn [sse] 28 | (d*/with-open-sse sse 29 | (try 30 | (d*/console-log! sse 31 | "dummy val" 32 | {d*/retry-duration :faulty-value}) 33 | (catch Exception e 34 | (raise e)))))})))) 35 | 36 | 37 | (def routes 38 | [["/error" {:handler faulty-event}]]) 39 | 40 | 41 | (def router 42 | (rr/router routes)) 43 | 44 | 45 | (def wrap-print-response 46 | {:name ::print-reponse 47 | :wrap (fn [handler] 48 | (fn 49 | ([req] 50 | (let [response (handler req)] 51 | (pp/pprint response) 52 | response)) 53 | ([req respond raise] 54 | (handler req 55 | #(respond (do 56 | (pp/pprint %) 57 | %)) 58 | #(do 59 | (println "captured the faulty event with raise in async mode") 60 | (raise %))))))}) 61 | 62 | 63 | (def default-handler (rr/create-default-handler)) 64 | 65 | (def handler 66 | (rr/ring-handler router 67 | default-handler 68 | {:middleware [wrap-print-response]})) 69 | 70 | ;; curl -vv http://localhost:8081/error 71 | (comment 72 | (u/clear-terminal!) 73 | (u/reboot-jetty-server! #'handler) 74 | (u/reboot-jetty-server! #'handler {:async? true}) 75 | (u/reboot-rj9a-server! #'handler) 76 | (u/reboot-rj9a-server! #'handler {:async? true})) 77 | 78 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/adapter/test.cljc: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.test 2 | "Utilities providing a test SSEGenerator and a mock `->sse-response` 3 | function." 4 | (:require 5 | [starfederation.datastar.clojure.adapter.common :as ac] 6 | [starfederation.datastar.clojure.api.sse :as sse] 7 | [starfederation.datastar.clojure.protocols :as p] 8 | [starfederation.datastar.clojure.utils :as u]) 9 | (:import 10 | #?(:clj [java.io Closeable]) 11 | java.lang.StringBuilder 12 | [java.util.concurrent.locks ReentrantLock])) 13 | 14 | 15 | 16 | (deftype ReturnMsgGen [] 17 | p/SSEGenerator 18 | (send-event! [_ event-type data-lines opts] 19 | (-> (StringBuilder.) 20 | (sse/write-event! event-type data-lines opts) 21 | str)) 22 | 23 | (get-lock [_]) 24 | 25 | (close-sse! [_]) 26 | (sse-gen? [_] true)) 27 | 28 | 29 | 30 | (defn ->sse-gen 31 | "Returns a SSEGenerator whose 32 | [[starfederation.datastar.clojure.protocols/send-event!]] implementation 33 | is a stub that returns the event string instead of sending it." 34 | [& _] 35 | (->ReturnMsgGen)) 36 | 37 | 38 | 39 | 40 | (deftype RecordMsgGen [lock !rec !open?] 41 | p/SSEGenerator 42 | (send-event! [_ event-type data-lines opts] 43 | (u/lock! lock 44 | (vswap! !rec conj (-> (StringBuilder.) 45 | (sse/write-event! event-type data-lines opts) 46 | str)))) 47 | 48 | (get-lock [_] lock) 49 | 50 | (close-sse! [_] 51 | (u/lock! lock 52 | (vreset! !open? false))) 53 | 54 | (sse-gen? [_] true) 55 | 56 | #?@(:bb [] 57 | :clj [Closeable 58 | (close [this] 59 | (p/close-sse! this))])) 60 | 61 | 62 | (defn ->sse-response 63 | "Fake a sse-response, it returns the ring response map with 64 | the `:status`, `:headers` and `:body` keys sent. 65 | 66 | The events sent with sse-gen during the `on-open` callback are recorded in a 67 | vector stored in an atom. This atom is provided as the value for the 68 | response's `:body`." 69 | [req {on-open ac/on-open 70 | :keys [status headers]}] 71 | (let [ 72 | !rec (volatile! []) 73 | sse-gen (->RecordMsgGen (ReentrantLock.) 74 | !rec 75 | (volatile! true))] 76 | (on-open sse-gen) 77 | {:status (or status 200) 78 | :headers (merge headers (sse/headers req)) 79 | :body !rec})) 80 | 81 | -------------------------------------------------------------------------------- /libraries/build_stub.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[clojure.string :as str] 3 | '[clojure.tools.build.api :as b] 4 | '[clojure.edn :as edn]) 5 | 6 | (def root-project (-> (edn/read-string (slurp "../../deps.edn")) 7 | :aliases :neil :project)) 8 | (def repo-url-prefix (:url root-project)) 9 | (def scm (:scm root-project)) 10 | (def project (-> (edn/read-string (slurp "deps.edn")) 11 | :aliases :neil :project)) 12 | (def cwd (-> (java.io.File. ".") .getCanonicalFile .getName)) 13 | (def rev (str/trim (b/git-process {:git-args "rev-parse HEAD"}))) 14 | (def lib (:name project)) 15 | (def version (:version project)) 16 | (def description (:description project)) 17 | (assert lib ":name must be set in deps.edn under the :neil alias") 18 | (assert version ":version must be set in deps.edn under the :neil alias") 19 | (assert description ":description must be set in deps.edn under the :neil alias") 20 | 21 | (def class-dir "target/classes") 22 | (def basis (delay (b/create-basis {:project "deps.edn"}))) 23 | (def jar-file (format "target/%s-%s.jar" (name lib) version)) 24 | 25 | (defn clean [_] 26 | (b/delete {:path "target"})) 27 | 28 | (defn permalink [subpath] 29 | (str repo-url-prefix "/blob/" rev "/" subpath)) 30 | 31 | (defn jar [_] 32 | 33 | (b/write-pom {:class-dir class-dir 34 | :lib lib 35 | :version version 36 | :basis @basis 37 | :src-dirs ["src/main" "resources"] 38 | :pom-data [[:description description] 39 | [:url (permalink (str "libraries/" cwd))] 40 | [:licenses 41 | [:license 42 | [:name "The MIT License"] 43 | [:url (permalink "LICENSE.md")]]] 44 | (conj scm [:tag (str "v" version)])]}) 45 | (b/copy-dir {:src-dirs ["src/main"] 46 | :target-dir class-dir}) 47 | (b/jar {:class-dir class-dir 48 | :jar-file jar-file})) 49 | 50 | (defn install [_] 51 | (jar {}) 52 | (b/install {:basis @basis 53 | :lib lib 54 | :version version 55 | :jar-file jar-file 56 | :class-dir class-dir})) 57 | 58 | (defn deploy [opts] 59 | (jar opts) 60 | ((requiring-resolve 'deps-deploy.deps-deploy/deploy) 61 | (merge {:installer :remote 62 | :artifact jar-file 63 | :pom-file (b/pom-path {:lib lib :class-dir class-dir})} 64 | opts)) 65 | opts) 66 | -------------------------------------------------------------------------------- /src/dev/examples/remove_fragments.clj: -------------------------------------------------------------------------------- 1 | (ns dev.examples.remove-fragments 2 | (:require 3 | [examples.common :as c] 4 | [examples.utils :as u] 5 | [dev.onionpancakes.chassis.core :as h] 6 | [reitit.ring :as rr] 7 | [reitit.ring.middleware.parameters :as reitit-params] 8 | [ring.util.response :as ruresp] 9 | [starfederation.datastar.clojure.api :as d*] 10 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) 11 | 12 | 13 | ;; Appending and removing fragments with the D* api 14 | 15 | (def page 16 | (h/html 17 | (c/page-scaffold 18 | [[:h1 "Test page"] 19 | [:input {:type "text" :data-bind:input true :required true}] 20 | [:button {:data-attr:disabled "!$input" 21 | :data-on:click (str (d*/sse-get "/add-fragment") 22 | "; $input = ''")} 23 | "Send input"] 24 | [:br] 25 | [:ul {:id "list"}]]))) 26 | 27 | 28 | (defn home [_] 29 | (ruresp/response page)) 30 | 31 | 32 | (defonce !counter (atom 0)) 33 | 34 | 35 | (defn id! [] 36 | (-> !counter 37 | (swap-vals! inc) 38 | first 39 | (->> (str "id-")))) 40 | 41 | (defn ->fragment [id val] 42 | (h/html 43 | [:li {:id id} 44 | val 45 | [:button {:data-on:click (d*/sse-post (str "/remove-fragment/" id))} "remove me"]])) 46 | 47 | 48 | (defn add-element [req] 49 | (let [signals (u/get-signals req) 50 | input-val (get signals "input")] 51 | (hk-gen/->sse-response req 52 | {hk-gen/on-open 53 | (fn [sse] 54 | (d*/with-open-sse sse 55 | (d*/patch-elements! sse 56 | (->fragment (id!) input-val) 57 | {d*/selector "#list" 58 | d*/patch-mode d*/pm-append})))}))) 59 | 60 | 61 | (defn remove-element [req] 62 | (let [id (-> req :path-params :id)] 63 | (hk-gen/->sse-response req 64 | {hk-gen/on-open 65 | (fn [sse-gen] 66 | (d*/with-open-sse sse-gen 67 | (d*/remove-element! sse-gen (str "#" id))))}))) 68 | 69 | 70 | (def router (rr/router 71 | [["/" {:handler #'home}] 72 | ["/add-fragment" {:handler #'add-element}] 73 | ["/remove-fragment/:id" {:handler #'remove-element}]])) 74 | 75 | 76 | (def default-handler (rr/create-default-handler)) 77 | 78 | 79 | (def handler 80 | (rr/ring-handler router 81 | default-handler 82 | {:middleware [reitit-params/parameters-middleware]})) 83 | 84 | 85 | (comment 86 | (u/reboot-hk-server! handler)) 87 | 88 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.scripts 2 | (:require 3 | [starfederation.datastar.clojure.api.common :as common] 4 | [starfederation.datastar.clojure.api.elements :as elements] 5 | [starfederation.datastar.clojure.consts :as consts])) 6 | 7 | 8 | (defn ->script-tag [script opts] 9 | (let [auto-remove (common/auto-remove opts) 10 | attrs (common/attributes opts) 11 | script-tag-builder (StringBuilder.)] 12 | 13 | ;; Opening 14 | (.append script-tag-builder "") 31 | 32 | ;; Content of the script 33 | (.append script-tag-builder script) 34 | 35 | ;; Closing 36 | (.append script-tag-builder "") 37 | 38 | ;; Returning the built tag 39 | (str script-tag-builder))) 40 | 41 | 42 | (def patch-opts 43 | {common/selector "body" 44 | common/patch-mode consts/element-patch-mode-append}) 45 | 46 | 47 | (defn execute-script! [sse-gen script-text opts] 48 | (elements/patch-elements! sse-gen 49 | (->script-tag script-text opts) 50 | (merge opts patch-opts))) 51 | 52 | (comment 53 | (= (->script-tag "console.log('hello')" {}) 54 | "") 55 | 56 | (= (->script-tag "console.log('hello')" 57 | {common/auto-remove false}) 58 | "") 59 | 60 | 61 | (= (->script-tag "console.log('hello')" 62 | {common/auto-remove false 63 | common/attributes {:type "module"}}) 64 | "") 65 | 66 | 67 | (= (->script-tag "console.log('hello');\nconsole.log('world!!!')" 68 | {common/auto-remove :true 69 | common/attributes {:type "module" :data-something 1}}) 70 | "")) 71 | 72 | 73 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/utils.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.utils 2 | (:refer-clojure :exclude [assert]) 3 | (:require 4 | [clojure.string :as string]) 5 | (:import 6 | [java.util.concurrent.locks ReentrantLock])) 7 | 8 | 9 | (defmacro assert 10 | "Same as clojure's [[assert]] except that it throws a `clojure.lang.ExceptionInfo`." 11 | {:added "1.0"} 12 | ([x] 13 | (when *assert* 14 | `(when-not ~x 15 | (throw (ex-info (str "Assert failed: " (pr-str '~x)) {}))))) 16 | ([x message] 17 | (when *assert* 18 | `(when-not ~x 19 | (throw (ex-info (str "Assert failed: " ~message "\n" (pr-str '~x)) {})))))) 20 | 21 | (comment 22 | (assert (number? :a))) 23 | 24 | ;; ----------------------------------------------------------------------------- 25 | ;; Locking utility 26 | ;; ----------------------------------------------------------------------------- 27 | (defn reantrant-lock? [l] 28 | (instance? ReentrantLock l)) 29 | 30 | ;; Shamelessly adapted from https://github.com/clojure/clojure/blob/clojure-1.12.0/src/clj/clojure/core.clj#L1662 31 | (defmacro lock! 32 | [x & body] 33 | `(let [lockee# ~x] 34 | (assert (reantrant-lock? ~x)) 35 | (try 36 | (let [^ReentrantLock locklocal# lockee#] 37 | (.lock locklocal#) 38 | (try 39 | ~@body 40 | (finally 41 | (.unlock locklocal#))))))) 42 | 43 | (comment 44 | (macroexpand-1 '(lock! x (do (stuff))))) 45 | 46 | ;; ----------------------------------------------------------------------------- 47 | ;; Other 48 | ;; ----------------------------------------------------------------------------- 49 | (defmacro transient-> [v & body] 50 | `(-> ~v transient ~@body persistent!)) 51 | 52 | (comment 53 | (macroexpand-1 '(transient-> [] (conj! 1) (conj! 2)))) 54 | 55 | 56 | (defn not-empty-string? [s] 57 | (not (string/blank? s))) 58 | 59 | 60 | (defn merge-transient! 61 | "Merge a map `m` into a transient map `tm`. 62 | Returns the transient map without calling [[persistent!]] on it." 63 | [tm m] 64 | (reduce-kv (fn [acc k v] 65 | (assoc! acc k v)) 66 | tm 67 | m)) 68 | 69 | 70 | (defmacro def-clone 71 | "Little utility to clone simple var. It brings their docstring to the clone." 72 | ([src] 73 | (let [dest (-> src name symbol)] 74 | `(def-clone ~dest ~src))) 75 | ([dest src] 76 | (let [src-var (resolve src) 77 | doc (-> src-var meta :doc)] 78 | `(do 79 | (def ~dest ~(symbol src-var)) 80 | (alter-meta! (resolve '~dest) assoc :doc ~doc) 81 | (var ~dest))))) 82 | 83 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit/README.md: -------------------------------------------------------------------------------- 1 | # Datastar http-kit adapter 2 | 3 | ## Installation 4 | 5 | Install using clojars deps coordinates: 6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/dev.data-star.clojure/http-kit.svg)](https://clojars.org/dev.data-star.clojure/http-kit) 8 | 9 | [![cljdoc badge](https://cljdoc.org/badge/dev.data-star.clojure/http-kit)](https://cljdoc.org/d/dev.data-star.clojure/http-kit/CURRENT) 10 | 11 | This library already depends on the core SDK lib. 12 | 13 | > [!IMPORTANT] 14 | > This library adds (and needs) a dependency to Http-kit as recent as the current 15 | > `v2.9.0-beta2`. We do not recommend using older versions (`v2.8.1` being the 16 | > current stable) as they do not work properly with SSE. 17 | 18 | ## Overview 19 | 20 | This library provides an implementation of the 21 | `starfederation.datastar.clojure.protocols/SSEGenerator` for Http-kit. 22 | 23 | It provides 2 APIs to create ring SSE response tailored to Http-kit. 24 | 25 | ### `starfederation.datastar.clojure.adapter.http-kit` 26 | 27 | This this the original API, it is mostly one function: `->sse-response`. 28 | 29 | Using this namespace is straightforward but it has a downside. 30 | The way it works, the response's status and headers will be sent as soon has 31 | your ring handler is done. It means that middleware that would modify the 32 | response (or interceptors having a `:leave` function) will have no effect. 33 | 34 | ### `starfederation.datastar.clojure.adapter.http-kit2` 35 | 36 | This is the latest API for using the SDK with Http-kit. It was designed to fix 37 | the middleware (and interceptor) incompatibilities of the first. It is inspired 38 | by the way [Pedestal](https://github.com/pedestal/pedestal) uses Http-kit. 39 | 40 | Using this API, `->sse-reponse` will not start the SSE stream. It returns a 41 | response map that can be modified by middleware and interceptors after the 42 | handler (and thus `->sse-reponse`) is done. 43 | 44 | The SSE stream is initiated either by the `wrap-start-responding` middleware or 45 | the `start-responding-interceptor`. To be sure all other middleware you may use 46 | work properly, `wrap-start-responding` should be the first in the chain and thus 47 | the last to finish before handing the response to the adapter. 48 | 49 | ## Specific behavior 50 | 51 | ### Detecting a closed connection 52 | 53 | Http-kit detects closed connections by itself. When it dos the `on-close` 54 | callback of `->sse-response` will be called. 55 | 56 | ### SSE connection lifetime 57 | 58 | The connection stays alive until the client or your code explicitly closes it 59 | server side regardless of the ring API (sync vs async) you are using. 60 | -------------------------------------------------------------------------------- /libraries/sdk-ring/src/main/starfederation/datastar/clojure/adapter/ring.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.ring 2 | (:require 3 | [starfederation.datastar.clojure.adapter.ring.impl :as impl] 4 | [starfederation.datastar.clojure.adapter.common :as ac] 5 | [starfederation.datastar.clojure.utils :refer [def-clone]])) 6 | 7 | 8 | (def-clone on-open ac/on-open) 9 | (def-clone on-close ac/on-close) 10 | (def-clone on-exception ac/on-exception) 11 | (def-clone default-on-exception ac/default-on-exception) 12 | 13 | 14 | (def-clone write-profile ac/write-profile) 15 | 16 | (def-clone basic-profile ac/basic-profile) 17 | (def-clone buffered-writer-profile ac/buffered-writer-profile) 18 | (def-clone gzip-profile ac/gzip-profile) 19 | (def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile) 20 | 21 | 22 | (defn ->sse-response 23 | "Returns a ring response that will start a SSE stream. 24 | 25 | The status code will be either 200 or the user provided one. 26 | 27 | Specific SSE headers are set automatically, the user provided ones will be 28 | merged. The response body is a sse generator implementing 29 | `ring.core.protocols/StreamableResponseBody`. 30 | 31 | 32 | The body is a special value that is part of the SSE machinery and should not 33 | be used directly. In other words you must only interact with the SSEGen 34 | provided as argument of the different callbacks described below. 35 | 36 | In sync mode, the connection is closed automatically when the handler is 37 | done running. You need to explicitly close it in rinc async. 38 | 39 | Opts: 40 | - `:status`: status for the HTTP response, defaults to 200 41 | - `:headers`: Ring headers map to add to the response 42 | - [[on-open]]: Mandatory callback (fn [sse-gen] ...) called when the generator 43 | is ready to send. 44 | - [[on-close]]: callback (fn [sse-gen] ...) called right after the generator 45 | has closed it's connection. 46 | - [[on-exception]]: callback called when sending a SSE event throws 47 | - [[write-profile]]: write profile for the connection 48 | defaults to [[basic-profile]] 49 | 50 | When it comes to write profiles, the SDK provides: 51 | - [[basic-profile]] 52 | - [[buffered-writer-profile]] 53 | - [[gzip-profile]] 54 | - [[gzip-buffered-writer-profile]] 55 | 56 | You can also take a look at the `starfederation.datastar.clojure.adapter.common` 57 | namespace if you want to write your own profiles. 58 | " 59 | [ring-request {:keys [status] :as opts}] 60 | {:pre [(ac/on-open opts)]} 61 | {:status (or status 200) 62 | :headers (ac/headers ring-request opts) 63 | :body (impl/->sse-gen) 64 | ::impl/opts opts}) 65 | -------------------------------------------------------------------------------- /src/dev/examples/data_dsl.clj: -------------------------------------------------------------------------------- 1 | (ns examples.data-dsl 2 | (:require 3 | [starfederation.datastar.clojure.consts :as consts] 4 | [starfederation.datastar.clojure.api :as d*])) 5 | 6 | ;; Examples of how one might want to build a higher level api 7 | ;; on top of the SDK 8 | 9 | (def example 10 | {:event ::patch-elements 11 | :elements "
hello
" 12 | d*/selector "foo" 13 | d*/patch-mode d*/pm-append 14 | d*/use-view-transition true}) 15 | 16 | 17 | 18 | ;; ----------------------------------------------------------------------------- 19 | ;; Pure version just for data-lines 20 | ;; ----------------------------------------------------------------------------- 21 | (require '[starfederation.datastar.clojure.api.elements :as elements]) 22 | 23 | 24 | (defn sse-event [e] 25 | (case (:event e) 26 | ::patch-elements 27 | (elements/->patch-elements (:elements e) e))) 28 | 29 | 30 | (sse-event example) 31 | ; ["selector foo" 32 | ; "mergeMode append" 33 | ; "useViewTransition true" 34 | ; "fragments
hello
"] 35 | 36 | 37 | ;; ----------------------------------------------------------------------------- 38 | ;; Pure version handling buffer 39 | ;; ----------------------------------------------------------------------------- 40 | (require '[starfederation.datastar.clojure.api.sse :as sse]) 41 | 42 | (defn elements->str [e] 43 | (let [buffer (StringBuilder.)] 44 | (sse/write-event! buffer 45 | consts/event-type-patch-elements 46 | (elements/->patch-elements (:fragments e) e) 47 | e) 48 | (str buffer))) 49 | 50 | 51 | (defn event->str [e] 52 | (case (:event e) 53 | ::patch-elements 54 | (elements->str e))) 55 | 56 | (event->str example) 57 | ; "event: datastar-merge-fragments\n 58 | ; retry: 1000\n 59 | ; data: selector foo\n 60 | ; data: mergeMode append\n 61 | ; data: useViewTransition true\n 62 | ; data: fragments
hello
\n\n\n" 63 | 64 | ;; ----------------------------------------------------------------------------- 65 | ;; Side effecting version 66 | ;; ----------------------------------------------------------------------------- 67 | (require '[starfederation.datastar.clojure.adapter.test :as at]) 68 | 69 | ;; SSE generator that returns the sse event string instead of sending it 70 | (def sse-gen (at/->sse-gen)) 71 | 72 | 73 | 74 | (defn sse-event! [sse-gen e] 75 | (case (:event e) 76 | ::patch-elements (d*/patch-elements! sse-gen (:fragments e) e))) 77 | 78 | 79 | (sse-event! sse-gen example) 80 | ; "event: datastar-merge-fragments\n 81 | ; retry: 1000\n 82 | ; data: selector foo\n 83 | ; data: mergeMode append\n 84 | ; data: useViewTransition true\n 85 | ; data: fragments
hello
\n\n\n" 86 | -------------------------------------------------------------------------------- /examples/hello-httpkit/src/hello_httpkit.clj: -------------------------------------------------------------------------------- 1 | (ns hello-httpkit 2 | (:require 3 | [charred.api] 4 | [clojure.java.io :as io] 5 | [dev.onionpancakes.chassis.compiler :as hc] 6 | [dev.onionpancakes.chassis.core :as h] 7 | [org.httpkit.server] 8 | [reitit.ring.middleware.parameters] 9 | [reitit.ring] 10 | [ring.util.response] 11 | [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] 12 | [starfederation.datastar.clojure.api :as d*])) 13 | 14 | (def read-json (charred.api/parse-json-fn {:async? false :bufsize 1024})) 15 | 16 | (defn get-signals [req] 17 | (-> req d*/get-signals read-json)) 18 | 19 | (def home-page 20 | (slurp (io/resource "hello-world.html"))) 21 | 22 | (defn home [_] 23 | (-> home-page 24 | (ring.util.response/response) 25 | (ring.util.response/content-type "text/html"))) 26 | 27 | (def message "Hello, world!") 28 | 29 | (defn ->frag [i] 30 | (h/html 31 | (hc/compile 32 | [:div {:id "message"} 33 | (subs message 0 (inc i))]))) 34 | 35 | (defn hello-world [request] 36 | (let [d (-> request get-signals (get "delay") int)] 37 | (->sse-response request 38 | {on-open 39 | (fn [sse] 40 | (d*/with-open-sse sse 41 | (dotimes [i (count message)] 42 | (d*/patch-elements! sse (->frag i)) 43 | (Thread/sleep d))))}))) 44 | 45 | (def routes 46 | [["/" {:handler home}] 47 | ["/hello-world" {:handler hello-world 48 | :middleware [reitit.ring.middleware.parameters/parameters-middleware]}]]) 49 | 50 | (def router (reitit.ring/router routes)) 51 | 52 | (def handler (reitit.ring/ring-handler router)) 53 | 54 | ;; ------------------------------------------------------------ 55 | ;; Server 56 | ;; ------------------------------------------------------------ 57 | (defonce !server (atom nil)) 58 | 59 | (defn stop! [] 60 | (if-let [s @!server] 61 | (do (org.httpkit.server/server-stop! s) 62 | (reset! !server nil)) 63 | (throw (ex-info "Server not running" {})))) 64 | 65 | (defn start! [handler opts] 66 | (when-not (nil? @!server) 67 | (stop!)) 68 | (reset! !server 69 | (org.httpkit.server/run-server 70 | handler 71 | (merge {:port 8080} 72 | opts 73 | {:legacy-return-value? false})))) 74 | 75 | (comment 76 | (stop!) 77 | (start! #'handler {}) 78 | ) 79 | 80 | ;; ------------------------------------------------------------ 81 | ;; Main 82 | ;; ------------------------------------------------------------ 83 | (defn -main [& _] 84 | (start! #'handler {:port 8080}) 85 | (.addShutdownHook (Runtime/getRuntime) 86 | (Thread. #(do (stop!) (shutdown-agents))))) 87 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation/handlers.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.animation.handlers 2 | (:require 3 | [bb-example.animation.rendering :as rendering] 4 | [bb-example.animation.state :as state] 5 | [bb-example.common :as c] 6 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) 7 | 8 | 9 | (defn home-handler 10 | ([_] 11 | (c/response (str (rendering/page @state/!state)))) 12 | ([req respond _raise] 13 | (respond 14 | (home-handler req)))) 15 | 16 | 17 | (defn ->updates-handler [{:as opts}] 18 | (fn updates-handler [req] 19 | (hk-gen/->sse-response req 20 | (merge opts 21 | {hk-gen/on-open 22 | (fn [sse] 23 | (state/add-conn! sse)) 24 | hk-gen/on-close 25 | (fn on-close 26 | ([sse] 27 | (state/remove-conn! sse)) 28 | ([sse _status] 29 | (on-close sse)))})))) 30 | 31 | 32 | (def id-regex #"[^-]*-(\d*)-(\d*)") 33 | 34 | 35 | (defn recover-coords [req] 36 | (when-let [[_ x y] (-> req 37 | :params 38 | :id 39 | (->> (re-find id-regex)))] 40 | {:x (Integer/parseInt x) 41 | :y (Integer/parseInt y)})) 42 | 43 | 44 | (defn ping-handler 45 | ([req] 46 | (when-let [coords (recover-coords req)] 47 | (state/add-ping! coords)) 48 | {:status 204}) 49 | ([req respond _raise] 50 | (respond (ping-handler req)))) 51 | 52 | 53 | (defn random-pings-handler 54 | ([_req] 55 | (state/add-random-pings!) 56 | {:status 204}) 57 | ([req respond _raise] 58 | (respond 59 | (random-pings-handler req)))) 60 | 61 | 62 | (defn reset-handler 63 | ([_req] 64 | (state/reset-state!) 65 | {:status 204}) 66 | ([req respond _raise] 67 | (respond (reset-handler req)))) 68 | 69 | (defn step-handler 70 | ([_req] 71 | (state/step-state!) 72 | {:status 204}) 73 | ([req respond _raise] 74 | (respond 75 | (step-handler req)))) 76 | 77 | 78 | 79 | (defn play-handler 80 | ([_req] 81 | (state/start-animating!) 82 | {:status 204}) 83 | ([req respond _raise] 84 | (respond (play-handler req)))) 85 | 86 | 87 | (defn pause-handler 88 | ([_req] 89 | (state/stop-animating!) 90 | {:status 204}) 91 | ([req respond _raise] 92 | (respond (pause-handler req)))) 93 | 94 | (defn resize-handler 95 | ([req] 96 | (let [{x "rows" y "columns"} (c/get-signals req)] 97 | (state/resize! x y) 98 | {:status 204})) 99 | ([req respond _raise] 100 | (respond 101 | (resize-handler req)))) 102 | 103 | 104 | (defn refresh-handler 105 | ([_req] 106 | {:status 200 107 | :headers {"Content-Type" "text/html"} 108 | :body (rendering/render-content @state/!state)}) 109 | ([req respond _raise] 110 | (respond (refresh-handler req)))) 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/dev/examples/forms/datastar.clj: -------------------------------------------------------------------------------- 1 | (ns examples.forms.datastar 2 | (:require 3 | [examples.common :as c] 4 | [dev.onionpancakes.chassis.core :as h] 5 | [dev.onionpancakes.chassis.compiler :as hc] 6 | [examples.utils :as u] 7 | [ring.util.response :as rur] 8 | [reitit.ring.middleware.parameters :as params] 9 | [starfederation.datastar.clojure.api :as d*] 10 | [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) 11 | 12 | 13 | 14 | 15 | (defn result-area [res-from-form] 16 | (hc/compile 17 | [:div {:id "form-result"} 18 | [:span "From signals: " [:span {:data-text "$input1"}]] 19 | [:br] 20 | [:span "from backend form: " [:span res-from-form]]])) 21 | 22 | 23 | (defn page-get [result] 24 | (h/html 25 | (c/page-scaffold 26 | [:div 27 | [:h2 "Html GET form"] 28 | [:form {:action "" 29 | :data-on:submit "@get('/datastar/get', {contentType: 'form'})"} "submit" 30 | [:input {:type "text" 31 | :id "input1" 32 | :name "input1" 33 | :data-bind:input1 true}] 34 | [:button "submit"]] 35 | (result-area result)]))) 36 | 37 | 38 | (defn get-home [req] 39 | (if (not (d*/datastar-request? req)) 40 | (-> (page-get "") 41 | (rur/response) 42 | (rur/content-type "text/html")) 43 | (let [v (get-in req [:params "input1"])] 44 | (u/clear-terminal!) 45 | (u/pp-request req) 46 | (println "got here " v) 47 | (->sse-response req 48 | {on-open 49 | (fn [sse] 50 | (d*/with-open-sse sse 51 | (d*/patch-elements! sse (h/html (result-area v)))))})))) 52 | 53 | 54 | (defn page-post [result] 55 | (h/html 56 | (c/page-scaffold 57 | [:div 58 | [:h2 "Html POST form !!!!"] 59 | [:form {:action "" :data-on:submit "@post('/datastar/post', {contentType: 'form'})"} 60 | [:input {:type "text" 61 | :id "input1" 62 | :name "input1" 63 | :data-bind:input1 true}] 64 | [:button "submit"]] 65 | (result-area result)]))) 66 | 67 | 68 | (defn post-home [req] 69 | (if (not (d*/datastar-request? req)) 70 | (rur/response (page-post "")) 71 | (let [v (get-in req [:params "input1"])] 72 | (u/clear-terminal!) 73 | (u/pp-request req) 74 | (->sse-response req 75 | {on-open 76 | (fn [sse] 77 | (d*/with-open-sse sse 78 | (d*/patch-elements! sse (h/html (result-area v)))))})))) 79 | 80 | 81 | 82 | 83 | (def routes 84 | ["/datastar" 85 | ["/get" {:handler #'get-home}] 86 | ;:middleware [params/parameters-middleware]}] 87 | ["/post" {:handler #'post-home 88 | :middleware [params/parameters-middleware]}]]) 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/test/brotli/starfederation/datastar/clojure/brotli_test.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.brotli-test 2 | (:require 3 | [starfederation.datastar.clojure.api :as d*] 4 | [starfederation.datastar.clojure.adapter.test :as at] 5 | [starfederation.datastar.clojure.adapter.common-test :as ct] 6 | [starfederation.datastar.clojure.brotli :as brotli] 7 | [lazytest.core :as lt :refer [defdescribe describe specify expect]]) 8 | (:import 9 | [java.io InputStream ByteArrayOutputStream 10 | ByteArrayInputStream InputStreamReader BufferedReader] 11 | [java.nio.charset StandardCharsets])) 12 | 13 | 14 | 15 | (defn ->input-stream-reader [^InputStream is] 16 | (InputStreamReader. is StandardCharsets/UTF_8)) 17 | 18 | 19 | (defn ->ba [v] 20 | (cond 21 | (bytes? v) 22 | v 23 | 24 | (instance? ByteArrayOutputStream v) 25 | (.toByteArray ^ByteArrayOutputStream v))) 26 | 27 | 28 | (defn read-bytes [ba opts] 29 | (if (:brotli opts) 30 | (brotli/decompress ba) 31 | (-> ba 32 | ->ba 33 | (ByteArrayInputStream.) 34 | (->input-stream-reader) 35 | (BufferedReader.) 36 | (slurp)))) 37 | 38 | (defdescribe reading-bytes 39 | (specify "We can do str -> bytes -> str" 40 | (let [original (str (d*/patch-elements! (at/->sse-gen) "msg"))] 41 | (expect 42 | (= original 43 | (-> original 44 | (.getBytes) 45 | (read-bytes {}))))))) 46 | 47 | ;; ----------------------------------------------------------------------------- 48 | ;; Tests 49 | ;; ----------------------------------------------------------------------------- 50 | (defn simple-round-trip [write-profile] 51 | (let [!res (atom nil) 52 | machinery (ct/->machinery write-profile) 53 | baos (ct/get-baos machinery)] 54 | (with-open [_baos baos 55 | writer (ct/get-writer machinery)] 56 | (ct/append-then-flush writer "some text")) 57 | (reset! !res (-> baos .toByteArray (read-bytes write-profile))) 58 | (expect (= @!res "some text")))) 59 | 60 | 61 | 62 | (defdescribe brotli 63 | (describe "Writing of text with compression" 64 | (specify "We can do a simple round trip" 65 | (simple-round-trip (assoc (brotli/->brotli-profile) 66 | :brotli true))) 67 | 68 | (specify "We can compress several messages" 69 | (let [machinery (ct/->machinery (brotli/->brotli-profile)) 70 | baos (ct/get-baos machinery) 71 | !res (atom [])] 72 | (with-open [writer (ct/get-writer machinery)] 73 | (ct/append-then-flush writer "some text") 74 | (ct/append-then-flush writer "some other text")) 75 | (reset! !res (-> baos .toByteArray (read-bytes {:brotli true}))) 76 | (expect (= @!res "some textsome other text")))))) 77 | 78 | 79 | 80 | (comment 81 | (require '[lazytest.repl :as ltr]) 82 | (ltr/run-test-var #'reading-bytes) 83 | (ltr/run-test-var #'brotli)) 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /libraries/sdk/src/main/starfederation/datastar/clojure/consts.clj: -------------------------------------------------------------------------------- 1 | ;; This is auto-generated by Datastar. DO NOT EDIT. 2 | (ns starfederation.datastar.clojure.consts) 3 | 4 | 5 | (def datastar-key "datastar") 6 | 7 | 8 | ;; ----------------------------------------------------------------------------- 9 | ;; Default durations 10 | ;; ----------------------------------------------------------------------------- 11 | (def default-sse-retry-duration 12 | "The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE." 13 | 1000) 14 | 15 | 16 | ;; ----------------------------------------------------------------------------- 17 | ;; Dataline literals 18 | ;; ----------------------------------------------------------------------------- 19 | (def selector-dataline-literal "selector ") 20 | (def mode-dataline-literal "mode ") 21 | (def elements-dataline-literal "elements ") 22 | (def use-view-transition-dataline-literal "useViewTransition ") 23 | (def signals-dataline-literal "signals ") 24 | (def only-if-missing-dataline-literal "onlyIfMissing ") 25 | 26 | 27 | ;; ----------------------------------------------------------------------------- 28 | ;; Default booleans 29 | ;; ----------------------------------------------------------------------------- 30 | (def default-elements-use-view-transitions 31 | "Should elements be patched using the ViewTransition API?" 32 | false) 33 | 34 | (def default-patch-signals-only-if-missing 35 | "Should a given set of signals patch if they are missing?" 36 | false) 37 | 38 | 39 | 40 | ;; ----------------------------------------------------------------------------- 41 | ;; Enums 42 | ;; ----------------------------------------------------------------------------- 43 | ;; ElementPatchMode 44 | 45 | (def element-patch-mode-outer 46 | "Morphs the element into the existing element." 47 | "outer") 48 | 49 | (def element-patch-mode-inner 50 | "Replaces the inner HTML of the existing element." 51 | "inner") 52 | 53 | (def element-patch-mode-remove 54 | "Removes the existing element." 55 | "remove") 56 | 57 | (def element-patch-mode-replace 58 | "Replaces the existing element with the new element." 59 | "replace") 60 | 61 | (def element-patch-mode-prepend 62 | "Prepends the element inside to the existing element." 63 | "prepend") 64 | 65 | (def element-patch-mode-append 66 | "Appends the element inside the existing element." 67 | "append") 68 | 69 | (def element-patch-mode-before 70 | "Inserts the element before the existing element." 71 | "before") 72 | 73 | (def element-patch-mode-after 74 | "Inserts the element after the existing element." 75 | "after") 76 | 77 | 78 | (def default-element-patch-mode 79 | "Default value for ElementPatchMode. 80 | Morphs the element into the existing element." 81 | element-patch-mode-outer) 82 | 83 | 84 | ;; EventType 85 | 86 | (def event-type-patch-elements 87 | "An event for patching HTML elements into the DOM." 88 | "datastar-patch-elements") 89 | 90 | (def event-type-patch-signals 91 | "An event for patching signals." 92 | "datastar-patch-signals") 93 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api-schemas 2 | (:require 3 | [malli.core :as m] 4 | [starfederation.datastar.clojure.api] 5 | [starfederation.datastar.clojure.api.common-schemas :as cs])) 6 | 7 | 8 | (m/=> starfederation.datastar.clojure.api/close-sse! 9 | [:-> cs/sse-gen-schema :any]) 10 | 11 | 12 | (m/=> starfederation.datastar.clojure.api/patch-elements! 13 | [:function 14 | [:-> cs/sse-gen-schema cs/elements-schema :any] 15 | [:-> cs/sse-gen-schema cs/elements-schema cs/patch-element-options-schemas :any]]) 16 | 17 | 18 | (m/=> starfederation.datastar.clojure.api/patch-elements-seq! 19 | [:function 20 | [:-> cs/sse-gen-schema cs/elements-seq-schema :any] 21 | [:-> cs/sse-gen-schema cs/elements-seq-schema cs/patch-element-options-schemas :any]]) 22 | 23 | 24 | (m/=> starfederation.datastar.clojure.api/remove-element! 25 | [:function 26 | [:-> cs/sse-gen-schema cs/selector-schema :any] 27 | [:-> cs/sse-gen-schema cs/selector-schema cs/remove-element-options-schemas :any]]) 28 | 29 | 30 | (m/=> starfederation.datastar.clojure.api/patch-signals! 31 | [:function 32 | [:-> cs/sse-gen-schema cs/signals-schema :any] 33 | [:-> cs/sse-gen-schema cs/signals-schema cs/patch-signals-options-schemas :any]]) 34 | 35 | 36 | (m/=> starfederation.datastar.clojure.api/execute-script! 37 | [:function 38 | [:-> cs/sse-gen-schema cs/script-content-schema :any] 39 | [:-> cs/sse-gen-schema cs/script-content-schema cs/execute-script-options-schemas :any]]) 40 | 41 | 42 | (m/=> starfederation.datastar.clojure.api/sse-get 43 | [:function 44 | [:-> :string :string] 45 | [:-> :string :string :string]]) 46 | 47 | (m/=> starfederation.datastar.clojure.api/sse-post 48 | [:function 49 | [:-> :string :string] 50 | [:-> :string :string :string]]) 51 | 52 | 53 | (m/=> starfederation.datastar.clojure.api/sse-put 54 | [:function 55 | [:-> :string :string] 56 | [:-> :string :string :string]]) 57 | 58 | (m/=> starfederation.datastar.clojure.api/sse-patch 59 | [:function 60 | [:-> :string :string] 61 | [:-> :string :string :string]]) 62 | 63 | (m/=> starfederation.datastar.clojure.api/sse-delete 64 | [:function 65 | [:-> :string :string] 66 | [:-> :string :string :string]]) 67 | 68 | 69 | (m/=> starfederation.datastar.clojure.api/console-log! 70 | [:function 71 | [:-> cs/sse-gen-schema :string :any] 72 | [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) 73 | 74 | 75 | (m/=> starfederation.datastar.clojure.api/console-error! 76 | [:function 77 | [:-> cs/sse-gen-schema :string :any] 78 | [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) 79 | 80 | 81 | (m/=> starfederation.datastar.clojure.api/redirect! 82 | [:function 83 | [:-> cs/sse-gen-schema :string :any] 84 | [:-> cs/sse-gen-schema :string cs/execute-script-options-schemas :any]]) 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip.clj: -------------------------------------------------------------------------------- 1 | (ns examples.animation-gzip 2 | (:require 3 | [examples.animation-gzip.broadcast :as broadcast] 4 | [examples.animation-gzip.handlers :as handlers] 5 | [examples.animation-gzip.rendering :as rendering] 6 | [examples.animation-gzip.state :as state] 7 | [examples.utils :as u] 8 | [reitit.ring :as rr] 9 | [reitit.ring.middleware.exception :as reitit-exception] 10 | [reitit.ring.middleware.parameters :as reitit-params] 11 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] 12 | [starfederation.datastar.clojure.adapter.http-kit-schemas] 13 | [starfederation.datastar.clojure.adapter.ring :as ring-gen] 14 | [starfederation.datastar.clojure.adapter.ring-schemas] 15 | [starfederation.datastar.clojure.api-schemas] 16 | [starfederation.datastar.clojure.brotli :as brotli])) 17 | 18 | ;; This example let's use play with fat updates and compression 19 | ;; to get an idea of the gains compression can help use achieve 20 | ;; in terms of network usage. 21 | (broadcast/install-watch!) 22 | 23 | 24 | (defn ->routes [->sse-response opts] 25 | [["/" handlers/home-handler] 26 | ["/ping/:id" {:handler handlers/ping-handler 27 | :middleware [reitit-params/parameters-middleware]}] 28 | ["/random-10" handlers/random-pings-handler] 29 | ["/reset" handlers/reset-handler] 30 | ["/step1" handlers/step-handler] 31 | ["/play" handlers/play-handler] 32 | ["/pause" handlers/pause-handler] 33 | ["/updates" (handlers/->updates-handler ->sse-response opts)] 34 | ["/refresh" handlers/refresh-handler] 35 | ["/resize" handlers/resize-handler]]) 36 | 37 | 38 | (defn ->router [->sse-handler opts] 39 | (rr/router (->routes ->sse-handler opts))) 40 | 41 | 42 | (defn ->handler [->sse-response & {:as opts}] 43 | (rr/ring-handler 44 | (->router ->sse-response opts) 45 | (rr/create-default-handler) 46 | {:middleware [reitit-exception/exception-middleware]})) 47 | 48 | 49 | (def handler-http-kit (->handler hk-gen/->sse-response 50 | {hk-gen/write-profile (brotli/->brotli-profile)})) 51 | 52 | (def handler-ring (->handler ring-gen/->sse-response 53 | {ring-gen/write-profile ring-gen/gzip-profile})) 54 | 55 | (defn after-ns-reload [] 56 | (println "rebooting servers") 57 | (u/reboot-hk-server! #'handler-http-kit) 58 | (u/reboot-jetty-server! #'handler-ring {:async? true})) 59 | 60 | 61 | (comment 62 | #_{:clj-kondo/ignore true} 63 | (user/reload!) 64 | :help 65 | :dbg 66 | :rec 67 | :stop 68 | *e 69 | state/!state 70 | state/!conns 71 | (reset! state/!conns #{}) 72 | 73 | (-> state/!state 74 | deref 75 | rendering/page) 76 | (state/resize! 10 10) 77 | (state/resize! 20 20) 78 | (state/resize! 25 25) 79 | (state/resize! 30 30) 80 | (state/resize! 50 50) 81 | (state/reset-state!) 82 | (state/add-random-pings!) 83 | (state/step-state!) 84 | (state/start-animating!) 85 | (u/clear-terminal!) 86 | (u/reboot-hk-server! #'handler-http-kit) 87 | (u/reboot-jetty-server! #'handler-ring {:async? true})) 88 | -------------------------------------------------------------------------------- /src/test/adapter-http-kit/test/http_kit_test.clj: -------------------------------------------------------------------------------- 1 | (ns test.http-kit-test 2 | (:require 3 | [test.common :as common] 4 | [test.examples.http-kit-handler :as hkh] 5 | [lazytest.core :as lt :refer [defdescribe expect it]] 6 | [org.httpkit.server :as hk-server] 7 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) 8 | 9 | ;; ----------------------------------------------------------------------------- 10 | ;; HTTP-Kit stuff 11 | ;; ----------------------------------------------------------------------------- 12 | (def http-kit-basic-opts 13 | {:start! hk-server/run-server 14 | :stop! hk-server/server-stop! 15 | :get-port hk-server/server-port 16 | :legacy-return-value? false}) 17 | 18 | 19 | ;; ----------------------------------------------------------------------------- 20 | (defdescribe counters-test 21 | {:webdriver true 22 | :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} 23 | (it "manages signals" 24 | (doseq [[driver-type driver] common/drivers] 25 | (let [res (common/run-counters! @driver)] 26 | (expect (= res common/expected-counters) (str driver-type)))))) 27 | 28 | 29 | (defdescribe counters-test-async 30 | {:webdriver true 31 | :context [(common/with-server-f hkh/handler (assoc http-kit-basic-opts 32 | :ring-async? true))]} 33 | (it "manages signals" 34 | (doseq [[driver-type driver] common/drivers] 35 | (let [res (common/run-counters! @driver)] 36 | (expect (= res common/expected-counters) (str driver-type)))))) 37 | 38 | ;; ----------------------------------------------------------------------------- 39 | ;; Tests 40 | ;; ----------------------------------------------------------------------------- 41 | (defdescribe form-test 42 | {:webdriver true 43 | :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} 44 | (it "manages forms" 45 | (doseq [[driver-type driver] common/drivers] 46 | (let [res (common/run-form-test! @driver)] 47 | (expect (= res common/expected-form-vals) (str driver-type)))))) 48 | 49 | 50 | ;; ----------------------------------------------------------------------------- 51 | (defdescribe persistent-sse-test 52 | {:context [(common/persistent-sse-f hk-gen/->sse-response 53 | http-kit-basic-opts)]} 54 | (it "handles persistent connections" 55 | (let [res (common/run-persistent-sse-test!)] 56 | (expect (map? res)) 57 | (expect (common/p-sse-status-ok? res)) 58 | (expect (common/p-sse-http1-headers-ok? res)) 59 | (expect (common/p-sse-body-ok? res))))) 60 | 61 | 62 | (defdescribe persistent-sse-test-async 63 | {:context [(common/persistent-sse-f hk-gen/->sse-response 64 | (assoc http-kit-basic-opts 65 | :ring-async? true))]} 66 | (it "handles persistent connections" 67 | (let [res (common/run-persistent-sse-test!)] 68 | (expect (map? res)) 69 | (common/p-sse-status-ok? res) 70 | (common/p-sse-http1-headers-ok? res) 71 | (common/p-sse-body-ok? res)))) 72 | 73 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.http-kit 2 | (:require 3 | [org.httpkit.server :as hk-server] 4 | [starfederation.datastar.clojure.adapter.common :as ac] 5 | [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] 6 | [starfederation.datastar.clojure.utils :refer [def-clone]])) 7 | 8 | 9 | (def-clone on-open ac/on-open) 10 | (def-clone on-close ac/on-close) 11 | (def-clone on-exception ac/on-exception) 12 | (def-clone default-on-exception ac/default-on-exception) 13 | 14 | 15 | (def-clone write-profile ac/write-profile) 16 | 17 | (def-clone basic-profile impl/basic-profile) 18 | (def-clone buffered-writer-profile ac/buffered-writer-profile) 19 | (def-clone gzip-profile ac/gzip-profile) 20 | (def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile) 21 | 22 | 23 | (defn ->sse-response 24 | "Make a Ring like response that will start a SSE stream. 25 | 26 | The status code and the the SSE specific headers are sent automatically 27 | before [[on-open]] is called. 28 | 29 | Note that the SSE connection stays opened util you close it. 30 | 31 | General options: 32 | - `:status`: status for the HTTP response, defaults to 200. 33 | - `:headers`: ring headers map to add to the response. 34 | - [[on-open]]: mandatory callback called when the generator is ready to send. 35 | - [[on-close]]: callback called when the underlying Http-kit AsyncChannel is 36 | closed. It receives a second argument, the `:status-code` value we get from 37 | the closing AsyncChannel. 38 | - [[on-exception]]: callback called when sending a SSE event throws. 39 | - [[write-profile]]: write profile for the connection. 40 | Defaults to [[basic-profile]] 41 | 42 | SDK provided write profiles: 43 | - [[basic-profile]] 44 | - [[buffered-writer-profile]] 45 | - [[gzip-profile]] 46 | - [[gzip-buffered-writer-profile]] 47 | 48 | You can also take a look at the `starfederation.datastar.clojure.adapter.common` 49 | namespace if you want to write your own profiles. 50 | " 51 | [ring-request opts] 52 | {:pre [(ac/on-open opts)]} 53 | (let [on-open-cb (ac/on-open opts) 54 | on-close-cb (ac/on-close opts) 55 | future-send! (promise) 56 | future-gen (promise)] 57 | (hk-server/as-channel ring-request 58 | {:on-open 59 | (fn [ch] 60 | (impl/send-base-sse-response! ch ring-request opts) 61 | (let [send! (impl/->send! ch opts) 62 | sse-gen (impl/->sse-gen ch send! opts)] 63 | (deliver future-gen sse-gen) 64 | (deliver future-send! send!) 65 | (on-open-cb sse-gen))) 66 | 67 | :on-close 68 | (fn [_ status] 69 | (let [closing-res 70 | (ac/close-sse! 71 | #(when-let [send! (deref future-send! 0 nil)] (send!)) 72 | #(when on-close-cb 73 | (on-close-cb (deref future-gen 0 nil) status)))] 74 | (if (instance? Exception closing-res) 75 | (throw closing-res) 76 | closing-res)))}))) 77 | -------------------------------------------------------------------------------- /src/dev/examples/http_kit2/animation.clj: -------------------------------------------------------------------------------- 1 | (ns examples.http-kit2.animation 2 | (:require 3 | [examples.animation-gzip.broadcast :as broadcast] 4 | [examples.animation-gzip.handlers :as handlers] 5 | [examples.animation-gzip.rendering :as rendering] 6 | [examples.animation-gzip.state :as state] 7 | [examples.utils :as u] 8 | [reitit.ring :as rr] 9 | [reitit.ring.middleware.exception :as reitit-exception] 10 | [reitit.ring.middleware.parameters :as reitit-params] 11 | [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen] 12 | [starfederation.datastar.clojure.adapter.http-kit-schemas] 13 | [starfederation.datastar.clojure.adapter.ring :as ring-gen] 14 | [starfederation.datastar.clojure.adapter.ring-schemas] 15 | [starfederation.datastar.clojure.api-schemas] 16 | [starfederation.datastar.clojure.brotli :as brotli])) 17 | 18 | ;; This example let's use play with fat updates and compression 19 | ;; to get an idea of the gains compression can help use achieve 20 | ;; in terms of network usage. 21 | 22 | (broadcast/install-watch!) 23 | 24 | 25 | (defn ->routes [->sse-response opts] 26 | [["/" handlers/home-handler] 27 | ["/ping/:id" {:handler handlers/ping-handler 28 | :middleware [reitit-params/parameters-middleware]}] 29 | ["/random-10" handlers/random-pings-handler] 30 | ["/reset" handlers/reset-handler] 31 | ["/step1" handlers/step-handler] 32 | ["/play" handlers/play-handler] 33 | ["/pause" handlers/pause-handler] 34 | ["/updates" {:handler (handlers/->updates-handler ->sse-response opts) 35 | :middleware [[hk-gen/start-responding-middleware]]}] 36 | ["/refresh" handlers/refresh-handler] 37 | ["/resize" handlers/resize-handler]]) 38 | 39 | 40 | (defn ->router [->sse-handler opts] 41 | (rr/router (->routes ->sse-handler opts))) 42 | 43 | 44 | (defn ->handler [->sse-response & {:as opts}] 45 | (rr/ring-handler 46 | (->router ->sse-response opts) 47 | (rr/create-default-handler) 48 | {:middleware [reitit-exception/exception-middleware]})) 49 | 50 | 51 | (def handler-http-kit (->handler hk-gen/->sse-response 52 | {hk-gen/write-profile (brotli/->brotli-profile)})) 53 | 54 | (def handler-ring (->handler ring-gen/->sse-response 55 | {ring-gen/write-profile ring-gen/gzip-profile})) 56 | 57 | (defn after-ns-reload [] 58 | (println "rebooting servers") 59 | (u/reboot-hk-server! #'handler-http-kit) 60 | (u/reboot-jetty-server! #'handler-ring {:async? true})) 61 | 62 | 63 | (comment 64 | #_{:clj-kondo/ignore true} 65 | (user/reload!) 66 | :help 67 | :dbg 68 | :rec 69 | :stop 70 | *e 71 | state/!state 72 | state/!conns 73 | (reset! state/!conns #{}) 74 | 75 | (-> state/!state 76 | deref 77 | rendering/page) 78 | (state/resize! 10 10) 79 | (state/resize! 20 20) 80 | (state/resize! 25 25) 81 | (state/resize! 30 30) 82 | (state/resize! 50 50) 83 | (state/reset-state!) 84 | (state/add-random-pings!) 85 | (state/step-state!) 86 | (state/start-animating!) 87 | (u/clear-terminal!) 88 | (u/reboot-hk-server! #'handler-http-kit) 89 | (u/reboot-jetty-server! #'handler-ring {:async? true})) 90 | 91 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip/handlers.clj: -------------------------------------------------------------------------------- 1 | (ns examples.animation-gzip.handlers 2 | (:require 3 | [examples.animation-gzip.rendering :as rendering] 4 | [examples.animation-gzip.state :as state] 5 | [examples.utils :as u] 6 | [ring.util.response :as ruresp] 7 | [starfederation.datastar.clojure.adapter.common :as ac])) 8 | 9 | 10 | (defn home-handler 11 | ([_] 12 | (ruresp/response (rendering/page @state/!state))) 13 | ([req respond _raise] 14 | (respond 15 | (home-handler req)))) 16 | 17 | 18 | (defn ->updates-handler 19 | [->sse-response & {:as opts}] 20 | (fn updates-handler 21 | ([req] 22 | (->sse-response req 23 | (merge opts 24 | {ac/on-open 25 | (fn [sse] 26 | (state/add-conn! sse)) 27 | ac/on-close 28 | (fn on-close 29 | ([sse] 30 | (state/remove-conn! sse)) 31 | ([sse _status] 32 | (on-close sse)))}))) 33 | ([req respond _raise] 34 | (respond 35 | (updates-handler req))))) 36 | 37 | 38 | (def id-regex #"[^-]*-(\d*)-(\d*)") 39 | 40 | 41 | (defn recover-coords [req] 42 | (when-let [[_ x y] (-> req 43 | :path-params 44 | :id 45 | (->> (re-find id-regex)))] 46 | {:x (Integer/parseInt x) 47 | :y (Integer/parseInt y)})) 48 | 49 | 50 | (defn ping-handler 51 | ([req] 52 | (when-let [coords (recover-coords req)] 53 | (println "-- ping " coords) 54 | (state/add-ping! coords)) 55 | {:status 204}) 56 | ([req respond _raise] 57 | (respond (ping-handler req)))) 58 | 59 | 60 | (defn random-pings-handler 61 | ([_req] 62 | (println "-- add pixels") 63 | (state/add-random-pings!) 64 | {:status 204}) 65 | ([req respond _raise] 66 | (respond 67 | (random-pings-handler req)))) 68 | 69 | 70 | (defn reset-handler 71 | ([_req] 72 | (println "-- reseting state") 73 | (state/reset-state!) 74 | {:status 204}) 75 | ([req respond _raise] 76 | (respond (reset-handler req)))) 77 | 78 | (defn step-handler 79 | ([_req] 80 | (println "-- Step 1") 81 | (state/step-state!) 82 | {:status 204}) 83 | ([req respond _raise] 84 | (respond 85 | (step-handler req)))) 86 | 87 | 88 | 89 | (defn play-handler 90 | ([_req] 91 | (println "-- play animation") 92 | (state/start-animating!) 93 | {:status 204}) 94 | ([req respond _raise] 95 | (respond (play-handler req)))) 96 | 97 | 98 | (defn pause-handler 99 | ([_req] 100 | (println "-- pause animation") 101 | (state/stop-animating!) 102 | {:status 204}) 103 | ([req respond _raise] 104 | (respond (pause-handler req)))) 105 | 106 | (defn resize-handler 107 | ([req] 108 | (let [{x "rows" y "columns"} (u/get-signals req)] 109 | (println "-- resize" x y) 110 | (state/resize! x y) 111 | {:status 204})) 112 | ([req respond _raise] 113 | (respond 114 | (resize-handler req)))) 115 | 116 | 117 | (defn refresh-handler 118 | ([_req] 119 | {:status 200 120 | :headers {"Content-Type" "text/html"} 121 | :body (rendering/render-content @state/!state)}) 122 | ([req respond _raise] 123 | (respond (refresh-handler req)))) 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/test/adapter-http-kit/test/http_kit2_test.clj: -------------------------------------------------------------------------------- 1 | (ns test.http-kit2-test 2 | (:require 3 | [test.common :as common] 4 | [test.examples.http-kit-handler2 :as hkh] 5 | [lazytest.core :as lt :refer [defdescribe expect it]] 6 | [org.httpkit.server :as hk-server] 7 | [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) 8 | 9 | ;; ----------------------------------------------------------------------------- 10 | ;; HTTP-Kit stuff 11 | ;; ----------------------------------------------------------------------------- 12 | (def http-kit-basic-opts 13 | {:start! hk-server/run-server 14 | :stop! hk-server/server-stop! 15 | :get-port hk-server/server-port 16 | :legacy-return-value? false}) 17 | 18 | 19 | ;; ----------------------------------------------------------------------------- 20 | (defdescribe counters-test 21 | {:webdriver true 22 | :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} 23 | (it "manages signals" 24 | (doseq [[driver-type driver] common/drivers] 25 | (let [res (common/run-counters! @driver)] 26 | (expect (= res common/expected-counters) (str driver-type)))))) 27 | 28 | 29 | (defdescribe counters-test-async 30 | {:webdriver true 31 | :context [(common/with-server-f hkh/handler (assoc http-kit-basic-opts 32 | :ring-async? true))]} 33 | (it "manages signals" 34 | (doseq [[driver-type driver] common/drivers] 35 | (let [res (common/run-counters! @driver)] 36 | (expect (= res common/expected-counters) (str driver-type)))))) 37 | 38 | ;; ----------------------------------------------------------------------------- 39 | ;; Tests 40 | ;; ----------------------------------------------------------------------------- 41 | (defdescribe form-test 42 | {:webdriver true 43 | :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} 44 | (it "manages forms" 45 | (doseq [[driver-type driver] common/drivers] 46 | (let [res (common/run-form-test! @driver)] 47 | (expect (= res common/expected-form-vals) (str driver-type)))))) 48 | 49 | 50 | ;; ----------------------------------------------------------------------------- 51 | (defdescribe persistent-sse-test 52 | {:context [(common/persistent-sse-f hk-gen/->sse-response 53 | (assoc http-kit-basic-opts 54 | :wrap hk-gen/wrap-start-responding))]} 55 | (it "handles persistent connections" 56 | (let [res (common/run-persistent-sse-test!)] 57 | (expect (map? res)) 58 | (expect (common/p-sse-status-ok? res)) 59 | (expect (common/p-sse-http1-headers-ok? res)) 60 | (expect (common/p-sse-body-ok? res))))) 61 | 62 | 63 | (defdescribe persistent-sse-test-async 64 | {:context [(common/persistent-sse-f hk-gen/->sse-response 65 | (assoc http-kit-basic-opts 66 | :wrap hk-gen/wrap-start-responding 67 | :ring-async? true))]} 68 | (it "handles persistent connections" 69 | (let [res (common/run-persistent-sse-test!)] 70 | (expect (map? res)) 71 | (common/p-sse-status-ok? res) 72 | (common/p-sse-http1-headers-ok? res) 73 | (common/p-sse-body-ok? res)))) 74 | 75 | -------------------------------------------------------------------------------- /src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.http-kit.impl-test 2 | (:require 3 | [lazytest.core :as lt :refer [defdescribe it expect]] 4 | [org.httpkit.server :as hk-server] 5 | [starfederation.datastar.clojure.api :as d*] 6 | [starfederation.datastar.clojure.adapter.common :as ac] 7 | [starfederation.datastar.clojure.adapter.test :as at] 8 | [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] 9 | [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) 10 | (:import 11 | [java.io Closeable ByteArrayOutputStream OutputStreamWriter])) 12 | 13 | ;; Mock Http-kit channel 14 | (defrecord Channel [^ByteArrayOutputStream baos 15 | !ch-open? 16 | !on-close] 17 | hk-server/Channel 18 | ;; websocket stuff 19 | (open? [_] @!ch-open?) 20 | (websocket? [_] false) 21 | 22 | (on-receive [_ _callback]) 23 | (on-ping [_ _callback]) 24 | 25 | (close [_] 26 | (if @!ch-open? 27 | (do 28 | (vreset! !ch-open? false) 29 | (when-let [on-close @!on-close] 30 | (on-close :whatever)) 31 | true) 32 | false)) 33 | 34 | (on-close [_ callback] 35 | (vreset! !on-close callback)) 36 | 37 | (send! [this data] 38 | (hk-server/send! this data true)) 39 | 40 | (send! [this data close-after-send?] 41 | (cond 42 | (string? data) 43 | (let [^OutputStreamWriter osw (ac/->os-writer baos)] 44 | (doto osw 45 | (.append (str data)) 46 | (.flush))) 47 | 48 | (bytes? data) 49 | (-> baos 50 | (.write (bytes data)))) 51 | 52 | (when close-after-send? 53 | (hk-server/close this)))) 54 | 55 | 56 | (defn ->channel [baos] 57 | (Channel. baos 58 | (volatile! true) 59 | (volatile! nil))) 60 | 61 | 62 | (defn ->sse-gen [baos opts] 63 | (let [c (->channel baos) 64 | send! (impl/->send! c opts)] 65 | (hk-server/on-close c 66 | (fn [status] 67 | (send!) 68 | (when-let [callback (ac/on-close opts)] 69 | (callback c status)))) 70 | (impl/->sse-gen c send!))) 71 | 72 | 73 | (def expected-event-result 74 | (d*/patch-elements! (at/->sse-gen) "msg")) 75 | 76 | (defn send-SSE-event [opts] 77 | (let [baos (ByteArrayOutputStream.)] 78 | (with-open [_baos baos 79 | sse-gen ^Closeable (->sse-gen baos opts)] 80 | (d*/patch-elements! sse-gen "msg" {})) 81 | 82 | (expect 83 | (= (read-bytes baos opts) 84 | expected-event-result)))) 85 | 86 | 87 | (defdescribe simple-test 88 | (it "We can send events using a temp buffer" 89 | (send-SSE-event {})) 90 | 91 | (it "We can send events using a persistent buffered reader" 92 | (send-SSE-event {ac/write-profile ac/buffered-writer-profile})) 93 | 94 | (it "We can send gziped events using a temp buffer" 95 | (send-SSE-event {ac/write-profile ac/gzip-profile :gzip? true})) 96 | 97 | (it "We can send gziped events using a persistent buffered reader" 98 | (send-SSE-event {ac/write-profile ac/gzip-buffered-writer-profile :gzip? true}))) 99 | 100 | 101 | (comment 102 | (require '[lazytest.repl :as ltr]) 103 | (ltr/run-test-var #'simple-test) 104 | :dbg 105 | :rec) 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/test/adapter-common/test/examples/counter.clj: -------------------------------------------------------------------------------- 1 | (ns test.examples.counter 2 | (:require 3 | [test.examples.common :as common] 4 | [dev.onionpancakes.chassis.core :as h] 5 | [dev.onionpancakes.chassis.compiler :as hc] 6 | [ring.util.response :as rur] 7 | [starfederation.datastar.clojure.adapter.common :as ac] 8 | [starfederation.datastar.clojure.api :as d*] 9 | [test.utils :as u])) 10 | 11 | ;; ----------------------------------------------------------------------------- 12 | ;; Views 13 | ;; ----------------------------------------------------------------------------- 14 | (defn ->->id [prefix] 15 | (fn [s] 16 | (str prefix s))) 17 | 18 | (def ->inc-id (->->id "increment-")) 19 | (def ->dec-id (->->id "decrement-")) 20 | 21 | (defn inc-url [id] 22 | (str "/counters/increment/" id)) 23 | 24 | (defn dec-url [id] 25 | (str "/counters/decrement/" id)) 26 | 27 | 28 | (defn sse-inc [counter-id] 29 | (d*/sse-get (inc-url counter-id))) 30 | 31 | (defn sse-inc-post [counter-id] 32 | (d*/sse-post (inc-url counter-id))) 33 | 34 | (defn signal-inc [counter-id] 35 | (format "$%s += 1" counter-id)) 36 | 37 | (defn sse-dec [counter-id] 38 | (d*/sse-get (dec-url counter-id))) 39 | 40 | (defn sse-dec-post [counter-id] 41 | (d*/sse-post (dec-url counter-id))) 42 | 43 | 44 | (defn signal-dec [counter-id] 45 | (format "$%s -= 1" counter-id)) 46 | 47 | 48 | (defn counter-button [id text action] 49 | (hc/compile 50 | [:button 51 | {:id id 52 | :data-on:click action} 53 | text])) 54 | 55 | 56 | (defn counter [id & {:keys [inc dec] 57 | :or {inc sse-inc 58 | dec sse-dec}}] 59 | (let [counter-id (str "counter" id)] 60 | (hc/compile 61 | [:div {(keyword (str "data-signals:" counter-id)) "0"} 62 | (counter-button (->inc-id id) "inc" (inc counter-id)) 63 | (counter-button (->dec-id id) "dec" (dec counter-id)) 64 | [:span {:id counter-id 65 | :data-text (str "$"counter-id)}]]))) 66 | 67 | 68 | (defn counter-page [] 69 | (common/scaffold 70 | (hc/compile 71 | [:div 72 | [:h2 "Counter page"] 73 | [:div 74 | [:h3 "Server side with get"] 75 | (counter "1")] 76 | [:div 77 | [:h3 "Server side with post"] 78 | (counter "2" :inc sse-inc-post :dec sse-dec-post)] 79 | [:div 80 | [:h3 "Client side"] 81 | (counter "3" :inc signal-inc :dec signal-dec)]]))) 82 | 83 | 84 | (def page (h/html (counter-page))) 85 | 86 | 87 | ;; ----------------------------------------------------------------------------- 88 | ;; Handlers common logic 89 | ;; ----------------------------------------------------------------------------- 90 | (defn counters 91 | ([_] 92 | (rur/response page)) 93 | ([_ respond _] 94 | (respond (rur/response page)))) 95 | 96 | 97 | (defn update-signal* [req f & args] 98 | (let [signals (-> req d*/get-signals u/read-json) 99 | id (-> req :path-params :id) 100 | val (get signals id)] 101 | (format "{'%s':%s}" id (apply f val args)))) 102 | 103 | 104 | (defn ->update-signal [->sse-response] 105 | (fn update-signal [req f & args] 106 | (->sse-response req 107 | {ac/on-open (fn [sse-gen] 108 | (d*/patch-signals! sse-gen (apply update-signal* req f args)) 109 | (d*/close-sse! sse-gen))}))) 110 | 111 | 112 | -------------------------------------------------------------------------------- /libraries/sdk-ring/src/main/starfederation/datastar/clojure/adapter/ring/impl.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.ring.impl 2 | (:require 3 | [starfederation.datastar.clojure.adapter.common :as ac] 4 | [starfederation.datastar.clojure.protocols :as p] 5 | [starfederation.datastar.clojure.utils :as u] 6 | [ring.core.protocols :as rp]) 7 | (:import 8 | [java.io Closeable OutputStream] 9 | java.util.concurrent.locks.ReentrantLock)) 10 | 11 | 12 | (def default-write-profile ac/basic-profile) 13 | 14 | 15 | (defn ->send [os opts] 16 | (let [{wrap ac/wrap-output-stream 17 | write! ac/write!} (ac/write-profile opts default-write-profile) 18 | writer (wrap os)] 19 | (fn 20 | ([] 21 | (.close ^Closeable writer)) 22 | ([event-type data-lines event-opts] 23 | (write! writer event-type data-lines event-opts) 24 | (ac/flush writer))))) 25 | 26 | 27 | ;; Note that the send! field has 2 usages: 28 | ;; - it stores the sending function 29 | ;; - it acts as a `is-open?` flag 30 | ;; Also the on-close not being nil means the callback hasn't been called yet. 31 | (deftype SSEGenerator [^:unsynchronized-mutable send! 32 | ^ReentrantLock lock 33 | ^:unsynchronized-mutable on-close 34 | ^:unsynchronized-mutable on-exception] 35 | rp/StreamableResponseBody 36 | (write-body-to-stream [this response output-stream] 37 | (let [opts (::opts response) 38 | on-open (ac/on-open opts)] 39 | 40 | ;; Set the SSEGenerator's state 41 | (set! send! (->send output-stream opts)) 42 | (set! on-exception (or (ac/on-exception opts) 43 | ac/default-on-exception)) 44 | (when-let [cb (ac/on-close opts)] 45 | (set! on-close cb)) 46 | 47 | ;; flush the HTTP headers as soon as possible 48 | (.flush ^OutputStream output-stream) 49 | 50 | ;; SSE connection is ready we call user code 51 | (on-open this))) 52 | 53 | p/SSEGenerator 54 | (send-event! [this event-type data-lines opts] 55 | (u/lock! lock 56 | (if send! ;; still open? 57 | (try 58 | (send! event-type data-lines opts) 59 | true ;; successful send 60 | (catch Exception e 61 | (when (on-exception this e {:sse-gen this 62 | :event-type event-type 63 | :data-lines data-lines 64 | :opts opts}) 65 | (set! send! nil) 66 | (p/close-sse! this)) 67 | false)) ;; the event wasn't sent 68 | false))) ; closed return false 69 | 70 | (get-lock [_] lock) 71 | 72 | (close-sse! [this] 73 | (u/lock! lock 74 | ;; If either send! or on-close are here we try to close them 75 | (if (or send! on-close) 76 | (let [res (ac/close-sse! #(when send! (send!)) 77 | #(when on-close (on-close this)))] 78 | ;; We make sure to clean them up after closing 79 | (set! send! nil) 80 | (set! on-close nil) 81 | (if (instance? Exception res) 82 | (throw res) 83 | true)) 84 | false))) 85 | 86 | (sse-gen? [_] true) 87 | 88 | Closeable 89 | (close [this] 90 | (p/close-sse! this))) 91 | 92 | (defn ->sse-gen 93 | {:tag SSEGenerator} 94 | [] 95 | (SSEGenerator. nil 96 | (ReentrantLock.) 97 | nil 98 | nil)) 99 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.api.common-schemas 2 | (:require 3 | [malli.core :as m] 4 | [malli.util :as mu] 5 | [starfederation.datastar.clojure.api.common :as common] 6 | [starfederation.datastar.clojure.consts :as consts] 7 | [starfederation.datastar.clojure.protocols :as p])) 8 | 9 | (def sse-gen-schema [:fn {:error/message "argument should be a SSEGenerator"} 10 | p/sse-gen?]) 11 | 12 | 13 | (def event-type-schema 14 | [:enum 15 | consts/event-type-patch-elements 16 | consts/event-type-patch-signals]) 17 | 18 | (def data-lines-schema [:seqable :string]) 19 | 20 | (def sse-options-schema 21 | (mu/optional-keys 22 | [:map 23 | [common/id :string] 24 | [common/retry-duration number?]])) 25 | 26 | 27 | (comment 28 | (m/validate sse-options-schema {common/id "1"}) 29 | (m/validate sse-options-schema {common/id 1})) 30 | 31 | ;; ----------------------------------------------------------------------------- 32 | (def elements-schema :string) 33 | (def elements-seq-schema [:seqable :string]) 34 | 35 | 36 | (def patch-modes-schema 37 | [:enum 38 | consts/element-patch-mode-outer 39 | consts/element-patch-mode-inner 40 | consts/element-patch-mode-replace 41 | consts/element-patch-mode-prepend 42 | consts/element-patch-mode-append 43 | consts/element-patch-mode-before 44 | consts/element-patch-mode-after 45 | consts/element-patch-mode-remove]) 46 | 47 | (comment 48 | (m/validate patch-modes-schema consts/element-patch-mode-after) 49 | (m/validate patch-modes-schema "toto")) 50 | 51 | 52 | (def patch-element-options-schemas 53 | (mu/merge 54 | sse-options-schema 55 | (mu/optional-keys 56 | [:map 57 | [common/selector :string] 58 | [common/patch-mode patch-modes-schema] 59 | [common/use-view-transition :boolean]]))) 60 | 61 | ;; ----------------------------------------------------------------------------- 62 | (def selector-schema :string) 63 | 64 | (def remove-element-options-schemas patch-element-options-schemas) 65 | 66 | 67 | ;; ----------------------------------------------------------------------------- 68 | (def signals-schema :string) 69 | 70 | (def patch-signals-options-schemas 71 | (mu/merge 72 | sse-options-schema 73 | (mu/optional-keys 74 | [:map 75 | [common/only-if-missing :boolean]]))) 76 | 77 | 78 | ;; ----------------------------------------------------------------------------- 79 | (def signal-paths-schema [:seqable :string]) 80 | 81 | ;; ----------------------------------------------------------------------------- 82 | (def script-content-schema :string) 83 | 84 | (def execute-script-options-schemas 85 | (mu/merge 86 | sse-options-schema 87 | (mu/optional-keys 88 | [:map 89 | [common/auto-remove :boolean] 90 | [common/attributes [:map-of [:or :string :keyword] :any]]]))) 91 | 92 | 93 | (comment 94 | (m/validate execute-script-options-schemas {common/auto-remove true}) 95 | (m/validate execute-script-options-schemas {common/auto-remove "1"}) 96 | (m/validate execute-script-options-schemas {common/attributes {:t1 1}}) 97 | (m/validate execute-script-options-schemas {common/attributes {"t1" 1}}) 98 | (m/validate execute-script-options-schemas {common/attributes {1 1}}) 99 | (m/validate execute-script-options-schemas {common/attributes :t1})) 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/test/adapter-ring-jetty/test/ring_jetty_test.clj: -------------------------------------------------------------------------------- 1 | (ns test.ring-jetty-test 2 | (:require 3 | [test.common :as common] 4 | [test.examples.ring-handler :as rh] 5 | [lazytest.core :as lt :refer [defdescribe expect it]] 6 | [ring.adapter.jetty :as jetty] 7 | [starfederation.datastar.clojure.adapter.ring :as jetty-gen]) 8 | (:import 9 | [org.eclipse.jetty.server Server ServerConnector])) 10 | 11 | ;; ----------------------------------------------------------------------------- 12 | ;; Ring Jetty stuff 13 | ;; ----------------------------------------------------------------------------- 14 | (defn stop-jetty! [^Server server] 15 | (.stop server)) 16 | 17 | 18 | (defn jetty-server-port [jetty-server] 19 | (let [connector (-> jetty-server 20 | Server/.getConnectors 21 | seq 22 | first)] 23 | (.getLocalPort ^ServerConnector connector))) 24 | 25 | 26 | (def ring-jetty-basic-opts 27 | {:start! jetty/run-jetty 28 | :stop! stop-jetty! 29 | :get-port jetty-server-port 30 | :join? false}) 31 | 32 | 33 | ;; ----------------------------------------------------------------------------- 34 | ;; Tests 35 | ;; ----------------------------------------------------------------------------- 36 | (defdescribe counters-test 37 | {:webdriver true 38 | :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} 39 | (it "manages signals" 40 | (doseq [[driver-type driver] common/drivers] 41 | (let [res (common/run-counters! @driver)] 42 | (expect (= res common/expected-counters) (str driver-type)))))) 43 | 44 | 45 | (defdescribe counters-async-test 46 | {:webdriver true 47 | :context [(common/with-server-f rh/handler 48 | (assoc ring-jetty-basic-opts :async? true))]} 49 | (it "manages signals" 50 | (doseq [[driver-type driver] common/drivers] 51 | (let [res (common/run-counters! @driver)] 52 | (expect (= res common/expected-counters) (str driver-type)))))) 53 | 54 | ;; ----------------------------------------------------------------------------- 55 | (defdescribe form-test 56 | {:webdriver true 57 | :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} 58 | (it "manages forms" 59 | (doseq [[driver-type driver] common/drivers] 60 | (let [res (common/run-form-test! @driver)] 61 | (expect (= res common/expected-form-vals) (str driver-type)))))) 62 | 63 | 64 | (defdescribe form-test-async 65 | {:webdriver true 66 | :context [(common/with-server-f rh/handler 67 | (assoc ring-jetty-basic-opts :async? true))]} 68 | (it "manages forms" 69 | (doseq [[driver-type driver] common/drivers] 70 | (let [res (common/run-form-test! @driver)] 71 | (expect (= res common/expected-form-vals) (str driver-type)))))) 72 | 73 | 74 | ;; ----------------------------------------------------------------------------- 75 | (defdescribe persistent-sse-test 76 | "Testing persistent connection, events are sent from outide the ring handler." 77 | {:context [(common/persistent-sse-f jetty-gen/->sse-response 78 | (assoc ring-jetty-basic-opts 79 | :async? true))]} 80 | (it "handles persistent connections" 81 | (let [res (common/run-persistent-sse-test!)] 82 | (expect (map? res)) 83 | (common/p-sse-status-ok? res) 84 | (common/p-sse-http1-headers-ok? res) 85 | (common/p-sse-body-ok? res)))) 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /libraries/sdk-malli-schemas/src/main/starfederation/datastar/clojure/adapter/common_schemas.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.common-schemas 2 | (:require 3 | [malli.core :as m] 4 | [malli.util :as mu] 5 | [starfederation.datastar.clojure.adapter.common :as ac]) 6 | (:import 7 | [java.io BufferedWriter OutputStream OutputStreamWriter Writer] 8 | java.nio.charset.Charset 9 | java.util.zip.GZIPOutputStream)) 10 | 11 | 12 | (defn output-stream? [o] 13 | (instance? OutputStream o)) 14 | 15 | (def output-stream-schema 16 | [:fn {:error/message "should be a java.io.OutputStream"} 17 | output-stream?]) 18 | 19 | 20 | (defn gzip-output-stream? [o] 21 | (instance? GZIPOutputStream o)) 22 | 23 | (def gzip-output-stream-schema 24 | [:fn {:error/message "should be a java.util.zip.GZIPOutputStream"} 25 | gzip-output-stream?]) 26 | 27 | 28 | (m/=> starfederation.datastar.clojure.adapter.common/->gzip-os 29 | [:function 30 | [:-> output-stream-schema gzip-output-stream-schema] 31 | [:-> output-stream-schema :int gzip-output-stream-schema]]) 32 | 33 | 34 | (defn output-stream-writer? [o] 35 | (instance? OutputStreamWriter o)) 36 | 37 | (def output-stream-writer-schema 38 | [:fn {:error/message "should be a java.io.OutputStreamWriter"} 39 | output-stream-writer?]) 40 | 41 | 42 | (defn charset? [c] 43 | (instance? Charset c)) 44 | 45 | (def charset-schema 46 | [:fn {:error/message "should be a java.nio.charset.Charset"} 47 | charset?]) 48 | 49 | 50 | (m/=> starfederation.datastar.clojure.adapter.common/->os-writer 51 | [:function 52 | [:-> output-stream-schema output-stream-writer-schema] 53 | [:-> output-stream-schema charset-schema output-stream-writer-schema]]) 54 | 55 | 56 | (defn buffered-writer? [o] 57 | (instance? BufferedWriter o)) 58 | 59 | (def buffered-writer-schema 60 | [:fn {:error/message "should be a java.io.BufferedWriter"} 61 | buffered-writer?]) 62 | 63 | 64 | (m/=> starfederation.datastar.clojure.adapter.common/->buffered-writer 65 | [:function 66 | [:-> output-stream-writer-schema buffered-writer-schema] 67 | [:-> output-stream-writer-schema :int buffered-writer-schema]]) 68 | 69 | 70 | (defn writer? [x] 71 | (instance? Writer x)) 72 | 73 | (def writer-schema 74 | [:fn {:error/message "should be a java.io.Writer"} 75 | writer?]) 76 | 77 | (def wrap-output-stream-schema 78 | [:-> output-stream-schema writer-schema]) 79 | 80 | (def write-profile-schema 81 | (mu/optional-keys 82 | [:map 83 | [ac/wrap-output-stream wrap-output-stream-schema] 84 | [ac/write! fn?] 85 | [ac/content-encoding :string]] 86 | 87 | [ac/content-encoding])) 88 | 89 | (def SSE-write-profile-opts 90 | (mu/optional-keys 91 | [:map 92 | [ac/write-profile write-profile-schema]])) 93 | 94 | 95 | (def ->sse-response-http-options-schema 96 | (mu/optional-keys 97 | [:map 98 | [:status number?] 99 | [:headers [:map-of :string [:or :string [:seqable :string]]]]] 100 | [:status :headers])) 101 | 102 | 103 | (def ->sse-response-callbacks-options-schema 104 | (mu/optional-keys 105 | [:map 106 | [ac/on-open fn?] 107 | [ac/on-close fn?] 108 | [ac/on-exception fn?]] 109 | [ac/on-close ac/on-exception])) 110 | 111 | 112 | (def ->sse-response-options-schema 113 | (-> ->sse-response-http-options-schema 114 | (mu/merge ->sse-response-callbacks-options-schema) 115 | (mu/merge SSE-write-profile-opts))) 116 | 117 | -------------------------------------------------------------------------------- /src/bb-example/src/main/bb_example/animation/core.clj: -------------------------------------------------------------------------------- 1 | (ns bb-example.animation.core 2 | (:require 3 | [clojure.math :as math])) 4 | 5 | 6 | ;; ----------------------------------------------------------------------------- 7 | ;; Basic math 8 | ;; ----------------------------------------------------------------------------- 9 | (defn point [x y] 10 | {:x x :y y}) 11 | 12 | (defn distance [p1 p2] 13 | (let [x1 (:x p1) 14 | y1 (:y p1) 15 | x2 (:x p2) 16 | y2 (:y p2)] 17 | (math/sqrt (+ 18 | (math/pow (- x2 x1) 2) 19 | (math/pow (- y2 y1) 2))))) 20 | 21 | 22 | (defn clamp [low n high] 23 | (min high (max low n))) 24 | 25 | (defn clamp-color [v] 26 | (clamp 0 v 255)) 27 | 28 | (comment 29 | (clamp-color -1) 30 | (clamp-color 100) 31 | (clamp-color 300)) 32 | 33 | ;; ----------------------------------------------------------------------------- 34 | ;; State management 35 | ;; ----------------------------------------------------------------------------- 36 | (defn next-color [current] 37 | (case current 38 | :r :g 39 | :g :b 40 | :b :r)) 41 | 42 | 43 | (def starting-state 44 | {:animator nil 45 | :animation-tick 100 46 | :clock 0 47 | :size {:x 50 :y 50} 48 | :color :r 49 | :pings []}) 50 | 51 | 52 | 53 | (def default-ping-duration 20) 54 | (def default-ping-speed 0.5) 55 | 56 | 57 | (defn resize [state x y] 58 | (assoc state :size {:x x :y y})) 59 | 60 | 61 | (defn ->ping [state pos duration speed] 62 | {:clock (:clock state) 63 | :color (:color state) 64 | :duration duration 65 | :speed speed 66 | :traveled 0 67 | :pos pos}) 68 | 69 | 70 | (defn add-ping 71 | ([state pos] 72 | (add-ping state pos default-ping-duration default-ping-speed)) 73 | ([state pos duration speed] 74 | (-> state 75 | (update :color next-color) 76 | (update :pings conj (->ping state pos duration speed))))) 77 | 78 | 79 | (defn add-random-pings [state n] 80 | (let [size (:size state) 81 | x (:x size) 82 | y (:y size)] 83 | (reduce 84 | (fn [acc _] 85 | (add-ping acc (point (inc (rand-int x)) 86 | (inc (rand-int y))))) 87 | state 88 | (range n)))) 89 | 90 | 91 | (defn keep-ping? [general-clock ping] 92 | (-> (:clock ping) 93 | (+ (:duration ping)) 94 | (- general-clock) 95 | pos?)) 96 | 97 | 98 | (defn traveled-distance [general-clock ping-clock speed] 99 | (let [elapsed-time (- general-clock ping-clock)] 100 | (int (math/floor (* speed elapsed-time))))) 101 | 102 | 103 | (defn update-ping [general-clock ping] 104 | (let [c (:clock ping) 105 | s (:speed ping)] 106 | (assoc ping 107 | :traveled (traveled-distance general-clock c s)))) 108 | 109 | (defn ->x-update-pings [general-clock] 110 | (comp 111 | (filter #(keep-ping? general-clock %)) 112 | (map #(update-ping general-clock %)))) 113 | 114 | 115 | (defn start-animating [state id] 116 | (assoc state :animator id)) 117 | 118 | 119 | (defn stop-animating [state] 120 | (dissoc state :animator)) 121 | 122 | 123 | (defn step-state [state] 124 | (let [new-clock (-> state :clock inc) 125 | pings (:pings state) 126 | x-update-pings (->x-update-pings new-clock) 127 | new-pings (into [] x-update-pings pings)] 128 | (-> state 129 | transient 130 | (assoc! :clock new-clock 131 | :color (next-color (:color state)) 132 | :pings new-pings) 133 | persistent! 134 | (cond-> 135 | (empty? new-pings) stop-animating)))) 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/dev/examples/animation_gzip/animation.clj: -------------------------------------------------------------------------------- 1 | (ns examples.animation-gzip.animation 2 | (:require 3 | [clojure.math :as math])) 4 | 5 | 6 | ;; ----------------------------------------------------------------------------- 7 | ;; Basic math 8 | ;; ----------------------------------------------------------------------------- 9 | (defn point [x y] 10 | {:x x :y y}) 11 | 12 | (defn distance [p1 p2] 13 | (let [x1 (:x p1) 14 | y1 (:y p1) 15 | x2 (:x p2) 16 | y2 (:y p2)] 17 | (math/sqrt (+ 18 | (math/pow (- x2 x1) 2) 19 | (math/pow (- y2 y1) 2))))) 20 | 21 | 22 | (defn clamp [low n high] 23 | (min high (max low n))) 24 | 25 | (defn clamp-color [v] 26 | (clamp 0 v 255)) 27 | 28 | (comment 29 | (clamp-color -1) 30 | (clamp-color 100) 31 | (clamp-color 300)) 32 | 33 | ;; ----------------------------------------------------------------------------- 34 | ;; State management 35 | ;; ----------------------------------------------------------------------------- 36 | (defn next-color [current] 37 | (case current 38 | :r :g 39 | :g :b 40 | :b :r)) 41 | 42 | 43 | (def starting-state 44 | {:animator nil 45 | :animation-tick 100 46 | :clock 0 47 | :size {:x 50 :y 50} 48 | :color :r 49 | :pings []}) 50 | 51 | 52 | 53 | (def default-ping-duration 20) 54 | (def default-ping-speed 0.5) 55 | 56 | 57 | (defn resize [state x y] 58 | (assoc state :size {:x x :y y})) 59 | 60 | 61 | (defn ->ping [state pos duration speed] 62 | {:clock (:clock state) 63 | :color (:color state) 64 | :duration duration 65 | :speed speed 66 | :traveled 0 67 | :pos pos}) 68 | 69 | 70 | (defn add-ping 71 | ([state pos] 72 | (add-ping state pos default-ping-duration default-ping-speed)) 73 | ([state pos duration speed] 74 | (-> state 75 | (update :color next-color) 76 | (update :pings conj (->ping state pos duration speed))))) 77 | 78 | 79 | (defn add-random-pings [state n] 80 | (let [size (:size state) 81 | x (:x size) 82 | y (:y size)] 83 | (reduce 84 | (fn [acc _] 85 | (add-ping acc (point (inc (rand-int x)) 86 | (inc (rand-int y))))) 87 | state 88 | (range n)))) 89 | 90 | 91 | (defn keep-ping? [general-clock ping] 92 | (-> (:clock ping) 93 | (+ (:duration ping)) 94 | (- general-clock) 95 | pos?)) 96 | 97 | 98 | (defn traveled-distance [general-clock ping-clock speed] 99 | (let [elapsed-time (- general-clock ping-clock)] 100 | (int (math/floor (* speed elapsed-time))))) 101 | 102 | 103 | (defn update-ping [general-clock ping] 104 | (let [c (:clock ping) 105 | s (:speed ping)] 106 | (assoc ping 107 | :traveled (traveled-distance general-clock c s)))) 108 | 109 | (defn ->x-update-pings [general-clock] 110 | (comp 111 | (filter #(keep-ping? general-clock %)) 112 | (map #(update-ping general-clock %)))) 113 | 114 | 115 | (defn start-animating [state id] 116 | (assoc state :animator id)) 117 | 118 | 119 | (defn stop-animating [state] 120 | (dissoc state :animator)) 121 | 122 | 123 | (defn step-state [state] 124 | (let [new-clock (-> state :clock inc) 125 | pings (:pings state) 126 | x-update-pings (->x-update-pings new-clock) 127 | new-pings (into [] x-update-pings pings)] 128 | (-> state 129 | transient 130 | (assoc! :clock new-clock 131 | :color (next-color (:color state)) 132 | :pings new-pings) 133 | persistent! 134 | (cond-> 135 | (empty? new-pings) stop-animating)))) 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/test/adapter-rj9a/test/rj9a_test.clj: -------------------------------------------------------------------------------- 1 | (ns test.rj9a-test 2 | (:require 3 | [test.common :as common] 4 | [test.examples.ring-handler :as rh] 5 | [lazytest.core :as lt :refer [defdescribe expect it]] 6 | [ring.adapter.jetty9 :as jetty] 7 | [starfederation.datastar.clojure.adapter.ring :as jetty-gen]) 8 | (:import 9 | [org.eclipse.jetty.server Server ServerConnector])) 10 | 11 | ;; ----------------------------------------------------------------------------- 12 | ;; Ring Jetty stuff 13 | ;; ----------------------------------------------------------------------------- 14 | (defn stop-jetty! [^Server server] 15 | (.stop server)) 16 | 17 | 18 | (defn jetty-server-port [jetty-server] 19 | (let [connector (-> jetty-server 20 | Server/.getConnectors 21 | seq 22 | first)] 23 | (.getLocalPort ^ServerConnector connector))) 24 | 25 | 26 | (def ring-jetty-basic-opts 27 | {:start! jetty/run-jetty 28 | :stop! stop-jetty! 29 | :get-port jetty-server-port 30 | :join? false}) 31 | 32 | 33 | ;; ----------------------------------------------------------------------------- 34 | ;; Tests 35 | ;; ----------------------------------------------------------------------------- 36 | (defdescribe counters-test 37 | {:webdriver true 38 | :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} 39 | (it "manages signals" 40 | (doseq [[driver-type driver] common/drivers] 41 | (let [res (common/run-counters! @driver)] 42 | (expect (= res common/expected-counters) (str driver-type)))))) 43 | 44 | 45 | (defdescribe counters-async-test 46 | {:webdriver true 47 | :context [(common/with-server-f rh/handler 48 | (assoc ring-jetty-basic-opts :async? true))]} 49 | (it "manages signals" 50 | (doseq [[driver-type driver] common/drivers] 51 | (let [res (common/run-counters! @driver)] 52 | (expect (= res common/expected-counters) (str driver-type)))))) 53 | 54 | ;; ----------------------------------------------------------------------------- 55 | (defdescribe form-test 56 | {:webdriver true 57 | :context [(common/with-server-f rh/handler ring-jetty-basic-opts)]} 58 | (it "manages forms" 59 | (doseq [[driver-type driver] common/drivers] 60 | (let [res (common/run-form-test! @driver)] 61 | (expect (= res common/expected-form-vals) (str driver-type)))))) 62 | 63 | 64 | (defdescribe form-test-async 65 | {:webdriver true 66 | :context [(common/with-server-f rh/handler 67 | (assoc ring-jetty-basic-opts :async? true))]} 68 | (it "manages forms" 69 | (doseq [[driver-type driver] common/drivers] 70 | (let [res (common/run-form-test! @driver)] 71 | (expect (= res common/expected-form-vals) (str driver-type)))))) 72 | 73 | 74 | ;; ----------------------------------------------------------------------------- 75 | (defdescribe persistent-sse-test 76 | "Testing persistent connection, events are sent from outide the ring handler." 77 | {:context [(common/persistent-sse-f jetty-gen/->sse-response 78 | (assoc ring-jetty-basic-opts 79 | :async? true))]} 80 | (it "handles persistent connections" 81 | (let [res (common/run-persistent-sse-test!)] 82 | (expect (map? res)) 83 | (common/p-sse-status-ok? res) 84 | (common/p-sse-http1-headers-ok? res) 85 | (common/p-sse-body-ok? res)))) 86 | 87 | 88 | 89 | (comment 90 | (require '[lazytest.repl :as ltr]) 91 | (ltr/run-test-var #'counters-test) 92 | (ltr/run-test-var #'form-test) 93 | (ltr/run-test-var #'persistent-sse-test) 94 | (user/clear-terminal!)) 95 | -------------------------------------------------------------------------------- /src/dev/examples/utils.clj: -------------------------------------------------------------------------------- 1 | (ns examples.utils 2 | (:require 3 | [charred.api :as charred] 4 | [fireworks.core :refer [?]] 5 | [puget.printer :as pp] 6 | [starfederation.datastar.clojure.api :as d*])) 7 | 8 | 9 | ;; ----------------------------------------------------------------------------- 10 | ;; Misc utils 11 | ;; ----------------------------------------------------------------------------- 12 | (defn clear-terminal! [] 13 | (binding [*out* (java.io.PrintWriter. System/out)] 14 | (print "\033c") 15 | (flush))) 16 | 17 | 18 | (defmacro force-out [& body] 19 | `(binding [*out* (java.io.OutputStreamWriter. System/out)] 20 | ~@body)) 21 | 22 | 23 | (defn pp-request [req] 24 | (-> req 25 | (dissoc :reitit.core/match :reitit.core/router) 26 | pp/pprint 27 | pp/with-color)) 28 | 29 | 30 | (defn ?req [req] 31 | (? (dissoc req :reitit.core/match :reitit.core/router))) 32 | 33 | 34 | (def ^:private bufSize 1024) 35 | (def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) 36 | 37 | 38 | 39 | (defn get-signals [req] 40 | (some-> req d*/get-signals read-json)) 41 | 42 | 43 | (defn rr [sym] 44 | (try 45 | (requiring-resolve sym) 46 | (catch Exception _ 47 | nil))) 48 | 49 | ;; ----------------------------------------------------------------------------- 50 | ;; httpkit server 51 | ;; ----------------------------------------------------------------------------- 52 | (defonce !hk-server (atom nil)) 53 | 54 | (def http-kit-run! (rr 'org.httpkit.server/run-server)) 55 | (def http-kit-stop! (rr 'org.httpkit.server/server-stop!)) 56 | 57 | (defn reboot-hk-server! [handler] 58 | (if-not http-kit-run! 59 | (println "http kit isn't in the classpath") 60 | (swap! !hk-server 61 | (fn [server] 62 | (when server 63 | (http-kit-stop! server)) 64 | (http-kit-run! handler 65 | {:port 8080 66 | :legacy-return-value? false}))))) 67 | 68 | ;; ----------------------------------------------------------------------------- 69 | ;; ring jetty server 70 | ;; ----------------------------------------------------------------------------- 71 | (defonce !jetty-server (atom nil)) 72 | 73 | (def ring-jetty-run! (rr 'ring.adapter.jetty/run-jetty)) 74 | 75 | 76 | (defn reboot-jetty-server! [handler & {:as opts}] 77 | (if-not ring-jetty-run! 78 | (println "Ring jetty isn't in the classpath") 79 | (swap! !jetty-server 80 | (fn [server] 81 | (when server 82 | (.stop server)) 83 | (ring-jetty-run! handler 84 | (merge 85 | {:port 8081 86 | :join? false} 87 | opts)))))) 88 | 89 | ;; ----------------------------------------------------------------------------- 90 | ;; rj9a server 91 | ;; ----------------------------------------------------------------------------- 92 | (defonce !rj9a-server (atom nil)) 93 | 94 | (def rj9a-run! (rr 'ring.adapter.jetty9/run-jetty)) 95 | 96 | 97 | (defn reboot-rj9a-server! [handler & {:as opts}] 98 | (if-not rj9a-run! 99 | (println "Ring jetty isn't in the classpath") 100 | (swap! !rj9a-server 101 | (fn [server] 102 | (when server 103 | (.stop server)) 104 | (rj9a-run! handler 105 | (merge 106 | {:port 8082 107 | :join? false} 108 | opts)))))) 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/dev/examples/tiny_gzip.clj: -------------------------------------------------------------------------------- 1 | (ns examples.tiny-gzip 2 | (:require 3 | [examples.common :as c] 4 | [examples.utils :as u] 5 | [dev.onionpancakes.chassis.core :as h] 6 | [ring.util.response :as ruresp] 7 | [reitit.ring :as rr] 8 | [reitit.ring.middleware.parameters :as reitit-params] 9 | [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] 10 | [starfederation.datastar.clojure.adapter.ring :as ring-gen] 11 | [starfederation.datastar.clojure.adapter.common :as ac] 12 | [starfederation.datastar.clojure.api :as d*])) 13 | 14 | 15 | ;; Here we try to use compression on little update to see if some 16 | ;; server buffer holds updates for short fragments. 17 | ;; It doesn't seem to be the case, compressing tiny events seems to work fine 18 | 19 | 20 | (defonce !current-val (atom nil)) 21 | (defonce !sses (atom #{})) 22 | 23 | 24 | (defn render-val [current-val] 25 | [:span#val current-val]) 26 | 27 | 28 | (defn page [current-val] 29 | (h/html 30 | (c/page-scaffold 31 | [:div#page {:data-init (d*/sse-get "/updates")} 32 | [:h1 "Test page"] 33 | [:input {:type "text" 34 | :data-bind:input true 35 | :data-on:input__debounce.100ms(d*/sse-get "/change-val")}] 36 | [:br] 37 | [:div 38 | (render-val current-val)]]))) 39 | 40 | 41 | (defn home 42 | ([_] 43 | (ruresp/response (page @!current-val))) 44 | ([req respond _] 45 | (respond (home req)))) 46 | 47 | (defn send-val! [sse v] 48 | (try 49 | (d*/patch-elements! sse (h/html (render-val v))) 50 | (catch Exception e 51 | (println e)))) 52 | 53 | (defn broadcast-new-val! [sses v] 54 | (doseq [sse sses] 55 | (send-val! sse v))) 56 | 57 | 58 | (add-watch !current-val ::watch 59 | (fn [_key _ref _old new] 60 | (broadcast-new-val! @!sses new))) 61 | 62 | 63 | (defn ->change-val [->sse-response] 64 | (fn change-val 65 | ([req] 66 | (let [signals (u/get-signals req) 67 | input-val (get signals "input")] 68 | (->sse-response req 69 | {:status 204 70 | ac/on-open 71 | (fn [sse] 72 | (d*/with-open-sse sse 73 | (reset! !current-val input-val)))}))) 74 | ([req respond _raise] 75 | (respond (change-val req))))) 76 | 77 | 78 | (defn ->updates[->sse-response opts] 79 | (fn updates 80 | ([req] 81 | (->sse-response req 82 | (merge opts 83 | {ac/on-open 84 | (fn [sse] 85 | (swap! !sses conj sse)) 86 | ac/on-close 87 | (fn [sse & _args] 88 | (swap! !sses disj sse))}))) 89 | ([req respond _raise] 90 | (respond (updates req))))) 91 | 92 | 93 | (defn ->router [->sse-response opts] 94 | (rr/router 95 | [["/" {:handler home}] 96 | ["/change-val" {:handler (->change-val ->sse-response) 97 | :middleware [reitit-params/parameters-middleware]}] 98 | ["/updates" {:handler (->updates ->sse-response opts)}]])) 99 | 100 | (def default-handler (rr/create-default-handler)) 101 | 102 | 103 | (defn ->handler [->sse-response & {:as opts}] 104 | (rr/ring-handler (->router ->sse-response opts) 105 | default-handler)) 106 | 107 | 108 | 109 | (def handler-hk (->handler hk-gen/->sse-response 110 | hk-gen/write-profile hk-gen/gzip-profile)) 111 | (def handler-ring (->handler ring-gen/->sse-response 112 | ring-gen/write-profile ring-gen/gzip-profile)) 113 | 114 | (comment 115 | :dbg 116 | :rec 117 | (u/clear-terminal!) 118 | !sses 119 | (reset! !sses #{}) 120 | (u/reboot-hk-server! #'handler-hk) 121 | (u/reboot-jetty-server! #'handler-ring {:async? true})) 122 | 123 | -------------------------------------------------------------------------------- /libraries/sdk-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit/impl.cljc: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.adapter.http-kit.impl 2 | (:require 3 | [starfederation.datastar.clojure.adapter.common :as ac] 4 | [starfederation.datastar.clojure.protocols :as p] 5 | [starfederation.datastar.clojure.utils :as u] 6 | [org.httpkit.server :as hk-server]) 7 | (:import 8 | java.lang.Exception 9 | [java.io ByteArrayOutputStream Closeable] 10 | [java.util.concurrent.locks ReentrantLock])) 11 | 12 | 13 | (def basic-profile 14 | "Basic write profile using temporary [[StringBuilder]]s, no output stream and 15 | no compression." 16 | {ac/write! (ac/->build-event-str)}) 17 | 18 | 19 | ;; ----------------------------------------------------------------------------- 20 | ;; Sending the headers 21 | ;; ----------------------------------------------------------------------------- 22 | (defn send-base-sse-response! 23 | "Send the response headers, this should be done as soon as the conneciton is 24 | open." 25 | [ch req {:keys [status] :as opts}] 26 | (hk-server/send! ch 27 | {:status (or status 200) 28 | :headers (ac/headers req opts)} 29 | false)) 30 | 31 | ;; ----------------------------------------------------------------------------- 32 | ;; Sending events machinery 33 | ;; ----------------------------------------------------------------------------- 34 | (defn ->send-simple [ch write-profile] 35 | (let [write! (ac/write! write-profile)] 36 | (fn 37 | ([]) 38 | ([event-type data-lines opts] 39 | (let [event (write! event-type data-lines opts)] 40 | (hk-server/send! ch event false)))))) 41 | 42 | 43 | (defn flush-baos! [^ByteArrayOutputStream baos ch] 44 | (let [msg (.toByteArray baos)] 45 | (.reset baos) 46 | (hk-server/send! ch msg false))) 47 | 48 | 49 | (defn ->send-with-output-stream [ch write-profile] 50 | (let [^ByteArrayOutputStream baos (ByteArrayOutputStream.) 51 | {wrap-os ac/wrap-output-stream 52 | write! ac/write!} write-profile 53 | writer (wrap-os baos)] 54 | (fn 55 | ([] 56 | ;; Close the writer first to finish the gzip process 57 | (.close ^Closeable writer) 58 | ;; Flush towards SSE out 59 | (flush-baos! baos ch)) 60 | ([event-type data-lines opts] 61 | (write! writer event-type data-lines opts) 62 | (ac/flush writer) 63 | (flush-baos! baos ch))))) 64 | 65 | 66 | (defn ->send! [ch opts] 67 | (let [write-profile (or (ac/write-profile opts) 68 | basic-profile)] 69 | (if (ac/wrap-output-stream write-profile) 70 | (->send-with-output-stream ch write-profile) 71 | (->send-simple ch write-profile)))) 72 | 73 | ;; ----------------------------------------------------------------------------- 74 | ;; SSE gen 75 | ;; ----------------------------------------------------------------------------- 76 | (deftype SSEGenerator [ch lock send! on-exception] 77 | p/SSEGenerator 78 | (send-event! [this event-type data-lines opts] 79 | (u/lock! lock 80 | (try 81 | (send! event-type data-lines opts) 82 | (catch Exception e 83 | (when (on-exception this e {:sse-gen this 84 | :event-type event-type 85 | :data-lines data-lines 86 | :opts opts}) 87 | (p/close-sse! this)) 88 | false)))) 89 | 90 | (get-lock [_] lock) 91 | 92 | (close-sse! [_] 93 | (hk-server/close ch)) 94 | 95 | (sse-gen? [_] true) 96 | 97 | #?@(:bb [] 98 | :clj [Closeable 99 | (close [this] 100 | (p/close-sse! this))])) 101 | 102 | 103 | (defn ->sse-gen 104 | ([ch send!] 105 | (->sse-gen ch send! {})) 106 | ([ch send! opts] 107 | (SSEGenerator. ch (ReentrantLock.) send! (or (ac/on-exception opts) 108 | ac/default-on-exception)))) 109 | -------------------------------------------------------------------------------- /libraries/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj: -------------------------------------------------------------------------------- 1 | (ns starfederation.datastar.clojure.brotli 2 | "Tools to work with Brotli. 3 | 4 | The main api is 5 | - [[compress]] 6 | - [[decompress]] 7 | - [[->brotli-profile]] 8 | - [[->brotli-buffered-writer-profile]] 9 | " 10 | (:require 11 | [clojure.math :as m] 12 | [starfederation.datastar.clojure.adapter.common :as ac]) 13 | (:import 14 | com.aayushatharva.brotli4j.Brotli4jLoader 15 | [com.aayushatharva.brotli4j.encoder 16 | Encoder 17 | Encoder$Parameters 18 | Encoder$Mode 19 | BrotliOutputStream] 20 | [com.aayushatharva.brotli4j.decoder Decoder] 21 | [java.io OutputStream])) 22 | 23 | 24 | ;; Code taken from https://github.com/andersmurphy/hyperlith 25 | ;; Thanks Anders! 26 | ;; ----------------------------------------------------------------------------- 27 | ;; Setup & helpers 28 | ;; ----------------------------------------------------------------------------- 29 | (defonce ensure-br 30 | (Brotli4jLoader/ensureAvailability)) 31 | 32 | 33 | (defn window-size->kb [window-size] 34 | (/ (- (m/pow 2 window-size) 16) 1000)) 35 | 36 | 37 | (defn encoder-params 38 | "Options used when creating a brotli encoder. 39 | 40 | Arg keys: 41 | - `:quality`: Brotli quality defaults to 5 42 | - `:window-size`: Brotli window size defaults to 24 43 | " 44 | [{:keys [quality window-size]}] 45 | (doto (Encoder$Parameters/new) 46 | (.setMode Encoder$Mode/TEXT) 47 | ;; LZ77 window size (0, 10-24) (default: 24) 48 | ;; window size is (pow(2, NUM) - 16) 49 | (.setWindow (or window-size 24)) 50 | (.setQuality (or quality 5)))) 51 | 52 | 53 | ;; ----------------------------------------------------------------------------- 54 | ;; 1 shot compression 55 | ;; ----------------------------------------------------------------------------- 56 | (defn compress 57 | " 58 | Compress `data` (either a byte array or a string) using Brotli. 59 | 60 | Opts keys from [[encoder-params]]: 61 | - `:quality`: Brotli quality 62 | - `:window-size`: Brotli window size 63 | " 64 | 65 | [data & {:as opts}] 66 | (-> (if (string? data) 67 | (String/.getBytes data) 68 | ^byte/1 data) 69 | (Encoder/compress (encoder-params opts)))) 70 | 71 | 72 | (defn decompress 73 | "Decompress Brotli compressed data, returns a string." 74 | [data] 75 | (let [decompressed (Decoder/decompress data)] 76 | (String/new (.getDecompressedData decompressed)))) 77 | 78 | 79 | (comment 80 | (decompress (compress "hello"))) 81 | 82 | 83 | ;; ----------------------------------------------------------------------------- 84 | ;; Write profiles 85 | ;; ----------------------------------------------------------------------------- 86 | (defn ->brotli-os 87 | "Wrap `out-stream` with Brotli compression. 88 | 89 | Opts from [[encoder-params]]: 90 | - `:quality`: Brotli quality 91 | - `:window-size`: Brotli window size 92 | " 93 | [^OutputStream out-stream & {:as opts}] 94 | (BrotliOutputStream/new out-stream (encoder-params opts))) 95 | 96 | 97 | (def brotli-content-encoding "br") 98 | 99 | 100 | (defn ->brotli-profile 101 | "Make a write profile using Brotli compression and a temporary buffer 102 | strategy. 103 | 104 | Opts from [[encoder-params]]: 105 | - `:quality`: Brotli quality 106 | - `:window-size`: Brotli window size 107 | " 108 | [& {:as opts}] 109 | {ac/wrap-output-stream 110 | (fn [^OutputStream os] 111 | (-> os 112 | (->brotli-os opts) 113 | ac/->os-writer)) 114 | ac/write! (ac/->write-with-temp-buffer!) 115 | ac/content-encoding brotli-content-encoding}) 116 | 117 | 118 | (defn ->brotli-buffered-writer-profile 119 | "Make a write profile using Brotli compression and a permanent buffer 120 | strategy. 121 | 122 | Opts from [[encoder-params]]: 123 | - `:quality`: Brotli quality 124 | - `:window-size`: Brotli window size 125 | " 126 | [& {:as opts}] 127 | {ac/wrap-output-stream 128 | (fn [^OutputStream os] 129 | (-> os 130 | (->brotli-os opts) 131 | ac/->os-writer 132 | ac/->buffered-writer)) 133 | ac/write! ac/write-to-buffered-writer! 134 | ac/content-encoding brotli-content-encoding}) 135 | 136 | 137 | -------------------------------------------------------------------------------- /.github/workflows/release-sdk.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | --- 3 | name: Release Clojure SDK 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - ./** 10 | - .github/workflows/release-sdk.yml 11 | pull_request: 12 | paths: 13 | - ./** 14 | - .github/workflows/release-sdk.yml 15 | workflow_dispatch: 16 | inputs: 17 | publish: 18 | description: "Publish artifacts to Clojars" 19 | required: true 20 | type: boolean 21 | default: false 22 | jobs: 23 | build-clojure: 24 | runs-on: ubuntu-24.04 25 | defaults: 26 | run: 27 | working-directory: ./ 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag: v4.22 32 | 33 | - name: Setup java 21 as default java 34 | run: | 35 | echo "JAVA_HOME=$JAVA_HOME_21_X64" >> $GITHUB_ENV 36 | echo "$JAVA_HOME_21_X64/bin" >> $GITHUB_PATH 37 | shell: bash 38 | 39 | - name: Install clojure + tools 40 | uses: DeLaGuardo/setup-clojure@ada62bb3282a01a296659d48378b812b8e097360 # tag 13.2 41 | with: 42 | cli: latest 43 | bb: latest 44 | 45 | - name: Cache clojure dependencies 46 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # tag: v4.2.3 47 | with: 48 | path: | 49 | ~/.m2/repository 50 | ~/.gitlibs 51 | ~/.deps.clj 52 | key: cljdeps-${{ hashFiles('**/deps.edn') }} 53 | restore-keys: cljdeps- 54 | 55 | - name: Run Clojure test suite 56 | run: bb test:all 57 | 58 | - name: Run Babashka test suite 59 | run: bb test:bb 60 | 61 | - name: Build jar artifacts 62 | run: bb install:all 63 | 64 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 65 | with: 66 | name: sdk.jar 67 | path: libraries/sdk/target/*.jar 68 | if-no-files-found: error 69 | compression-level: 0 70 | 71 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 72 | with: 73 | name: brotli.jar 74 | path: libraries/sdk-brotli/target/*.jar 75 | if-no-files-found: error 76 | compression-level: 0 77 | 78 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 79 | with: 80 | name: adapter-http-kit.jar 81 | path: libraries/sdk-http-kit/target/*.jar 82 | if-no-files-found: error 83 | compression-level: 0 84 | 85 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 86 | with: 87 | name: adapter-http-kit-malli-schemas.jar 88 | path: libraries/sdk-http-kit-malli-schemas/target/*.jar 89 | if-no-files-found: error 90 | compression-level: 0 91 | 92 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 93 | with: 94 | name: malli-schemas.jar 95 | path: libraries/sdk-malli-schemas/target/*.jar 96 | if-no-files-found: error 97 | compression-level: 0 98 | 99 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 100 | with: 101 | name: adapter-ring.jar 102 | path: libraries/sdk-ring/target/*.jar 103 | if-no-files-found: error 104 | compression-level: 0 105 | 106 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag: v4.6.2 107 | with: 108 | name: adapter-ring-malli-schemas.jar 109 | path: libraries/sdk-ring-malli-schemas/target/*.jar 110 | if-no-files-found: error 111 | compression-level: 0 112 | 113 | - name: Publish artifacts to clojars 114 | if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' 115 | env: 116 | CLOJARS_USERNAME: ${{ secrets.CLOJARS_USERNAME }} 117 | CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }} 118 | run: bb publish:all 119 | --------------------------------------------------------------------------------