|
morph 0.1.0
Asynchronous bridge between GUI thread and business models
|
Central dispatcher that routes typed actions to an IBackend. More...
#include <bridge.hpp>
Public Member Functions | |
| Bridge (std::unique_ptr<::morph::backend::detail::IBackend > backend, ::morph::exec::IExecutor &owner) | |
Constructs a bridge that dispatches through backend, owned by owner. | |
| ~Bridge () | |
| Clears the backend's reconnect handler, rejects every call still waiting for a bind with BridgeDestroyedError, and cancels the backend's pending calls with the same error. | |
| Bridge (const Bridge &)=delete | |
| Bridge & | operator= (const Bridge &)=delete |
| Bridge (Bridge &&)=delete | |
| Bridge & | operator= (Bridge &&)=delete |
| ::morph::exec::IExecutor & | owner () const noexcept |
| The executor this bridge belongs to. | |
| bool | onOwner () const noexcept |
| Whether the calling thread is on this bridge's owner. | |
| template<typename Model> | |
| std::shared_ptr< detail::HandlerBinding > | registerHandler () |
| Creates and registers a new HandlerBinding for Model. | |
| void | registerHandler (const std::shared_ptr< detail::HandlerBinding > &binding) |
Tracks binding and issues its bind on the active backend. | |
| template<typename Model> | |
| std::shared_ptr< detail::HandlerBinding > | registerSharedHandler () |
| Creates a shared, initially unattached binding for Model. | |
| void | attach (const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary) |
Attaches (or re-points) a shared binding to the instance for primary, creating it if no live instance holds the key. | |
| template<typename Model> | |
| void | attachHandler (const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary) |
| attach for a typed caller. | |
| template<typename Model> | |
| void | assignHandlerPrimary (const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary) |
Files binding's current instance under primary, in place. | |
| std::string | bindingPrimary (const std::shared_ptr< detail::HandlerBinding > &binding) const |
Returns binding's current primary key, or empty if unattached. | |
| template<typename Model> | |
| ::morph::async::Completion< std::vector< std::string > > | instancesOf (::morph::exec::IExecutor &cbExec) |
| Lists the live shared primary keys of Model on the active backend. | |
| void | addSubscription (const std::shared_ptr< detail::HandlerBinding > &binding, std::type_index type, std::function< void(const std::any &)> sink, ::morph::exec::IExecutor *exec) |
Registers a result-type subscription for binding. | |
| void | removeSubscription (const std::shared_ptr< detail::HandlerBinding > &binding, std::type_index type) |
Removes binding's subscription for type, if any. | |
| bool | hasSubscribers () const noexcept |
| Whether any subscription is currently registered on this bridge. | |
| void | publishResult (::morph::exec::detail::ModelId mid, std::type_index type, const std::any &value) |
Delivers value to every subscriber attached to instance mid. | |
| void | setDefaultSession (::morph::session::Context session) |
| Installs a default session context that executeVia stamps onto the ActionCall of every subsequent call. | |
| void | setExecuteDeadline (std::chrono::milliseconds deadline) |
| Sets (or disables) the client-side execute deadline. | |
| std::chrono::milliseconds | executeDeadline () const |
| Returns the currently installed client-side execute deadline. | |
| ::morph::session::Context | defaultSession () const |
| Returns a copy of the currently installed default session. | |
| void | setPrincipal (::morph::session::Principal principal) |
| Installs the verified Principal for this Bridge. | |
| ::morph::session::Principal | currentPrincipal () const |
| Returns a copy of the currently installed Principal. | |
| std::size_t | pendingCalls () const noexcept |
| Returns the number of calls made through the bridge — typed (executeVia() and its keyed variants) or raw (executeRaw(), executeRawOn()) — that have not yet resolved. | |
| template<typename Backend> requires std::derived_from<Backend, ::morph::backend::detail::IBackend> | |
| void | switchBackend (std::unique_ptr< Backend > newBackend) |
Replaces the active backend with newBackend. | |
| void | switchBackend (std::shared_ptr<::morph::backend::detail::IBackend > newBackend) |
Replaces the active backend with newBackend. | |
| void | deregisterHandler (const std::shared_ptr< detail::HandlerBinding > &binding) |
Deregisters binding from the active backend and removes it from tracking. | |
| template<typename Model, typename Action> | |
| ::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > | executeVia (const std::shared_ptr< detail::HandlerBinding > &binding, Action action, ::morph::exec::IExecutor *cbExec, std::function< void(const typename ::morph::model::ActionTraits< Action >::Result &)> onResult={}) |
Dispatches action against the model identified by binding. | |
| template<typename Model, typename Action> | |
| ::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > | executeAttachedVia (const std::shared_ptr< detail::HandlerBinding > &binding, Action action, ::morph::exec::IExecutor *cbExec, std::string key) |
Dispatches a payload-keyed action on a shared binding, attaching it to the instance key names first. | |
| template<typename Model, typename Action> | |
| ::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > | executeCreatingVia (const std::shared_ptr< detail::HandlerBinding > &binding, Action action, ::morph::exec::IExecutor *cbExec) |
Dispatches a result-keyed action on a shared binding, giving it an anonymous instance first if it has none, and promoting that instance under the key its result carries. | |
| std::shared_ptr< detail::HandlerBinding > | bindByType (std::string typeId, BindSharing sharing, std::string instanceKey={}) |
Binds the model registered as typeId, for a caller compiled without the model's type. | |
| ::morph::async::Completion< std::string > | executeRaw (const std::shared_ptr< detail::HandlerBinding > &binding, std::string actionId, std::string bodyJson, ::morph::exec::IExecutor *cbExec) |
Dispatches the action registered as actionId with a JSON body through binding, and resolves with the JSON reply. | |
| ::morph::async::Completion< std::string > | executeRaw (const std::shared_ptr< detail::HandlerBinding > &binding, std::string actionId, std::string bodyJson, ::morph::exec::IExecutor *cbExec, ::core::async::StopToken stop) |
executeRaw, cancellable through stop. | |
| ::morph::async::Completion< std::string > | executeRawOn (const std::shared_ptr< detail::HandlerBinding > &binding, std::string instanceKey, std::string actionId, std::string bodyJson, ::morph::exec::IExecutor *cbExec) |
executeRaw on the instance for instanceKey: attaches the shared binding to it first when it holds another or none. | |
Static Public Member Functions | |
| static bool | isBound (const std::shared_ptr< detail::HandlerBinding > &binding) noexcept |
Whether binding currently has a live ModelId. | |
Central dispatcher that routes typed actions to an IBackend.
Bridge owns exactly one active backend at a time. It tracks all registered HandlerBinding instances and re-registers them when switchBackend() is called or the backend reconnects, enabling seamless local ↔ remote transitions.
Definition at line 747 of file bridge.hpp.
|
inline |
Constructs a bridge that dispatches through backend, owned by owner.
Tells the backend its owner, installs a reconnect handler (posted to owner) so a backend with a recoverable transport re-registers every live handler on the owner after a reconnect, and pushes the (initially empty) default session to the backend.
A bridge always has a backend: every handler binds on construction, so a bridge without one could not serve its first handler.
| backend | Initial backend. Ownership is transferred. Must not be null. |
| owner | The executor this bridge, its handlers and its backend belong to. Construct the bridge there: inside one of its tasks, or on the thread that runs them. Borrowed: it must outlive this bridge and every reply still in flight when the bridge is destroyed. |
| std::invalid_argument | if backend is null. |
Definition at line 767 of file bridge.hpp.
|
inline |
Clears the backend's reconnect handler, rejects every call still waiting for a bind with BridgeDestroyedError, and cancels the backend's pending calls with the same error.
On the owner. A reply or reconnect still queued there finds the bridge's token expired and touches nothing; a bind reply for a binding the bridge no longer tracks releases its instance on the backend that issued it.
Definition at line 786 of file bridge.hpp.
|
inlinenodiscardnoexcept |
The executor this bridge belongs to.
Definition at line 811 of file bridge.hpp.
|
inlinenodiscardnoexcept |
Whether the calling thread is on this bridge's owner.
Definition at line 816 of file bridge.hpp.
|
inline |
Creates and registers a new HandlerBinding for Model.
Uses the default ModelFactory::create<Model>() factory. The bind is issued and this returns; see registerHandler(binding).
| Model | Concrete model type. Must have a registered ModelTraits specialisation. |
Definition at line 825 of file bridge.hpp.
|
inline |
Tracks binding and issues its bind on the active backend.
Never waits. A backend that settles the bind before returning has bound binding by the time this returns; otherwise a call made through it is held until the bind settles. A failed bind is logged and rejects the calls held for it with its error.
| binding | Pre-constructed binding. Its typeId, modelFactory and contextKey must be set. |
Definition at line 839 of file bridge.hpp.
|
inline |
Creates a shared, initially unattached binding for Model.
Registers nothing on the backend: a shared handler has no instance until a keyed action or an explicit attach names one. The binding is tracked from the start so switchBackend() knows about it.
| Model | Concrete model type. Must have a registered ModelTraits. |
Definition at line 861 of file bridge.hpp.
|
inline |
Attaches (or re-points) a shared binding to the instance for primary, creating it if no live instance holds the key.
Idempotent: attaching to the primary a binding already holds is a no-op. A different primary re-points the binding — the previous instance is released and survives only if another handler still holds it. The binding's contextKey is set to primary as well, so a keyed model's journal entries carry the entity key.
Issued after any bind already in flight for binding, and never waits: a call made through the binding meanwhile is held until it settles. A refused attach is logged and leaves the binding on the instance it held; a call held behind it is rejected with its error only when the binding has no instance to fall back to.
Needs no model type, so a binding from bindByType attaches through it as a typed one does. The key is matched exactly, so it must be the canonical encoding keyToString gives the model's key type: another spelling of the same key names a different instance.
| binding | Shared binding, from registerSharedHandler<Model>() or bindByType. |
| primary | Canonical string encoding of the primary key to attach to. |
| std::invalid_argument | for an empty primary: the bridge reads an empty primary as "never attached". |
| std::logic_error | for a private binding, whose instance is re-created private by every re-bind. |
Definition at line 895 of file bridge.hpp.
|
inline |
attach for a typed caller.
Skips attach's private-binding check, since a typed handler's sharing is fixed by its type, and refuses an empty primary without throwing: in its turn after any bind in flight, the refusal is logged and leaves the binding on the instance it held. A binding with no instance records the refusal as its bind failure, so a call made through it is rejected with that error.
| Model | Concrete model type. |
| binding | Shared binding, as returned by registerSharedHandler<Model>(). |
| primary | Canonical string encoding of the primary key to attach to. |
Definition at line 917 of file bridge.hpp.
|
inline |
Files binding's current instance under primary, in place.
The instance keeps everything the creating action just did — nothing is re-created and nothing is stranded. A no-op if binding is unbound or already holds a real primary (the backend refuses to re-key a keyed instance — see IBackend::assignPrimary).
Reaches the backend through IBackend::promoteModel, delivered on the owner. A backend that settles before returning has promoted the binding by the time this returns, which a result-keyed execute relies on: its .then then already sees the new primary. A failure is logged.
Known gap: if the target key is already held by a different instance, the backend declines to promote (the existing holder always wins) but reports success, so the binding caches a primary the backend never filed it under. Closing it needs assignPrimary's outcome to become observable across every backend.
| Model | Concrete model type. |
| binding | Shared binding whose instance is being promoted. |
| primary | Canonical string encoding of the key to file it under. |
Definition at line 992 of file bridge.hpp.
|
inlinenodiscard |
Returns binding's current primary key, or empty if unattached.
| binding | Binding to inspect. |
Definition at line 1026 of file bridge.hpp.
|
inlinestaticnodiscardnoexcept |
Whether binding currently has a live ModelId.
A point-in-time atomic read, safe from any thread. Nothing needs to wait on it: a call made through an unbound handler whose bind is in flight is held until the bind settles.
| binding | Binding to inspect. |
Definition at line 1038 of file bridge.hpp.
|
inlinenodiscard |
Lists the live shared primary keys of Model on the active backend.
| Model | Concrete model type. |
| cbExec | Executor the answer is delivered on. Borrowed: it must outlive the returned Completion. |
Definition at line 1049 of file bridge.hpp.
|
inline |
Registers a result-type subscription for binding.
The subscription is stored against the binding, not against a fixed instance id, and is matched at publish time by comparing the binding's current instance, so re-pointing a handler moves its subscriptions with it. See detail::SubscriptionRegistry.
| binding | Handler binding that owns the subscription. |
| type | Result type being subscribed to. |
| sink | Type-erased delivery callback; receives the boxed result. |
| exec | Executor the callback is delivered on. |
Definition at line 1065 of file bridge.hpp.
|
inline |
Removes binding's subscription for type, if any.
| binding | Handler binding that owns the subscription. |
| type | Result type to stop hearing about. |
Definition at line 1074 of file bridge.hpp.
|
inlinenodiscardnoexcept |
Whether any subscription is currently registered on this bridge.
A single relaxed atomic load, safe from any thread: a result with no subscriber is never boxed and never sent to the owner.
Definition at line 1084 of file bridge.hpp.
|
inline |
Delivers value to every subscriber attached to instance mid.
Called, on the owner, for every successful action result that has subscribers. Subscribers are matched on the instance the result was produced on, so a handler hears about work another handler — or, with a shared instance, another screen entirely — did on the model it is attached to. The producing handler is notified too.
| mid | Instance the result was produced on. |
| type | Result type produced. |
| value | Boxed result. |
Definition at line 1097 of file bridge.hpp.
|
inline |
Installs a default session context that executeVia stamps onto the ActionCall of every subsequent call.
There is no per-call session override — this default is applied to all calls until replaced or cleared. Also pushed to the active backend, so every control envelope it builds afterwards carries it too.
| session | The new default. Pass {} to clear. |
Definition at line 1110 of file bridge.hpp.
|
inline |
Sets (or disables) the client-side execute deadline.
Every executeVia() call after this point races the real reply against deadline; whichever settles first wins (CompletionState::setValue/ setException are idempotent — see completion.hpp). If deadline elapses first, the pending Completion fails with morph::backend::ClientTimeoutError; the real reply, if it arrives later, is silently discarded exactly like any other late write to an already-resolved CompletionState.
The clock starts inside executeVia(), when the call is made — before a wait for its bind, if it has to wait — so deadline covers the whole round trip.
Disabled (std::chrono::milliseconds{0}, the default): a dropped frame or a hung server leaves the Completion pending forever.
The backing TimeoutScheduler (and the exec::IoLoop it owns, with that loop's one thread) is created lazily on the first call that enables a deadline, so a Bridge that never opts in spawns no extra thread. Once created it lives until ~Bridge().
| deadline | Maximum time to wait for any reply. 0 disables the deadline. |
Definition at line 1140 of file bridge.hpp.
|
inlinenodiscard |
Returns the currently installed client-side execute deadline.
Definition at line 1150 of file bridge.hpp.
|
inlinenodiscard |
Returns a copy of the currently installed default session.
Definition at line 1157 of file bridge.hpp.
|
inline |
Installs the verified Principal for this Bridge.
Typically called once right after a successful login dispatch, from data the server actually returned (see session::Principal's doc comment on the trust model). Distinct from setDefaultSession: that installs the per-call Context forwarded with every dispatch; this installs the longer-lived identity UI code reads outside a dispatch via currentPrincipal() to shape itself.
| principal | Verified identity to install. Pass a default-constructed Principal{} (or call this from a sign-out handler) to clear it. |
Definition at line 1173 of file bridge.hpp.
|
inlinenodiscard |
Returns a copy of the currently installed Principal.
Default-constructed (empty id, no roles) if setPrincipal was never called or the application signed out by clearing it.
Definition at line 1183 of file bridge.hpp.
|
inlinenodiscardnoexcept |
Returns the number of calls made through the bridge — typed (executeVia() and its keyed variants) or raw (executeRaw(), executeRawOn()) — that have not yet resolved.
Incremented once per call, when it is made (a call held for its bind included); decremented exactly once when the returned Completion settles on success or on error (including a cancellation error from switchBackend()/~Bridge()/a dropped transport). A client can poll this to build a "still loading" indicator or gate a feature on quiescence. A single relaxed atomic load, safe from any thread.
Definition at line 1200 of file bridge.hpp.
|
inline |
Replaces the active backend with newBackend.
Backend (rather than taking unique_ptr<IBackend> directly) so it is an exact match for a unique_ptr<Concrete> argument (e.g. std::make_unique<LocalBackend>(...)) and is preferred over the shared_ptr<IBackend> overload during overload resolution — both would otherwise require an equally-ranked user-defined conversion, making every existing call site ambiguous. | Backend | Concrete backend type; must derive from IBackend. |
| newBackend | Replacement backend. Ownership is transferred. Must not be null. |
| std::invalid_argument | if newBackend is null. |
Definition at line 1216 of file bridge.hpp.
|
inline |
Replaces the active backend with newBackend.
Tells newBackend its owner and the current default session, then issues a bind on it for every live binding (a shared one holding neither a key nor an instance has nothing to re-create; an anonymous one, not yet promoted, re-binds anonymous). Binds the new backend settles before returning decide the switch: if any of them failed, every instance acquired so far is released, and the failure is rethrown with the old backend and every binding untouched. Otherwise the switch commits: the settled ids are published, a bind still in flight leaves its binding unbound with its calls held until it settles, the backend is swapped, notifyBackendChanged() runs, the reconnect handler moves to the new backend, and the old backend's pending calls are cancelled with BackendChangedError (a Task handler still running there is asked to stop). A failure of a bind still in flight at the commit cannot roll the switch back; it is recorded on its binding.
The caller keeps shared ownership of newBackend, so the same backend can be re-installed later without reconstructing it.
| newBackend | Replacement backend, shared with the caller. Must not be null. |
| std::invalid_argument | if newBackend is null; the bridge keeps its backend and every binding. |
Definition at line 1245 of file bridge.hpp.
|
inline |
Deregisters binding from the active backend and removes it from tracking.
Every call held for a bind in flight is rejected with HandlerDestroyedError, and that bind's reply, when it lands, releases its instance rather than binding it. After this, a call made through binding fails with "handler not bound".
| binding | Binding to remove. |
Definition at line 1348 of file bridge.hpp.
|
inline |
Dispatches action against the model identified by binding.
Counts the call as pending and arms the client-side deadline at once. When binding is bound and no bind is in flight, the action is dispatched here, through IBackend::executeInto, handing the backend a detail::BridgeSink<R> that is both the caller's typed completion state and the backend's settle sink. Otherwise the call is held by the binding and dispatched on the owner when the bind settles; a failed bind rejects it with the bind's error, a destroyed handler with HandlerDestroyedError. An AllowShared binding that was never attached rejects it with "handler not bound".
On LocalBackend, the localOp this method builds first overwrites any declared computed fields from their inputs (morph::forms::recomputeAll, a no-op for actions with no computedFields), then enforces morph::model::ActionValidator<Action>::ready(action) before calling Model::execute, mirroring ActionDispatcher::registerAction's runner (registry.hpp) for the in-process path. A false result resolves the returned Completion through onError with a morph::model::ValidationError instead of executing the action — see docs/spec/core/registry.md and docs/spec/forms/forms.md.
A call whose handler returns a Task carries a stop source, requested by the execute deadline, by a CallbackScope a callback was attached through (see Completion::then), and by the backend's cancelPending.
| Model | Model type that owns the handler. |
| Action | Action type to dispatch. |
| binding | Binding returned by registerHandler<Model>(). |
| action | Action to execute (moved in). |
| cbExec | Executor on which the Completion callbacks are posted. |
| onResult | Optional observer run on the owner on the typed result before it is moved into the returned Completion and before the caller's own .then. Used to adopt a result-sourced primary key so the binding is already promoted by the time user code sees the result; empty for every other call. |
Definition at line 1409 of file bridge.hpp.
|
inline |
Dispatches a payload-keyed action on a shared binding, attaching it to the instance key names first.
Waits for any bind in flight, then attaches (or re-points) the binding to key if it does not hold it already, and dispatches once the attach has settled. A refused attach rejects the call with its error; an empty key is refused at once with std::invalid_argument, leaving the binding as it was.
| Model | Model type that owns the handler. |
| Action | Action type to dispatch. |
| binding | Shared binding. |
| action | Action to execute (moved in). |
| cbExec | Executor on which the Completion callbacks are posted. |
| key | Canonical primary key the action names. |
Definition at line 1452 of file bridge.hpp.
|
inline |
Dispatches a result-keyed action on a shared binding, giving it an anonymous instance first if it has none, and promoting that instance under the key its result carries.
| Model | Model type that owns the handler. |
| Action | Action type to dispatch. |
| binding | Shared binding. |
| action | Action to execute (moved in). |
| cbExec | Executor on which the Completion callbacks are posted. |
Definition at line 1483 of file bridge.hpp.
|
inline |
Binds the model registered as typeId, for a caller compiled without the model's type.
A private binding is bound at once, as registerHandler<Model>() binds one. A shared binding is bound when attached: here when instanceKey is not empty, otherwise by a later attach. The binding is tracked for switchBackend(), deregistered by deregisterHandler, and its instance is owned by the session's principal on a server exactly as a typed handler's is.
Called off the owner — inside a running action, where a BridgeHandler may be constructed too — the registration is posted to the owner, and a call made meanwhile is held. Attaching is the owner's, so a key is refused there.
The bridge cannot tell whether the model is keyed: a shared binding of an unkeyed model is refused, if at all, by the backend when attached.
| typeId | The model's registered type id. |
| sharing | Private or shared. |
| instanceKey | For a shared binding, the key to attach to now, or empty to stay unattached. Must be empty for a private binding, and off the owner. |
| std::invalid_argument | for a private binding with a key. |
| std::logic_error | for a key given off the owner. |
Definition at line 1633 of file bridge.hpp.
|
inline |
Dispatches the action registered as actionId with a JSON body through binding, and resolves with the JSON reply.
The call is stamped with the default session, raced against the execute deadline, counted in pendingCalls(), held while a bind is in flight, and settled by cancelPending exactly as a typed call. It always carries a stop source, since whether the handler is a Task is not known here. Neither the client-side recompute nor the action's validator runs here: they need the action's type, and whoever runs the model runs both. The reply is not offered to typed subscriptions: it is JSON text, not the result a subscriber expects.
The call runs on the instance binding holds now. Unlike a typed call, it does not attach to the key a payload-keyed action names, nor create and promote an instance for a result-keyed one: a keyed action sent through a shared binding attached elsewhere runs on that other instance, and one sent before any attach is rejected as not bound. A keyed action goes through executeRawOn.
| binding | A binding from bindByType. |
| actionId | The action's registered type id. |
| bodyJson | The action's JSON body. |
| cbExec | Executor the Completion callbacks are posted on. |
Definition at line 1673 of file bridge.hpp.
|
inline |
executeRaw, cancellable through stop.
A stop before the call settles rejects it with core::async::OperationCancelled and asks its stop source to stop; a Task handler on a local backend sees that at its next stop-aware co_await. A token already stopped rejects without dispatching.
| binding | A binding from bindByType. |
| actionId | The action's registered type id. |
| bodyJson | The action's JSON body. |
| cbExec | Executor the Completion callbacks are posted on. |
| stop | The caller's cancel. |
Definition at line 1712 of file bridge.hpp.
|
inline |
executeRaw on the instance for instanceKey: attaches the shared binding to it first when it holds another or none.
What a keyed action needs from a caller without its type, which cannot read the key out of the body: the call runs on instanceKey's instance or not at all. An attach that is refused, or superseded by a re-bind, rejects the call with its failure instead of running it on the instance the binding fell back to. The binding stays attached to instanceKey afterwards, as a typed payload-keyed call leaves it. Otherwise as executeRaw.
| binding | A shared binding from bindByType. |
| instanceKey | Canonical string encoding of the primary key; see attach. |
| actionId | The action's registered type id. |
| bodyJson | The action's JSON body. |
| cbExec | Executor the Completion callbacks are posted on. |
| std::invalid_argument | for an empty instanceKey. |
| std::logic_error | for a private binding. |
Definition at line 1747 of file bridge.hpp.