Skip to content
scalive
Menu
ConnectingLiveReconnectingOffline
GitHub

Connected lifecycle registration

A LiveView whose connected mount acquires a registration and releases it when the LiveView's lifecycle closes. Ordinary messages do not acquire it again.

Try it

  1. Read Status and Handle. The status shows "Acquired": the connected mount registered this LiveView and stored the handle in its model.

  2. Click Update model a few times. Model-only checks counts up, and the handle stays the same.

  3. Open this page in another tab. That tab shows a different handle, because it is a separate lifecycle.

  4. Click Reset checks. The checks return to 0 without a new registration.

The release happens when the lifecycle closes, so this page cannot show it. The example's tests check that closing the LiveView releases the displayed handle.

Live result

ConnectedReconnectingRead-only

StatusWaiting for connected mount

HandleNot acquired

Model-only checks0

Disconnected. Controls resume after reconnection.

LiveView observability

Interaction inspector

See how actions in the live result above travel through typed messages, server state, protocol frames, and DOM updates.

Reconnect to inspect live interactions

The live result and its inspector resume when the connection returns.

LiveView and registration boundary
scala
final case class LifecycleRegistration(id: String)

trait LifecycleRegistrations:
  def register(owner: String): UIO[LifecycleRegistration]
  def unregister(registration: LifecycleRegistration): UIO[Unit]

final class ConnectedResourceExample(
  instanceId: String,
  registrations: LifecycleRegistrations)
    extends LiveView[ConnectedResourceExample.Msg, ConnectedResourceExample.Model]:
  import ConnectedResourceExample.*

  def mount(ctx: MountContext): Task[Model] = ctx.connection match
    case Connection.Disconnected         => ZIO.succeed(Model())
    case Connection.Connected(connected) =>
      connected.resources
        .acquireRelease(registrations.register(instanceId))(registrations.unregister)
        .map(registration => Model(Some(registration)))

  def handleMessage(model: Model, ctx: MessageContext) =
    case Msg.Check =>
      ZIO.succeed(model.copy(checks = model.checks + 1))
    case Msg.Reset =>
      ZIO.succeed(model.copy(checks = 0))

  def view(model: Signal[Model]): HtmlElement[Msg] =
    val status = model.map(_.registration.fold("Waiting for connected mount")(_ => "Acquired"))
    val handle = model.map(_.registration.fold("Not acquired")(_.id))

    div(
      cls := "docs-managed-work",
      sectionTag(
        cls        := "docs-managed-work-state",
        aria.label := "Connected lifecycle registration",
        p("Status", strong(dataAttr("connected-resource-status") := "", status)),
        p("Handle", code(dataAttr("connected-resource-handle") := "", handle)),
        p(
          "Model-only checks",
          strong(dataAttr("connected-resource-checks") := "", model.map(_.checks.toString))
        )
      ),
      div(
        cls := "docs-managed-work-controls",
        button(typ := "button", on.click(Msg.Check), "Update model"),
        button(typ := "button", on.click(Msg.Reset), "Reset checks")
      )
    )
end ConnectedResourceExample

object ConnectedResourceExample:
  final case class Model(
    registration: Option[LifecycleRegistration] = None,
    checks: Int = 0)

  enum Msg:
    case Check, Reset

How it works

  • mount matches ctx.connection and calls connected.resources.acquireRelease(register)(unregister) only in the connected branch, so the disconnected render acquires nothing.

  • Scalive runs unregister once, with the exact handle, when the lifecycle closes, for example when the visitor leaves or the connection drops. A reconnect mounts a new lifecycle, which registers again.

  • Nested LiveViews own their resources the same way, even when they share one WebSocket with their parent. State shared by a logical application session belongs in a longer-lived service instead.

Register at a route boundary

Keep acquisition in mount when the acquired value belongs in the initial model, as in the preview above. When registration is only a lifecycle side effect, attach it to the route instead, without forwarding the page's lifecycle methods:

Source
scala
object ConnectedResourceRouteExample:
  def application[Msg, Model](
    registrations: LifecycleRegistrations,
    page: LiveView[Msg, Model]
  ): LiveApplication[Any] =
    val registeredRoute = (live / "registered")
      .withConnectedResources { (_, resources) =>
        resources
          .acquireRelease(registrations.register("registered-page"))(registrations.unregister)
          .unit
      }

    Live.router(registeredRoute -> page)

This separate application accepts an ordinary page and captures the registration service. The initializer runs only for the connected root, after all admission and context checks and before the page's connected mount; it does not change the page's context or model. Its test uses the application/route harness to check that disconnected rendering does not acquire, acquisition precedes connected page mount, and leaving releases once even when the transport subsequently closes.

withConnectedResources is also available on named live-session builders. Session initializers run before route initializers, in declaration order at each level. They still own resources per connected root lifecycle, not per named session, application session, or shared WebSocket. See the initializer contract for failures, context projection, and remount behavior.