Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

ADR-013: Frontend state management — Svelte 5 runes in `.svelte.ts` singletons

ADR-013: Frontend state management — Svelte 5 runes in .svelte.ts singletons

Status

Accepted

Context

The frontend (SvelteKit 2 + Svelte 5, SPA mode — frontend-architecture.md §1) has shared state that crosses the shell, the room framework, and rooms: playback state, the queue, session search, library, plugin status, toasts, settings. The queue is already frontend-authoritative in “Svelte state” (ADR-010), but no policy exists for how all shared state is built.

Svelte 5 offers two reactivity models: legacy writable/readable/derived stores and the $state/$derived/$effect rune system. Runes work in plain .svelte.ts modules (module scope), not just components — they can model state outside the component tree, which ADR-012 requires (state hoisting so rooms can unmount without losing state).

Decision

Shared frontend state is built on Svelte 5 runes, in *.svelte.ts modules. Legacy stores are not used for new code; if a need for one arises (external store interop, StartStopNotifier semantics), it is a localized exception, not a precedent.

The canonical pattern (frontend-architecture.md §5.1):

  • A class with $state fields, exported as a singleton instance. The instance is a stable reference — every importer sees the same reactive object, the runes-era equivalent of a store without subscription machinery.
  • $state for fields, $derived/$derived.by for computed values, methods for actions. $effect is reserved for genuine side effects (e.g. announcing now-playing to screen readers) — never to sync state to state.
  • Files that use runes at the top level are named *.svelte.ts (mandatory — runes run in module scope).
  • Export singletons, never raw $state primitives — that loses the stable-reference benefit and invites re-assignment bugs.
  • State modules may call the IPC bridge and other state modules; they must not import components, routes, or $app/*.

Alternatives considered

  • Legacy stores (writable/derived). Work, but the two reasons to reach for them — external store interop and StartStopNotifier semantics — do not arise in v1. They add subscription machinery the rune system already makes unnecessary and split the codebase across two reactivity models.
  • Third-party signals / global state (Zustand-style). An extra dependency to recreate what Svelte 5 runes provide natively; loses Svelte’s framework-integrated $derived and template tracking.
  • Non-reactive module singletons (plain exported objects + manual notification). No reactivity; components would hand-subscribe. Rejected.
  • Component-context-only state (setContext/getContext). Cannot outlive a route’s unmount, so it cannot carry the FR-ROOM-1 preserved-state contract that ADR-012 depends on.

Consequences

Positive:

  • Single reactivity model across the frontend; no store/rune translation layer for rooms or components.
  • State survives route unmount (module scope), which is what makes ADR-012’s unmount + rehydrate model work.
  • State modules are plain modules — testable in Vitest outside components, with the IPC bridge mocked (frontend-architecture.md §13).
  • Follows ADR-010’s existing direction (the frontend already owns the authoritative queue in Svelte state).

Negative:

  • Rune usage at module scope requires the *.svelte.ts extension; forgetting it produces hard-to-trace Svelte 5 errors. Enforced by convention + review.
  • $effect is a footgun (runs after render, reacts to any accessed state); the policy restricts it to genuine side effects.
  • Svelte 5 runes are newer than the legacy stores many contributors know; the canonical module pattern (§5.1) is the reference to copy.

References

  • frontend-architecture.md §5 (state layer — runes, not stores), §5.1/§5.2 (canonical module pattern, state module inventory), §13 (testing)
  • ADR-010 (queue management — frontend-authoritative; Svelte state precedent)
  • ADR-012 (room lifecycle — unmount + state hoisting, the consumer of this policy)

Was this page helpful?