Five header-only primitives that carry every piece of state in the app. They are
pure C++17 with no Qt, no QObject and no moc, so core/ logic and its tests build
and run on the host with no Qt/SDL/sodium present.
Data flows one way. A source of truth owns state, composers purely derive from it,
the UI binds and renders, and the UI sends commands back to the source. Nothing
below qml/ knows the UI exists. docs/ARCHITECTURE.md covers how the layers above
this kernel fit together; this file covers the primitives themselves.
| You have… | Use | File |
|---|---|---|
| a value that changes over time, observed by others | Observable<S> |
Observable.h |
| state owned from a socket/timer/cache/setting | StateSource<S> (expose state(), mutate via setState) |
StateSource.h |
| one value purely derived from other Observables | Composer<Out, Ins...> (one pure transform; no IO/events) |
Composer.h / Combiner.h |
| a side effect driven by a state | Controller<S> (implement apply; start() idempotent; choose stop() teardown) |
Controller.h |
| durable keyed storage | Repository<K,V> / KeyedRepository<K,V> (dumb, synchronous, thread-safe; wrap in a Source for reactive reads) |
Repository.h |
Three more conventions are classes you write per feature rather than base classes
here. A Coordinator is a plain QObject service for imperative cross-cutting
commands over several sources; it re-exposes a child's Observable by reference and
never mirrors it, because a mirror is a second writer. A Gateway is an IO or
native boundary holding no domain state. A Reducer/Mapper is a pure free
function in core/ for a (state, event) -> result decision or a domain-to-UI shape.
Observableis hot and always has a value.set/updatenotify only on change (operator==), soSmust be equality-comparable.subscribe(cb, emitCurrent=true)replays the latest to a new subscriber. The returnedSubscriptionis a move-only RAII unsubscribe handle that is safe in either destruction order.subscribeisconstso aControllerholding aconst Observable&can still attach.Combiner/Composercompute once eagerly at construction, so a consumer never observes a stale initial value, and recompute on any upstream change. Keep thetransforma free function so it is testable in isolation. They are non-movable (they capturethis); own one as a member or viamake_unique.StateSourceexposesstate()read-only and mutates only through the protectedsetState.start()/stop()are opt-in; most sources here are process-scoped and simply live for the lifetime of the app.Controllerapplies the current value onstart()and is idempotent.stop()defaults to tearing down the subscription, and is overridable for the deliberate cases that must survive a restart.
Notifications fire on the thread that called set. When binding to the UI from a
background thread, marshal to the GUI loop (Qt::QueuedConnection /
QMetaObject::invokeMethod).
Do not route the input hot path through this kernel. The path from an input event
to the sendto call stays plain, allocation-free C++.
tests/test_kernel.cpp exercises all five primitives. The probes alongside it
(StateSourceProbe.h, ComposerProbe.h, ControllerProbe.h) capture emission
sequences rather than just final values, and RepositoryContract.h runs the eight
property tests every concrete repository must pass.