Skip to content

Commit d86d859

Browse files
authored
Clojure SDK: ADR changes and 2 new features (#961)
* Refactor: clojure-SDK Converting to the new ADR and some improvements - moved clj-kondo config to allows other lib to use the starfederation.datastar.clojure ns for their clj-kondo configs. - refactored the base SDK unit tests - removing superfluous newline char when terminating a sse event - using the bundle from the repo in dev examples and tests * Feature: clojure-SDK Brotli support as a library * Feature: clojure-SDK alternative http-kit api * Fix: clojure-SDK forgotten changelog info * Fix: forgotten malli schema for the new http-kit api. * Doc: Brotli README * Fix: errors in test snippets
1 parent 513ccd1 commit d86d859

File tree

84 files changed

+2302
-1280
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+2302
-1280
lines changed

examples/clojure/hello-world/src/main/example/core.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
(fn [sse]
4747
(d*/with-open-sse sse
4848
(dotimes [i msg-count]
49-
(d*/merge-fragment! sse (->frag i))
49+
(d*/patch-elements! sse (->frag i))
5050
(Thread/sleep d))))})))
5151

5252

sdk/clojure/CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Release notes for the Clojure SDK
22

3+
## 2025-06-22
4+
5+
### Changed
6+
7+
- The public API has seen it's main functions renamed following the new SDK ADR.
8+
Several functions have been renamed or removed:
9+
10+
| Old | new |
11+
| ------------------- | --------------------- |
12+
| `merge-fragment!` | `patch-elements!` |
13+
| `merge-fragments!` | `patch-elements-seq!` |
14+
| `remove-fragments!` | `remove-element!` |
15+
| `merge-signals!` | `patch-signals!` |
16+
| `remove-signals` | removed |
17+
18+
- All the examples and snippets have been updated following the ADR changes.
19+
20+
### Fixed
21+
22+
- A superflous newline character was send when marking the end of a SSE event
23+
- The clj-kondo config file for the SDK has been moved in a
24+
`clj-kondo.exports/starfederation.datastar.clojure/sdk` directory. This change
25+
allows for other projects to use
26+
`starfederation.datastar.clojure/XXXX/config.edn` for their clj-kondo config.
27+
28+
### Added
29+
30+
- There is a new http-kit API that allows a more natural ring response model when
31+
using SSE. With the current API the status and headers for a response are
32+
sent directly while `->sse-response` is running, the `on-open` callback runs
33+
just after. For instance that any middleware that would add headers after the
34+
execution of the `->sse-response` function won't work, the initial response
35+
being already sent.
36+
The new `starfederation.datastar.clojure.adapter.http-kit2`
37+
API changes this behavior. In this new api the initial response is not sent
38+
during `->sse-response`. Instead a middleware takes care of sending it and
39+
only then calls the `on-open` callback. If this middleware is the last to run
40+
on the return any addition to the response map will be taken into account.
41+
- A new library providing Brotli write profile has been added.
42+
343
## 2025-04-07
444

545
### Added

sdk/clojure/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Using the adapter you create ring responses in your handlers:
9393
(hk-gen/->sse-response request
9494
{hk-gen/on-open
9595
(fn [sse-gen]
96-
(d*/merge-fragment! sse-gen "<div>test</div>")
96+
(d*/patch-elements! sse-gen "<div>test</div>")
9797
(d*/close-sse! sse-gen))}))
9898

