morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
store.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <concepts>
5#include <exception>
6#include <functional>
7#include <memory>
8#include <type_traits>
9#include <utility>
10#include <variant>
11
12#include "../attributes.hpp"
13#include "../core/bridge.hpp"
14#include "../core/callback_scope.hpp"
15#include "../core/registry.hpp"
16#include "runtime.hpp"
17
18/// @file
19/// @brief `morph::reactive::Store`: user-intent view state, changed only by exhaustive messages.
20///
21/// Specified in `docs/spec/reactive/store.md`.
22
23namespace morph::reactive {
24
25namespace detail {
26
27/// @brief Whether @p Update handles every alternative of the variant @p Msg.
28/// @tparam Update The update callable.
29/// @tparam ViewState The state type.
30/// @tparam Msg Anything but a `std::variant`: never exhaustive.
31template <typename Update, typename ViewState, typename Msg>
32struct UpdateCoversAll : std::false_type {};
33
34/// @brief Whether @p Update handles every alternative of `std::variant<Alts...>`.
35/// @tparam Update The update callable.
36/// @tparam ViewState The state type.
37/// @tparam Alts The message alternatives.
38template <typename Update, typename ViewState, typename... Alts>
39struct UpdateCoversAll<Update, ViewState, std::variant<Alts...>>
40 : std::bool_constant<(std::invocable<Update&, ViewState&, Alts const&> && ...)> {};
41
42/// @brief One update in progress, on the stack of the `send()` running it.
44 bool storeDied = false; ///< Set by the Store's destructor while the update is still running.
45};
46
47/// @brief Marks a Store as inside its update for the guard's lifetime.
49public:
50 /// @param slot The Store's in-update slot; it points at @p frame until the guard ends.
51 /// @param frame The update's frame.
52 UpdateGuard(UpdateFrame*& slot, UpdateFrame& frame) noexcept : _slot{&slot}, _frame{&frame} { slot = &frame; }
53 /// @brief Clears the Store's in-update slot, also when the update throws, unless the Store died.
55 if (!_frame->storeDied) {
56 *_slot = nullptr;
57 }
58 }
59 UpdateGuard(UpdateGuard const&) = delete;
60 UpdateGuard& operator=(UpdateGuard const&) = delete;
61 UpdateGuard(UpdateGuard&&) = delete;
62 UpdateGuard& operator=(UpdateGuard&&) = delete;
63
64private:
65 UpdateFrame** _slot;
66 UpdateFrame* _frame;
67};
68
69} // namespace detail
70
71/// @brief An update that handles every message: `Update&` is invocable as `(ViewState&, Alt const&)` for
72/// each alternative `Alt` of the `std::variant` `Msg`. Any `Msg` that is not a `std::variant` is
73/// never satisfied.
74template <typename Update, typename ViewState, typename Msg>
76
77/// @brief User-intent view state — what is selected, typed, open — changed only by messages.
78///
79/// `ViewState` is a struct of `Signal<T>` fields, built in place by `init` (signals cannot move).
80/// Every `send()` runs the update in one batch, untracked, so N field writes are one flush.
81/// The view reads `state()`; only the update writes.
82///
83/// `send()` off the Runtime's owner is reported (`detail::site::kOffOwner`) and dropped, as is a
84/// `send()` from inside this Store's own update (`detail::site::kSendInUpdate`): an update is a pure
85/// state transition, and a message it sent would run against a half-applied state.
86///
87/// An update may destroy its own Store (a controller that closes its view): the update's callable
88/// stays alive until it returns, and `send()` touches nothing of the Store afterwards. The update itself
89/// must not use the state or the Store once it has destroyed them.
90/// @tparam ViewState A struct of `Signal<T>` fields.
91/// @tparam Msg A `std::variant` of message types.
92template <typename ViewState, typename Msg>
93class Store {
94public:
95 /// @tparam Init Callable `(Runtime&) -> ViewState`, returning by prvalue.
96 /// @tparam Update Callable satisfying `ExhaustiveUpdate<Update, ViewState, Msg>`; it may be move-only.
97 /// @param runtime The runtime. Borrowed: it must outlive the Store.
98 /// @param init Builds the initial state.
99 /// @param update Applies one message to the state.
100 template <typename Init, typename Update>
101 requires std::invocable<Init&, Runtime&> && std::same_as<std::invoke_result_t<Init&, Runtime&>, ViewState> &&
102 std::move_constructible<Update> && ExhaustiveUpdate<Update, ViewState, Msg>
103 Store(Runtime& runtime MORPH_LIFETIMEBOUND, Init init, Update update)
104 : _rt{&runtime},
105 _state{init(runtime)},
106 _update{std::make_shared<Update>(std::move(update))},
107 _apply{&applyUpdate<Update>} {}
108
109 /// @brief Destroys the state, and tells an update still running that the Store is gone.
111 if (_updating != nullptr) {
112 _updating->storeDied = true;
113 }
114 }
115 Store(Store const&) = delete;
116 Store& operator=(Store const&) = delete;
117 Store(Store&&) = delete;
118 Store& operator=(Store&&) = delete;
119
120 /// @brief Applies @p msg in one batch. Refused off the owner, and inside an update.
121 ///
122 /// An exception from the update propagates to the caller; writes made before it still flush.
123 /// @param msg The message.
124 void send(Msg msg) {
125 auto& core = *_rt->core();
126 if (!core.checkOwner()) {
127 return;
128 }
129 if (_updating != nullptr) {
130 core.report(detail::site::kSendInUpdate);
131 return;
132 }
133 // The update may destroy this Store: everything used after it starts is a local, and the
134 // callable is held here so that it outlives the Store until it returns.
135 Runtime& runtime = *_rt;
136 std::shared_ptr<void> const update = _update;
137 void (*const apply)(void*, ViewState&, Msg const&) = _apply;
138 ViewState& state = _state;
139 detail::UpdateFrame*& slot = _updating;
141 // The guard empties the slot when the update ends, unless the Store died, and then the slot is gone.
142 // NOLINTNEXTLINE(clang-analyzer-core.StackAddressEscape)
143 runtime.batch([&] {
144 runtime.untracked([&] {
145 detail::UpdateGuard const guard{slot, frame};
146 apply(update.get(), state, msg);
147 });
148 });
149 }
150
151 /// @brief The state, for reading. A field's `get()` tracks exactly that field.
152 /// @return The state.
153 [[nodiscard]] ViewState const& state() const noexcept { return _state; }
154
155 /// @brief A callable that sends @p msg each time it is called — what a button binds to.
156 /// @param msg The message, copied into the callable.
157 /// @return The callable; it refers to this Store, which must outlive it.
158 [[nodiscard]] std::function<void()> action(Msg msg) {
159 return [this, msg = std::move(msg)] { send(msg); };
160 }
161
162private:
163 template <typename Update>
164 static void applyUpdate(void* update, ViewState& state, Msg const& msg) {
165 std::visit([&](auto const& alternative) { (*static_cast<Update*>(update))(state, alternative); }, msg);
166 }
167
168 Runtime* _rt;
169 ViewState _state;
170 // Type-erased and shared, so a move-only update fits and a running send() can keep it alive.
171 std::shared_ptr<void> _update;
172 void (*_apply)(void*, ViewState&, Msg const&);
173 detail::UpdateFrame* _updating = nullptr;
174};
175
176/// @brief Executes @p action and sends its result, or its failure, to @p store as one message.
177///
178/// Callbacks land on the handler's GUI executor, which must be the Store's owner; each delivery is
179/// one batch. @p scope gates both. `Query` and `Mutation` follow the same delivery rules without a
180/// Store to send into.
181///
182/// Every call delivers: there is no latest-wins. Superseding an earlier call means stopping, resetting
183/// or destroying the scope it was given; `Query` provides latest-wins.
184///
185/// A throw from @p toMsg or from the update it sends to, on the success path, or from @p toFailMsg, is
186/// caught and logged by the `Completion` delivering it. It is not turned into a failure message.
187/// @tparam ViewState The Store's state type.
188/// @tparam Msg The Store's message variant.
189/// @tparam Model The handler's model type.
190/// @tparam Sharing The handler's sharing policy.
191/// @tparam Action The action type.
192/// @tparam ToMsg Callable `(Result const&) -> alternative of Msg`.
193/// @tparam ToFailMsg Callable `(std::exception_ptr const&) -> alternative of Msg`.
194/// @param store Receives the message. Must outlive every delivery @p scope does not gate.
195/// @param handler Executes the action.
196/// @param scope Gate: a reply arriving after it is stopped or destroyed is dropped.
197/// @param action The action.
198/// @param toMsg Maps the result to a message.
199/// @param toFailMsg Maps the failure to a message.
200template <typename ViewState, typename Msg, typename Model, typename Sharing, typename Action, typename ToMsg,
201 typename ToFailMsg>
203 async::CallbackScope const& scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg) {
205 handler.execute(std::move(action))
206 .then(scope, [&store, toMsg = std::move(toMsg)](Result const& result) { store.send(Msg{toMsg(result)}); })
207 .onError(scope, [&store, toFailMsg = std::move(toFailMsg)](std::exception_ptr const& error) {
208 store.send(Msg{toFailMsg(error)});
209 });
210}
211
212} // namespace morph::reactive
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
RAII wrapper that binds a single model type to a Bridge.
Definition bridge.hpp:2583
::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > execute(Action action)
Dispatches action via the underlying Bridge and returns a Completion.
Definition bridge.hpp:2667
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
decltype(auto) batch(F &&fn)
Runs fn as one batch: every write inside it is one flush.
Definition runtime.hpp:104
decltype(auto) untracked(F &&fn) const
Runs fn with tracking off: reads inside it subscribe nothing.
Definition runtime.hpp:136
User-intent view state — what is selected, typed, open — changed only by messages.
Definition store.hpp:93
~Store()
Destroys the state, and tells an update still running that the Store is gone.
Definition store.hpp:110
void send(Msg msg)
Applies msg in one batch. Refused off the owner, and inside an update.
Definition store.hpp:124
Store(Runtime &runtime, Init init, Update update)
Definition store.hpp:103
ViewState const & state() const noexcept
The state, for reading. A field's get() tracks exactly that field.
Definition store.hpp:153
std::function< void()> action(Msg msg)
A callable that sends msg each time it is called — what a button binds to.
Definition store.hpp:158
Marks a Store as inside its update for the guard's lifetime.
Definition store.hpp:48
~UpdateGuard()
Clears the Store's in-update slot, also when the update throws, unless the Store died.
Definition store.hpp:54
UpdateGuard(UpdateFrame *&slot, UpdateFrame &frame) noexcept
Definition store.hpp:52
An update that handles every message: Update& is invocable as (ViewState&, Alt const&) for each alter...
Definition store.hpp:75
constexpr char const * kSendInUpdate
Store::send was called from inside an update; the message is dropped.
Definition graph.hpp:48
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Definition registry.hpp:48
Whether Update handles every alternative of the variant Msg.
Definition store.hpp:32
One update in progress, on the stack of the send() running it.
Definition store.hpp:43
bool storeDied
Set by the Store's destructor while the update is still running.
Definition store.hpp:44
std::function< void()> Action
A widget event handler taking no arguments.
Definition view.hpp:33