├── 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 | [](https://clojars.org/dev.data-star.clojure/ring-malli-schemas)
8 |
9 | [](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 | [](https://clojars.org/dev.data-star.clojure/http-kit-malli-schemas)
8 |
9 | [](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 | [](https://clojars.org/dev.data-star.clojure/malli-schemas)
8 |
9 | [](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 | [](https://clojars.org/dev.data-star.clojure/brotli)
8 | [](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 | [](https://clojars.org/dev.data-star.clojure/sdk)
10 | [](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 |

15 |
16 |
17 | SSE events will be streamed from the backend to the frontend.
18 |
19 |
20 |
23 |
24 |
25 |
28 |
29 |
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 |

17 |
18 |
19 | SSE events will be streamed from the backend to the frontend.
20 |
21 |
22 |
25 |
26 |
27 |
30 |
31 |
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 | [](https://clojars.org/dev.data-star.clojure/ring)
8 |
9 | [](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 "")
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 | [](https://clojars.org/dev.data-star.clojure/http-kit)
8 |
9 | [](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 |
--------------------------------------------------------------------------------