9999
```
@@ -118,9 +118,9 @@ it somewhere and use it later:
118118
(swap! !connections disj sse-gen))}))
119119

120120

121-
(defn broadcast-fragment! [fragment]
121+
(defn broadcast-elements! [elements]
122122
(doseq [c @!connections]
123-
(d*/merge-fragment! c fragment)))
123+
(d*/patch-elements! c elements)))
124124

125125
```
126126

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
(ns starfederation.datastar.clojure.adapter.http-kit2
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- as-channel
24+
"
25+
Replacement for [[hk-server/as-channel]] that doesn't deal with websockets
26+
and doen't call `on-open` itself.
27+
28+
`on-open` is meant to be called by either a middleware or an interceptor on the return.
29+
"
30+
[ring-req {:keys [on-close on-open init]}]
31+
32+
(when-let [ch (:async-channel ring-req)]
33+
34+
(when-let [f init] (f ch))
35+
(when-let [f on-close] (org.httpkit.server/on-close ch (partial f ch)))
36+
37+
{:body ch ::on-open on-open}))
38+
39+
40+
(defn ->sse-response
41+
"Make a Ring like response that will start a SSE stream.
42+
43+
The status code and the the SSE specific headers are not sent automatically.
44+
You need to use either [[start-responding-middleware]] or
45+
[[start-responding-interceptor]].
46+
47+
Note that the SSE connection stays opened util you close it.
48+
49+
General options:
50+
- `:status`: status for the HTTP response, defaults to 200.
51+
- `:headers`: ring headers map to add to the response.
52+
- [[on-open]]: mandatory callback called when the generator is ready to send.
53+
- [[on-close]]: callback called when the underlying Http-kit AsyncChannel is
54+
closed. It receives a second argument, the `:status-code` value we get from
55+
the closing AsyncChannel.
56+
- [[on-exception]]: callback called when sending a SSE event throws.
57+
- [[write-profile]]: write profile for the connection.
58+
Defaults to [[basic-profile]]
59+
60+
SDK provided write profiles:
61+
- [[basic-profile]]
62+
- [[buffered-writer-profile]]
63+
- [[gzip-profile]]
64+
- [[gzip-buffered-writer-profile]]
65+
66+
You can also take a look at the `starfederation.datastar.clojure.adapter.common`
67+
namespace if you want to write your own profiles.
68+
"
69+
[ring-request {:keys [status] :as opts}]
70+
{:pre [(ac/on-open opts)]}
71+
(let [on-open-cb (ac/on-open opts)
72+
on-close-cb (ac/on-close opts)
73+
future-send! (promise)
74+
future-gen (promise)]
75+
(assoc
76+
(as-channel ring-request
77+
{:on-open
78+
(fn [ch]
79+
(let [send! (impl/->send! ch opts)
80+
sse-gen (impl/->sse-gen ch send! opts)]
81+
(deliver future-gen sse-gen)
82+
(deliver future-send! send!)
83+
(on-open-cb sse-gen)))
84+
85+
:on-close
86+
(fn [_ status]
87+
(let [closing-res
88+
(ac/close-sse!
89+
#(when-let [send! (deref future-send! 0 nil)] (send!))
90+
#(when on-close-cb
91+
(on-close-cb (deref future-gen 0 nil) status)))]
92+
(if (instance? Exception closing-res)
93+
(throw closing-res)
94+
closing-res)))})
95+
:status (or status 200)
96+
:headers (ac/headers ring-request opts)
97+
::datastar-sse-response true)))
98+
99+
100+
(defn start-responding!
101+
"Function that takes a ring response map and sends HTTP status & headers using
102+
the [[AsyncChannel]] that should be in the body if the
103+
`::datastar-sse-response` key is present."
104+
[response]
105+
(if (::datastar-sse-response response)
106+
(let [{on-open ::on-open
107+
ch :body} response
108+
response (dissoc response :body ::on-open ::datastar-sse-response)]
109+
(hk-server/send! ch response false)
110+
(on-open ch))
111+
response))
112+
113+
114+
115+
(defn wrap-start-responding
116+
"Middleware necessary to use in conjunction with [[->sse-response]].
117+
118+
It will check if the response is a datastar-sse-response
119+
(created with [[->sse-response]]). In this case it will send the initial
120+
response containing headers and status code, then call `on-open`."
121+
[handler]
122+
(fn
123+
([req]
124+
(let [response (handler req)]
125+
(start-responding! response)
126+
response))
127+
([req respond _raise]
128+
(let [response (handler req)]
129+
(start-responding! response)
130+
(respond response)))))
131+
132+
133+
(def start-responding-middleware
134+
"Reitit middleware map for [[wrap-start-responding]]."
135+
{:name ::start-responding
136+
:wrap wrap-start-responding})
137+
138+
139+
(def start-responding-interceptor
140+
"
141+
Interceptor necessary to use in conjunction with [[->sse-response]].
142+
143+
In the `:leave` fn, it will check if the response is a datastar-sse-response
144+
(created with [[->sse-response]]). In this case it will send the initial
145+
response containing headers and status code, then call `on-open`."
146+
{:name ::start-responding
147+
:leave (fn [ctx]
148+
(let [response (:response ctx)]
149+
(start-responding! response)
150+
ctx))})
151+

