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"
34namespace morph::reactive {
39[[nodiscard]]
inline std::string
errorMessage(std::exception_ptr
const& error) {
40 if (error ==
nullptr) {
44 std::rethrow_exception(error);
45 }
catch (std::exception
const& exception) {
46 return exception.what();
48 return "unknown error";
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;
78 if (_target !=
nullptr) {
84 void clear() noexcept { _target =
nullptr; }
88 [[nodiscard]]
bool live() const noexcept {
return _target !=
nullptr; }
108template <
typename M,
typename S>
112 std::shared_ptr<RuntimeCore>
const& core = runtime.
core();
117 if (&delivery == &core->owner()) {
120 delivery.
post([weak = std::weak_ptr<RuntimeCore>{core}] {
121 if (
auto const live = weak.lock(); live !=
nullptr && !live->onOwner()) {
181template <typename A, typename R = typename model::ActionTraits<A>::Result>
182class Query final :
public Refetchable {
185 using Key = std::function<std::optional<A>()>;
187 using Fetch = std::function<async::Completion<R>(A
const&)>;
199 _fetch{std::make_shared<
Fetch const>(std::move(fetch))},
200 _pending{runtime, false},
201 _value{runtime, std::nullopt},
202 _error{runtime, nullptr},
204 if (options.refreshEvery.count() < 0 || options.debounce.count() < 0) {
205 throw std::invalid_argument{
"Query: refreshEvery and debounce must not be negative"};
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"};
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()) {
219 _rt->untracked([&] { onKey(std::move(next)); });
234 template <
typename M,
typename S>
237 :
Query(runtime, [&handler](A const& action) {
return handler.execute(action); }, std::move(key), options) {
238 detail::checkHandlerExecutor(runtime, handler);
251 [[nodiscard]]
bool pending()
const {
return _pending.get(); }
257 [[nodiscard]] std::optional<R>
const&
value()
const {
return _value.get(); }
262 [[nodiscard]] std::exception_ptr
error()
const {
return _error.get(); }
270 if (!core.checkOwner()) {
275 if (core.isComputing()) {
279 if (_current.has_value()) {
286 [[nodiscard]] std::shared_ptr<InvalidationLink>
link()
const {
return _link; }
296 template <
typename Pub,
typename M,
typename S>
298 handler.template subscribe<Pub>(_lifetime, [
this](Pub
const&) {
refetch(); });
302 void onKey(std::optional<A> next) {
304 if (_keyed && next == _current) {
308 bool const first = !_keyed;
310 _current = std::move(next);
312 if (_refresh.debounce.count() > 0 && !first && _current.has_value()) {
317 _debounce = _refresh.scheduler->after(_refresh.debounce, _lifetime.guard([
this] { onDebounced(); }));
324 if (!_rt->core()->checkOwner()) {
333 if (!_current.has_value()) {
337 _value.set(std::nullopt);
346 std::shared_ptr<Fetch const>
const fetch = _fetch;
347 async::CallbackToken
const alive = _lifetime.token();
348 async::Completion<R> completion;
352 completion = _rt->untracked([&] {
return (*fetch)(*_current); });
354 if (alive.expired()) {
358 fail(std::current_exception());
361 if (alive.expired()) {
367 [
this](R
const& result) {
368 if (!_rt->core()->checkOwner()) {
378 .onError(_inflight, [
this](std::exception_ptr
const& error) {
379 if (!_rt->core()->checkOwner()) {
388 void fail(std::exception_ptr error) {
390 _error.set(std::move(error));
391 if (_valueKey != _keys && _value.peek().has_value()) {
392 _value.set(std::nullopt);
401 if (_refresh.refreshEvery.count() > 0) {
402 _timer = _refresh.scheduler->after(_refresh.refreshEvery, _lifetime.guard([
this] { onRefreshTick(); }));
406 void onRefreshTick() {
407 if (!_rt->core()->checkOwner() || !_current.has_value()) {
410 if (_pending.peek()) {
419 std::shared_ptr<InvalidationLink> _link;
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;
427 std::uint64_t _keys = 0;
428 std::uint64_t _valueKey = 0;
430 QueryOptions _refresh;
431 std::unique_ptr<Effect> _effect;
438 async::CallbackScope _lifetime;
440 async::CallbackScope _inflight;
493template <typename A, typename R = typename model::ActionTraits<A>::Result>
497 using Run = std::function<async::Completion<R>(A)>;
505 _run{std::make_shared<Run const>(std::move(run))},
506 _options{std::move(options)},
508 _error{runtime, nullptr},
509 _last{runtime, std::nullopt},
510 _successes{runtime, 0} {}
520 template <
typename M,
typename S>
523 :
Mutation(runtime, [&handler](A action) {
return handler.execute(std::move(action)); }, std::move(options)) {
524 detail::checkHandlerExecutor(runtime, handler);
527 ~Mutation() =
default;
528 Mutation(Mutation
const&) =
delete;
529 Mutation& operator=(Mutation
const&) =
delete;
530 Mutation(Mutation&&) =
delete;
531 Mutation& operator=(Mutation&&) =
delete;
542 if (!core.checkOwner()) {
547 if (core.isComputing()) {
555 send(std::move(action), std::nullopt,
false);
558 std::string key = _options.serialKey ? _options.serialKey(action) : std::string{};
559 Lane& lane = _lanes[key];
561 lane.queued.push_back(std::move(action));
562 _busy.set(_busy.peek() + 1);
566 send(std::move(action), std::move(key),
false);
572 [[nodiscard]]
bool pending()
const {
return _busy.get() != 0; }
576 [[nodiscard]] std::exception_ptr
error()
const {
return _error.get(); }
580 [[nodiscard]] std::optional<R>
const&
lastResult()
const {
return _last.get(); }
585 [[nodiscard]] std::uint64_t
successCount()
const {
return _successes.get(); }
592 std::vector<A> queued;
597 void send(A action, std::optional<std::string> lane,
bool counted) {
598 std::uint64_t
const generation = ++_generation;
600 std::shared_ptr<Run const>
const runner = _run;
606 completion = _rt->untracked([&] {
return (*runner)(std::move(action)); });
609 settle(generation, lane, std::nullopt, std::current_exception(), counted);
617 _busy.set(_busy.peek() + 1);
621 [
this, generation, lane](R
const& result) {
622 if (_rt->core()->checkOwner()) {
623 settle(generation, lane, result,
nullptr,
true);
626 .onError(_lifetime, [
this, generation, lane](std::exception_ptr
const& error) {
627 if (_rt->core()->checkOwner()) {
628 settle(generation, lane, std::nullopt, error,
true);
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();
640 _busy.set(_busy.peek() - 1);
645 if (error !=
nullptr) {
649 _last.set(std::move(result));
651 _successes.set(_successes.peek() + 1);
653 for (
auto const& link : _options.invalidates) {
655 if (alive.expired()) {
660 if (alive.expired() || !lane.has_value()) {
663 auto const found = _lanes.find(*lane);
664 if (found == _lanes.end()) {
667 if (found->second.queued.empty()) {
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);
678 std::shared_ptr<Run const> _run;
679 MutationOptions<A> _options;
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;
689 async::CallbackScope _lifetime;
713 template <
typename M,
typename S>
715 : _rt{&runtime}, _latest{runtime, std::nullopt} {
717 handler.template subscribe<R>(_lifetime, [
this](R value) {
718 if (!_rt->core()->checkOwner()) {
721 _latest.set(std::move(value));
733 [[nodiscard]] std::optional<R>
const&
latest()
const {
return _latest.get(); }
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.
::morph::exec::IExecutor * guiExecutor() const noexcept
The executor used to deliver this handler's Completion callbacks.
::morph::exec::IExecutor & owner() const noexcept
The executor this handler and its bridge belong to.
A query's registration with the mutations that invalidate it.
bool live() const noexcept
Whether the query is still registered.
void clear() noexcept
Unregisters the query; called by its destructor.
InvalidationLink(Refetchable &target) noexcept
void refetch() const
Refetches the query, or does nothing once it is gone.
A command: issues a write and tracks it, then refetches what the write invalidates.
Mutation(Runtime &runtime, Run run, MutationOptions< A > options={})
Builds a mutation over any runner — the test seam, and the interpreter's path.
bool pending() const
Whether a run is in flight or queued. Tracked.
std::function< async::Completion< R >(A)> Run
Issues one call.
std::uint64_t successCount() const
How many successes have been applied. Tracked; it ticks once per applied success, so an Effect can re...
Mutation(Runtime &runtime, bridge::BridgeHandler< M, S > &handler, MutationOptions< A > options={})
Builds a mutation that executes through a bridge handler.
bool run(A action)
Issues, queues or refuses action, as MutationOptions::concurrency says. Refused, and reported,...
std::exception_ptr error() const
The most recent applied failure. Tracked.
std::optional< R > const & lastResult() const
The most recent applied result. Tracked.
A derived async resource: the result of fetching whatever its tracked key names.
void refetch() override
Re-issues the current key under a new generation, ending a debounce wait; idle stays idle....
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.
Query(Runtime &runtime, bridge::BridgeHandler< M, S > &handler, Key key, QueryOptions options={})
Builds a query that fetches through a bridge handler.
void refreshOn(bridge::BridgeHandler< M, S > &handler)
Re-fetches whenever the bridge publishes a Pub on handler's instance.
std::exception_ptr error() const
The last failure. Tracked.
std::shared_ptr< InvalidationLink > link() const
This query's link, for MutationOptions::invalidates.
std::optional< R > const & value() const
The last successful result. Tracked.
std::function< async::Completion< R >(A const &)> Fetch
Issues one request.
std::function< std::optional< A >()> Key
The tracked key: which request the query currently stands for, or nullopt for none.
bool pending() const
Whether a request for the current key is in flight. Tracked.
~Query() override
Clears the query's invalidation link, so no mutation refetches it any more, and cancels what it has i...
Something a Mutation can tell to fetch again after it succeeds.
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.
std::shared_ptr< detail::RuntimeCore > const & core() const noexcept
The shared core every node holds.
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.
The latest R the bridge published on a handler's instance, as reactive state.
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...
std::optional< R > const & latest() const
The latest published value. Tracked.
State shared by one Runtime and every node made from it.
std::string errorMessage(std::exception_ptr const &error)
The one place an exception_ptr becomes display text.
Concurrency
What a Mutation does with a run while another is in flight.
@ Parallel
Sends the run; every reply is applied as it arrives.
@ Latest
Sends the run; only the newest run's reply is applied.
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 ...
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.
constexpr char const * kHandlerExecutor
A control node was built over a handler whose callbacks are not delivered on the Runtime's owner.
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.
Abstract executor interface.
virtual bool isSerial() const noexcept
Whether this executor runs one task at a time, each after the tasks posted before it: a strand,...
virtual void post(std::function< void()> task)=0
Schedules task for asynchronous execution.
How a Mutation sequences its runs, and what a success invalidates.
std::vector< std::shared_ptr< InvalidationLink > > invalidates
The queries refetched, in the same batch as the result, after every applied success....
Concurrency concurrency
What a run does while another is in flight.
std::function< std::string(A const &)> serialKey
For Serial: the queue a run joins. Empty means one queue for every run.
How a Query refreshes besides key changes, and how long it lets a key settle.
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...
std::chrono::milliseconds debounce
Fetch a changed key only once it has been stable this long; zero fetches at once, and a negative valu...
Scheduler * scheduler
Runs the timed refresh and the debounce; required when either is positive. Borrowed: it must outlive ...
reactive::TimerHandle TimerHandle
Owns one scheduled timer and cancels it when destroyed; see reactive::TimerHandle.