morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
morph::async::CallbackScope Class Reference

Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class. More...

#include <callback_scope.hpp>

Public Member Functions

 CallbackScope ()
 Constructs a live, un-stopped scope.
 ~CallbackScope ()
 Refuses every outstanding token, then releases the state.
 CallbackScope (const CallbackScope &)=delete
CallbackScope & operator= (const CallbackScope &)=delete
 CallbackScope (CallbackScope &&)=delete
CallbackScope & operator= (CallbackScope &&)=delete
void requestStop () const noexcept
 Marks this generation stopped: gated callbacks stop being delivered, and every call the scope owns is asked to stop.
void reset ()
 Retires every token issued so far and starts a fresh, live generation.
bool stopRequested () const noexcept
 Whether this generation has been stopped.
CallbackToken token () const noexcept
 Issues a weak token for the current generation.
template<typename F>
auto guard (F &&fn) const
 Wraps fn so it runs only while this scope is alive and un-stopped.

Detailed Description

Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.

A receiver that attaches callbacks to a Completion<T>, a BridgeHandler subscription, a timer tick or any other posted closure holds one of these as an ordinary member and hands out CallbackTokens. A gated callback runs only while the scope is both alive and not stopped.

Composition is the point. Requiring every consumer to derive from a framework base class constrains hierarchies that are already QObjects, already have a base, or are aggregates — for what is an implementation detail of one screen. A member composes with all of them, and code that never opts in is unaffected.

class BoardPresenter {
void load() {
_handler.execute(GetBoard{}).then(_callbacks, [this](GetBoardResult r) { render(r); });
}
void onUserNavigatedAway() { _callbacks.requestStop(); } // alive, but no longer interested
void onNewQuery() { _callbacks.reset(); } // supersede: old replies dead, new ones live
morph::async::CallbackScope _callbacks; // declared LAST — see below
};
Three verbs, two states
  • requestStop() — "I no longer care". The owner is still here; pending and future callbacks on this generation are refused. Idempotent.
  • reset() — "supersede". Every token issued so far goes permanently dead and the scope becomes deliverable again under a fresh generation, so "a new query cancels the old one" needs no heap-reallocated scope.
  • destruction — the same refusal as requestStop(), plus the owner is gone.

Destruction and requestStop() are indistinguishable to a callback; they differ only in whether the owner still exists, which CallbackToken::status() reports as Expired versus Stopped.

Declared last, destroyed first
Put the member after (below) everything its callbacks touch. Members are destroyed in reverse declaration order, so a last-declared scope is the first thing to die and every gated callback is already refused before the fields it would have touched are torn down. This replaces the per-class "token must stay last-declared" convention that used to be re-derived and re-documented in every class that hand-rolled the pattern.
Teardown that pumps
Members are destroyed after the destructor body runs. A destructor body that can pump a nested event loop (a blocking call that spins a QEventLoop) can therefore still deliver into a half-destroyed receiver. Such a destructor must call requestStop() as its first statement.
Boundary of the guarantee
  • Executor-affine use gets the full guarantee. When the scope is destroyed / stopped / reset on the same thread the delivery executor runs on — the normal case, where receiver and callbacks both live on the GUI thread — check-then-run is atomic with respect to those operations, and a gated callback never touches a dead or stopped receiver.
  • Cross-thread stop is advisory. A scope torn down on a different thread from the delivery executor can turn inactive between the token's check and the callback body. That is exactly the boundary of the hand-rolled weak_ptr idiom this type replaces — locking the token pins the token, never the receiver — and external synchronisation there remains the caller's job.
  • Deliberately no block-until-drained. Neither requestStop() nor the destructor waits for an in-flight callback to finish, QObject::disconnect style. A GUI-thread destructor blocking on a pool-thread callback that is itself blocked posting back to the GUI executor is a deadlock by construction.
Thread safety
token(), guard(), requestStop(), stopRequested() and the destructor are safe to call concurrently from any thread. stopRequested() is a lock-free atomic load; requestStop() stores the same atomic and then requests stop on the generation's core::async::StopSource, which runs the stop of every call the scope owns on the calling thread.

reset() is the exception and must be externally synchronised against the others — call it from the thread that owns the scope. It replaces the _state handle itself, and reading a shared_ptr while another thread assigns it is a data race on the handle: the control block's refcount is atomic, the pointer object is not. That narrowing is a portability constraint, not taste — see _state's own comment. In the usage reset() exists for (void onNewQuery() { _callbacks.reset(); }, the supersede verb) the owning thread is the caller anyway. Within a single generation the guarantee is unconditional: reset() stops the outgoing generation before releasing it, so a token holder that pinned it still observes refusal.

Identity, not a value: neither copyable nor movable. A moved-from scope would have to either strand or silently retarget tokens already captured in flight; reset() covers the one case (regeneration) that motivates a move at all.

Definition at line 235 of file callback_scope.hpp.

Constructor & Destructor Documentation

◆ CallbackScope()

morph::async::CallbackScope::CallbackScope ( )
inline

Constructs a live, un-stopped scope.

Definition at line 238 of file callback_scope.hpp.

◆ ~CallbackScope()

morph::async::CallbackScope::~CallbackScope ( )
inline

Refuses every outstanding token, then releases the state.

Equivalent, from a pending callback's point of view, to requestStop(). Does not wait for in-flight callbacks — see "Boundary of the guarantee".

Definition at line 244 of file callback_scope.hpp.

Member Function Documentation

◆ requestStop()

void morph::async::CallbackScope::requestStop ( ) const
inlinenoexcept

Marks this generation stopped: gated callbacks stop being delivered, and every call the scope owns is asked to stop.

A call is owned by the scope once a Completion carrying a stop source (a Bridge call whose handler returns a Task) has a callback attached through one of its tokens; its stop is requested here, on this thread, before this returns. Idempotent and safe from any thread. The owner remains alive, so tokens report CallbackStatus::Stopped rather than Expired. Undone only by reset(), which starts a new generation.

Definition at line 260 of file callback_scope.hpp.

◆ reset()

void morph::async::CallbackScope::reset ( )
inline

Retires every token issued so far and starts a fresh, live generation.

The supersede verb: an old in-flight reply can no longer be delivered (its token reports Expired), while callbacks attached after this call are deliverable again even if requestStop() had been called before it. The outgoing generation is stopped before it is released, so a token holder that pinned it while racing this call still observes refusal.

Definition at line 275 of file callback_scope.hpp.

◆ stopRequested()

bool morph::async::CallbackScope::stopRequested ( ) const
inlinenodiscardnoexcept

Whether this generation has been stopped.

Returns
true after requestStop(), until the next reset().

Definition at line 283 of file callback_scope.hpp.

◆ token()

CallbackToken morph::async::CallbackScope::token ( ) const
inlinenodiscardnoexcept

Issues a weak token for the current generation.

Returns
A CallbackToken observing this scope; keeps nothing alive.

Definition at line 287 of file callback_scope.hpp.

◆ guard()

template<typename F>
auto morph::async::CallbackScope::guard ( F && fn) const
inlinenodiscard

Wraps fn so it runs only while this scope is alive and un-stopped.

Forwards to CallbackToken::guard() on a token for the current generation, so a later reset() retires the wrapper. This is the general-purpose form for the callbacks that are not Completion attachments — timer ticks, IExecutor::post closures, poller dispatch, event sinks.

Template Parameters
FCallable type to wrap; must return void (see CallbackToken::guard()).
Parameters
fnCallable to gate. Moved into the returned wrapper.
Returns
A callable with fn's argument list and a void return.

Definition at line 301 of file callback_scope.hpp.


The documentation for this class was generated from the following file: