Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
async_command.hpp
Go to the documentation of this file.
1#pragma once
2
3// AsyncCommand<R, Args...>
4//
5// A command whose action is asynchronous (returns Task<R>). Automatically
6// tracks:
7// - is_executing : Property<bool> (true while ANY invocation is in flight)
8// - last_error : Property<std::optional<aria::Error>> (nullopt when fine)
9// - last_error_message : Property<std::string> ("" when fine; convenience projection)
10// - last_result : Property<std::optional<R>> (only when R != void)
11//
12// All Property mutations happen on the UI executor that you pass at
13// construction. The view binds to those Properties and never sees a Task.
14//
15// ── UI executor contract ───────────────────────────────────────────────
16// **The `ui` executor you pass to the constructor MUST be the reactive
17// graph thread** — i.e. the same executor that owns the Properties this
18// command's coroutine writes back to (`is_executing`, `last_error`,
19// `last_result`). The reactive graph is strictly single-threaded; writing
20// a Property from an unrelated thread will trip the graph-thread assert
21// (and, in Release, may glitch observers).
22//
23// In the typical app layout the UI executor and the graph thread are the
24// same thing by design: the main thread pumps both. If you are running
25// headless tests or non-GUI contexts, make sure the `ui` you pass is
26// the executor you pinned your Properties to — not just "some executor
27// that eventually delivers messages to main".
28//
29// ── Action signature ───────────────────────────────────────────────────
30// The action callable you pass to the constructor may have one of two
31// shapes:
32//
33// Task<R> action(Args...) // "plain" shape
34// Task<R> action(CancellationToken, Args...) // "cancellable" shape
35//
36// Use the second form when your action contains cooperative
37// cancellation points. The token is scoped to a SINGLE invocation of
38// `execute` — cancelling the command (dtor, latest_only reset, etc.)
39// flips it; a brand-new invocation gets a fresh token.
40//
41// ── Concurrency policy ─────────────────────────────────────────────────
42// Optional third constructor argument (`AsyncCommandPolicy`):
43//
44// Parallel (default) — every execute() starts its own coroutine;
45// is_executing stays true until the LAST finishes.
46// LatestOnly — a new execute() cancels ANY in-flight invocations
47// first. Ideal for search-as-you-type, filtered lists.
48// DropIfRunning — silently ignore execute() while any invocation is
49// running. Ideal for "Save" buttons that must not
50// double-fire.
51//
52// ── Lifetime safety ────────────────────────────────────────────────────
53// Every piece of state that the coroutine body touches lives in a shared
54// control block held by `std::shared_ptr<SharedState>`. The coroutine
55// captures ONLY this shared_ptr — never `this`. Consequences:
56// 1. If AsyncCommand is destroyed while a coroutine is still running, the
57// `state_` block is kept alive by the coroutine until it completes.
58// 2. AsyncCommand's destructor cancels `state_->cancel`, which causes the
59// next probe inside the coroutine to throw OperationCancelled, and the
60// coroutine unwinds cleanly.
61//
62// To keep the *user-facing* API identical to the old version
63// (`cmd.is_executing.bind(...)`), AsyncCommand exposes `Property<T>&`
64// members that forward into `state_`. Those references are valid as long
65// as the AsyncCommand itself is alive (which is the natural lifetime of
66// user bindings).
67//
68// ── Internal structure ─────────────────────────────────────────────────
69// The concurrency machinery shared by both the `R != void` primary
70// template and the `R == void` specialisation lives in
71// `detail::AsyncCommandCore<R, Args...>`:
72//
73// * `make_action_<Fn>()` — normalises "plain" vs "cancellable" shapes
74// * `accept_new_invocation_()` / `cancel_all_in_flight_()`
75// * `detail::Invocation<R>` — RAII: constructing it registers a
76// per-invocation CancellationSource, flips is_executing / inflight
77// on first entry, exposes the cmd / invocation tokens; destructor
78// reverses the ledger on last exit.
79//
80// The derived shells (primary + void specialisation) hold a
81// `AsyncCommandCore` by **composition** (not inheritance). This keeps
82// the derived code free of `this->` / `typename Mixin::` dependent-name
83// noise — the members read cleanly as `core_.accept_new_invocation_()`.
84//
85// The two `run_to_result_` bodies are kept separate on purpose: the
86// `co_return co_await` vs `co_await` syntactic split is intrinsic to
87// C++ coroutines for `T` vs `void`, and forcing them into a single
88// `if constexpr` branch hurts readability more than the handful of
89// duplicated lines ever could. Both are noexcept-by-design — every
90// failure path folds into an `AsyncCommandResult<R>` outcome instead
91// of propagating an exception out of the coroutine.
92
96#include "aria/error.hpp"
97#include "aria/property.hpp"
98#include "aria/subscription.hpp"
102#include "aria/async/task.hpp"
103#include "aria/async/timeout.hpp" // for TimeoutError detection in classify_async_exception
104
105#include <algorithm>
106#include <atomic>
107#include <exception>
108#include <functional>
109#include <memory>
110#include <mutex>
111#include <optional>
112#include <stdexcept>
113#include <string>
114#include <type_traits>
115#include <utility>
116#include <vector>
117
118namespace aria::async {
119
127
128namespace detail {
129
135inline void check_executor_safety_runtime(IExecutor& ui, IExecutor& worker) {
136 if (!ui.is_safe_graph_executor()) {
137 throw std::invalid_argument(
138 "AsyncCommand: ui executor is not safe to use as the "
139 "graph-thread executor. Remedy: install a main-thread "
140 "IExecutor before constructing AsyncCommand-owning view "
141 "models -- use MainThreadExecutor, or wrap your platform "
142 "dispatcher with aria::runtime::DispatcherExecutor "
143 "(aria/runtime/dispatcher_executor.hpp). Third-party "
144 "executors must override IExecutor::caps() to advertise "
145 "SchedulerCaps::GraphSafe.");
146 }
147 if (!worker.is_safe_worker_executor()) {
148 throw std::invalid_argument(
149 "AsyncCommand: worker executor is not safe to host worker "
150 "tasks. Remedy: pass a ThreadPoolExecutor (or "
151 "MainThreadExecutor for single-threaded hosts) as the worker. "
152 "Third-party executors must override IExecutor::caps() to "
153 "advertise SchedulerCaps::WorkerSafe.");
154 }
155 auto* ui_inline = dynamic_cast<InlineExecutor*>(&ui);
156 auto* worker_inline = dynamic_cast<InlineExecutor*>(&worker);
157 if (ui_inline && !worker_inline) {
158 throw std::invalid_argument(
159 "AsyncCommand: cannot use InlineExecutor as the "
160 "graph-thread executor when worker runs on a different "
161 "thread. The final co_await schedule_on(ui) would "
162 "inline-resume on the worker thread and write reactive "
163 "Properties from there, tripping the graph thread-affinity "
164 "invariant. Remedy: install a real main-thread executor "
165 "BEFORE constructing this view model -- MainThreadExecutor "
166 "in tests / console apps, or "
167 "aria::runtime::DispatcherExecutor{*main_dispatcher()} in a "
168 "GUI host (aria/runtime/dispatcher_executor.hpp). See "
169 "docs/reference/lifecycle.md for the startup ordering "
170 "contract.");
171 }
172}
173
176template<typename R, typename... Args>
177struct AsyncCommandState {
178 using ArgsTuple = std::tuple<Args...>;
179
180 IExecutor* ui;
181 IExecutor* worker;
182 CancellationSource cancel; // dtor cancels all running coroutines
183 std::atomic<int> inflight{0};
184
185 Property<bool> is_executing{false};
186 Property<std::optional<::aria::Error>> last_error{std::nullopt};
187 Property<std::string> last_error_message{""};
188 Property<std::optional<R>> last_result{std::optional<R>{}};
189
190 std::mutex m_sources;
191 std::vector<std::shared_ptr<CancellationSource>> invocation_sources;
192
193 AsyncCommandState(IExecutor& u, IExecutor& w) : ui(&u), worker(&w) {}
194};
195
196template<>
197struct AsyncCommandState<void> {
198 IExecutor* ui;
199 IExecutor* worker;
200 CancellationSource cancel;
201 std::atomic<int> inflight{0};
202
203 Property<bool> is_executing{false};
204 Property<std::optional<::aria::Error>> last_error{std::nullopt};
205 Property<std::string> last_error_message{""};
206
207 std::mutex m_sources;
208 std::vector<std::shared_ptr<CancellationSource>> invocation_sources;
209
210 AsyncCommandState(IExecutor& u, IExecutor& w) : ui(&u), worker(&w) {}
211};
212
217enum class AsyncFailureKind : std::uint8_t {
218 Cancellation,
219 Failure,
220};
221
222struct AsyncFailureClassification {
223 AsyncFailureKind kind;
224 ::aria::Error error;
225};
226
243inline AsyncFailureClassification classify_async_exception(
244 std::exception_ptr ex,
245 Property<std::optional<::aria::Error>>& last_error,
246 Property<std::string>& last_error_message,
247 std::string source_tag = "AsyncCommand")
248{
249 try { std::rethrow_exception(ex); }
250 catch (const OperationCancelled&) {
251 auto err = ::aria::Error::cancellation(source_tag);
254 ::aria::trace::Async{source_tag, "cancelled", 0},
255 err);
256 }
257 // Intentionally NOT touching last_error: cancellation is not
258 // an observable failure. Awaiters still see r.cancelled() via
259 // the returned classification.
260 return {AsyncFailureKind::Cancellation, std::move(err)};
261 }
262 catch (const TimeoutError& e) {
263 auto err = ::aria::Error::timeout(source_tag);
264 err.message = e.what();
267 ::aria::trace::Async{source_tag, "timeout", 0},
268 err);
269 }
270 last_error = err;
271 last_error_message = err.message;
272 return {AsyncFailureKind::Failure, std::move(err)};
273 }
274 catch (...) {
275 auto err = ::aria::Error::from_exception(ex, source_tag);
278 ::aria::trace::Async{source_tag, "failure", 0},
279 err);
280 }
281 last_error_message = err.message;
282 last_error = err;
283 return {AsyncFailureKind::Failure, std::move(err)};
284 }
285}
286
287template<typename F, typename... Args>
288concept CancellableAction =
289 std::invocable<F, CancellationToken, Args...>;
290
291template<typename F, typename... Args>
292concept PlainAction =
293 std::invocable<F, Args...>;
294
309template<typename R>
310class Invocation {
311public:
312 using State = AsyncCommandState<R>;
313
314 explicit Invocation(std::shared_ptr<State> s)
315 : state_(std::move(s)),
316 src_(std::make_shared<CancellationSource>()),
317 cmd_tok_(state_->cancel.token()),
318 inv_tok_(src_->token())
319 {
320 {
321 std::lock_guard lk(state_->m_sources);
322 state_->invocation_sources.push_back(src_);
323 }
324 const bool first = (state_->inflight.fetch_add(1, std::memory_order_acq_rel) == 0);
325 if (first) {
326 state_->is_executing = true;
327 state_->last_error = std::nullopt;
328 state_->last_error_message = "";
329 }
332 ::aria::trace::Async{
333 "AsyncCommand",
334 "execute_start",
335 static_cast<std::uint64_t>(state_->inflight.load(std::memory_order_relaxed)),
336 });
337 }
338 }
339
340 ~Invocation() {
341 {
342 std::lock_guard lk(state_->m_sources);
343 auto& v = state_->invocation_sources;
344 v.erase(std::remove(v.begin(), v.end(), src_), v.end());
345 }
346 const bool last = (state_->inflight.fetch_sub(1, std::memory_order_acq_rel) == 1);
347 if (last) {
348 state_->is_executing = false;
349 }
352 ::aria::trace::Async{
353 "AsyncCommand",
354 "execute_finish",
355 static_cast<std::uint64_t>(state_->inflight.load(std::memory_order_relaxed)),
356 });
357 }
358 }
359
360 Invocation(const Invocation&) = delete;
361 Invocation& operator=(const Invocation&) = delete;
362
363 const CancellationToken& cmd_tok() const noexcept { return cmd_tok_; }
364 const CancellationToken& inv_tok() const noexcept { return inv_tok_; }
365
367 void throw_if_cancelled() const {
368 cmd_tok_.throw_if_cancelled();
369 inv_tok_.throw_if_cancelled();
370 }
371
372private:
373 std::shared_ptr<State> state_;
374 std::shared_ptr<CancellationSource> src_;
375 CancellationToken cmd_tok_;
376 CancellationToken inv_tok_;
377};
378
382template<typename R, typename... Args>
383class AsyncCommandCore {
384public:
385 using State = AsyncCommandState<R>;
386 using Action = std::function<Task<R>(CancellationToken, Args...)>;
387 using ArgsTuple = std::shared_ptr<std::tuple<Args...>>;
388
389 AsyncCommandCore(std::shared_ptr<State> s, Action a, AsyncCommandPolicy p)
390 : state(std::move(s)), action(std::move(a)), policy(p) {}
391
392 std::shared_ptr<State> state;
393 Action action;
394 AsyncCommandPolicy policy;
395
400 template<typename Fn>
401 static Action make_action(Fn f) {
402 if constexpr (CancellableAction<Fn, Args...>) {
403 return Action(std::move(f));
404 } else if constexpr (std::is_void_v<R>) {
405 return [f = std::move(f)](CancellationToken,
406 Args... a) mutable -> Task<void> {
407 co_await f(std::move(a)...);
408 };
409 } else {
410 return [f = std::move(f)](CancellationToken,
411 Args... a) mutable -> Task<R> {
412 co_return co_await f(std::move(a)...);
413 };
414 }
415 }
416
419 bool accept_new_invocation() {
420 switch (policy) {
422 return true;
424 cancel_all_in_flight();
425 return true;
427 return state->inflight.load(std::memory_order_acquire) == 0;
428 }
429 return true;
430 }
431
432 void cancel_all_in_flight() {
433 std::vector<std::shared_ptr<CancellationSource>> victims;
434 {
435 std::lock_guard lk(state->m_sources);
436 victims = state->invocation_sources;
437 }
438 for (auto& s : victims) s->cancel();
439 }
440
441 void cancel_on_destruction() {
442 // Keep the shared state and each invocation source alive independently
443 // of callbacks that synchronously resume and destroy their frames.
444 // Swap rather than copy: teardown does not need to allocate a snapshot.
445 auto keep_alive = state;
446 std::vector<std::shared_ptr<CancellationSource>> victims;
447 {
448 std::lock_guard lk(keep_alive->m_sources);
449 victims.swap(keep_alive->invocation_sources);
450 }
451 keep_alive->cancel.cancel();
452 // The action receives the invocation token, not the command token.
453 // Cancel outside m_sources: resumed Invocation destructors take it.
454 for (auto& source : victims) source->cancel();
455 }
456};
457
458} // namespace detail
459
460// ── primary template (R != void) ──────────────────────────────────────
461template<typename R, typename... Args>
463 using Core = detail::AsyncCommandCore<R, Args...>;
464 using State = typename Core::State;
465 using ArgsTuple = typename Core::ArgsTuple;
466 using Invocation = detail::Invocation<R>;
467
468 Core core_;
469
470public:
471 using Action = typename Core::Action;
472
486 template<typename Ui, typename Worker, typename Fn>
487 requires (detail::CancellableAction<Fn, Args...>
488 || detail::PlainAction<Fn, Args...>)
489 && std::is_base_of_v<IExecutor, Ui>
490 && std::is_base_of_v<IExecutor, Worker>
491 && (!std::is_same_v<Ui, IExecutor>
492 || !std::is_same_v<Worker, IExecutor>)
493 AsyncCommand(Ui& ui, Worker& worker, Fn action,
495 : core_(std::make_shared<State>(ui, worker),
496 Core::template make_action<Fn>(std::move(action)),
497 policy),
498 is_executing (core_.state->is_executing),
499 last_error (core_.state->last_error),
501 last_result (core_.state->last_result)
502 {
503 if constexpr (!std::is_same_v<Ui, IExecutor>) {
504 static_assert(is_safe_graph_executor_v<Ui>,
505 "AsyncCommand: `ui` must be a graph-thread executor. "
506 "Specialise `aria::async::is_safe_graph_executor<YourExec>` "
507 "or use `MainThreadExecutor`.");
508 }
509 if constexpr (!std::is_same_v<Worker, IExecutor>) {
511 "AsyncCommand: `worker` must be a worker-capable executor. "
512 "Specialise `aria::async::is_safe_worker_executor<YourExec>` "
513 "or use `ThreadPoolExecutor` / `MainThreadExecutor`.");
514 }
515 if constexpr (!std::is_same_v<Ui, IExecutor>
516 && !std::is_same_v<Worker, IExecutor>) {
517 static_assert(!(std::is_same_v<Ui, InlineExecutor>
518 && !std::is_same_v<Worker, InlineExecutor>),
519 "AsyncCommand: cannot use `InlineExecutor` as the graph-thread "
520 "executor when `worker` runs on a different thread. Use "
521 "`MainThreadExecutor` for the `ui` parameter.");
522 }
523 detail::check_executor_safety_runtime(ui, worker);
524 }
525
528 template<typename Fn>
529 requires detail::CancellableAction<Fn, Args...>
530 || detail::PlainAction<Fn, Args...>
531 AsyncCommand(IExecutor& ui, IExecutor& worker, Fn action,
533 : core_(std::make_shared<State>(ui, worker),
534 Core::template make_action<Fn>(std::move(action)),
535 policy),
536 is_executing (core_.state->is_executing),
537 last_error (core_.state->last_error),
539 last_result (core_.state->last_result)
540 {
541 detail::check_executor_safety_runtime(ui, worker);
542 }
543
545 // Signal every in-flight coroutine to bail on its next probe.
546 // state lives on as long as some coroutine still references it,
547 // so Property writes inside those coroutines remain safe.
548 core_.cancel_on_destruction();
549 }
550
551 AsyncCommand(const AsyncCommand&) = delete;
553
558 void execute(Args... args) {
559 if (!core_.accept_new_invocation()) return;
560 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
561 fire_and_forget_(tup).start_detached_();
562 }
563
575 if (!core_.accept_new_invocation()) {
577 }
578 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
579 co_return co_await run_to_result_(tup);
580 }
581
582 [[nodiscard]] AsyncCommandPolicy policy() const noexcept { return core_.policy; }
583
584 // Public observable handles.
589
590private:
595 Task<void> fire_and_forget_(ArgsTuple args) {
596 auto r = co_await run_to_result_(std::move(args));
597 if (r.failed() && r.error) {
598 report_async_error(std::string("AsyncCommand: ") + r.error->message);
599 }
600 // Cancelled / Completed / (Dropped — never reaches here, the
601 // policy gate above filters it out before run_to_result_) are
602 // intentionally silent on the sink path; observers learn about
603 // them via is_executing / last_result Properties.
604 }
605
610 Task<AsyncCommandResult<R>> run_to_result_(ArgsTuple args) {
611 auto state = core_.state;
612 auto action = core_.action;
613 Invocation inv{state};
614
615 co_await schedule_on(*state->ui);
616 // Pre-flight cancellation check — if the command was cancelled
617 // between accept_new_invocation() and our first hop onto the
618 // ui executor, surface that as Cancelled without ever invoking
619 // the user's action.
620 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
622 ::aria::Error::cancellation("AsyncCommand"));
623 }
624
625 std::optional<R> value;
626 std::exception_ptr ex;
627 try {
628 co_await schedule_on(*state->worker);
629 inv.throw_if_cancelled();
630 value.emplace(co_await std::apply(
631 [&](auto&&... a) -> Task<R> {
632 return action(inv.inv_tok(), std::forward<decltype(a)>(a)...);
633 }, *args));
634 } catch (...) {
635 ex = std::current_exception();
636 }
637
638 co_await schedule_on(*state->ui);
639 if (ex) {
640 auto cls = detail::classify_async_exception(
641 ex, state->last_error, state->last_error_message);
642 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
643 co_return AsyncCommandResult<R>::cancelled_(std::move(cls.error));
644 }
645 co_return AsyncCommandResult<R>::failed_(std::move(cls.error));
646 }
647 state->last_result = value;
648 co_return AsyncCommandResult<R>::completed_with(std::move(*value));
649 }
650};
651
652// ── partial specialisation (R == void) ────────────────────────────
653template<typename... Args>
654class AsyncCommand<void, Args...> {
655 using Core = detail::AsyncCommandCore<void, Args...>;
656 using State = typename Core::State;
657 using ArgsTuple = typename Core::ArgsTuple;
658 using Invocation = detail::Invocation<void>;
659
660 Core core_;
661
662public:
663 using Action = typename Core::Action;
664
665 template<typename Ui, typename Worker, typename Fn>
666 requires (detail::CancellableAction<Fn, Args...>
667 || detail::PlainAction<Fn, Args...>)
668 && std::is_base_of_v<IExecutor, Ui>
669 && std::is_base_of_v<IExecutor, Worker>
670 && (!std::is_same_v<Ui, IExecutor>
671 || !std::is_same_v<Worker, IExecutor>)
672 AsyncCommand(Ui& ui, Worker& worker, Fn action,
674 : core_(std::make_shared<State>(ui, worker),
675 Core::template make_action<Fn>(std::move(action)),
676 policy),
677 is_executing (core_.state->is_executing),
678 last_error (core_.state->last_error),
680 {
681 if constexpr (!std::is_same_v<Ui, IExecutor>) {
682 static_assert(is_safe_graph_executor_v<Ui>,
683 "AsyncCommand<void>: `ui` must be a graph-thread executor. "
684 "Use `MainThreadExecutor` for any multi-threaded scenario.");
685 }
686 if constexpr (!std::is_same_v<Worker, IExecutor>) {
688 "AsyncCommand<void>: `worker` must be a worker-capable executor.");
689 }
690 if constexpr (!std::is_same_v<Ui, IExecutor>
691 && !std::is_same_v<Worker, IExecutor>) {
692 static_assert(!(std::is_same_v<Ui, InlineExecutor>
693 && !std::is_same_v<Worker, InlineExecutor>),
694 "AsyncCommand<void>: cannot use `InlineExecutor` as the "
695 "graph-thread executor when `worker` runs on a different thread. "
696 "Use `MainThreadExecutor` for `ui`.");
697 }
698 detail::check_executor_safety_runtime(ui, worker);
699 }
700
706 template<typename Fn>
707 requires detail::CancellableAction<Fn, Args...>
708 || detail::PlainAction<Fn, Args...>
709 AsyncCommand(IExecutor& ui, IExecutor& worker, Fn action,
711 : core_(std::make_shared<State>(ui, worker),
712 Core::template make_action<Fn>(std::move(action)),
713 policy),
714 is_executing (core_.state->is_executing),
715 last_error (core_.state->last_error),
717 {
718 detail::check_executor_safety_runtime(ui, worker);
719 }
720
721 ~AsyncCommand() { core_.cancel_on_destruction(); }
722
723 AsyncCommand(const AsyncCommand&) = delete;
725
728 void execute(Args... args) {
729 if (!core_.accept_new_invocation()) return;
730 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
731 fire_and_forget_(tup).start_detached_();
732 }
733
739 if (!core_.accept_new_invocation()) {
741 }
742 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
743 co_return co_await run_to_result_(tup);
744 }
745
746 [[nodiscard]] AsyncCommandPolicy policy() const noexcept { return core_.policy; }
747
751
752private:
753 Task<void> fire_and_forget_(ArgsTuple args) {
754 auto r = co_await run_to_result_(std::move(args));
755 if (r.failed() && r.error) {
756 report_async_error(std::string("AsyncCommand: ") + r.error->message);
757 }
758 }
759
761 Task<AsyncCommandResult<void>> run_to_result_(ArgsTuple args) {
762 auto state = core_.state;
763 auto action = core_.action;
764 Invocation inv{state};
765
766 co_await schedule_on(*state->ui);
767 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
769 ::aria::Error::cancellation("AsyncCommand"));
770 }
771
772 std::exception_ptr ex;
773 try {
774 co_await schedule_on(*state->worker);
775 inv.throw_if_cancelled();
776 co_await std::apply(
777 [&](auto&&... a) -> Task<void> {
778 return action(inv.inv_tok(), std::forward<decltype(a)>(a)...);
779 }, *args);
780 } catch (...) {
781 ex = std::current_exception();
782 }
783
784 co_await schedule_on(*state->ui);
785 if (ex) {
786 auto cls = detail::classify_async_exception(
787 ex, state->last_error, state->last_error_message);
788 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
789 co_return AsyncCommandResult<void>::cancelled_(std::move(cls.error));
790 }
791 co_return AsyncCommandResult<void>::failed_(std::move(cls.error));
792 }
794 }
795};
796
797} // namespace aria::async
typename Core::Action Action
Definition async_command.hpp:663
Property< bool > & is_executing
Definition async_command.hpp:748
Property< std::string > & last_error_message
Definition async_command.hpp:750
AsyncCommand(Ui &ui, Worker &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Definition async_command.hpp:672
void execute(Args... args)
Fire-and-forget.
Definition async_command.hpp:728
AsyncCommandPolicy policy() const noexcept
Definition async_command.hpp:746
Property< std::optional<::aria::Error > > & last_error
Definition async_command.hpp:749
AsyncCommand(const AsyncCommand &)=delete
AsyncCommand(IExecutor &ui, IExecutor &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Type-erased overload — same role as on the primary template: the choice for ViewModels that receive I...
Definition async_command.hpp:709
Task< AsyncCommandResult< void > > co_execute(Args... args)
Awaitable version: caller co_awaits a structured result.
Definition async_command.hpp:738
~AsyncCommand()
Definition async_command.hpp:721
AsyncCommand & operator=(const AsyncCommand &)=delete
Property< std::optional< R > > & last_result
Definition async_command.hpp:588
AsyncCommand & operator=(const AsyncCommand &)=delete
void execute(Args... args)
Fire-and-forget.
Definition async_command.hpp:558
Property< std::optional<::aria::Error > > & last_error
Definition async_command.hpp:586
AsyncCommand(IExecutor &ui, IExecutor &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Type-erased overload — the choice for ViewModels that receive IExecutor& from a DI container.
Definition async_command.hpp:531
typename Core::Action Action
Definition async_command.hpp:471
Task< AsyncCommandResult< R > > co_execute(Args... args)
Awaitable version: caller co_awaits a structured result.
Definition async_command.hpp:574
AsyncCommand(Ui &ui, Worker &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Construct with a "plain" or "cancellable" action.
Definition async_command.hpp:493
Property< bool > & is_executing
Definition async_command.hpp:585
Property< std::string > & last_error_message
Definition async_command.hpp:587
AsyncCommand(const AsyncCommand &)=delete
~AsyncCommand()
Definition async_command.hpp:544
AsyncCommandPolicy policy() const noexcept
Definition async_command.hpp:582
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:36
Definition task.hpp:78
Definition property.hpp:103
Definition async_command.hpp:118
auto schedule_on(IExecutor &exec)
Schedule a coroutine to resume on the given executor.
Definition executor.hpp:396
constexpr bool is_safe_worker_executor_v
Definition executor_traits.hpp:46
void report_async_error(std::string_view msg) noexcept
Definition async_error_sink.hpp:59
AsyncCommandPolicy
Concurrency strategy when execute() is called while another invocation is already running.
Definition async_command.hpp:122
@ DropIfRunning
silently ignore new invocations while busy
Definition async_command.hpp:125
@ Parallel
default — all invocations run concurrently
Definition async_command.hpp:123
@ LatestOnly
cancel any in-flight invocations before starting
Definition async_command.hpp:124
constexpr bool is_safe_graph_executor_v
Definition executor_traits.hpp:44
void publish_trace_unchecked(const TraceEvent &event) noexcept
Publish an already-built event using one owning sink snapshot.
Definition diagnostics.hpp:293
bool has_trace_sink() noexcept
True iff a sink is currently installed.
Definition diagnostics.hpp:286
@ Async
AsyncCommand / AsyncResource lifecycle.
Definition diagnostics.hpp:68
@ Error
Definition loadable.hpp:62
Definition validation_key.hpp:110
static Error from_exception(std::exception_ptr ex, std::string source_tag)
Catch-all converter from a thrown exception_ptr to a typed Error.
Definition error.hpp:237
static Error cancellation(std::string source_tag="AsyncCommand")
Cancellation.
Definition error.hpp:192
static Error timeout(std::string source_tag="AsyncCommand")
with_timeout deadline expired.
Definition error.hpp:198
static AsyncCommandResult dropped_()
Definition async_command_result.hpp:150
static AsyncCommandResult failed_(::aria::Error e)
Definition async_command_result.hpp:161
static AsyncCommandResult cancelled_(::aria::Error e)
Definition async_command_result.hpp:155
static AsyncCommandResult completed_with(R v)
Definition async_command_result.hpp:144