sxtp: patch + signals primitives (Datastar-borrowed)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 46s

Adds two new top-level SXTP message types alongside
request/response/condition/event, modelled on Datastar's
datastar-patch-elements and datastar-patch-signals SSE events:

  (patch :target "#x" :mode outer :body (~card)) - DOM fragment
    morph. Subsumes HTMX swap modes. Mode is outer (default) |
    inner | replace | prepend | append | before | after | remove.

  (signals :values {:n 3} :only-if-missing false) - reactive
    state patch. nil value removes the signal. only-if-missing
    skips existing signals (lazy init).

A server response stream can mix both freely; clients dispatch
by head symbol, ordering preserved. Cleaner than HTMX's
swap-mode-per-trigger because the patch shape is decoupled from
the triggering element/attribute.

Spec at applications/sxtp/spec.sx (patch-fields, signals-fields,
patch-modes, example-patch-stream). Constructors / predicates /
accessors / serialise / parse in lib/host/sxtp.sx. 25 new tests
in lib/host/tests/sxtp.sx (predicates, mode normalisation, fixed
field order, remove-without-body, signals round-trip). Host
conformance 129/129 (was 104/104).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:22:37 +00:00
parent fac15d6140
commit 1d02afb64a
3 changed files with 148 additions and 5 deletions

View File

@@ -24,6 +24,10 @@
(host-sx-test "request not response" (sxtp/response? host-sx-req) false)
(host-sx-test "response?" (sxtp/response? host-sx-resp) true)
(host-sx-test "condition?" (sxtp/condition? (sxtp/condition "x" {})) true)
(host-sx-test "patch?" (sxtp/patch? (sxtp/patch "#x" {})) true)
(host-sx-test "patch not event" (sxtp/event? (sxtp/patch "#x" {})) false)
(host-sx-test "signals?" (sxtp/signals? (sxtp/signals {:n 3} {})) true)
(host-sx-test "signals not patch" (sxtp/patch? (sxtp/signals {:n 3} {})) false)
;; ── accessors (verb/status are symbols) ────────────────────────────
(host-sx-test "verb" (symbol->string (sxtp/verb host-sx-req)) "navigate")
@@ -68,6 +72,68 @@
(contains? (sxtp/serialize host-sx-resp) ":msg")
false)
;; ── patch + signals (Datastar-borrowed) ───────────────────────────
;; Mode defaults to outer; accepts string OR symbol input.
(host-sx-test
"patch default mode is outer symbol"
(symbol->string (sxtp/mode (sxtp/patch "#x" {})))
"outer")
(host-sx-test
"patch accepts symbol mode"
(symbol->string (sxtp/mode (sxtp/patch "#x" {:mode (string->symbol "inner")})))
"inner")
(host-sx-test
"patch accepts string mode and normalises"
(symbol->string (sxtp/mode (sxtp/patch "#x" {:mode "append"})))
"append")
(host-sx-test
"patch target accessor"
(sxtp/target (sxtp/patch "#cart" {}))
"#cart")
(host-sx-test
"patch serialises with target/mode/body in fixed order"
(sxtp/serialize (sxtp/patch "#x" {:body "hi"}))
"(patch :target \"#x\" :mode outer :body \"hi\")")
(host-sx-test
"patch remove mode serialises without :body"
(sxtp/serialize (sxtp/patch "#x" {:mode "remove"}))
"(patch :target \"#x\" :mode remove)")
(host-sx-test
"patch transition? predicate"
(sxtp/transition? (sxtp/patch "#x" {:transition true}))
true)
(host-sx-test
"signals accessor"
(get (sxtp/values (sxtp/signals {:cart/count 3} {})) :cart/count)
3)
(host-sx-test
"signals only-if-missing default false"
(sxtp/only-if-missing? (sxtp/signals {:n 1} {}))
false)
(host-sx-test
"signals only-if-missing true round-trips"
(sxtp/only-if-missing? (sxtp/signals {:n 1} {:only-if-missing true}))
true)
(host-sx-test
"signals serialise"
(sxtp/serialize (sxtp/signals {:cart/count 3} {}))
"(signals :values {:cart/count 3})")
;; ── round-trip ────────────────────────────────────────────────────
(define host-sx-patch-rt
(sxtp/parse (sxtp/serialize (sxtp/patch "#mini" {:mode "inner" :body "n=3"}))))
(host-sx-test "patch rt msg" (sxtp/patch? host-sx-patch-rt) true)
(host-sx-test "patch rt target" (sxtp/target host-sx-patch-rt) "#mini")
(host-sx-test "patch rt mode" (symbol->string (sxtp/mode host-sx-patch-rt)) "inner")
(define host-sx-signals-rt
(sxtp/parse (sxtp/serialize (sxtp/signals {:a 1 :b "x"} {:only-if-missing true}))))
(host-sx-test "signals rt msg" (sxtp/signals? host-sx-signals-rt) true)
(host-sx-test "signals rt values"
(get (sxtp/values host-sx-signals-rt) :a) 1)
(host-sx-test "signals rt only-if-missing"
(sxtp/only-if-missing? host-sx-signals-rt) true)
;; ── parse + round-trip ─────────────────────────────────────────────
(define host-sx-parsed
(sxtp/parse "(request :verb query :path \"/events\" :headers {:host \"h\"})"))