Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/clojure/hello-world/src/main/example/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
(fn [sse]
(d*/with-open-sse sse
(dotimes [i msg-count]
(d*/merge-fragment! sse (->frag i))
(d*/patch-elements! sse (->frag i))
(Thread/sleep d))))})))


Expand Down
40 changes: 40 additions & 0 deletions sdk/clojure/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Release notes for the Clojure SDK

## 2025-06-22

### Changed

- The public API has seen it's main functions renamed following the new SDK ADR.
Several functions have been renamed or removed:

| Old | new |
| ------------------- | --------------------- |
| `merge-fragment!` | `patch-elements!` |
| `merge-fragments!` | `patch-elements-seq!` |
| `remove-fragments!` | `remove-element!` |
| `merge-signals!` | `patch-signals!` |
| `remove-signals` | removed |

- All the examples and snippets have been updated following the ADR changes.

### Fixed

- A superflous newline character was send when marking the end of a SSE event
- The clj-kondo config file for the SDK has been moved in a
`clj-kondo.exports/starfederation.datastar.clojure/sdk` directory. This change
allows for other projects to use
`starfederation.datastar.clojure/XXXX/config.edn` for their clj-kondo config.

### Added

- There is a new http-kit API that allows a more natural ring response model when
using SSE. With the current API the status and headers for a response are
sent directly while `->sse-response` is running, the `on-open` callback runs
just after. For instance that any middleware that would add headers after the
execution of the `->sse-response` function won't work, the initial response
being already sent.
The new `starfederation.datastar.clojure.adapter.http-kit2`
API changes this behavior. In this new api the initial response is not sent
during `->sse-response`. Instead a middleware takes care of sending it and
only then calls the `on-open` callback. If this middleware is the last to run
on the return any addition to the response map will be taken into account.
- A new library providing Brotli write profile has been added.

## 2025-04-07

### Added
Expand Down
6 changes: 3 additions & 3 deletions sdk/clojure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Using the adapter you create ring responses in your handlers:
(hk-gen/->sse-response request
{hk-gen/on-open
(fn [sse-gen]
(d*/merge-fragment! sse-gen "<div>test</div>")
(d*/patch-elements! sse-gen "<div>test</div>")
(d*/close-sse! sse-gen))}))

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


(defn broadcast-fragment! [fragment]
(defn broadcast-elements! [elements]
(doseq [c @!connections]
(d*/merge-fragment! c fragment)))
(d*/patch-elements! c elements)))

