Functions for reading and patching Datastar signals via SSE.
signals = Dstar.Signals.read(conn)
conn |> patch(%{count: 42, message: "Hello"})
conn |> patch(%{count: 42}, only_if_missing: true)
conn |> remove_signals("user.session")
Summary
Functions
Formats a signals patch as an SSE event string (for stateless responses).
Formats a signal removal as an SSE event string (for stateless responses).
Signals that a collection changed, without saying how.
Patches signals on the client by sending an SSE event.
Patches signals using a raw JSON string.
Reads signals from a Plug connection.
Removes signals from the client by setting them to nil.
Functions
Formats a signals patch as an SSE event string (for stateless responses).
Example
format_patch(%{count: 42})
# => "event: datastar-patch-signals\ndata: signals {\"count\":42}\n\n"
Formats a signal removal as an SSE event string (for stateless responses).
Examples
format_remove("user.profile")
# => "event: datastar-patch-signals\ndata: signals {\"user\":{\"profile\":null}}\n\n"
format_remove(["user.a", "user.b"])
# => "event: datastar-patch-signals\ndata: signals {\"user\":{\"a\":null,\"b\":null}}\n\n"
@spec nudge(Plug.Conn.t(), String.t() | atom(), keyword()) :: Plug.Conn.t()
Signals that a collection changed, without saying how.
Patches nudges.<key> with a fresh integer. Tabs watching that signal
(see Dstar.Actions.on_nudge/2) re-run their own load action, which
carries their current filter/sort/page signals — so every tab gets a
view correct for that tab. This is the default for any list that is
filtered, sorted or paginated; the stream is one-way and cannot learn a
tab's view state after connect. See the
Live collections guide.
The value is System.unique_integer([:positive, :monotonic]). The client
only fires its handler when a signal's value actually changes, so
re-sending a constant would be a silent no-op.
key is a single signal path segment: ~r/^[a-zA-Z0-9_]+$/.
Example
def handle_info({:posts_changed, _}, conn), do: nudge(conn, "posts")
@spec patch(Plug.Conn.t(), map(), keyword()) :: Plug.Conn.t()
Patches signals on the client by sending an SSE event.
Options
:only_if_missing- Only patch signals that don't exist on the client (default: false):event_id- Event ID for client tracking:retry- Retry duration in milliseconds
Example
conn
|> Dstar.Signals.patch(%{count: 42})
|> Dstar.Signals.patch(%{message: "Hello"}, only_if_missing: true)
@spec patch_raw(Plug.Conn.t(), String.t(), keyword()) :: Plug.Conn.t()
Patches signals using a raw JSON string.
The JSON must be a single line. Embedded line breaks are split across
multiple SSE data: lines for wire safety, which the client will not
reassemble into one signals payload — pass compact JSON (as
Jason.encode!/1 produces) or use patch/3.
Example
conn
|> Dstar.Signals.patch_raw(~s({"count": 42}))
@spec read(Plug.Conn.t()) :: map()
Reads signals from a Plug connection.
For GET and DELETE requests, reads from query parameters under the "datastar" key. For other methods, reads from the JSON request body. This matches Datastar v1.0's behavior, where GET and DELETE requests do not carry a body.
Returns a map of signals or an empty map if no signals are present.
Example
signals = Dstar.Signals.read(conn)
# => %{"count" => 10, "message" => "Hello"}
@spec remove_signals(Plug.Conn.t(), String.t() | [String.t()], keyword()) :: Plug.Conn.t()
Removes signals from the client by setting them to nil.
Accepts a single dot-notated path string or a list of paths.
Paths are converted to a nested map with nil values,
then passed to patch/3.
Examples
# Remove a single signal
conn |> remove_signals("user.profile.theme")
# Remove multiple signals with shared prefix
conn |> remove_signals(["user.name", "user.email"])
# Remove top-level signal
conn |> remove_signals("count")Options
:only_if_missing- Only remove if signal doesn't exist (default: false):event_id- Event ID for client tracking:retry- Retry duration in milliseconds