Aria 3.2.6
C++23 MVVM framework — 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 using PreparedAction = std::function<Task<R>(const CancellationToken&, std::tuple<Args...>&)>;
389
390 AsyncCommandCore(std::shared_ptr<State> s, PreparedAction a, AsyncCommandPolicy p)
391 : state(std::move(s)), action(std::move(a)), policy(p) {}
392
393 std::shared_ptr<State> state;
394 PreparedAction action;
395 AsyncCommandPolicy policy;
396
403 template<typename Fn>
404 static PreparedAction make_action(Fn f) {
405 auto target = std::make_shared<Fn>(std::move(f));
406 return [target = std::move(target)]([[maybe_unused]] const CancellationToken& token,
407 std::tuple<Args...>& args) -> Task<R> {
408 return std::apply([&](auto&... values) -> Task<R> {
409 if constexpr (CancellableAction<Fn, Args...>) {
410 return (*target)(token, std::move(values)...);
411 } else {
412 return (*target)(std::move(values)...);
413 }
414 }, args);
415 };
416 }
417
420 bool accept_new_invocation() {
421 switch (policy) {
423 return true;
425 cancel_all_in_flight();
426 return true;
428 return state->inflight.load(std::memory_order_acquire) == 0;
429 }
430 return true;
431 }
432
433 void cancel_all_in_flight() {
434 std::vector<std::shared_ptr<CancellationSource>> victims;
435 {
436 std::lock_guard lk(state->m_sources);
437 victims = state->invocation_sources;
438 }
439 for (auto& s : victims) s->cancel();
440 }
441
442 void cancel_on_destruction() {
443 // Keep the shared state and each invocation source alive independently
444 // of callbacks that synchronously resume and destroy their frames.
445 // Swap rather than copy: teardown does not need to allocate a snapshot.
446 auto keep_alive = state;
447 std::vector<std::shared_ptr<CancellationSource>> victims;
448 {
449 std::lock_guard lk(keep_alive->m_sources);
450 victims.swap(keep_alive->invocation_sources);
451 }
452 keep_alive->cancel.cancel();
453 // The action receives the invocation token, not the command token.
454 // Cancel outside m_sources: resumed Invocation destructors take it.
455 for (auto& source : victims) source->cancel();
456 }
457};
458
459} // namespace detail
460
461// ── primary template (R != void) ──────────────────────────────────────
462template<typename R, typename... Args>
464 using Core = detail::AsyncCommandCore<R, Args...>;
465 using State = typename Core::State;
466 using ArgsTuple = typename Core::ArgsTuple;
467 using Invocation = detail::Invocation<R>;
468
469 Core core_;
470
471public:
472 using Action = typename Core::Action;
473
487 template<typename Ui, typename Worker, typename Fn>
488 requires (detail::CancellableAction<Fn, Args...>
489 || detail::PlainAction<Fn, Args...>)
490 && std::is_base_of_v<IExecutor, Ui>
491 && std::is_base_of_v<IExecutor, Worker>
492 && (!std::is_same_v<Ui, IExecutor>
493 || !std::is_same_v<Worker, IExecutor>)
494 AsyncCommand(Ui& ui, Worker& worker, Fn action,
496 : core_(std::make_shared<State>(ui, worker),
497 Core::template make_action<Fn>(std::move(action)),
498 policy),
499 is_executing (core_.state->is_executing),
500 last_error (core_.state->last_error),
502 last_result (core_.state->last_result)
503 {
504 if constexpr (!std::is_same_v<Ui, IExecutor>) {
505 static_assert(is_safe_graph_executor_v<Ui>,
506 "AsyncCommand: `ui` must be a graph-thread executor. "
507 "Specialise `aria::async::is_safe_graph_executor<YourExec>` "
508 "or use `MainThreadExecutor`.");
509 }
510 if constexpr (!std::is_same_v<Worker, IExecutor>) {
512 "AsyncCommand: `worker` must be a worker-capable executor. "
513 "Specialise `aria::async::is_safe_worker_executor<YourExec>` "
514 "or use `ThreadPoolExecutor` / `MainThreadExecutor`.");
515 }
516 if constexpr (!std::is_same_v<Ui, IExecutor>
517 && !std::is_same_v<Worker, IExecutor>) {
518 static_assert(!(std::is_same_v<Ui, InlineExecutor>
519 && !std::is_same_v<Worker, InlineExecutor>),
520 "AsyncCommand: cannot use `InlineExecutor` as the graph-thread "
521 "executor when `worker` runs on a different thread. Use "
522 "`MainThreadExecutor` for the `ui` parameter.");
523 }
524 detail::check_executor_safety_runtime(ui, worker);
525 }
526
529 template<typename Fn>
530 requires detail::CancellableAction<Fn, Args...>
531 || detail::PlainAction<Fn, Args...>
532 AsyncCommand(IExecutor& ui, IExecutor& worker, Fn action,
534 : core_(std::make_shared<State>(ui, worker),
535 Core::template make_action<Fn>(std::move(action)),
536 policy),
537 is_executing (core_.state->is_executing),
538 last_error (core_.state->last_error),
540 last_result (core_.state->last_result)
541 {
542 detail::check_executor_safety_runtime(ui, worker);
543 }
544
546 // Signal every in-flight coroutine to bail on its next probe.
547 // state lives on as long as some coroutine still references it,
548 // so Property writes inside those coroutines remain safe.
549 core_.cancel_on_destruction();
550 }
551
552 AsyncCommand(const AsyncCommand&) = delete;
554
559 void execute(Args... args) {
560 if (!core_.accept_new_invocation()) return;
561 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
562 fire_and_forget_(tup).start_detached_();
563 }
564
576 if (!core_.accept_new_invocation()) {
578 }
579 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
580 co_return co_await run_to_result_(tup);
581 }
582
583 [[nodiscard]] AsyncCommandPolicy policy() const noexcept { return core_.policy; }
584
585 // Public observable handles.
590
591private:
596 Task<void> fire_and_forget_(ArgsTuple args) {
597 auto r = co_await run_to_result_(std::move(args));
598 if (r.failed() && r.error) {
599 report_async_error(std::string("AsyncCommand: ") + r.error->message);
600 }
601 // Cancelled / Completed / (Dropped — never reaches here, the
602 // policy gate above filters it out before run_to_result_) are
603 // intentionally silent on the sink path; observers learn about
604 // them via is_executing / last_result Properties.
605 }
606
611 Task<AsyncCommandResult<R>> run_to_result_(ArgsTuple args) {
612 auto state = core_.state;
613 auto action = core_.action;
614 Invocation inv{state};
615
616 co_await schedule_on(*state->ui);
617 // Pre-flight cancellation check — if the command was cancelled
618 // between accept_new_invocation() and our first hop onto the
619 // ui executor, surface that as Cancelled without ever invoking
620 // the user's action.
621 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
623 ::aria::Error::cancellation("AsyncCommand"));
624 }
625
626 std::optional<R> value;
627 std::exception_ptr ex;
628 try {
629 co_await schedule_on(*state->worker);
630 inv.throw_if_cancelled();
631 value.emplace(co_await action(inv.inv_tok(), *args));
632 } catch (...) {
633 ex = std::current_exception();
634 }
635
636 co_await schedule_on(*state->ui);
637 if (ex) {
638 auto cls = detail::classify_async_exception(
639 ex, state->last_error, state->last_error_message);
640 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
641 co_return AsyncCommandResult<R>::cancelled_(std::move(cls.error));
642 }
643 co_return AsyncCommandResult<R>::failed_(std::move(cls.error));
644 }
645 state->last_result = value;
646 co_return AsyncCommandResult<R>::completed_with(std::move(*value));
647 }
648};
649
650// ── partial specialisation (R == void) ────────────────────────────
651template<typename... Args>
652class AsyncCommand<void, Args...> {
653 using Core = detail::AsyncCommandCore<void, Args...>;
654 using State = typename Core::State;
655 using ArgsTuple = typename Core::ArgsTuple;
656 using Invocation = detail::Invocation<void>;
657
658 Core core_;
659
660public:
661 using Action = typename Core::Action;
662
663 template<typename Ui, typename Worker, typename Fn>
664 requires (detail::CancellableAction<Fn, Args...>
665 || detail::PlainAction<Fn, Args...>)
666 && std::is_base_of_v<IExecutor, Ui>
667 && std::is_base_of_v<IExecutor, Worker>
668 && (!std::is_same_v<Ui, IExecutor>
669 || !std::is_same_v<Worker, IExecutor>)
670 AsyncCommand(Ui& ui, Worker& worker, Fn action,
672 : core_(std::make_shared<State>(ui, worker),
673 Core::template make_action<Fn>(std::move(action)),
674 policy),
675 is_executing (core_.state->is_executing),
676 last_error (core_.state->last_error),
678 {
679 if constexpr (!std::is_same_v<Ui, IExecutor>) {
680 static_assert(is_safe_graph_executor_v<Ui>,
681 "AsyncCommand<void>: `ui` must be a graph-thread executor. "
682 "Use `MainThreadExecutor` for any multi-threaded scenario.");
683 }
684 if constexpr (!std::is_same_v<Worker, IExecutor>) {
686 "AsyncCommand<void>: `worker` must be a worker-capable executor.");
687 }
688 if constexpr (!std::is_same_v<Ui, IExecutor>
689 && !std::is_same_v<Worker, IExecutor>) {
690 static_assert(!(std::is_same_v<Ui, InlineExecutor>
691 && !std::is_same_v<Worker, InlineExecutor>),
692 "AsyncCommand<void>: cannot use `InlineExecutor` as the "
693 "graph-thread executor when `worker` runs on a different thread. "
694 "Use `MainThreadExecutor` for `ui`.");
695 }
696 detail::check_executor_safety_runtime(ui, worker);
697 }
698
704 template<typename Fn>
705 requires detail::CancellableAction<Fn, Args...>
706 || detail::PlainAction<Fn, Args...>
707 AsyncCommand(IExecutor& ui, IExecutor& worker, Fn action,
709 : core_(std::make_shared<State>(ui, worker),
710 Core::template make_action<Fn>(std::move(action)),
711 policy),
712 is_executing (core_.state->is_executing),
713 last_error (core_.state->last_error),
715 {
716 detail::check_executor_safety_runtime(ui, worker);
717 }
718
719 ~AsyncCommand() { core_.cancel_on_destruction(); }
720
721 AsyncCommand(const AsyncCommand&) = delete;
723
726 void execute(Args... args) {
727 if (!core_.accept_new_invocation()) return;
728 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
729 fire_and_forget_(tup).start_detached_();
730 }
731
737 if (!core_.accept_new_invocation()) {
739 }
740 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
741 co_return co_await run_to_result_(tup);
742 }
743
744 [[nodiscard]] AsyncCommandPolicy policy() const noexcept { return core_.policy; }
745
749
750private:
751 Task<void> fire_and_forget_(ArgsTuple args) {
752 auto r = co_await run_to_result_(std::move(args));
753 if (r.failed() && r.error) {
754 report_async_error(std::string("AsyncCommand: ") + r.error->message);
755 }
756 }
757
759 Task<AsyncCommandResult<void>> run_to_result_(ArgsTuple args) {
760 auto state = core_.state;
761 auto action = core_.action;
762 Invocation inv{state};
763
764 co_await schedule_on(*state->ui);
765 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
767 ::aria::Error::cancellation("AsyncCommand"));
768 }
769
770 std::exception_ptr ex;
771 try {
772 co_await schedule_on(*state->worker);
773 inv.throw_if_cancelled();
774 co_await action(inv.inv_tok(), *args);
775 } catch (...) {
776 ex = std::current_exception();
777 }
778
779 co_await schedule_on(*state->ui);
780 if (ex) {
781 auto cls = detail::classify_async_exception(
782 ex, state->last_error, state->last_error_message);
783 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
784 co_return AsyncCommandResult<void>::cancelled_(std::move(cls.error));
785 }
786 co_return AsyncCommandResult<void>::failed_(std::move(cls.error));
787 }
789 }
790};
791
792} // namespace aria::async
typename Core::Action Action
Definition async_command.hpp:661
Property< bool > & is_executing
Definition async_command.hpp:746
Property< std::string > & last_error_message
Definition async_command.hpp:748
AsyncCommand(Ui &ui, Worker &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Definition async_command.hpp:670
void execute(Args... args)
Fire-and-forget.
Definition async_command.hpp:726
AsyncCommandPolicy policy() const noexcept
Definition async_command.hpp:744
Property< std::optional<::aria::Error > > & last_error
Definition async_command.hpp:747
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:707
Task< AsyncCommandResult< void > > co_execute(Args... args)
Awaitable version: caller co_awaits a structured result.
Definition async_command.hpp:736
~AsyncCommand()
Definition async_command.hpp:719
AsyncCommand & operator=(const AsyncCommand &)=delete
Property< std::optional< R > > & last_result
Definition async_command.hpp:589
AsyncCommand & operator=(const AsyncCommand &)=delete
void execute(Args... args)
Fire-and-forget.
Definition async_command.hpp:559
Property< std::optional<::aria::Error > > & last_error
Definition async_command.hpp:587
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:532
typename Core::Action Action
Definition async_command.hpp:472
Task< AsyncCommandResult< R > > co_execute(Args... args)
Awaitable version: caller co_awaits a structured result.
Definition async_command.hpp:575
AsyncCommand(Ui &ui, Worker &worker, Fn action, AsyncCommandPolicy policy=AsyncCommandPolicy::Parallel)
Construct with a "plain" or "cancellable" action.
Definition async_command.hpp:494
Property< bool > & is_executing
Definition async_command.hpp:586
Property< std::string > & last_error_message
Definition async_command.hpp:588
AsyncCommand(const AsyncCommand &)=delete
~AsyncCommand()
Definition async_command.hpp:545
AsyncCommandPolicy policy() const noexcept
Definition async_command.hpp:583
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:37
Definition task.hpp:79
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:472
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