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
Read Status and Handle. The status shows "Acquired": the connected mount registered this LiveView and stored the handle in its model.
Click Update model a few times. Model-only checks counts up, and the handle stays the same.
Open this page in another tab. That tab shows a different handle, because it is a separate lifecycle.
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-onlyStatusWaiting for connected mount
HandleNot acquired
Model-only checks0
Disconnected. Controls resume after reconnection.
Interaction inspector
See how actions in the live result above travel through typed messages, server state, protocol frames, and DOM updates.
The live result and its inspector resume when the connection returns.
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, ResetView source (documentation/site/src/scalive/docs/examples/ConnectedResourceExample.scala:10-67)
How it works
mountmatchesctx.connectionand callsconnected.resources.acquireRelease(register)(unregister)only in the connected branch, so the disconnected render acquires nothing.Scalive runs
unregisteronce, 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:
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)View source (documentation/site/src/scalive/docs/examples/ConnectedResourceExample.scala:71-83)
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.
Related guides
Connected resources explains acquisition, release, and when to use a route initializer.