```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
(ns starfederation.datastar.clojure.adapter.http-kit2
(:require
[org.httpkit.server :as hk-server]
[starfederation.datastar.clojure.adapter.common :as ac]
[starfederation.datastar.clojure.adapter.http-kit.impl :as impl]
[starfederation.datastar.clojure.utils :refer [def-clone]]))


(def-clone on-open ac/on-open)
(def-clone on-close ac/on-close)
(def-clone on-exception ac/on-exception)
(def-clone default-on-exception ac/default-on-exception)


(def-clone write-profile ac/write-profile)

(def-clone basic-profile impl/basic-profile)
(def-clone buffered-writer-profile ac/buffered-writer-profile)
(def-clone gzip-profile ac/gzip-profile)
(def-clone gzip-buffered-writer-profile ac/gzip-buffered-writer-profile)


(defn- as-channel
"
Replacement for [[hk-server/as-channel]] that doesn't deal with websockets
and doen't call `on-open` itself.

`on-open` is meant to be called by either a middleware or an interceptor on the return.
"
[ring-req {:keys [on-close on-open init]}]

(when-let [ch (:async-channel ring-req)]

(when-let [f init] (f ch))
(when-let [f on-close] (org.httpkit.server/on-close ch (partial f ch)))

{:body ch ::on-open on-open}))


(defn ->sse-response
"Make a Ring like response that will start a SSE stream.

The status code and the the SSE specific headers are not sent automatically.
You need to use either [[start-responding-middleware]] or
[[start-responding-interceptor]].

Note that the SSE connection stays opened util you close it.

General options:
- `:status`: status for the HTTP response, defaults to 200.
- `:headers`: ring headers map to add to the response.
- [[on-open]]: mandatory callback called when the generator is ready to send.
- [[on-close]]: callback called when the underlying Http-kit AsyncChannel is
closed. It receives a second argument, the `:status-code` value we get from
the closing AsyncChannel.
- [[on-exception]]: callback called when sending a SSE event throws.
- [[write-profile]]: write profile for the connection.
Defaults to [[basic-profile]]

SDK provided write profiles:
- [[basic-profile]]
- [[buffered-writer-profile]]
- [[gzip-profile]]
- [[gzip-buffered-writer-profile]]

You can also take a look at the `starfederation.datastar.clojure.adapter.common`
namespace if you want to write your own profiles.
"
[ring-request {:keys [status] :as opts}]
{:pre [(ac/on-open opts)]}
(let [on-open-cb (ac/on-open opts)
on-close-cb (ac/on-close opts)
future-send! (promise)
future-gen (promise)]
(assoc
(as-channel ring-request
{:on-open
(fn [ch]
(let [send! (impl/->send! ch opts)
sse-gen (impl/->sse-gen ch send! opts)]
(deliver future-gen sse-gen)
(deliver future-send! send!)
(on-open-cb sse-gen)))

:on-close
(fn [_ status]
(let [closing-res
(ac/close-sse!
#(when-let [send! (deref future-send! 0 nil)] (send!))
#(when on-close-cb
(on-close-cb (deref future-gen 0 nil) status)))]
(if (instance? Exception closing-res)
(throw closing-res)
closing-res)))})
:status (or status 200)
:headers (ac/headers ring-request opts)
::datastar-sse-response true)))


(defn start-responding!
"Function that takes a ring response map and sends HTTP status & headers using
the [[AsyncChannel]] that should be in the body if the
`::datastar-sse-response` key is present."
[response]
(if (::datastar-sse-response response)
(let [{on-open ::on-open
ch :body} response
response (dissoc response :body ::on-open ::datastar-sse-response)]
(hk-server/send! ch response false)
(on-open ch))
response))



(defn wrap-start-responding
"Middleware necessary to use in conjunction with [[->sse-response]].

It will check if the response is a datastar-sse-response
(created with [[->sse-response]]). In this case it will send the initial
response containing headers and status code, then call `on-open`."
[handler]
(fn
([req]
(let [response (handler req)]
(start-responding! response)
response))
([req respond _raise]
(let [response (handler req)]
(start-responding! response)
(respond response)))))


(def start-responding-middleware
"Reitit middleware map for [[wrap-start-responding]]."
{:name ::start-responding
:wrap wrap-start-responding})


(def start-responding-interceptor
"
Interceptor necessary to use in conjunction with [[->sse-response]].

In the `:leave` fn, it will check if the response is a datastar-sse-response
(created with [[->sse-response]]). In this case it will send the initial
response containing headers and status code, then call `on-open`."
{:name ::start-responding
:leave (fn [ctx]
(let [response (:response ctx)]
(start-responding! response)
ctx))})

8 changes: 6 additions & 2 deletions sdk/clojure/bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@

test:all (t/lazytest [:http-kit
:ring-jetty
:malli-schemas]
:malli-schemas
:sdk-brotli]
[:test.paths/core-sdk
:test.paths/malli-schemas
:test.paths/brotli
:test.paths/adapter-ring
:test.paths/adapter-http-kit
:test.paths/adapter-ring-jetty])

test:all-w (t/lazytest [:http-kit
:ring-jetty
:malli-schemas]
:malli-schemas
:sdk-brotli]
[:test.paths/core-sdk
:test.paths/malli-schemas
:test.paths/brotli
:test.paths/adapter-ring
:test.paths/adapter-ring-jetty
:test.paths/adapter-http-kit]
Expand Down
18 changes: 12 additions & 6 deletions sdk/clojure/deps.edn
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{:paths []

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

:aliases
{:repl {:extra-paths ["src/dev"]
Expand All @@ -26,6 +27,7 @@
:test {:extra-paths ["test-resources/"
:test.paths/core-sdk
:test.paths/malli-schemas
:test.paths/brotli
:test.paths/adapter-common
:test.paths/adapter-http-kit
:test.paths/adapter-ring
Expand All @@ -45,13 +47,17 @@
:ring-jetty {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
ring/ring-jetty-adapter {:mvn/version "1.14.1"}}}

:ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}}
:ring-rj9a {:extra-deps {sdk/adapter-ring {:local/root "./adapter-ring"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.36.1"}}}

:malli-schemas {:extra-deps {sdk/malli {:local/root "./malli-schemas"}}}

:sdk-brotli {:extra-deps {sdk/brotli {:local/root "./sdk-brotli"}
com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}}


:test.paths/core-sdk ["src/test/core-sdk"]
:test.paths/brotli ["src/test/brotli"]
:test.paths/malli-schemas ["src/test/malli-schemas"]
:test.paths/adapter-common ["src/test/adapter-common"]
:test.paths/adapter-ring ["src/test/adapter-ring"]
Expand Down
2 changes: 1 addition & 1 deletion sdk/clojure/doc/Write-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ When using the `->sse-response` function we can do:
on-open
(fn [sse]
(d*/with-open-sse sse
(d*/merge-fragment! sse "some big fragment")))}))
(d*/patch-elements! sse "some big element")))}))
```

This response will have the right `Content-Encoding` header and will compress
Expand Down
7 changes: 5 additions & 2 deletions sdk/clojure/doc/maintainers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
In the whole Datastar project:

- `examples/clojure`
- `site/static/code_snippets/*.clojuresnippet`
- `site/static/going_deeper/*.clojuresnippet`
- `site/static/how_tos/*.clojuresnippet`

In the SDK code proper `sdk/clojure`:

Expand Down Expand Up @@ -50,9 +53,9 @@ In the SDK code proper `sdk/clojure`:
- The Clojars account is managed by Ben Croker, the DNS verification is managed by Delaney.
- The Clojars deploy token is also managed by Ben and added to this repo as a GH Actions Secret
- Secret name: `CLOJARS_USERNAME`
Value: *the clojars account username*
Value: _the clojars account username_
- Secret name: `CLOJARS_PASSWORD`
Value: *the clojars deploy token*
Value: _the clojars deploy token_
- 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.

The Github Actions [CI workflow for clojure](../../.github/workflows/clojure-sdk.yml) will always run the tests and produce jar artifacts.
Expand Down
72 changes: 72 additions & 0 deletions sdk/clojure/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Divergence of behavior from beta11 to RC12 when it comes to forms

## The setup
To test the forms behavior I made little page that lets me send the content of a
text input to the server via a click on a button.

They are 3 parameter used in this test:
- the form method (GET or POST)
- whether the button has a type attribute of button or no such attribute
- whether the `contentType` option is used in the Datastar action

This give me a HTML page like this:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js" ></script>
</head>
<body>
<div>
<form action="">
<label for="input1">Enter text</label>
<br />
<input id="input1" type="text" name="input-1" data-bind-input-1 />
<br />
<br />

<!-- Section where the different button behaviors are tested -->
<button data-on-click="@get('/endpoint')"> get - nct - nt</button> <br />
<button type="button" data-on-click="@get('/endpoint')"> get - nct - t </button> <br />
<button data-on-click="@get('/endpoint', {contentType: 'form'})"> get - ct - nt </button> <br />
<button type="button" data-on-click="@get('/endpoint', {contentType: 'form'})" > get - ct - t </button> <br />

<br />

<button data-on-click="@post('/endpoint')"> post - nct - nt</button> <br />
<button type="button" data-on-click="@post('/endpoint')"> post - nct - t </button> <br />
<button data-on-click="@post('/endpoint', {contentType: 'form'})"> post - ct - nt </button> <br />
<button type="button" data-on-click="@post('/endpoint', {contentType: 'form'})" > post - ct - t </button>
<br />
<!-- END -->

</form>
<span id="form-result"></span>
</div>
</body>
</html>
```
The idea is that when I click one button I observe if I correctly get the value
from the input and where I get the value from.

> [!note]
> Note that the case where I don't set the type attribute on the button while
> not using the `contentType` option (regardless of the HTTP Method) is not a
> valid use of D* with forms.


| method | btn type="button" | contentType set | beta11 | RC12 |
| ------ | ----------------- | --------------- | ------------------------------------------- | ------------------------ |
| GET | no | yes | btn works as submit, expected HTML behavior | idem |
| GET | no | no | values via signals in the query str | idem |
| GET | yes | yes | values the query str (HTML behavior) | no value in query string |
| GET | yes | no | values the query str (HTML behavior) | no value in query string |
| POST | no | yes | btn works as submit, expected HTML behavior | idem |
| POST | no | no | values via signals in the request's body | idem |
| POST | yes | yes | values via the request body (HTML behavior) | idem |
| POST | yes | no | values via the request body (HTML behavior) | idem |



Loading
Loading