From 4f4d9e4ad104b279e6e9b1b1b1f3aac313ef4a12 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Sat, 21 Jun 2025 21:59:06 +0200 Subject: [PATCH 1/7] 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 --- .../hello-world/src/main/example/core.clj | 2 +- sdk/clojure/CHANGELOG.md | 24 + sdk/clojure/README.md | 6 +- sdk/clojure/deps.edn | 2 + sdk/clojure/doc/Write-profiles.md | 2 +- sdk/clojure/doc/maintainers-guide.md | 7 +- sdk/clojure/forms.md | 72 +++ .../datastar/clojure/api/common_schemas.clj | 48 +- .../datastar/clojure/api/elements_schemas.clj | 14 + .../clojure/api/fragments_schemas.clj | 17 - .../datastar/clojure/api/scripts_schemas.clj | 4 +- .../datastar/clojure/api/signals_schemas.clj | 7 +- .../datastar/clojure/api_schemas.clj | 26 +- .../datastar/clojure/sdk_test/core.clj | 41 +- .../sdk}/config.edn | 0 .../starfederation/datastar/clojure/api.clj | 232 ++++---- .../datastar/clojure/api/common.clj | 21 +- .../datastar/clojure/api/elements.clj | 150 +++++ .../datastar/clojure/api/fragments.clj | 181 ------ .../datastar/clojure/api/scripts.clj | 126 ++--- .../datastar/clojure/api/signals.clj | 88 +-- .../datastar/clojure/api/sse.clj | 2 +- .../datastar/clojure/consts.clj | 6 +- .../datastar/clojure/protocols.clj | 24 +- .../starfederation/datastar/clojure/utils.clj | 3 +- .../src/dev/examples/animation_gzip.clj | 38 +- .../dev/examples/animation_gzip/animation.clj | 6 +- .../dev/examples/animation_gzip/handlers.clj | 159 +++--- .../dev/examples/animation_gzip/rendering.clj | 18 +- .../src/dev/examples/animation_gzip/state.clj | 2 + .../src/dev/examples/animation_gzip/style.css | 12 +- .../src/dev/examples/broadcast_ring.clj | 2 +- sdk/clojure/src/dev/examples/common.clj | 15 +- sdk/clojure/src/dev/examples/data_dsl.clj | 26 +- sdk/clojure/src/dev/examples/faulty_event.clj | 2 +- .../src/dev/examples/form_behavior.clj | 117 ---- .../src/dev/examples/form_behavior/core.clj | 141 +++++ .../src/dev/examples/form_behavior/style.css | 4 + sdk/clojure/src/dev/examples/forms/common.clj | 14 + sdk/clojure/src/dev/examples/forms/core.clj | 41 ++ .../src/dev/examples/forms/datastar.clj | 91 +++ sdk/clojure/src/dev/examples/forms/html.clj | 63 +++ .../src/dev/examples/jetty_disconnect.clj | 2 +- sdk/clojure/src/dev/examples/malli.clj | 12 +- .../src/dev/examples/multiple_fragments.clj | 9 +- sdk/clojure/src/dev/examples/redirect.clj | 5 +- .../src/dev/examples/remove_fragments.clj | 21 +- sdk/clojure/src/dev/examples/scripts.clj | 3 +- sdk/clojure/src/dev/examples/snippets.clj | 16 +- .../src/dev/examples/snippets/load_more.clj | 8 +- .../src/dev/examples/snippets/polling1.clj | 2 +- .../src/dev/examples/snippets/polling2.clj | 2 +- .../src/dev/examples/snippets/redirect1.clj | 2 +- .../src/dev/examples/snippets/redirect2.clj | 4 +- .../src/dev/examples/snippets/redirect3.clj | 2 +- sdk/clojure/src/dev/examples/tiny_gzip.clj | 6 +- sdk/clojure/src/dev/examples/utils.clj | 21 +- sdk/clojure/src/dev/user.clj | 7 +- .../src/test/adapter-common/test/common.clj | 19 +- .../adapter-common/test/examples/common.clj | 17 +- .../adapter-common/test/examples/counter.clj | 2 +- .../adapter-common/test/examples/form.clj | 24 +- .../src/test/adapter-common/test/utils.clj | 2 +- .../clojure/adapter/http_kit/impl_test.clj | 10 +- .../test/examples/http_kit_handler.clj | 3 +- .../clojure/adapter/ring/impl_test.clj | 11 +- .../test/examples/ring_handler.clj | 3 +- .../datastar/clojure/adapter/common_test.clj | 2 +- .../datastar/clojure/api_test.clj | 524 ++++++++---------- .../datastar/clojure}/api_schemas_test.clj | 29 +- 70 files changed, 1423 insertions(+), 1201 deletions(-) create mode 100644 sdk/clojure/forms.md create mode 100644 sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj delete mode 100644 sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/fragments_schemas.clj rename sdk/clojure/sdk/resources/clj-kondo.exports/{starfederation.datastar/clojure => starfederation.datastar.clojure/sdk}/config.edn (100%) create mode 100644 sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj delete mode 100644 sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/fragments.clj delete mode 100644 sdk/clojure/src/dev/examples/form_behavior.clj create mode 100644 sdk/clojure/src/dev/examples/form_behavior/core.clj create mode 100644 sdk/clojure/src/dev/examples/form_behavior/style.css create mode 100644 sdk/clojure/src/dev/examples/forms/common.clj create mode 100644 sdk/clojure/src/dev/examples/forms/core.clj create mode 100644 sdk/clojure/src/dev/examples/forms/datastar.clj create mode 100644 sdk/clojure/src/dev/examples/forms/html.clj rename sdk/clojure/src/test/malli-schemas/{test => starfederation/datastar/clojure}/api_schemas_test.clj (60%) diff --git a/examples/clojure/hello-world/src/main/example/core.clj b/examples/clojure/hello-world/src/main/example/core.clj index 2e78e0f21..04bcf9311 100644 --- a/examples/clojure/hello-world/src/main/example/core.clj +++ b/examples/clojure/hello-world/src/main/example/core.clj @@ -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))))}))) diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md index a9bc97466..c6ba9eb35 100644 --- a/sdk/clojure/CHANGELOG.md +++ b/sdk/clojure/CHANGELOG.md @@ -1,5 +1,29 @@ # 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 + +- 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. + ## 2025-04-07 ### Added diff --git a/sdk/clojure/README.md b/sdk/clojure/README.md index e20f3b229..5a4d6105d 100644 --- a/sdk/clojure/README.md +++ b/sdk/clojure/README.md @@ -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 "
test
") + (d*/patch-elements! sse-gen "
test
") (d*/close-sse! sse-gen))})) ``` @@ -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))) ``` diff --git a/sdk/clojure/deps.edn b/sdk/clojure/deps.edn index 54f804e5f..9536fbf18 100644 --- a/sdk/clojure/deps.edn +++ b/sdk/clojure/deps.edn @@ -1,7 +1,9 @@ {:paths [] :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"} com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"}} diff --git a/sdk/clojure/doc/Write-profiles.md b/sdk/clojure/doc/Write-profiles.md index 315527c78..51b8415b5 100644 --- a/sdk/clojure/doc/Write-profiles.md +++ b/sdk/clojure/doc/Write-profiles.md @@ -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 diff --git a/sdk/clojure/doc/maintainers-guide.md b/sdk/clojure/doc/maintainers-guide.md index 49f8bf027..4d79598e7 100644 --- a/sdk/clojure/doc/maintainers-guide.md +++ b/sdk/clojure/doc/maintainers-guide.md @@ -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`: @@ -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. diff --git a/sdk/clojure/forms.md b/sdk/clojure/forms.md new file mode 100644 index 000000000..769392f63 --- /dev/null +++ b/sdk/clojure/forms.md @@ -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 + + + + + + + +
+
+ +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ + +
+ +
+ + +``` +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 | + + + diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj index 5cbfda22a..fa4287808 100644 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/common_schemas.clj @@ -12,11 +12,8 @@ (def event-type-schema [:enum - consts/event-type-merge-fragments - consts/event-type-remove-fragments - consts/event-type-merge-signals - consts/event-type-remove-signals - consts/event-type-execute-script]) + consts/event-type-patch-elements + consts/event-type-patch-signals]) (def data-lines-schema [:seqable :string]) @@ -32,50 +29,45 @@ (m/validate sse-options-schema {common/id 1})) ;; ----------------------------------------------------------------------------- -(def fragment-schema :string) -(def fragments-schema [:seqable :string]) +(def elements-schema :string) +(def elements-seq-schema [:seqable :string]) -(def merge-modes-schema +(def patch-modes-schema [:enum - consts/fragment-merge-mode-morph - consts/fragment-merge-mode-inner - consts/fragment-merge-mode-outer - consts/fragment-merge-mode-prepend - consts/fragment-merge-mode-append - consts/fragment-merge-mode-before - consts/fragment-merge-mode-after - consts/fragment-merge-mode-upsert-attributes]) + consts/element-patch-mode-outer + consts/element-patch-mode-inner + consts/element-patch-mode-replace + consts/element-patch-mode-prepend + consts/element-patch-mode-append + consts/element-patch-mode-before + consts/element-patch-mode-after + consts/element-patch-mode-remove]) (comment - (m/validate merge-modes-schema consts/fragment-merge-mode-after) - (m/validate merge-modes-schema "toto")) + (m/validate patch-modes-schema consts/element-patch-mode-after) + (m/validate patch-modes-schema "toto")) -(def merge-fragment-options-schemas +(def patch-element-options-schemas (mu/merge sse-options-schema (mu/optional-keys [:map [common/selector :string] - [common/merge-mode merge-modes-schema] + [common/patch-mode patch-modes-schema] [common/use-view-transition :boolean]]))) ;; ----------------------------------------------------------------------------- (def selector-schema :string) -(def remove-fragments-options-schemas - (mu/merge - sse-options-schema - (mu/optional-keys - [:map - [common/use-view-transition :boolean]]))) +(def remove-element-options-schemas patch-element-options-schemas) ;; ----------------------------------------------------------------------------- (def signals-schema :string) -(def merge-signals-options-schemas +(def patch-signals-options-schemas (mu/merge sse-options-schema (mu/optional-keys @@ -86,8 +78,6 @@ ;; ----------------------------------------------------------------------------- (def signal-paths-schema [:seqable :string]) -(def remove-signals-options-schemas sse-options-schema) - ;; ----------------------------------------------------------------------------- (def script-content-schema :string) diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj new file mode 100644 index 000000000..f24253fde --- /dev/null +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/elements_schemas.clj @@ -0,0 +1,14 @@ +(ns starfederation.datastar.clojure.api.elements-schemas + (:require + [malli.core :as m] + [starfederation.datastar.clojure.api.common-schemas :as cs] + [starfederation.datastar.clojure.api.elements])) + + +(m/=> starfederation.datastar.clojure.api.elements/->patch-elements + [:-> cs/elements-schema cs/patch-element-options-schemas cs/data-lines-schema]) + + +(m/=> starfederation.datastar.clojure.api.elements/->patch-elements-seq + [:-> cs/elements-seq-schema cs/patch-element-options-schemas cs/data-lines-schema]) + diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/fragments_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/fragments_schemas.clj deleted file mode 100644 index 3e980712d..000000000 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/fragments_schemas.clj +++ /dev/null @@ -1,17 +0,0 @@ -(ns starfederation.datastar.clojure.api.fragments-schemas - (:require - [malli.core :as m] - [starfederation.datastar.clojure.api.common-schemas :as cs] - [starfederation.datastar.clojure.api.fragments])) - - -(m/=> starfederation.datastar.clojure.api.fragments/->merge-fragment - [:-> cs/fragment-schema cs/merge-fragment-options-schemas cs/data-lines-schema]) - - -(m/=> starfederation.datastar.clojure.api.fragments/->merge-fragments - [:-> cs/fragments-schema cs/merge-fragment-options-schemas cs/data-lines-schema]) - - -(m/=> starfederation.datastar.clojure.api.fragments/->remove-fragment - [:-> cs/selector-schema cs/remove-fragments-options-schemas cs/data-lines-schema]) diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj index cf986e9f2..baabbec6a 100644 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/scripts_schemas.clj @@ -4,6 +4,6 @@ [starfederation.datastar.clojure.api.common-schemas :as cs] [starfederation.datastar.clojure.api.scripts])) -(m/=> starfederation.datastar.clojure.api.scripts/->script - [:-> cs/script-content-schema cs/execute-script-options-schemas cs/data-lines-schema]) +(m/=> starfederation.datastar.clojure.api.scripts/->script-tag + [:-> cs/script-content-schema cs/execute-script-options-schemas :string]) diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj index 40047105f..44635b4dc 100644 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api/signals_schemas.clj @@ -5,9 +5,6 @@ [starfederation.datastar.clojure.api.signals])) -(m/=> starfederation.datastar.clojure.api.signals/->merge-signals - [:-> cs/signals-schema cs/merge-signals-options-schemas cs/data-lines-schema]) +(m/=> starfederation.datastar.clojure.api.signals/->patch-signals + [:-> cs/signals-schema cs/patch-signals-options-schemas cs/data-lines-schema]) - -(m/=> starfederation.datastar.clojure.api.signals/->remove-signals - [:-> cs/signal-paths-schema cs/data-lines-schema]) diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj index 73e97049e..0c4bb7d92 100644 --- a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/api_schemas.clj @@ -9,34 +9,28 @@ [:-> cs/sse-gen-schema :any]) -(m/=> starfederation.datastar.clojure.api/merge-fragment! +(m/=> starfederation.datastar.clojure.api/patch-elements! [:function - [:-> cs/sse-gen-schema cs/fragment-schema :any] - [:-> cs/sse-gen-schema cs/fragment-schema cs/merge-fragment-options-schemas :any]]) + [:-> cs/sse-gen-schema cs/elements-schema :any] + [:-> cs/sse-gen-schema cs/elements-schema cs/patch-element-options-schemas :any]]) -(m/=> starfederation.datastar.clojure.api/merge-fragments! +(m/=> starfederation.datastar.clojure.api/patch-elements-seq! [:function - [:-> cs/sse-gen-schema cs/fragments-schema :any] - [:-> cs/sse-gen-schema cs/fragments-schema cs/merge-fragment-options-schemas :any]]) + [:-> cs/sse-gen-schema cs/elements-seq-schema :any] + [:-> cs/sse-gen-schema cs/elements-seq-schema cs/patch-element-options-schemas :any]]) -(m/=> starfederation.datastar.clojure.api/remove-fragment! +(m/=> starfederation.datastar.clojure.api/remove-element! [:function [:-> cs/sse-gen-schema cs/selector-schema :any] - [:-> cs/sse-gen-schema cs/selector-schema cs/remove-fragments-options-schemas :any]]) + [:-> cs/sse-gen-schema cs/selector-schema cs/remove-element-options-schemas :any]]) -(m/=> starfederation.datastar.clojure.api/merge-signals! +(m/=> starfederation.datastar.clojure.api/patch-signals! [:function [:-> cs/sse-gen-schema cs/signals-schema :any] - [:-> cs/sse-gen-schema cs/signals-schema cs/merge-signals-options-schemas :any]]) - - -(m/=> starfederation.datastar.clojure.api/remove-signals! - [:function - [:-> cs/sse-gen-schema cs/signal-paths-schema :any] - [:-> cs/sse-gen-schema cs/signal-paths-schema cs/remove-signals-options-schemas :any]]) + [:-> cs/sse-gen-schema cs/signals-schema cs/patch-signals-options-schemas :any]]) (m/=> starfederation.datastar.clojure.api/execute-script! diff --git a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj b/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj index 53e9fab9e..f3aedc269 100644 --- a/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj +++ b/sdk/clojure/sdk-tests/src/main/starfederation/datastar/clojure/sdk_test/core.clj @@ -36,47 +36,52 @@ (def str->datastar-opt - {"eventId" d*/id + ;; SSE + {"eventId" d*/id "retryDuration" d*/retry-duration - "selector" d*/selector - "mergeMode" d*/merge-mode + ;; patch elements + "selector" d*/selector + "patchMode" d*/patch-mode "useViewTransition" d*/use-view-transition + ;; patch signals "onlyIfMissing" d*/only-if-missing - "autoRemove" d*/auto-remove + ;; execute script + "autoRemove" d*/auto-remove "attributes" d*/attributes}) (def options (set (keys str->datastar-opt))) ;; ----------------------------------------------------------------------------- -;; Testing code: We want to send back event received as datastar signal values +;; Testing code: We want to send back the event that we received as datastar +;; signal values in the HTTP request ;; ----------------------------------------------------------------------------- -(defn merge-fragments! [sse event] - (let [frags (get event "fragments") +(defn patch-elements! [sse event] + (let [elements (get event "elements") opts (-> event (select-keys options) (set/rename-keys str->datastar-opt))] - (d*/merge-fragment! sse frags opts))) + (d*/patch-elements! sse elements opts))) -(defn remove-fragments! [sse event] +(defn remove-element! [sse event] (let [selector (get event "selector") opts (-> event (select-keys options) (set/rename-keys str->datastar-opt))] - (d*/remove-fragment! sse selector opts))) + (d*/remove-element! sse selector opts))) -(defn merge-signals! [sse event] +(defn patch-signals! [sse event] (let [signals (-> (get event "signals") (->> (into (sorted-map))) ;; for the purpose of the test, keys need to be ordered (charred/write-json-str)) opts (-> event (select-keys options) (set/rename-keys str->datastar-opt))] - (d*/merge-signals! sse signals opts))) + (d*/patch-signals! sse signals opts))) (defn remove-signals! [sse event] @@ -84,7 +89,7 @@ opts (-> event (select-keys options) (set/rename-keys str->datastar-opt))] - (d*/remove-signals! sse paths opts))) + (d*/patch-signals! sse paths opts))) (defn execute-script! [sse event] @@ -98,11 +103,11 @@ (def dispatch - {"mergeFragments" merge-fragments! - "removeFragments" remove-fragments! - "mergeSignals" merge-signals! - "removeSignals" remove-signals! - "executeScript" execute-script!}) + {"patchElements" patch-elements! + "removeElements" remove-element! + "patchSignals" patch-signals! + "removeSignals" remove-signals! + "executeScript" execute-script!}) (defn send-event-back! [sse event] diff --git a/sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar/clojure/config.edn b/sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn similarity index 100% rename from sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar/clojure/config.edn rename to sdk/clojure/sdk/resources/clj-kondo.exports/starfederation.datastar.clojure/sdk/config.edn diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj index 545a45735..6c7427a91 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api.clj @@ -2,18 +2,18 @@ " Public api for the Datastar SDK. -The api consists of 5 main functions that operate on SSE generators, see: -- [[merge-fragment!]] -- [[remove-fragment!]] -- [[merge-signals!]] -- [[remove-signals!]] +The main api consists several functions that operate on SSE generators, see: +- [[patch-elements!]] +- [[patch-elements-seq!]] +- [[remove-element!]] +- [[patch-signals!]] - [[execute-script!]] These function take options map whose keys are: - [[id]] - [[retry-duration]] - [[selector]] -- [[merge-mode]] +- [[patch-mode]] - [[use-view-transition]] - [[only-if-missing]] - [[auto-remove]] @@ -35,13 +35,13 @@ Some scripts are provided: - [[console-error!]] - [[redirect!]]" (:require - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.fragments :as fragments] - [starfederation.datastar.clojure.api.signals :as signals] - [starfederation.datastar.clojure.api.scripts :as scripts] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.protocols :as p] - [starfederation.datastar.clojure.utils :as u])) + [starfederation.datastar.clojure.api.common :as common] + [starfederation.datastar.clojure.api.elements :as elements] + [starfederation.datastar.clojure.api.signals :as signals] + [starfederation.datastar.clojure.api.scripts :as scripts] + [starfederation.datastar.clojure.consts :as consts] + [starfederation.datastar.clojure.protocols :as p] + [starfederation.datastar.clojure.utils :as u])) ;; ----------------------------------------------------------------------------- ;; SSE generator management @@ -54,8 +54,8 @@ Some scripts are provided: Ex: ```clojure (lock-sse! my-sse-gen - (merge-fragment! sse frags) - (merge-signals! sse signals)) + (patch-elements! sse frags) + (patch-signals! sse signals)) ``` " [sse-gen & body] @@ -66,8 +66,8 @@ Some scripts are provided: (macroexpand-1 (macroexpand-1 '(lock-sse! my-sse-gen - (merge-fragment! sse frags) - (merge-signals! sse signals))))) + (patch-elements! sse frags) + (patch-signals! sse signals))))) (defn close-sse! @@ -88,8 +88,8 @@ Some scripts are provided: Ex: ``` (with-open-sse sse-gen - (d*/merge-fragment! sse-gen frag1) - (d*/merge-signals! sse-gen signals)) + (d*/patch-elements! sse-gen frag1) + (d*/patch-signals! sse-gen signals)) ``` " [sse-gen & body] @@ -127,35 +127,35 @@ Some scripts are provided: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry" common/retry-duration) -;; Merge fragment opts +;; patch element opts (def selector - "[[merge-fragment!]] option, string: + "[[patch-elements!]] & [[patch-elements-seq!]] option, string: - The CSS selector to use to insert the fragments. If not + The CSS selector to use to insert the elements. If not provided or empty, Datastar will default to using the id attribute of the - fragment." + element." common/selector) -(def merge-mode - "[[merge-fragment!]] option, string: +(def patch-mode + "[[patch-elements!]] & [[patch-elements-seq!]] option, string: - The mode to use when merging fragments into the DOM. + The mode to use when merging elements into the DOM. If not provided the Datastar client side will default to morph. The set of valid values is: - - [[mm-morph]] - - [[mm-inner]] - - [[mm-outer]] - - [[mm-prepend]] - - [[mm-append]] - - [[mm-before]] - - [[mm-after]] - - [[mm-upsert-attributes]] + - [[pm-outer]] default + - [[pm-inner]] + - [[pm-remove]] + - [[pm-prepend]] + - [[pm-append]] + - [[pm-before]] + - [[pm-after]] + - [[pm-replace]] " - common/merge-mode) + common/patch-mode) (def use-view-transition - "[[merge-fragment!]] / [[remove-fragment!] option, boolean: + "[[patch-elements!]] / [[remove-element!] option, boolean: Whether to use view transitions, if not provided the Datastar client side will default to false." @@ -163,11 +163,11 @@ Some scripts are provided: ;;Signals opts (def only-if-missing - "[[merge-signals!]] option, boolean: + "[[patch-signals!]] option, boolean: - Whether to merge the signal only if it does not already + Whether to patch the signal only if it does not already exist. If not provided, the Datastar client side will default to false, which - will cause the data to be merged into the signals." + will cause the data to be patchd into the signals." common/only-if-missing) ;; Script opts @@ -181,89 +181,89 @@ Some scripts are provided: (def attributes "[[execute-script!]] option, map: - A map of attributes to add to the script element, - if not provided the Datastar client side will default to - `{:type \"module\"}`." + A map of attributes to add to the script element." common/attributes) ;; ----------------------------------------------------------------------------- ;; Data-star base api ;; ----------------------------------------------------------------------------- -(def mm-morph - "Merge mode: morphs the fragment into the existing element using idiomorph." - consts/fragment-merge-mode-morph) +(def pm-outer + "patch mode: replaces the outer HTML of the existing element." + consts/element-patch-mode-outer) -(def mm-inner - "Merge mode: replaces the inner HTML of the existing element." - consts/fragment-merge-mode-inner) +(def pm-inner + "patch mode: replaces the inner HTML of the existing element." + consts/element-patch-mode-inner) -(def mm-outer - "Merge mode: replaces the outer HTML of the existing element." - consts/fragment-merge-mode-outer) +(def pm-remove + "patch mode: remove the existing element from the dom." + consts/element-patch-mode-remove) -(def mm-prepend - "Merge mode: prepends the fragment to the existing element." - consts/fragment-merge-mode-prepend) +(def pm-prepend + "patch mode: prepends the element to the existing element." + consts/element-patch-mode-prepend) -(def mm-append - "Merge mode: appends the fragment to the existing element." - consts/fragment-merge-mode-append) +(def pm-append + "patch mode: appends the element to the existing element." + consts/element-patch-mode-append) -(def mm-before - "Merge mode: inserts the fragment before the existing element." - consts/fragment-merge-mode-before) +(def pm-before + "patch mode: inserts the element before the existing element." + consts/element-patch-mode-before) -(def mm-after - "Merge mode: inserts the fragment after the existing element." - consts/fragment-merge-mode-after) +(def pm-after + "patch mode: inserts the element after the existing element." + consts/element-patch-mode-after) -(def mm-upsert-attributes - "Merge mode: upserts the attributes of the existing element." - consts/fragment-merge-mode-upsert-attributes) +(def pm-replace + "patch mode: Do not morph, simply replace the whole element and reset any + related state." + consts/element-patch-mode-replace) -(defn merge-fragment! - "Send HTML fragments to the browser to be merged into the DOM. +(defn patch-elements! + "Send HTML elements to the browser to be patchd into the DOM. Args: - `sse-gen`: the sse generator to send from - - `fragments`: A string of HTML fragments. + - `elements`: A string of HTML elements. - `opts`: An options map Options keys: - [[id]] - [[retry-duration]] - [[selector]] - - [[merge-mode]] + - [[patch-mode]] - [[use-view-transition]] Return value: - `false` if the connection is closed - `true` otherwise " - ([sse-gen fragments] - (merge-fragment! sse-gen fragments {})) - ([sse-gen fragments opts] - (fragments/merge-fragment! sse-gen fragments opts))) + ([sse-gen elements] + (patch-elements! sse-gen elements {})) + ([sse-gen elements opts] + (elements/patch-elements! sse-gen elements opts))) -(defn merge-fragments! - "Same as [[merge-fragment!]] except that it takes a seq of fragments. - " - ([sse-gen fragments] - (merge-fragments! sse-gen fragments {})) - ([sse-gen fragments opts] - (fragments/merge-fragments! sse-gen fragments opts))) +(defn patch-elements-seq! + "Same as [[patch-elements!]] except that it takes a seq of elements." + ([sse-gen elements] + (patch-elements-seq! sse-gen elements {})) + ([sse-gen elements opts] + (elements/patch-elements-seq! sse-gen elements opts))) -(defn remove-fragment! - "Send a selector to the browser to remove HTML fragments from the DOM. +(defn remove-element! + "Remove element(s) from the dom. It is a convenience function using + [[patch-elements!]] with the [[patch-mode]] options set to [[pm-remove]] + and a [[selector]] set to `selector`. Args: - `sse-gen`: the sse generator to send from - - `selector`: string, CSS selector that represents the fragments to be + - `selector`: string, CSS selector that represents the elements to be removed from the DOM. - `opts`: options map @@ -277,20 +277,23 @@ Some scripts are provided: - `true` otherwise " ([sse-gen selector] - (remove-fragment! sse-gen selector {})) + (remove-element! sse-gen selector {})) ([sse-gen selector opts] - (fragments/remove-fragment! sse-gen selector opts))) + (elements/remove-element! sse-gen selector opts))) -(defn merge-signals! - "Send one or more signals to the browser to be merged into the signals. +(defn patch-signals! + " + Send signals to the browser using + [RFC 7386 JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) + semantics. Args: - `sse-gen`: the sse generator to send from - `signals-content`: a JavaScript object or JSON string that will be sent to - the browser to update signals in the signals. The data must evaluate to a - valid JavaScript. It will be converted to signals by the Datastar client - side. + the browser to update signals. The data must evaluate to a + valid JavaScript Object. `null` values for keys in this JSON object mean + that the signal at these keys are to be removed. - `opts`: An options map Options keys: @@ -303,34 +306,9 @@ Some scripts are provided: - `true` otherwise " ([sse-gen signals-content] - (merge-signals! sse-gen signals-content {})) + (patch-signals! sse-gen signals-content {})) ([sse-gen signals-content opts] - (signals/merge-signals! sse-gen signals-content opts))) - - -(defn remove-signals! - "Send signals to the browser to be removed from the signals. - - Args: - - `sse-gen`: the sse generator to send from - - `paths`: seq of strings that represent the signal paths to be removed from - the signals. The paths must be valid `.` delimited paths to signals within the - signals. The Datastar client side will use these paths to remove the data - from the signals. - - Options keys: - - [[id]] - - [[retry-duration]] - - [[only-if-missing]] - - Return value: - - `false` if the connection is closed - - `true` otherwise - " - ([sse-gen paths] - (signals/remove-signals! sse-gen paths {})) - ([sse-gen paths opts] - (signals/remove-signals! sse-gen paths opts))) + (signals/patch-signals! sse-gen signals-content opts))) (defn get-signals @@ -347,28 +325,32 @@ Some scripts are provided: (defn execute-script! " - Send an execute script event to the client. + Construct a HTML script tag using `script-text` as its content. Then sends it + to the brower using [[patch-elements!]] with [[patch-mode]] set to + [[pm-append]] and [[selector]] set to `\"body\"`. + + The default behavior is to auto remove the script after it has run. Args: - `sse-gen`: the sse generator to send from - - `script-content`: string that represents the JavaScript to be executed + - `script-text`: string that represents the JavaScript to be executed by the browser. - `opts`: An options map Options keys: - [[id]] - [[retry-duration]] - - [[auto-remove]] + - [[auto-remove]] defaults to true - [[attributes]] Return value: - `false` if the connection is closed - `true` otherwise " - ([sse-gen script-content] - (scripts/execute-script! sse-gen script-content {})) - ([sse-gen script-content opts] - (scripts/execute-script! sse-gen script-content opts))) + ([sse-gen script-text] + (scripts/execute-script! sse-gen script-text {})) + ([sse-gen script-text opts] + (scripts/execute-script! sse-gen script-text opts))) diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj index 90d0b0e67..ad8c4b879 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/common.clj @@ -9,13 +9,14 @@ (def retry-duration :d*.sse/retry-duration) ;; Merge fragment opts -(def selector :d*.fragments/selector) -(def merge-mode :d*.fragments/merge-mode) -(def use-view-transition :d*.fragments/use-view-transition) +(def selector :d*.elements/selector) +(def patch-mode :d*.elements/patch-mode) +(def use-view-transition :d*.elements/use-view-transition) ;;Signals opts (def only-if-missing :d*.signals/only-if-missing) + ;; Script opts (def auto-remove :d*.scripts/auto-remove) (def attributes :d*.scripts/attributes) @@ -32,26 +33,20 @@ - `data-lines`: a transient vector of data-lines that will be written in a sse event - `prefix`: The Datastar specific preffix for that line - - `test`: function applied to `v` if the result is true the data-line is - added, it is elided otherwise. If not test is provided, the data-line will - be added. - `v`: the value for that line " - ([data-lines! prefix v] - (conj! data-lines! (str prefix v))) - ([data-lines! test prefix v] - (cond-> data-lines! - (test v) (conj! (str prefix v))))) + [data-lines! prefix v] + (conj! data-lines! (str prefix v))) (defn add-data-lines! "Add several data-lines to the `data-lines!` transient vector." - [data-lines! prefix lines] + [data-lines! prefix lines-seq] (reduce (fn [acc part] (conj! acc (str prefix part))) data-lines! - lines)) + lines-seq)) (defn add-boolean-option? diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj new file mode 100644 index 000000000..1774ac996 --- /dev/null +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/elements.clj @@ -0,0 +1,150 @@ +(ns starfederation.datastar.clojure.api.elements + (:require + [clojure.string :as string] + [starfederation.datastar.clojure.api.common :as common] + [starfederation.datastar.clojure.api.sse :as sse] + [starfederation.datastar.clojure.consts :as consts] + [starfederation.datastar.clojure.utils :as u])) + + +;; ----------------------------------------------------------------------------- +;; Patch Elements options handling +;; ----------------------------------------------------------------------------- +(def ^:private valid-selector? u/not-empty-string?) + +(defn- add-epm? [fmm] + (and fmm (not= fmm consts/default-element-patch-mode))) + +(defn- add-view-transition? [v] + (common/add-boolean-option? consts/default-elements-use-view-transitions v)) + + +(defn conj-patch-element-opts! + "Conj the optional data-lines to the transient `data-lines` vector. + vector." + [data-lines! opts] + (let [sel (common/selector opts) + patch-mode (common/patch-mode opts) + use-vt (common/use-view-transition opts)] + + (cond-> data-lines! + (and sel (valid-selector? sel)) + (common/add-opt-line! consts/selector-dataline-literal sel) + + (and patch-mode (add-epm? patch-mode)) + (common/add-opt-line! consts/mode-dataline-literal patch-mode) + + (and use-vt (add-view-transition? use-vt)) + (common/add-opt-line! consts/use-view-transition-dataline-literal use-vt)))) + + + +;; ----------------------------------------------------------------------------- +;; Patch Element +;; ----------------------------------------------------------------------------- +(defn conj-patch-elements! + "Adds a the data-lines when patching a string of elements." + [data-lines! element] + (cond-> data-lines! + (u/not-empty-string? element) + (common/add-data-lines! consts/elements-dataline-literal + (string/split-lines element)))) + + +(defn ->patch-elements + "Make the data-lines for a patch-element operation." + [element opts] + (u/transient-> [] + (conj-patch-element-opts! opts) + (conj-patch-elements! element))) + + +(defn patch-elements! [sse-gen element opts] + (try + (sse/send-event! sse-gen + consts/event-type-patch-elements + (->patch-elements element opts) + opts) + (catch Exception e + (throw (ex-info "Failed to send element." + {:element element} + e))))) + +(comment + (= (->patch-elements "
hello
" {}) + ["elements
hello
"]) + + (= (->patch-elements "
hello
\n
world!!!
" + {common/selector "#toto" + common/patch-mode consts/element-patch-mode-after + common/use-view-transition true}) + ["selector #toto" + "mode after" + "useViewTransition true" + "elements
hello
" + "elements
world!!!
"])) + + +;; ----------------------------------------------------------------------------- +;; Patch Elements +;; ----------------------------------------------------------------------------- +(defn conj-patch-elements-seq + "Adds a the data-lines when patching a seq of strings elements." + [data-lines! elements-seq] + (cond-> data-lines! + (seq elements-seq) + (common/add-data-lines! consts/elements-dataline-literal + (eduction + (comp (mapcat string/split-lines) + (remove string/blank?)) + elements-seq)))) + + +(defn ->patch-elements-seq + "Make the data-lines for a patch-elements operation." + [elements-seq opts] + (u/transient-> [] + (conj-patch-element-opts! opts) + (conj-patch-elements-seq elements-seq))) + + +(defn patch-elements-seq! [sse-gen elements opts] + (try + (sse/send-event! sse-gen + consts/event-type-patch-elements + (->patch-elements-seq elements opts) + opts) + (catch Exception e + (throw (ex-info "Failed to send fragment." + {:elements elements} + e))))) + + +(comment + (= (->patch-elements-seq ["
hello
" " " "
\nworld\n
"] {}) + ["elements
hello
" + "elements
" + "elements world" + "elements
"]) + + (= (->patch-elements-seq ["
hello
\n
world!!!
" "
world!!!
"] + {common/selector "#toto" + common/patch-mode consts/element-patch-mode-after + common/use-view-transition true}) + ["selector #toto" + "mode after" + "useViewTransition true" + "elements
hello
" + "elements
world!!!
" + "elements
world!!!
"])) + + +;; ----------------------------------------------------------------------------- +;; Remove Element +;; ----------------------------------------------------------------------------- +(defn remove-element! [sse-gen selector opts] + (patch-elements! sse-gen "" (assoc opts + common/selector selector + common/patch-mode consts/element-patch-mode-remove))) + + diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/fragments.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/fragments.clj deleted file mode 100644 index d83f43cbf..000000000 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/fragments.clj +++ /dev/null @@ -1,181 +0,0 @@ -(ns starfederation.datastar.clojure.api.fragments - (:require - [clojure.string :as string] - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.utils :as u])) - - - -;; ----------------------------------------------------------------------------- -;; Selector helpers -(def ^:private valid-selector? u/not-empty-string?) - -(defn- add-selector! [data-lines! selector] - (common/add-opt-line! data-lines! - consts/selector-dataline-literal - selector)) - -(defn- add-selector?! [data-lines! selector] - (common/add-opt-line! - data-lines! - valid-selector? - consts/selector-dataline-literal - selector)) - - -;; ----------------------------------------------------------------------------- -;; Fragment merge mode helpers -(defn- add-fmm? [fmm] - (and fmm (not= fmm consts/fragment-merge-mode-morph))) - - -(defn- add-fragment-merge-mode?! [data-lines! fmm] - (common/add-opt-line! - data-lines! - add-fmm? - consts/merge-mode-dataline-literal - fmm)) - - -;; ----------------------------------------------------------------------------- -;; View transition helpers -(defn add-view-transition? [val] - (common/add-boolean-option? consts/default-fragments-use-view-transitions - val)) - -(defn- add-view-transition?! [data-lines! uvt] - (common/add-opt-line! - data-lines! - add-view-transition? - consts/use-view-transition-dataline-literal - uvt)) - - -;; ----------------------------------------------------------------------------- -;; Fragment -> data lines -(defn- add-merge-fragment! [data-lines! fragment] - (cond-> data-lines! - (u/not-empty-string? fragment) - (common/add-data-lines! consts/fragments-dataline-literal - (string/split-lines fragment)))) - -;; ----------------------------------------------------------------------------- -;; Merge fragment -;; ----------------------------------------------------------------------------- -(defn ->merge-fragment [fragment opts] - (u/transient-> [] - (add-selector?! (common/selector opts)) - (add-fragment-merge-mode?! (common/merge-mode opts)) - (add-view-transition?! (common/use-view-transition opts)) - (add-merge-fragment! fragment))) - - -(comment - (->merge-fragment "
hello
" {}) - := ["fragments
hello
"] - - (->merge-fragment "
hello
\n
world!!!
" - {common/selector "#toto" - common/merge-mode consts/fragment-merge-mode-after - common/use-view-transition :toto}) - := ["selector #toto" - "mergeMode after" - "useViewTransition true" - "fragments
hello
" - "fragments
world!!!
"]) - - -(defn merge-fragment! [sse-gen fragment opts] - (try - (sse/send-event! sse-gen - consts/event-type-merge-fragments - (->merge-fragment fragment opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send fragment." - {:fragment fragment} - e))))) - -;; ----------------------------------------------------------------------------- -;; Merge fragments -;; ----------------------------------------------------------------------------- -(defn- add-merge-fragments! [data-lines! fragments] - (cond-> data-lines! - (seq fragments) - (common/add-data-lines! consts/fragments-dataline-literal - (eduction - (comp (mapcat string/split-lines) - (remove string/blank?)) - fragments)))) - - -(defn ->merge-fragments [fragments opts] - (u/transient-> [] - (add-selector?! (common/selector opts)) - (add-fragment-merge-mode?! (common/merge-mode opts)) - (add-view-transition?! (common/use-view-transition opts)) - (add-merge-fragments! fragments))) - - -(defn merge-fragments! [sse-gen fragments opts] - (try - (sse/send-event! sse-gen - consts/event-type-merge-fragments - (->merge-fragments fragments opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send fragment." - {:fragments fragments} - e))))) - - -(comment - (->merge-fragments ["
hello
" " " "
\nworld\n
"] {}) - := ["fragments
hello
" - "fragments
" - "fragments world" - "fragments
"] - - (->merge-fragments ["
hello
\n
world!!!
" "
world!!!
"] - {common/selector "#toto" - common/merge-mode consts/fragment-merge-mode-after - common/use-view-transition true}) - := ["selector #toto" - "mergeMode after" - "useViewTransition true" - "fragments
hello
" - "fragments
world!!!
" - "fragments
world!!!
"]) - - -;; ----------------------------------------------------------------------------- -;; Remove fragment -;; ----------------------------------------------------------------------------- -(defn ->remove-fragment [selector opts] - (u/transient-> [] - (add-view-transition?! (common/use-view-transition opts)) - (add-selector! selector))) - - -(comment - (->remove-fragment "#titi" - {common/use-view-transition true}) - := ["selector #titi" "useViewTransition true"]) - - -(defn remove-fragment! [sse-gen selector opts] - (when-not (valid-selector? selector) - (throw (ex-info "Invalid selector" {:selector selector}))) - - (try - (sse/send-event! sse-gen - consts/event-type-remove-fragments - (->remove-fragment selector opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send remove." - {:selector selector} - e))))) - diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj index b3bf649a1..93cb65caa 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/scripts.clj @@ -1,92 +1,72 @@ (ns starfederation.datastar.clojure.api.scripts (:require - [clojure.string :as string] - [starfederation.datastar.clojure.api.common :as common] - [starfederation.datastar.clojure.api.sse :as sse] - [starfederation.datastar.clojure.consts :as consts] - [starfederation.datastar.clojure.utils :as u])) + [starfederation.datastar.clojure.api.common :as common] + [starfederation.datastar.clojure.api.elements :as elements] + [starfederation.datastar.clojure.consts :as consts])) -(defn add-auto-remove? [val] - (common/add-boolean-option? consts/default-execute-script-auto-remove - val)) +(defn ->script-tag [script opts] + (let [auto-remove (common/auto-remove opts) + attrs (common/attributes opts) + script-tag-builder (StringBuilder.)] -(defn- add-auto-remove?! [data-lines! ar] - (common/add-opt-line! - data-lines! - add-auto-remove? - consts/auto-remove-dataline-literal - ar)) + ;; Opening + (.append script-tag-builder "lines [m] - (persistent! - (reduce-kv - (fn [acc k v] - (conj! acc (str (name k) \space v))) - (transient []) - m))) + ;; Adding auto-remove logic + (when (or (nil? auto-remove) auto-remove) + (.append script-tag-builder " data-effect=\"el.remove()\"")) + ;; Opening done + (.append script-tag-builder ">") -(defn- add-attributes? [attributes] - (and attributes - (not= attributes consts/default-execute-script-attributes))) + ;; Content of the script + (.append script-tag-builder script) -(defn- add-attributes! [data-lines! attributes] - (cond-> data-lines! - (add-attributes? attributes) - (common/add-data-lines! consts/attributes-dataline-literal - (attributes->lines attributes)))) + ;; Closing + (.append script-tag-builder "") + ;; Returning the built tag + (str script-tag-builder))) -(defn- add-script! [data-lines! script-content] - (cond-> data-lines! - (u/not-empty-string? script-content) - (common/add-data-lines! consts/script-dataline-literal - (string/split-lines script-content)))) +(def patch-opts + {common/selector "body" + common/patch-mode consts/element-patch-mode-append}) -(defn ->script [script-content opts] - (u/transient-> [] - (add-attributes! (common/attributes opts)) - (add-auto-remove?! (common/auto-remove opts)) - (add-script! script-content))) - +(defn execute-script! [sse-gen script-text opts] + (elements/patch-elements! sse-gen + (->script-tag script-text opts) + (merge opts patch-opts))) (comment - (->script "console.log('hello')" {}) - := ["script console.log('hello')"] - - (->script "console.log('hello')" - {common/auto-remove false}) - := ["autoRemove false" "script console.log('hello')"] - - - - (->script "console.log('hello')" - {common/auto-remove false - common/attributes {:type "module"}}) - := ["autoRemove false" "script console.log('hello')"] - - - (->script "console.log('hello')\nconsole.log('world!!!')" - {common/attributes {:type "module" :data-something 1}}) - := ["attributes type module" - "attributes data-something 1" - "script console.log('hello')" - "script console.log('world!!!')"]) - - -(defn execute-script! [sse-gen script-content opts] - (try - (sse/send-event! sse-gen - consts/event-type-execute-script - (->script script-content opts) - opts) - (catch Exception e - (throw (ex-info "Failed to send script" - {:script script-content} - e))))) + (= (->script-tag "console.log('hello')" {}) + "") + + (= (->script-tag "console.log('hello')" + {common/auto-remove false}) + "") + + + (= (->script-tag "console.log('hello')" + {common/auto-remove false + common/attributes {:type "module"}}) + "") + + + (= (->script-tag "console.log('hello');\nconsole.log('world!!!')" + {common/auto-remove :true + common/attributes {:type "module" :data-something 1}}) + "")) diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj index 3bf3085ce..d7eea6597 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/signals.clj @@ -10,49 +10,32 @@ ;; ----------------------------------------------------------------------------- ;; Merge signal ;; ----------------------------------------------------------------------------- -(defn add-only-if-missing? [val] - (common/add-boolean-option? consts/default-merge-signals-only-if-missing - val)) +(defn add-only-if-missing? [v] + (common/add-boolean-option? consts/default-patch-signals-only-if-missing v)) -(defn- add-only-if-missing?! [data-lines! only-if-missing] - (common/add-opt-line! - data-lines! - add-only-if-missing? - consts/only-if-missing-dataline-literal - only-if-missing)) +(defn ->patch-signals [signals opts] + (let [oim (common/only-if-missing opts)] + (u/transient-> [] + (cond-> + (and oim (add-only-if-missing? oim)) + (common/add-opt-line! consts/only-if-missing-dataline-literal oim) + (u/not-empty-string? signals) + (common/add-data-lines! consts/signals-dataline-literal + (string/split-lines signals)))))) -(defn- add-merge-signals! [data-lines! signals] - (cond-> data-lines! - (u/not-empty-string? signals) - (common/add-data-lines! consts/signals-dataline-literal - (string/split-lines signals)))) - - -(defn ->merge-signals [signals opts] - (u/transient-> [] - (add-only-if-missing?! (common/only-if-missing opts)) - (add-merge-signals! signals))) (comment - (->merge-signals "{some json}\n{some other json}" {}) - := ["signals {some json}" - "signals {some other json}"] - - (->merge-signals "{some json}\n{some other json}" - {common/only-if-missing true}) - := ["onlyIfMissing true" - "signals {some json}" - "signals {some other json}"]) - - + (= (->patch-signals "{'some': \n 'json'}" {}) + ["signals {'some': " + "signals 'json'}"])) -(defn merge-signals! [sse-gen signals-content opts] +(defn patch-signals! [sse-gen signals-content opts] (try (sse/send-event! sse-gen - consts/event-type-merge-signals - (->merge-signals signals-content opts) + consts/event-type-patch-signals + (->patch-signals signals-content opts) opts) (catch Exception e (throw (ex-info "Failed to send merge signals" @@ -60,43 +43,6 @@ e))))) -;; ----------------------------------------------------------------------------- -;; Remove signals -;; ----------------------------------------------------------------------------- -(defn add-remove-signals-paths! [data-lines! paths] - (common/add-data-lines! - data-lines! - consts/paths-dataline-literal - paths)) - - -(defn ->remove-signals [paths] - (u/transient-> [] - (add-remove-signals-paths! paths))) - -(comment - (->remove-signals ["foo.bar" "foo.baz" "bar"]) - := ["paths foo.bar" - "paths foo.baz" - "paths bar"]) - - -(defn remove-signals! [sse-gen paths opts] - (when-not (seq paths) - (throw (ex-info "Invalid signal paths to remove." - {:paths paths}))) - - (try - (sse/send-event! sse-gen - consts/event-type-remove-signals - (->remove-signals paths) - opts) - (catch Exception e - (throw (ex-info "Failed to send remove signals" - {:signals paths} - e))))) - - ;; ----------------------------------------------------------------------------- ;; Read signals ;; ----------------------------------------------------------------------------- diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj index 88db849cd..2e94fee04 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/api/sse.clj @@ -63,7 +63,7 @@ (def ^:private data-line-prefix "data: ") (def ^:private new-line "\n") -(def ^:private end-event (str new-line new-line)) +(def ^:private end-event new-line) ;; ----------------------------------------------------------------------------- diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj index c25a1205e..ee5093145 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/consts.clj @@ -1,7 +1,5 @@ ;; This is auto-generated by Datastar. DO NOT EDIT. -(ns starfederation.datastar.clojure.consts - (:require - [clojure.string :as string])) +(ns starfederation.datastar.clojure.consts) (def datastar-key "datastar") @@ -96,4 +94,4 @@ (def event-type-patch-signals "An event for patching signals." - "datastar-patch-signals") \ No newline at end of file + "datastar-patch-signals") diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj index 39b0135eb..2ff01bd53 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/protocols.clj @@ -8,11 +8,33 @@ (sse-gen? [this] "Test wheter a value is a SSEGenerator.")) +(defn throw-not-implemented [type method] + (throw (ex-info (str "Type " type " is not a SSEGenerator.") {:type type :method method}))) + + (extend-protocol SSEGenerator nil (sse-gen? [_] false) + (send-event! [_this _event-type _data-lines _opts] + (throw-not-implemented nil :send-event!)) + + (get-lock [_this] + (throw-not-implemented nil :get-lock)) + + (close-sse! [_this] + (throw-not-implemented nil :close-sse!)) + + Object - (sse-gen? [_] false)) + (sse-gen? [_] false) + + (send-event! [_this _event-type _data-lines _opts] + (throw-not-implemented Object :send-event!)) + + (get-lock [_this] + (throw-not-implemented Object :get-lock)) + (close-sse! [_this] + (throw-not-implemented Object :close-sse!))) diff --git a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj index e08d98ad2..d466b9330 100644 --- a/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj +++ b/sdk/clojure/sdk/src/main/starfederation/datastar/clojure/utils.clj @@ -77,5 +77,6 @@ doc (-> src-var meta :doc)] `(do (def ~dest ~(symbol src-var)) - (alter-meta! (resolve '~dest) assoc :doc ~doc))))) + (alter-meta! (resolve '~dest) assoc :doc ~doc) + (var ~dest))))) diff --git a/sdk/clojure/src/dev/examples/animation_gzip.clj b/sdk/clojure/src/dev/examples/animation_gzip.clj index 2cbaeccce..d16f76cb8 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip.clj @@ -1,9 +1,11 @@ (ns examples.animation-gzip (:require + [dev.onionpancakes.chassis.core :as h] [examples.animation-gzip.handlers :as handlers] [examples.animation-gzip.rendering :as rendering] [examples.animation-gzip.state :as state] [examples.animation-gzip.brotli :as brotli] + [examples.common :as c] [examples.utils :as u] [reitit.ring :as rr] [reitit.ring.middleware.exception :as reitit-exception] @@ -21,38 +23,40 @@ (defn send-frame! [sse frame] (try - (d*/merge-fragment! sse frame) + (d*/patch-elements! sse frame) (catch Exception e (println e)))) -(defn broadcast-new-frame! [current-state] +(defn broadcast-new-frame! [frame] (let [sses @state/!conns] - (when (seq sses) - (let [frame (rendering/render-content current-state)] - (doseq [sse sses] - (send-frame! sse frame)))))) + (doseq [sse sses] + (send-frame! sse frame)))) (defn install-watch! [] (add-watch state/!state ::watch (fn [_k _ref old new] (when-not (identical? old new) - (broadcast-new-frame! new))))) + (let [frame (rendering/render-content new)] + (broadcast-new-frame! frame)))))) (install-watch!) (defn ->routes [->sse-response opts] [["/" handlers/home-handler] - ["/ping/:id" {:handler (handlers/->ping-handler ->sse-response) + ["/ping/:id" {:handler handlers/ping-handler :middleware [reitit-params/parameters-middleware]}] - ["/random-10" (handlers/->random-pings-handler ->sse-response)] - ["/reset" (handlers/->reset-handler ->sse-response)] - ["/play" (handlers/->play-handler ->sse-response)] - ["/pause" (handlers/->pause-handler ->sse-response)] + ["/random-10" handlers/random-pings-handler] + ["/reset" handlers/reset-handler] + ["/step1" handlers/step-handler] + ["/play" handlers/play-handler] + ["/pause" handlers/pause-handler] ["/updates" (handlers/->updates-handler ->sse-response opts)] - ["/refresh" (handlers/->refresh-handler ->sse-response opts)]]) + ["/refresh" handlers/refresh-handler] + ["/resize" handlers/resize-handler] + c/datastar-route]) (defn ->router [->sse-handler opts] @@ -90,6 +94,14 @@ state/!conns (reset! state/!conns #{}) + (-> state/!state + deref + rendering/page) + (state/resize! 10 10) + (state/resize! 20 20) + (state/resize! 25 25) + (state/resize! 30 30) + (state/resize! 50 50) (state/reset-state!) (state/add-random-pings!) (state/step-state!) diff --git a/sdk/clojure/src/dev/examples/animation_gzip/animation.clj b/sdk/clojure/src/dev/examples/animation_gzip/animation.clj index eada20f97..b8d18e0d6 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip/animation.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip/animation.clj @@ -44,7 +44,7 @@ {:animator nil :animation-tick 100 :clock 0 - :size {:x 50 :y 50} + :size {:x 10 :y 10} :color :r :pings []}) @@ -54,6 +54,10 @@ (def default-ping-speed 0.5) +(defn resize [state x y] + (assoc state :size {:x x :y y})) + + (defn ->ping [state pos duration speed] {:clock (:clock state) :color (:color state) diff --git a/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj b/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj index 67337d4d0..2a3e3c66e 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip/handlers.clj @@ -2,8 +2,8 @@ (:require [examples.animation-gzip.rendering :as rendering] [examples.animation-gzip.state :as state] + [examples.utils :as u] [ring.util.response :as ruresp] - [starfederation.datastar.clojure.api :as d*] [starfederation.datastar.clojure.adapter.common :as ac])) @@ -47,90 +47,79 @@ :y (Integer/parseInt y)})) -(defn ->ping-handler [->sse-response] - (fn ping-handler - ([req] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (when-let [coords (recover-coords req)] - (state/add-ping! coords))))})) - ([req respond _raise] - (respond - (ping-handler req))))) - - -(defn ->random-pings-handler [->sse-response] - (fn random-pings-handler - ([req] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (state/add-random-pings!)))})) - ([req respond _raise] - (respond - (random-pings-handler req))))) - - -(defn ->reset-handler [->sse-response] - (fn reset-handler - ([req] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (state/reset-state!)))})) - ([req respond _raise] - (respond - (reset-handler req))))) - - -(defn ->play-handler [->sse-response] - (fn play-handler - ([req] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (state/start-animating!)))})) - ([req respond _raise] - (respond - (play-handler req))))) - - -(defn ->pause-handler [->sse-response] - (fn pause-handler - ([req] - (->sse-response req - {:status 204 - ac/on-open - (fn [sse] - (d*/with-open-sse sse - (state/stop-animating!)))})) - ([req respond _raise] - (respond - (pause-handler req))))) - - -(defn ->refresh-handler [->sse-response & {:as opts}] - (fn refresh-handler - ([req] - (->sse-response req - (merge opts - {ac/on-open - (fn [sse] - (d*/with-open-sse sse - (d*/merge-fragment! sse - (rendering/render-content @state/!state))))}))) - ([req respond _raise] - (respond - (refresh-handler req))))) +(defn ping-handler + ([req] + (when-let [coords (recover-coords req)] + (println "-- ping " coords) + (state/add-ping! coords)) + {:status 204}) + ([req respond _raise] + (respond (ping-handler req)))) + + +(defn random-pings-handler + ([_req] + (println "-- add pixels") + (state/add-random-pings!) + {:status 204}) + ([req respond _raise] + (respond + (random-pings-handler req)))) + + +(defn reset-handler + ([_req] + (println "-- reseting state") + (state/reset-state!) + {:status 204}) + ([req respond _raise] + (respond (reset-handler req)))) + +(defn step-handler + ([_req] + (println "-- Step 1") + (state/step-state!) + {:status 204}) + ([req respond _raise] + (respond + (step-handler req)))) +(defn play-handler + ([_req] + (println "-- play animation") + (state/start-animating!) + {:status 204}) + ([req respond _raise] + (respond (play-handler req)))) + + +(defn pause-handler + ([_req] + (println "-- pause animation") + (state/stop-animating!) + {:status 204}) + ([req respond _raise] + (respond (pause-handler req)))) + +(defn resize-handler + ([req] + (let [{x "rows" y "columns"} (u/get-signals req)] + (println "-- resize" x y) + (state/resize! x y) + {:status 204})) + ([req respond _raise] + (respond + (resize-handler req)))) + + +(defn refresh-handler + ([_req] + {:status 200 + :headers {"Content-Type" "text/html"} + :body (rendering/render-content @state/!state)}) + ([req respond _raise] + (respond (refresh-handler req)))) + + diff --git a/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj b/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj index 6c895fc60..d68ffb765 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip/rendering.clj @@ -105,24 +105,34 @@ (hc/compile [:div [:h3 "Controls"] - [:ul#controls + [:ul.h-list [:li [:button {:data-on-click (d*/sse-get "/refresh")} "refresh"]] [:li [:button {:data-on-click (d*/sse-get "/reset")} "reset"]] [:li [:button {:data-on-click (d*/sse-get "/random-10")} "add 10"]] + [:li [:button {:data-on-click (d*/sse-get "/step1")} "step1"]] (if (:animator state) (hc/compile [:li [:button {:data-on-click (d*/sse-get "/pause")} "pause"]]) (hc/compile - [:li [:button {:data-on-click (d*/sse-get "/play")} "play"]]))]])) + [:li [:button {:data-on-click (d*/sse-get "/play")} "play"]]))] + + [:ul.h-list {:data-signals-rows (-> state :size :x) + :data-signals-columns (-> state :size :y)} + [:li "rows: " [:input {:type "number" + :data-bind "rows" + :data-on-change (d*/sse-post "/resize")}]] + [:li "columns: " [:input {:type "number" + :data-bind "columns" + :data-on-change (d*/sse-post "/resize")}]]]])) (defn log-pane [state] (hc/compile [:div#log-pane.stack [:h3 "State"] - [:div.stack [:h4 "General state"] + [:pre (pr-str (:size state))] [:span "clock: " (:clock state)] [:span "animator:" (:animator state)]] @@ -164,7 +174,7 @@ (h/html (c/page-scaffold [:div {:data-on-load (d*/sse-get "/updates")} - [:style css] + [:style (h/raw css)] [:h2.center "lets get something fun going"] (content state)]))) diff --git a/sdk/clojure/src/dev/examples/animation_gzip/state.clj b/sdk/clojure/src/dev/examples/animation_gzip/state.clj index 08e87833a..91e2cad64 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip/state.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip/state.clj @@ -11,6 +11,8 @@ (defn reset-state! [] (reset! !state animation/starting-state)) +(defn resize! [x y] + (swap! !state animation/resize x y)) (defn add-ping! ([pos] diff --git a/sdk/clojure/src/dev/examples/animation_gzip/style.css b/sdk/clojure/src/dev/examples/animation_gzip/style.css index d9f97ab8b..85ebe6c55 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip/style.css +++ b/sdk/clojure/src/dev/examples/animation_gzip/style.css @@ -2,11 +2,12 @@ width: 70%; display: flex; align-items: flex-start; + justify-content: space-between; flex-direction: row; } #right-pane { - width: 40%; + width: auto; } #controls { @@ -15,13 +16,20 @@ flex-direction: row; } +.h-list { + list-style: none; + display: flex; + flex-direction: row; + gap: 5px; +} + h2 { width: 50%; } .pseudo-canvas { display: grid; - gap: 0px; + gap: 0; border: 1px dotted black; } diff --git a/sdk/clojure/src/dev/examples/broadcast_ring.clj b/sdk/clojure/src/dev/examples/broadcast_ring.clj index 13812b171..f4a83b93e 100644 --- a/sdk/clojure/src/dev/examples/broadcast_ring.clj +++ b/sdk/clojure/src/dev/examples/broadcast_ring.clj @@ -56,7 +56,7 @@ (defn broadcast-lines! [n] (doseq [conn @!conns] (try - (d*/merge-fragment! conn (->> (range 0 n) + (d*/patch-elements! conn (->> (range 0 n) (map (fn [x] (str "-----------------" x "-------------"))) (string/join "\n"))) diff --git a/sdk/clojure/src/dev/examples/common.clj b/sdk/clojure/src/dev/examples/common.clj index 9d059d9a1..670fd7f63 100644 --- a/sdk/clojure/src/dev/examples/common.clj +++ b/sdk/clojure/src/dev/examples/common.clj @@ -2,6 +2,7 @@ (:require [dev.onionpancakes.chassis.core :as h] [dev.onionpancakes.chassis.compiler :as hc] + [ring.util.response :as rur] [starfederation.datastar.clojure.consts :as consts])) @@ -10,6 +11,17 @@ consts/version "/bundles/datastar.js")) +(def datastar-response + (-> (slurp "../../bundles/datastar.js") + rur/response + (rur/content-type "text/javascript"))) + + +(def datastar-route + ["/datastar.js" + (fn ([_req] datastar-response) + ([_req respond _raise] (respond datastar-response)))]) + (defn page-scaffold [body] (hc/compile @@ -18,5 +30,6 @@ [:head [:meta {:charset "UTF-8"}] [:script {:type "module" - :src cdn-url}]] + :src "/datastar.js"}]] [:body body]]])) + diff --git a/sdk/clojure/src/dev/examples/data_dsl.clj b/sdk/clojure/src/dev/examples/data_dsl.clj index 24dc58248..992b55dcb 100644 --- a/sdk/clojure/src/dev/examples/data_dsl.clj +++ b/sdk/clojure/src/dev/examples/data_dsl.clj @@ -7,10 +7,10 @@ ;; on top of the SDK (def example - {:event ::merge-fragments - :fragments ["
hello
"] + {:event ::patch-elements + :elements "
hello
" d*/selector "foo" - d*/merge-mode d*/mm-append + d*/patch-mode d*/pm-append d*/use-view-transition true}) @@ -18,13 +18,13 @@ ;; ----------------------------------------------------------------------------- ;; Pure version just for data-lines ;; ----------------------------------------------------------------------------- -(require '[starfederation.datastar.clojure.api.fragments :as frags]) +(require '[starfederation.datastar.clojure.api.elements :as elements]) (defn sse-event [e] (case (:event e) - ::merge-fragments - (frags/->merge-fragments (:fragments e) e))) + ::patch-elements + (elements/->patch-elements (:elements e) e))) (sse-event example) @@ -39,19 +39,19 @@ ;; ----------------------------------------------------------------------------- (require '[starfederation.datastar.clojure.api.sse :as sse]) -(defn fragment->str [e] +(defn elements->str [e] (let [buffer (StringBuilder.)] (sse/write-event! buffer - consts/event-type-merge-fragments - (frags/->merge-fragments (:fragments e) e) + consts/event-type-patch-elements + (elements/->patch-elements (:fragments e) e) e) (str buffer))) (defn event->str [e] (case (:event e) - ::merge-fragments - (fragment->str e))) + ::patch-elements + (elements->str e))) (event->str example) ; "event: datastar-merge-fragments\n @@ -64,7 +64,7 @@ ;; ----------------------------------------------------------------------------- ;; Side effecting version ;; ----------------------------------------------------------------------------- -(require '[starfederation.datastar.clojure.adapters.test :as at]) +(require '[starfederation.datastar.clojure.adapter.test :as at]) ;; SSE generator that returns the sse event string instead of sending it (def sse-gen (at/->sse-gen)) @@ -73,7 +73,7 @@ (defn sse-event! [sse-gen e] (case (:event e) - ::merge-fragments (d*/merge-fragments! sse-gen (:fragments e) e))) + ::patch-elements (d*/patch-elements! sse-gen (:fragments e) e))) (sse-event! sse-gen example) diff --git a/sdk/clojure/src/dev/examples/faulty_event.clj b/sdk/clojure/src/dev/examples/faulty_event.clj index 9ee05fc97..73fc3eee1 100644 --- a/sdk/clojure/src/dev/examples/faulty_event.clj +++ b/sdk/clojure/src/dev/examples/faulty_event.clj @@ -4,7 +4,7 @@ [examples.utils :as u] [reitit.ring :as rr] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open on-close]])) + [starfederation.datastar.clojure.adapter.ring :refer [->sse-response on-open]])) ;; Testing several ways exception might be caught when using a ring adapter diff --git a/sdk/clojure/src/dev/examples/form_behavior.clj b/sdk/clojure/src/dev/examples/form_behavior.clj deleted file mode 100644 index 663b0776f..000000000 --- a/sdk/clojure/src/dev/examples/form_behavior.clj +++ /dev/null @@ -1,117 +0,0 @@ -(ns examples.form-behavior - (:require - [examples.common :as c] - [dev.onionpancakes.chassis.core :as h] - [dev.onionpancakes.chassis.compiler :as hc] - [examples.utils :as u] - [fireworks.core :refer [?]] - [ring.util.response :as rur] - [reitit.ring :as rr] - [reitit.ring.middleware.parameters :as params] - [reitit.ring.middleware.multipart :as mpparams] - [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] - [starfederation.datastar.clojure.api :as d*])) - - -;; Trying out several way we might rightly and wrongly use html forms - -(defn button [form action-fn] - (let [method (if (= action-fn d*/sse-get) - "get" - "post")] - (hc/compile - [:button {:id (str method (when-not form "-no") "-form") - :data-on-click (if form - (action-fn "/endpoint" "{contentType: 'form'}") - (action-fn "/endpoint"))} - (str method (when-not form " no") " form")]))) - - -(defn result-area [res] - (hc/compile - [:span {:id "form-result"} res])) - - -(def page - (h/html - (c/page-scaffold - [:div - [:h2 "Form page"] - [:form {:action ""} - [:h3 "D* post form"] - [:label {:for "input-1"} "Enter text"] - [:br] - - [:input {:type "text" - :id "input-1" - :name "input-1" - :data-bind-input-1 true}] - [:br] - (button false d*/sse-get) - - [:br] - [:button {:type "button" - :data-on-click (d*/sse-get "/endpoint")} - "Correct non form get"] - [:br] - (button true d*/sse-get) - - [:br] - (button false d*/sse-post) - - [:br] - (button true d*/sse-post) - [:br]] - - [:h3 "Outside form "] - (button false d*/sse-get) - (button false d*/sse-post) - - [:br] - (result-area "")]))) - - -(defn form - ([_] - (rur/response page)) - ([req respond _] - (respond (form req)))) - - -(defn process-endpoint [request] - (let [input-val (get-in request [:params "input-1"]) - signals (u/get-signals request) - val (or input-val (get signals "input-1"))] - (->sse-response request - {on-open - (fn [sse-gen] - (u/clear-terminal!) - (? (dissoc request :reitit.core/match :reitit.core/router)) - (println signals) - (d*/with-open-sse sse-gen - (d*/merge-fragment! sse-gen (h/html (result-area val)))))}))) - - -(defn endpoint - ([request] - (process-endpoint request)) - ([request respond _raise] - (respond (endpoint request)))) - - -(def router - (rr/router - [["/" {:handler form}] - ["/endpoint" {:handler endpoint - :parameters {:multipart true} - :middleware [mpparams/multipart-middleware]}]])) - - -(def handler - (rr/ring-handler router - (rr/create-default-handler) - {:middleware [params/parameters-middleware]})) - - -(comment - (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/form_behavior/core.clj b/sdk/clojure/src/dev/examples/form_behavior/core.clj new file mode 100644 index 000000000..618f734bf --- /dev/null +++ b/sdk/clojure/src/dev/examples/form_behavior/core.clj @@ -0,0 +1,141 @@ +(ns examples.form-behavior.core + (:require + [clojure.java.io :as io] + [examples.common :as c] + [dev.onionpancakes.chassis.core :as h] + [dev.onionpancakes.chassis.compiler :as hc] + [examples.utils :as u] + [ring.util.response :as rur] + [reitit.ring :as rr] + [reitit.ring.middleware.parameters :as params] + [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]] + [starfederation.datastar.clojure.api :as d*])) + + +;; Trying out several way we might rightly and wrongly use html forms +;; TODO: still need to figure out some things + +(defn result-area [value] + (hc/compile + [:div {:id "form-result"} + [:span "From signals: " [:span {:data-text "$input1"}]] + [:br] + [:span "from backend: " [:span (str value "- " (random-uuid))]]])) + +(def style (slurp (io/resource "examples/form_behavior/style.css"))) + +(def page + (h/html + (c/page-scaffold + [:div + [:style style] + [:h2 "Form page"] + [:form {:action ""} + [:h3 "D* post form"] + [:label {:for "input1"} "Enter text"] + [:br] + + [:input {:type "text" + :id "input1" + :name "input-1" + :data-bind-input1 true}] + + + [:br] + (result-area "") + + [:h3 "Inside form"] + [:div#buttons + ;; GET + [:div + [:h4 "GET"] + [:button { :data-on-click "@get('/endpoint')"} "get - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@get('/endpoint')"} "get - no ct - type"][:br] + [:button { :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct - no type "][:br] + [:button {:type "button" :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct -type"][:br]] + + ;; POST + [:div + [:h4 "POST"] + [:button { :data-on-click "@post('/endpoint')"} "post - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@post('/endpoint')"} "post - no ct - type"][:br] + [:button { :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct - no type "][:br] + [:button {:type "button" :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct -type"][:br]] + + ;; PUT + [:div + [:h4 "PUT"] + [:button { :data-on-click "@put('/endpoint')"} "put - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@put('/endpoint')"} "put - no ct - type"][:br] + [:button { :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct - no type "][:br] + [:button {:type "button" :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct -type"][:br]] + + ;; PATCH + [:div + [:h4 "PATCH"] + [:button { :data-on-click "@patch('/endpoint')"} "patch - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@patch('/endpoint')"} "patch - no ct - type"][:br] + [:button { :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct - no type "][:br] + [:button {:type "button" :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct -type"][:br]] + + ;; DELETE + [:div + [:h4 "DELETE"] + [:button { :data-on-click "@delete('/endpoint')"} "delete - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@delete('/endpoint')"} "delete - no ct - type"][:br] + [:button { :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct - no type "][:br] + [:button {:type "button" :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct -type"][:br]]]] + [:h3 "Outside form "] + [:button + {:id "get-no-form", :data-on-click "@get('/endpoint')"} + "get no form"] + [:button + {:id "post-no-form", :data-on-click "@post('/endpoint')"} + "post no form"] + + [:br]]))) + + +(defn form + ([_] + (rur/response page)) + ([req respond _] + (respond (form req)))) + + +(defn process-form + ([request] + (let [value (or (some->> (get-in request [:params "input-1"]) + (str "Form value - ")) + (some-> request + u/get-signals + (get "input1") + (->> (str "Signal value - "))))] + (u/clear-terminal!) + (u/pp-request request) + (println "value" value) + (->sse-response request + {on-open + (fn [sse-gen] + (d*/with-open-sse sse-gen + (d*/patch-elements! sse-gen (h/html (result-area value)))))}))) + ([request respond _raise] + (respond (process-form request)))) + + +(def router + (rr/router + [["/" {:handler form}] + ["/endpoint" {:handler process-form}] + ;:parameters {:multipart true} :middleware [mpparams/multipart-middleware]}] + c/datastar-route])) + + +(def handler + (rr/ring-handler router + (rr/create-default-handler) + {:middleware [params/parameters-middleware]})) + + +(comment + (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/form_behavior/style.css b/sdk/clojure/src/dev/examples/form_behavior/style.css new file mode 100644 index 000000000..3bce4af4f --- /dev/null +++ b/sdk/clojure/src/dev/examples/form_behavior/style.css @@ -0,0 +1,4 @@ +#buttons { + display: flex; + gap: 10px; +} diff --git a/sdk/clojure/src/dev/examples/forms/common.clj b/sdk/clojure/src/dev/examples/forms/common.clj new file mode 100644 index 000000000..b770bd501 --- /dev/null +++ b/sdk/clojure/src/dev/examples/forms/common.clj @@ -0,0 +1,14 @@ +(ns examples.forms.common + (:require + [dev.onionpancakes.chassis.compiler :as hc])) + + +(defn result-area [res-from-signalsl res-from-form] + (hc/compile + [:div {:id "form-result"} + [:span "From signals: " [:span {:data-text "$input1"}]] + [:br] + [:span "from backend signals: " [:span res-from-signalsl]] + [:br] + [:span "from backend form: " [:span res-from-form]]])) + diff --git a/sdk/clojure/src/dev/examples/forms/core.clj b/sdk/clojure/src/dev/examples/forms/core.clj new file mode 100644 index 000000000..1d21dbf2b --- /dev/null +++ b/sdk/clojure/src/dev/examples/forms/core.clj @@ -0,0 +1,41 @@ +(ns examples.forms.core + (:require + [examples.common :as c] + [examples.forms.html :as efh] + [examples.forms.datastar :as efd*] + [dev.onionpancakes.chassis.core :as h] + [examples.utils :as u] + [ring.util.response :as rur] + [reitit.ring :as rr])) + +;; We test here several ways to manage forms, whether the plain HTML way +;; or using D* + +(def home + (h/html + (c/page-scaffold + [[:h1 "Forms Forms Forms"] + [:ul + [:li [:a {:href "/html/get"} "html GET example"]] + [:li [:a {:href "/html/post"}"html POST example"]] + [:li [:a {:href "/datastar/get"}"html GET example"]] + [:li [:a {:href "/datastar/post"}"html POST example"]]]]))) + +(def router + (rr/router + [["/" {:handler (constantly (rur/response home))}] + ["" efh/routes] + ["" efd*/routes] + c/datastar-route])) + + + +(def handler + (rr/ring-handler router (rr/create-default-handler))) + +(defn after-ns-reload [] + (println "rebooting server") + (u/reboot-hk-server! #'handler)) + +(comment + (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/dev/examples/forms/datastar.clj b/sdk/clojure/src/dev/examples/forms/datastar.clj new file mode 100644 index 000000000..6144cf241 --- /dev/null +++ b/sdk/clojure/src/dev/examples/forms/datastar.clj @@ -0,0 +1,91 @@ +(ns examples.forms.datastar + (:require + [examples.common :as c] + [dev.onionpancakes.chassis.core :as h] + [dev.onionpancakes.chassis.compiler :as hc] + [examples.utils :as u] + [ring.util.response :as rur] + [reitit.ring.middleware.parameters :as params] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])) + + + + +(defn result-area [res-from-form] + (hc/compile + [:div {:id "form-result"} + [:span "From signals: " [:span {:data-text "$input1"}]] + [:br] + [:span "from backend form: " [:span res-from-form]]])) + + +(defn page-get [result] + (h/html + (c/page-scaffold + [:div + [:h2 "Html GET form"] + [:form {:action "" + :data-on-submit "@get('/datastar/get', {contentType: 'form'})"} "submit" + [:input {:type "text" + :id "input1" + :name "input1" + :data-bind-input1 true}] + [:button "submit"]] + (result-area result)]))) + + +(defn get-home [req] + (if (not (d*/datastar-request? req)) + (-> (page-get "") + (rur/response) + (rur/content-type "text/html")) + (let [v (get-in req [:params "input1"])] + (u/clear-terminal!) + (u/pp-request req) + (println "got here " v) + (->sse-response req + {on-open + (fn [sse] + (d*/with-open-sse sse + (d*/patch-elements! sse (h/html (result-area v)))))})))) + + +(defn page-post [result] + (h/html + (c/page-scaffold + [:div + [:h2 "Html POST form !!!!"] + [:form {:action "" :data-on-submit "@post('/datastar/post', {contentType: 'form'})"} + [:input {:type "text" + :id "input1" + :name "input1" + :data-bind-input1 true}] + [:button "submit"]] + (result-area result)]))) + + +(defn post-home [req] + (if (not (d*/datastar-request? req)) + (rur/response (page-post "")) + (let [v (get-in req [:params "input1"])] + (u/clear-terminal!) + (u/pp-request req) + (->sse-response req + {on-open + (fn [sse] + (d*/with-open-sse sse + (d*/patch-elements! sse (h/html (result-area v)))))})))) + + + + +(def routes + ["/datastar" + ["/get" {:handler #'get-home}] + ;:middleware [params/parameters-middleware]}] + ["/post" {:handler #'post-home + :middleware [params/parameters-middleware]}]]) + + + diff --git a/sdk/clojure/src/dev/examples/forms/html.clj b/sdk/clojure/src/dev/examples/forms/html.clj new file mode 100644 index 000000000..e52ce106c --- /dev/null +++ b/sdk/clojure/src/dev/examples/forms/html.clj @@ -0,0 +1,63 @@ +(ns examples.forms.html + (:require + [examples.common :as c] + [dev.onionpancakes.chassis.core :as h] + [examples.utils :as u] + [ring.util.response :as rur] + [reitit.ring.middleware.parameters :as params])) + + + +(defn page-get [result] + (h/html + (c/page-scaffold + [:div + [:h2 "Html GET form"] + [:form {:action "" :method "GET"} + [:input {:type "text" + :id "input1" + :name "input1" + :data-bind-input1 true}] + [:button "submit"]] + [:div result]]))) + +(defn get-home [req] + (let [v (get-in req [:params "input1"])] + (u/clear-terminal!) + (u/?req req) + (println "got here " v) + (rur/response (page-get v)))) + + +(defn page-post [result] + (h/html + (c/page-scaffold + [:div + [:h2 "Html POST form !!!!"] + [:form {:action "" :method "POST"} + [:input {:type "text" + :id "input1" + :name "input1" + :data-bind-input1 true}] + [:button "submit"]] + [:div result]]))) + + +(defn post-home [req] + (let [v (get-in req [:params "input1"])] + (u/clear-terminal!) + (u/?req req) + (println "got here " v) + (rur/response (page-post v)))) + + + +(def routes + ["/html" + ["/get" {:handler #'get-home + :middleware [params/parameters-middleware]}] + ["/post" {:handler #'post-home + :middleware [params/parameters-middleware]}]]) + + + diff --git a/sdk/clojure/src/dev/examples/jetty_disconnect.clj b/sdk/clojure/src/dev/examples/jetty_disconnect.clj index 698851d00..87ce7ddcb 100644 --- a/sdk/clojure/src/dev/examples/jetty_disconnect.clj +++ b/sdk/clojure/src/dev/examples/jetty_disconnect.clj @@ -59,7 +59,7 @@ (defn send-big-event! [] - (d*/merge-fragment! @!conn big-message)) + (d*/patch-elements! @!conn big-message)) ;; curl -vv http://localhost:8081/persistent (comment diff --git a/sdk/clojure/src/dev/examples/malli.clj b/sdk/clojure/src/dev/examples/malli.clj index d20ec66b3..f4496d4a5 100644 --- a/sdk/clojure/src/dev/examples/malli.clj +++ b/sdk/clojure/src/dev/examples/malli.clj @@ -5,8 +5,8 @@ [malli.dev :as mdev] [starfederation.datastar.clojure.adapter.test :as at] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api.fragments :as f] - [starfederation.datastar.clojure.api.fragments-schemas] + [starfederation.datastar.clojure.api.elements :as e] + [starfederation.datastar.clojure.api.elements-schemas] [starfederation.datastar.clojure.api.common :as c])) ;; Testing how instrumentation works and how it's activated @@ -19,10 +19,10 @@ (mdev/stop!)) (comment - (d*/merge-fragment! {} "frag") - (d*/merge-fragment! (at/->sse-gen) "frag") - (f/->merge-fragment "f" {c/retry-duration :a}) - (f/->merge-fragment "f" {c/retry-duration 1022})) + (d*/patch-elements! {} "frag") + (d*/patch-elements! (at/->sse-gen) "frag") + (e/->patch-elements "f" {c/retry-duration :a}) + (e/->patch-elements "f" {c/retry-duration 1022})) (comment diff --git a/sdk/clojure/src/dev/examples/multiple_fragments.clj b/sdk/clojure/src/dev/examples/multiple_fragments.clj index c0b08aec2..42e46c8a8 100644 --- a/sdk/clojure/src/dev/examples/multiple_fragments.clj +++ b/sdk/clojure/src/dev/examples/multiple_fragments.clj @@ -25,6 +25,8 @@ [:input {:type "text" :data-bind-input true}] [:button {:data-on-click (d*/sse-get "/endpoint")} "Send input"] [:br] + [:span {:data-text "$input"}] + [:br] [:div "res: " (res "res-1" "")] [:div "duplicate res: " (res "res-2" "")]]))) @@ -33,7 +35,7 @@ (ruresp/response page)) -(defn ->fragments [input-val] +(defn ->elements [input-val] [(h/html (res "res-1" input-val)) (h/html (res "res-2" input-val))]) @@ -46,13 +48,14 @@ {ac/on-open (fn [sse] (d*/with-open-sse sse - (d*/merge-fragments! sse (->fragments input-val))))})))) + (d*/patch-elements-seq! sse (->elements input-val))))})))) (defn ->router [->sse-response] (rr/router [["/" {:handler home}] - ["/endpoint" {:handler (->endpoint ->sse-response)}]])) + ["/endpoint" {:handler (->endpoint ->sse-response)}] + c/datastar-route])) (def default-handler (rr/create-default-handler)) diff --git a/sdk/clojure/src/dev/examples/redirect.clj b/sdk/clojure/src/dev/examples/redirect.clj index 5a460c0fd..cb1109405 100644 --- a/sdk/clojure/src/dev/examples/redirect.clj +++ b/sdk/clojure/src/dev/examples/redirect.clj @@ -38,7 +38,7 @@ (->sse-response ring-request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#indicator "Redirecting in 3 seconds..."])) (Thread/sleep 3000) (d*/redirect! sse "/guide") @@ -50,7 +50,8 @@ (def router (rr/router [["/" {:handler home}] ["/guide" {:handler guide}] - ["/redirect-me" {:handler redirect-handler}]])) + ["/redirect-me" {:handler redirect-handler}] + c/datastar-route])) (def default-handler (rr/create-default-handler)) diff --git a/sdk/clojure/src/dev/examples/remove_fragments.clj b/sdk/clojure/src/dev/examples/remove_fragments.clj index 87fd55750..9c5fb4797 100644 --- a/sdk/clojure/src/dev/examples/remove_fragments.clj +++ b/sdk/clojure/src/dev/examples/remove_fragments.clj @@ -7,8 +7,7 @@ [reitit.ring.middleware.parameters :as reitit-params] [ring.util.response :as ruresp] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.http-kit :as hk-gen] - [starfederation.datastar.clojure.consts :as consts])) + [starfederation.datastar.clojure.adapter.http-kit :as hk-gen])) ;; Appending and removing fragments with the D* api @@ -46,31 +45,33 @@ [:button {:data-on-click (d*/sse-post (str "/remove-fragment/" id))} "remove me"]])) -(defn add-fragment [req] +(defn add-element [req] (let [signals (u/get-signals req) input-val (get signals "input")] (hk-gen/->sse-response req {hk-gen/on-open (fn [sse] (d*/with-open-sse sse - (d*/merge-fragment! sse (->fragment (id!) input-val) + (d*/patch-elements! sse + (->fragment (id!) input-val) {d*/selector "#list" - d*/merge-mode consts/fragment-merge-mode-append})))}))) + d*/patch-mode d*/pm-append})))}))) -(defn remove-fragment [req] +(defn remove-element [req] (let [id (-> req :path-params :id)] (hk-gen/->sse-response req {hk-gen/on-open (fn [sse-gen] - (d*/remove-fragment! sse-gen (str "#" id)))}))) - + (d*/with-open-sse sse-gen + (d*/remove-element! sse-gen (str "#" id))))}))) (def router (rr/router [["/" {:handler #'home}] - ["/add-fragment" {:handler #'add-fragment}] - ["/remove-fragment/:id" {:handler #'remove-fragment}]])) + ["/add-fragment" {:handler #'add-element}] + ["/remove-fragment/:id" {:handler #'remove-element}] + c/datastar-route])) (def default-handler (rr/create-default-handler)) diff --git a/sdk/clojure/src/dev/examples/scripts.clj b/sdk/clojure/src/dev/examples/scripts.clj index 45f20ada9..ef125ac3a 100644 --- a/sdk/clojure/src/dev/examples/scripts.clj +++ b/sdk/clojure/src/dev/examples/scripts.clj @@ -35,7 +35,8 @@ (def router (rr/router [["/" {:handler home}] - ["/endpoint" {:handler endpoint}]])) + ["/endpoint" {:handler endpoint}] + c/datastar-route])) (def default-handler (rr/create-default-handler)) diff --git a/sdk/clojure/src/dev/examples/snippets.clj b/sdk/clojure/src/dev/examples/snippets.clj index db82ff4b0..e72566235 100644 --- a/sdk/clojure/src/dev/examples/snippets.clj +++ b/sdk/clojure/src/dev/examples/snippets.clj @@ -10,10 +10,10 @@ (def sse (at/->sse-gen)) ;; multiple_events -(d*/merge-fragment! sse "
...
") -(d*/merge-fragment! sse "
...
") -(d*/merge-signals! sse "{answer: '...'}") -(d*/merge-signals! sse "{prize: '...'}") +(d*/patch-elements! sse "
...
") +(d*/patch-elements! sse "
...
") +(d*/patch-elements! sse "{answer: '...'}") +(d*/patch-elements! sse "{prize: '...'}") ;; setup #_{:clj-kondo/ignore true} @@ -27,10 +27,10 @@ (->sse-response request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse "
What do you put in a toaster?
") - (d*/merge-signals! sse "{response: '', answer: 'bread'}"))})) + (d*/patch-elements! sse "{response: '', answer: 'bread'}"))})) (comment (handler {})) @@ -48,6 +48,6 @@ (->sse-response request {on-open (fn [sse] - (d*/merge-fragment! sse "
Hello, world!
") - (d*/merge-signals! sse "{foo: {bar: 1}}") + (d*/patch-elements! sse "
Hello, world!
") + (d*/patch-signals! sse "{foo: {bar: 1}}") (d*/execute-script! sse "console.log('Success!')"))})) diff --git a/sdk/clojure/src/dev/examples/snippets/load_more.clj b/sdk/clojure/src/dev/examples/snippets/load_more.clj index cdd456433..215ae5501 100644 --- a/sdk/clojure/src/dev/examples/snippets/load_more.clj +++ b/sdk/clojure/src/dev/examples/snippets/load_more.clj @@ -36,14 +36,14 @@ limit 1 new-offset (+ offset limit)] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div "Item " new-offset]) {d*/selector "#list" - d*/merge-mode d*/mm-append}) + d*/patch-mode d*/pm-append}) (if (< new-offset max-offset) - (d*/merge-signals! sse (write-json-str {"offset" new-offset})) - (d*/remove-fragment! sse "#load-more")) + (d*/patch-signals! sse (write-json-str {"offset" new-offset})) + (d*/remove-element! sse "#load-more")) (d*/close-sse! sse)))})) diff --git a/sdk/clojure/src/dev/examples/snippets/polling1.clj b/sdk/clojure/src/dev/examples/snippets/polling1.clj index 1042126a8..b76e65b50 100644 --- a/sdk/clojure/src/dev/examples/snippets/polling1.clj +++ b/sdk/clojure/src/dev/examples/snippets/polling1.clj @@ -24,7 +24,7 @@ (->sse-response ring-request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#time {:data-on-interval__duration.5s (d*/sse-get "/endpoint")} (LocalDateTime/.format (LocalDateTime/now) formatter)])) (d*/close-sse! sse))})) diff --git a/sdk/clojure/src/dev/examples/snippets/polling2.clj b/sdk/clojure/src/dev/examples/snippets/polling2.clj index d12d19d08..12b5c7ce2 100644 --- a/sdk/clojure/src/dev/examples/snippets/polling2.clj +++ b/sdk/clojure/src/dev/examples/snippets/polling2.clj @@ -30,7 +30,7 @@ duration (if (neg? (compare seconds "50")) "5" "1")] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#time {(str "data-on-interval__duration." duration "s") (d*/sse-get "/endpoint")} current-time])) diff --git a/sdk/clojure/src/dev/examples/snippets/redirect1.clj b/sdk/clojure/src/dev/examples/snippets/redirect1.clj index 603f4f448..f5e1716f9 100644 --- a/sdk/clojure/src/dev/examples/snippets/redirect1.clj +++ b/sdk/clojure/src/dev/examples/snippets/redirect1.clj @@ -19,7 +19,7 @@ (->sse-response ring-request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#indicator "Redirecting in 3 seconds..."])) (Thread/sleep 3000) (d*/execute-script! sse "window.location = \"/guide\"") diff --git a/sdk/clojure/src/dev/examples/snippets/redirect2.clj b/sdk/clojure/src/dev/examples/snippets/redirect2.clj index 434a224c3..22c7ddede 100644 --- a/sdk/clojure/src/dev/examples/snippets/redirect2.clj +++ b/sdk/clojure/src/dev/examples/snippets/redirect2.clj @@ -3,7 +3,7 @@ (:require [dev.onionpancakes.chassis.core :refer [html]] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.adapter.test :refer [on-open]] + [starfederation.datastar.clojure.adapter.common :refer [on-open]] [starfederation.datastar.clojure.adapter.test :refer [->sse-response]])) @@ -19,7 +19,7 @@ (->sse-response ring-request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#indicator "Redirecting in 3 seconds..."])) (Thread/sleep 3000) (d*/execute-script! sse diff --git a/sdk/clojure/src/dev/examples/snippets/redirect3.clj b/sdk/clojure/src/dev/examples/snippets/redirect3.clj index 54ffc8ca8..f76c42f5a 100644 --- a/sdk/clojure/src/dev/examples/snippets/redirect3.clj +++ b/sdk/clojure/src/dev/examples/snippets/redirect3.clj @@ -19,7 +19,7 @@ (->sse-response ring-request {on-open (fn [sse] - (d*/merge-fragment! sse + (d*/patch-elements! sse (html [:div#indicator "Redirecting in 3 seconds..."])) (Thread/sleep 3000) (d*/redirect! sse "/guide") diff --git a/sdk/clojure/src/dev/examples/tiny_gzip.clj b/sdk/clojure/src/dev/examples/tiny_gzip.clj index 75b0b4dc0..c9203f001 100644 --- a/sdk/clojure/src/dev/examples/tiny_gzip.clj +++ b/sdk/clojure/src/dev/examples/tiny_gzip.clj @@ -47,7 +47,7 @@ (defn send-val! [sse v] (try - (d*/merge-fragment! sse (h/html (render-val v))) + (d*/patch-elements! sse (h/html (render-val v))) (catch Exception e (println e)))) @@ -96,8 +96,8 @@ [["/" {:handler home}] ["/change-val" {:handler (->change-val ->sse-response) :middleware [reitit-params/parameters-middleware]}] - ["/updates" {:handler (->updates ->sse-response opts)}]])) - + ["/updates" {:handler (->updates ->sse-response opts)}] + c/datastar-route])) (def default-handler (rr/create-default-handler)) diff --git a/sdk/clojure/src/dev/examples/utils.clj b/sdk/clojure/src/dev/examples/utils.clj index 234dec080..e1b190c63 100644 --- a/sdk/clojure/src/dev/examples/utils.clj +++ b/sdk/clojure/src/dev/examples/utils.clj @@ -1,7 +1,8 @@ (ns examples.utils (:require - [charred.api :as charred] - [fireworks.core :refer [?]] + [charred.api :as charred] + [fireworks.core :refer [?]] + [puget.printer :as pp] [starfederation.datastar.clojure.api :as d*])) @@ -17,7 +18,18 @@ (defmacro force-out [& body] `(binding [*out* (java.io.OutputStreamWriter. System/out)] ~@body)) + +(defn pp-request [req] + (-> req + (dissoc :reitit.core/match :reitit.core/router) + pp/pprint + pp/with-color)) + + +(defn ?req [req] + (? (dissoc req :reitit.core/match :reitit.core/router))) + (def ^:private bufSize 1024) (def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) @@ -25,7 +37,7 @@ (defn get-signals [req] - (-> req d*/get-signals read-json)) + (some-> req d*/get-signals read-json)) (defn rr [sym] @@ -96,6 +108,3 @@ opts)))))) -(defn ?req [req] - (? (dissoc req :reitit.core/match :reitit.core/router))) - diff --git a/sdk/clojure/src/dev/user.clj b/sdk/clojure/src/dev/user.clj index 71a0f8735..119296eb0 100644 --- a/sdk/clojure/src/dev/user.clj +++ b/sdk/clojure/src/dev/user.clj @@ -1,13 +1,16 @@ (ns user (:require [clojure.repl.deps :as crdeps] - [clj-reload.core :as reload] - [malli.dev :as mdev])) + [clojure+.hashp :as hashp] + [clj-reload.core :as reload] + [malli.dev :as mdev])) (alter-var-root #'*warn-on-reflection* (constantly true)) +(hashp/install!) + (reload/init {:no-reload ['user]}) diff --git a/sdk/clojure/src/test/adapter-common/test/common.clj b/sdk/clojure/src/test/adapter-common/test/common.clj index 401e36805..09cd20c14 100644 --- a/sdk/clojure/src/test/adapter-common/test/common.clj +++ b/sdk/clojure/src/test/adapter-common/test/common.clj @@ -11,6 +11,7 @@ [starfederation.datastar.clojure.adapter.common :as ac] [starfederation.datastar.clojure.api :as d*] [starfederation.datastar.clojure.api.sse :as sse] + [test.examples.form :as ef] [test.utils :as u])) @@ -125,15 +126,15 @@ ;; ----------------------------------------------------------------------------- (defn do-form! [driver msg button] (ea/go driver (u/url (:port *ctx*) "form")) - (ea/fill driver :input-1 msg) + (ea/fill driver ef/input-id msg) (ea/click driver button) - (ea/clear driver :input-1) - (ea/get-element-text driver :form-result)) + (ea/clear driver ef/input-id) + (ea/get-element-text driver ef/form-result-id)) (defn run-form-test! [driver] - {:get (do-form! driver "get" :get-form) - :post (do-form! driver "post" :post-form)}) + {:get (do-form! driver "get" ef/get-button-id) + :post (do-form! driver "post" ef/post-button-id)}) (def expected-form-vals @@ -182,8 +183,8 @@ (defn persistent-see-send-events! [sse-gen] - (d*/merge-fragment! sse-gen "1") - (d*/merge-fragment! sse-gen "2")) + (d*/patch-elements! sse-gen "1") + (d*/patch-elements! sse-gen "2")) (defn run-persistent-sse-test! [] @@ -218,8 +219,8 @@ (def expected-p-sse-res-body (let [sse-gen (test-gen/->sse-gen)] - (str (d*/merge-fragment! sse-gen "1") - (d*/merge-fragment! sse-gen "2")))) + (str (d*/patch-elements! sse-gen "1") + (d*/patch-elements! sse-gen "2")))) (defn p-sse-body-ok? [response] diff --git a/sdk/clojure/src/test/adapter-common/test/examples/common.clj b/sdk/clojure/src/test/adapter-common/test/examples/common.clj index 1956a75ee..5023fe247 100644 --- a/sdk/clojure/src/test/adapter-common/test/examples/common.clj +++ b/sdk/clojure/src/test/adapter-common/test/examples/common.clj @@ -4,6 +4,7 @@ [dev.onionpancakes.chassis.compiler :as hc] [ring.middleware.params :as rmp] [ring.middleware.multipart-params :as rmpp] + [ring.util.response :as rur] [reitit.ring :as rr] [starfederation.datastar.clojure.consts :as consts])) @@ -18,8 +19,22 @@ consts/version "/bundles/datastar.js")) + +(def datastar-response + (-> (slurp "../../bundles/datastar.js") + rur/response + (rur/content-type "text/javascript"))) + + +(def datastar-route + ["/datastar.js" + (fn ([_req] datastar-response) + ([_req respond _raise] (respond datastar-response)))]) + + + (def datastar - (script "module" cdn-url)) + (script "module" "/datastar.js")) (defn scaffold [content & {:as _}] diff --git a/sdk/clojure/src/test/adapter-common/test/examples/counter.clj b/sdk/clojure/src/test/adapter-common/test/examples/counter.clj index afbeca7af..96fbfdcfa 100644 --- a/sdk/clojure/src/test/adapter-common/test/examples/counter.clj +++ b/sdk/clojure/src/test/adapter-common/test/examples/counter.clj @@ -105,7 +105,7 @@ (fn update-signal [req f & args] (->sse-response req {ac/on-open (fn [sse-gen] - (d*/merge-signals! sse-gen (apply update-signal* req f args)) + (d*/patch-signals! sse-gen (apply update-signal* req f args)) (d*/close-sse! sse-gen))}))) diff --git a/sdk/clojure/src/test/adapter-common/test/examples/form.clj b/sdk/clojure/src/test/adapter-common/test/examples/form.clj index 84b367790..21e5c1b58 100644 --- a/sdk/clojure/src/test/adapter-common/test/examples/form.clj +++ b/sdk/clojure/src/test/adapter-common/test/examples/form.clj @@ -12,6 +12,12 @@ ;; ----------------------------------------------------------------------------- ;; Views ;; ----------------------------------------------------------------------------- +(def input-id :input1) +(def get-button-id :get-form) +(def post-button-id :post-form) +(def form-result-id :form-result) + + (defn form-get [url] (d*/sse-get url "{contentType: 'form'}")) @@ -22,8 +28,7 @@ (defn result-area [res] (hc/compile - [:span {:id "form-result"} res])) - + [:span {:id form-result-id} res])) (defn form-page [] (common/scaffold @@ -32,20 +37,19 @@ [:h2 "Form page"] [:form {:action ""} [:h3 "D* post form"] - [:label {:for "input-1"} "Enter text"] + [:label {:for input-id} "Enter text"] [:br] [:input {:type "text" - :id "input-1" - :name "input-1" - :data-bind-input-1 true}] + :id input-id + :name input-id}] [:br] - [:button {:id "get-form" + [:button {:id get-button-id :data-on-click (form-get "/form/endpoint")} "Form get"] [:br] - [:button {:id "post-form" + [:button {:id post-button-id :data-on-click (form-post "/form/endpoint")} "Form post"] @@ -66,12 +70,12 @@ (defn process-endpoint [request ->sse-response] - (let [input-val (get-in request [:params "input-1"])] + (let [input-val (get-in request [:params (name input-id)])] (->sse-response request {ac/on-open (fn [sse-gen] (d*/with-open-sse sse-gen - (d*/merge-fragment! sse-gen (h/html (result-area input-val)))))}))) + (d*/patch-elements! sse-gen (h/html (result-area input-val)))))}))) (defn ->endpoint [->sse-response] diff --git a/sdk/clojure/src/test/adapter-common/test/utils.clj b/sdk/clojure/src/test/adapter-common/test/utils.clj index effd3e70b..258d231fd 100644 --- a/sdk/clojure/src/test/adapter-common/test/utils.clj +++ b/sdk/clojure/src/test/adapter-common/test/utils.clj @@ -6,7 +6,7 @@ java.net.ServerSocket)) ;; ----------------------------------------------------------------------------- -;; Json helpers +;; JSON helpers ;; ----------------------------------------------------------------------------- (def ^:private bufSize 1024) (def read-json (charred/parse-json-fn {:async? false :bufsize bufSize})) diff --git a/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj b/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj index e647c9e26..aecaf88e5 100644 --- a/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj +++ b/sdk/clojure/src/test/adapter-http-kit/starfederation/datastar/clojure/adapter/http_kit/impl_test.clj @@ -4,6 +4,7 @@ [org.httpkit.server :as hk-server] [starfederation.datastar.clojure.api :as d*] [starfederation.datastar.clojure.adapter.common :as ac] + [starfederation.datastar.clojure.adapter.test :as at] [starfederation.datastar.clojure.adapter.http-kit.impl :as impl] [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) (:import @@ -69,15 +70,18 @@ (impl/->sse-gen c send!))) +(def expected-event-result + (d*/patch-elements! (at/->sse-gen) "msg")) + (defn send-SSE-event [opts] (let [baos (ByteArrayOutputStream.)] (with-open [_baos baos sse-gen ^Closeable (->sse-gen baos opts)] - (d*/merge-fragment! sse-gen "msg" {})) + (d*/patch-elements! sse-gen "msg" {})) (expect (= (read-bytes baos opts) - "event: datastar-merge-fragments\ndata: fragments msg\n\n\n")))) + expected-event-result)))) (defdescribe simple-test @@ -86,7 +90,7 @@ (it "We can send events using a persistent buffered reader" (send-SSE-event {ac/write-profile ac/buffered-writer-profile})) - + (it "We can send gziped events using a temp buffer" (send-SSE-event {ac/write-profile ac/gzip-profile :gzip? true})) diff --git a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj index 0c3976b49..04e015dbb 100644 --- a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj +++ b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler.clj @@ -52,7 +52,8 @@ (def router (rr/router - [counter-routes + [common/datastar-route + counter-routes form-routes])) diff --git a/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj b/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj index 6c2e8b346..4b59520a9 100644 --- a/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj +++ b/sdk/clojure/src/test/adapter-ring/starfederation/datastar/clojure/adapter/ring/impl_test.clj @@ -4,6 +4,7 @@ [ring.core.protocols :as p] [starfederation.datastar.clojure.adapter.common :as ac] [starfederation.datastar.clojure.adapter.ring.impl :as impl] + [starfederation.datastar.clojure.adapter.test :as at] [starfederation.datastar.clojure.api :as d*] [starfederation.datastar.clojure.adapter.common-test :refer [read-bytes]]) (:import @@ -115,17 +116,19 @@ ;; ----------------------------------------------------------------------------- ;; Basic sending of a SSE event without any server ;; ----------------------------------------------------------------------------- +(def expected-event-result + (d*/patch-elements! (at/->sse-gen) "msg")) + (defn send-SSE-event [response] (let [baos (ByteArrayOutputStream.)] (with-open [sse-gen (impl/->sse-gen) baos baos] (p/write-body-to-stream sse-gen response baos) - (d*/merge-fragment! sse-gen "msg" {})) + (d*/patch-elements! sse-gen "msg" {})) (expect (= (read-bytes baos (::impl/opts response)) - "event: datastar-merge-fragments\ndata: fragments msg\n\n\n")))) - + expected-event-result)))) (defdescribe simple-test (it "can send events with a using temp buffers" @@ -133,7 +136,7 @@ (it "can send events with a using a persistent buffered reader" (send-SSE-event {::impl/opts {ac/write-profile ac/buffered-writer-profile}})) - + (it "can send gziped events with a using temp buffers" (send-SSE-event {::impl/opts {ac/write-profile ac/gzip-profile :gzip? true}})) diff --git a/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj b/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj index 6547ba576..e27cc89ae 100644 --- a/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj +++ b/sdk/clojure/src/test/adapter-ring/test/examples/ring_handler.clj @@ -47,7 +47,8 @@ (def router (rr/router - [counter-routes + [common/datastar-route + counter-routes form-routes])) diff --git a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj index 7e0a9363d..f285d3068 100644 --- a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj +++ b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj @@ -40,7 +40,7 @@ (defdescribe reading-bytes (specify "We can do str -> bytes -> str" - (let [original (str (d*/merge-fragment! (at/->sse-gen) "msg"))] + (let [original (str (d*/patch-elements! (at/->sse-gen) "msg"))] (expect (= original (-> original diff --git a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj index 811285145..bbfd1bead 100644 --- a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj +++ b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/api_test.clj @@ -1,22 +1,14 @@ (ns starfederation.datastar.clojure.api-test (:require [clojure.string :as string] - [clojure.template :as ct] [starfederation.datastar.clojure.api :as d*] [starfederation.datastar.clojure.consts :as consts] [starfederation.datastar.clojure.adapter.test :as at] - [lazytest.core :as lt :refer [defdescribe describe it expect]])) + [lazytest.core :as lt :refer [defdescribe describe it expect specify]])) -(def merge-fragment-t consts/event-type-merge-fragments) -(def remove-fragment-t consts/event-type-remove-fragments) -(def merge-signals-t consts/event-type-merge-signals) -(def remove-signals-t consts/event-type-remove-signals) -(def execute-script-t consts/event-type-execute-script) - - -(defn ->data-line [line-literal val] - (format "data: %s%s" line-literal val)) +(def patch-element-t consts/event-type-patch-elements) +(def patch-signals-t consts/event-type-patch-signals) ;; ----------------------------------------------------------------------------- @@ -33,11 +25,11 @@ (and retry-duration (> retry-duration 0) - (not= retry-duration consts/default-sse-retry-duration)) + (not= retry-duration consts/default-sse-retry-duration)) (conj (format "retry: %s" retry-duration)))) -(def event-end ["" "" ""]) +(def event-end ["" ""]) (defn event [event-type data-lines & {:as opts}] @@ -47,355 +39,289 @@ event-end))) -(def basic-selector "#id") -(def sel-id-data-line "data: selector #id") - - ;; ----------------------------------------------------------------------------- ;; Testing that basic send-event! options ;; ----------------------------------------------------------------------------- -(defmacro basic-test - "Testing management of sse level options by high level functions." - [sse-gen tested-fn input event-type data-lines] - (ct/apply-template - '[sse-gen tested-fn input event-type data-lines] - '(describe tested-fn - (lt/expect-it "sends minimal fragment" - (= (tested-fn sse-gen input {}) - (event event-type data-lines))) +(defn basic-test + "Testing the handling of SSE options." + [tested-fn input event-type data-lines] + (describe tested-fn + (lt/expect-it "sends minimal fragment" + (= (tested-fn (at/->sse-gen) input {}) + (event event-type data-lines))) - (lt/expect-it "handles ids" - (= (tested-fn sse-gen input {d*/id "1"}) - (event event-type data-lines {d*/id "1"}))) + (lt/expect-it "handles ids" + (= (tested-fn (at/->sse-gen) input {d*/id "1"}) + (event event-type data-lines {d*/id "1"}))) - (it "handles retry duration" - (expect - (= (tested-fn sse-gen input {d*/retry-duration 1}) - (event event-type data-lines {d*/retry-duration 1}))) + (it "handles retry duration" + (expect + (= (tested-fn (at/->sse-gen) input {d*/retry-duration 1}) + (event event-type data-lines {d*/retry-duration 1}))) - (expect - (= (tested-fn sse-gen input {d*/retry-duration 0}) - (event event-type data-lines {d*/retry-duration 0})))) + (expect + (= (tested-fn (at/->sse-gen) input {d*/retry-duration 0}) + (event event-type data-lines {d*/retry-duration 0})))) - (lt/expect-it "handles both" - (= (tested-fn sse-gen input {d*/id "1" d*/retry-duration 1}) - (event event-type data-lines {d*/id "1" d*/retry-duration 1})))) - [sse-gen tested-fn input event-type data-lines])) + (lt/expect-it "handles both" + (= (tested-fn (at/->sse-gen) input {d*/id "1" d*/retry-duration 1}) + (event event-type data-lines {d*/id "1" d*/retry-duration 1}))))) -(defdescribe common-opts - (let [sse-gen (at/->sse-gen) - remove-fragment-expected-dls [sel-id-data-line]] - (basic-test sse-gen d*/merge-fragment! "" merge-fragment-t []) - (basic-test sse-gen d*/merge-fragments! [] merge-fragment-t []) - (basic-test sse-gen d*/remove-fragment! basic-selector remove-fragment-t remove-fragment-expected-dls) - (basic-test sse-gen d*/merge-signals! "" merge-signals-t []) - (basic-test sse-gen d*/execute-script! "" execute-script-t []))) - +(defdescribe test-common-sse-opts + (basic-test d*/patch-elements! "" patch-element-t []) + (basic-test d*/patch-elements-seq! [] patch-element-t []) + (basic-test d*/patch-signals! "" patch-signals-t [])) -;; ----------------------------------------------------------------------------- -;; Merge fragment -;; ----------------------------------------------------------------------------- -(def div-fragment "
\n hello\n
") -(def ->fragment-line (partial ->data-line consts/fragments-dataline-literal)) +(comment + (require '[lazytest.repl :as ltr]) + (ltr/run-test-var #'test-common-sse-opts)) -(def div-data - [(->fragment-line "
") - (->fragment-line " hello") - (->fragment-line "
")]) +;; ----------------------------------------------------------------------------- +;; Patch elements helpers +;; ----------------------------------------------------------------------------- +(defn ->data-line [line-literal val] + (format "data: %s%s" line-literal val)) + +(def basic-selector "#id") (def selector-line (->data-line consts/selector-dataline-literal basic-selector)) -(def test-merge-mode consts/fragment-merge-mode-after) -(def merge-mode-line - (->data-line consts/merge-mode-dataline-literal test-merge-mode)) - +(def test-merge-mode consts/element-patch-mode-after) +(def patch-mode-line + (->data-line consts/mode-dataline-literal test-merge-mode)) (def use-view-transition-line (->data-line consts/use-view-transition-dataline-literal true)) +(def ->element-line (partial ->data-line consts/elements-dataline-literal)) -(defmacro basic-expect [sse-gen tested-fn input opts event-type data-lines] - (ct/apply-template - '[sse-gen tested-fn input opts event-type data-lines] - '(expect (= (tested-fn sse-gen input opts) - (event event-type data-lines))) - [sse-gen tested-fn input opts event-type data-lines])) +;; ----------------------------------------------------------------------------- +;; Patch elements tests cases +;; ----------------------------------------------------------------------------- +(defn patch-simple-test + "No options give a simple event with the patch data-lines" + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {}) + (event patch-element-t expected-datalines)))) + + +(defn patch-selector-test + "We see the selector data-line added." + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {d*/selector basic-selector}) + (event patch-element-t (list* selector-line expected-datalines))))) + + +(defn patch-mode-test + "We see the patch mode data-line added." + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {d*/patch-mode test-merge-mode}) + (event patch-element-t (list* patch-mode-line expected-datalines))))) + + +(defn patch-vt-false-test + "No view transition on false" + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition false}) + (event patch-element-t expected-datalines)))) + +(defn patch-vt-non-bool-test + "No view transition on non boolean." + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition :true}) + (event patch-element-t expected-datalines)))) + + +(defn patch-vt-true-test + "View transition line is added on true." + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) input {d*/use-view-transition true}) + (event patch-element-t (list* use-view-transition-line expected-datalines))))) + + +(defn patch-all-options-test + "All options, we see all additional lines." + [tested-patch-fn input expected-datalines] + (expect (= (tested-patch-fn (at/->sse-gen) + input + {d*/selector basic-selector + d*/patch-mode test-merge-mode + d*/use-view-transition true}) + (event patch-element-t + (list* + selector-line + patch-mode-line + use-view-transition-line + expected-datalines))))) -(defmacro expect-merge-fragment [sse-gen opts data-lines] - (ct/apply-template - '[sse-gen opts data-lines] - '(basic-expect sse-gen - d*/merge-fragment! div-fragment opts - merge-fragment-t data-lines) - [sse-gen opts data-lines])) +;; ----------------------------------------------------------------------------- +;; patch-elements! test definition +;; ----------------------------------------------------------------------------- +(def div-element "
\n hello\n
") +(def div-data + [(->element-line "
") + (->element-line " hello") + (->element-line "
")]) -(defdescribe merge-fragment! - (let [gen (at/->sse-gen)] - (describe d*/merge-fragment! - (it "works for a basic fragment" - (expect-merge-fragment gen {} div-data)) +(defdescribe test-patch-elements! + (describe d*/patch-elements! + (it "handles no options" + (patch-simple-test d*/patch-elements! div-element div-data)) + (it "handles selectors" + (patch-selector-test d*/patch-elements! div-element div-data)) + (it "handles patch modes" + (patch-mode-test d*/patch-elements! div-element div-data)) - (it "handles selectors" - (expect-merge-fragment gen {d*/selector basic-selector} - (list* selector-line div-data))) + (describe "handles view-transitions" + (specify "no view transition on false" + (patch-vt-false-test d*/patch-elements! div-element div-data)) + (specify "no view transition on non boolean value" + (patch-vt-non-bool-test d*/patch-elements! div-element div-data)) + (specify "view transition on true" + (patch-vt-true-test d*/patch-elements! div-element div-data))) - (it "handles merge-mode" - (expect-merge-fragment gen {d*/merge-mode test-merge-mode} - (list* merge-mode-line div-data))) + (it "handles all options" + (patch-all-options-test d*/patch-elements! div-element div-data)))) - (it "handles view transitions" - (expect-merge-fragment gen {d*/use-view-transition true} - (list* use-view-transition-line div-data)) - (expect-merge-fragment gen {d*/use-view-transition :true} div-data) - (expect-merge-fragment gen {d*/use-view-transition false} div-data)) - - (it "handles all options" - (expect-merge-fragment gen {d*/selector basic-selector - d*/merge-mode test-merge-mode - d*/use-view-transition true} - (list* selector-line - merge-mode-line - use-view-transition-line - div-data)))))) +(comment + (ltr/run-test-var #'test-patch-elements!)) ;; ----------------------------------------------------------------------------- -;; Merge fragments +;; patch-elements-seq! test definition ;; ----------------------------------------------------------------------------- -(def multi-fragments ["
\n hello\n
" - "
\n world\n
"]) +(def multi-elements ["
\n hello\n
" + "
\n world\n
"]) (def multi-data - [(->fragment-line "
") - (->fragment-line " hello") - (->fragment-line "
") - (->fragment-line "
") - (->fragment-line " world") - (->fragment-line "
")]) - - - -(defmacro expect-merge-fragments [sse-gen opts data-lines] - (ct/apply-template - '[sse-gen opts data-lines] - '(basic-expect sse-gen - d*/merge-fragments! multi-fragments opts - merge-fragment-t data-lines) - [sse-gen opts data-lines])) - - -(defdescribe merge-fragments! - (let [gen (at/->sse-gen)] - (describe d*/merge-fragment! - - (it "works for a basic fragment" - (expect-merge-fragments gen {} multi-data)) - - (it "handles selectors" - (expect-merge-fragments gen {d*/selector basic-selector} - (list* selector-line multi-data))) - - (it "handles merge-mode" - (expect-merge-fragments gen {d*/merge-mode test-merge-mode} - (list* merge-mode-line multi-data))) - - (it "handles view transitions" - (expect-merge-fragments gen {d*/use-view-transition true} - (list* use-view-transition-line multi-data)) - (expect-merge-fragments gen {d*/use-view-transition :true} - multi-data) - (expect-merge-fragments gen {d*/use-view-transition false} - multi-data)) - - (it "handles all options" - (expect-merge-fragments gen {d*/selector basic-selector - d*/merge-mode test-merge-mode - d*/use-view-transition true} - (list* selector-line - merge-mode-line - use-view-transition-line - multi-data)))))) + [(->element-line "
") + (->element-line " hello") + (->element-line "
") + (->element-line "
") + (->element-line " world") + (->element-line "
")]) -;; ----------------------------------------------------------------------------- -;; Remove fragments -;; ----------------------------------------------------------------------------- -(defmacro expect-remove-fragment [sse-gen opts data-lines] - (ct/apply-template - '[sse-gen opts data-lines] - '(basic-expect sse-gen - d*/remove-fragment! basic-selector opts - remove-fragment-t data-lines) - [sse-gen opts data-lines])) - - -(defdescribe remove-fragment! - (let [gen (at/->sse-gen)] - (describe d*/remove-fragment! - (it "Throws on no selector" - (expect (lt/throws? clojure.lang.ExceptionInfo #(d*/remove-fragment! gen "" {})))) - - (it "handles view transitions" - (expect-remove-fragment gen {d*/use-view-transition true} - [use-view-transition-line selector-line]) - (expect-remove-fragment gen {d*/use-view-transition :true} - [selector-line]) - (expect-remove-fragment gen {d*/use-view-transition false} - [selector-line])) - - (it "handles all options" - (expect-remove-fragment gen {d*/use-view-transition true} - [use-view-transition-line - selector-line]))))) +(defdescribe test-patch-elements-seq! + (describe d*/patch-elements-seq! + (it "handles no options" + (patch-simple-test d*/patch-elements-seq! multi-elements multi-data)) + (it "handles selectors" + (patch-selector-test d*/patch-elements-seq! multi-elements multi-data)) + (it "handles patch modes" + (patch-mode-test d*/patch-elements-seq! multi-elements multi-data)) -;; ----------------------------------------------------------------------------- -;; Merge signals -;; ----------------------------------------------------------------------------- + (describe "handles view-transitions" + (specify "no view transition on false" + (patch-vt-false-test d*/patch-elements-seq! multi-elements multi-data)) + (specify "no view transition on non boolean value" + (patch-vt-non-bool-test d*/patch-elements-seq! multi-elements multi-data)) + (specify "view transition on true" + (patch-vt-true-test d*/patch-elements-seq! multi-elements multi-data))) -(def test-signals-content-1 "{\"a\":1,\"b\":2,\"c\":{\"d\":1}}") -(def test-signals-content-2 "{\"toto\":\"a\"}") + (it "handles all options" + (patch-all-options-test d*/patch-elements-seq! multi-elements multi-data)))) -(def test-signals-content - (string/join \newline [test-signals-content-1 test-signals-content-2])) -(def ->merge-signals-line (partial ->data-line consts/signals-dataline-literal)) -(def test-signals-lines - [(->merge-signals-line test-signals-content-1) - (->merge-signals-line test-signals-content-2)]) +(comment + (ltr/run-test-var #'test-patch-elements-seq!)) -(def only-if-missing-line - (->data-line consts/only-if-missing-dataline-literal true)) - - -(defmacro expect-merge-signals [sse-gen opts data-lines] - (ct/apply-template - '[sse-gen opts data-lines] - '(basic-expect sse-gen - d*/merge-signals! test-signals-content opts - merge-signals-t data-lines) - [sse-gen opts data-lines])) +;; ----------------------------------------------------------------------------- +;; remove-element! test definition +;; ----------------------------------------------------------------------------- +(def patch-mode-remove-line + (->data-line consts/mode-dataline-literal consts/element-patch-mode-remove)) -(defdescribe merge-signals - (let [gen (at/->sse-gen)] - (describe d*/merge-fragment! - - (it "works for a basic signal" - (expect-merge-signals gen {} test-signals-lines)) - +(defdescribe test-remove-element! + (describe d*/remove-element! + (it "produces a well formed event" + (expect (= (d*/remove-element! (at/->sse-gen) "#id") + (event patch-element-t [selector-line patch-mode-remove-line])))))) - (it "handles the only if missing option" - (expect-merge-signals gen {d*/only-if-missing true} - (list* only-if-missing-line test-signals-lines)) - (expect-merge-signals gen {d*/only-if-missing :true} - test-signals-lines) - (expect-merge-signals gen {d*/only-if-missing false} - test-signals-lines))))) +(comment + (ltr/run-test-var #'test-remove-element!)) ;; ----------------------------------------------------------------------------- -;; Remove signals +;; Merge signals ;; ----------------------------------------------------------------------------- -(def test-signal-paths ["foo.bar" "foo.baz" "bar"]) +(def test-signals-content "{\"a\":1,\"b\":2,\"c\":{\"d\":1}}") -(def ->remove-signals-line (partial ->data-line consts/paths-dataline-literal)) +(def patch-signals-lines + [(->data-line consts/signals-dataline-literal test-signals-content)]) -(def remove-signals-lines - [(->remove-signals-line "foo.bar") - (->remove-signals-line "foo.baz") - (->remove-signals-line "bar")]) +(def only-if-missing-line + (->data-line consts/only-if-missing-dataline-literal true)) -(defdescribe remove-signals! - (let [gen (at/->sse-gen)] - (describe d*/remove-signals! - (it "Throws on no paths" - (expect (lt/throws? clojure.lang.ExceptionInfo #(d*/remove-signals! gen [])))) +(defdescribe test-patch-signals! + (describe d*/patch-signals! + (it "works with no options" + (expect (= (d*/patch-signals! (at/->sse-gen) test-signals-content {}) + (event patch-signals-t patch-signals-lines)))) - (it "works for several paths" - (expect - (= (d*/remove-signals! gen test-signal-paths) - (event remove-signals-t remove-signals-lines))))))) + (it "adds only-if-missing line" + (expect (= (d*/patch-signals! (at/->sse-gen) test-signals-content {d*/only-if-missing true}) + (event patch-signals-t + (list* only-if-missing-line + patch-signals-lines))))))) +(comment + (ltr/run-test-var #'test-patch-signals!)) ;; ----------------------------------------------------------------------------- ;; Execute scripts ;; ----------------------------------------------------------------------------- -(def script-content - "console.log('hello')\nconsole.log('world!!!')") - -(def ->script-line (partial ->data-line consts/script-dataline-literal)) - -(def script-content-lines - [(->script-line "console.log('hello')") - (->script-line "console.log('world!!!')")]) - -(def test-attrs {:attr1 "val1" :attr2 "val2"}) - -(def ->attr-line (partial ->data-line consts/attributes-dataline-literal)) - -(def test-attrs-lines - [(->attr-line "attr1 val1") - (->attr-line "attr2 val2")]) - -(def auto-remove-line - (->data-line consts/auto-remove-dataline-literal false)) - - -(defmacro expect-execute-script [sse-gen opts data-lines] - (ct/apply-template - '[sse-gen opts data-lines] - '(basic-expect sse-gen - d*/execute-script! script-content opts - execute-script-t data-lines) - [sse-gen opts data-lines])) - - -(defdescribe execute-script - (let [gen (at/->sse-gen)] - (describe d*/execute-script! - - (it "works for a basic script" - (expect-execute-script gen {} script-content-lines)) - - (describe "the handling script attributes" - (it "doesn't add the default type attribute" - (expect-execute-script gen {d*/attributes {:type "module"}} - script-content-lines)) - - (it "adds attributes" - (expect-execute-script gen {d*/attributes test-attrs} - (concat test-attrs-lines - script-content-lines)))) - - (describe "handles the auto remove opt" - (it "auto removes on truthy values" - (expect-execute-script gen {d*/auto-remove :true} script-content-lines)) - - (it "auto remove on false value" - (expect-execute-script gen {d*/auto-remove nil} script-content-lines) - (expect-execute-script gen {d*/auto-remove false} (list* auto-remove-line script-content-lines)))) - - (it "handles all options at once" - (expect-execute-script gen {d*/attributes test-attrs - d*/auto-remove true} - (concat test-attrs-lines - script-content-lines)) +(def script-content "console.log('hello')") + +(defn script-event [script-tag] + (event patch-element-t + [(->data-line consts/selector-dataline-literal "body") + (->data-line consts/mode-dataline-literal consts/element-patch-mode-append) + (->data-line consts/elements-dataline-literal script-tag)])) + +(defdescribe test-execute-script! + (describe d*/execute-script! + (specify "auto-remove is the default behavior" + (expect (= (d*/execute-script! (at/->sse-gen) script-content) + (script-event "")))) + + (specify "we can disable auto-remove" + (expect (= (d*/execute-script! (at/->sse-gen) + script-content + {d*/auto-remove false}) + (script-event "")))) + + (specify "we can disable auto-remove and add attributes" + (expect (= (d*/execute-script! (at/->sse-gen) + script-content + {d*/auto-remove false + d*/attributes {:type "module"}}) + (script-event "")))) + + (specify "we can add attributes auto-remove is there" + (expect (= (d*/execute-script! (at/->sse-gen) + script-content + {d*/attributes {:type "module" :data-something 1}}) + (d*/execute-script! (at/->sse-gen) + script-content + {d*/auto-remove true + d*/attributes {:type "module" :data-something 1}}) + (script-event "")))))) + +(comment + (ltr/run-test-var #'test-execute-script!)) - (expect-execute-script gen {d*/attributes test-attrs - d*/auto-remove false} - (concat test-attrs-lines - [auto-remove-line] - script-content-lines)))))) diff --git a/sdk/clojure/src/test/malli-schemas/test/api_schemas_test.clj b/sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj similarity index 60% rename from sdk/clojure/src/test/malli-schemas/test/api_schemas_test.clj rename to sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj index 20f12dd82..69d5bb35c 100644 --- a/sdk/clojure/src/test/malli-schemas/test/api_schemas_test.clj +++ b/sdk/clojure/src/test/malli-schemas/starfederation/datastar/clojure/api_schemas_test.clj @@ -1,13 +1,11 @@ -(ns test.api-schemas-test +(ns starfederation.datastar.clojure.api-schemas-test (:require [lazytest.core :as lt :refer [defdescribe describe expect it]] [malli.instrument :as mi] [starfederation.datastar.clojure.adapter.test :as at] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api-schemas] - [starfederation.datastar.clojure.api.fragments :as frags]) - (:import - clojure.lang.ExceptionInfo)) + [starfederation.datastar.clojure.api.elements :as elements] + [starfederation.datastar.clojure.api-schemas])) (def with-malli @@ -39,17 +37,18 @@ (def dumy-script "console.log('hello')") -#_{:clj-kondo/ignore true} + (def thunk-wrong-script-type #(d*/execute-script! sse-gen :test)) (def thunk-wrong-option-type #(d*/execute-script! sse-gen dumy-script {d*/auto-remove :test})) -(defdescribe malli-schemas +(defdescribe test-malli-schemas (describe "without malli" (it "error can go through" - (expect (lt/throws? ExceptionInfo thunk-wrong-script-type)) - (expect (= (d*/execute-script! sse-gen dumy-script {d*/auto-remove :wrong-type}) - "event: datastar-execute-script\ndata: script console.log('hello')\n\n\n")))) + (expect (= (thunk-wrong-script-type) + "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")) + (expect (= (thunk-wrong-option-type) + "event: datastar-patch-elements\ndata: selector body\ndata: mode append\ndata: elements \n\n")))) (describe "with malli" {:context [with-malli]} @@ -61,12 +60,10 @@ (describe "Schemas not required" (it "doesn't trigger instrumentation" - (expect (= (frags/->merge-fragment "" {d*/retry-duration :test}) + (expect (= (elements/->patch-elements "" {d*/retry-duration :test}) []))))) - - - - - +(comment + (require '[lazytest.repl :as ltr]) + (ltr/run-test-var #'test-malli-schemas)) From 19f2f04b15ab01626caa1044d895b8e06b364101 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Wed, 2 Jul 2025 18:31:21 +0200 Subject: [PATCH 2/7] Feature: clojure-SDK Brotli support as a library --- sdk/clojure/CHANGELOG.md | 3 + sdk/clojure/bb.edn | 8 +- sdk/clojure/deps.edn | 20 +-- sdk/clojure/sdk-brotli/README.md | 22 +++ sdk/clojure/sdk-brotli/deps.edn | 3 + .../datastar/clojure/brotli.clj | 136 ++++++++++++++++++ sdk/clojure/src/bb/tasks.clj | 3 +- .../src/dev/examples/animation_gzip.clj | 6 +- .../dev/examples/animation_gzip/brotli.clj | 48 ------- .../datastar/clojure/brotli_test.clj | 87 +++++++++++ .../datastar/clojure/adapter/common_test.clj | 2 +- 11 files changed, 275 insertions(+), 63 deletions(-) create mode 100644 sdk/clojure/sdk-brotli/README.md create mode 100644 sdk/clojure/sdk-brotli/deps.edn create mode 100644 sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj delete mode 100644 sdk/clojure/src/dev/examples/animation_gzip/brotli.clj create mode 100644 sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md index c6ba9eb35..afebd7f0d 100644 --- a/sdk/clojure/CHANGELOG.md +++ b/sdk/clojure/CHANGELOG.md @@ -24,6 +24,9 @@ allows for other projects to use `starfederation.datastar.clojure/XXXX/config.edn` for their clj-kondo config. +### Added + +- A new library providing Brotli write profile has been added. ## 2025-04-07 ### Added diff --git a/sdk/clojure/bb.edn b/sdk/clojure/bb.edn index 981472388..9f2e960b7 100644 --- a/sdk/clojure/bb.edn +++ b/sdk/clojure/bb.edn @@ -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] diff --git a/sdk/clojure/deps.edn b/sdk/clojure/deps.edn index 9536fbf18..a72ee3870 100644 --- a/sdk/clojure/deps.edn +++ b/sdk/clojure/deps.edn @@ -1,11 +1,10 @@ {:paths [] - :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"} - 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"] @@ -28,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 @@ -47,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"] diff --git a/sdk/clojure/sdk-brotli/README.md b/sdk/clojure/sdk-brotli/README.md new file mode 100644 index 000000000..89b560e8d --- /dev/null +++ b/sdk/clojure/sdk-brotli/README.md @@ -0,0 +1,22 @@ +# Datastar Brotli write profile + +This library contains some utilities to work with Brotli. + +Credits to [Anders]'(https://andersmurphy.com/) and his work on [Hyperlith](https://github.com/andersmurphy/hyperlith) +from which this library takes it's code. + +## Installation + +```clojure +{datastar/brotli {:git/url "https://github.com/starfederation/datastar/" + :git/sha "LATEST SHA" + :deps/root "sdk/clojure/sdk-brotli"} + +``` + +> [!important] +> +> - Replace `LATEST_SHA` in the git coordinates below by the actual latest +> commit sha of the repository. +> - You need to add a dependency for your specific plateform see the [Brotli4j page](https://github.com/hyperxpro/Brotli4j) +> For instance on linux `com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}}` diff --git a/sdk/clojure/sdk-brotli/deps.edn b/sdk/clojure/sdk-brotli/deps.edn new file mode 100644 index 000000000..dc1514b2b --- /dev/null +++ b/sdk/clojure/sdk-brotli/deps.edn @@ -0,0 +1,3 @@ +{:paths ["src/main"] + :deps {com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"} + io.netty/netty-buffer {:mvn/version "4.1.119.Final"}}} diff --git a/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj b/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj new file mode 100644 index 000000000..91249a8e1 --- /dev/null +++ b/sdk/clojure/sdk-brotli/src/main/starfederation/datastar/clojure/brotli.clj @@ -0,0 +1,136 @@ +(ns starfederation.datastar.clojure.brotli + "Tools to work with Brotli. + + The main api is + - [[compress]] + - [[decompress]] + - [[->brotli-profile]] + - [[->brotli-buffered-writer-profile]] + " + (:require + [clojure.math :as m] + [starfederation.datastar.clojure.adapter.common :as ac]) + (:import + com.aayushatharva.brotli4j.Brotli4jLoader + [com.aayushatharva.brotli4j.encoder + Encoder + Encoder$Parameters + Encoder$Mode + BrotliOutputStream] + [com.aayushatharva.brotli4j.decoder Decoder] + [java.io OutputStream])) + + +;; Code taken from https://github.com/andersmurphy/hyperlith +;; Thanks Anders! +;; ----------------------------------------------------------------------------- +;; Setup & helpers +;; ----------------------------------------------------------------------------- +(defonce ensure-br + (Brotli4jLoader/ensureAvailability)) + + +(defn window-size->kb [window-size] + (/ (- (m/pow 2 window-size) 16) 1000)) + + +(defn encoder-params + "Options used when creating a brotli encoder. + + Arg keys: + - `:quality`: Brotli quality defaults to 5 + - `:window-size`: Brotli window size defaults to 24 + " + [{:keys [quality window-size]}] + (doto (Encoder$Parameters/new) + (.setMode Encoder$Mode/TEXT) + ;; LZ77 window size (0, 10-24) (default: 24) + ;; window size is (pow(2, NUM) - 16) + (.setWindow (or window-size 24)) + (.setQuality (or quality 5)))) + + +;; ----------------------------------------------------------------------------- +;; 1 shot compression +;; ----------------------------------------------------------------------------- +(defn compress + " + Compress `data` (either a byte array or a string) using Brotli. + + Opts keys from [[encoder-params]]: + - `:quality`: Brotli quality + - `:window-size`: Brotli window size + " + + [data & {:as opts}] + (-> (if (string? data) + (String/.getBytes data) + ^byte/1 data) + (Encoder/compress (encoder-params opts)))) + + +(defn decompress + "Decompress Brotli compressed data, returns a string." + [data] + (let [decompressed (Decoder/decompress data)] + (String/new (.getDecompressedData decompressed)))) + + +(comment + (decompress (compress "hello"))) + + +;; ----------------------------------------------------------------------------- +;; Write profiles +;; ----------------------------------------------------------------------------- +(defn ->brotli-os + "Wrap `out-stream` with Brotli compression. + + Opts from [[encoder-params]]: + - `:quality`: Brotli quality + - `:window-size`: Brotli window size + " + [^OutputStream out-stream & {:as opts}] + (BrotliOutputStream/new out-stream (encoder-params opts))) + + +(def brotli-content-encoding "br") + + +(defn ->brotli-profile + "Make a write profile using Brotli compression and a temporary buffer + strategy. + + Opts from [[encoder-params]]: + - `:quality`: Brotli quality + - `:window-size`: Brotli window size + " + [& {:as opts}] + {ac/wrap-output-stream + (fn [^OutputStream os] + (-> os + (->brotli-os opts) + ac/->os-writer)) + ac/write! (ac/->write-with-temp-buffer!) + ac/content-encoding brotli-content-encoding}) + + +(defn ->brotli-buffered-writer-profile + "Make a write profile using Brotli compression and a permanent buffer + strategy. + + Opts from [[encoder-params]]: + - `:quality`: Brotli quality + - `:window-size`: Brotli window size + " + [& {:as opts}] + {ac/wrap-output-stream + (fn [^OutputStream os] + (-> os + (->brotli-os opts) + ac/->os-writer + ac/->buffered-writer)) + ac/write! ac/write-to-buffered-writer! + ac/content-encoding brotli-content-encoding}) + + diff --git a/sdk/clojure/src/bb/tasks.clj b/sdk/clojure/src/bb/tasks.clj index 5ae875100..42484c1c9 100644 --- a/sdk/clojure/src/bb/tasks.clj +++ b/sdk/clojure/src/bb/tasks.clj @@ -21,7 +21,8 @@ (def dev-aliases [:test :repl - :malli-schemas]) + :malli-schemas + :sdk-brotli]) (defn arg->kw [s] (if (string/starts-with? s ":") diff --git a/sdk/clojure/src/dev/examples/animation_gzip.clj b/sdk/clojure/src/dev/examples/animation_gzip.clj index d16f76cb8..92ae3617a 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip.clj @@ -4,7 +4,6 @@ [examples.animation-gzip.handlers :as handlers] [examples.animation-gzip.rendering :as rendering] [examples.animation-gzip.state :as state] - [examples.animation-gzip.brotli :as brotli] [examples.common :as c] [examples.utils :as u] [reitit.ring :as rr] @@ -15,7 +14,8 @@ [starfederation.datastar.clojure.adapter.ring :as ring-gen] [starfederation.datastar.clojure.adapter.ring-schemas] [starfederation.datastar.clojure.api :as d*] - [starfederation.datastar.clojure.api-schemas])) + [starfederation.datastar.clojure.api-schemas] + [starfederation.datastar.clojure.brotli :as brotli])) ;; This example let's use play with fat updates and compression ;; to get an idea of the gains compression can help use achieve @@ -71,7 +71,7 @@ (def handler-http-kit (->handler hk-gen/->sse-response - {hk-gen/write-profile hk-gen/gzip-profile})) + {hk-gen/write-profile (brotli/->brotli-profile)})) (def handler-ring (->handler ring-gen/->sse-response {ring-gen/write-profile ring-gen/gzip-profile})) diff --git a/sdk/clojure/src/dev/examples/animation_gzip/brotli.clj b/sdk/clojure/src/dev/examples/animation_gzip/brotli.clj deleted file mode 100644 index 54d731b54..000000000 --- a/sdk/clojure/src/dev/examples/animation_gzip/brotli.clj +++ /dev/null @@ -1,48 +0,0 @@ -(ns examples.animation-gzip.brotli - (:require - [starfederation.datastar.clojure.adapter.common :as ac]) - (:import - com.aayushatharva.brotli4j.Brotli4jLoader - [com.aayushatharva.brotli4j.encoder Encoder$Parameters - Encoder$Mode BrotliOutputStream] - [java.io OutputStream])) - -;; This code is adapted from https://github.com/andersmurphy/hyperlith/blob/master/src/hyperlith/impl/brotli.clj -;; Thank you Anders! - -#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(defonce ensure-br - (Brotli4jLoader/ensureAvailability)) - - -(defn encoder-params [{:keys [quality window-size]}] - (let [encoder-params (Encoder$Parameters/new)] - (.setMode encoder-params Encoder$Mode/TEXT) - (when window-size - (.setWindow encoder-params window-size)) - (when quality - (.setQuality encoder-params quality)) - encoder-params)) - - -(defn ->brotli-os - ([^OutputStream out-stream & {:as opts}] - (BrotliOutputStream/new out-stream (encoder-params opts)))) - -(def brotli-content-encoding "br") - -#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(def brotli-profile - {ac/wrap-output-stream (fn [^OutputStream os] - (-> os ->brotli-os ac/->os-writer)) - ac/write! (ac/->write-with-temp-buffer!) - ac/content-encoding brotli-content-encoding}) - - -#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(def brotli-buffered-writer-profile - {ac/wrap-output-stream (fn [^OutputStream os] - (-> os ->brotli-os ac/->os-writer ac/->buffered-writer)) - ac/write! ac/write-to-buffered-writer! - ac/content-encoding brotli-content-encoding}) - diff --git a/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj b/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj new file mode 100644 index 000000000..0cc88cec4 --- /dev/null +++ b/sdk/clojure/src/test/brotli/starfederation/datastar/clojure/brotli_test.clj @@ -0,0 +1,87 @@ +(ns starfederation.datastar.clojure.brotli-test + (:require + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.adapter.test :as at] + [starfederation.datastar.clojure.adapter.common-test :as ct] + [starfederation.datastar.clojure.brotli :as brotli] + [lazytest.core :as lt :refer [defdescribe describe specify expect]]) + (:import + [java.io InputStream ByteArrayOutputStream + ByteArrayInputStream InputStreamReader BufferedReader] + [java.nio.charset StandardCharsets])) + + + +(defn ->input-stream-reader [^InputStream is] + (InputStreamReader. is StandardCharsets/UTF_8)) + + +(defn ->ba [v] + (cond + (bytes? v) + v + + (instance? ByteArrayOutputStream v) + (.toByteArray ^ByteArrayOutputStream v))) + + +(defn read-bytes [ba opts] + (if (:brotli opts) + (brotli/decompress ba) + (-> ba + ->ba + (ByteArrayInputStream.) + (->input-stream-reader) + (BufferedReader.) + (slurp)))) + +(defdescribe reading-bytes + (specify "We can do str -> bytes -> str" + (let [original (str (d*/patch-elements! (at/->sse-gen) "msg"))] + (expect + (= original + (-> original + (.getBytes) + (read-bytes {}))))))) + +;; ----------------------------------------------------------------------------- +;; Tests +;; ----------------------------------------------------------------------------- +(defn simple-round-trip [write-profile] + (let [!res (atom nil) + machinery (ct/->machinery write-profile) + baos (ct/get-baos machinery)] + (with-open [_baos baos + writer (ct/get-writer machinery)] + (ct/append-then-flush writer "some text")) + (reset! !res (-> baos .toByteArray (read-bytes write-profile))) + (expect (= @!res "some text")))) + + + +(defdescribe brotli + (describe "Writing of text with compression" + (specify "We can do a simple round trip" + (simple-round-trip (assoc (brotli/->brotli-profile) + :brotli true))) + + (specify "We can compress several messages" + (let [machinery (ct/->machinery (brotli/->brotli-profile)) + baos (ct/get-baos machinery) + !res (atom [])] + (with-open [writer (ct/get-writer machinery)] + (ct/append-then-flush writer "some text") + (ct/append-then-flush writer "some other text")) + (reset! !res (-> baos .toByteArray (read-bytes {:brotli true}))) + (expect (= @!res "some textsome other text")))))) + + + +(comment + (require '[lazytest.repl :as ltr]) + (ltr/run-test-var #'reading-bytes) + (ltr/run-test-var #'brotli)) + + + + diff --git a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj index f285d3068..1a7a8ed33 100644 --- a/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj +++ b/sdk/clojure/src/test/core-sdk/starfederation/datastar/clojure/adapter/common_test.clj @@ -52,7 +52,7 @@ ;; ----------------------------------------------------------------------------- (defn ->machinery [write-profile] (let [^ByteArrayOutputStream baos (ByteArrayOutputStream.) - {write! ac/write-to-buffered-writer! + {write! ac/write! wrap ac/wrap-output-stream} write-profile writer (wrap baos)] {:write! write! From c367324543fa6dcb30c3ad7d7ef89566d9bd4138 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Wed, 2 Jul 2025 19:08:52 +0200 Subject: [PATCH 3/7] Feature: clojure-SDK alternative http-kit api --- sdk/clojure/CHANGELOG.md | 12 ++ .../datastar/clojure/adapter/http_kit2.clj | 151 ++++++++++++++++++ .../src/dev/examples/animation_gzip.clj | 25 +-- .../dev/examples/animation_gzip/broadcast.clj | 27 ++++ .../src/dev/examples/http_kit2/animation.clj | 94 +++++++++++ .../dev/examples/http_kit2/form_behavior.clj | 138 ++++++++++++++++ .../src/test/adapter-common/test/common.clj | 5 +- .../test/examples/http_kit_handler2.clj | 66 ++++++++ .../adapter-http-kit/test/http_kit2_test.clj | 74 +++++++++ 9 files changed, 567 insertions(+), 25 deletions(-) create mode 100644 sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj create mode 100644 sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj create mode 100644 sdk/clojure/src/dev/examples/http_kit2/animation.clj create mode 100644 sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj create mode 100644 sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj create mode 100644 sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md index afebd7f0d..18b5c0735 100644 --- a/sdk/clojure/CHANGELOG.md +++ b/sdk/clojure/CHANGELOG.md @@ -26,7 +26,19 @@ ### 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 diff --git a/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj b/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj new file mode 100644 index 000000000..1ac84e75a --- /dev/null +++ b/sdk/clojure/adapter-http-kit/src/main/starfederation/datastar/clojure/adapter/http_kit2.clj @@ -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))}) + diff --git a/sdk/clojure/src/dev/examples/animation_gzip.clj b/sdk/clojure/src/dev/examples/animation_gzip.clj index 92ae3617a..c8f355cd4 100644 --- a/sdk/clojure/src/dev/examples/animation_gzip.clj +++ b/sdk/clojure/src/dev/examples/animation_gzip.clj @@ -1,6 +1,6 @@ (ns examples.animation-gzip (:require - [dev.onionpancakes.chassis.core :as h] + [examples.animation-gzip.broadcast :as broadcast] [examples.animation-gzip.handlers :as handlers] [examples.animation-gzip.rendering :as rendering] [examples.animation-gzip.state :as state] @@ -20,28 +20,7 @@ ;; This example let's use play with fat updates and compression ;; to get an idea of the gains compression can help use achieve ;; in terms of network usage. - -(defn send-frame! [sse frame] - (try - (d*/patch-elements! sse frame) - (catch Exception e - (println e)))) - - -(defn broadcast-new-frame! [frame] - (let [sses @state/!conns] - (doseq [sse sses] - (send-frame! sse frame)))) - - -(defn install-watch! [] - (add-watch state/!state ::watch - (fn [_k _ref old new] - (when-not (identical? old new) - (let [frame (rendering/render-content new)] - (broadcast-new-frame! frame)))))) - -(install-watch!) +(broadcast/install-watch!) (defn ->routes [->sse-response opts] diff --git a/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj b/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj new file mode 100644 index 000000000..94af16359 --- /dev/null +++ b/sdk/clojure/src/dev/examples/animation_gzip/broadcast.clj @@ -0,0 +1,27 @@ +(ns examples.animation-gzip.broadcast + (:require + [examples.animation-gzip.rendering :as rendering] + [examples.animation-gzip.state :as state] + [starfederation.datastar.clojure.api :as d*])) + + +(defn send-frame! [sse frame] + (try + (d*/patch-elements! sse frame) + (catch Exception e + (println e)))) + + +(defn broadcast-new-frame! [frame] + (let [sses @state/!conns] + (doseq [sse sses] + (send-frame! sse frame)))) + + +(defn install-watch! [] + (add-watch state/!state ::watch + (fn [_k _ref old new] + (when-not (identical? old new) + (let [frame (rendering/render-content new)] + (broadcast-new-frame! frame)))))) + diff --git a/sdk/clojure/src/dev/examples/http_kit2/animation.clj b/sdk/clojure/src/dev/examples/http_kit2/animation.clj new file mode 100644 index 000000000..4ba04432a --- /dev/null +++ b/sdk/clojure/src/dev/examples/http_kit2/animation.clj @@ -0,0 +1,94 @@ +(ns examples.http-kit2.animation + (:require + [examples.animation-gzip.broadcast :as broadcast] + [examples.animation-gzip.handlers :as handlers] + [examples.animation-gzip.rendering :as rendering] + [examples.animation-gzip.state :as state] + [examples.common :as c] + [examples.utils :as u] + [reitit.ring :as rr] + [reitit.ring.middleware.exception :as reitit-exception] + [reitit.ring.middleware.parameters :as reitit-params] + [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen] + [starfederation.datastar.clojure.adapter.http-kit-schemas] + [starfederation.datastar.clojure.adapter.ring :as ring-gen] + [starfederation.datastar.clojure.adapter.ring-schemas] + [starfederation.datastar.clojure.api-schemas] + [starfederation.datastar.clojure.brotli :as brotli])) + +;; This example let's use play with fat updates and compression +;; to get an idea of the gains compression can help use achieve +;; in terms of network usage. + +(broadcast/install-watch!) + + +(defn ->routes [->sse-response opts] + [["/" handlers/home-handler] + ["/ping/:id" {:handler handlers/ping-handler + :middleware [reitit-params/parameters-middleware]}] + ["/random-10" handlers/random-pings-handler] + ["/reset" handlers/reset-handler] + ["/step1" handlers/step-handler] + ["/play" handlers/play-handler] + ["/pause" handlers/pause-handler] + ["/updates" {:handler (handlers/->updates-handler ->sse-response opts) + :middleware [[hk-gen/start-responding-middleware]]}] + ["/refresh" handlers/refresh-handler] + ["/resize" handlers/resize-handler] + c/datastar-route]) + + +(defn ->router [->sse-handler opts] + (rr/router (->routes ->sse-handler opts))) + + +(defn ->handler [->sse-response & {:as opts}] + (rr/ring-handler + (->router ->sse-response opts) + (rr/create-default-handler) + {:middleware [reitit-exception/exception-middleware]})) + + +(def handler-http-kit (->handler hk-gen/->sse-response + {hk-gen/write-profile (brotli/->brotli-profile)})) + +(def handler-ring (->handler ring-gen/->sse-response + {ring-gen/write-profile ring-gen/gzip-profile})) + +(defn after-ns-reload [] + (println "rebooting servers") + (u/reboot-hk-server! #'handler-http-kit) + (u/reboot-jetty-server! #'handler-ring {:async? true})) + + +(comment + #_{:clj-kondo/ignore true} + (user/reload!) + :help + :dbg + :rec + :stop + *e + state/!state + state/!conns + (reset! state/!conns #{}) + + (-> state/!state + deref + rendering/page) + (state/resize! 10 10) + (state/resize! 20 20) + (state/resize! 25 25) + (state/resize! 30 30) + (state/resize! 50 50) + (state/reset-state!) + (state/add-random-pings!) + (state/step-state!) + (state/start-animating!) + (u/clear-terminal!) + (u/reboot-hk-server! #'handler-http-kit) + (u/reboot-jetty-server! #'handler-ring {:async? true})) + + + diff --git a/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj b/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj new file mode 100644 index 000000000..6f708ed26 --- /dev/null +++ b/sdk/clojure/src/dev/examples/http_kit2/form_behavior.clj @@ -0,0 +1,138 @@ +(ns examples.http-kit2.form-behavior + (:require + [clojure.java.io :as io] + [examples.common :as c] + [dev.onionpancakes.chassis.core :as h] + [dev.onionpancakes.chassis.compiler :as hc] + [examples.utils :as u] + [ring.util.response :as rur] + [reitit.ring :as rr] + [reitit.ring.middleware.parameters :as params] + [starfederation.datastar.clojure.adapter.http-kit2 :refer [->sse-response on-open start-responding-middleware]] + [starfederation.datastar.clojure.api :as d*])) + + +;; Trying out several way we might rightly and wrongly use html forms +;; TODO: still need to figure out some things + +(defn result-area [value] + (hc/compile + [:div {:id "form-result"} + [:span "From signals: " [:span {:data-text "$input1"}]] + [:br] + [:span "from backend: " [:span (str value "- " (random-uuid))]]])) + +(def style (slurp (io/resource "examples/form_behavior/style.css"))) + +(def page + (h/html + (c/page-scaffold + [:div + [:style style] + [:h2 "Form page"] + [:form {:action ""} + [:h3 "D* post form"] + [:label {:for "input1"} "Enter text"] + [:br] + + [:input {:type "text" + :id "input1" + :name "input-1" + :data-bind-input1 true}] + + + [:br] + (result-area "") + + [:h3 "Inside form"] + [:div#buttons + ;; GET + [:div + [:h4 "GET"] + [:button { :data-on-click "@get('/endpoint')"} "get - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@get('/endpoint')"} "get - no ct - type"][:br] + [:button { :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct - no type "][:br] + [:button {:type "button" :data-on-click "@get('/endpoint', {contentType: 'form'})"} "get - ct -type"][:br]] + + ;; POST + [:div + [:h4 "POST"] + [:button { :data-on-click "@post('/endpoint')"} "post - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@post('/endpoint')"} "post - no ct - type"][:br] + [:button { :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct - no type "][:br] + [:button {:type "button" :data-on-click "@post('/endpoint', {contentType: 'form'})"} "post - ct -type"][:br]] + + ;; PUT + [:div + [:h4 "PUT"] + [:button { :data-on-click "@put('/endpoint')"} "put - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@put('/endpoint')"} "put - no ct - type"][:br] + [:button { :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct - no type "][:br] + [:button {:type "button" :data-on-click "@put('/endpoint', {contentType: 'form'})"} "put - ct -type"][:br]] + + ;; PATCH + [:div + [:h4 "PATCH"] + [:button { :data-on-click "@patch('/endpoint')"} "patch - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@patch('/endpoint')"} "patch - no ct - type"][:br] + [:button { :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct - no type "][:br] + [:button {:type "button" :data-on-click "@patch('/endpoint', {contentType: 'form'})"} "patch - ct -type"][:br]] + + ;; DELETE + [:div + [:h4 "DELETE"] + [:button { :data-on-click "@delete('/endpoint')"} "delete - no ct - no type"][:br] + [:button {:type "button" :data-on-click "@delete('/endpoint')"} "delete - no ct - type"][:br] + [:button { :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct - no type "][:br] + [:button {:type "button" :data-on-click "@delete('/endpoint', {contentType: 'form'})"} "delete - ct -type"][:br]]]] + [:h3 "Outside form "] + [:button + {:id "get-no-form", :data-on-click "@get('/endpoint')"} + "get no form"] + [:button + {:id "post-no-form", :data-on-click "@post('/endpoint')"} + "post no form"] + + [:br]]))) + + +(defn form + ([_] + (rur/response page)) + ([req respond _] + (respond (form req)))) + + +(defn process-form + ([request] + (let [value (or (some->> (get-in request [:params "input-1"]) + (str "Form value - ")) + (some-> request + u/get-signals + (get "input1") + (->> (str "Signal value - "))))] + (->sse-response request + {on-open + (fn [sse-gen] + (d*/with-open-sse sse-gen + (d*/patch-elements! sse-gen (h/html (result-area value)))))}))) + ([request respond _raise] + (respond (process-form request)))) + + +(def router + (rr/router + [["/" {:handler form}] + ["/endpoint" {:handler process-form + :middleware [start-responding-middleware]}] + c/datastar-route])) + + +(def handler + (rr/ring-handler router + (rr/create-default-handler) + {:middleware [params/parameters-middleware]})) + + +(comment + (u/reboot-hk-server! #'handler)) diff --git a/sdk/clojure/src/test/adapter-common/test/common.clj b/sdk/clojure/src/test/adapter-common/test/common.clj index 09cd20c14..8700c0d5f 100644 --- a/sdk/clojure/src/test/adapter-common/test/common.clj +++ b/sdk/clojure/src/test/adapter-common/test/common.clj @@ -174,8 +174,9 @@ needed to run the test (see [[setup-persistent-see-state]])." [->sse-response server-opts] (lt/around [f] - (let [{:keys [get-port]} server-opts - {:keys [!conn latch handler]} (setup-persistent-see-state ->sse-response)] + (let [{:keys [get-port wrap]} server-opts + {:keys [!conn latch handler]} (setup-persistent-see-state ->sse-response) + handler (cond-> handler wrap wrap)] (u/with-server server handler (dissoc server-opts :get-port) (binding [*ctx* {:port (get-port server) :!conn !conn}] diff --git a/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj new file mode 100644 index 000000000..377404be6 --- /dev/null +++ b/sdk/clojure/src/test/adapter-http-kit/test/examples/http_kit_handler2.clj @@ -0,0 +1,66 @@ +(ns test.examples.http-kit-handler2 + (:require + [test.examples.common :as common] + [test.examples.counter :as counters] + [test.examples.form :as form] + [reitit.ring :as rr] + [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) + + +;; ----------------------------------------------------------------------------- +;; Counters +;; ----------------------------------------------------------------------------- +(def update-signal (counters/->update-signal hk-gen/->sse-response)) + + +(defn increment + ([req] + (update-signal req inc)) + ([req respond _raise] + (respond (increment req)))) + + +(defn decrement + ([req] + (update-signal req dec)) + ([req respond _raise] + (respond (decrement req)))) + + +(def counter-routes + ["/counters/" + ["" {:handler #'counters/counters}] + ["increment/:id" #'increment] + ["decrement/:id" #'decrement]]) + + + +;; ----------------------------------------------------------------------------- +;; Form +;; ----------------------------------------------------------------------------- +(def endpoint (form/->endpoint hk-gen/->sse-response)) + + +(def form-routes + ["/form" + ["" {:handler #'form/form}] + ["/endpoint" {:middleware [common/wrap-mpparams] + :handler #'endpoint}]]) + + + + +(def router + (rr/router + [common/datastar-route + counter-routes + form-routes])) + + +(def handler + (rr/ring-handler router + common/default-handler + {:middleware (into [[hk-gen/start-responding-middleware]] common/global-middleware)})) + + + diff --git a/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj b/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj new file mode 100644 index 000000000..744778e9e --- /dev/null +++ b/sdk/clojure/src/test/adapter-http-kit/test/http_kit2_test.clj @@ -0,0 +1,74 @@ +(ns test.http-kit2-test + (:require + [test.common :as common] + [test.examples.http-kit-handler2 :as hkh] + [lazytest.core :as lt :refer [defdescribe expect it]] + [org.httpkit.server :as hk-server] + [starfederation.datastar.clojure.adapter.http-kit2 :as hk-gen])) + +;; ----------------------------------------------------------------------------- +;; HTTP-Kit stuff +;; ----------------------------------------------------------------------------- +(def http-kit-basic-opts + {:start! hk-server/run-server + :stop! hk-server/server-stop! + :get-port hk-server/server-port + :legacy-return-value? false}) + + +;; ----------------------------------------------------------------------------- +(defdescribe counters-test + {:webdriver true + :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} + (it "manages signals" + (doseq [[driver-type driver] common/drivers] + (let [res (common/run-counters! @driver)] + (expect (= res common/expected-counters) (str driver-type)))))) + + +(defdescribe counters-test-async + {:webdriver true + :context [(common/with-server-f hkh/handler (assoc http-kit-basic-opts + :ring-async? true))]} + (it "manages signals" + (doseq [[driver-type driver] common/drivers] + (let [res (common/run-counters! @driver)] + (expect (= res common/expected-counters) (str driver-type)))))) + +;; ----------------------------------------------------------------------------- +;; Tests +;; ----------------------------------------------------------------------------- +(defdescribe form-test + {:webdriver true + :context [(common/with-server-f hkh/handler http-kit-basic-opts)]} + (it "manages forms" + (doseq [[driver-type driver] common/drivers] + (let [res (common/run-form-test! @driver)] + (expect (= res common/expected-form-vals) (str driver-type)))))) + + +;; ----------------------------------------------------------------------------- +(defdescribe persistent-sse-test + {:context [(common/persistent-sse-f hk-gen/->sse-response + (assoc http-kit-basic-opts + :wrap hk-gen/wrap-start-responding))]} + (it "handles persistent connections" + (let [res (common/run-persistent-sse-test!)] + (expect (map? res)) + (expect (common/p-sse-status-ok? res)) + (expect (common/p-sse-http1-headers-ok? res)) + (expect (common/p-sse-body-ok? res))))) + + +(defdescribe persistent-sse-test-async + {:context [(common/persistent-sse-f hk-gen/->sse-response + (assoc http-kit-basic-opts + :wrap hk-gen/wrap-start-responding + :ring-async? true))]} + (it "handles persistent connections" + (let [res (common/run-persistent-sse-test!)] + (expect (map? res)) + (common/p-sse-status-ok? res) + (common/p-sse-http1-headers-ok? res) + (common/p-sse-body-ok? res)))) + From d30ce7ec9264dbf10a7322a109b53dfc02d39ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Thu, 3 Jul 2025 15:10:37 +0200 Subject: [PATCH 4/7] Fix: clojure-SDK forgotten changelog info --- sdk/clojure/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/clojure/CHANGELOG.md b/sdk/clojure/CHANGELOG.md index 18b5c0735..9e82851a3 100644 --- a/sdk/clojure/CHANGELOG.md +++ b/sdk/clojure/CHANGELOG.md @@ -19,6 +19,7 @@ ### 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 From f6a3f5576a2faaa91ce175bdb447566163dbc617 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Fri, 4 Jul 2025 19:38:19 +0200 Subject: [PATCH 5/7] Fix: forgotten malli schema for the new http-kit api. --- .../clojure/adapter/http_kit2_schemas.clj | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj diff --git a/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj new file mode 100644 index 000000000..3ff6ad993 --- /dev/null +++ b/sdk/clojure/malli-schemas/src/main/starfederation/datastar/clojure/adapter/http_kit2_schemas.clj @@ -0,0 +1,19 @@ +(ns starfederation.datastar.clojure.adapter.http-kit2-schemas + (:require + [malli.core :as m] + [malli.util :as mu] + [starfederation.datastar.clojure.adapter.common :as ac] + [starfederation.datastar.clojure.adapter.http-kit2] + [starfederation.datastar.clojure.adapter.common-schemas :as cs])) + + +(def options-schema + (mu/update-in cs/->sse-response-options-schema + [ac/write-profile] + (fn [x] + (mu/optional-keys x [ac/wrap-output-stream])))) + + +(m/=> starfederation.datastar.clojure.adapter.http-kit2/->sse-response + [:-> :map options-schema :any]) + From 59740d5da73a0c8a760ed2b92aa1fa5f067ce34a Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Mon, 7 Jul 2025 14:50:56 +0200 Subject: [PATCH 6/7] Doc: Brotli README --- sdk/clojure/sdk-brotli/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/sdk/clojure/sdk-brotli/README.md b/sdk/clojure/sdk-brotli/README.md index 89b560e8d..c7e2f72fd 100644 --- a/sdk/clojure/sdk-brotli/README.md +++ b/sdk/clojure/sdk-brotli/README.md @@ -20,3 +20,29 @@ from which this library takes it's code. > commit sha of the repository. > - You need to add a dependency for your specific plateform see the [Brotli4j page](https://github.com/hyperxpro/Brotli4j) > For instance on linux `com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"}}}` + +## Supported ring adapters + +At this moment only Http-kit is supported. + +## Usage + +This library provides brotil write profiles you can use like this: + +```clojure +(require + '[starfederation.datastar.clojure.api :as d*]) + '[starfederation.datastar.clojure.adapter.Http-kit :as hk-gen]) + '[starfederation.datastar.clojure.brotli :as brotli])) + +(defn handler [req] + (hk-gen/->sse-response + {hk-gen/write-profile (brotli/->brotli-profile) + hk-gen/on-open + (fn [sse] + (d*/with-open-sse sse + (do ...)}) +``` + +See docstrings in the `starfederation.datastar.clojure.brotli` namespace for +more information. From 57b82e5d18803a21d80aecaf5d65f1cb6b72b5b9 Mon Sep 17 00:00:00 2001 From: Jeremy Schoffen Date: Thu, 10 Jul 2025 17:01:06 +0200 Subject: [PATCH 7/7] Fix: errors in test snippets --- sdk/clojure/src/dev/examples/snippets.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/clojure/src/dev/examples/snippets.clj b/sdk/clojure/src/dev/examples/snippets.clj index e72566235..98fab68d9 100644 --- a/sdk/clojure/src/dev/examples/snippets.clj +++ b/sdk/clojure/src/dev/examples/snippets.clj @@ -12,8 +12,8 @@ ;; multiple_events (d*/patch-elements! sse "
...
") (d*/patch-elements! sse "
...
") -(d*/patch-elements! sse "{answer: '...'}") -(d*/patch-elements! sse "{prize: '...'}") +(d*/patch-signals! sse "{answer: '...'}") +(d*/patch-signals! sse "{prize: '...'}") ;; setup #_{:clj-kondo/ignore true} @@ -30,7 +30,7 @@ (d*/patch-elements! sse "
What do you put in a toaster?
") - (d*/patch-elements! sse "{response: '', answer: 'bread'}"))})) + (d*/patch-signals! sse "{response: '', answer: 'bread'}"))})) (comment (handler {}))