morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
completion.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <atomic>
5#include <concepts>
6#include <core/async/Cancellation.hpp>
7#include <core/async/ExecutorContext.hpp>
8#include <core/async/StopToken.hpp>
9#include <exception>
10#include <functional>
11#include <memory>
12#include <optional>
13#include <stdexcept>
14#include <string>
15#include <thread>
16#include <utility>
17#include <vector>
18
19#include "../attributes.hpp"
20#include "callback_scope.hpp"
23#include "executor.hpp"
24#include "logger.hpp"
25
26namespace morph::async {
27
28namespace detail {
29
30// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
31//
32// One state per operation, shared by the producer that settles it and the
33// consumer that attaches to it, with no lock between them:
34//
35// - **Settling** is claimed once with an atomic exchange (`settled`), so a late
36// settle -- a reply after `cancelPending` -- returns before it allocates
37// anything. The claimant stores `value` or `error` and publishes it with a
38// release store of `ready`; both are write-once, so any thread that sees
39// `ready` may read them. It then posts `deliver()` to `cbExec`.
40// - **Everything else is the owner's**: `onOk`, `onErr`, `delivered`,
41// `stopLinks` are touched only on `cbExec` -- by `deliver()`, and by the
42// consumer's attaches, which run there (`onOwner`). The executor's queue is
43// the happens-before edge between the settle and the handlers.
44//
45// `enable_shared_from_this` is load-bearing: `deliver()` and a late attach's
46// fire-now closure capture a `shared_ptr` to this state and read the settled
47// value *in place*. That is what makes the copy budget independent of how many
48// handlers are attached, what lets `T` be move-only, and what keeps the state
49// -- and so its orphan logger -- alive until delivery has run. Every
50// `CompletionState<T>` in the tree is created by `std::make_shared`.
51template <typename T>
52struct CompletionState : std::enable_shared_from_this<CompletionState<T>> {
53 static_assert(std::move_constructible<T>,
54 "morph::async::Completion<T> requires only that T be move-constructible. Copyability is a "
55 "per-handler obligation: a handler taking `const T&` imposes nothing, a handler taking `T` by "
56 "value requires T to be copyable and is diagnosed where that handler is written.");
57
58 std::optional<T> value;
59 std::exception_ptr error;
60 // Claimed by the first settle; every later one returns at once.
61 std::atomic<bool> settled{false};
62 // Published by the settle that claimed `settled`, after `value` or `error`
63 // is stored: `true` means exactly one of them is engaged and final.
64 std::atomic<bool> ready{false};
65 // Every handler attached while the state is not yet delivered is kept (not
66 // overwritten): a second/third attach composes with earlier ones instead of
67 // silently discarding them. `deliver()` invokes all of them, in attachment
68 // order. See docs/spec/core/completion.md, "Failure modes" / fan-out.
69 //
70 // Erased as `void(const T&)`, not `void(T)`. One erased type accepts every
71 // handler spelling a caller already writes -- `[](const T&)`, `[](T)`, a
72 // `std::function<void(T)>` object -- because each is invocable with
73 // `const T&`. A handler that wants its own value gets exactly one copy, at
74 // its own parameter binding, where the reader of that call site can see it;
75 // a handler that only observes pays nothing. Erasing as `void(T)` would
76 // charge every handler a copy whether or not it wanted one.
77 std::vector<std::function<void(const T&)>> onOk;
78 std::vector<std::function<void(std::exception_ptr)>> onErr;
79 // Whether the error is someone's to handle, which silences the orphan
80 // logger. Atomic because `bridge::detail::takeSettled` takes an outcome
81 // without attaching, on whichever thread holds the completion.
82 std::atomic<bool> onErrAttached{false};
83 // Set by `deliver()`, on the owner. A handler attached after it is posted on
84 // its own; one attached before is in the lists `deliver()` drains.
85 std::atomic<bool> delivered{false};
86 // The thread an attach outside every executor's task presumed to be the
87 // owner's own, and the thread `deliver()` ran on: compared, in both
88 // orders, so that presumption is checked rather than trusted.
89 std::atomic<std::thread::id> presumedOwnerThread;
90 std::atomic<std::thread::id> deliveredOn;
91 ::morph::exec::IExecutor* cbExec = nullptr;
92 // The stop source of the call this state reports on, or null for a call
93 // that cannot be stopped. Set by the producer before the state is handed
94 // out, and read only on the owner.
95 std::shared_ptr<::core::async::StopSource> stopSource;
96 // One `StopCallback` per `CallbackScope` a gated handler was attached
97 // through, relaying that scope's stop to `stopSource`. Released when the
98 // state is delivered: a settled call has nothing left to stop.
99 std::vector<std::shared_ptr<void>> stopLinks;
100
101 // Relays a scope's stop request to the call's own stop source. Holds the
102 // source weakly, so a link outliving its call keeps nothing alive.
103 struct StopRelay {
104 std::weak_ptr<::core::async::StopSource> source;
105 void operator()() const noexcept {
106 if (auto const strong = source.lock()) {
107 static_cast<void>(strong->request_stop());
108 }
109 }
110 };
111
112 /// Whether the calling thread may touch the owner's half of this state,
113 /// reporting it (assert, or the test probe) when it may not.
114 ///
115 /// The owner is `cbExec`. Inside one of its tasks is the owner. Outside
116 /// every executor's task is presumed to be the owner's own thread running
117 /// code outside its tasks -- a Qt slot, a test body -- and recorded, so
118 /// `deliver()` can check the presumption against where it actually runs.
119 void checkOwner(char const* site) noexcept {
120 if (::morph::exec::runningOn(*cbExec)) {
121 return;
122 }
123 if (::core::async::ExecutorScope::innermost() != nullptr) {
124 ::morph::exec::detail::noteOwner(site, cbExec->coreExecutor(), false);
125 return;
126 }
127 auto const self = std::this_thread::get_id();
128 presumedOwnerThread.store(self, std::memory_order_relaxed);
129 if (delivered.load(std::memory_order_acquire) && deliveredOn.load(std::memory_order_relaxed) != self) {
130 ::morph::exec::detail::noteOwner(site, cbExec->coreExecutor(), false);
131 }
132 }
133
134 // Links the stop of the scope @p scopeStop belongs to with this call's own
135 // stop source, until the state is delivered. A no-op for a call with no
136 // stop source, a scope that can no longer be stopped, a state already
137 // delivered, or one with no executor to deliver on. A scope already stopped
138 // stops the call here. Runs on the owner.
139 void linkStop(::core::async::StopToken scopeStop) {
140 if (cbExec == nullptr || stopSource == nullptr || delivered.load(std::memory_order_relaxed) ||
141 !scopeStop.stop_possible()) {
142 return;
143 }
144 checkOwner("Completion::linkStop");
145 stopLinks.push_back(std::make_shared<::core::async::StopCallback<StopRelay>>(
146 std::move(scopeStop), StopRelay{std::weak_ptr<::core::async::StopSource>{stopSource}}));
147 }
148
149 // Relays a caller's cancel to the call: settles it with `OperationCancelled`
150 // first, so that outcome wins over the one the stopped work produces, then
151 // requests stop on its stop source. Holds both weakly. Runs on whichever
152 // thread requested the stop; a settle only posts its delivery.
153 struct CancelRelay {
154 std::weak_ptr<CompletionState> state;
155 std::weak_ptr<::core::async::StopSource> source;
156 void operator()() const noexcept {
157 if (auto const strong = state.lock()) {
158 try {
159 strong->setException(std::make_exception_ptr(::core::async::OperationCancelled{}));
160 } catch (...) { // NOLINT(bugprone-empty-catch): a stop callback must not throw
161 // An executor that refuses the delivery leaves nothing to tell.
162 }
163 }
164 if (auto const strong = source.lock()) {
165 static_cast<void>(strong->request_stop());
166 }
167 }
168 };
169
170 // Links @p cancel to this call until the state is delivered: a stop on it
171 // settles the call with `OperationCancelled` and stops its stop source, if
172 // it has one. A no-op for a token that can never be stopped, a state
173 // already delivered, or one with no executor. A token already stopped
174 // cancels the call here. Runs on the owner.
175 void linkCancel(::core::async::StopToken cancel) {
176 if (cbExec == nullptr || delivered.load(std::memory_order_relaxed) || !cancel.stop_possible()) {
177 return;
178 }
179 checkOwner("Completion::linkCancel");
180 stopLinks.push_back(std::make_shared<::core::async::StopCallback<CancelRelay>>(
181 std::move(cancel), CancelRelay{
182 .state = this->weak_from_this(),
183 .source = std::weak_ptr<::core::async::StopSource>{stopSource},
184 }));
185 }
186
187 void setValue(T val) {
188 if (settled.exchange(true, std::memory_order_acq_rel)) {
189 return;
190 }
191 // `emplace`, not `value = std::move(val)`: assigning through
192 // `std::optional` requires `T` to be move-*assignable* as well as
193 // move-constructible, which would quietly make the `static_assert`
194 // above a lie for a `T` with a deleted assignment operator. A store
195 // whose move throws leaves `value` disengaged and releases the claim,
196 // so the state is still unsettled with every handler kept.
197 try {
198 value.emplace(std::move(val));
199 } catch (...) {
200 settled.store(false, std::memory_order_release);
201 throw;
202 }
203 publish();
204 }
205
206 void setException(const std::exception_ptr& exc) {
207 if (settled.exchange(true, std::memory_order_acq_rel)) {
208 return;
209 }
210 // A null `exc` must never reach `error`. Storing one would publish
211 // `ready` with `error` still falsy -- a state no attach can act on,
212 // because `deliver()` and a late `attachOnError` both test `error`, and
213 // `attachThen` tests `value`, so a handler is neither fired nor kept.
214 // It would also hand an attached handler a null `exception_ptr`, which
215 // is UB to `std::rethrow_exception` -- the idiomatic handler body, this
216 // file's own orphan logger included.
217 //
218 // Substituting here rather than at any one producer is deliberate:
219 // `reject(nullptr)` is reachable through the public `Promise<T>` seam,
220 // and many sites forward an `exception_ptr` straight through
221 // (`.onError([state](auto e) { state->setException(e); })`) without
222 // inspecting it.
223 error = exc ? exc
224 : std::make_exception_ptr(
225 std::runtime_error{"completion rejected with no exception (null exception_ptr)"});
226 publish();
227 }
228
229 void attachThen(std::function<void(const T&)> handler) {
230 if (cbExec == nullptr) {
231 return; // Nothing could ever deliver it.
232 }
233 checkOwner("Completion::attach");
234 if (!delivered.load(std::memory_order_relaxed)) {
235 onOk.push_back(std::move(handler));
236 return;
237 }
238 if (!value) {
239 return; // Settled with an error: a `then` has nothing to observe.
240 }
241 // Posted, not run here: a handler attached later must not overtake
242 // one attached earlier, and a handler never runs inside the call that
243 // attached it. The closure reads `value` in place.
244 cbExec->post([self = this->shared_from_this(), handler = std::move(handler)]() {
245 try {
246 handler(*self->value);
247 } catch (...) {
248 ::morph::log::logError("[completion] then handler threw; continuing with next handler");
249 }
250 });
251 }
252
253 void attachOnError(std::function<void(std::exception_ptr)> handler) {
254 if (cbExec == nullptr) {
255 // Nothing could ever deliver it, so the error stays the orphan
256 // logger's rather than being dropped and silenced at once.
257 return;
258 }
259 checkOwner("Completion::attach");
260 onErrAttached.store(true, std::memory_order_relaxed);
261 if (!delivered.load(std::memory_order_relaxed)) {
262 onErr.push_back(std::move(handler));
263 return;
264 }
265 if (!error) {
266 return; // Settled with a value.
267 }
268 cbExec->post([handler = std::move(handler), savedErr = error]() mutable {
269 try {
270 handler(savedErr);
271 } catch (...) {
272 ::morph::log::logError("[completion] onError handler threw; continuing with next handler");
273 }
274 });
275 }
276
277 ~CompletionState() {
278 if (!ready.load(std::memory_order_acquire) || !error || onErrAttached.load(std::memory_order_relaxed)) {
279 return;
280 }
281 // No local guard around the logging calls: `morph::log`'s helpers are
282 // `noexcept` (docs/spec/core/logger.md, "Failure modes"), which is
283 // what makes them safe to call from a destructor at all. The variadic
284 // overload is deliberate — it formats *inside* that guarantee, where
285 // building the message by concatenation out here would allocate
286 // outside it and could still escape.
287 try {
288 std::rethrow_exception(error);
289 } catch (const std::exception& exc) {
290 ::morph::log::logError("[orphan] unhandled exception: {}", exc.what());
291 } catch (...) {
292 ::morph::log::logError("[orphan] unhandled unknown exception");
293 }
294 }
295
296private:
297 /// Publishes the stored outcome and hands its delivery to the owner.
298 void publish() {
299 ready.store(true, std::memory_order_release);
300 if (cbExec != nullptr) {
301 cbExec->post([self = this->shared_from_this()] { self->deliver(); });
302 }
303 }
304
305 /// Runs on `cbExec`: every handler attached so far, in attachment order,
306 /// each isolated so one that throws cannot cost its siblings their turn.
307 void deliver() {
308 auto const here = std::this_thread::get_id();
309 deliveredOn.store(here, std::memory_order_relaxed);
310 delivered.store(true, std::memory_order_release);
311 // An attach outside every task presumed this thread was the owner's;
312 // delivery running elsewhere refutes it -- that attach raced this.
313 if (auto const presumed = presumedOwnerThread.load(std::memory_order_relaxed);
314 presumed != std::thread::id{} && presumed != here) {
315 ::morph::exec::detail::noteOwner("Completion::deliver", cbExec->coreExecutor(), false);
316 }
317 // Released here, on the owner, where they were added. Both lists leave
318 // the state now, not just the one that runs: the state is destroyed
319 // wherever its last reference drops (a timer thread's, say), and the
320 // list that did not run holds captures that must not die there.
321 auto const links = std::move(stopLinks);
322 auto const okHandlers = std::move(onOk);
323 auto const errHandlers = std::move(onErr);
324 if (value) {
325 for (const auto& handler : okHandlers) {
326 try {
327 handler(*value);
328 } catch (...) {
329 ::morph::log::logError("[completion] then handler threw; continuing with next handler");
330 }
331 }
332 return;
333 }
334 for (const auto& handler : errHandlers) {
335 try {
336 handler(error);
337 } catch (...) {
338 ::morph::log::logError("[completion] onError handler threw; continuing with next handler");
339 }
340 }
341 }
342};
343
344// NOLINTEND(cppcoreguidelines-special-member-functions)
345
346/// @brief A place a backend settles one dispatch, without naming its type.
347///
348/// The narrow half of `CompletionState<std::shared_ptr<void>>`: the two calls a
349/// backend actually makes on the completion it produced. Splitting them out is
350/// what lets `Bridge` hand the backend a sink that *is* the caller's typed
351/// completion state, instead of a second, erased completion whose only job is
352/// to be forwarded into the first.
353///
354/// Such forwarding costs six heap allocations per dispatch — the erased state,
355/// the `.then` and `.onError` closures, their two handler vectors, and one of
356/// the two posted settle tasks — against the 14.06 a local round trip takes.
357/// Through a sink it is one.
358///
359/// @par Contract for implementers
360/// - **Settle once.** `settleValue` and `settleException` are mutually
361/// exclusive and each may be called at most once *in effect*; an
362/// implementation must tolerate extra calls and ignore them, because
363/// `IBackend::cancelPending` settles a sink that a reply may be racing to
364/// settle at the same moment. `CompletionState` is already first-result-wins,
365/// but anything an implementation does *besides* forwarding — decrementing a
366/// counter, cancelling a timer — needs its own latch.
367/// - **Do not let an exception escape** where one can be avoided. A sink is
368/// settled from a strand task or a transport thread whose only handler is the
369/// one the backend wrote around the call. The methods are deliberately *not*
370/// `noexcept`: `CompletionState::setValue` is not either (it builds a
371/// `std::function` for the posted callback), and promising more here would
372/// turn a `bad_alloc` into a `std::terminate` that today it is not.
373class ISettleSink {
374public:
375 ISettleSink() = default;
376 ISettleSink(const ISettleSink&) = delete;
377 ISettleSink(ISettleSink&&) = delete;
378 ISettleSink& operator=(const ISettleSink&) = delete;
379 ISettleSink& operator=(ISettleSink&&) = delete;
380 virtual ~ISettleSink() = default;
381
382 /// @brief Settles the dispatch with its opaque result.
383 /// @param value The result, as the backend produced it.
384 virtual void settleValue(std::shared_ptr<void> value) = 0;
385
386 /// @brief Settles the dispatch with a failure.
387 /// @param exc The exception to deliver.
388 virtual void settleException(const std::exception_ptr& exc) = 0;
389};
390
391/// @brief The `ISettleSink` that simply is a `CompletionState<std::shared_ptr<void>>`.
392///
393/// The adapter for every caller that wants the old shape: `IBackend::execute`
394/// returns a `Completion<std::shared_ptr<void>>`, so a backend implementing
395/// `executeInto` can serve `execute` by wrapping its own state in one of these.
396/// Nothing but forwarding happens here, so it needs no settle-once latch of its
397/// own — `CompletionState` already has one.
398class CompletionSettleSink final : public ISettleSink {
399public:
400 /// @brief Binds the sink to @p state.
401 /// @param state The completion state to settle. Must not be null.
402 explicit CompletionSettleSink(std::shared_ptr<CompletionState<std::shared_ptr<void>>> state)
403 : _state{std::move(state)} {}
404
405 /// @brief Forwards to `CompletionState::setValue`.
406 /// @param value The result to store.
407 void settleValue(std::shared_ptr<void> value) override { _state->setValue(std::move(value)); }
408
409 /// @brief Forwards to `CompletionState::setException`.
410 /// @param exc The exception to store.
411 void settleException(const std::exception_ptr& exc) override { _state->setException(exc); }
412
413private:
414 std::shared_ptr<CompletionState<std::shared_ptr<void>>> _state;
415};
416
417} // namespace detail
418
419/// @brief Move-only handle representing the eventual result of an asynchronous operation.
420///
421/// Callbacks are posted to the `IExecutor` supplied at construction time, so
422/// they always run on the intended thread (e.g. the GUI thread).
423///
424/// @par Thread safety
425/// A completion belongs to the executor supplied at construction, its owner.
426/// It may be settled from any thread: the first settle wins, and its delivery
427/// is posted to the owner, where every registered callback runs. `then()` and
428/// `onError()` must be called on the owner -- inside one of its tasks, or on
429/// its own thread outside them (a Qt slot, a test body). Calling them from
430/// another executor's task is a contract violation, asserted in a debug
431/// build. `co_await` may be used from any coroutine: it posts its attach to
432/// the owner when it is not running there.
433///
434/// @par Orphan detection
435/// If a `Completion` is destroyed before an `onError()` handler is attached and
436/// the operation has already failed, the exception is logged as an orphan error.
437///
438/// @par Lifetime and stop gating
439/// Because delivery always goes through an executor, the receiver can be
440/// destroyed — or simply lose interest — before the handler runs. Pass a
441/// `morph::async::CallbackScope` (or one of its `CallbackToken`s) as the first
442/// argument to `then()` / `onError()` and the handler is refused unless the
443/// scope is still alive and un-stopped at delivery time. `thenDetached()` /
444/// `onErrorDetached()` are the same ungated attachments as `then(fn)` /
445/// `onError(fn)`, spelled so a deliberately unmanaged callback says so.
446/// See `callback_scope.hpp` and docs/spec/core/callback_scope.md.
447///
448/// @par Value-handling contract
449/// `T` need only be **move-constructible**; that is the whole type requirement.
450/// Copyability is a *per-handler* obligation, reported where the handler is
451/// written rather than imposed on the whole instantiation:
452/// - a handler taking `const T&` costs **zero** copies;
453/// - a handler taking `T` by value costs **exactly one**, at its own parameter
454/// binding, and requires `T` to be copyable.
455///
456/// The budget does not depend on how many handlers are attached, nor on whether
457/// they attached before or after the completion settled. The value is
458/// **observed, never consumed**: no handler can move out of the stored value, so
459/// a `then()` attached after settling still sees the genuine result. A handler
460/// that wants to consume takes `T` by value and moves out of its own copy.
461/// See docs/spec/core/completion.md, "Value-handling contract".
462///
463/// @tparam T Type of the success value. Must be move-constructible.
464template <typename T>
465// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
467public:
468 /// @brief Constructs an empty (no-op) completion.
469 Completion() = default;
470
471 /// @brief Constructs a completion backed by @p statePtr, delivering callbacks via @p execPtr.
472 /// @param statePtr Shared state produced by the backend.
473 /// @param execPtr The completion's owner: settling posts the delivery there, and
474 /// callbacks are attached and run there. It must be serial
475 /// (`IExecutor::isSerial`): a pool or the inline executor is
476 /// refused, asserted in a debug build. If `nullptr`, callbacks
477 /// are never delivered — attaching one is a no-op.
478 Completion(std::shared_ptr<detail::CompletionState<T>> statePtr, ::morph::exec::IExecutor* execPtr)
479 : _state{std::move(statePtr)} {
480 if (execPtr != nullptr && !execPtr->isSerial()) {
481 // The owner touches the handler list in its tasks; a pool runs two
482 // of them at once, and the inline executor runs one wherever the
483 // settle happened. A debug build stops here, where the owner is
484 // named, rather than at a race nothing can see.
485 ::morph::exec::detail::noteOwner("Completion: an owner that is not serial", execPtr->coreExecutor(),
486 false);
487 }
488 if (_state != nullptr) {
489 _state->cbExec = execPtr;
490 }
491 }
492
493 /// @brief Move constructor — transfers ownership of the underlying state.
494 Completion(Completion&&) noexcept = default;
495 /// @brief Move assignment — transfers ownership of the underlying state.
496 /// @return `*this`.
497 Completion& operator=(Completion&&) noexcept = default;
498 Completion(const Completion&) = delete;
499 Completion& operator=(const Completion&) = delete;
500
501 /// @brief Registers a success callback.
502 ///
503 /// @p handler runs on the executor with the result value when the
504 /// operation completes successfully. If the result has already been
505 /// delivered, the callback is posted on its own. Call on the owner (see
506 /// the class's thread-safety note).
507 ///
508 /// @param handler Callable receiving the result. Take `const T&` to observe it for free;
509 /// take `T` by value to get your own copy, at the cost of exactly one copy.
510 /// @return `*this` for chaining — a reference into this `Completion`, valid
511 /// only for as long as it is.
512 Completion& then(std::function<void(const T&)> handler) MORPH_LIFETIMEBOUND {
513 if (_state != nullptr) {
514 _state->attachThen(std::move(handler));
515 }
516 return *this;
517 }
518
519 /// @brief Registers an error callback.
520 ///
521 /// @p handler runs on the executor with the `std::exception_ptr` when
522 /// the operation fails. If the failure has already been delivered, the
523 /// callback is posted on its own. Attaching this handler suppresses orphan
524 /// logging. Call on the owner (see the class's thread-safety note).
525 ///
526 /// @param handler Callable receiving the exception pointer.
527 /// @return `*this` for chaining — a reference into this `Completion`, valid
528 /// only for as long as it is.
529 Completion& onError(std::function<void(std::exception_ptr)> handler) MORPH_LIFETIMEBOUND {
530 if (_state != nullptr) {
531 _state->attachOnError(std::move(handler));
532 }
533 return *this;
534 }
535
536 /// @brief Registers a success callback gated on @p scope's lifetime and stop state.
537 ///
538 /// Identical to `then(handler)` except that @p handler is wrapped in a
539 /// `CallbackToken` gate at attach time: when the result is delivered, the
540 /// handler runs only if @p scope is still alive and has not been stopped or
541 /// `reset()`. Nothing else changes — the gate lives inside the stored
542 /// handler, so fan-out, the attach-after-ready fire-now path and executor
543 /// marshalling all behave exactly as they do for the ungated form.
544 ///
545 /// Attaching also makes the call this completion reports on one the scope
546 /// owns: when the call carries a stop source (a `Bridge` call whose handler
547 /// returns a `Task`), stopping, resetting or destroying the scope before it
548 /// settles requests stop on it.
549 ///
550 /// The scope is observed weakly; attaching does **not** extend its lifetime.
551 ///
552 /// @param scope Receiver-owned gate. Only a token for its *current*
553 /// generation is captured, so a later `reset()` retires this
554 /// attachment.
555 /// @param handler Callable receiving the result. Take `const T&` to observe it for free;
556 /// take `T` by value to get your own copy, at the cost of exactly one copy.
557 /// @return `*this` for chaining — a reference into this `Completion`, valid
558 /// only for as long as it is.
559 Completion& then(const CallbackScope& scope, std::function<void(const T&)> handler) MORPH_LIFETIMEBOUND {
560 return then(scope.token(), std::move(handler));
561 }
562
563 /// @brief Registers a success callback gated on an already-issued @p token.
564 ///
565 /// The token-taking form of `then(const CallbackScope&, handler)`, for
566 /// callers that hold a token rather than the scope itself (a helper that
567 /// was handed one, or code that captured a token before dispatching).
568 ///
569 /// @param token Gate observing some receiver's `CallbackScope`. A
570 /// default-constructed token suppresses unconditionally.
571 /// @param handler Callable receiving the result. Take `const T&` to observe it for free;
572 /// take `T` by value to get your own copy, at the cost of exactly one copy.
573 /// @return `*this` for chaining — a reference into this `Completion`, valid
574 /// only for as long as it is.
575 Completion& then(CallbackToken token, std::function<void(const T&)> handler) MORPH_LIFETIMEBOUND {
576 if (_state != nullptr) {
577 _state->linkStop(token.stopToken());
578 }
579 return then(std::function<void(const T&)>{token.guard(std::move(handler))});
580 }
581
582 /// @brief Registers an error callback gated on @p scope's lifetime and stop state.
583 ///
584 /// Identical to `onError(handler)` except that @p handler is wrapped in a
585 /// `CallbackToken` gate at attach time.
586 ///
587 /// Orphan logging is unaffected: attaching a gated handler suppresses it
588 /// exactly as the ungated form does, and an error whose delivery the scope
589 /// then refuses still counts as **handled**. Suppression is a deliberate act
590 /// by the receiver, not a dropped error nobody asked about.
591 ///
592 /// @param scope Receiver-owned gate; observed weakly.
593 /// @param handler Callable receiving the exception pointer.
594 /// @return `*this` for chaining — a reference into this `Completion`, valid
595 /// only for as long as it is.
597 std::function<void(std::exception_ptr)> handler) MORPH_LIFETIMEBOUND {
598 return onError(scope.token(), std::move(handler));
599 }
600
601 /// @brief Registers an error callback gated on an already-issued @p token.
602 ///
603 /// The token-taking form of `onError(const CallbackScope&, handler)`.
604 ///
605 /// @param token Gate observing some receiver's `CallbackScope`. A
606 /// default-constructed token suppresses unconditionally.
607 /// @param handler Callable receiving the exception pointer.
608 /// @return `*this` for chaining — a reference into this `Completion`, valid
609 /// only for as long as it is.
610 Completion& onError(CallbackToken token, std::function<void(std::exception_ptr)> handler) MORPH_LIFETIMEBOUND {
611 if (_state != nullptr) {
612 _state->linkStop(token.stopToken());
613 }
614 return onError(std::function<void(std::exception_ptr)>{token.guard(std::move(handler))});
615 }
616
617 /// @brief Registers a success callback whose lifetime is deliberately unmanaged.
618 ///
619 /// Exactly `then(handler)`, spelled so that "no scope gates this" is a
620 /// statement the author made on purpose and a reviewer can grep for. Use it
621 /// when the handler genuinely owns everything it touches — it captures only
622 /// values, or a `shared_ptr` it keeps alive itself.
623 ///
624 /// @param handler Callable receiving the result. Take `const T&` to observe it for free;
625 /// take `T` by value to get your own copy, at the cost of exactly one copy.
626 /// @return `*this` for chaining — a reference into this `Completion`, valid
627 /// only for as long as it is.
628 Completion& thenDetached(std::function<void(const T&)> handler) MORPH_LIFETIMEBOUND {
629 return then(std::move(handler));
630 }
631
632 /// @brief Registers an error callback whose lifetime is deliberately unmanaged.
633 ///
634 /// The `onError` counterpart of `thenDetached()`.
635 ///
636 /// @param handler Callable receiving the exception pointer.
637 /// @return `*this` for chaining — a reference into this `Completion`, valid
638 /// only for as long as it is.
639 Completion& onErrorDetached(std::function<void(std::exception_ptr)> handler) MORPH_LIFETIMEBOUND {
640 return onError(std::move(handler));
641 }
642
643 /// @brief Awaits this completion from a coroutine, consuming it.
644 ///
645 /// `co_await std::move(completion)` yields a copy of the settled value or
646 /// rethrows the stored exception. The coroutine resumes in the resumption
647 /// context it suspended in -- a `spawn`ed task's executor, a Task handler's
648 /// strand -- or, with none, on this completion's executor, where `then()`
649 /// handlers run. A stop requested on the awaiting coroutine's token
650 /// withdraws the await and resumes it with `core::async::OperationCancelled`.
651 /// Rvalue only: awaiting moves the state out, so an lvalue `co_await` would
652 /// hide that the completion is empty afterwards. See
653 /// `docs/spec/core/coroutines.md`.
654 /// @return The awaiter; not for direct use.
655 [[nodiscard]] detail::CompletionAwaiter<T> operator co_await() && {
656 static_assert(std::copy_constructible<T>,
657 "co_await on a morph::async::Completion<T> copies the settled value out of the shared state, "
658 "which other handlers may still read, so T must be copy-constructible.");
659 return detail::CompletionAwaiter<T>{std::move(_state)};
660 }
661
662 /// @brief Returns the underlying shared state (for advanced / internal use).
663 /// @return Shared pointer to the completion state, or `nullptr` for empty completions.
664 [[nodiscard]] std::shared_ptr<detail::CompletionState<T>> state() const { return _state; }
665
666 /// @brief Producer-side handle paired with a `Completion<T>` by `makeSettleable()`.
667 ///
668 /// `Promise<T>` is the public settling counterpart to `Completion<T>`: it
669 /// exposes exactly `resolve()`/`reject()` against the same shared state a
670 /// paired `Completion<T>` observes via `then()`/`onError()`, without ever
671 /// naming `morph::async::detail::CompletionState<T>`. Intended for test code
672 /// that needs to construct a `Completion<T>` it can settle on demand — e.g.
673 /// standing in for a `Bridge`/`IBackend` round trip — instead of reaching
674 /// into `detail::CompletionState<T>` directly (see docs/spec/core/completion.md,
675 /// "Settleable promise seam").
676 ///
677 /// Move-only, mirroring `Completion<T>`: exactly one producer settles a
678 /// given operation.
679 class Promise {
680 public:
681 /// @brief Move constructor — transfers ownership of the shared state.
682 Promise(Promise&&) noexcept = default;
683 /// @brief Move assignment — transfers ownership of the shared state.
684 /// @return `*this`.
685 Promise& operator=(Promise&&) noexcept = default;
686 Promise(const Promise&) = delete;
687 Promise& operator=(const Promise&) = delete;
688 ~Promise() = default;
689
690 /// @brief Resolves the paired `Completion<T>` with @p val.
691 ///
692 /// No-op if the state is already settled (first result wins — see
693 /// `detail::CompletionState<T>::setValue`), or if this `Promise` was
694 /// moved from (mirrors `Completion<T>::then()`'s null-state no-op).
695 /// Safe to call from any thread; the delivery is posted to the
696 /// completion's executor.
697 /// @param val Success value delivered to every attached `then()` handler.
698 void resolve(T val) {
699 if (_state != nullptr) {
700 _state->setValue(std::move(val));
701 }
702 }
703
704 /// @brief Rejects the paired `Completion<T>` with @p exc.
705 ///
706 /// No-op if the state is already settled (first result wins — see
707 /// `detail::CompletionState<T>::setException`), or if this `Promise` was
708 /// moved from (mirrors `Completion<T>::onError()`'s null-state no-op).
709 /// Safe to call from any thread; the delivery is posted to the
710 /// completion's executor.
711 /// @param exc Error delivered to every attached `onError()` handler, or
712 /// logged as an orphan if none is ever attached.
713 void reject(std::exception_ptr exc) {
714 if (_state != nullptr) {
715 _state->setException(exc);
716 }
717 }
718
719 private:
720 friend class Completion<T>;
721 explicit Promise(std::shared_ptr<detail::CompletionState<T>> state) : _state{std::move(state)} {}
722
723 std::shared_ptr<detail::CompletionState<T>> _state;
724 };
725
726 /// @brief Constructs a `Completion<T>`/`Promise<T>` pair sharing one settleable state.
727 ///
728 /// The public "settleable promise" seam: lets a caller — typically
729 /// test code — construct a `Completion<T>` it can resolve or reject on demand,
730 /// without a full `Bridge`/`IBackend` round trip and without reaching into
731 /// `morph::async::detail::CompletionState<T>`. Everything `Completion(state,
732 /// executor)` already provided by hand is available through this factory
733 /// instead: the returned `Completion<T>` is exactly what `then()`/`onError()`
734 /// observe; the returned `Promise` is exactly what settles it.
735 /// @param execPtr Executor callbacks are posted on; `nullptr` for a
736 /// write-only completion (see the two-argument constructor).
737 /// @return A `{Completion<T>, Promise}` pair sharing one `CompletionState<T>`.
738 [[nodiscard]] static std::pair<Completion<T>, Promise> makeSettleable(::morph::exec::IExecutor* execPtr) {
739 auto state = std::make_shared<detail::CompletionState<T>>();
740 Completion<T> completion{state, execPtr};
741 Promise promise{state};
742 return {std::move(completion), std::move(promise)};
743 }
744
745private:
746 std::shared_ptr<detail::CompletionState<T>> _state;
747};
748
749} // namespace morph::async
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
CallbackToken token() const noexcept
Issues a weak token for the current generation.
Weak, copyable observer of a CallbackScope — the half a callback captures.
auto guard(F &&fn) const
Wraps fn so it runs only while this token is Active.
::core::async::StopToken stopToken() const noexcept
The stop token of the issuing scope's generation: stop is requested on it when the scope is stopped,...
Producer-side handle paired with a Completion<T> by makeSettleable().
void reject(std::exception_ptr exc)
Rejects the paired Completion<T> with exc.
Promise(Promise &&) noexcept=default
Move constructor — transfers ownership of the shared state.
void resolve(T val)
Resolves the paired Completion<T> with val.
Completion & then(CallbackToken token, std::function< void(const T &)> handler)
Registers a success callback gated on an already-issued token.
Completion & then(std::function< void(const T &)> handler)
Registers a success callback.
Completion & thenDetached(std::function< void(const T &)> handler)
Registers a success callback whose lifetime is deliberately unmanaged.
Completion & onError(std::function< void(std::exception_ptr)> handler)
Registers an error callback.
std::shared_ptr< detail::CompletionState< T > > state() const
Returns the underlying shared state (for advanced / internal use).
Completion & then(const CallbackScope &scope, std::function< void(const T &)> handler)
Registers a success callback gated on scope's lifetime and stop state.
Completion & onError(const CallbackScope &scope, std::function< void(std::exception_ptr)> handler)
Registers an error callback gated on scope's lifetime and stop state.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
Completion()=default
Constructs an empty (no-op) completion.
Completion(std::shared_ptr< detail::CompletionState< T > > statePtr, ::morph::exec::IExecutor *execPtr)
Constructs a completion backed by statePtr, delivering callbacks via execPtr.
Completion(Completion &&) noexcept=default
Move constructor — transfers ownership of the underlying state.
Completion & onErrorDetached(std::function< void(std::exception_ptr)> handler)
Registers an error callback whose lifetime is deliberately unmanaged.
Completion & onError(CallbackToken token, std::function< void(std::exception_ptr)> handler)
Registers an error callback gated on an already-issued token.
The awaiter behind Completion<T>::operator co_await.
The check every loop-owned body makes on entry: "am I on my owner?".
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