ADR-012: Room lifecycle — unmount + state hoisting
ADR-012: Room lifecycle — unmount + state hoisting
Status
Accepted
Context
room-lifecycle.md (state machine) specifies that deactivated rooms stay fully
mounted in the DOM and are hidden via CSS, so reactivation is instant and
ephemeral state (scroll position, sub-view stack, search results, draft form
input) is preserved. It explicitly rejects a Suspended state for v1 and notes
lazy mounting as the escape hatch if memory becomes a problem.
The frontend architecture (frontend-architecture.md §1.1) runs as a SvelteKit
SPA with adapter-static and fallback: 'index.html'. Route navigation
(/, /rooms/music, /settings) unmounts the outgoing route’s
component tree — SvelteKit has no keep-alive mechanism. The keep-mounted,
CSS-hidden model therefore does not match how the app actually renders.
FR-ROOM-1 requires the user-visible contract: switching rooms is instant and room state survives deactivation. The question is how that contract is implemented given SvelteKit’s unmounting navigation.
Decision
Deactivated rooms unmount on navigation. Room state is hoisted out of
the component tree into *.svelte.ts state modules (the runes-era store
equivalent, per ADR-013) and rehydrated on remount:
rooms.svelte.tskeeps a per-room preserved-state map (sub-view stack, scroll position, in-flight search groups).- A room’s component owns its rendering; the state modules own anything that must survive deactivation.
- No CSS-hidden keep-alive, no lazy mounting in v1.
This preserves the FR-ROOM-1 contract (instant switch, state preserved) through state hoisting instead of DOM retention.
Alternatives considered
- Keep mounted, hidden via CSS (the
room-lifecycle.mdmodel). Conflicts with SvelteKit route unmounting; would require re-architecting navigation or holding every visited room’s DOM alive for the app’s lifetime — worse for idle memory (undermines the NFR-RESC-1 baseline) and leaks memory as rooms accumulate history. - Lazy mounting (don’t mount until first activation). Solves the memory question but not state preservation — a lazily unmounted room still loses its component-local state on deactivation.
- Suspend/resume (serialize room state, restore on activation). Rejected
for v1 in
room-lifecycle.md; requires memory-pressure signals and a serialization contract for arbitrary room state. A v2 concern.
Consequences
Positive:
- Matches the actual rendering model (SvelteKit SPA unmounting), so the documented lifecycle and the implementation cannot drift.
- Better idle memory than keep-mounted — only the active room’s DOM exists — supporting the app-level NFR-RESC-1 baseline.
- Simpler than lazy mounting: no mount gating logic; the shell just renders the active room’s route.
- Rehydration from state modules is testable in isolation (Vitest, per frontend-architecture.md §13).
Negative:
- Room authors must keep any state that must survive deactivation in
*.svelte.tsmodules, not component-local$state— a constraint the room framework API must communicate (the preserved-state map is shell-managed). - First activation pays a mount cost (component tree build + rehydrate) instead of a CSS show/hide; negligible for v1’s small fixed room set.
- Supersedes the keep-mounted lifecycle note in
room-lifecycle.md; that document’sSuspendedrejection stands, but its “deactivated rooms remain fully mounted” line no longer applies.
References
room-lifecycle.md— superseded mount model (keep-mounted / CSS-hidden)- FR-ROOM-1 (room registration and lifecycle)
frontend-architecture.md§1.1 (SPA mode), §5 (state layer), §16.1 (this deviation)- ADR-009 (Room API TypeScript interface)
- ADR-013 (state management — runes, not stores; the hoisting mechanism)