Datastar SSE helpers for Elixir/Phoenix.
A trivially small library: SSE connection management, signal patching, DOM element patching. That's it.
Quick Example
def increment(conn, _params) do
signals = Dstar.read_signals(conn)
count = signals["count"] || 0
conn
|> Dstar.start()
|> Dstar.patch_signals(%{count: count + 1})
endModules
Dstar.SSE— Open SSE connections, send raw eventsDstar.Signals— Read signals from requests, patch signals on the clientDstar.Elements— Patch and remove DOM elementsDstar.Actions— Generate@post(...)expressions for Datastar attributesDstar.Plugs.Dispatch— Optional dynamic event dispatch plug
Summary
Functions
Appends an element as the last child of a container, via SSE.
Checks if an SSE connection is still open.
Logs a message to the browser console via SSE.
Generates a @delete(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Executes JavaScript on the client by appending a script tag via SSE.
Safely fetches Datastar signals and returns the updated Plug conn.
Generates a @get(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Signals that a collection changed, so each tab reloads it with its own filter/sort/page signals.
Generates a @patch(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Patches a DOM element on the client via SSE.
Patches signals on the client via SSE.
Generates a @post(...) expression for Datastar attributes.
Generates a @put(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Reads already-fetched Datastar signals as a map.
Redirects the client to the given URL via JavaScript.
Removes DOM elements on the client via SSE.
Removes signals from the client by setting them to nil.
Starts an SSE connection on the given Plug conn.
Starts an SSE stream with per-tab deduplication.
Morphs the element whose DOM id matches the root of html, via SSE.
Functions
Appends an element as the last child of a container, via SSE.
Example
conn |> Dstar.append_elements(post_row(%{post: post}), "#posts")
Checks if an SSE connection is still open.
Returns {:ok, conn} if open, {:error, conn} if closed.
Useful in streaming loops to detect disconnections.
Example
case Dstar.check_connection(conn) do
{:ok, conn} -> stream_loop(conn)
{:error, _} -> :ok
end
Logs a message to the browser console via SSE.
Example
conn |> Dstar.console_log("Debug info")
Generates a @delete(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Executes JavaScript on the client by appending a script tag via SSE.
Example
conn |> Dstar.execute_script("alert('Hello!')")
Safely fetches Datastar signals and returns the updated Plug conn.
Use this for plain Plug conns whose body has not already been parsed. It
accepts JSON objects only and bounds raw/query payloads with :max_bytes.
case Dstar.fetch_signals(conn, max_bytes: 64_000) do
{:ok, signals, conn} -> handle(conn, signals)
{:error, reason, conn} -> Dstar.Signals.send_error(conn, reason)
end
Generates a @get(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
See Dstar.Actions.get/3.
Signals that a collection changed, so each tab reloads it with its own filter/sort/page signals.
Example
conn |> Dstar.nudge("posts")
Generates a @patch(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
Patches a DOM element on the client via SSE.
Takes a :selector, or — with no selector — targets by the id on each
top-level element of html. See Dstar.Elements.patch/3 for all options.
Example
conn |> Dstar.patch_elements("<span id=\"count\">42</span>", selector: "#count")
Patches signals on the client via SSE.
Example
conn |> Dstar.patch_signals(%{count: 42})
Generates a @post(...) expression for Datastar attributes.
Examples
Dstar.post(MyAppWeb.CounterHandler, "increment")
# => ~s|@post("/ds/my_app_web-counter_handler/increment")|
expression = Dstar.post("increment")
# The `$_dstar_module` signal is percent-encoded at runtime.
String.contains?(expression, "encodeURIComponent")
# => trueEvent and module values are percent-encoded as one route segment. Exact empty
and dot segments are rejected. In the module form, :prefix must be a local
absolute application path beginning with one /; :module on the dynamic
form is a literal module override, not JavaScript or a signal name.
See Dstar.Actions.post/3.
Generates a @put(...) expression for Datastar attributes.
See Dstar.post/2 for usage — same API, different HTTP verb.
See Dstar.Actions.put/3.
Reads already-fetched Datastar signals as a map.
Plug.Parsers/Phoenix body params and already-fetched GET/DELETE query
params work here. For an unfetched raw body, use fetch_signals/2; this
convenience function cannot return the adapter-updated conn.
Redirects the client to the given URL via JavaScript.
Default destination policy is same-origin and path-absolute
(/workspaces, ?x=1, #frag). Off-origin http/https requires
external: true or allow: ["host"]. See Dstar.Scripts.redirect/3.
Example
conn |> Dstar.redirect("/workspaces")
conn |> Dstar.redirect("https://ok.example/docs", external: true)
Removes DOM elements on the client via SSE.
Example
conn |> Dstar.remove_elements("#old-item")
Removes signals from the client by setting them to nil.
Accepts a single path string or list of dot-notated paths.
Examples
conn |> Dstar.remove_signals("user.profile.theme")
conn |> Dstar.remove_signals(["user.name", "user.email"])
Starts an SSE connection on the given Plug conn.
Sets content type to text/event-stream, disables caching,
and initiates a chunked response. Returns the conn.
Example
conn = Dstar.start(conn)
Starts an SSE stream with per-tab deduplication.
Requires Dstar.Utility.StreamRegistry in your supervision tree and a
tabId signal in your root layout. A usable tabId is claimed atomically
before SSE starts. Claim failure returns a halted, plain-text 503 conn; it
never starts an undeduplicated keyed stream. Missing/invalid tabId is the
intentional unkeyed fallback.
Prefer Dstar.Stream.open/2 with key: in plain controllers — it returns
{:ok, conn} / {:error, conn} and Dstar.Stream.run/2 handles takeover and
release. Hand-rolled loops that own their receive must check conn.halted
before subscribing/looping and call
Dstar.Utility.StreamRegistry.release(conn) during teardown. See that
module's docs for setup. Pass max_bytes: n as a third argument to bound
raw/query signal input per call.
Example
conn = Dstar.start_stream(conn, current_user.id)
Morphs the element whose DOM id matches the root of html, via SSE.
Dropped by the client if this tab has no element with that id — see
Dstar.Elements.upsert/3.
Example
conn |> Dstar.upsert_elements(post_row(%{post: post}))