Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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