Lifecycle, state ownership, and reconnects
Treat Each Mount As Independent
The project anatomy introduced the disconnected HTTP mount and the separate connected socket mount. Both must build a valid model from their inputs; the connected mount cannot recover the model created for the HTTP response.
Match ctx.connectiondef connection: Connection[Connected]
when initial state or work depends on the lifecycle. The lifecycle example only
records which kind of mount created the model:
def mount(ctx: MountContext): Task[Model] =
val connectedMount = ctx.connection match
case Connection.Disconnected => false
case Connection.Connected(_) => true
ZIO.succeed(Model(connectedMount, currentTitle = DefaultTitle))Start connection-owned work only from a matched
Connection.Connected(capabilities) branch. This example starts no clock or
other background task.
Follow The Handoff From HTTP To Live
The browser keeps one document while server ownership crosses two independent
lifecycles. Read the following sequences as one handoff: the HTTP request creates
Model A and useful DOM, then LiveSocket uses the retained bootstrap data to
join and create Model B.
Disconnected HTTP render. The initial request owns Model A only long enough to produce the complete response:
Lifecycle trace
Disconnected HTTP render
One HTTP request produces useful HTML; its temporary model is not retained after the response.
- Browser
- Scalive runtime
- Your LiveView
Phase 1Request
Browser
01NavigateStarts navigation to the Live route.
Scalive runtime
03Prepare lifecycleDecodes the route and prepares the request-scoped lifecycle.
Phase 2Disconnected lifecycle
Scalive runtime
08Assemble documentApplies layouts, creates the live root, and embeds the signed session and CSRF metadata.
Phase 3Response and teardown
Lifecycle boundary
10End request lifecycleModel A ends with the request and is not carried into the socket lifecycle. A future LiveView join creates a fresh model.
When that request ends, the browser retains the rendered DOM, signed session, and CSRF metadata. Model A and its request-owned resources are gone.
Connected LiveSocket mount. The browser presents that bootstrap data while joining the live endpoint. A successful join starts a fresh lifecycle:
Lifecycle trace
Connected LiveSocket mount
A LiveSocket join validates the document's bootstrap data, creates Model B, and reconciles the existing DOM.
- Browser
- Scalive runtime
- Your LiveView
Phase 1Connect and join
Browser
01Discover live rootReads the root id, signed session, and CSRF metadata from the disconnected document.
Scalive runtime
04Validate joinVerifies CSRF authorization, the signed topic-bound session, route, live session, mount claims, and root layout. Browser connect parameters remain untrusted.
Phase 2Connected lifecycle
Scalive runtime
09Commit Model BModel B and its rendered snapshot become current only after the initial render and its after-render hooks succeed; the runtime then computes the initial diff.
Phase 3Join response
Browser
11Reconcile DOMPatches the existing disconnected DOM and marks the LiveView connected.
The join reply contains an initial rendered diff for Model B. The browser reconciles it with the existing disconnected DOM rather than loading a second HTML document. This preserves a useful initial page while keeping the two server models and their resources independent.
Follow The Lifecycle Timeline
The complete lifecycle can now be summarized as:
| Stage | Connection | Model and work |
|---|---|---|
| HTTP mount | Disconnected | Build a temporary model for useful initial HTML |
| HTTP render | Disconnected | Render the response inside layouts; this model ends with the request |
| Socket mount | Connected | Build a new model and start connection-owned work |
| Initial live render | Connected | Render and commit the initial connected tree |
| Message handling | Connected | Produce a proposed model from the last committed model |
| Render and diff | Connected | Render the proposal, compare trees, then commit after success |
| Socket termination | Ending | Interrupt and release connection-owned resources |
| Rejoin | Connected | Start a new lifecycle and mount again from durable inputs |
Async completions and subscription values enter the same typed message flow as browser events. Lifecycle capabilities such as flash, navigation, async work, and subscriptions belong to the context for the phase in which they are valid.
Put State In The Right Lifetime
| State | Owner and lifetime | Examples |
|---|---|---|
| Render-derived value | Recomputed from the model | Labels, totals, disabled state |
| Disconnected model | One HTTP render | Initial page data and useful no-JavaScript HTML |
| Connected model | One socket lifecycle | Selection, validation, loaded view data |
| Lifecycle resource | Current connection | Subscriptions, async tasks, uploads |
| Injected service | Application-defined lifetime | Repositories, caches, shared domain state |
| Durable storage | Beyond the process or connection | Orders, documents, audit history |
| Browser-local state | Current document or hook | Focus, scroll, third-party widget state |
A module-level mutable value is not visitor state. A LiveView model is isolated to one lifecycle. An injected service can deliberately outlive that lifecycle, but it must define its own concurrency, isolation, and durability semantics.
State that must survive reconnect belongs in a service or durable store. Reload it during mount and keep only the connection's rendering and interaction state in the model.
Treat Reconnect As A New Lifecycle
When the transport rejoins, the LiveView mounts again. Rebuild its model from durable inputs, restart required connection-scoped work, and expect the old socket's subscriptions, async tasks, uploads, and nested LiveViews to be released.
Use this checklist:
Make
mountsafe to run repeatedly.Match
ctx.connectionand run socket-only work only in theConnection.Connected(capabilities)branch.Use lifecycle-managed APIs for async work and subscriptions.
Make repeated external mount effects idempotent where necessary.
Test the reconnect behavior that matters to the application in a browser.
Understand Failure And Commit Boundaries
A handler returns a proposed model. Scalive renders that model and makes it the next committed model only after the transition and render path succeed. An unhandled handler or render failure therefore does not commit the proposal.
This is not a database transaction. Service calls, writes, or other external effects completed before a later render failure are not rolled back. Recover expected, user-actionable failures into explicit model state and use normal transaction boundaries for durable operations.
A failed connected lifecycle may terminate and later be replaced by a fresh join. Reconnect logic must not depend on recovering the previous model object. Exact protocol diagnostics and operational recovery belong in the troubleshooting guide.
Render Connection State Declaratively
connection.visibleWhenConnectedlazy val visibleWhenConnected: Vector[Mod[Nothing]]
and
connection.visibleWhenDisconnectedlazy val visibleWhenDisconnected: Vector[Mod[Nothing]]
render browser bindings that react immediately to socket state. Use them for an
offline label or to disable controls whose events cannot reach the server.
The server does not receive a normal application message merely because the transport drops. Design recovery around remounting rather than an assumed disconnect message.
Test At The Lifecycle Boundary
Use a disconnected test for initial HTTP state and a connected test for typed server interactions. Use a real browser when the behavior depends on transport loss, reconnects, JavaScript, or DOM patching. The testing guide explains these boundaries and their available support.