StarView.Stream (star_view v0.7.0)

Copy Markdown View Source

Opens an SSE stream and owns its receive loop, liveness probes, and teardown.

case StarView.Stream.open(conn, key: current_user.id) do
  {:ok, conn} ->
    StarView.Stream.run(conn,
      connect: &subscribe/1,
      info: &handle_update/2,
      disconnect: &unsubscribe/1
    )

  {:error, conn} -> conn
end

Callbacks receive and return the current conn; :info may return {:halt, conn}. :disconnect runs once after a normal end, after the exact stream claim is released. Raises propagate after claim release. A typed SSE write failure with reason :closed, :econnreset, or :epipe ends normally.

Probes run on a fixed deadline (:idle_check, default 30,000ms), even when application messages keep arriving. Bandit's HTTP/2 flow-control messages remain in the mailbox for the adapter. Generation-tagged replacement messages stop only their own stream, so a stale message cannot stop a newer stream on a reused HTTP/1.1 keep-alive process.

Coalesced refresh

Pass :render to re-render after scoped invalidations. :info (or any callback) calls invalidate/1; the loop then calls :render once for every burst of invalidations already queued. :render_interval (ms, default 0) is the minimum time between render starts. With :render_on_connect (default true when :render is given) one snapshot render follows :connect, closing the gap between page load, subscription and reconnect.

StarView.Stream.run(conn,
  connect: &subscribe/1,
  info: fn {:order_changed, _id}, conn -> StarView.Stream.invalidate(conn) end,
  render: &render_orders/1,
  render_interval: 50
)

:render may return {:halt, conn}. Reload and authorize business state in the callback from trusted conn assigns; invalidation messages should carry identifiers, not data. Pending timers and render messages are cancelled when run/2 returns or raises. A request process can own one active run/2; nesting another run inside a callback raises ArgumentError.

Lifecycle concepts adapted from dstar (MIT), Copyright (c) 2025 Rico Trevisan.

Summary

Functions

Requests one coalesced :render call from the running stream loop.

Starts SSE, optionally claiming {key, tabId} atomically.

Runs until a client disconnects, :info halts, or a newer stream takes over.

Functions

invalidate(conn)

@spec invalidate(Plug.Conn.t()) :: Plug.Conn.t()

Requests one coalesced :render call from the running stream loop.

Further calls before that render are no-ops, and the render waits for :render_interval since the previous render started. Raises ArgumentError unless the conn belongs to the currently running run/2 with a :render callback; a conn kept after run/2 returns (including in the :disconnect callback) cannot schedule a render.

open(conn, opts \\ [])

@spec open(Plug.Conn.t(), keyword()) :: {:ok, Plug.Conn.t()} | {:error, Plug.Conn.t()}

Starts SSE, optionally claiming {key, tabId} atomically.

Returns {:ok, conn} with chunked SSE, or {:error, conn} with a halted normal HTTP response: 400/413 for invalid/oversized signals, 503 for a refused keyed claim. Do not subscribe on the error path. Accepts :max_bytes for raw signals. The optional registry is required only when a usable tab id and :key are both present.

run(conn, opts \\ [])

@spec run(Plug.Conn.t(), keyword()) :: Plug.Conn.t()

Runs until a client disconnects, :info halts, or a newer stream takes over.

Options: :connect (conn -> conn), :info (message, conn -> conn or {:halt, conn}), :disconnect (conn -> any), :idle_check (positive ms). Optional :replaced receives the legacy {:EXIT, pid, :replaced} message after exact claim release and before disconnect.

Refresh options: :render (conn -> conn or {:halt, conn}), :render_interval (non-negative ms, default 0) and :render_on_connect (boolean, default true when :render is given). See "Coalesced refresh".