Datastar Racket SDK
| (require datastar) | package: datastar-lib |
This package provides a Racket SDK for working with Datastar.
1 Usage
Example usage (adapted from the Datastar Go SDK docs)
#lang racket (require datastar web-server/http web-server/safety-limits web-server/servlet-dispatch web-server/web-server json) (struct store (message count) #:transparent) (define (handler req) ; Read signals from the request (define signals-data (read-signals req)) (define current-store (store (hash-ref signals-data 'message "") (hash-ref signals-data 'count 0))) ; Create a Server-Sent Event response (datastar-sse (lambda (sse) ; Patch elements in the DOM (patch-elements/xexpr sse '(div ((id "output")) "Hello from Datastar!")) ; Remove elements from the DOM (remove-elements sse "#temporary-element") ; Patch signals (update client-side state) (patch-signals sse (hash 'message "Updated message" 'count (+ (store-count current-store) 1))) ; Execute JavaScript in the browser (execute-script sse "console.log(\"Hello from server!\")") ; Redirect the browser (redirect sse "/new-page")))) (define stop (serve #:dispatch (dispatch/servlet handler) #:tcp@ datastar-tcp@ #:listen-ip "127.0.0.1" #:port 8000 #:connection-close? #t #:safety-limits (make-safety-limits #:response-timeout +inf.0 #:response-send-timeout +inf.0))) (with-handlers ([exn:break? (lambda (e) (stop))]) (sync/enable-break never-evt))
For more advanced usage with streaming updates, use the callback to loop directly. If the client disconnects or a send fails, an exception is raised which datastar-sse catches automatically, triggering cleanup via on-close:
(define (streaming-handler req) (datastar-sse (lambda (sse) (for ([i (in-range 10)]) (patch-elements/xexpr sse `(div ((id "counter")) ,(format "Count: ~a" i))) (patch-signals sse (hash 'counter i)) (sleep 1)))))
You can also use the #:on-close callback for cleanup when the connection ends:
(define connections (mutable-set)) (define (streaming-handler req) (datastar-sse (lambda (sse) (set-add! connections sse) (console-log sse "connected")) #:on-close (lambda (sse) (set-remove! connections sse))))
For more examples, see the examples directory on GitHub.
2 Server Setup
value
datastar-tcp@ : (unit/c (import) (export tcp^))
When provided, datastar-sse monitors the underlying TCP input port and interrupts on-open as soon as the client goes away, ensuring prompt cleanup via on-close. Without it, everything still works but disconnections are only detected on the next failed write.
Use dispatch/servlet from web-server/servlet-dispatch to convert your servlet into a dispatcher for serve:
(serve #:dispatch (dispatch/servlet handler) #:tcp@ datastar-tcp@ #:connection-close? #t #:safety-limits (make-safety-limits #:response-timeout +inf.0 #:response-send-timeout +inf.0))
dispatch/servlet composes with the standard web server dispatchers (dispatch-sequencer, dispatch-filter, dispatch-files, etc.) for routing and static file serving. See the examples directory for a full example.
3 Reading Requests
procedure
(read-signals request) → jsexpr?
request : request?
procedure
(datastar-request? request) → boolean?
request : request?
4 SSE Events
Functions for creating and sending Datastar SSE events. See the Datastar SSE events reference for full details on event types and their data lines.
4.1 SSE Generator
procedure
(datastar-sse on-open [#:on-close on-close]) → response?
on-open : (-> sse? any) on-close : (or/c (-> sse? any) #f) = #f
When the server is set up with datastar-tcp@, client disconnections are detected immediately: the SDK monitors the TCP input port and interrupts on-open as soon as the client goes away, ensuring prompt cleanup via on-close.
Important: When using serve, the following settings are required for SSE to work correctly:
#:tcp@ should be datastar-tcp@ for instant disconnect detection. Without this, disconnections are only detected on the next failed write, which means on-close may not fire promptly if the handler is blocked waiting for data.
#:connection-close? must be #t. Without this, the web server uses chunked transfer encoding with an internal pipe that silently absorbs writes to dead connections, preventing disconnect detection from working and on-close from firing.
#:safety-limits must disable both #:response-timeout and #:response-send-timeout (set to +inf.0). The defaults of 60 seconds will kill idle SSE connections. #:response-timeout limits the total time a handler can run, and #:response-send-timeout limits the time between successive writes. Both must be infinite for long-lived SSE connections.
procedure
(sse-closed? sse) → boolean?
sse : sse?
procedure
(call-with-sse-lock sse thunk) → any
sse : sse? thunk : (-> any)
This is only needed when multiple threads send through the same sse? generator. If each generator is used by a single thread (the common case), individual sends are already thread-safe and this is not needed.
The lock is re-entrant: all send functions (patch-elements, patch-signals, etc.) use call-with-sse-lock internally, so calling them inside a locked region does not deadlock.
(call-with-sse-lock sse (lambda () (patch-elements/xexpr sse '(div ((id "a")) "part 1")) (patch-elements/xexpr sse '(div ((id "b")) "part 2")) (patch-signals sse (hash 'status "updated"))))
If an exception is raised inside thunk, the lock is released via dynamic-wind, so subsequent sends can still proceed.
syntax
(with-sse-lock sse body ...)
(with-sse-lock sse (patch-elements/xexpr sse '(div ((id "a")) "part 1")) (patch-elements/xexpr sse '(div ((id "b")) "part 2")) (patch-signals sse (hash 'status "updated")))
4.2 Sending Events
All send functions take an sse? generator as their first argument. If the connection is closed or an I/O error occurs, an exception is raised. Within datastar-sse, these exceptions are caught automatically, triggering cleanup via on-close. Sends are thread-safe: multiple threads can send events through the same generator and delivery order is serialized. If multiple threads share a single generator, use with-sse-lock to send a group of events without interleaving.
procedure
(patch-elements sse elements [ #:selector selector #:mode mode #:namespace namespace #:use-view-transitions use-view-transitions #:event-id event-id #:retry-duration retry-duration]) → void? sse : sse? elements : (or/c string? #f) selector : (or/c string? #f) = #f mode : element-patch-mode/c = #f namespace : element-namespace/c = #f use-view-transitions : (or/c boolean? #f) = #f event-id : (or/c string? #f) = #f retry-duration : (or/c exact-positive-integer? #f) = #f
The #:mode parameter controls how elements are patched. Use the named constants: patch-mode-outer (morph entire element, default), patch-mode-inner (morph inner HTML), patch-mode-replace (replace entire element), patch-mode-prepend, patch-mode-append, patch-mode-before, patch-mode-after, and patch-mode-remove. When #f or patch-mode-outer, the mode data line is omitted.
(patch-elements sse "<div id=\"out\">hello</div>") (patch-elements sse "<svg>...</svg>" #:namespace element-namespace-svg) (patch-elements sse "<li>item</li>" #:selector "#list" #:mode patch-mode-append)
procedure
(patch-elements/xexpr sse xexpr [ #:selector selector #:mode mode #:namespace namespace #:use-view-transitions use-view-transitions #:event-id event-id #:retry-duration retry-duration]) → void? sse : sse? xexpr : xexpr/c selector : (or/c string? #f) = #f mode : element-patch-mode/c = #f namespace : element-namespace/c = #f use-view-transitions : (or/c boolean? #f) = #f event-id : (or/c string? #f) = #f retry-duration : (or/c exact-positive-integer? #f) = #f
(patch-elements/xexpr sse '(div ((id "out")) "hello")) (patch-elements/xexpr sse '(svg "...") #:namespace element-namespace-svg) (patch-elements/xexpr sse '(li "item") #:selector "#list" #:mode patch-mode-append)
procedure
(remove-elements sse selector [ #:event-id event-id #:retry-duration retry-duration]) → void? sse : sse? selector : string? event-id : (or/c string? #f) = #f retry-duration : (or/c exact-positive-integer? #f) = #f
procedure
(patch-signals sse signals [ #:event-id event-id #:only-if-missing only-if-missing #:retry-duration retry-duration]) → void? sse : sse? signals : (or/c string? jsexpr?) event-id : (or/c string? #f) = #f only-if-missing : (or/c boolean? #f) = #f retry-duration : (or/c exact-positive-integer? #f) = #f
procedure
(execute-script sse script [ #:auto-remove auto-remove #:attributes attributes #:event-id event-id #:retry-duration retry-duration]) → void? sse : sse? script : string? auto-remove : boolean? = #t
attributes : (or/c (hash/c symbol? any/c) (listof string?) #f) = #f event-id : (or/c string? #f) = #f retry-duration : (or/c exact-positive-integer? #f) = #f
procedure
(console-log sse message) → void?
sse : sse? message : string?
procedure
(console-error sse message) → void?
sse : sse? message : string?
procedure
(replace-url sse location) → void?
sse : sse? location : string?
4.3 Patch Modes
Named constants for the #:mode parameter of patch-elements. Values are symbols ('outer, 'inner, etc.).
value
value
value
value
value
value
value
value
4.4 Element Namespaces
Named constants for the #:namespace parameter of patch-elements. Values are symbols ('html, 'svg, 'mathml).
value
value
value
5 Compression
To add Brotli compression, wrap your app handler with wrap-brotli-compress from web-server-compress:
(require datastar web-server-compress web-server/dispatch web-server/servlet-dispatch web-server/web-server) (define (events-handler req) (datastar-sse (lambda (sse) ...))) (define-values (app _uri) (dispatch-rules ...)) (serve #:dispatch (dispatch/servlet (wrap-brotli-compress app)) ...)
Responses are compressed only when the client sends Accept-Encoding: br. Each SSE event is flushed to the client immediately. Optional keyword arguments like #:quality and #:compress? let you tune the encoder or override which responses are compressed; see the web-server-compress docs for the full API.
6 Frontend Helpers
Convenience functions for generating Datastar HTML attributes and backend action strings in x-expression templates. These are not part of the core SDK protocol; they provide Racket-friendly sugar over raw data-* attribute strings.
6.1 Attribute Helpers
Functions for generating Datastar data-* HTML attributes as x-expression attribute pairs. Each function returns (list 'attr-name "value") which drops directly into x-expression templates via unquote. See the Datastar attribute reference for full details on each attribute’s behavior.
Without attribute helpers:
> `(button ((data-on:click__debounce.500ms "@post('/search')") (data-class:active "$enabled") (data-show "$query != ''")))
'(button
((data-on:click__debounce.500ms "@post('/search')")
(data-class:active "$enabled")
(data-show "$query != ''")))
With attribute helpers:
> `(button (,(data-on "click" (sse-post "/search") #:debounce "500ms") ,(data-class 'active "$enabled") ,(data-show "$query != ''")))
'(button
((data-on:click__debounce.500ms "@post('/search')")
(data-class:active "$enabled")
(data-show "$query != ''")))
Modifiers (debounce, throttle, once, etc.) are expressed as keyword arguments rather than method chaining. Boolean modifiers take #t; parameterized modifiers take their value directly.
In the keyed form, key-or-hash is the attribute name and value-or-unused is the expression.
In the hash form, key-or-hash is a hash mapping attribute names to expressions.
Keyed form:
> `(button (,(data-attr 'disabled "$loading"))) '(button ((data-attr:disabled "$loading")))
Hash form:
> `(button (,(data-attr (hash "disabled" "$loading" "aria-busy" "$loading")))) '(button ((data-attr "{\"disabled\": $loading, \"aria-busy\": $loading}")))
The signal is the signal name. The optional value is rarely needed since the element’s own value is used.
> `(input (,(data-bind "username"))) '(input ((data-bind:username "")))
> `(select (,(data-bind "choice")) (option ((value "a")) "A") (option ((value "b")) "B"))
'(select
((data-bind:choice ""))
(option ((value "a")) "A")
(option ((value "b")) "B"))
procedure
(data-class key-or-hash [value-or-unused]) → list?
key-or-hash : (or/c symbol? string? hash?) value-or-unused : any/c = #f
In the keyed form, key-or-hash is the class name and value-or-unused is a boolean expression.
In the hash form, key-or-hash is a hash mapping class names to boolean expressions.
Keyed form:
> `(button (,(data-class 'active "$selected"))) '(button ((data-class:active "$selected")))
Hash form:
> `(div (,(data-class (hash "font-bold" "$important" "text-red" "$error")))) '(div ((data-class "{\"text-red\": $error, \"font-bold\": $important}")))
procedure
(data-computed key-or-hash [value-or-unused])
→ (or/c list? (listof list?)) key-or-hash : (or/c symbol? string? hash?) value-or-unused : any/c = #f
In the keyed form, key-or-hash is the signal name and value-or-unused is the expression.
In the hash form, key-or-hash is a hash mapping signal names to expressions. The hash form returns a list of attribute pairs, not a single pair. Use ,@"@" (unquote-splicing) to insert them into an x-expression.
Keyed form:
> `(div (,(data-computed 'total "$price * $quantity"))) '(div ((data-computed:total "$price * $quantity")))
Hash form (returns list of pairs, use ,@"@"):
> `(div (,@(data-computed (hash 'total "$price * $qty" 'valid "$total > 0"))))
'(div
((data-computed:total "$price * $qty") (data-computed:valid "$total > 0")))
procedure
(data-effect expression) → list?
expression : string?
> `(div (,(data-effect "$total = $price * $quantity"))) '(div ((data-effect "$total = $price * $quantity")))
procedure
(data-ignore [#:self self]) → list?
self : boolean? = #f
When #:self is #t, only the element itself is ignored; its children are still processed.
> `(div (,(data-ignore)) "Datastar will not process this or its children") '(div ((data-ignore "")) "Datastar will not process this or its children")
> `(div (,(data-ignore #:self #t)) "Only this element is ignored") '(div ((data-ignore__self "")) "Only this element is ignored")
procedure
> `(div (,(data-ignore-morph)) "This content will not be morphed") '(div ((data-ignore-morph "")) "This content will not be morphed")
procedure
(data-indicator signal) → list?
signal : string?
> `(button (,(data-indicator "loading") ,(data-on "click" (sse-get "/data"))) "Fetch") '(button ((data-indicator:loading "") (data-on:click "@get('/data')")) "Fetch")
> `(div (,(data-show "$loading")) "Loading...") '(div ((data-show "$loading")) "Loading...")
procedure
(data-init expression [ #:delay delay #:viewtransition viewtransition]) → list? expression : string? delay : (or/c string? number? #f) = #f viewtransition : boolean? = #f
> (data-init (sse-get "/events")) '(data-init "@get('/events')")
procedure
(data-json-signals [ #:include include #:exclude exclude #:terse terse]) → list? include : (or/c string? #f) = #f exclude : (or/c string? #f) = #f terse : boolean? = #f
> `(pre (,(data-json-signals))) '(pre ((data-json-signals "")))
> `(pre (,(data-json-signals #:include "/^user/" #:terse #t))) '(pre ((data-json-signals__terse "{\"include\": \"/^user/\"}")))
procedure
(data-on event expression [ #:once once #:passive passive #:capture capture #:window window #:outside outside #:prevent prevent #:stop stop #:trust trust #:debounce debounce #:debounce-leading debounce-leading #:debounce-notrailing debounce-notrailing #:throttle throttle #:throttle-noleading throttle-noleading #:throttle-trailing throttle-trailing #:delay delay #:viewtransition viewtransition]) → list? event : string? expression : string? once : boolean? = #f passive : boolean? = #f capture : boolean? = #f window : boolean? = #f outside : boolean? = #f prevent : boolean? = #f stop : boolean? = #f trust : boolean? = #f debounce : (or/c string? number? #f) = #f debounce-leading : boolean? = #f debounce-notrailing : boolean? = #f throttle : (or/c string? number? #f) = #f throttle-noleading : boolean? = #f throttle-trailing : boolean? = #f delay : (or/c string? number? #f) = #f viewtransition : boolean? = #f
> (data-on "click" "$count++") '(data-on:click "$count++")
> (data-on "input" (sse-post "/search") #:debounce "250ms") '(data-on:input__debounce.250ms "@post('/search')")
> (data-on "click" (sse-get "/data") #:once #t #:prevent #t) '(data-on:click__once__prevent "@get('/data')")
> (data-on "keydown" "$handleKey(evt)" #:window #t) '(data-on:keydown__window "$handleKey(evt)")
procedure
(data-on-intersect expression [ #:once once #:half half #:full full #:exit exit #:threshold threshold #:debounce debounce #:debounce-leading debounce-leading #:debounce-notrailing debounce-notrailing #:throttle throttle #:throttle-noleading throttle-noleading #:throttle-trailing throttle-trailing #:delay delay #:viewtransition viewtransition]) → list? expression : string? once : boolean? = #f half : boolean? = #f full : boolean? = #f exit : boolean? = #f threshold : (or/c string? number? #f) = #f debounce : (or/c string? number? #f) = #f debounce-leading : boolean? = #f debounce-notrailing : boolean? = #f throttle : (or/c string? number? #f) = #f throttle-noleading : boolean? = #f throttle-trailing : boolean? = #f delay : (or/c string? number? #f) = #f viewtransition : boolean? = #f
> (data-on-intersect (sse-get "/load-more") #:once #t #:half #t) '(data-on-intersect__once__half "@get('/load-more')")
procedure
(data-on-interval expression [ #:duration duration #:duration-leading duration-leading #:viewtransition viewtransition]) → list? expression : string? duration : (or/c string? number? #f) = #f duration-leading : boolean? = #f viewtransition : boolean? = #f
> (data-on-interval "$count++" #:duration "2s") '(data-on-interval__duration.2s "$count++")
> (data-on-interval (sse-get "/poll") #:duration "5s" #:duration-leading #t) '(data-on-interval__duration.5s.leading "@get('/poll')")
procedure
(data-on-signal-patch expression [ #:include include #:exclude exclude #:debounce debounce #:debounce-leading debounce-leading #:debounce-notrailing debounce-notrailing #:throttle throttle #:throttle-noleading throttle-noleading #:throttle-trailing throttle-trailing #:delay delay]) → (or/c list? (listof list?)) expression : string? include : (or/c string? #f) = #f exclude : (or/c string? #f) = #f debounce : (or/c string? number? #f) = #f debounce-leading : boolean? = #f debounce-notrailing : boolean? = #f throttle : (or/c string? number? #f) = #f throttle-noleading : boolean? = #f throttle-trailing : boolean? = #f delay : (or/c string? number? #f) = #f
When #:include or #:exclude are provided, a separate data-on-signal-patch-filter attribute is generated alongside the main attribute. In this case, the function returns a list of two attribute pairs instead of a single pair. Use ,@"@" (unquote-splicing) to insert them.
No filter (returns single pair):
> `(div (,(data-on-signal-patch "console.log('patched')"))) '(div ((data-on-signal-patch "console.log('patched')")))
With filter (returns list of two pairs, use ,@"@"):
> `(div (,@(data-on-signal-patch "console.log('counter changed')" #:include "/^counter$/" #:debounce "300ms")))
'(div
((data-on-signal-patch__debounce.300ms "console.log('counter changed')")
(data-on-signal-patch-filter "{\"include\": \"/^counter$/\"}")))
attrs can be a single attribute name string or a list of attribute name strings.
Preserve the open attribute on a <details> element:
> `(details ((open "")) (,(data-preserve-attrs "open")) (summary "Title") "Content")
'(details
((open ""))
((data-preserve-attr "open"))
(summary "Title")
"Content")
Preserve multiple attributes:
> `(details ((open "") (class "custom")) (,(data-preserve-attrs '("open" "class"))) (summary "Title") "Content")
'(details
((open "") (class "custom"))
((data-preserve-attr "open class"))
(summary "Title")
"Content")
> `(div (,(data-ref "myDiv"))) '(div ((data-ref:myDiv "")))
> `(div (,(data-show "$loggedIn")) "Welcome back") '(div ((data-show "$loggedIn")) "Welcome back")
procedure
(data-signals key-or-hash [ value-or-unused #:ifmissing ifmissing]) → list? key-or-hash : (or/c symbol? string? hash?) value-or-unused : any/c = #f ifmissing : boolean? = #f
In the keyed form, key-or-hash is a symbol or string signal name and value-or-unused is the signal’s initial value as a string expression.
In the hash form, key-or-hash is a Racket hash that is serialized to JSON. Nested hashes produce nested signals.
When #:ifmissing is #t, signals are only set if they don’t already exist.
Keyed form:
> `(div (,(data-signals 'count "0"))) '(div ((data-signals:count "0")))
Keyed form with ifmissing:
> `(div (,(data-signals 'count "0" #:ifmissing #t))) '(div ((data-signals:count__ifmissing "0")))
Hash form:
> `(div (,(data-signals (hash 'count 0 'name "hello")))) '(div ((data-signals "{\"count\":0,\"name\":\"hello\"}")))
Nested signals:
> `(div (,(data-signals (hash 'form (hash 'name "" 'email ""))))) '(div ((data-signals "{\"form\":{\"email\":\"\",\"name\":\"\"}}")))
procedure
(data-style key-or-hash [value-or-unused]) → list?
key-or-hash : (or/c symbol? string? hash?) value-or-unused : any/c = #f
In the keyed form, key-or-hash is the CSS property name and value-or-unused is the expression.
In the hash form, key-or-hash is a hash mapping CSS property names to expressions.
Keyed form:
> `(div (,(data-style 'background-color "$dark ? 'black' : 'white'"))) '(div ((data-style:background-color "$dark ? 'black' : 'white'")))
Hash form:
> `(div (,(data-style (hash "display" "$hidden && 'none'" "color" "$error ? 'red' : 'black'"))))
'(div
((data-style
"{\"color\": $error ? 'red' : 'black', \"display\": $hidden && 'none'}")))
> `(span (,(data-text "$count"))) '(span ((data-text "$count")))
6.1.1 Pro Attributes
procedure
(data-custom-validity expression) → list?
expression : string?
> `(input (,(data-bind "password") ,(data-custom-validity "$password.length < 8 ? 'Must be 8+ characters' : ''")))
'(input
((data-bind:password "")
(data-custom-validity
"$password.length < 8 ? 'Must be 8+ characters' : ''")))
procedure
(data-match-media signal query) → list?
signal : string? query : string?
> `(div (,(data-match-media "is-dark" "'prefers-color-scheme: dark'"))) '(div ((data-match-media:is-dark "'prefers-color-scheme: dark'")))
procedure
(data-on-raf expression [ #:throttle throttle #:throttle-noleading throttle-noleading #:throttle-trailing throttle-trailing]) → list? expression : string? throttle : (or/c string? number? #f) = #f throttle-noleading : boolean? = #f throttle-trailing : boolean? = #f
procedure
(data-on-resize expression [ #:debounce debounce #:debounce-leading debounce-leading #:debounce-notrailing debounce-notrailing #:throttle throttle #:throttle-noleading throttle-noleading #:throttle-trailing throttle-trailing]) → list? expression : string? debounce : (or/c string? number? #f) = #f debounce-leading : boolean? = #f debounce-notrailing : boolean? = #f throttle : (or/c string? number? #f) = #f throttle-noleading : boolean? = #f throttle-trailing : boolean? = #f
procedure
(data-persist [ #:key key #:include include #:exclude exclude #:session session]) → list? key : (or/c string? #f) = #f include : (or/c string? #f) = #f exclude : (or/c string? #f) = #f session : boolean? = #f
> `(div (,(data-persist))) '(div ((data-persist "")))
> `(div (,(data-persist #:key "myapp" #:include "/^user\\./" #:session #t))) '(div ((data-persist:myapp__session "{\"include\": \"/^user\\\\./\"}")))
procedure
(data-query-string [ #:include include #:exclude exclude #:filter filter #:history history]) → list? include : (or/c string? #f) = #f exclude : (or/c string? #f) = #f filter : boolean? = #f history : boolean? = #f
> `(div (,(data-query-string #:filter #t #:history #t))) '(div ((data-query-string__filter__history "")))
procedure
(data-replace-url expression) → list?
expression : string?
> `(div (,(data-replace-url "`/page${$page}`"))) '(div ((data-replace-url "`/page${$page}`")))
procedure
(data-scroll-into-view [ #:smooth smooth #:instant instant #:auto auto #:hstart hstart #:hcenter hcenter #:hend hend #:hnearest hnearest #:vstart vstart #:vcenter vcenter #:vend vend #:vnearest vnearest #:focus focus]) → list? smooth : boolean? = #f instant : boolean? = #f auto : boolean? = #f hstart : boolean? = #f hcenter : boolean? = #f hend : boolean? = #f hnearest : boolean? = #f vstart : boolean? = #f vcenter : boolean? = #f vend : boolean? = #f vnearest : boolean? = #f focus : boolean? = #f
> (data-scroll-into-view #:smooth #t #:vcenter #t) '(data-scroll-into-view__smooth__vcenter "")
procedure
(data-view-transition expression) → list?
expression : string?
> `(div (,(data-view-transition "$transitionName"))) '(div ((data-view-transition "$transitionName")))
6.2 Action Helpers
Convenience functions for generating Datastar backend action attribute strings.
> (define tid 42)
> `(main ((id "main") ,(data-init (sse-get "/events"))) (form (,(data-on "submit" (sse-post "/todo/create"))) (button (,(data-on "click" (sse-post (format "/todo/delete/~a" tid)))) "Delete")))
'(main
((id "main") (data-init "@get('/events')"))
(form
((data-on:submit "@post('/todo/create')"))
(button ((data-on:click "@post('/todo/delete/42')")) "Delete")))
> (sse-get "/events") "@get('/events')"
> (sse-get "/events" "{includeLocal: true}") "@get('/events', {includeLocal: true})"
procedure
(sse-delete url [args]) → string?
url : string? args : string? = #f
6.3 CDN
value
> `(script ((type "module") (src ,datastar-cdn-url)))
'(script
((type "module")
(src
"https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.8/bundles/datastar.js")))
value
value
7 Testing Utilities
| (require datastar/testing) | package: datastar-lib |
Mock SSE generators for testing Datastar handlers without a real HTTP connection. Require this module separately from datastar.
procedure
(make-mock-sse) →
sse? (-> string?)
> (define-values (sse get-output) (make-mock-sse)) > (patch-elements/xexpr sse '(div ((id "x")) "hi")) > (get-output) "event: datastar-patch-elements\ndata: elements <div id=\"x\">hi</div>\n\n"
procedure
(make-recording-sse) →
sse? (-> (listof sse-event?))
The events are parsed from the same SSE text that would go over the wire, so they reflect exactly what a real client would receive.
> (define-values (sse2 get-events) (make-recording-sse)) > (patch-elements/xexpr sse2 '(div "test")) > (patch-signals sse2 (hash 'x 1)) > (define events (get-events)) > (length events) 2
> (sse-event-type (first events)) "datastar-patch-elements"
> (sse-event-type (second events)) "datastar-patch-signals"
struct
(struct sse-event (type id retry data-lines) #:transparent) type : string? id : (or/c string? #f) retry : (or/c exact-nonnegative-integer? #f) data-lines : (listof string?)
type – event type string (e.g. "datastar-patch-elements").
id – event ID, or #f if none was set.
retry – retry duration in milliseconds, or #f if the default was used (defaults are omitted from SSE output).
data-lines – list of data line contents without the data: prefix. For example, '("elements <div>hello</div>").