morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
control.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <chrono>
5#include <cstddef>
6#include <cstdint>
7#include <exception>
8#include <functional>
9#include <map>
10#include <memory>
11#include <optional>
12#include <stdexcept>
13#include <string>
14#include <utility>
15#include <vector>
16
17#include "../attributes.hpp"
18#include "../core/bridge.hpp"
19#include "../core/callback_scope.hpp"
20#include "../core/completion.hpp"
21#include "../core/registry.hpp"
22#include "detail/graph.hpp"
23#include "runtime.hpp"
24#include "scheduler.hpp"
25#include "signal.hpp"
26
27/// @file
28/// @brief Declarative control: `Query`, `Mutation` and `Subscription` turn server interaction
29/// into reactive state, so a controller states what it depends on and what a write
30/// invalidates instead of sequencing calls by hand.
31///
32/// Specified in `docs/spec/reactive/control.md`.
33
34namespace morph::reactive {
35
36/// @brief The one place an `exception_ptr` becomes display text.
37/// @param error The captured exception, or null.
38/// @return `what()` for a `std::exception`, `"unknown error"` for anything else, empty for null.
39[[nodiscard]] inline std::string errorMessage(std::exception_ptr const& error) {
40 if (error == nullptr) {
41 return {};
42 }
43 try {
44 std::rethrow_exception(error);
45 } catch (std::exception const& exception) {
46 return exception.what();
47 } catch (...) {
48 return "unknown error";
49 }
50}
51
52/// @brief Something a `Mutation` can tell to fetch again after it succeeds.
53class Refetchable {
54public:
55 Refetchable() = default;
56 virtual ~Refetchable() = default;
57 Refetchable(Refetchable const&) = delete;
58 Refetchable& operator=(Refetchable const&) = delete;
59 Refetchable(Refetchable&&) = delete;
60 Refetchable& operator=(Refetchable&&) = delete;
61
62 /// @brief Re-issues the current request; does nothing while idle.
63 virtual void refetch() = 0;
64};
65
66/// @brief A query's registration with the mutations that invalidate it.
67///
68/// The query owns the link and clears it when it is destroyed; a mutation holds it shared
69/// (`MutationOptions::invalidates`). So a mutation never refetches a query that is gone, and the two may be
70/// destroyed in either order.
71class InvalidationLink final {
72public:
73 /// @param target The query to refetch. Borrowed until `clear()`.
74 explicit InvalidationLink(Refetchable& target MORPH_LIFETIMEBOUND) noexcept : _target{&target} {}
75
76 /// @brief Refetches the query, or does nothing once it is gone.
77 void refetch() const {
78 if (_target != nullptr) {
79 _target->refetch();
80 }
81 }
82
83 /// @brief Unregisters the query; called by its destructor.
84 void clear() noexcept { _target = nullptr; }
85
86 /// @brief Whether the query is still registered.
87 /// @return False once the query has been destroyed.
88 [[nodiscard]] bool live() const noexcept { return _target != nullptr; }
89
90private:
91 Refetchable* _target;
92};
93
94namespace detail {
95
96/// @brief Checks that @p handler delivers its callbacks on @p runtime's owner, reporting `site::kHandlerExecutor`
97/// when it does not.
98///
99/// A control node writes its signals from the handler's callbacks, so they must run where the runtime runs. The
100/// check is by affinity, not by pointer identity: a distinct serial executor that runs its tasks on the owner's
101/// thread is accepted. An executor that is not serial is reported at once. Any other serial executor is checked
102/// where it runs, by one posted task, which reports when it finds itself off the owner. No callback executor
103/// means the bridge's owner.
104/// @tparam M The handler's model type.
105/// @tparam S The handler's sharing policy.
106/// @param runtime The runtime the node belongs to.
107/// @param handler The handler the node is built over.
108template <typename M, typename S>
109void checkHandlerExecutor(Runtime const& runtime, bridge::BridgeHandler<M, S> const& handler) {
110 exec::IExecutor* const callbacks = handler.guiExecutor();
111 exec::IExecutor& delivery = callbacks != nullptr ? *callbacks : handler.owner();
112 std::shared_ptr<RuntimeCore> const& core = runtime.core();
113 if (!delivery.isSerial()) {
114 core->report(site::kHandlerExecutor);
115 return;
116 }
117 if (&delivery == &core->owner()) {
118 return;
119 }
120 delivery.post([weak = std::weak_ptr<RuntimeCore>{core}] {
121 if (auto const live = weak.lock(); live != nullptr && !live->onOwner()) {
122 live->report(site::kHandlerExecutor);
123 }
124 });
125}
126
127} // namespace detail
128
129/// @brief How a `Query` refreshes besides key changes, and how long it lets a key settle.
131 /// @brief Runs the timed refresh and the debounce; required when either is positive. Borrowed: it must
132 /// outlive the query.
134 /// @brief Re-fetch the current key this long after each call is issued, unless a call is then in flight;
135 /// zero means no timed refresh, and a negative value is refused.
136 std::chrono::milliseconds refreshEvery{0};
137 /// @brief Fetch a changed key only once it has been stable this long; zero fetches at once, and a negative
138 /// value is refused. The first key and a change to idle apply at once.
139 std::chrono::milliseconds debounce{0};
140};
141
142/// @brief A derived async resource: the result of fetching whatever its tracked key names.
143///
144/// An internal Effect reads `key`. When the key changes — equality-gated when `==` is usable for `A`
145/// (`kEqualityUsable`), otherwise on every re-run — the query fetches it under a fresh
146/// generation of its `CallbackScope`, so a reply for a superseded key is dropped (**latest wins**) and a
147/// call that carries a stop source (a bridge call whose handler returns a `Task`) is asked to stop.
148/// Destroying the query does the same to the call in flight. `nullopt` means idle: nothing in flight,
149/// value and error cleared. `value()` is kept while a refetch or a new key is in flight. A failure clears
150/// it when the call was for a different key than the one `value()` was fetched for, so a view never shows
151/// one key's result beside another key's failure; a failed refetch of the same key keeps it. Without `==` on
152/// `A`, every re-run of the key counts as a different key.
153///
154/// With `QueryOptions::refreshEvery`, the query re-fetches its current key one period after each call is
155/// issued, whatever issued it: a key change, `refetch()`, an invalidation, or the timer itself. A tick that
156/// finds a call in flight issues nothing and waits another period, so a timed refresh never supersedes a call
157/// and a fetch slower than the period still delivers. While idle no timer runs. A tick off the Runtime's
158/// owner is reported (`detail::site::kOffOwner`) and refused, and the timer then stays stopped until the next
159/// call is issued. Destroying the query cancels the timer, and a tick that arrives afterwards is dropped.
160///
161/// The fetcher runs untracked, whoever issues the call: a `refetch()` from inside an Effect does not
162/// subscribe that Effect to what the fetcher reads. `refetch()` inside a Computed's computation is reported
163/// (`detail::site::kIssueInComputed`) and refused before anything is issued.
164///
165/// With `QueryOptions::debounce`, a changed key is fetched only once it has stayed the same for that long: the
166/// call for the key it replaces is superseded at once (its reply is dropped), `pending()` is true while the query
167/// waits, and each further change restarts the wait. The first key, and a change to idle, apply at once; so does
168/// `refetch()`, which also ends a wait. Destroying the query cancels the wait.
169///
170/// A fetcher that throws instead of returning a `Completion` fails the request: the exception becomes
171/// `error()` and nothing is pending. A key that throws is an Effect that throws: reported
172/// (`detail::site::kEffectThrew`), and the query keeps its state. A delivery off the Runtime's owner is
173/// reported (`detail::site::kOffOwner`) and dropped.
174///
175/// The key or the fetcher may destroy the query (a remount), and so may another handler of the reply
176/// or an Effect the reply wakes; the query is not touched afterwards. The action a fetcher receives
177/// belongs to the query, so a fetcher that destroys the query must not use it afterwards.
178/// @tparam A The action (request) type.
179/// @tparam R The result type; defaults to the action's registered result.
180// NOLINTNEXTLINE(readability-redundant-typename) -- valid everywhere; implicit typename here is unverified on MSVC
181template <typename A, typename R = typename model::ActionTraits<A>::Result>
182class Query final : public Refetchable {
183public:
184 /// @brief The tracked key: which request the query currently stands for, or `nullopt` for none.
185 using Key = std::function<std::optional<A>()>;
186 /// @brief Issues one request.
187 using Fetch = std::function<async::Completion<R>(A const&)>;
188
189 /// @brief Builds a query over any fetcher — the test seam, and the way to query a non-bridge source.
190 /// @param runtime The runtime. Borrowed: it must outlive the query.
191 /// @param fetch Issues a request; its `Completion` must deliver on the runtime's owner.
192 /// @param key The tracked key.
193 /// @param options Timed refresh and debounce; see QueryOptions.
194 /// @throws std::invalid_argument when `options.refreshEvery` or `options.debounce` is negative, or either is
195 /// positive while `options.scheduler` is null; nothing has been fetched then.
196 Query(Runtime& runtime MORPH_LIFETIMEBOUND, Fetch fetch, Key key, QueryOptions options = {})
197 : _rt{&runtime},
198 _link{std::make_shared<InvalidationLink>(*this)},
199 _fetch{std::make_shared<Fetch const>(std::move(fetch))},
200 _pending{runtime, false},
201 _value{runtime, std::nullopt},
202 _error{runtime, nullptr},
203 _refresh{options} {
204 if (options.refreshEvery.count() < 0 || options.debounce.count() < 0) {
205 throw std::invalid_argument{"Query: refreshEvery and debounce must not be negative"};
206 }
207 if ((options.refreshEvery.count() > 0 || options.debounce.count() > 0) && options.scheduler == nullptr) {
208 throw std::invalid_argument{"Query: refreshEvery and debounce need a scheduler"};
209 }
210 // The Effect runs at once and issues through `_inflight`, which is declared after it so that it is
211 // destroyed first. The key lives in the Effect's body, which the graph keeps alive for a run that
212 // destroys the query.
213 _effect = std::make_unique<Effect>(runtime, [this, key = std::move(key)] {
214 async::CallbackToken const alive = _lifetime.token();
215 std::optional<A> next = key();
216 if (alive.expired()) {
217 return;
218 }
219 _rt->untracked([&] { onKey(std::move(next)); });
220 });
221 }
222
223 /// @brief Builds a query that fetches through a bridge handler.
224 ///
225 /// Checks the handler's callback executor against the runtime's owner (`detail::checkHandlerExecutor`).
226 /// @tparam M The handler's model type.
227 /// @tparam S The handler's sharing policy.
228 /// @param runtime The runtime. Borrowed: it must outlive the query.
229 /// @param handler Executes the requests; its callbacks must be delivered on the runtime's owner. Borrowed.
230 /// @param key The tracked key.
231 /// @param options Timed refresh and debounce; see QueryOptions.
232 /// @throws std::invalid_argument when `options.refreshEvery` or `options.debounce` is negative, or either is
233 /// positive while `options.scheduler` is null; nothing has been fetched then.
234 template <typename M, typename S>
235 Query(Runtime& runtime MORPH_LIFETIMEBOUND, bridge::BridgeHandler<M, S>& handler MORPH_LIFETIMEBOUND, Key key,
236 QueryOptions options = {})
237 : Query(runtime, [&handler](A const& action) { return handler.execute(action); }, std::move(key), options) {
238 detail::checkHandlerExecutor(runtime, handler);
239 }
240
241 /// @brief Clears the query's invalidation link, so no mutation refetches it any more, and cancels what it
242 /// has in flight.
243 ~Query() override { _link->clear(); }
244 Query(Query const&) = delete;
245 Query& operator=(Query const&) = delete;
246 Query(Query&&) = delete;
247 Query& operator=(Query&&) = delete;
248
249 /// @brief Whether a request for the current key is in flight. Tracked.
250 /// @return True from issue to delivery.
251 [[nodiscard]] bool pending() const { return _pending.get(); }
252
253 /// @brief The last successful result. Tracked.
254 /// @return The result, kept while a refetch or a new key is in flight and through a failed refetch of
255 /// the same key; `nullopt` before the first, while idle, and after a failure for a different
256 /// key. Valid until the next delivery.
257 [[nodiscard]] std::optional<R> const& value() const { return _value.get(); }
258
259 /// @brief The last failure. Tracked.
260 /// @return The exception, or null; cleared by the next success. After a key change, the previous key's
261 /// failure remains until the new key settles.
262 [[nodiscard]] std::exception_ptr error() const { return _error.get(); }
263
264 /// @brief Re-issues the current key under a new generation, ending a debounce wait; idle stays idle. Refused,
265 /// and reported,
266 /// off the Runtime's owner (`detail::site::kOffOwner`) and inside a Computed's computation
267 /// (`detail::site::kIssueInComputed`); nothing is issued then.
268 void refetch() override {
269 detail::RuntimeCore const& core = *_rt->core();
270 if (!core.checkOwner()) {
271 return;
272 }
273 // A computation is pure: one that fetched would fetch again on every recomputation, with the
274 // pending flag and any synchronous failure it cannot write lost.
275 if (core.isComputing()) {
277 return;
278 }
279 if (_current.has_value()) {
280 issue();
281 }
282 }
283
284 /// @brief This query's link, for `MutationOptions::invalidates`.
285 /// @return The link; it refetches nothing once this query is destroyed.
286 [[nodiscard]] std::shared_ptr<InvalidationLink> link() const { return _link; }
287
288 /// @brief Re-fetches whenever the bridge publishes a @p Pub on @p handler's instance.
289 ///
290 /// Uses the handler's one subscription slot for `Pub`: two consumers of the same `Pub` use two
291 /// handlers.
292 /// @tparam Pub The published result type to listen for.
293 /// @tparam M The handler's model type.
294 /// @tparam S The handler's sharing policy.
295 /// @param handler The handler whose instance publishes. Borrowed: it must outlive the query.
296 template <typename Pub, typename M, typename S>
298 handler.template subscribe<Pub>(_lifetime, [this](Pub const&) { refetch(); });
299 }
300
301private:
302 void onKey(std::optional<A> next) {
303 if constexpr (kEqualityUsable<A>) {
304 if (_keyed && next == _current) {
305 return;
306 }
307 }
308 bool const first = !_keyed;
309 _keyed = true;
310 _current = std::move(next);
311 ++_keys;
312 if (_refresh.debounce.count() > 0 && !first && _current.has_value()) {
313 // The call for the key being replaced is stale from now on; the new key waits until it has been
314 // stable for the debounce interval. Replacing the handle cancels the previous wait.
315 _inflight.reset();
316 _pending.set(true);
317 _debounce = _refresh.scheduler->after(_refresh.debounce, _lifetime.guard([this] { onDebounced(); }));
318 return;
319 }
320 issue();
321 }
322
323 void onDebounced() {
324 if (!_rt->core()->checkOwner()) {
325 return;
326 }
327 issue();
328 }
329
330 void issue() {
331 _debounce.cancel();
332 _inflight.reset();
333 if (!_current.has_value()) {
334 _timer.cancel();
335 _rt->batch([&] {
336 _pending.set(false);
337 _value.set(std::nullopt);
338 _error.set(nullptr);
339 });
340 return;
341 }
342 // Armed before the fetcher runs: the period restarts even for a fetcher that throws, and a fetcher
343 // may destroy the query.
344 armRefresh();
345 // Held here, so a fetcher that destroys the query keeps itself alive until it returns.
346 std::shared_ptr<Fetch const> const fetch = _fetch;
347 async::CallbackToken const alive = _lifetime.token();
348 async::Completion<R> completion;
349 try {
350 // Untracked: a refetch() called from an Effect must not subscribe that Effect to what the
351 // fetcher reads, or a change there would re-run it and issue again.
352 completion = _rt->untracked([&] { return (*fetch)(*_current); });
353 } catch (...) {
354 if (alive.expired()) {
355 return;
356 }
357 // The call it replaced was superseded above, so nothing is in flight any more.
358 fail(std::current_exception());
359 return;
360 }
361 if (alive.expired()) {
362 return;
363 }
364 _pending.set(true);
365 completion
366 .then(_inflight,
367 [this](R const& result) {
368 if (!_rt->core()->checkOwner()) {
369 return;
370 }
371 _rt->batch([&] {
372 _value.set(result);
373 _valueKey = _keys;
374 _error.set(nullptr);
375 _pending.set(false);
376 });
377 })
378 .onError(_inflight, [this](std::exception_ptr const& error) {
379 if (!_rt->core()->checkOwner()) {
380 return;
381 }
382 fail(error);
383 });
384 }
385
386 // Settles the current call with @p error. Every call in flight is for the current key, since a key change
387 // supersedes, so a value fetched under an earlier key belongs to a key the query no longer stands for.
388 void fail(std::exception_ptr error) {
389 _rt->batch([&] {
390 _error.set(std::move(error));
391 if (_valueKey != _keys && _value.peek().has_value()) {
392 _value.set(std::nullopt);
393 }
394 _pending.set(false);
395 });
396 }
397
398 // (Re)starts the refresh period. The tick is gated by `_lifetime`, so one a scheduler delivers after the
399 // query is gone is dropped.
400 void armRefresh() {
401 if (_refresh.refreshEvery.count() > 0) {
402 _timer = _refresh.scheduler->after(_refresh.refreshEvery, _lifetime.guard([this] { onRefreshTick(); }));
403 }
404 }
405
406 void onRefreshTick() {
407 if (!_rt->core()->checkOwner() || !_current.has_value()) {
408 return;
409 }
410 if (_pending.peek()) {
411 armRefresh();
412 return;
413 }
414 issue();
415 }
416
417 Runtime* _rt;
418 // What the mutations that invalidate this query hold; cleared by the destructor.
419 std::shared_ptr<InvalidationLink> _link;
420 // Shared, so a running issue() can keep the fetcher alive past the query.
421 std::shared_ptr<Fetch const> _fetch;
422 Signal<bool> _pending;
423 Signal<std::optional<R>> _value;
424 Signal<std::exception_ptr> _error;
425 std::optional<A> _current;
426 // How many keys the query has taken, and which of them `_value` was fetched for.
427 std::uint64_t _keys = 0;
428 std::uint64_t _valueKey = 0;
429 bool _keyed = false;
430 QueryOptions _refresh;
431 std::unique_ptr<Effect> _effect;
432 // Destroyed before the Effect and the signals a timed refresh or a debounced fetch writes, so neither
433 // outlives them.
434 TimerHandle _timer;
435 TimerHandle _debounce;
436 // Gates the subscription `refreshOn` installs. Its tokens expire with the query, which is how a run
437 // that destroyed the query finds out.
438 async::CallbackScope _lifetime;
439 // Last member, so it is destroyed first: a reply still in flight finds it stopped.
440 async::CallbackScope _inflight;
441};
442
443/// @brief What a `Mutation` does with a run while another is in flight.
444enum class Concurrency : std::uint8_t {
445 /// Refuses the run: `run()` returns false and nothing is sent. The default: a second click on a pending
446 /// button does nothing.
448 /// Queues the run and sends it when the previous one settles, a failure included; one queue per
449 /// `MutationOptions::serialKey`.
451 /// Sends the run; only the newest run's reply is applied.
453 /// Sends the run; every reply is applied as it arrives.
455};
456
457/// @brief How a `Mutation` sequences its runs, and what a success invalidates.
458/// @tparam A The action type.
459template <typename A>
461 /// @brief What a run does while another is in flight.
463 /// @brief For `Serial`: the queue a run joins. Empty means one queue for every run.
464 std::function<std::string(A const&)> serialKey;
465 /// @brief The queries refetched, in the same batch as the result, after every applied success. A link whose
466 /// query is gone refetches nothing.
467 std::vector<std::shared_ptr<InvalidationLink>> invalidates;
468};
469
470/// @brief A command: issues a write and tracks it, then refetches what the write invalidates.
471///
472/// `MutationOptions::concurrency` decides what a run does while another is in flight (`Concurrency`).
473/// `pending()` is true while a run is in flight or queued. An applied success records `lastResult()`, clears
474/// `error()`, ticks `successCount()` and refetches every live query in `MutationOptions::invalidates` — one
475/// batch, so one flush and one frame. An applied failure records `error()` and invalidates nothing. A reply
476/// `Latest` does not apply only stops counting as pending.
477///
478/// The runner runs untracked: a `run()` from inside an Effect does not subscribe that Effect to what the
479/// runner reads, so a change there cannot issue the write again.
480///
481/// A runner that throws instead of returning a `Completion` fails that run: the exception becomes `error()`
482/// at once, and nothing is left pending for it. `run()` off the Runtime's owner, and a delivery off it, are
483/// reported (`detail::site::kOffOwner`) and dropped. A dropped delivery is never counted down, so `pending()`
484/// stays true for good and an `Exclusive` mutation refuses every later run: it is a wiring error, not a state
485/// to recover from. `run()` inside a Computed's computation is reported (`detail::site::kIssueInComputed`)
486/// and refused before anything is issued.
487///
488/// The runner may destroy the mutation, and so may a query's fetcher during an invalidation, another handler
489/// of the reply, or an Effect the reply wakes; the mutation is not touched afterwards.
490/// @tparam A The action type.
491/// @tparam R The result type; defaults to the action's registered result.
492// NOLINTNEXTLINE(readability-redundant-typename) -- valid everywhere; implicit typename here is unverified on MSVC
493template <typename A, typename R = typename model::ActionTraits<A>::Result>
494class Mutation final {
495public:
496 /// @brief Issues one call.
497 using Run = std::function<async::Completion<R>(A)>;
498
499 /// @brief Builds a mutation over any runner — the test seam, and the interpreter's path.
500 /// @param runtime The runtime. Borrowed: it must outlive the mutation.
501 /// @param run Issues a call; its `Completion` must deliver on the runtime's owner.
502 /// @param options Concurrency and invalidation.
503 Mutation(Runtime& runtime MORPH_LIFETIMEBOUND, Run run, MutationOptions<A> options = {})
504 : _rt{&runtime},
505 _run{std::make_shared<Run const>(std::move(run))},
506 _options{std::move(options)},
507 _busy{runtime, 0},
508 _error{runtime, nullptr},
509 _last{runtime, std::nullopt},
510 _successes{runtime, 0} {}
511
512 /// @brief Builds a mutation that executes through a bridge handler.
513 ///
514 /// Checks the handler's callback executor against the runtime's owner (`detail::checkHandlerExecutor`).
515 /// @tparam M The handler's model type.
516 /// @tparam S The handler's sharing policy.
517 /// @param runtime The runtime. Borrowed: it must outlive the mutation.
518 /// @param handler Executes the calls; its callbacks must be delivered on the runtime's owner. Borrowed.
519 /// @param options Concurrency and invalidation.
520 template <typename M, typename S>
521 Mutation(Runtime& runtime MORPH_LIFETIMEBOUND, bridge::BridgeHandler<M, S>& handler MORPH_LIFETIMEBOUND,
522 MutationOptions<A> options = {})
523 : Mutation(runtime, [&handler](A action) { return handler.execute(std::move(action)); }, std::move(options)) {
524 detail::checkHandlerExecutor(runtime, handler);
525 }
526
527 ~Mutation() = default;
528 Mutation(Mutation const&) = delete;
529 Mutation& operator=(Mutation const&) = delete;
530 Mutation(Mutation&&) = delete;
531 Mutation& operator=(Mutation&&) = delete;
532
533 /// @brief Issues, queues or refuses @p action, as `MutationOptions::concurrency` says. Refused, and reported,
534 /// off the Runtime's owner (`detail::site::kOffOwner`) and inside a Computed's computation
535 /// (`detail::site::kIssueInComputed`).
536 /// @param action The action; built by the caller per gesture, so an idempotency key minted while building it
537 /// is fresh per click.
538 /// @return False when the run was refused: `Exclusive` with a run in flight, off the owner, or inside a
539 /// Computed. A run that was sent or queued returns true, even when its runner threw.
540 bool run(A action) {
541 detail::RuntimeCore const& core = *_rt->core();
542 if (!core.checkOwner()) {
543 return false;
544 }
545 // A computation is pure: one that issued a write would issue it on every recomputation, and the
546 // pending count it cannot write would never be counted back down.
547 if (core.isComputing()) {
549 return false;
550 }
551 if (_options.concurrency == Concurrency::Exclusive && _busy.peek() != 0) {
552 return false;
553 }
554 if (_options.concurrency != Concurrency::Serial) {
555 send(std::move(action), std::nullopt, false);
556 return true;
557 }
558 std::string key = _options.serialKey ? _options.serialKey(action) : std::string{};
559 Lane& lane = _lanes[key];
560 if (lane.active) {
561 lane.queued.push_back(std::move(action));
562 _busy.set(_busy.peek() + 1);
563 return true;
564 }
565 lane.active = true;
566 send(std::move(action), std::move(key), false);
567 return true;
568 }
569
570 /// @brief Whether a run is in flight or queued. Tracked.
571 /// @return True while any is.
572 [[nodiscard]] bool pending() const { return _busy.get() != 0; }
573
574 /// @brief The most recent applied failure. Tracked.
575 /// @return The exception, or null; cleared by the next applied success.
576 [[nodiscard]] std::exception_ptr error() const { return _error.get(); }
577
578 /// @brief The most recent applied result. Tracked.
579 /// @return The result, or `nullopt` before the first success. Valid until the next applied success.
580 [[nodiscard]] std::optional<R> const& lastResult() const { return _last.get(); }
581
582 /// @brief How many successes have been applied. Tracked; it ticks once per applied success, so an Effect can
583 /// react to "it succeeded again" when the result is equal to the last one.
584 /// @return The count.
585 [[nodiscard]] std::uint64_t successCount() const { return _successes.get(); }
586
587private:
588 struct Lane {
589 bool active = false;
590 // A vector, not a deque: GCC's -Wstrict-overflow fires inside std::deque's destructor in
591 // optimised builds, and a Serial lane queues a handful of actions at most.
592 std::vector<A> queued;
593 };
594
595 // Sends one run. @p lane is the Serial queue it belongs to; @p counted says whether `_busy` already counts it
596 // (a queued run does).
597 void send(A action, std::optional<std::string> lane, bool counted) {
598 std::uint64_t const generation = ++_generation;
599 // Held here, so a runner that destroys the mutation keeps itself alive until it returns.
600 std::shared_ptr<Run const> const runner = _run;
601 async::CallbackToken const alive = _lifetime.token();
602 async::Completion<R> completion;
603 try {
604 // Untracked: a run() called from an Effect must not subscribe that Effect to what the runner reads,
605 // or a change there would re-run it and issue the write again.
606 completion = _rt->untracked([&] { return (*runner)(std::move(action)); });
607 } catch (...) {
608 if (!alive.expired()) {
609 settle(generation, lane, std::nullopt, std::current_exception(), counted);
610 }
611 return;
612 }
613 if (alive.expired()) {
614 return;
615 }
616 if (!counted) {
617 _busy.set(_busy.peek() + 1);
618 }
619 completion
620 .then(_lifetime,
621 [this, generation, lane](R const& result) {
622 if (_rt->core()->checkOwner()) {
623 settle(generation, lane, result, nullptr, true);
624 }
625 })
626 .onError(_lifetime, [this, generation, lane](std::exception_ptr const& error) {
627 if (_rt->core()->checkOwner()) {
628 settle(generation, lane, std::nullopt, error, true);
629 }
630 });
631 }
632
633 // Applies one settled run, then sends the next queued run of its Serial lane.
634 void settle(std::uint64_t generation, std::optional<std::string> const& lane, std::optional<R> result,
635 std::exception_ptr const& error, bool counted) {
636 bool const applies = _options.concurrency != Concurrency::Latest || generation == _generation;
637 async::CallbackToken const alive = _lifetime.token();
638 _rt->batch([&] {
639 if (counted) {
640 _busy.set(_busy.peek() - 1);
641 }
642 if (!applies) {
643 return;
644 }
645 if (error != nullptr) {
646 _error.set(error);
647 return;
648 }
649 _last.set(std::move(result));
650 _error.set(nullptr);
651 _successes.set(_successes.peek() + 1);
652 // A refetch runs the query's fetcher, which may destroy this mutation.
653 for (auto const& link : _options.invalidates) {
654 link->refetch();
655 if (alive.expired()) {
656 return;
657 }
658 }
659 });
660 if (alive.expired() || !lane.has_value()) {
661 return;
662 }
663 auto const found = _lanes.find(*lane);
664 if (found == _lanes.end()) {
665 return;
666 }
667 if (found->second.queued.empty()) {
668 _lanes.erase(found);
669 return;
670 }
671 A next = std::move(found->second.queued.front());
672 found->second.queued.erase(found->second.queued.begin());
673 send(std::move(next), lane, true);
674 }
675
676 Runtime* _rt;
677 // Shared, so a running send() can keep the runner alive past the mutation.
678 std::shared_ptr<Run const> _run;
679 MutationOptions<A> _options;
680 // Runs in flight or queued.
681 Signal<std::size_t> _busy;
682 Signal<std::exception_ptr> _error;
683 Signal<std::optional<R>> _last;
684 Signal<std::uint64_t> _successes;
685 std::map<std::string, Lane> _lanes;
686 std::uint64_t _generation = 0;
687 // Last member, so it is destroyed first: a reply still in flight finds it stopped. Its tokens expire with the
688 // mutation, which is how a runner or a refetch that destroyed the mutation finds out.
689 async::CallbackScope _lifetime;
690};
691
692/// @brief The latest `R` the bridge published on a handler's instance, as reactive state.
693///
694/// Uses the handler's one subscription slot for `R`: two consumers of the same `R` use two handlers. The
695/// subscription is gated by a scope the Subscription owns, so a publish delivered after it is destroyed —
696/// one already queued included — is dropped. The handler keeps the dead entry until it is destroyed or
697/// subscribes to `R` again. A delivery off the Runtime's owner is reported (`detail::site::kOffOwner`)
698/// and dropped.
699///
700/// `latest()` is state, not an event stream: when `R` compares with `==`, a publish equal to the current
701/// value does not notify. For something to happen on every publish, use `Query::refreshOn`.
702/// @tparam R The published result type.
703template <typename R>
704class Subscription final {
705public:
706 /// @brief Subscribes to every `R` published on @p handler's instance, and checks the handler's callback
707 /// executor against the runtime's owner (`detail::checkHandlerExecutor`).
708 /// @tparam M The handler's model type.
709 /// @tparam S The handler's sharing policy.
710 /// @param runtime The runtime. Borrowed: it must outlive the subscription.
711 /// @param handler The handler whose instance publishes; its GUI executor must be the runtime's
712 /// owner. Borrowed: it must outlive the subscription.
713 template <typename M, typename S>
714 Subscription(Runtime& runtime MORPH_LIFETIMEBOUND, bridge::BridgeHandler<M, S>& handler MORPH_LIFETIMEBOUND)
715 : _rt{&runtime}, _latest{runtime, std::nullopt} {
716 detail::checkHandlerExecutor(runtime, handler);
717 handler.template subscribe<R>(_lifetime, [this](R value) {
718 if (!_rt->core()->checkOwner()) {
719 return;
720 }
721 _latest.set(std::move(value));
722 });
723 }
724
725 ~Subscription() = default;
726 Subscription(Subscription const&) = delete;
727 Subscription& operator=(Subscription const&) = delete;
728 Subscription(Subscription&&) = delete;
729 Subscription& operator=(Subscription&&) = delete;
730
731 /// @brief The latest published value. Tracked.
732 /// @return The value, or `nullopt` before the first publish. Valid until the next publish.
733 [[nodiscard]] std::optional<R> const& latest() const { return _latest.get(); }
734
735private:
736 Runtime* _rt;
738 // Last member, so it is destroyed first: a publish still in flight finds it stopped.
739 async::CallbackScope _lifetime;
740};
741
742} // namespace morph::reactive
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
Weak, copyable observer of a CallbackScope — the half a callback captures.
bool expired() const noexcept
Whether the issuing scope generation is gone (destroyed or reset()).
Move-only handle representing the eventual result of an asynchronous operation.
Completion & then(std::function< void(const T &)> handler)
Registers a success callback.
RAII wrapper that binds a single model type to a Bridge.
Definition bridge.hpp:2583
::morph::exec::IExecutor * guiExecutor() const noexcept
The executor used to deliver this handler's Completion callbacks.
Definition bridge.hpp:2849
::morph::exec::IExecutor & owner() const noexcept
The executor this handler and its bridge belong to.
Definition bridge.hpp:2853
A command: issues a write and tracks it, then refetches what the write invalidates.
Definition control.hpp:494
Mutation(Runtime &runtime, Run run, MutationOptions< A > options={})
Builds a mutation over any runner — the test seam, and the interpreter's path.
Definition control.hpp:503
bool pending() const
Whether a run is in flight or queued. Tracked.
Definition control.hpp:572
std::function< async::Completion< R >(A)> Run
Issues one call.
Definition control.hpp:497
std::uint64_t successCount() const
How many successes have been applied. Tracked; it ticks once per applied success, so an Effect can re...
Definition control.hpp:585
Mutation(Runtime &runtime, bridge::BridgeHandler< M, S > &handler, MutationOptions< A > options={})
Builds a mutation that executes through a bridge handler.
Definition control.hpp:521
bool run(A action)
Issues, queues or refuses action, as MutationOptions::concurrency says. Refused, and reported,...
Definition control.hpp:540
std::exception_ptr error() const
The most recent applied failure. Tracked.
Definition control.hpp:576
std::optional< R > const & lastResult() const
The most recent applied result. Tracked.
Definition control.hpp:580
A derived async resource: the result of fetching whatever its tracked key names.
Definition control.hpp:182
void refetch() override
Re-issues the current key under a new generation, ending a debounce wait; idle stays idle....
Definition control.hpp:268
Query(Runtime &runtime, Fetch fetch, Key key, QueryOptions options={})
Builds a query over any fetcher — the test seam, and the way to query a non-bridge source.
Definition control.hpp:196
Query(Runtime &runtime, bridge::BridgeHandler< M, S > &handler, Key key, QueryOptions options={})
Builds a query that fetches through a bridge handler.
Definition control.hpp:235
void refreshOn(bridge::BridgeHandler< M, S > &handler)
Re-fetches whenever the bridge publishes a Pub on handler's instance.
Definition control.hpp:297
std::exception_ptr error() const
The last failure. Tracked.
Definition control.hpp:262
std::shared_ptr< InvalidationLink > link() const
This query's link, for MutationOptions::invalidates.
Definition control.hpp:286
std::optional< R > const & value() const
The last successful result. Tracked.
Definition control.hpp:257
std::function< async::Completion< R >(A const &)> Fetch
Issues one request.
Definition control.hpp:187
std::function< std::optional< A >()> Key
The tracked key: which request the query currently stands for, or nullopt for none.
Definition control.hpp:185
bool pending() const
Whether a request for the current key is in flight. Tracked.
Definition control.hpp:251
~Query() override
Clears the query's invalidation link, so no mutation refetches it any more, and cancels what it has i...
Definition control.hpp:243
Something a Mutation can tell to fetch again after it succeeds.
Definition control.hpp:53
virtual void refetch()=0
Re-issues the current request; does nothing while idle.
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
std::shared_ptr< detail::RuntimeCore > const & core() const noexcept
The shared core every node holds.
Definition runtime.hpp:170
Runs callbacks after a delay or on a period, on the runtime's owner executor.
A source value. Reading it with get() inside a tracked run subscribes that run.
Definition signal.hpp:49
The latest R the bridge published on a handler's instance, as reactive state.
Definition control.hpp:704
Subscription(Runtime &runtime, bridge::BridgeHandler< M, S > &handler)
Subscribes to every R published on handler's instance, and checks the handler's callback executor aga...
Definition control.hpp:714
std::optional< R > const & latest() const
The latest published value. Tracked.
Definition control.hpp:733
State shared by one Runtime and every node made from it.
Definition graph.hpp:135
std::string errorMessage(std::exception_ptr const &error)
The one place an exception_ptr becomes display text.
Definition control.hpp:39
Concurrency
What a Mutation does with a run while another is in flight.
Definition control.hpp:444
@ Parallel
Sends the run; every reply is applied as it arrives.
Definition control.hpp:454
@ Latest
Sends the run; only the newest run's reply is applied.
Definition control.hpp:452
void checkHandlerExecutor(Runtime const &runtime, bridge::BridgeHandler< M, S > const &handler)
Checks that handler delivers its callbacks on runtime's owner, reporting site::kHandlerExecutor when ...
Definition control.hpp:109
The reactive graph's internals: node colours and links, the tracking frame, batching and the flush qu...
constexpr char const * kIssueInComputed
Query::refetch or Mutation::run was called while a Computed was computing; nothing is issued.
Definition graph.hpp:50
constexpr char const * kHandlerExecutor
A control node was built over a handler whose callbacks are not delivered on the Runtime's owner.
Definition graph.hpp:57
morph::reactive::Scheduler: timers a frontend runs on the runtime's owner executor.
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
constexpr bool kEqualityUsable
Whether == is usable for T, which is what the EqualityPolicy::Auto skip needs.
Definition signal.hpp:40
Abstract executor interface.
Definition executor.hpp:70
virtual bool isSerial() const noexcept
Whether this executor runs one task at a time, each after the tasks posted before it: a strand,...
Definition executor.hpp:94
virtual void post(std::function< void()> task)=0
Schedules task for asynchronous execution.
How a Mutation sequences its runs, and what a success invalidates.
Definition control.hpp:460
std::vector< std::shared_ptr< InvalidationLink > > invalidates
The queries refetched, in the same batch as the result, after every applied success....
Definition control.hpp:467
Concurrency concurrency
What a run does while another is in flight.
Definition control.hpp:462
std::function< std::string(A const &)> serialKey
For Serial: the queue a run joins. Empty means one queue for every run.
Definition control.hpp:464
How a Query refreshes besides key changes, and how long it lets a key settle.
Definition control.hpp:130
std::chrono::milliseconds refreshEvery
Re-fetch the current key this long after each call is issued, unless a call is then in flight; zero m...
Definition control.hpp:136
std::chrono::milliseconds debounce
Fetch a changed key only once it has been stable this long; zero fetches at once, and a negative valu...
Definition control.hpp:139
Scheduler * scheduler
Runs the timed refresh and the debounce; required when either is positive. Borrowed: it must outlive ...
Definition control.hpp:133
reactive::TimerHandle TimerHandle
Owns one scheduled timer and cancels it when destroyed; see reactive::TimerHandle.
Definition frontend.hpp:44