HTTP server, routing, and middleware.
Provides Ring handler creation for hyper applications.
HTTP server, routing, and middleware. Provides Ring handler creation for hyper applications.
(action-handler app-state*)Handler for action POST requests.
All actions use Datastar's @post(), which sends all non-underscore
signals as a JSON body. Client params ($value, $checked, etc.) are
passed as URL query parameters.
context/*signals*effects/*pending* to collect side-effects (cookies, scripts)Handler for action POST requests. All actions use Datastar's `@post()`, which sends all non-underscore signals as a JSON body. Client params ($value, $checked, etc.) are passed as URL query parameters. - Parses the body as signal values and binds them to `context/*signals*` - Extracts client params from URL query parameters (JSON-decoded) - Binds `effects/*pending*` to collect side-effects (cookies, scripts) - Returns 204 (with any Set-Cookie headers) to prevent Datastar from merging the response into signals - Queues any pending scripts to the renderer for SSE delivery
(cleanup-tab! app-state* tab-id)Clean up all resources for a tab: watchers, reactive components, renderer thread, actions, and state.
Clean up all resources for a tab: watchers, reactive components, renderer thread, actions, and state.
(create-handler routes app-state*)(create-handler routes
app-state*
{:keys [watches head base-path middleware render-middleware
render-error not-found render-guard disconnect-grace-ms
heartbeat-ms max-file-size max-file-count
upload-expires-in]
:or {render-error render.error/minimal
not-found render.error/not-found
render-guard :warn
disconnect-grace-ms default-disconnect-grace-ms
heartbeat-ms default-heartbeat-ms
upload-expires-in 3600}
:as opts})Create a Ring handler for the hyper application.
routes: Vector of reitit routes, or a Var holding routes for live reloading. When a Var is provided, route changes are picked up on the next request without restarting the server — ideal for REPL-driven development. app-state*: Atom containing application state
Options:
(fn [error req] -> hiccup) rendered in place
of a view whose render-fn threw. May be a Var (e.g.
#'my.app/error-page) to pick up redefinitions without
restarting the server. Defaults to
hyper.render.error/minimal (generic, production-safe).
Use hyper.render.error/explain in development to see
the message, ex-data, and full stack trace.(fn [req] -> hiccup) rendered when no route
matches, served as a full page with HTTP 404 (and over
SSE for client-side navigation). May be a Var to pick
up redefinitions. Defaults to
hyper.render.error/not-found; pass nil to disable
and fall back to reitit's plain-text 404.Routes should use :get handlers that return hiccup (Chassis vectors). Hyper will wrap them to provide full HTML responses and SSE connections.
Create a Ring handler for the hyper application.
routes: Vector of reitit routes, or a Var holding routes for live reloading.
When a Var is provided, route changes are picked up on the next request
without restarting the server — ideal for REPL-driven development.
app-state*: Atom containing application state
Options:
- :head Hiccup nodes appended to the <head> (e.g. stylesheet <link>),
or (fn [req] ...) -> hiccup nodes appended to the <head>
- :datastar-script Hiccup nodes for the Datastar script (or nil to suppress)
- :open-when-hidden? Keep the SSE connection open when the browser tab is hidden
(default true). When false, Datastar closes the connection
on tab hide and reopens it when the tab becomes visible.
- :webkit-sse-shim? Inject a small client shim (into <head>, only for
WebKit/Safari user agents) that routes the GET render
stream through a native EventSource instead of
fetch+ReadableStream, working around a WebKit bug where a
large isolated SSE patch is held back until the next
write. Other browsers are unaffected and never receive
it. Default true; pass false to disable.
- :base-path URL path prefix for reverse-proxy deployments where the app
is served under a subfolder (e.g. "/my-app"). When set,
all internal hyper endpoints (/hyper/events, /hyper/actions,
/hyper/navigate) are mounted and referenced under this prefix.
Must start with "/" and have no trailing slash.
- :static-resources Classpath resource root(s) to serve as static assets
- :static-dir Filesystem directory (or directories) to serve as static assets
- :watches Vector of Watchable sources added to every page route.
Useful for top-level atoms that should trigger a re-render
on any page (e.g. a global config or feature-flags atom).
- :middleware Vector of Ring middleware fns applied inside the HTTP stack.
Each is (fn [handler] (fn [req] ...)). Runs after Hyper's
built-in cookie, params, and session middleware, so your
middleware sees parsed :cookies, :params, :hyper/session-id,
and :hyper/tab-id. Use this for auth, :hyper/env setup, and
other request-level concerns. Middleware can also be applied
outside create-handler, but will not have access to parsed
cookies/params.
- :render-middleware Vector of middleware fns applied to every page render.
Each is (fn [handler] (fn [req] ...)), identical to Ring
middleware. Runs outermost (before per-route middleware).
Applied on both initial HTTP page loads and SSE re-renders.
- :render-error Function `(fn [error req] -> hiccup)` rendered in place
of a view whose render-fn threw. May be a Var (e.g.
`#'my.app/error-page`) to pick up redefinitions without
restarting the server. Defaults to
`hyper.render.error/minimal` (generic, production-safe).
Use `hyper.render.error/explain` in development to see
the message, ex-data, and full stack trace.
- :not-found Function `(fn [req] -> hiccup)` rendered when no route
matches, served as a full page with HTTP 404 (and over
SSE for client-side navigation). May be a Var to pick
up redefinitions. Defaults to
`hyper.render.error/not-found`; pass `nil` to disable
and fall back to reitit's plain-text 404.
- :disconnect-grace-ms
How long (ms) a tab's state, signals, subviews,
watchers, and background workers are kept alive after
its SSE connection drops. A reconnect within this
window seamlessly re-attaches a fresh renderer with no
state loss and no mount/unmount churn; only after it
elapses is the tab fully torn down. Defaults to
180000 (3 minutes).
- :heartbeat-ms How often (ms) an idle SSE connection emits a keepalive
comment. Keeps connections warm through reverse-proxy
idle timeouts and surfaces dead/half-open channels on
the next write (so the disconnect grace window can
start). Defaults to 25000 (25s); pass nil or 0 to
disable.
- :max-file-size Max bytes allowed per uploaded file on the /hyper/upload
route; a larger file gets a 413. Default: no limit.
- :max-file-count Max number of files allowed per upload request.
Default: no limit.
- :upload-expires-in TTL (seconds) for upload temp files before they are
reaped. Default 3600 (1 hour). Move/stream :tempfile
to permanent storage in your handler.
Routes should use :get handlers that return hiccup (Chassis vectors).
Hyper will wrap them to provide full HTML responses and SSE connections.(default-datastar-script)Returns the default Datastar CDN script tag.
Returns the default Datastar CDN script tag.
(detach-tab! app-state* tab-id)Disconnect a tab's renderer/channel while preserving everything else.
Called when an SSE connection drops. Unlike cleanup-tab!, this is a
graceful disconnect: the renderer loop is stopped (its channel is closed)
but the tab's cursor state, signals, subviews, watchers, and background
workers are left intact so a reconnect within the grace window can
re-attach a fresh renderer. A :disconnected-at timestamp arms the reaper.
No-op when the tab no longer exists.
Disconnect a tab's renderer/channel while preserving everything else. Called when an SSE connection drops. Unlike `cleanup-tab!`, this is a *graceful* disconnect: the renderer loop is stopped (its channel is closed) but the tab's cursor state, signals, subviews, watchers, and background workers are left intact so a reconnect within the grace window can re-attach a fresh renderer. A `:disconnected-at` timestamp arms the reaper. No-op when the tab no longer exists.
(page-handler app-state* opts)Wrap a page render function to provide full HTML response. Delegates rendering to render/render-tab so initial page loads share the same render pipeline as SSE updates (error boundaries, live route re-resolution, title/head resolution).
Options:
Wrap a page render function to provide full HTML response. Delegates rendering to render/render-tab so initial page loads share the same render pipeline as SSE updates (error boundaries, live route re-resolution, title/head resolution). Options: - :datastar-script - Hiccup content for Datastar script added to document head, or nil.
(reap-disconnected-tabs! app-state* now)Fully tear down every tab whose disconnect grace window has elapsed.
A tab is eligible when it carries a :disconnected-at stamp older than the
configured :disconnect-grace-ms (default 3 minutes). Reconnecting clears
the stamp, so a tab that came back is never reaped. now is the current
epoch-millis (injectable for testing).
Fully tear down every tab whose disconnect grace window has elapsed. A tab is eligible when it carries a `:disconnected-at` stamp older than the configured `:disconnect-grace-ms` (default 3 minutes). Reconnecting clears the stamp, so a tab that came back is never reaped. `now` is the current epoch-millis (injectable for testing).
(safari-request? req)True when the request's User-Agent is WebKit-on-Apple (desktop Safari or any
iOS browser — all of which use WebKit and are affected by the fetch-streaming
strand), and not a desktop Blink/Gecko browser that merely carries the legacy
Safari token (Chrome, Edge, Opera, Android WebView).
True when the request's User-Agent is WebKit-on-Apple (desktop Safari or any iOS browser — all of which use WebKit and are affected by the fetch-streaming strand), and not a desktop Blink/Gecko browser that merely carries the legacy `Safari` token (Chrome, Edge, Opera, Android WebView).
(sse-connect-expr base-path tab-id open-when-hidden?)The Datastar @get expression that opens (or re-opens) this tab's SSE
connection. Shared by the initial <body> data-init and h/reconnect
so the two can never drift in endpoint, base-path, or fetch options.
The Datastar `@get` expression that opens (or re-opens) this tab's SSE connection. Shared by the initial `<body>` `data-init` and `h/reconnect` so the two can never drift in endpoint, base-path, or fetch options.
(sse-events-handler app-state*)Handler for the SSE event stream.
Returns a Ring response whose body is a StreamableResponseBody; Jetty
invokes write-body-to-stream on a (virtual) thread, where run-sse-loop!
owns the output stream for the connection's lifetime — sending the connected
event, wiring reactivity, and streaming re-renders. The SSE headers are set
on the response map here so they are committed before the body streams.
Handler for the SSE event stream. Returns a Ring response whose body is a `StreamableResponseBody`; Jetty invokes `write-body-to-stream` on a (virtual) thread, where `run-sse-loop!` owns the output stream for the connection's lifetime — sending the connected event, wiring reactivity, and streaming re-renders. The SSE headers are set on the response map here so they are committed before the body streams.
(start! handler
{:keys [port stop-timeout-ms] :or {port 3000 stop-timeout-ms 1000}})Start the HTTP server with the given handler.
handler: Ring handler (created with create-handler) options:
A tab's SSE render loop blocks its thread in write-body-to-stream for the connection's whole lifetime, so the threading setup matters:
Returns a stop function. Call (stop-fn) or pass to stop! to shut down the server and clean up all tab resources (renderer threads, watchers, actions).
Start the HTTP server with the given handler. handler: Ring handler (created with create-handler) options: - :port - Port to run on (default: 3000) - :stop-timeout-ms - Jetty graceful-stop timeout (default 1000). A tab's SSE render loop blocks its thread in write-body-to-stream for the connection's whole lifetime, so the threading setup matters: - Sync mode (not :async?): Jetty dispatches each request off the selector onto a worker thread. In async mode the streaming body-write is driven on a selector thread, and Jetty has only ~cores/2 of them, so a handful of open SSE tabs pins every selector and new connections can't be read (page loads hang). We flush the OutputStream ourselves in send-sse!, so sync mode still streams incrementally. - QueuedThreadPool + a virtual-threads executor: Jetty's I/O (acceptors/selectors) stays on platform threads, while request handling (and thus the blocking render loop) is dispatched onto a virtual thread — so each connected tab costs one virtual thread, not a platform one. Returns a stop function. Call (stop-fn) or pass to stop! to shut down the server and clean up all tab resources (renderer threads, watchers, actions).
(stop! stop-fn)Stop the HTTP server and clean up all resources.
Stop the HTTP server and clean up all resources.
(upload-handler app-state*)Handler for multipart file-upload POST requests (/hyper/upload).
Uploads are h/actions whose body uses a file param ($form/$files); the
client posts a real multipart/form-data request here (rather than Datastar's
JSON @post()). Wrapped with Ring's multipart middleware, which streams each
part to a temp file on disk, this:
:upload-ref by action-id.{:form :files} client-params and
folds non-file fields into context/*signals* (so @signal* reads resolve
to submitted field values by matching input name).:result) / :error, pushing each change to the client over SSE.Handler for multipart file-upload POST requests (`/hyper/upload`).
Uploads are `h/action`s whose body uses a file param ($form/$files); the
client posts a real multipart/form-data request here (rather than Datastar's
JSON @post()). Wrapped with Ring's multipart middleware, which streams each
part to a temp file on disk, this:
- Looks up the registered action + its `:upload-ref` by action-id.
- Parses the multipart body into the `{:form :files}` client-params and
folds non-file fields into `context/*signals*` (so `@signal*` reads resolve
to submitted field values by matching input name).
- Transitions the status ref :processing -> :done (handler return as
`:result`) / :error, pushing each change to the client over SSE.
- Returns 204 (with any cookies from effects), like the action handler.(wrap-hyper-context app-state*)Middleware that adds session-id and tab-id to the request.
Middleware that adds session-id and tab-id to the request.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |