Dstar.Signals (dstar v0.1.4)

Copy Markdown View Source

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

format_patch(signals, opts \\ [])

@spec format_patch(
  map(),
  keyword()
) :: String.t()

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"

format_remove(paths, opts \\ [])

@spec format_remove(
  String.t() | [String.t()],
  keyword()
) :: String.t()

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"

nudge(conn, key, opts \\ [])

@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")

patch(conn, signals, opts \\ [])

@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)

patch_raw(conn, json, opts \\ [])

@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}))

read(conn)

@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"}

remove_signals(conn, paths, opts \\ [])

@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