sdk/clojure/bb.edn

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,22 @@
1414

1515
test:all (t/lazytest [:http-kit
1616
:ring-jetty
17-
:malli-schemas]
17+
:malli-schemas
18+
:sdk-brotli]
1819
[:test.paths/core-sdk
1920
:test.paths/malli-schemas
21+
:test.paths/brotli
2022
:test.paths/adapter-ring
2123
:test.paths/adapter-http-kit
2224
:test.paths/adapter-ring-jetty])
2325

2426
test:all-w (t/lazytest [:http-kit
2527
:ring-jetty
26-
:malli-schemas]
28+
:malli-schemas
29+
:sdk-brotli]
2730
[:test.paths/core-sdk
2831
:test.paths/malli-schemas
32+
:test.paths/brotli
2933
:test.paths/adapter-ring
3034
:test.paths/adapter-ring-jetty
3135
:test.paths/adapter-http-kit]

sdk/clojure/deps.edn

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{:paths []
22

3-
:deps {sdk/sdk {:local/root "./sdk"}
4-
io.github.paintparty/fireworks {:mvn/version "0.10.4"}
5-
com.taoensso/telemere {:mvn/version "1.0.0-RC3"}
6-
com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"}}
3+
:deps {sdk/sdk {:local/root "./sdk"}
4+
io.github.tonsky/clojure-plus {:mvn/version "1.6.1"}
5+
io.github.paintparty/fireworks {:mvn/version "0.10.4"}
6+
mvxcvi/puget {:mvn/version "1.3.4"}
7+
com.taoensso/telemere {:mvn/version "1.0.0-RC3"}}
78

89
:aliases
910
{:repl {:extra-paths ["src/dev"]
@@ -26,6 +27,7 @@
2627
:test {:extra-paths ["test-resources/"
2728
:test.paths/core-sdk
2829
:test.paths/malli-schemas
30+
:test.paths/brotli
2931
:test.paths/adapter-common
3032
:test.paths/adapter-http-kit
3133
:test.paths/adapter-ring
@@ -45,13 +47,17 @@
4547
:ring-jetty {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
4648
ring/ring-jetty-adapter {:mvn/version "1.14.1"}}}
4749

48-
:ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
49-
info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}}
50+
:ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
51+
info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}}
5052

5153
:malli-schemas {:extra-deps {sdk/malli {:local/root "./malli-schemas"}}}
54+
55+
:sdk-brotli {:extra-deps {sdk/brotli {:local/root "./sdk-brotli"}
56+
com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}}
5257

5358

5459
:test.paths/core-sdk ["src/test/core-sdk"]
60+
:test.paths/brotli ["src/test/brotli"]
5561
:test.paths/malli-schemas ["src/test/malli-schemas"]
5662
:test.paths/adapter-common ["src/test/adapter-common"]
5763
:test.paths/adapter-ring ["src/test/adapter-ring"]

sdk/clojure/doc/Write-profiles.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ When using the `->sse-response` function we can do:
4747
on-open
4848
(fn [sse]
4949
(d*/with-open-sse sse
50-
(d*/merge-fragment! sse "some big fragment")))}))
50+
(d*/patch-elements! sse "some big element")))}))
5151
```
5252

5353
This response will have the right `Content-Encoding` header and will compress

sdk/clojure/doc/maintainers-guide.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
In the whole Datastar project:
66

77
- `examples/clojure`
8+
- `site/static/code_snippets/*.clojuresnippet`
9+
- `site/static/going_deeper/*.clojuresnippet`
10+
- `site/static/how_tos/*.clojuresnippet`
811

912
In the SDK code proper `sdk/clojure`:
1013

@@ -50,9 +53,9 @@ In the SDK code proper `sdk/clojure`:
5053
- The Clojars account is managed by Ben Croker, the DNS verification is managed by Delaney.
5154
- The Clojars deploy token is also managed by Ben and added to this repo as a GH Actions Secret
5255
- Secret name: `CLOJARS_USERNAME`
53-
Value: *the clojars account username*
56+
Value: _the clojars account username_
5457
- Secret name: `CLOJARS_PASSWORD`
55-
Value: *the clojars deploy token*
58+
Value: _the clojars deploy token_
5659
- The libraries' versions are bumped in lockstep so that there is no confusion over which version of the common lib should be used with an adapter lib.
5760

5861
The Github Actions [CI workflow for clojure](../../.github/workflows/clojure-sdk.yml) will always run the tests and produce jar artifacts.

sdk/clojure/forms.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Divergence of behavior from beta11 to RC12 when it comes to forms
2+
3+
## The setup
4+
To test the forms behavior I made little page that lets me send the content of a
5+
text input to the server via a click on a button.
6+
7+
They are 3 parameter used in this test:
8+
- the form method (GET or POST)
9+
- whether the button has a type attribute of button or no such attribute
10+
- whether the `contentType` option is used in the Datastar action
11+
12+
This give me a HTML page like this:
13+
14+
```html
15+
<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<meta charset="UTF-8" />
19+
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js" ></script>
20+
</head>
21+
<body>
22+
<div>
23+
<form action="">
24+
<label for="input1">Enter text</label>
25+
<br />
26+
<input id="input1" type="text" name="input-1" data-bind-input-1 />
27+
<br />
28+
<br />
29+
30+
<!-- Section where the different button behaviors are tested -->
31+
<button data-on-click="@get('/endpoint')"> get - nct - nt</button> <br />
32+
<button type="button" data-on-click="@get('/endpoint')"> get - nct - t </button> <br />
33+
<button data-on-click="@get('/endpoint', {contentType: 'form'})"> get - ct - nt </button> <br />
34+
<button type="button" data-on-click="@get('/endpoint', {contentType: 'form'})" > get - ct - t </button> <br />
35+
36+
<br />
37+
38+
<button data-on-click="@post('/endpoint')"> post - nct - nt</button> <br />
39+
<button type="button" data-on-click="@post('/endpoint')"> post - nct - t </button> <br />
40+
<button data-on-click="@post('/endpoint', {contentType: 'form'})"> post - ct - nt </button> <br />
41+
<button type="button" data-on-click="@post('/endpoint', {contentType: 'form'})" > post - ct - t </button>
42+
<br />
43+
<!-- END -->
44+
45+
</form>
46+
<span id="form-result"></span>
47+
</div>
48+
</body>
49+
</html>
50+
```
51+
The idea is that when I click one button I observe if I correctly get the value
52+
from the input and where I get the value from.
53+
54+
> [!note]
55+
> Note that the case where I don't set the type attribute on the button while
56+
> not using the `contentType` option (regardless of the HTTP Method) is not a
57+
> valid use of D* with forms.
58+
59+
60+
| method | btn type="button" | contentType set | beta11 | RC12 |
61+
| ------ | ----------------- | --------------- | ------------------------------------------- | ------------------------ |
62+
| GET | no | yes | btn works as submit, expected HTML behavior | idem |
63+
| GET | no | no | values via signals in the query str | idem |
64+
| GET | yes | yes | values the query str (HTML behavior) | no value in query string |
65+
| GET | yes | no | values the query str (HTML behavior) | no value in query string |
66+
| POST | no | yes | btn works as submit, expected HTML behavior | idem |
67+
| POST | no | no | values via signals in the request's body | idem |
68+
| POST | yes | yes | values via the request body (HTML behavior) | idem |
69+
| POST | yes | no | values via the request body (HTML behavior) | idem |
70+
71+
72+

0 commit comments

Comments
 (0)