ADR-009: Room API contract — TypeScript interface with lifecycle hooks
ADR-009: Room API contract — TypeScript interface with lifecycle hooks
Status
Accepted
Context
Rooms are in-core UI components (Constraint C7). They define navigation, search behavior, metadata layout, player expansion, and context menus (Principle 2). The room framework API is the contract between the shell and room authors.
Three approaches to defining this contract:
- Svelte component slot / store contract: The shell exposes Svelte stores; rooms consume them. Couples room authors to Svelte reactivity primitives.
- TypeScript interface: A defined
Roominterface that rooms implement. Thecomponentfield is a Svelte component (because that’s what the shell renders), but the contract is the interface, not the component framework. - Web Component / iframe: Maximum decoupling — rooms could be written in any framework. But loses all Svelte integration, reactivity sharing, and bundle optimization.
FR-ROOM-1 through FR-ROOM-3 define what the API must support: registration, lifecycle (activate/deactivate/suspend), search filter declaration, and player expansion registration.
Decision
The room API is a TypeScript interface. Room authors implement this interface; the shell registers the implementation. The shell renders the component field, which is a Svelte component, but this is an implementation detail of the shell, not part of the contract.
interface Room {
id: string;
displayName: string;
icon: string; // icon identifier or path
// The shell renders this component
component: ComponentType<RoomProps>;
// Lifecycle hooks — optional
onActivate?(ctx: RoomContext): void;
onDeactivate?(): void;
// Search — optional. If not implemented, this room
// uses the default room-level search (all plugins, all types)
onSearch?(query: string): SearchHandler | void;
// Player expansion — optional. If not implemented,
// the default player expansion is shown
playerExpansion?: ComponentType<PlayerExpansionProps>;
}
interface RoomProps {
active: boolean;
ctx: RoomContext;
}
interface RoomContext {
search: (query: string) => Promise<SearchResults>;
queue: QueueHandle;
library: LibraryHandle;
playback: PlaybackHandle;
navigate: (view: string) => void;
}
Key design rules:
- The interface is the contract. The Svelte component is a rendering detail.
RoomContextprovides access to platform services (search, queue, library, playback). These are handles, not stores — the room calls them imperatively.- Lifecycle hooks (
onActivate,onDeactivate) are optional. A room that doesn’t need cleanup doesn’t implement them. onSearchis optional. If not implemented, the room uses the default shell-level search. If implemented, the search manager applies the room’s declared filters (FR-ROOM-2).playerExpansionis optional (FR-ROOM-3). If not provided, the shell shows a default expansion view.
Alternatives considered
- Svelte store contract: Rooms would interact with the platform through Svelte stores (
$searchResults,$currentTrack, etc.). This couples room authors to Svelte’s reactivity model. A room built with$staterunes vswritablestores vs a third-party state library would need to translate between its internal model and the shell’s store contract. The interface approach is framework-agnostic at the contract level. - Slot/component injection: The shell provides
<slot>positions and rooms inject content. This lets the shell dictate layout (sidebar, header, content area). R9 in the risk register warns against coupling rooms to a specific UI toolkit. Slots couple rooms to the shell’s layout; the interface approach leaves layout to the room. - Full abstraction (Web Components): Rooms as custom elements. Framework-independent, but loses HMR, bundle sharing, and type safety for room-specific props. Overkill for v1 when all rooms are in-repo.
Consequences
Positive:
- The contract is decoupled from the rendering framework. A future room migration to Web Components or iframes is possible if the
componentfield’s type changes. RoomContextis a capability-based API — the shell injects only what the room needs. This naturally prevents rooms from accessing internal platform state.- Optional lifecycle hooks keep the API surface minimal. A simple room (Music Room v1) implements only
component,id, anddisplayName.
Negative:
ComponentType<RoomProps>is Svelte-specific. A room author must write a Svelte component. This conflicts with the statement that the contract is framework-independent. Mitigation (v1): re-exportComponentTypefrom a shell-owned module rather than importing it directly from Svelte. Room authors import from the shell, not fromsvelte. When Web Component or WASM room support is added in v2, the re-export is swapped and room code does not change:
The interface structure (lifecycle hooks, context API, search handler) remains the same even if// @rymflux/shell/types.ts export type { ComponentType } from 'svelte'; // re-exported, not leakedcomponentchanged to a different type.- TypeScript types are documentation that can drift from implementation. Mitigation: the room framework validates room registrations at runtime (type-check the implemented interface).
- The interface is defined by the core platform team. Room authors outside the core cannot extend it. Mitigation: the interface is versioned; new hooks can be added as optional fields without breaking existing rooms.
References
- Constraint C7 (rooms are UI components) — (constraints-assumptions.md)
- Principle 2 (rooms own experiences) — (vision.md)
- FR-ROOM-1, FR-ROOM-2, FR-ROOM-3 (functional.md)
- R9 (risk-register.md — coupling rooms to UI toolkit)
Extension (immutable note)
Per this ADR’s versioning rule (new hooks/fields may be added as optional
members without breaking existing rooms), ui-design-system.md §4.4 extends
Room with an optional themeId?: string — the room’s default theme when
active, falling back to the global default. Optional, additive, and shell
managed; room components never touch theming. No change to any required
member; recorded here rather than a new ADR.