morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
bridge.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <any>
6#include <atomic>
7#include <cassert>
8#include <chrono>
9#include <concepts>
10#include <core/async/Cancellation.hpp>
11#include <core/async/StopToken.hpp>
12#include <deque>
13#include <exception>
14#include <functional>
15#include <memory>
16#include <mutex>
17#include <optional>
18#include <stdexcept>
19#include <string>
20#include <string_view>
21#include <type_traits>
22#include <typeindex>
23#include <unordered_map>
24#include <utility>
25#include <vector>
26
27#include "../attributes.hpp"
28#include "../core/backend.hpp"
29#include "../forms/forms.hpp"
30#include "../session/session.hpp"
31#include "callback_scope.hpp"
32#include "completion.hpp"
35#include "model_key.hpp"
36#include "profiler.hpp"
37#include "registry.hpp"
38#include "timeout_scheduler.hpp"
39
40namespace morph::bridge {
41
42/// @brief Type-erased, JSON-in/JSON-out execute path for actions whose
43/// concrete C++ type is only known by its registered string id at
44/// the call site (e.g. a schema-driven GUI that reads action names
45/// out of a JSON Schema at runtime).
46///
47/// Populated automatically by `BRIDGE_REGISTER_ACTION` — no action-specific
48/// code is required at any call site. Every entry calls through the real
49/// `BridgeHandler<Model>::execute<Action>()` (so sessions, backend
50/// switches, and completions all behave exactly as they do for hand-written
51/// call sites), unlike `morph::model::detail::ActionDispatcher`, which
52/// calls `Model::execute` directly against an already-owned model holder
53/// and is only ever used server-side.
54///
55/// HARD REQUIREMENT (other direction of the same constraint documented on
56/// `morph::model::detail::registerActionExecutorOnce` in `registry.hpp`): registration into
57/// this registry happens via `registerActionExecutorOnce<Model, Action>`, which
58/// `BRIDGE_REGISTER_ACTION` calls unconditionally but which is only defined here, in
59/// `bridge.hpp`. Every translation unit that calls `BRIDGE_REGISTER_ACTION` must therefore
60/// include this header (directly or transitively), or that translation unit's static
61/// initializer will fail to link.
63public:
64 /// @brief Deserialises `bodyJson`, dispatches through the handler's
65 /// `Bridge`, and resolves with the JSON-encoded result.
66 using Executor = std::function<::morph::async::Completion<std::string>(void*, std::string_view)>;
67
68 /// @brief Registers the executors for `(Model, Action)` under the given string ids.
69 ///
70 /// Populates one `Executor` per `Sharing` policy the framework defines
71 /// (`NoSharing` and `AllowShared`), keyed by
72 /// `(modelId, actionId, typeid(Sharing))`, rather than a single executor
73 /// that unconditionally assumes `NoSharing`. A handler whose `Sharing` is
74 /// anything else must be dispatched through the *matching* executor: the
75 /// wrong one `static_cast`s `handlerVoid` to the wrong `BridgeHandler<Model,
76 /// Sharing>` instantiation, so `kShared` resolves incorrectly inside it and
77 /// a payload-/result-keyed action's attach-or-promote step silently never
78 /// runs. Both executors' bodies are otherwise byte-for-byte identical
79 /// (built once, from the same generic lambda template, instantiated for
80 /// `NoSharing` and again for `AllowShared` — the only two sharing tags
81 /// `morph::bridge` defines), so this costs one extra closure per
82 /// registered action, not per call.
83 /// Defined out-of-line after BridgeHandler to avoid forward reference issues.
84 /// @tparam Model Model type whose handler will execute the action.
85 /// @tparam Action Action type to register.
86 /// @param modelId String id the model is registered under.
87 /// @param actionId String id the action is registered under.
88 template <typename Model, typename Action>
89 void registerAction(std::string_view modelId, std::string_view actionId);
90
91 /// @brief Looks up and invokes the executor for `(modelId, actionId)`,
92 /// specialised for the caller's own `Sharing` policy.
93 /// @tparam Sharing `NoSharing` or `AllowShared` — the caller's own sharing policy.
94 /// @param modelId String id of the target model.
95 /// @param actionId String id of the action to execute.
96 /// @param handler Type-erased `BridgeHandler<Model, Sharing>*` matching `modelId`.
97 /// @param bodyJson JSON-encoded action payload.
98 /// @return Completion that resolves with the JSON-encoded action result.
99 /// @throws std::runtime_error if no executor was registered for that triple.
100 template <typename Sharing>
101 [[nodiscard]] ::morph::async::Completion<std::string> execute(std::string_view modelId, std::string_view actionId,
102 void* handler, std::string_view bodyJson) const {
103 // See `ActionDispatcher::dispatch` -- the registration-phase latch,
104 // closed on the first read of a process-level registry so a later
105 // registration can assert. Debug builds only.
106 ::morph::model::detail::noteRegistryRead(this == &instance());
107 auto iter = _executors.find(
108 KeyView{.modelId = modelId, .actionId = actionId, .sharing = std::type_index{typeid(Sharing)}});
109 if (iter == _executors.end()) {
110 throw std::runtime_error("unknown action for executeJson: " + std::string{modelId} + "/" +
111 std::string{actionId});
112 }
113 return iter->second(handler, bodyJson);
114 }
115
116 /// @brief Checks whether an executor is registered for `(modelId, actionId)`,
117 /// specialised for the caller's own `Sharing` policy, without invoking it.
118 ///
119 /// `registerAction` always files both the `NoSharing` and `AllowShared`
120 /// executors for a given `(Model, Action)` pair (see its own doc comment),
121 /// so for any action registered via `BRIDGE_REGISTER_ACTION` this answers
122 /// the same for either `Sharing` -- the template parameter exists for
123 /// symmetry with `execute<Sharing>` and to stay correct if that ever
124 /// changes, not because the two currently disagree.
125 /// @tparam Sharing `NoSharing` or `AllowShared` — the caller's own sharing policy.
126 /// @param modelId String id of the target model.
127 /// @param actionId String id of the action to check.
128 /// @return `true` if `execute<Sharing>(modelId, actionId, ...)` would find an executor.
129 template <typename Sharing>
130 [[nodiscard]] bool contains(std::string_view modelId, std::string_view actionId) const noexcept {
131 // Same registration-phase latch `execute` closes (see its own doc
132 // comment): this is a read of `_executors` too, so a registration
133 // racing a routing-only caller that never calls `execute` (e.g. one
134 // that only ever probes unrouted actions) must still be caught.
135 ::morph::model::detail::noteRegistryRead(this == &instance());
136 return _executors.contains(
137 KeyView{.modelId = modelId, .actionId = actionId, .sharing = std::type_index{typeid(Sharing)}});
138 }
139
140 /// @brief Returns the process-level singleton registry.
141 /// @return Reference to the singleton `ActionExecuteRegistry`.
143
144private:
145 // The stored key: the registry owns its ids, because nothing else outlives
146 // a registration.
147 struct Key {
148 std::string modelId;
149 std::string actionId;
150 std::type_index sharing;
151 bool operator==(const Key&) const = default;
152 };
153 // The key a caller looks an entry up *with*. Every id reaching `execute`
154 // arrives as a `string_view` -- a schema-driven GUI's decoded action name,
155 // or a `constexpr` `ModelTraits<M>::typeId()` -- so materialising `Key`
156 // just to hash it would charge every `executeJson` two `std::string`
157 // constructions. Deliberately not
158 // `morph::model::detail::PairKeyView`: this key carries a `std::type_index`
159 // as well as the two ids, so it needs its own view type and its own
160 // functors rather than a reuse that would silently drop the sharing tag.
161 struct KeyView {
162 std::string_view modelId;
163 std::string_view actionId;
164 std::type_index sharing;
165 };
166 struct KeyHash {
167 // Marks the functor transparent; `KeyEqual` below is the other half
168 // `unordered_map` needs before a heterogeneous `find` compiles at all.
169 using is_transparent = void;
170
171 std::size_t operator()(KeyView key) const noexcept {
172 // Spelled with the view type rather than a braced list: `PairKeyHash`
173 // is transparent now, so a braced `{modelId, actionId}` is equally
174 // convertible to both of its overloads and would be ambiguous.
175 std::size_t const modelHash =
176 ::morph::model::detail::PairKeyHash{}(::morph::model::detail::PairKeyView{key.modelId, key.actionId});
177 return modelHash ^ (key.sharing.hash_code() + 0x9e3779b9U + (modelHash << 6) + (modelHash >> 2));
178 }
179
180 // Routed through the view body rather than hashing the stored strings
181 // separately, for the reason `PairKeyHash`'s own comment gives: two
182 // bodies that happen to agree are not a property a heterogeneous
183 // lookup may rest on. If they disagreed, a transparent `find()` would
184 // hash into the wrong bucket and report a registered action as
185 // unknown, with no diagnostic anywhere.
186 std::size_t operator()(const Key& key) const noexcept {
187 return (*this)(KeyView{.modelId = key.modelId, .actionId = key.actionId, .sharing = key.sharing});
188 }
189 };
190 struct KeyEqual {
191 // Marks the functor transparent, enabling heterogeneous lookup. A
192 // transparent hash alone is not enough: `unordered_map` requires both
193 // before a heterogeneous `find` compiles, and naming only the hash
194 // leaves every lookup silently materialising a `Key`.
195 using is_transparent = void;
196
197 // Accepts any mix of `Key` and `KeyView` on either side.
198 template <typename LhsT, typename RhsT>
199 bool operator()(const LhsT& lhs, const RhsT& rhs) const noexcept {
200 return lhs.sharing == rhs.sharing && std::string_view{lhs.modelId} == std::string_view{rhs.modelId} &&
201 std::string_view{lhs.actionId} == std::string_view{rhs.actionId};
202 }
203 };
204 std::unordered_map<Key, Executor, KeyHash, KeyEqual> _executors;
205 /// @brief The action type each `(model, action)` id pair was registered for, keyed with `void` as its sharing.
206 std::unordered_map<Key, std::type_index, KeyHash, KeyEqual> _types;
207};
208
210 static ActionExecuteRegistry inst;
211 return inst;
212}
213
214} // namespace morph::bridge
215
216namespace morph::model::detail {
217
218template <typename Model, typename Action>
219inline bool registerActionExecutorOnce(std::string_view modelId, std::string_view actionId) noexcept {
220 // clang-tidy's misc-static-assert fires on any `assert(!f())` whose `f` takes no
221 // argument, whether or not `f` is constexpr -- and this one loads an atomic.
222 // Applying the check's own fix does not compile: `static_assert` on this
223 // condition is "static assertion expression is not an integral constant
224 // expression / non-constexpr function 'registrationPhaseClosed' cannot be used
225 // in a constant expression".
226 //
227 // Last re-checked at clang-tidy 22.1.8, the version CI pins: the diagnostic
228 // still fires and the suggested fix still does not compile. The stamp is here
229 // because nothing re-checks it for you -- no script flags a directive that has
230 // stopped suppressing anything, and nothing triggers a re-read on a
231 // CLANG_VERSION bump. Re-read this when the pin moves; if the finding is gone,
232 // delete all four directives together.
233 // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
234 assert(!::morph::model::registrationPhaseClosed() &&
235 "registerActionExecutorOnce: registration after the registration phase closed. The "
236 "process-level registries are unsynchronised and are read-only once dispatch begins -- "
237 "registering now races their internals against concurrent lookups (see "
238 "docs/spec/core/registry.md, \"Thread safety\"). Load and register plugin modules before "
239 "the first dispatch.");
240 try {
241 ::morph::bridge::ActionExecuteRegistry::instance().registerAction<Model, Action>(modelId, actionId);
242 } catch (::morph::model::DuplicateRegistrationError const& error) {
243 abortRegistration(error);
244 }
245 return true;
246}
247
248} // namespace morph::model::detail
249
250namespace morph::bridge {
251
252namespace detail {
253
254/// @brief Compile-time decomposition of a pointer-to-data-member type.
255///
256/// Recovers both the class and the member type from a single non-type template
257/// parameter, so callers name a field as `&MyAction::c` with no redundant type
258/// arguments.
259///
260/// @tparam T The pointer-to-member type (e.g. `double MyAction::*`).
261template <typename T>
262struct MemberPointerTraits;
263
264template <typename V, typename A>
265struct MemberPointerTraits<V A::*> {
266 /// @brief The class the member belongs to.
267 using ClassType = A;
268 /// @brief The type of the member itself.
269 using ValueType = V;
270};
271
272/// @brief Internal linkage record between a model type and its active backend id.
273///
274/// Shared between `Bridge` and the handler that owns it, a `BridgeHandler` or
275/// a `RawHandler`. Every field is touched only on
276/// the bridge's owner except `currentId`, an atomic so `isBound()` can be asked
277/// from anywhere.
278struct HandlerBinding {
279 /// @brief String type-id of the model: `ModelTraits<Model>::typeId()` for a
280 /// typed handler, the id given to `Bridge::bindByType` for a raw one.
281 std::string typeId;
282
283 /// @brief Factory used to re-register the model on a backend switch.
284 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> modelFactory;
285
286 /// @brief Stable identity of this model instance (e.g. an account id).
287 ///
288 /// Empty by default — the common local-mode case needs nothing here, since
289 /// `modelFactory` can already call `IModelHolder::attachActionLog` directly
290 /// with whatever identity it captures. Set this when the active backend may
291 /// be remote (`SimulatedRemoteBackend`, `QtWebSocketBackend`): it travels in
292 /// the `register` wire envelope so a server-side `RemoteServer::LogProvider`
293 /// can attach a log to the instance it creates. Set it before the binding
294 /// is registered.
295 std::string contextKey;
296
297 /// @brief Canonical string encoding of this instance's primary key.
298 ///
299 /// Empty until a keyed action or an explicit `attach` supplies one. Only
300 /// meaningful when `shared` is set; a private binding never consults it.
301 std::string primary;
302
303 /// @brief Whether this binding participates in the shared instance directory.
304 ///
305 /// Set once at construction — from `BridgeHandler`'s `Sharing` template
306 /// argument, or from `bindByType`'s `BindSharing` — and never changed. A
307 /// shared binding acquires an instance by attaching to a primary, or an
308 /// anonymous one for a result-keyed action, which keeps no primary until
309 /// the result's key promotes it.
310 bool shared = false;
311
312 /// @brief Current `ModelId` value in the active backend (0 = unbound).
313 std::atomic<uint64_t> currentId{0};
314
315 /// @brief Whether a bind issued for this binding has not settled yet.
316 bool bindInFlight = false;
317
318 /// @brief Counts the binds issued for this binding. A reply carrying an
319 /// older count was superseded — by a switch, a reconnect, or the
320 /// handler's destruction — and is released rather than applied.
321 std::uint64_t bindGeneration = 0;
322
323 /// @brief The last bind's failure, or null. What a held call is rejected
324 /// with when the bind it waited for failed.
325 std::exception_ptr bindFailure;
326
327 /// @brief The continuation of the operation that issued the bind in flight
328 /// (an attach, a first bind for a result-keyed action), run when it
329 /// settles, before `waiting`. Null to proceed, an error to give up.
330 std::function<void(std::exception_ptr)> afterBind;
331
332 /// @brief Calls held until no bind is in flight, in the order they were
333 /// made. Each is resumed with null to proceed, or with the error it
334 /// is rejected with.
335 std::deque<std::function<void(std::exception_ptr)>> waiting;
336};
337
338/// @brief Renders @p failure as the diagnostic string a log line wants.
339///
340/// The `Completion` surface carries an `exception_ptr`; the one remaining place
341/// that needs text rather than a rethrowable failure is a log message. Kept
342/// here rather than inline so "what does a rejected control call read like"
343/// has one answer.
344///
345/// @param failure Rejection to describe; may be null.
346/// @return `what()` for a `std::exception`, or a generic stand-in otherwise.
347inline std::string describeFailure(const std::exception_ptr& failure) {
348 if (!failure) {
349 return "unknown error";
350 }
351 try {
352 std::rethrow_exception(failure);
353 } catch (const std::exception& exc) {
354 return exc.what();
355 } catch (...) {
356 return "unknown error";
357 }
358}
359
360/// @brief A bind's outcome, taken from a `Completion` that settled before the
361/// call that returned it came back.
362struct BindOutcome {
363 /// @brief The bound instance; meaningful only without a failure.
364 ::morph::exec::detail::ModelId id{};
365 /// @brief The failure, or null.
366 std::exception_ptr failure;
367};
368
369/// @brief Takes @p completion's outcome if it has already settled.
370///
371/// How the bind rule's "a backend that can settles it before returning" is
372/// honoured: the caller applies the outcome in the same call, so a handler
373/// over such a backend is bound when its constructor returns. Taking a failure
374/// counts as handling it, so the state logs no orphan.
375/// @param completion A `bindModel`/`promoteModel` result, just returned.
376/// @return The outcome, or `std::nullopt` while the backend has not settled it.
377inline std::optional<BindOutcome> takeSettled(
378 const ::morph::async::Completion<::morph::exec::detail::ModelId>& completion) {
379 auto const state = completion.state();
380 if (state == nullptr) {
381 return BindOutcome{.id = {}, .failure = std::make_exception_ptr(std::runtime_error{"empty bind completion"})};
382 }
383 // `ready` publishes the write-once outcome, so reading it here needs no
384 // hop to the completion's executor.
385 if (!state->ready.load(std::memory_order_acquire)) {
386 return std::nullopt;
387 }
388 if (state->value) {
389 return BindOutcome{.id = *state->value, .failure = nullptr};
390 }
391 state->onErrAttached.store(true, std::memory_order_relaxed);
392 return BindOutcome{.id = {}, .failure = state->error};
393}
394
395/// @brief Whether a settled result is offered to the typed subscriptions.
396enum class Publish : std::uint8_t {
397 /// A typed call: the result is `R` and subscribers to `R` receive it.
398 Typed,
399 /// A raw call: the result is JSON text, which no typed subscriber expects.
400 No,
401};
402
403/// @brief The typed completion state a backend settles directly.
404///
405/// This object *is* the typed completion state `Bridge::executeVia` hands the
406/// caller, and is also the `ISettleSink` the backend settles: one allocation
407/// per dispatch rather than a typed completion plus an erased one with a
408/// forwarding `.then`/`.onError` pair between them.
409///
410/// @par What the settle path holds
411/// - **The deadline disarm happens first**, before any other settle work, so a
412/// slow `onResult`/`publishResult` cannot give the timer a window to resolve
413/// this completion with `ClientTimeoutError` while the real result is in
414/// hand — `settleOnce`, called at the top of both settle methods.
415/// - **`_pendingCalls` is decremented on exactly one of two mutually exclusive
416/// paths**, whether or not the work that follows then throws — `settleOnce`
417/// again, which is also what makes a `cancelPending` racing a reply decrement
418/// once rather than twice.
419/// - **Bridge-side work runs on the owner.** A backend settles on its own
420/// thread. When the result has work to do on the bridge — `onResult` (a
421/// result-keyed action's promotion) or a subscription fan-out — the rest of
422/// the settle is posted to the bridge's owner and done there, gated on the
423/// bridge's `CallbackToken`; the value reaches the caller after it. Without
424/// such work the value is forwarded at once, where the backend settled.
425/// - **The value forwarding is guarded**, so a throwing move of `R` routes to
426/// this state's own error sink instead of escaping the settling thread.
427///
428/// @tparam R The action's result type.
429template <typename R>
430class BridgeSink final : public ::morph::async::detail::CompletionState<R>,
431 public ::morph::async::detail::ISettleSink {
432public:
433 /// @brief Builds the sink for one dispatch.
434 /// @param onResult Optional observer run on the owner on the typed
435 /// result, before it is moved into this state and
436 /// before the caller's `.then`.
437 /// @param pendingCalls The bridge's pinned in-flight counter.
438 /// @param subscriptions The bridge's pinned subscription registry; only its
439 /// atomic `hasSubscribers()` is read off the owner.
440 /// @param liveness The bridge's token: nothing bridge-side runs once
441 /// it has expired.
442 /// @param owner The bridge's owner.
443 /// @param publish Whether the result is offered to the typed
444 /// subscriptions; `Publish::No` for a call whose
445 /// `R` is not the action's typed result.
446 BridgeSink(std::function<void(const R&)> onResult, std::shared_ptr<std::atomic<std::size_t>> pendingCalls,
447 std::shared_ptr<SubscriptionRegistry<HandlerBinding>> subscriptions,
448 ::morph::async::CallbackToken liveness, ::morph::exec::detail::OwnerAffinity owner,
449 Publish publish = Publish::Typed)
450 : _onResult{std::move(onResult)},
451 _pendingCalls{std::move(pendingCalls)},
452 _subscriptions{std::move(subscriptions)},
453 _liveness{std::move(liveness)},
454 _owner{owner},
455 _publish{publish} {}
456
457 /// @brief Names the instance this dispatch targets, for the fan-out.
458 /// Called on the owner before the dispatch.
459 /// @param mid The instance.
460 void target(::morph::exec::detail::ModelId mid) noexcept { _mid = mid; }
461
462 /// @brief Hands the sink the deadline entry it must disarm when it settles.
463 ///
464 /// Called by `executeVia` before the dispatch, so the write is sequenced
465 /// before anything that could settle this sink on another thread. The
466 /// scheduler is held as a `shared_ptr` copy, so `cancel()` stays safe from
467 /// a settle that outlives the bridge.
468 ///
469 /// @param handle The scheduled entry.
470 /// @param scheduler The scheduler that owns it.
471 void armDeadline(::morph::async::detail::TimeoutScheduler::Handle handle,
472 std::shared_ptr<::morph::async::detail::TimeoutScheduler> scheduler) {
473 _deadlineHandle = handle;
474 _schedulerRef = std::move(scheduler);
475 }
476
477 /// @brief Whether the state already settled — a deadline that fired while
478 /// the call waited for its bind.
479 /// @return True once settled.
480 [[nodiscard]] bool alreadySettled() const { return this->ready.load(std::memory_order_acquire); }
481
482 /// @brief Undoes `armDeadline` and the pending count for a dispatch that
483 /// never started, because `IBackend::executeInto` threw or the
484 /// deadline settled the call while it waited for its bind.
485 ///
486 /// Routed through `settleOnce` so it cannot double-count against a sink
487 /// the backend had already settled before it threw.
488 void abandon() {
489 if (settleOnce()) {
490 _pendingCalls->fetch_sub(1, std::memory_order_relaxed);
491 }
492 }
493
494 /// @brief Settles with the backend's opaque result.
495 /// @param opaque The result, which must point at an `R`. Spelled
496 /// `opaque` rather than `value` only because `CompletionState`
497 /// already has a member of that name, and -Wshadow-field is on.
498 void settleValue(std::shared_ptr<void> opaque) override {
499 MORPH_ZONE("BridgeSink::settleValue");
500 if (!settleOnce()) {
501 return;
502 }
503 _pendingCalls->fetch_sub(1, std::memory_order_relaxed);
504 bool bridgeWork = static_cast<bool>(_onResult);
505 if constexpr (std::is_copy_constructible_v<R>) {
506 bridgeWork = bridgeWork || (_publish == Publish::Typed && _subscriptions->hasSubscribers());
507 }
508 if (!bridgeWork) {
509 forward(opaque, /*onOwner=*/false);
510 return;
511 }
512 if (_owner.here()) {
513 forward(opaque, /*onOwner=*/true);
514 return;
515 }
516 auto self = std::static_pointer_cast<BridgeSink>(this->shared_from_this());
517 _owner.owner().post([self = std::move(self), opaque = std::move(opaque)] { self->forward(opaque, true); });
518 }
519
520 /// @brief Settles with a failure from the backend.
521 /// @param exc The exception to deliver to the caller's `.onError`.
522 void settleException(const std::exception_ptr& exc) override {
523 MORPH_ZONE("BridgeSink::settleException");
524 if (settleOnce()) {
525 _pendingCalls->fetch_sub(1, std::memory_order_relaxed);
526 }
527 // First result wins: a late failure after a settle is dropped by the
528 // state itself.
529 this->setException(exc);
530 }
531
532private:
533 /// @brief Runs the bridge-side work, when on the owner and the bridge is
534 /// still there, then hands the value to the caller.
535 /// @param opaque The backend's result.
536 /// @param onOwner Whether this runs on the bridge's owner.
537 void forward(const std::shared_ptr<void>& opaque, bool onOwner) {
538 // Its own zone: when the bridge has work to do, this runs on the owner
539 // in a task `settleValue` posted, not inside `settleValue`'s zone.
540 MORPH_ZONE("BridgeSink::forward");
541 try {
542 auto* const typedResult = static_cast<R*>(opaque.get());
543 if (onOwner && _liveness.active()) {
544 if (_onResult) {
545 _onResult(*typedResult);
546 }
547 if constexpr (std::is_copy_constructible_v<R>) {
548 if (_publish == Publish::Typed && _subscriptions->hasSubscribers()) {
549 _owner.note("Bridge::publishResult");
550 _subscriptions->publishResult(_mid, std::type_index{typeid(R)}, std::any{*typedResult});
551 }
552 }
553 }
554 this->setValue(std::move(*typedResult));
555 } catch (...) {
556 this->setException(std::current_exception());
557 }
558 }
559
560 /// @brief Claims the one-time settle work; `true` for exactly one caller.
561 ///
562 /// The deadline disarm and the `_pendingCalls` decrement happen once per
563 /// dispatch. `IBackend::cancelPending` settles a sink from one thread while
564 /// a reply may be settling it from another, so the latch is explicit.
565 /// @return `true` if this call claimed the settle, `false` if another did.
566 bool settleOnce() {
567 if (_settled.test_and_set(std::memory_order_acq_rel)) {
568 return false;
569 }
570 if (_deadlineHandle && _schedulerRef) {
571 try {
572 _schedulerRef->cancel(*_deadlineHandle);
573 } catch (...) { // NOLINT(bugprone-empty-catch)
574 // Best-effort by contract: a deadline callback already running
575 // still settles, and this state discards it -- first result
576 // wins. That, not the disarm, is what makes the race harmless.
577 }
578 }
579 return true;
580 }
581
582 ::morph::exec::detail::ModelId _mid{};
583 std::function<void(const R&)> _onResult;
584 std::shared_ptr<std::atomic<std::size_t>> _pendingCalls;
585 std::shared_ptr<SubscriptionRegistry<HandlerBinding>> _subscriptions;
586 ::morph::async::CallbackToken _liveness;
587 ::morph::exec::detail::OwnerAffinity _owner;
588 Publish _publish;
589 std::optional<::morph::async::detail::TimeoutScheduler::Handle> _deadlineHandle;
590 std::shared_ptr<::morph::async::detail::TimeoutScheduler> _schedulerRef;
591 std::atomic_flag _settled = ATOMIC_FLAG_INIT;
592};
593
594/// @brief `ActionCall::localOpAsync` for an action whose handler returns
595/// `core::async::Task`: the local-path twin of `localOp`.
596///
597/// Recomputes the action's computed fields and enforces its validator exactly
598/// as `localOp` does, starts the handler on the model's strand, and -- when its
599/// Task completes -- journals the outcome and reports it through @p done. See
600/// `docs/spec/core/coroutines.md`. Declared in every build, so `executeVia` can
601/// name it; a `MORPH_CLIENT_ONLY` build never instantiates it.
602/// @tparam Model Concrete model type.
603/// @tparam Action Concrete action type.
604/// @param holder The model instance; kept alive by @p done's owner until it has run.
605/// @param actionOwner The action, owned: the handler's frame outlives this call.
606/// @param executor The handler's resumer, on the model's strand.
607/// @param token The stop token the handler observes.
608/// @param done Called exactly once, on the strand.
609template <typename Model, typename Action>
610// Validation, the handler call, and the journalling of each outcome, in the
611// order localOp keeps them; splitting it would put that order in two places.
612// NOLINTNEXTLINE(readability-function-cognitive-complexity)
613void localTaskOp(::morph::model::detail::IModelHolder& holder, std::shared_ptr<void> actionOwner,
614 const std::shared_ptr<::morph::exec::detail::TaskResumer>& executor, ::core::async::StopToken token,
615 ::morph::backend::detail::ActionCall::LocalDone done) {
616 using R = ::morph::model::ActionTraits<Action>::Result;
617 using Handler = decltype(std::declval<Model&>().execute(std::declval<Action&>()));
618 Action& actionRef = *static_cast<Action*>(actionOwner.get());
619 Handler task;
620 try {
621 ::morph::forms::recomputeAll(actionRef);
623 throw ::morph::model::ValidationError{::morph::model::ModelTraits<Model>::typeId(),
624 ::morph::model::ActionTraits<Action>::typeId()};
625 }
626 task = holder.template into<Model>().execute(actionRef);
627 } catch (...) {
628 done(nullptr, std::current_exception());
629 return;
630 }
631 ::morph::model::detail::startTaskHandler<R>(
632 executor, std::move(task), std::move(token),
633 [&holder, actionOwner = std::move(actionOwner), done = std::move(done)](std::optional<R> result,
634 const std::exception_ptr& error) {
635 // Read only to journal the outcome, which a Loggable::No action
636 // compiles out.
637 static_cast<void>(holder);
638 const Action& action = *static_cast<const Action*>(actionOwner.get());
639 if (error) {
640 // The handler failed, so the action was rejected: recorded
641 // Outcome::Failed, as localOp records a throw from
642 // Model::execute. A journal write that throws here fails the
643 // call with the handler's own exception still.
644 try {
645 if constexpr (::morph::model::detail::actionLoggable<Action>() == ::morph::model::Loggable::Yes) {
646 if (holder.hasActionLog()) {
647 try {
648 std::rethrow_exception(error);
649 } catch (const std::exception& exc) {
650 ::morph::model::detail::recordActionFailure(
651 holder, std::string{::morph::model::ModelTraits<Model>::typeId()},
652 std::string{::morph::model::ActionTraits<Action>::typeId()},
653 ::morph::model::ActionTraits<Action>::toJson(action),
654 ::morph::model::detail::actionPayloadSchema<Action>(), exc.what());
655 } catch (...) { // NOLINT(bugprone-empty-catch): as localOp, below
656 // Not a std::exception: recorded nowhere, as localOp's
657 // rethrow leaves such a throw unrecorded.
658 }
659 }
660 }
661 } catch (...) { // NOLINT(bugprone-empty-catch): the handler's exception is reported
662 }
663 done(nullptr, error);
664 return;
665 }
666 // The Task completed, so the model's mutation has committed: as in
667 // localOp, failing to serialise the result or to record it is an
668 // ActionRecordingError, never a rejection.
669 std::shared_ptr<void> value;
670 std::string resultJson;
671 try {
672 auto typed = std::make_shared<R>(std::move(*result));
673 if constexpr (::morph::model::detail::actionLoggable<Action>() == ::morph::model::Loggable::Yes) {
674 if (holder.hasActionLog()) {
675 resultJson = ::morph::model::ActionTraits<Action>::resultToJson(*typed);
676 ::morph::model::detail::recordActionSuccess(
677 holder, std::string{::morph::model::ModelTraits<Model>::typeId()},
678 std::string{::morph::model::ActionTraits<Action>::typeId()},
679 ::morph::model::ActionTraits<Action>::toJson(action),
680 ::morph::model::detail::actionPayloadSchema<Action>(), resultJson);
681 }
682 }
683 value = std::move(typed);
684 } catch (const std::exception& exc) {
685 done(nullptr,
686 std::make_exception_ptr(::morph::model::ActionRecordingError{std::move(resultJson), exc.what()}));
687 return;
688 } catch (...) {
689 done(nullptr, std::current_exception());
690 return;
691 }
692 done(std::move(value), nullptr);
693 });
694}
695
696/// @brief The action of a call dispatched by id: both ids and the JSON body.
697///
698/// Owned by the call's `ActionCall::action`; the call's `modelTypeId` and
699/// `actionTypeId` views point into it, so they outlive the dispatch however
700/// short-lived the caller's strings were.
701struct RawAction {
702 /// @brief The model's registered type id.
703 std::string modelTypeId;
704 /// @brief The action's registered type id.
705 std::string actionTypeId;
706 /// @brief The action's JSON body.
707 std::string body;
708};
709
710} // namespace detail
711
712/// @brief Whether a binding made by type id joins the shared instance directory.
713enum class BindSharing : std::uint8_t {
714 /// A private instance, created when the binding is bound.
715 Private,
716 /// A shared instance, created or joined when the binding is attached to a key.
717 Shared,
718};
719
720/// @brief Central dispatcher that routes typed actions to an `IBackend`.
721///
722/// `Bridge` owns exactly one active backend at a time. It tracks all registered
723/// `HandlerBinding` instances and re-registers them when `switchBackend()` is
724/// called or the backend reconnects, enabling seamless local ↔ remote
725/// transitions.
726///
727/// @par One owner
728/// A `Bridge`, its handlers and its backend belong to the executor given at
729/// construction. Every member function is called there — inside a task of the
730/// owner, or on the thread that constructed the bridge outside the owner's
731/// tasks (a GUI owner's own thread) — and every field is touched only there,
732/// so nothing here takes a lock. A call from anywhere else is a contract
733/// violation, asserted in a debug build. What reaches the bridge from other
734/// threads arrives as a task on the owner: bind and promote replies (the owner
735/// is the executor `bindModel`/`promoteModel` deliver on), reconnects (the
736/// executor the reconnect handler is posted to), and the bridge-side work an
737/// action's result triggers. `pendingCalls()`, `isBound()` and
738/// `hasSubscribers()` read atomics and may be asked from any thread. See
739/// `docs/spec/core/bridge.md`, "Thread safety — one owner".
740///
741/// @par The bind rule
742/// The bind is a `Completion` delivered on the owner; a backend that can
743/// settles it before returning, and that outcome is applied at once. `execute`
744/// dispatches immediately when the binding's `currentId` is set and no bind is
745/// in flight, and is otherwise held by the binding and dispatched when the bind
746/// settles.
747class Bridge {
748public:
749 /// @brief Constructs a bridge that dispatches through @p backend, owned by @p owner.
750 ///
751 /// Tells the backend its owner, installs a reconnect handler (posted to
752 /// @p owner) so a backend with a recoverable transport re-registers every
753 /// live handler on the owner after a reconnect, and pushes the (initially
754 /// empty) default session to the backend.
755 ///
756 /// A bridge always has a backend: every handler binds on construction,
757 /// so a bridge without one could not serve its first handler.
758 ///
759 /// @param backend Initial backend. Ownership is transferred. Must not be
760 /// null.
761 /// @param owner The executor this bridge, its handlers and its backend
762 /// belong to. Construct the bridge there: inside one of its
763 /// tasks, or on the thread that runs them. Borrowed: it must
764 /// outlive this bridge and every reply still in flight when
765 /// the bridge is destroyed.
766 /// @throws std::invalid_argument if @p backend is null.
767 Bridge(std::unique_ptr<::morph::backend::detail::IBackend> backend,
768 ::morph::exec::IExecutor& owner MORPH_LIFETIMEBOUND)
769 : _affinity{owner}, _backend{std::shared_ptr<::morph::backend::detail::IBackend>(std::move(backend))} {
770 if (!_backend) {
771 throw std::invalid_argument{"Bridge: the backend is null"};
772 }
773 _backend->setOwner(_affinity);
774 installReconnectHandler(_backend);
775 _backend->setSession(_defaultSession);
776 }
777
778 /// @brief Clears the backend's reconnect handler, rejects every call still
779 /// waiting for a bind with `BridgeDestroyedError`, and cancels the
780 /// backend's pending calls with the same error.
781 ///
782 /// On the owner. A reply or reconnect still queued there finds the
783 /// bridge's token expired and touches nothing; a bind reply for a binding
784 /// the bridge no longer tracks releases its instance on the backend that
785 /// issued it.
787 note("Bridge::~Bridge");
788 auto const destroyed = std::make_exception_ptr(::morph::backend::BridgeDestroyedError{});
789 _backend->setReconnectHandler(nullptr, nullptr);
790 std::vector<std::function<void(std::exception_ptr)>> rejected;
791 for (auto const& weak : _handlers) {
792 if (auto binding = weak.lock()) {
793 ++binding->bindGeneration;
794 binding->bindInFlight = false;
795 takeHeld(*binding, rejected);
796 }
797 }
798 for (auto& resume : rejected) {
799 resume(destroyed);
800 }
801 _backend->cancelPending(destroyed);
802 }
803
804 Bridge(const Bridge&) = delete;
805 Bridge& operator=(const Bridge&) = delete;
806 Bridge(Bridge&&) = delete;
807 Bridge& operator=(Bridge&&) = delete;
808
809 /// @brief The executor this bridge belongs to.
810 /// @return The owner given at construction.
811 [[nodiscard]] ::morph::exec::IExecutor& owner() const noexcept { return _affinity.owner(); }
812
813 /// @brief Whether the calling thread is on this bridge's owner.
814 /// @return True inside one of the owner's tasks, or on the thread that
815 /// constructed the bridge outside them.
816 [[nodiscard]] bool onOwner() const noexcept { return _affinity.here(); }
817
818 /// @brief Creates and registers a new `HandlerBinding` for `Model`.
819 ///
820 /// Uses the default `ModelFactory::create<Model>()` factory. The bind is
821 /// issued and this returns; see `registerHandler(binding)`.
822 /// @tparam Model Concrete model type. Must have a registered `ModelTraits` specialisation.
823 /// @return Shared pointer to the new binding.
824 template <typename Model>
825 std::shared_ptr<detail::HandlerBinding> registerHandler() {
826 auto binding = makeBinding<Model>(false);
827 registerHandler(binding);
828 return binding;
829 }
830
831 /// @brief Tracks @p binding and issues its bind on the active backend.
832 ///
833 /// Never waits. A backend that settles the bind before returning has bound
834 /// @p binding by the time this returns; otherwise a call made through it
835 /// is held until the bind settles. A failed bind is logged and rejects the
836 /// calls held for it with its error.
837 /// @param binding Pre-constructed binding. Its `typeId`, `modelFactory`
838 /// and `contextKey` must be set.
839 void registerHandler(const std::shared_ptr<detail::HandlerBinding>& binding) {
840 note("Bridge::registerHandler");
841 _handlers.push_back(binding);
842 auto superseded = startBind(binding,
843 ::morph::backend::detail::BindRequest{.typeId = binding->typeId,
844 .factory = binding->modelFactory,
845 .contextKey = binding->contextKey,
846 .primary = {},
847 .current = {}},
848 std::nullopt, {});
849 runSuperseded(superseded);
850 }
851
852 /// @brief Creates a shared, initially **unattached** binding for `Model`.
853 ///
854 /// Registers nothing on the backend: a shared handler has no instance
855 /// until a keyed action or an explicit `attach` names one. The binding is
856 /// tracked from the start so `switchBackend()` knows about it.
857 ///
858 /// @tparam Model Concrete model type. Must have a registered `ModelTraits`.
859 /// @return Shared pointer to the new, unattached binding.
860 template <typename Model>
861 std::shared_ptr<detail::HandlerBinding> registerSharedHandler() {
862 note("Bridge::registerSharedHandler");
863 auto binding = makeBinding<Model>(true);
864 _handlers.push_back(binding);
865 return binding;
866 }
867
868 /// @brief Attaches (or re-points) a shared @p binding to the instance for
869 /// @p primary, creating it if no live instance holds the key.
870 ///
871 /// Idempotent: attaching to the primary a binding already holds is a no-op.
872 /// A different primary re-points the binding — the previous instance is
873 /// released and survives only if another handler still holds it. The
874 /// binding's `contextKey` is set to @p primary as well, so a keyed model's
875 /// journal entries carry the entity key.
876 ///
877 /// Issued after any bind already in flight for @p binding, and never
878 /// waits: a call made through the binding meanwhile is held until it
879 /// settles. A refused attach is logged and leaves the binding on the
880 /// instance it held; a call held behind it is rejected with its error only
881 /// when the binding has no instance to fall back to.
882 ///
883 /// Needs no model type, so a binding from `bindByType` attaches through it
884 /// as a typed one does. The key is matched exactly, so it must be the
885 /// canonical encoding `keyToString` gives the model's key type: another
886 /// spelling of the same key names a different instance.
887 ///
888 /// @param binding Shared binding, from `registerSharedHandler<Model>()` or
889 /// `bindByType`.
890 /// @param primary Canonical string encoding of the primary key to attach to.
891 /// @throws std::invalid_argument for an empty @p primary: the bridge reads
892 /// an empty primary as "never attached".
893 /// @throws std::logic_error for a private @p binding, whose instance is
894 /// re-created private by every re-bind.
895 void attach(const std::shared_ptr<detail::HandlerBinding>& binding, std::string primary) {
896 if (primary.empty()) {
897 throw std::invalid_argument{"Bridge::attach: the instance key is empty"};
898 }
899 if (!binding->shared) {
900 throw std::logic_error{"Bridge::attach: the binding is private"};
901 }
902 attachBinding(binding, std::move(primary));
903 }
904
905 /// @brief `attach` for a typed caller.
906 ///
907 /// Skips `attach`'s private-binding check, since a typed handler's sharing
908 /// is fixed by its type, and refuses an empty @p primary without throwing:
909 /// in its turn after any bind in flight, the refusal is logged and leaves
910 /// the binding on the instance it held. A binding with no instance records
911 /// the refusal as its bind failure, so a call made through it is rejected
912 /// with that error.
913 /// @tparam Model Concrete model type.
914 /// @param binding Shared binding, as returned by `registerSharedHandler<Model>()`.
915 /// @param primary Canonical string encoding of the primary key to attach to.
916 template <typename Model>
917 void attachHandler(const std::shared_ptr<detail::HandlerBinding>& binding, std::string primary) {
918 if (primary.empty()) {
919 refuseAttach(binding, emptyKey());
920 return;
921 }
922 attachBinding(binding, std::move(primary));
923 }
924
925private:
926 /// @brief `attach`'s body, without its argument checks: a typed shared
927 /// handler's key and sharing are fixed by its type.
928 /// @param binding Shared binding.
929 /// @param primary Canonical string encoding of the primary key to attach to.
930 void attachBinding(const std::shared_ptr<detail::HandlerBinding>& binding, std::string primary) {
931 note("Bridge::attachHandler");
932 std::weak_ptr<detail::HandlerBinding> const weak{binding};
933 whenIdle(*binding, [this, weak, primary = std::move(primary)](const std::exception_ptr& failure) {
934 auto strong = weak.lock();
935 if (failure || !strong || (strong->primary == primary && strong->currentId.load() != 0U)) {
936 return;
937 }
938 runSuperseded(startBind(strong, attachRequest(*strong, primary), primary, {}));
939 });
940 }
941
942 /// @brief An empty key reads as "never attached" everywhere the bridge
943 /// decides what to re-bind, so it can name no shared instance.
944 /// @return The error an attach to an empty key is refused with.
945 [[nodiscard]] static std::exception_ptr emptyKey() {
946 return std::make_exception_ptr(std::invalid_argument{"Bridge: the instance key is empty"});
947 }
948
949 /// @brief Refuses a typed attach without throwing, in its turn after any
950 /// bind in flight for @p binding: logs @p refused, and records it as
951 /// the bind failure of a binding with no instance to fall back to.
952 /// @param binding Shared binding.
953 /// @param refused Why the attach is refused.
954 void refuseAttach(const std::shared_ptr<detail::HandlerBinding>& binding, std::exception_ptr refused) {
955 note("Bridge::attachHandler");
956 std::weak_ptr<detail::HandlerBinding> const weak{binding};
957 whenIdle(*binding, [weak, refused = std::move(refused)](const std::exception_ptr& failure) {
958 auto strong = weak.lock();
959 if (failure || !strong) {
960 return;
961 }
962 ::morph::log::logError("[bridge] attach of '" + strong->typeId +
963 "' refused: " + detail::describeFailure(refused));
964 if (strong->currentId.load() == 0U) {
965 strong->bindFailure = refused;
966 }
967 });
968 }
969
970public:
971 /// @brief Files @p binding's current instance under @p primary, in place.
972 ///
973 /// The instance keeps everything the creating action just did — nothing is
974 /// re-created and nothing is stranded. A no-op if @p binding is unbound or
975 /// already holds a real primary (the backend refuses to re-key a keyed
976 /// instance — see `IBackend::assignPrimary`).
977 ///
978 /// Reaches the backend through `IBackend::promoteModel`, delivered on the
979 /// owner. A backend that settles before returning has promoted the binding
980 /// by the time this returns, which a result-keyed `execute` relies on: its
981 /// `.then` then already sees the new primary. A failure is logged.
982 ///
983 /// Known gap: if the target key is already held by a different instance,
984 /// the backend declines to promote (the existing holder always wins) but
985 /// reports success, so the binding caches a primary the backend never
986 /// filed it under. Closing it needs `assignPrimary`'s outcome to become
987 /// observable across every backend.
988 /// @tparam Model Concrete model type.
989 /// @param binding Shared binding whose instance is being promoted.
990 /// @param primary Canonical string encoding of the key to file it under.
991 template <typename Model>
992 void assignHandlerPrimary(const std::shared_ptr<detail::HandlerBinding>& binding, std::string primary) {
993 note("Bridge::assignHandlerPrimary");
994 auto const raw = binding->currentId.load();
995 if (raw == 0U || primary.empty() || !binding->primary.empty()) {
996 return;
997 }
998 auto backend = _backend;
999 auto completion = backend->promoteModel(
1000 ::morph::backend::detail::PromoteRequest{
1001 .mid = ::morph::exec::detail::ModelId{raw}, .typeId = binding->typeId, .primary = primary},
1002 _affinity.owner());
1003 std::weak_ptr<detail::HandlerBinding> const weak{binding};
1004 std::weak_ptr<::morph::backend::detail::IBackend> const weakBackend{backend};
1005 if (auto settled = detail::takeSettled(completion)) {
1006 applyPromote(weak, weakBackend, settled->failure, primary);
1007 return;
1008 }
1009 auto const token = _callbacks.token();
1010 completion
1011 .then([this, token, weak, weakBackend, primary](::morph::exec::detail::ModelId) {
1012 if (token.active()) {
1013 applyPromote(weak, weakBackend, nullptr, primary);
1014 }
1015 })
1016 .onError([this, token, weak, weakBackend, primary](const std::exception_ptr& failure) {
1017 if (token.active()) {
1018 applyPromote(weak, weakBackend, failure, primary);
1019 }
1020 });
1021 }
1022
1023 /// @brief Returns @p binding's current primary key, or empty if unattached.
1024 /// @param binding Binding to inspect.
1025 /// @return Canonical key string, or an empty string when unattached.
1026 [[nodiscard]] std::string bindingPrimary(const std::shared_ptr<detail::HandlerBinding>& binding) const {
1027 note("Bridge::bindingPrimary");
1028 return binding->primary;
1029 }
1030
1031 /// @brief Whether @p binding currently has a live `ModelId`.
1032 ///
1033 /// A point-in-time atomic read, safe from any thread. Nothing needs to wait
1034 /// on it: a call made through an unbound handler whose bind is in flight
1035 /// is held until the bind settles.
1036 /// @param binding Binding to inspect.
1037 /// @return `true` if `currentId != 0`.
1038 [[nodiscard]] static bool isBound(const std::shared_ptr<detail::HandlerBinding>& binding) noexcept {
1039 return binding->currentId.load() != 0U;
1040 }
1041
1042 /// @brief Lists the live shared primary keys of `Model` on the active backend.
1043 /// @tparam Model Concrete model type.
1044 /// @param cbExec Executor the answer is delivered on. Borrowed: it must
1045 /// outlive the returned `Completion`.
1046 /// @return A `Completion` resolved with the canonical key strings, in
1047 /// unspecified order, or rejected with the backend's failure.
1048 template <typename Model>
1050 ::morph::exec::IExecutor& cbExec MORPH_LIFETIMEBOUND) {
1051 note("Bridge::instancesOf");
1052 return _backend->instances(std::string{::morph::model::ModelTraits<Model>::typeId()}, cbExec);
1053 }
1054
1055 /// @brief Registers a result-type subscription for @p binding.
1056 ///
1057 /// The subscription is stored against the *binding*, not against a fixed
1058 /// instance id, and is matched at publish time by comparing the binding's
1059 /// current instance, so re-pointing a handler moves its subscriptions with
1060 /// it. See `detail::SubscriptionRegistry`.
1061 /// @param binding Handler binding that owns the subscription.
1062 /// @param type Result type being subscribed to.
1063 /// @param sink Type-erased delivery callback; receives the boxed result.
1064 /// @param exec Executor the callback is delivered on.
1065 void addSubscription(const std::shared_ptr<detail::HandlerBinding>& binding, std::type_index type,
1066 std::function<void(const std::any&)> sink, ::morph::exec::IExecutor* exec) {
1067 note("Bridge::addSubscription");
1068 _subscriptions->addSubscription(binding, type, std::move(sink), exec);
1069 }
1070
1071 /// @brief Removes @p binding's subscription for @p type, if any.
1072 /// @param binding Handler binding that owns the subscription.
1073 /// @param type Result type to stop hearing about.
1074 void removeSubscription(const std::shared_ptr<detail::HandlerBinding>& binding, std::type_index type) {
1075 note("Bridge::removeSubscription");
1076 _subscriptions->removeSubscription(binding, type);
1077 }
1078
1079 /// @brief Whether any subscription is currently registered on this bridge.
1080 ///
1081 /// A single relaxed atomic load, safe from any thread: a result with no
1082 /// subscriber is never boxed and never sent to the owner.
1083 /// @return `true` if at least one subscription exists.
1084 [[nodiscard]] bool hasSubscribers() const noexcept { return _subscriptions->hasSubscribers(); }
1085
1086 /// @brief Delivers @p value to every subscriber attached to instance @p mid.
1087 ///
1088 /// Called, on the owner, for every successful action result that has
1089 /// subscribers. Subscribers are matched on *the instance the result was
1090 /// produced on*, so a handler hears about work another handler — or, with a
1091 /// shared instance, another screen entirely — did on the model it is
1092 /// attached to. The producing handler is notified too.
1093 ///
1094 /// @param mid Instance the result was produced on.
1095 /// @param type Result type produced.
1096 /// @param value Boxed result.
1097 void publishResult(::morph::exec::detail::ModelId mid, std::type_index type, const std::any& value) {
1098 note("Bridge::publishResult");
1099 _subscriptions->publishResult(mid, type, value);
1100 }
1101
1102 /// @brief Installs a default session context that `executeVia` stamps onto the
1103 /// `ActionCall` of every subsequent call.
1104 ///
1105 /// There is no per-call session override — this default is applied to all calls
1106 /// until replaced or cleared. Also pushed to the active backend, so every
1107 /// control envelope it builds afterwards carries it too.
1108 ///
1109 /// @param session The new default. Pass `{}` to clear.
1111 note("Bridge::setDefaultSession");
1112 _defaultSession = session;
1113 _backend->setSession(std::move(session));
1114 }
1115
1116 /// @brief Sets (or disables) the client-side execute deadline.
1117 ///
1118 /// Every `executeVia()` call after this point races the real reply against
1119 /// @p deadline; whichever settles first wins (`CompletionState::setValue`/
1120 /// `setException` are idempotent — see `completion.hpp`). If @p deadline
1121 /// elapses first, the pending `Completion` fails with
1122 /// `::morph::backend::ClientTimeoutError`; the real reply, if it arrives
1123 /// later, is silently discarded exactly like any other late write to an
1124 /// already-resolved `CompletionState`.
1125 ///
1126 /// The clock starts inside `executeVia()`, when the call is made — before a
1127 /// wait for its bind, if it has to wait — so @p deadline covers the whole
1128 /// round trip.
1129 ///
1130 /// Disabled (`std::chrono::milliseconds{0}`, the default): a dropped
1131 /// frame or a hung server leaves the `Completion` pending forever.
1132 ///
1133 /// The backing `TimeoutScheduler` (and the `exec::IoLoop` it owns, with
1134 /// that loop's one thread) is created lazily on the first call that enables
1135 /// a deadline, so a `Bridge` that never opts in spawns no extra thread. Once
1136 /// created it lives until `~Bridge()`.
1137 ///
1138 /// @param deadline Maximum time to wait for any reply. `0` disables the
1139 /// deadline.
1140 void setExecuteDeadline(std::chrono::milliseconds deadline) {
1141 note("Bridge::setExecuteDeadline");
1142 _executeDeadline = deadline;
1143 if (_executeDeadline.count() > 0 && !_timeoutScheduler) {
1144 _timeoutScheduler = std::make_shared<::morph::async::detail::TimeoutScheduler>();
1145 }
1146 }
1147
1148 /// @brief Returns the currently installed client-side execute deadline.
1149 /// @return The deadline; `std::chrono::milliseconds{0}` when disabled.
1150 [[nodiscard]] std::chrono::milliseconds executeDeadline() const {
1151 note("Bridge::executeDeadline");
1152 return _executeDeadline;
1153 }
1154
1155 /// @brief Returns a copy of the currently installed default session.
1156 /// @return Snapshot of the default `Context`.
1158 note("Bridge::defaultSession");
1159 return _defaultSession;
1160 }
1161
1162 /// @brief Installs the verified `Principal` for this `Bridge`.
1163 ///
1164 /// Typically called once right after a successful login dispatch, from
1165 /// data the server actually returned (see `session::Principal`'s doc
1166 /// comment on the trust model). Distinct from `setDefaultSession`: that
1167 /// installs the per-call `Context` forwarded with every dispatch; this
1168 /// installs the longer-lived identity UI code reads *outside* a dispatch
1169 /// via `currentPrincipal()` to shape itself.
1170 /// @param principal Verified identity to install. Pass a
1171 /// default-constructed `Principal{}` (or call this from a sign-out
1172 /// handler) to clear it.
1174 note("Bridge::setPrincipal");
1175 _principal = std::move(principal);
1176 }
1177
1178 /// @brief Returns a copy of the currently installed `Principal`.
1179 ///
1180 /// Default-constructed (empty `id`, no `roles`) if `setPrincipal` was
1181 /// never called or the application signed out by clearing it.
1182 /// @return Snapshot of the installed `Principal`.
1184 note("Bridge::currentPrincipal");
1185 return _principal;
1186 }
1187
1188 /// @brief Returns the number of calls made through the bridge — typed
1189 /// (`executeVia()` and its keyed variants) or raw (`executeRaw()`,
1190 /// `executeRawOn()`) — that have not yet resolved.
1191 ///
1192 /// Incremented once per call, when it is made (a call held for its bind
1193 /// included); decremented exactly once when the returned `Completion`
1194 /// settles on success or on error (including a cancellation error from
1195 /// `switchBackend()`/`~Bridge()`/a dropped transport). A client can poll
1196 /// this to build a "still loading" indicator or gate a feature on
1197 /// quiescence. A single relaxed atomic load, safe from any thread.
1198 ///
1199 /// @return Count of actions made but not yet resolved.
1200 [[nodiscard]] std::size_t pendingCalls() const noexcept { return _pendingCalls->load(std::memory_order_relaxed); }
1201
1202 /// @brief Replaces the active backend with @p newBackend.
1203 ///
1204 /// @note Templated on the concrete @p Backend (rather than taking
1205 /// `unique_ptr<IBackend>` directly) so it is an exact match for a
1206 /// `unique_ptr<Concrete>` argument (e.g. `std::make_unique<LocalBackend>(...)`)
1207 /// and is preferred over the `shared_ptr<IBackend>` overload during overload
1208 /// resolution — both would otherwise require an equally-ranked user-defined
1209 /// conversion, making every existing call site ambiguous.
1210 /// @tparam Backend Concrete backend type; must derive from `IBackend`.
1211 /// @param newBackend Replacement backend. Ownership is transferred. Must not
1212 /// be null.
1213 /// @throws std::invalid_argument if @p newBackend is null.
1214 template <typename Backend>
1215 requires std::derived_from<Backend, ::morph::backend::detail::IBackend>
1216 void switchBackend(std::unique_ptr<Backend> newBackend) {
1217 switchBackend(std::shared_ptr<::morph::backend::detail::IBackend>{std::move(newBackend)});
1218 }
1219
1220 /// @brief Replaces the active backend with @p newBackend.
1221 ///
1222 /// Tells @p newBackend its owner and the current default session, then
1223 /// issues a bind on it for every live binding (a shared one holding
1224 /// neither a key nor an instance has nothing to re-create; an anonymous
1225 /// one, not yet promoted, re-binds anonymous). Binds the new backend settles before
1226 /// returning decide the switch: if any of them failed, every instance
1227 /// acquired so far is released, and the failure is rethrown with the old
1228 /// backend and every binding untouched. Otherwise the switch commits: the
1229 /// settled ids are published, a bind still in flight leaves its binding
1230 /// unbound with its calls held until it settles, the backend is swapped,
1231 /// `notifyBackendChanged()` runs, the reconnect handler moves to the new
1232 /// backend, and the old backend's pending calls are cancelled with
1233 /// `BackendChangedError` (a Task handler still running there is asked to
1234 /// stop). A failure of a bind still in flight at the commit cannot roll the
1235 /// switch back; it is recorded on its binding.
1236 ///
1237 /// The caller keeps shared ownership of @p newBackend, so the same backend
1238 /// can be re-installed later without reconstructing it.
1239 ///
1240 /// @param newBackend Replacement backend, shared with the caller. Must not
1241 /// be null.
1242 /// @throws std::invalid_argument if @p newBackend is null; the bridge keeps
1243 /// its backend and every binding.
1244 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one ordered sequence -- pin, rebind, then reject what the old backend left waiting -- that reads worse split.
1245 void switchBackend(std::shared_ptr<::morph::backend::detail::IBackend> newBackend) {
1246 note("Bridge::switchBackend");
1247 if (!newBackend) {
1248 throw std::invalid_argument{"Bridge::switchBackend: the backend is null"};
1249 }
1250 auto next = std::move(newBackend);
1251 next->setOwner(_affinity);
1252 next->setSession(_defaultSession);
1253
1254 struct Issued {
1255 std::shared_ptr<detail::HandlerBinding> binding;
1257 std::optional<detail::BindOutcome> settled;
1258 };
1259 std::vector<Issued> issued;
1260 std::vector<std::weak_ptr<detail::HandlerBinding>> live;
1261 std::vector<std::shared_ptr<detail::HandlerBinding>> untouched;
1262 std::exception_ptr staging;
1263 try {
1264 for (auto const& weak : _handlers) {
1265 auto binding = weak.lock();
1266 if (!binding) {
1267 continue;
1268 }
1269 live.push_back(weak);
1270 if (holdsNothing(*binding)) {
1271 untouched.push_back(binding);
1272 continue;
1273 }
1274 auto completion = next->bindModel(rebindRequest(*binding), _affinity.owner());
1275 auto settled = detail::takeSettled(completion);
1276 bool const failed = settled && settled->failure;
1277 if (failed) {
1278 staging = settled->failure;
1279 }
1280 issued.push_back(Issued{.binding = binding, .completion = std::move(completion), .settled = settled});
1281 if (failed) {
1282 break;
1283 }
1284 }
1285 } catch (...) {
1286 staging = std::current_exception();
1287 }
1288 if (staging) {
1289 rollback(next, issued);
1290 std::rethrow_exception(staging);
1291 }
1292
1293 std::vector<std::function<void(std::exception_ptr)>> superseded;
1294 for (auto& entry : issued) {
1295 auto& binding = *entry.binding;
1296 auto const generation = ++binding.bindGeneration;
1297 if (auto own = std::exchange(binding.afterBind, {})) {
1298 superseded.push_back(std::move(own));
1299 }
1300 binding.bindFailure = nullptr;
1301 if (entry.settled) {
1302 binding.currentId.store(entry.settled->id.v);
1303 binding.bindInFlight = false;
1304 continue;
1305 }
1306 binding.currentId.store(0);
1307 binding.bindInFlight = true;
1308 awaitBind(entry.binding, next, generation, std::move(entry.completion), std::nullopt);
1309 }
1310 for (auto const& binding : untouched) {
1311 if (binding->bindInFlight) {
1312 // An attach in flight on the backend being replaced: its reply
1313 // is superseded, and a held keyed call re-issues it on the new
1314 // backend when it resumes.
1315 ++binding->bindGeneration;
1316 binding->bindInFlight = false;
1317 if (auto own = std::exchange(binding->afterBind, {})) {
1318 superseded.push_back(std::move(own));
1319 }
1320 }
1321 }
1322 _handlers = std::move(live);
1323 auto previous = std::exchange(_backend, next);
1324 next->notifyBackendChanged();
1325 installReconnectHandler(next);
1326 if (previous != next) {
1327 previous->setReconnectHandler(nullptr, nullptr);
1328 previous->cancelPending(std::make_exception_ptr(::morph::backend::BackendChangedError{}));
1329 }
1330 auto const changed = std::make_exception_ptr(::morph::backend::BackendChangedError{});
1331 for (auto& resume : superseded) {
1332 resume(changed);
1333 }
1334 for (auto const& weak : _handlers) {
1335 if (auto binding = weak.lock()) {
1336 drainWaiting(*binding);
1337 }
1338 }
1339 }
1340
1341 /// @brief Deregisters @p binding from the active backend and removes it from tracking.
1342 ///
1343 /// Every call held for a bind in flight is rejected with
1344 /// `HandlerDestroyedError`, and that bind's reply, when it lands, releases
1345 /// its instance rather than binding it. After this, a call made through
1346 /// @p binding fails with "handler not bound".
1347 /// @param binding Binding to remove.
1348 void deregisterHandler(const std::shared_ptr<detail::HandlerBinding>& binding) {
1349 note("Bridge::deregisterHandler");
1350 ++binding->bindGeneration;
1351 binding->bindInFlight = false;
1352 std::vector<std::function<void(std::exception_ptr)>> rejected;
1353 takeHeld(*binding, rejected);
1354 auto const raw = binding->currentId.exchange(0);
1355 if (raw != 0U) {
1356 _backend->deregisterModel(::morph::exec::detail::ModelId{raw});
1357 }
1358 std::erase_if(_handlers, [&binding](const auto& weak) {
1359 auto strong = weak.lock();
1360 return !strong || strong.get() == binding.get();
1361 });
1362 auto const destroyed = std::make_exception_ptr(::morph::backend::HandlerDestroyedError{});
1363 for (auto& resume : rejected) {
1364 resume(destroyed);
1365 }
1366 }
1367
1368 /// @brief Dispatches @p action against the model identified by @p binding.
1369 ///
1370 /// Counts the call as pending and arms the client-side deadline at once.
1371 /// When @p binding is bound and no bind is in flight, the action is
1372 /// dispatched here, through `IBackend::executeInto`, handing the backend a
1373 /// `detail::BridgeSink<R>` that is both the caller's typed completion state
1374 /// and the backend's settle sink. Otherwise the call is held by the binding
1375 /// and dispatched on the owner when the bind settles; a failed bind rejects
1376 /// it with the bind's error, a destroyed handler with
1377 /// `HandlerDestroyedError`. An `AllowShared` binding that was never
1378 /// attached rejects it with "handler not bound".
1379 ///
1380 /// On `LocalBackend`, the `localOp` this method builds first overwrites any
1381 /// declared computed fields from their inputs (`morph::forms::recomputeAll`,
1382 /// a no-op for actions with no `computedFields`), then enforces
1383 /// `morph::model::ActionValidator<Action>::ready(action)` before calling
1384 /// `Model::execute`, mirroring `ActionDispatcher::registerAction`'s runner
1385 /// (`registry.hpp`) for the in-process path. A `false` result resolves the
1386 /// returned `Completion` through `onError` with a `morph::model::ValidationError`
1387 /// instead of executing the action — see docs/spec/core/registry.md and
1388 /// docs/spec/forms/forms.md.
1389 ///
1390 /// A call whose handler returns a `Task` carries a stop source, requested
1391 /// by the execute deadline, by a `CallbackScope` a callback was attached
1392 /// through (see `Completion::then`), and by the backend's `cancelPending`.
1393 ///
1394 /// @tparam Model Model type that owns the handler.
1395 /// @tparam Action Action type to dispatch.
1396 /// @param binding Binding returned by `registerHandler<Model>()`.
1397 /// @param action Action to execute (moved in).
1398 /// @param cbExec Executor on which the `Completion` callbacks are posted.
1399 /// @param onResult Optional observer run on the owner on the typed result
1400 /// *before* it is moved into the returned `Completion` and
1401 /// before the caller's own `.then`. Used to adopt a
1402 /// result-sourced primary key so the binding is already
1403 /// promoted by the time user code sees the result; empty for
1404 /// every other call.
1405 /// @return Completion that resolves with the typed result or an exception
1406 /// (including `ValidationError` on `LocalBackend` when the action
1407 /// fails its validator).
1408 template <typename Model, typename Action>
1410 const std::shared_ptr<detail::HandlerBinding>& binding, Action action, ::morph::exec::IExecutor* cbExec,
1411 std::function<void(const typename ::morph::model::ActionTraits<Action>::Result&)> onResult = {}) {
1413 MORPH_ZONE("Bridge::executeVia");
1414 note("Bridge::executeVia");
1415 auto sink = makeSink<Model, Action>(std::move(onResult));
1416 ::morph::async::Completion<R> typed{sink, cbExec};
1417 armDeadline(sink);
1418 if (!binding->bindInFlight) {
1419 dispatchNow<Model, Action>(*binding, sink, std::move(action), cbExec, /*held=*/false);
1420 return typed;
1421 }
1422 std::weak_ptr<detail::HandlerBinding> const weak{binding};
1423 binding->waiting.push_back([this, weak, sink, cbExec, held = std::make_shared<Action>(std::move(action))](
1424 const std::exception_ptr& failure) {
1425 auto strong = weak.lock();
1426 if (failure || !strong) {
1427 sink->settleException(failure ? failure
1428 : std::make_exception_ptr(::morph::backend::HandlerDestroyedError{}));
1429 return;
1430 }
1431 dispatchNow<Model, Action>(*strong, sink, std::move(*held), cbExec, /*held=*/true);
1432 });
1433 return typed;
1434 }
1435
1436 /// @brief Dispatches a payload-keyed @p action on a shared @p binding,
1437 /// attaching it to the instance @p key names first.
1438 ///
1439 /// Waits for any bind in flight, then attaches (or re-points) the binding
1440 /// to @p key if it does not hold it already, and dispatches once the attach
1441 /// has settled. A refused attach rejects the call with its error; an empty
1442 /// @p key is refused at once with `std::invalid_argument`, leaving the
1443 /// binding as it was.
1444 /// @tparam Model Model type that owns the handler.
1445 /// @tparam Action Action type to dispatch.
1446 /// @param binding Shared binding.
1447 /// @param action Action to execute (moved in).
1448 /// @param cbExec Executor on which the `Completion` callbacks are posted.
1449 /// @param key Canonical primary key the action names.
1450 /// @return Completion that resolves with the typed result or an exception.
1451 template <typename Model, typename Action>
1453 const std::shared_ptr<detail::HandlerBinding>& binding, Action action, ::morph::exec::IExecutor* cbExec,
1454 std::string key) {
1456 MORPH_ZONE("Bridge::executeAttachedVia");
1457 note("Bridge::executeVia");
1458 if (key.empty()) {
1459 auto [refused, promise] = ::morph::async::Completion<R>::makeSettleable(cbExec);
1460 promise.reject(emptyKey());
1461 return std::move(refused);
1462 }
1463 auto sink = makeSink<Model, Action>({});
1464 ::morph::async::Completion<R> typed{sink, cbExec};
1465 armDeadline(sink);
1466 auto held = std::make_shared<Action>(std::move(action));
1467 dispatchAttached(binding, sink, std::move(key), [this, sink, cbExec, held](detail::HandlerBinding& bound) {
1468 dispatchNow<Model, Action>(bound, sink, std::move(*held), cbExec, /*held=*/true);
1469 });
1470 return typed;
1471 }
1472
1473 /// @brief Dispatches a result-keyed @p action on a shared @p binding,
1474 /// giving it an anonymous instance first if it has none, and
1475 /// promoting that instance under the key its result carries.
1476 /// @tparam Model Model type that owns the handler.
1477 /// @tparam Action Action type to dispatch.
1478 /// @param binding Shared binding.
1479 /// @param action Action to execute (moved in).
1480 /// @param cbExec Executor on which the `Completion` callbacks are posted.
1481 /// @return Completion that resolves with the typed result or an exception.
1482 template <typename Model, typename Action>
1484 const std::shared_ptr<detail::HandlerBinding>& binding, Action action, ::morph::exec::IExecutor* cbExec) {
1486 MORPH_ZONE("Bridge::executeCreatingVia");
1487 note("Bridge::executeVia");
1488 std::weak_ptr<detail::HandlerBinding> const weak{binding};
1489 auto sink = makeSink<Model, Action>([this, weak](const R& result) {
1490 if (auto strong = weak.lock()) {
1493 }
1494 });
1495 ::morph::async::Completion<R> typed{sink, cbExec};
1496 armDeadline(sink);
1497 auto held = std::make_shared<Action>(std::move(action));
1498 whenIdle(*binding, [this, weak, sink, cbExec, held](const std::exception_ptr& failure) {
1499 auto strong = weak.lock();
1500 if (rejectIfGone(sink, strong, failure)) {
1501 return;
1502 }
1503 if (strong->currentId.load() != 0U) {
1504 dispatchNow<Model, Action>(*strong, sink, std::move(*held), cbExec, /*held=*/true);
1505 return;
1506 }
1507 runSuperseded(startBind(strong,
1508 ::morph::backend::detail::BindRequest{.typeId = strong->typeId,
1509 .factory = strong->modelFactory,
1510 .contextKey = strong->contextKey,
1511 .primary = {},
1512 .current = {}},
1513 std::nullopt, [this, weak, sink, cbExec, held](const std::exception_ptr& settled) {
1514 auto bound = weak.lock();
1515 if (rejectIfGone(sink, bound, settled)) {
1516 return;
1517 }
1518 dispatchNow<Model, Action>(*bound, sink, std::move(*held), cbExec, true);
1519 }));
1520 });
1521 return typed;
1522 }
1523
1524private:
1525 /// @brief The call factory a raw dispatch hands `dispatchWith`: owns the
1526 /// ids and body, and builds the `ActionCall` when the call runs.
1527 /// @param binding The binding the call goes through; its type id is copied.
1528 /// @param actionId The action's registered type id.
1529 /// @param bodyJson The action's JSON body.
1530 /// @param sink The call's sink; its stop source goes on the call.
1531 /// @return A copyable callable returning the call.
1532 static auto rawCallMaker(const detail::HandlerBinding& binding, std::string actionId, std::string bodyJson,
1533 const std::shared_ptr<detail::BridgeSink<std::string>>& sink) {
1534 return [action = std::make_shared<detail::RawAction>(detail::RawAction{
1535 .modelTypeId = binding.typeId, .actionTypeId = std::move(actionId), .body = std::move(bodyJson)}),
1536 stop = sink->stopSource] { return makeRawCall(action, stop); };
1537 }
1538
1539 /// @brief Builds the `ActionCall` for one dispatch by id.
1540 ///
1541 /// The call owns @p action, and its id views point into it. On a local
1542 /// backend it runs the server's own JSON runner, which recomputes,
1543 /// validates and journals as a server does: `dispatch` for an ordinary
1544 /// handler, through `localOp`, and `dispatchAsync` for a Task handler,
1545 /// through `localOpAsync`. The dispatcher answers which one by id, so an
1546 /// ordinary handler does not pay for a Task run's resumer and strand
1547 /// enrolment.
1548 /// @param action The ids and body.
1549 /// @param stopSource The call's stop source; a Task handler on a local
1550 /// backend observes it.
1551 /// @return The call, without its session.
1552 static ::morph::backend::detail::ActionCall makeRawCall(std::shared_ptr<detail::RawAction> action,
1553 std::shared_ptr<::core::async::StopSource> stopSource) {
1554 ::morph::backend::detail::ActionCall call;
1555 call.modelTypeId = action->modelTypeId;
1556 call.actionTypeId = action->actionTypeId;
1557 call.serializeAction = [](const void* actionPtr) {
1558 return static_cast<const detail::RawAction*>(actionPtr)->body;
1559 };
1560 call.deserializeResult = [](std::string_view json) -> std::shared_ptr<void> {
1561 return std::make_shared<std::string>(json);
1562 };
1563 auto const& dispatcher = ::morph::model::detail::defaultDispatcher();
1564 if (!dispatcher.dispatchesAsync(action->modelTypeId, action->actionTypeId)) {
1565 // An unknown pair lands here too, and `dispatch` reports it.
1566 call.localOp = [](::morph::model::detail::IModelHolder& holder, void* actionPtr) -> std::shared_ptr<void> {
1567 auto const& raw = *static_cast<const detail::RawAction*>(actionPtr);
1568 return std::make_shared<std::string>(::morph::model::detail::defaultDispatcher().dispatch(
1569 raw.modelTypeId, raw.actionTypeId, holder, raw.body));
1570 };
1571 call.stopSource = std::move(stopSource);
1572 call.action = std::move(action);
1573 return call;
1574 }
1575 // The backend holds the action until `done` has run, so the views
1576 // `dispatchAsync` takes into it stay valid for the whole dispatch.
1577 // NOLINTNEXTLINE(performance-unnecessary-value-param): the signature is ActionCall::localOpAsync's
1578 call.localOpAsync = [](::morph::model::detail::IModelHolder& holder, std::shared_ptr<void> actionPtr,
1579 const std::shared_ptr<::morph::exec::detail::TaskResumer>& resumer,
1580 ::core::async::StopToken token, ::morph::backend::detail::ActionCall::LocalDone done) {
1581 auto const& raw = *static_cast<const detail::RawAction*>(actionPtr.get());
1582 ::morph::model::detail::defaultDispatcher().dispatchAsync(
1583 raw.modelTypeId, raw.actionTypeId, holder, raw.body, resumer, std::move(token),
1584 [done = std::move(done)](std::string result, const std::exception_ptr& err) {
1585 if (err) {
1586 done(nullptr, err);
1587 return;
1588 }
1589 // Runs from the coroutine's completion, where an escaping
1590 // throw terminates: a failed allocation fails the call.
1591 std::shared_ptr<void> boxed;
1592 try {
1593 boxed = std::make_shared<std::string>(std::move(result));
1594 } catch (...) {
1595 done(nullptr, std::current_exception());
1596 return;
1597 }
1598 done(std::move(boxed), nullptr);
1599 });
1600 };
1601 call.stopSource = std::move(stopSource);
1602 call.action = std::move(action);
1603 return call;
1604 }
1605
1606public:
1607 /// @brief Binds the model registered as @p typeId, for a caller compiled
1608 /// without the model's type.
1609 ///
1610 /// A private binding is bound at once, as `registerHandler<Model>()` binds
1611 /// one. A shared binding is bound when attached: here when @p instanceKey
1612 /// is not empty, otherwise by a later `attach`. The binding is tracked
1613 /// for `switchBackend()`, deregistered by `deregisterHandler`, and its
1614 /// instance is owned by the session's principal on a server exactly as a
1615 /// typed handler's is.
1616 ///
1617 /// Called off the owner — inside a running action, where a
1618 /// `BridgeHandler` may be constructed too — the registration is posted to
1619 /// the owner, and a call made meanwhile is held. Attaching is the owner's,
1620 /// so a key is refused there.
1621 ///
1622 /// The bridge cannot tell whether the model is keyed: a shared binding of
1623 /// an unkeyed model is refused, if at all, by the backend when attached.
1624 ///
1625 /// @param typeId The model's registered type id.
1626 /// @param sharing Private or shared.
1627 /// @param instanceKey For a shared binding, the key to attach to now, or
1628 /// empty to stay unattached. Must be empty for a
1629 /// private binding, and off the owner.
1630 /// @return The binding.
1631 /// @throws std::invalid_argument for a private binding with a key.
1632 /// @throws std::logic_error for a key given off the owner.
1633 std::shared_ptr<detail::HandlerBinding> bindByType(std::string typeId, BindSharing sharing,
1634 std::string instanceKey = {}) {
1635 if (sharing == BindSharing::Private && !instanceKey.empty()) {
1636 throw std::invalid_argument{"Bridge::bindByType: a private binding takes no instance key"};
1637 }
1638 if (!instanceKey.empty() && !onOwner()) {
1639 throw std::logic_error{"Bridge::bindByType: attach on the owner, not inside an action"};
1640 }
1641 auto binding = makeBindingByType(std::move(typeId), sharing == BindSharing::Shared);
1642 adoptHandler(binding);
1643 if (!instanceKey.empty()) {
1644 attach(binding, std::move(instanceKey));
1645 }
1646 return binding;
1647 }
1648
1649 /// @brief Dispatches the action registered as @p actionId with a JSON
1650 /// body through @p binding, and resolves with the JSON reply.
1651 ///
1652 /// The call is stamped with the default session, raced against the
1653 /// execute deadline, counted in `pendingCalls()`, held while a bind is in
1654 /// flight, and settled by `cancelPending` exactly as a typed call. It
1655 /// always carries a stop source, since whether the handler is a Task is
1656 /// not known here. Neither the client-side recompute nor the action's
1657 /// validator runs here: they need the action's type, and whoever runs the
1658 /// model runs both. The reply is not offered to typed subscriptions: it is
1659 /// JSON text, not the result a subscriber expects.
1660 ///
1661 /// The call runs on the instance @p binding holds now. Unlike a typed
1662 /// call, it does not attach to the key a payload-keyed action names, nor
1663 /// create and promote an instance for a result-keyed one: a keyed action
1664 /// sent through a shared binding attached elsewhere runs on that other
1665 /// instance, and one sent before any attach is rejected as not bound.
1666 /// A keyed action goes through `executeRawOn`.
1667 ///
1668 /// @param binding A binding from `bindByType`.
1669 /// @param actionId The action's registered type id.
1670 /// @param bodyJson The action's JSON body.
1671 /// @param cbExec Executor the `Completion` callbacks are posted on.
1672 /// @return Completion resolving with the reply's JSON text.
1673 ::morph::async::Completion<std::string> executeRaw(const std::shared_ptr<detail::HandlerBinding>& binding,
1674 std::string actionId, std::string bodyJson,
1675 ::morph::exec::IExecutor* cbExec) {
1676 MORPH_ZONE("Bridge::executeRaw");
1677 note("Bridge::executeRaw");
1678 auto sink = makeSinkFor<std::string>({}, /*stoppable=*/true, detail::Publish::No);
1679 ::morph::async::Completion<std::string> typed{sink, cbExec};
1680 armDeadline(sink);
1681 auto makeCall = rawCallMaker(*binding, std::move(actionId), std::move(bodyJson), sink);
1682 if (!binding->bindInFlight) {
1683 dispatchWith(*binding, sink, makeCall, cbExec, /*held=*/false);
1684 return typed;
1685 }
1686 std::weak_ptr<detail::HandlerBinding> const weak{binding};
1687 binding->waiting.emplace_back([this, weak, sink, cbExec, makeCall](const std::exception_ptr& failure) {
1688 auto strong = weak.lock();
1689 if (failure || !strong) {
1690 sink->settleException(failure ? failure
1691 : std::make_exception_ptr(::morph::backend::HandlerDestroyedError{}));
1692 return;
1693 }
1694 dispatchWith(*strong, sink, makeCall, cbExec, /*held=*/true);
1695 });
1696 return typed;
1697 }
1698
1699 /// @brief `executeRaw`, cancellable through @p stop.
1700 ///
1701 /// A stop before the call settles rejects it with
1702 /// `core::async::OperationCancelled` and asks its stop source to stop; a
1703 /// Task handler on a local backend sees that at its next stop-aware
1704 /// `co_await`. A token already stopped rejects without dispatching.
1705 /// @param binding A binding from `bindByType`.
1706 /// @param actionId The action's registered type id.
1707 /// @param bodyJson The action's JSON body.
1708 /// @param cbExec Executor the `Completion` callbacks are posted on.
1709 /// @param stop The caller's cancel.
1710 /// @return Completion resolving with the reply's JSON text, or rejected
1711 /// with `core::async::OperationCancelled`.
1712 ::morph::async::Completion<std::string> executeRaw(const std::shared_ptr<detail::HandlerBinding>& binding,
1713 std::string actionId, std::string bodyJson,
1715 ::core::async::StopToken stop) {
1716 if (stop.stop_requested()) {
1717 auto [cancelled, promise] = ::morph::async::Completion<std::string>::makeSettleable(cbExec);
1718 promise.reject(std::make_exception_ptr(::core::async::OperationCancelled{}));
1719 return std::move(cancelled);
1720 }
1721 auto completion = executeRaw(binding, std::move(actionId), std::move(bodyJson), cbExec);
1722 // Never empty: every path of `executeRaw` returns a completion built
1723 // on a state, a rejected one included.
1724 completion.state()->linkCancel(std::move(stop));
1725 return completion;
1726 }
1727
1728 /// @brief `executeRaw` on the instance for @p instanceKey: attaches the
1729 /// shared @p binding to it first when it holds another or none.
1730 ///
1731 /// What a keyed action needs from a caller without its type, which cannot
1732 /// read the key out of the body: the call runs on @p instanceKey's
1733 /// instance or not at all. An attach that is refused, or superseded by a
1734 /// re-bind, rejects the call with its failure instead of running it on the
1735 /// instance the binding fell back to. The binding stays attached to
1736 /// @p instanceKey afterwards, as a typed payload-keyed call leaves it.
1737 /// Otherwise as `executeRaw`.
1738 /// @param binding A shared binding from `bindByType`.
1739 /// @param instanceKey Canonical string encoding of the primary key; see
1740 /// `attach`.
1741 /// @param actionId The action's registered type id.
1742 /// @param bodyJson The action's JSON body.
1743 /// @param cbExec Executor the `Completion` callbacks are posted on.
1744 /// @return Completion resolving with the reply's JSON text.
1745 /// @throws std::invalid_argument for an empty @p instanceKey.
1746 /// @throws std::logic_error for a private @p binding.
1747 ::morph::async::Completion<std::string> executeRawOn(const std::shared_ptr<detail::HandlerBinding>& binding,
1748 std::string instanceKey, std::string actionId,
1749 std::string bodyJson, ::morph::exec::IExecutor* cbExec) {
1750 if (instanceKey.empty()) {
1751 throw std::invalid_argument{"Bridge::executeRawOn: the instance key is empty"};
1752 }
1753 if (!binding->shared) {
1754 throw std::logic_error{"Bridge::executeRawOn: the binding is private"};
1755 }
1756 MORPH_ZONE("Bridge::executeRawOn");
1757 note("Bridge::executeRawOn");
1758 auto sink = makeSinkFor<std::string>({}, /*stoppable=*/true, detail::Publish::No);
1759 ::morph::async::Completion<std::string> typed{sink, cbExec};
1760 armDeadline(sink);
1761 auto makeCall = rawCallMaker(*binding, std::move(actionId), std::move(bodyJson), sink);
1762 dispatchAttached(binding, sink, std::move(instanceKey),
1763 [this, sink, cbExec, makeCall](detail::HandlerBinding& bound) {
1764 dispatchWith(bound, sink, makeCall, cbExec, /*held=*/true);
1765 });
1766 return typed;
1767 }
1768
1769private:
1770 template <typename, typename>
1771 friend class BridgeHandler;
1772 friend class RawHandler;
1773
1774 using Resume = std::function<void(std::exception_ptr)>;
1775
1776 /// @brief Checks, in a debug build, that the caller is on the owner.
1777 /// @param site Name of the calling body.
1778 void note(char const* site) const noexcept { _affinity.note(site); }
1779
1780 /// @brief Weak observer of this bridge's lifetime. Every continuation that
1781 /// touches the bridge checks it first; on the owner the check is
1782 /// exact, because `~Bridge` runs there too.
1783 /// @return A token that expires when the bridge is destroyed.
1784 [[nodiscard]] ::morph::async::CallbackToken liveness() const { return _callbacks.token(); }
1785
1786 /// @brief The affinity every handler of this bridge checks itself against.
1787 /// @return A copy of the bridge's owner affinity.
1788 [[nodiscard]] ::morph::exec::detail::OwnerAffinity affinity() const noexcept { return _affinity; }
1789
1790 /// @brief Builds a binding for `Model`, not yet tracked or bound.
1791 /// @tparam Model Concrete model type.
1792 /// @param shared Whether the binding joins the shared directory.
1793 /// @return The new binding.
1794 template <typename Model>
1795 static std::shared_ptr<detail::HandlerBinding> makeBinding(bool shared) {
1796 auto binding = std::make_shared<detail::HandlerBinding>();
1797 binding->typeId = std::string{::morph::model::ModelTraits<Model>::typeId()};
1798 binding->modelFactory = [] { return ::morph::model::detail::ModelFactory::create<Model>(); };
1799 binding->shared = shared;
1800 return binding;
1801 }
1802
1803 /// @brief Builds a binding for the model registered as @p typeId, not yet
1804 /// tracked or bound. Its factory resolves @p typeId in the process
1805 /// registry when a local backend binds it; a remote backend sends
1806 /// the id and ignores the factory.
1807 /// @param typeId The model's registered type id.
1808 /// @param shared Whether the binding joins the shared directory.
1809 /// @return The new binding.
1810 static std::shared_ptr<detail::HandlerBinding> makeBindingByType(std::string typeId, bool shared) {
1811 auto binding = std::make_shared<detail::HandlerBinding>();
1812 binding->modelFactory = [typeId] { return ::morph::model::detail::defaultRegistry().create(typeId); };
1813 binding->typeId = std::move(typeId);
1814 binding->shared = shared;
1815 return binding;
1816 }
1817
1818 /// @brief Takes a handler's binding into this bridge: at once on the owner;
1819 /// off it — a handler constructed inside a running action — by
1820 /// posting the registration to the owner.
1821 ///
1822 /// Posted, the binding is marked as binding until the task runs, so a call
1823 /// made through it on the owner meanwhile is held rather than refused.
1824 /// @param binding The handler's binding; nothing else has seen it yet.
1825 void adoptHandler(const std::shared_ptr<detail::HandlerBinding>& binding) {
1826 if (onOwner()) {
1827 adoptOnOwner(binding);
1828 return;
1829 }
1830 binding->bindInFlight = true;
1831 _affinity.owner().post([this, token = _callbacks.token(), binding] {
1832 if (!token.active()) {
1833 return;
1834 }
1835 binding->bindInFlight = false;
1836 adoptOnOwner(binding);
1837 });
1838 }
1839
1840 /// @brief `adoptHandler`'s body, on the owner.
1841 /// @param binding The handler's binding.
1842 void adoptOnOwner(const std::shared_ptr<detail::HandlerBinding>& binding) {
1843 if (binding->shared) {
1844 note("Bridge::registerSharedHandler");
1845 _handlers.push_back(binding);
1846 drainWaiting(*binding);
1847 return;
1848 }
1849 registerHandler(binding);
1850 }
1851
1852 /// @brief Runs @p resume now when no bind is in flight for @p binding,
1853 /// or holds it until one settles.
1854 /// @param binding The binding.
1855 /// @param resume Resumed with null to proceed, or an error to give up.
1856 static void whenIdle(detail::HandlerBinding& binding, Resume resume) {
1857 if (binding.bindInFlight) {
1858 binding.waiting.push_back(std::move(resume));
1859 return;
1860 }
1861 resume(nullptr);
1862 }
1863
1864 /// @brief Resumes the calls held by @p binding while no bind is in flight.
1865 /// A resumed call may issue a bind of its own, which holds the rest.
1866 /// @param binding The binding.
1867 static void drainWaiting(detail::HandlerBinding& binding) {
1868 while (!binding.bindInFlight && !binding.waiting.empty()) {
1869 auto next = std::move(binding.waiting.front());
1870 binding.waiting.pop_front();
1871 next(nullptr);
1872 }
1873 }
1874
1875 /// @brief Moves everything @p binding holds — the in-flight operation's
1876 /// continuation, then the held calls — into @p out, to be rejected
1877 /// once the bridge's own state is consistent.
1878 /// @param binding The binding.
1879 /// @param out Where the continuations go.
1880 static void takeHeld(detail::HandlerBinding& binding, std::vector<Resume>& out) {
1881 if (auto own = std::exchange(binding.afterBind, {})) {
1882 out.push_back(std::move(own));
1883 }
1884 for (auto& resume : std::exchange(binding.waiting, {})) {
1885 out.push_back(std::move(resume));
1886 }
1887 }
1888
1889 /// @brief Rejects an operation's continuation that a newer bind replaced.
1890 /// @param superseded The continuation, or empty.
1891 static void runSuperseded(const Resume& superseded) {
1892 if (superseded) {
1893 superseded(std::make_exception_ptr(std::runtime_error{"bind superseded by a newer one"}));
1894 }
1895 }
1896
1897 /// @brief Whether @p binding is shared and holds neither a key nor an
1898 /// instance, so a switch or reconnect has nothing to re-create.
1899 ///
1900 /// Both are asked: a key with no instance is a binding whose re-bind is
1901 /// still owed, and an instance with no key is a result-keyed one not yet
1902 /// promoted, whose id belongs to the backend it was bound on.
1903 /// @param binding The binding.
1904 /// @return `true` for a shared binding with an empty primary and no id.
1905 [[nodiscard]] static bool holdsNothing(const detail::HandlerBinding& binding) {
1906 return binding.shared && binding.primary.empty() && binding.currentId.load() == 0U;
1907 }
1908
1909 /// @brief The error a call gets through a binding with no instance and no
1910 /// bind to wait for: an `AllowShared` handler never attached.
1911 /// @return The error.
1912 [[nodiscard]] static std::exception_ptr notBound() {
1913 return std::make_exception_ptr(std::runtime_error("handler not bound"));
1914 }
1915
1916 /// @brief Dispatches through @p binding once it holds the instance for
1917 /// @p key, attaching it first when it holds another or none.
1918 ///
1919 /// Waits for any bind in flight. An attach that leaves the binding on
1920 /// another key — refused, or superseded by a re-bind — rejects the call
1921 /// with the attach's failure rather than dispatching to the instance the
1922 /// binding fell back to.
1923 /// @tparam R The sink's result type.
1924 /// @tparam Dispatch Callable `void(detail::HandlerBinding&)`, run once on
1925 /// the owner with the attached binding.
1926 /// @param binding Shared binding.
1927 /// @param sink The call's sink, settled here when the call cannot run.
1928 /// @param key Canonical primary key the call must run on.
1929 /// @param dispatch Dispatches the call through the attached binding.
1930 template <typename R, typename Dispatch>
1931 void dispatchAttached(const std::shared_ptr<detail::HandlerBinding>& binding,
1932 const std::shared_ptr<detail::BridgeSink<R>>& sink, std::string key,
1933 const Dispatch& dispatch) {
1934 std::weak_ptr<detail::HandlerBinding> const weak{binding};
1935 whenIdle(*binding, [this, weak, sink, key = std::move(key), dispatch](const std::exception_ptr& failure) {
1936 auto strong = weak.lock();
1937 if (rejectIfGone(sink, strong, failure)) {
1938 return;
1939 }
1940 if (strong->primary == key && strong->currentId.load() != 0U) {
1941 dispatch(*strong);
1942 return;
1943 }
1944 runSuperseded(startBind(strong, attachRequest(*strong, key), key,
1945 [weak, sink, key, dispatch](const std::exception_ptr& settled) {
1946 auto bound = weak.lock();
1947 if (rejectIfGone(sink, bound, settled)) {
1948 return;
1949 }
1950 // An attach that left the binding on another key must not
1951 // dispatch there. One that kept the key but bound no
1952 // instance is rejected by the dispatch itself.
1953 if (bound->primary != key) {
1954 sink->settleException(bound->bindFailure ? bound->bindFailure
1955 : notBound());
1956 return;
1957 }
1958 dispatch(*bound);
1959 }));
1960 });
1961 }
1962
1963 /// @brief Rejects @p sink when a held operation cannot go on.
1964 /// @tparam Sink The sink's type.
1965 /// @param sink The call's sink.
1966 /// @param binding The binding, or null once its handler is gone.
1967 /// @param failure The error the operation was resumed with, or null.
1968 /// @return Whether @p sink was rejected.
1969 template <typename Sink>
1970 static bool rejectIfGone(const Sink& sink, const std::shared_ptr<detail::HandlerBinding>& binding,
1971 const std::exception_ptr& failure) {
1972 if (failure) {
1973 sink->settleException(failure);
1974 return true;
1975 }
1976 if (!binding) {
1977 sink->settleException(std::make_exception_ptr(::morph::backend::HandlerDestroyedError{}));
1978 return true;
1979 }
1980 return false;
1981 }
1982
1983 /// @brief The bind request that re-creates @p binding's instance on a
1984 /// backend: private, or the shared one for its primary.
1985 /// @param binding The binding.
1986 /// @return The request.
1987 [[nodiscard]] static ::morph::backend::detail::BindRequest rebindRequest(const detail::HandlerBinding& binding) {
1988 return ::morph::backend::detail::BindRequest{.typeId = binding.typeId,
1989 .factory = binding.modelFactory,
1990 .contextKey = binding.contextKey,
1991 .primary = binding.shared ? binding.primary : std::string{},
1992 .current = {}};
1993 }
1994
1995 /// @brief The bind request that attaches (or re-points) @p binding to @p key.
1996 /// @param binding The binding.
1997 /// @param key Canonical primary key.
1998 /// @return The request.
1999 [[nodiscard]] static ::morph::backend::detail::BindRequest attachRequest(const detail::HandlerBinding& binding,
2000 const std::string& key) {
2001 return ::morph::backend::detail::BindRequest{
2002 .typeId = binding.typeId,
2003 .factory = binding.modelFactory,
2004 .contextKey = key,
2005 .primary = key,
2006 .current = ::morph::exec::detail::ModelId{binding.currentId.load()}};
2007 }
2008
2009 /// @brief Issues @p request for @p binding on the active backend, with the
2010 /// owner as the executor its reply is delivered on.
2011 ///
2012 /// A backend that settles before returning has its outcome applied here;
2013 /// otherwise the continuation applies it on the owner, gated on the
2014 /// bridge's token. @p afterBind runs when the bind settles, before the calls
2015 /// held for it.
2016 /// @param binding The binding the bind is for.
2017 /// @param request Owning bind request.
2018 /// @param identity The key to publish as the binding's primary and
2019 /// context key on success, or none.
2020 /// @param afterBind The issuing operation's continuation, or empty.
2021 /// @return A continuation this bind superseded, for the caller to reject
2022 /// once its own state is consistent; usually empty.
2023 [[nodiscard]] Resume startBind(const std::shared_ptr<detail::HandlerBinding>& binding,
2024 ::morph::backend::detail::BindRequest request,
2025 const std::optional<std::string>& identity, Resume afterBind) {
2026 auto superseded = std::exchange(binding->afterBind, std::move(afterBind));
2027 auto const generation = ++binding->bindGeneration;
2028 binding->bindInFlight = true;
2029 binding->bindFailure = nullptr;
2030 auto backend = _backend;
2031 ::morph::async::Completion<::morph::exec::detail::ModelId> completion;
2032 try {
2033 completion = backend->bindModel(std::move(request), _affinity.owner());
2034 } catch (...) {
2035 applyBind(binding, backend, generation, {}, std::current_exception(), identity);
2036 return superseded;
2037 }
2038 if (auto settled = detail::takeSettled(completion)) {
2039 applyBind(binding, backend, generation, settled->id, settled->failure, identity);
2040 return superseded;
2041 }
2042 awaitBind(binding, backend, generation, std::move(completion), identity);
2043 return superseded;
2044 }
2045
2046 /// @brief Applies a bind's outcome when its reply arrives, on the owner.
2047 /// @param binding The binding the bind is for.
2048 /// @param backend The backend that will settle it.
2049 /// @param generation The binding's bind count when it was issued.
2050 /// @param completion The bind's result.
2051 /// @param identity What to publish as the binding's primary on success.
2052 void awaitBind(const std::shared_ptr<detail::HandlerBinding>& binding,
2053 const std::shared_ptr<::morph::backend::detail::IBackend>& backend, std::uint64_t generation,
2054 ::morph::async::Completion<::morph::exec::detail::ModelId> completion,
2055 const std::optional<std::string>& identity) {
2056 std::weak_ptr<detail::HandlerBinding> const weak{binding};
2057 std::weak_ptr<::morph::backend::detail::IBackend> const weakBackend{backend};
2058 auto const token = _callbacks.token();
2059 completion
2060 .then([this, token, weak, weakBackend, generation, identity](::morph::exec::detail::ModelId mid) {
2061 if (!token.active()) {
2062 // The bridge is gone: nothing will use this instance.
2063 if (auto owner = weakBackend.lock()) {
2064 owner->deregisterModel(mid);
2065 }
2066 return;
2067 }
2068 applyBind(weak.lock(), weakBackend.lock(), generation, mid, nullptr, identity);
2069 })
2070 .onError([this, token, weak, weakBackend, generation, identity](const std::exception_ptr& failure) {
2071 if (token.active()) {
2072 applyBind(weak.lock(), weakBackend.lock(), generation, {}, failure, identity);
2073 }
2074 });
2075 }
2076
2077 /// @brief Applies one bind outcome to @p binding, on the owner.
2078 ///
2079 /// A bind whose binding is gone, or whose count a newer bind has passed,
2080 /// releases a successful id on the backend that issued it instead. The
2081 /// current one publishes the id (and @p identity) or records the failure,
2082 /// then runs the issuing operation's continuation and the held calls.
2083 /// @param binding The binding, or null once its handler is gone.
2084 /// @param backend The backend that settled it, or null once it is gone.
2085 /// @param generation The binding's bind count when it was issued.
2086 /// @param mid The bound instance, on success.
2087 /// @param failure The failure, or null.
2088 /// @param identity What to publish as the binding's primary on success.
2089 void applyBind(const std::shared_ptr<detail::HandlerBinding>& binding,
2090 const std::shared_ptr<::morph::backend::detail::IBackend>& backend, std::uint64_t generation,
2091 ::morph::exec::detail::ModelId mid, const std::exception_ptr& failure,
2092 const std::optional<std::string>& identity) {
2093 note("Bridge::applyBind");
2094 if (!binding || binding->bindGeneration != generation) {
2095 if (!failure && mid.v != 0U && backend) {
2096 backend->deregisterModel(mid);
2097 }
2098 return;
2099 }
2100 binding->bindInFlight = false;
2101 if (failure) {
2102 binding->bindFailure = failure;
2103 ::morph::log::logError("[bridge] bind of '" + binding->typeId +
2104 "' failed: " + detail::describeFailure(failure));
2105 } else {
2106 binding->currentId.store(mid.v);
2107 if (identity) {
2108 binding->contextKey = *identity;
2109 binding->primary = *identity;
2110 }
2111 }
2112 if (auto own = std::exchange(binding->afterBind, {})) {
2113 own(nullptr);
2114 }
2115 drainWaiting(*binding);
2116 }
2117
2118 /// @brief Applies a promote's outcome, on the owner.
2119 /// @param weak The binding.
2120 /// @param weakBackend The backend the promote went to.
2121 /// @param failure The failure, or null.
2122 /// @param primary The key the instance was filed under.
2123 void applyPromote(const std::weak_ptr<detail::HandlerBinding>& weak,
2124 const std::weak_ptr<::morph::backend::detail::IBackend>& weakBackend,
2125 const std::exception_ptr& failure, const std::string& primary) {
2126 note("Bridge::assignHandlerPrimary");
2127 auto binding = weak.lock();
2128 if (!binding) {
2129 return;
2130 }
2131 if (failure) {
2132 ::morph::log::logError("[assignHandlerPrimary] promotion of '" + binding->typeId +
2133 "' failed: " + detail::describeFailure(failure));
2134 return;
2135 }
2136 // A switch that moved past this promotion re-created the instance; a
2137 // binding already keyed by an attach that raced ahead keeps its key.
2138 if (weakBackend.lock() != _backend || !binding->primary.empty()) {
2139 return;
2140 }
2141 binding->contextKey = primary;
2142 binding->primary = primary;
2143 }
2144
2145 /// @brief Releases every instance a failed switch acquired on @p backend,
2146 /// now or when its bind settles.
2147 /// @tparam Issued The switch's record of one bind.
2148 /// @param backend The backend the switch was going to.
2149 /// @param issued The binds issued.
2150 template <typename Issued>
2151 static void rollback(const std::shared_ptr<::morph::backend::detail::IBackend>& backend,
2152 std::vector<Issued>& issued) {
2153 for (auto& entry : issued) {
2154 if (entry.settled) {
2155 if (!entry.settled->failure) {
2156 try {
2157 backend->deregisterModel(entry.settled->id);
2158 } catch (const std::exception& exc) {
2159 ::morph::log::logError(std::string{"[switchBackend] rollback deregister failed: "} +
2160 exc.what());
2161 }
2162 }
2163 continue;
2164 }
2165 std::weak_ptr<::morph::backend::detail::IBackend> const weak{backend};
2166 entry.completion
2167 .then([weak](::morph::exec::detail::ModelId mid) {
2168 if (auto owner = weak.lock()) {
2169 owner->deregisterModel(mid);
2170 }
2171 })
2172 .onError([](const std::exception_ptr&) {});
2173 }
2174 }
2175
2176 /// @brief Builds the sink for one call: counts it pending and gives a
2177 /// Task-handler call its stop source. The deadline is armed
2178 /// separately, by `armDeadline`, once the call's `Completion` exists.
2179 /// @tparam Model Model type.
2180 /// @tparam Action Action type.
2181 /// @param onResult Optional owner-side observer of the result.
2182 /// @return The sink.
2183 template <typename Model, typename Action>
2184 std::shared_ptr<detail::BridgeSink<typename ::morph::model::ActionTraits<Action>::Result>> makeSink(
2185 std::function<void(const typename ::morph::model::ActionTraits<Action>::Result&)> onResult) {
2186 return makeSinkFor(std::move(onResult), isTaskHandlerCall<Model, Action>(), detail::Publish::Typed);
2187 }
2188
2189 /// @brief `makeSink`'s body, for a caller that decides the stop source and
2190 /// the publish policy itself.
2191 /// @tparam R Result type of the call.
2192 /// @param onResult Optional owner-side observer of the result.
2193 /// @param stoppable Whether the call gets a stop source.
2194 /// @param publish Whether the result is offered to the typed subscriptions.
2195 /// @return The sink.
2196 template <typename R>
2197 std::shared_ptr<detail::BridgeSink<R>> makeSinkFor(std::function<void(const R&)> onResult, bool stoppable,
2198 detail::Publish publish) {
2199 auto sink = std::make_shared<detail::BridgeSink<R>>(std::move(onResult), _pendingCalls, _subscriptions,
2200 _callbacks.token(), _affinity, publish);
2201 _pendingCalls->fetch_add(1, std::memory_order_relaxed);
2202 if (stoppable) {
2203 sink->stopSource = std::make_shared<::core::async::StopSource>();
2204 }
2205 return sink;
2206 }
2207
2208 /// @brief Arms the execute deadline on @p sink, when one is set.
2209 ///
2210 /// Called after the call's `Completion` is constructed: that constructor
2211 /// writes the state's callback executor, and a short deadline firing first
2212 /// would read it from the timeout thread while it is still being written.
2213 /// @tparam R Result type of the call.
2214 /// @param sink The call's sink; its stop source, if any, is requested on
2215 /// expiry.
2216 template <typename R>
2217 void armDeadline(const std::shared_ptr<detail::BridgeSink<R>>& sink) {
2218 if (_executeDeadline.count() <= 0 || !_timeoutScheduler) {
2219 return;
2220 }
2221 // The callback settles the *state*, not the sink: a deadline is not
2222 // one of the two resolution paths and must not decrement
2223 // `_pendingCalls`, which stays counted until the real reply lands
2224 // (or `dispatchNow` abandons a call the deadline settled while it
2225 // waited for its bind). It captures the sink alone, never `this`.
2226 auto const handle = _timeoutScheduler->schedule(_executeDeadline, [sink, stopSource = sink->stopSource] {
2227 sink->setException(std::make_exception_ptr(::morph::backend::ClientTimeoutError{}));
2228 if (stopSource) {
2229 static_cast<void>(stopSource->request_stop());
2230 }
2231 });
2232 sink->armDeadline(handle, _timeoutScheduler);
2233 }
2234
2235 /// @brief Whether `Model`'s handler for `Action` returns a `Task`.
2236 /// @tparam Model Model type.
2237 /// @tparam Action Action type.
2238 /// @return True for a coroutine handler.
2239 template <typename Model, typename Action>
2240 static consteval bool isTaskHandlerCall() {
2241#ifndef MORPH_CLIENT_ONLY
2242 return ::morph::model::isTaskHandler<decltype(std::declval<Model&>().execute(std::declval<Action&>()))>;
2243#else
2244 // A client-only build never runs a handler locally (see makeActionCall).
2245 return false;
2246#endif
2247 }
2248
2249 /// @brief Dispatches @p action through @p binding's instance now, or
2250 /// rejects the call when the binding has none.
2251 /// @tparam Model Model type.
2252 /// @tparam Action Action type.
2253 /// @param binding The binding.
2254 /// @param sink The call's sink.
2255 /// @param action The action, moved in.
2256 /// @param cbExec The caller's executor.
2257 /// @param held Whether the call waited for a bind: its caller has
2258 /// returned, so a throw from the backend rejects the call
2259 /// instead of propagating.
2260 template <typename Model, typename Action>
2261 void dispatchNow(
2262 detail::HandlerBinding& binding,
2263 const std::shared_ptr<detail::BridgeSink<typename ::morph::model::ActionTraits<Action>::Result>>& sink,
2264 Action action, ::morph::exec::IExecutor* cbExec, bool held) {
2265 dispatchWith(
2266 binding, sink, [&] { return makeActionCall<Model, Action>(std::move(action), sink->stopSource); }, cbExec,
2267 held);
2268 }
2269
2270 /// @brief `dispatchNow`'s body, for any way of building the call.
2271 /// @tparam R Result type of the call.
2272 /// @tparam MakeCall Callable returning the `ActionCall`, without its session.
2273 /// @param binding The binding.
2274 /// @param sink The call's sink.
2275 /// @param makeCall Builds the call; invoked at most once, and only when
2276 /// the call is dispatched.
2277 /// @param cbExec The caller's executor.
2278 /// @param held Whether the call waited for a bind: its caller has
2279 /// returned, so a throw from the backend rejects the call
2280 /// instead of propagating.
2281 template <typename R, typename MakeCall>
2282 void dispatchWith(detail::HandlerBinding& binding, const std::shared_ptr<detail::BridgeSink<R>>& sink,
2283 MakeCall&& makeCall, ::morph::exec::IExecutor* cbExec, bool held) {
2284 // Its own zone, not part of `executeVia`'s: a call that waited for a
2285 // bind is dispatched later, from the bind's settle.
2286 MORPH_ZONE("Bridge::dispatchNow");
2287 MORPH_ZONE_TEXT(_defaultSession.requestId);
2288 note("Bridge::dispatch");
2289 auto const raw = binding.currentId.load();
2290 if (raw == 0U) {
2291 sink->settleException(binding.bindFailure ? binding.bindFailure : notBound());
2292 return;
2293 }
2294 if (held && sink->alreadySettled()) {
2295 sink->abandon();
2296 return;
2297 }
2298 sink->target(::morph::exec::detail::ModelId{raw});
2299 auto call = std::forward<MakeCall>(makeCall)();
2300 call.session = _defaultSession;
2301 try {
2302 _backend->executeInto(::morph::exec::detail::ModelId{raw}, std::move(call), cbExec, sink);
2303 } catch (...) {
2304 if (held) {
2305 sink->settleException(std::current_exception());
2306 return;
2307 }
2308 // Undoes the pending count and the deadline together, through the
2309 // same settle-once latch the resolution paths use.
2310 sink->abandon();
2311 throw;
2312 }
2313 }
2314
2315 /// @brief Builds the `ActionCall` for one dispatch of @p action.
2316 /// @tparam Model Model type.
2317 /// @tparam Action Action type.
2318 /// @param action The action, moved in.
2319 /// @param stopSource The call's stop source, for a Task handler; else null.
2320 /// @return The call, without its session.
2321 template <typename Model, typename Action>
2322 // One block: the lambdas below are the whole of the local path's contract
2323 // (recompute, validate, execute, journal), kept in the order they run.
2324 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
2325 static ::morph::backend::detail::ActionCall makeActionCall(Action action,
2326 std::shared_ptr<::core::async::StopSource> stopSource) {
2327 using R = ::morph::model::ActionTraits<Action>::Result;
2328 constexpr bool taskHandler = isTaskHandlerCall<Model, Action>();
2329 ::morph::backend::detail::ActionCall call;
2330 // Views of `constexpr` string literals, and stateless operations
2331 // addressed rather than copied: this whole block allocates exactly
2332 // once (the action itself). Copying instead would cost four
2333 // allocations per call -- two `std::string` copies of compile-time
2334 // constants and two `std::function`s whose `shared_ptr` capture
2335 // defeats libstdc++'s small-object buffer -- including on the
2336 // `LocalBackend` calls that never look at `serializeAction` or
2337 // `deserializeResult`. See `backend::detail::ActionCall` for the
2338 // lifetime contract this shape carries.
2339 call.modelTypeId = ::morph::model::ModelTraits<Model>::typeId();
2340 call.actionTypeId = ::morph::model::ActionTraits<Action>::typeId();
2341 auto sharedAction = std::make_shared<Action>(std::move(action));
2342 call.action = sharedAction;
2343 call.serializeAction = [](const void* actionPtr) {
2344 MORPH_ZONE("ActionTraits::toJson");
2345 return ::morph::model::ActionTraits<Action>::toJson(*static_cast<const Action*>(actionPtr));
2346 };
2347 call.deserializeResult = [](std::string_view jsonStr) -> std::shared_ptr<void> {
2348 MORPH_ZONE("ActionTraits::resultFromJson");
2349 return std::make_shared<R>(::morph::model::ActionTraits<Action>::resultFromJson(jsonStr));
2350 };
2351 if constexpr (taskHandler) {
2352 call.localOpAsync = &detail::localTaskOp<Model, Action>;
2353 call.stopSource = std::move(stopSource);
2354 } else {
2355 call.localOp = [](::morph::model::detail::IModelHolder& holder, void* actionPtr) -> std::shared_ptr<void> {
2356 // The action `ActionCall::action` owns, handed back typed. The
2357 // backend that invokes this keeps that handle alive across the
2358 // call (LocalBackend carries it onto the strand with `localOp`).
2359 Action& actionRef = *static_cast<Action*>(actionPtr);
2360 // Enforce the action's validator on the local execution path too, so
2361 // a caller that constructs an Action by hand and calls
2362 // BridgeHandler<Model>::execute<Action>() directly is rejected the
2363 // same way a hand-built wire envelope is rejected by
2364 // ActionDispatcher::registerAction's runner (registry.hpp). No JSON is
2365 // involved on this path, so there is no declared-precision
2366 // reconciliation step here (that only applies to decoded wire
2367 // payloads); the Quantity fields carry whatever precision the caller
2368 // constructed them with. ActionValidator<Action>::ready defaults to
2369 // `true` for actions with no validator, so this is a no-op for
2370 // unvalidated actions (zero behavior change). The thrown exception is
2371 // caught by LocalBackend::execute's strand task (backend.hpp) and
2372 // resolves this Completion through onError.
2373 //
2374 // Overwrite any computed fields from their declared inputs before the
2375 // validator runs and the model ever sees the action -- the same
2376 // authoritative recompute ActionDispatcher::registerAction's runner
2377 // performs for remote topologies (registry.hpp), applied here for the
2378 // in-process LocalBackend path (every execute<Action>()/executeJson
2379 // call). Recompute must run before the validator check so a validator
2380 // inspecting a computed field sees the authoritative value, not
2381 // whatever the caller constructed the action with. No-op for actions
2382 // with no computedFields. See docs/spec/forms/forms.md.
2383 ::morph::forms::recomputeAll(actionRef);
2385 throw ::morph::model::ValidationError{::morph::model::ModelTraits<Model>::typeId(),
2386 ::morph::model::ActionTraits<Action>::typeId()};
2387 }
2388#ifdef MORPH_CLIENT_ONLY
2389 // A MORPH_CLIENT_ONLY build never links Model::execute's definition
2390 // (see docs/spec/core/registry.md, "MORPH_CLIENT_ONLY") -- this
2391 // #ifdef, not just the registration macros, is what actually makes
2392 // that true: ActionCall::localOp is constructed unconditionally
2393 // here regardless of which backend ends up installed, so the
2394 // `model.execute(...)` call below would otherwise still force the
2395 // linker to resolve it even for a build that only ever installs a
2396 // remote backend. LocalBackend must not be used in such a build;
2397 // reaching this point means it was anyway.
2398 static_cast<void>(holder);
2399 throw std::logic_error(
2400 "Bridge::executeVia: localOp invoked in a MORPH_CLIENT_ONLY build -- LocalBackend must not be "
2401 "used");
2402#else
2403 auto& model = holder.template into<Model>();
2404 // Local mode has no client/server split, so this is the same execution
2405 // site `ActionDispatcher::registerAction`'s runner is for remote modes
2406 // (registry.hpp) — see that overload's doc comment for the full story,
2407 // including why a rejected/throwing execute must not leave the audit
2408 // trail silent, and why `Model::execute` is the only call inside the
2409 // try that records Outcome::Failed.
2410// MSVC's C4702 fires on the `return` below for any action whose handler never
2411// returns -- a test double whose body is a bare `throw`, for instance. The
2412// warning is correct for that instantiation and wrong as a verdict on this
2413// statement, which every other instantiation reaches. It is suppressed here
2414// rather than in each translation unit that instantiates such a handler,
2415// because the set of those is open-ended: eleven test files already qualify.
2416#ifdef _MSC_VER
2417#pragma warning(push)
2418#pragma warning(disable : 4702)
2419#endif
2420 auto result = [&] {
2421 try {
2422 return std::make_shared<R>(model.execute(actionRef));
2423 } catch (const std::exception& exc [[maybe_unused]]) {
2424 if constexpr (::morph::model::detail::actionLoggable<Action>() ==
2425 ::morph::model::Loggable::Yes) {
2426 if (holder.hasActionLog()) {
2427 ::morph::model::detail::recordActionFailure(
2428 holder, std::string{::morph::model::ModelTraits<Model>::typeId()},
2429 std::string{::morph::model::ActionTraits<Action>::typeId()},
2430 ::morph::model::ActionTraits<Action>::toJson(actionRef),
2431 ::morph::model::detail::actionPayloadSchema<Action>(), exc.what());
2432 }
2433 }
2434 throw;
2435 }
2436 }();
2437#ifdef _MSC_VER
2438#pragma warning(pop)
2439#endif
2440 // Past this point the model's mutation has committed, so neither
2441 // serialising the result nor appending the entry may be reported as
2442 // an execution failure: both throw (ParseError; a sink that could
2443 // not reach its backend), and inside the try above that throw would
2444 // reject this call's Completion as if the model had refused the
2445 // action and file an Outcome::Failed entry blaming the action for an
2446 // infrastructure fault. ActionRecordingError says what is true
2447 // instead -- the action ran, the recording of it did not -- and
2448 // carries the result JSON the audit trail never received.
2449 if constexpr (::morph::model::detail::actionLoggable<Action>() == ::morph::model::Loggable::Yes) {
2450 if (holder.hasActionLog()) {
2451 std::string resultJson;
2452 try {
2453 resultJson = ::morph::model::ActionTraits<Action>::resultToJson(*result);
2454 // entityKey/principal/timestampMs are filled in by recordIfAttached.
2455 ::morph::model::detail::recordActionSuccess(
2456 holder, std::string{::morph::model::ModelTraits<Model>::typeId()},
2457 std::string{::morph::model::ActionTraits<Action>::typeId()},
2458 ::morph::model::ActionTraits<Action>::toJson(actionRef),
2459 ::morph::model::detail::actionPayloadSchema<Action>(), resultJson);
2460 } catch (const std::exception& exc) {
2461 throw ::morph::model::ActionRecordingError{std::move(resultJson), exc.what()};
2462 }
2463 }
2464 }
2465 return result;
2466#endif
2467 };
2468 }
2469 if constexpr (!taskHandler) {
2470 static_cast<void>(stopSource);
2471 }
2472 return call;
2473 }
2474
2475 /// @brief Installs this bridge's reconnect handler on @p backend, posted to
2476 /// the owner.
2477 /// @param backend The backend.
2478 void installReconnectHandler(const std::shared_ptr<::morph::backend::detail::IBackend>& backend) {
2479 std::weak_ptr<::morph::backend::detail::IBackend> const weak{backend};
2480 backend->setReconnectHandler(
2481 [this, weak, token = _callbacks.token()] {
2482 if (token.active()) {
2483 reconnect(weak);
2484 }
2485 },
2486 &_affinity.owner());
2487 }
2488
2489 /// @brief Re-issues a bind for every live binding after @p weak reconnected,
2490 /// on the owner.
2491 ///
2492 /// The ids the bindings held belonged to the connection that dropped, so
2493 /// each is cleared; a bind that settles here binds it at once, and one
2494 /// still in flight holds its calls until it settles. A reconnect of a
2495 /// backend this bridge has switched away from is ignored.
2496 /// @param weak The backend that reconnected.
2497 void reconnect(const std::weak_ptr<::morph::backend::detail::IBackend>& weak) {
2498 note("Bridge::reconnect");
2499 auto pinned = weak.lock();
2500 if (!pinned || pinned != _backend) {
2501 return;
2502 }
2503 std::vector<Resume> superseded;
2504 for (auto const& entry : _handlers) {
2505 auto binding = entry.lock();
2506 if (!binding || holdsNothing(*binding)) {
2507 continue;
2508 }
2509 binding->currentId.store(0);
2510 if (auto own = startBind(binding, rebindRequest(*binding), std::nullopt, {})) {
2511 superseded.push_back(std::move(own));
2512 }
2513 }
2514 auto const dropped = std::make_exception_ptr(::morph::backend::DisconnectedError{});
2515 for (auto& resume : superseded) {
2516 resume(dropped);
2517 }
2518 }
2519
2520 ::morph::exec::detail::OwnerAffinity _affinity;
2521 std::shared_ptr<::morph::backend::detail::IBackend> _backend;
2522 std::vector<std::weak_ptr<detail::HandlerBinding>> _handlers;
2523 ::morph::session::Context _defaultSession;
2524 ::morph::session::Principal _principal;
2525 // Client-side execute deadline (see setExecuteDeadline). The scheduler is
2526 // created lazily and shared with every sink that armed an entry on it, so a
2527 // sink's disarm outlives the bridge safely.
2528 std::chrono::milliseconds _executeDeadline{0};
2529 std::shared_ptr<::morph::async::detail::TimeoutScheduler> _timeoutScheduler;
2530 // Instance subscriptions; see `detail::SubscriptionRegistry`. Shared with
2531 // every sink, which reads its atomic count on the backend's thread to
2532 // decide whether a result needs the owner at all. Never null.
2533 std::shared_ptr<detail::SubscriptionRegistry<detail::HandlerBinding>> _subscriptions{
2534 std::make_shared<detail::SubscriptionRegistry<detail::HandlerBinding>>()};
2535 // Count of calls not yet resolved -- see pendingCalls(). Shared with every
2536 // sink, which decrements it on the backend's thread. Never null.
2537 std::shared_ptr<std::atomic<std::size_t>> _pendingCalls{std::make_shared<std::atomic<std::size_t>>(0)};
2538 // Declared last, so destroyed first: every continuation queued on the owner
2539 // finds its token expired before any member it would touch is torn down.
2540 ::morph::async::CallbackScope _callbacks;
2541};
2542
2543/// @brief `BridgeHandler` sharing policy: private, one instance per handler.
2544///
2545/// The default, and what every pre-existing call site gets. Such a handler
2546/// registers its own instance at construction and never enters the shared
2547/// directory, so two `BridgeHandler<M>` objects are two independent models —
2548/// byte-for-byte the behaviour morph has always had.
2549struct NoSharing {};
2550
2551/// @brief `BridgeHandler` sharing policy: joins the shared instance directory.
2552///
2553/// A shared handler registers **nothing** at construction. It acquires an
2554/// instance the first time a keyed action or an explicit `attach()` names a
2555/// primary key, and every other `AllowShared` handler naming that same key —
2556/// in this process or, for a remote backend, in any other client — reaches the
2557/// same instance. Releasing the last such handler destroys it.
2558///
2559/// A shared handler that only ever runs *keyless* actions never attaches, and
2560/// its `execute` fails fast with "handler not bound": there is no instance to
2561/// run against and inventing a private one would silently defeat the sharing
2562/// the caller asked for. Attach first — see docs/spec/core/shared_instances.md.
2563struct AllowShared {};
2564
2565/// @brief RAII wrapper that binds a single model type to a `Bridge`.
2566///
2567/// On construction, registers a `HandlerBinding` on the bridge. On destruction,
2568/// deregisters it. The handler is non-copyable, and belongs to its bridge's
2569/// owner: it is used and destroyed there.
2570///
2571/// @par Instance subscriptions
2572/// Beyond the one-shot `execute(action) -> Completion<R>` API, a handler can
2573/// observe *the instance it is attached to*:
2574///
2575/// - `subscribe<R>(cb)` fires whenever an `R` is produced on that instance, by
2576/// any handler attached to it.
2577/// - `unsubscribe<R>()` drops the callback.
2578///
2579/// @tparam Model Concrete model type.
2580/// @tparam Sharing `NoSharing` (the default) or `AllowShared`.
2581template <typename Model, typename Sharing = NoSharing>
2582// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
2584public:
2585 /// @brief Whether this handler participates in the shared instance directory.
2586 static constexpr bool kShared = std::is_same_v<Sharing, AllowShared>;
2587
2588 /// @brief Constructs and registers the handler using the default model factory.
2589 ///
2590 /// On the bridge's owner the bind is issued here; a backend that settles
2591 /// it before returning has bound the handler when this returns. Anywhere
2592 /// else — inside a running action on a pool thread — the registration is
2593 /// posted to the owner. Either way nothing waits: a call made before the
2594 /// bind settles is held and dispatched when it does.
2595 ///
2596 /// @param bridge The bridge to register on. Borrowed: it must outlive
2597 /// every call made on this handler.
2598 /// @param guiExec Executor used to deliver `Completion` callbacks: the
2599 /// bridge's owner, or an executor that runs its tasks on
2600 /// the owner's thread (a debug build checks it once).
2601 /// Borrowed: it must outlive this handler.
2602 BridgeHandler(Bridge& bridge MORPH_LIFETIMEBOUND, ::morph::exec::IExecutor* guiExec MORPH_LIFETIMEBOUND)
2603 : BridgeHandler(bridge, guiExec, Bridge::makeBinding<Model>(kShared)) {
2604 static_assert(!kShared || ::morph::model::KeyedModel<Model>,
2605 "BridgeHandler<Model, AllowShared> requires Model to declare a PrimaryKey alias");
2606 }
2607
2608 /// @brief Constructs the handler with a pre-built binding (for dependency injection).
2609 ///
2610 /// Registers @p binding exactly as the constructor above registers its own.
2611 /// @param bridge The bridge to register on. Borrowed, on the same terms as
2612 /// the constructor above.
2613 /// @param guiExec Executor for callback delivery, on the same terms as the
2614 /// constructor above.
2615 /// @param binding Pre-built binding whose factory captures injected
2616 /// dependencies. Its `contextKey` must be set before this call.
2617 BridgeHandler(Bridge& bridge MORPH_LIFETIMEBOUND, ::morph::exec::IExecutor* guiExec MORPH_LIFETIMEBOUND,
2618 std::shared_ptr<detail::HandlerBinding> binding)
2619 : _bridge{bridge},
2620 _liveness{bridge.liveness()},
2621 _affinity{bridge.affinity()},
2622 _guiExec{guiExec},
2623 _binding{std::move(binding)} {
2624 _bridge.adoptHandler(_binding);
2625#ifndef NDEBUG
2626 if (_guiExec != nullptr && _guiExec != &_affinity.owner()) {
2627 // Once, where the executor runs its tasks: a guiExec on another
2628 // thread would deliver this handler's callbacks off the owner.
2629 _guiExec->post([affinity = _affinity] { affinity.note("BridgeHandler::guiExec"); });
2630 }
2631#endif
2632 }
2633
2634 /// @brief Deregisters the binding from the bridge, on the owner.
2635 ///
2636 /// Every call still held for a bind in flight is rejected with
2637 /// `HandlerDestroyedError`. Destroyed after its bridge — still on the
2638 /// owner — it finds the bridge's token expired and deregisters nothing;
2639 /// on one thread that check is exact.
2641 _affinity.note("BridgeHandler::~BridgeHandler");
2642 if (_liveness.active()) {
2643 _bridge.deregisterHandler(_binding);
2644 }
2645 }
2646
2647 BridgeHandler(const BridgeHandler&) = delete;
2648 BridgeHandler& operator=(const BridgeHandler&) = delete;
2649
2650 /// @brief Dispatches @p action via the underlying `Bridge` and returns a `Completion`.
2651 ///
2652 /// The bridge's currently-installed default session is attached
2653 /// automatically. A call made before the handler's bind has settled is held
2654 /// and dispatched when it does; a failed bind rejects it with the bind's
2655 /// error.
2656 ///
2657 /// @tparam Action Concrete action type registered with `BRIDGE_REGISTER_ACTION`.
2658 /// @param action Action to execute (moved into the dispatch).
2659 /// @return Completion that resolves on the GUI executor. A payload- or
2660 /// result-keyed action's attach/promote step never throws out of
2661 /// this call, even when the backend refuses it (e.g. a remote
2662 /// server at `LimitPolicy::maxLiveModels`, a transport error, or
2663 /// an unauthorized attach) — the failure is instead delivered
2664 /// through the returned Completion's `.onError(...)`, exactly
2665 /// like any other dispatch failure.
2666 template <typename Action>
2669 if constexpr (kShared && ::morph::model::detail::PayloadKeyed<Action>) {
2670 // The action names its instance: attach (or re-point) before
2671 // dispatching, so the call lands on the instance it asked for. Key
2672 // extraction is user code, so a throw from it resolves the
2673 // Completion rather than escaping execute().
2674 std::string key;
2675 try {
2677 } catch (...) {
2678 auto [failed, promise] = ::morph::async::Completion<R>::makeSettleable(_guiExec);
2679 promise.reject(std::current_exception());
2680 return std::move(failed);
2681 }
2682 return _bridge.template executeAttachedVia<Model, Action>(_binding, std::move(action), _guiExec,
2683 std::move(key));
2684 } else if constexpr (kShared && ::morph::model::detail::ResultKeyed<Action>) {
2685 // The action *creates* the instance and its result carries the
2686 // generated key: run it on an anonymous instance, then promote that
2687 // instance in place before any user callback observes the result.
2688 return _bridge.template executeCreatingVia<Model, Action>(_binding, std::move(action), _guiExec);
2689 } else {
2690 return _bridge.template executeVia<Model, Action>(_binding, std::move(action), _guiExec);
2691 }
2692 }
2693
2694 /// @brief Dispatches @p action as `execute(action)` does, cancellable through @p stop.
2695 ///
2696 /// A stop on @p stop before the call settles cancels it: the returned
2697 /// `Completion` is rejected with `core::async::OperationCancelled`, posted
2698 /// to the GUI executor from the thread that requested the stop, and the
2699 /// call's stop source is asked to stop. A handler returning a `Task`, on a
2700 /// `LocalBackend`, sees that stop at its next stop-aware `co_await`; a
2701 /// synchronous handler runs to its end, and a remote server's handler runs
2702 /// on, since no cancel crosses the wire. Either way the call's own outcome,
2703 /// later, reaches nobody. A token already stopped rejects the call the same
2704 /// way without dispatching it.
2705 ///
2706 /// The call stays counted as pending until the backend settles it, as a
2707 /// call the execute deadline settled does.
2708 ///
2709 /// @tparam Action Concrete action type registered with `BRIDGE_REGISTER_ACTION`.
2710 /// @param action Action to execute (moved into the dispatch).
2711 /// @param stop The caller's cancel. Observed until the call settles; a
2712 /// token that can never be stopped makes this `execute(action)`.
2713 /// @return Completion that resolves on the GUI executor, or rejects with
2714 /// `core::async::OperationCancelled` once @p stop is stopped.
2715 template <typename Action>
2717 Action action, ::core::async::StopToken stop) {
2719 if (stop.stop_requested()) {
2720 auto [cancelled, promise] = ::morph::async::Completion<R>::makeSettleable(_guiExec);
2721 promise.reject(std::make_exception_ptr(::core::async::OperationCancelled{}));
2722 return std::move(cancelled);
2723 }
2724 auto completion = execute(std::move(action));
2725 // Never empty: every path of `execute(action)` returns a completion
2726 // built on a state, a rejected one included.
2727 completion.state()->linkCancel(std::move(stop));
2728 return completion;
2729 }
2730
2731 /// @brief Attaches (or re-points) this handler to the instance for @p key.
2732 ///
2733 /// Creates the instance if no live instance holds @p key, otherwise joins the
2734 /// existing one. Re-pointing an already-attached handler releases the old
2735 /// instance, which survives only if another handler still holds it. A
2736 /// backend that settles before returning has attached the handler by the
2737 /// time this returns; otherwise calls made meanwhile are held until it
2738 /// has. A refused attach is logged and leaves the handler on the instance
2739 /// it held; a call held behind it is rejected with its error only when
2740 /// the handler has no instance to fall back to. A key that names no
2741 /// instance — an empty string, a strong id with no value — is refused the
2742 /// same way: a call through a handler with no instance is then rejected
2743 /// with `std::invalid_argument` or `std::runtime_error` respectively.
2744 ///
2745 /// @tparam M Defaulted to `Model`; never named explicitly. Present only so the
2746 /// signature is instantiated lazily, since `PrimaryKeyOf` is
2747 /// ill-formed for an unkeyed model.
2748 /// @param key Primary key of the instance to attach to.
2749 template <typename M = Model>
2750 void attach(const ::morph::model::PrimaryKeyOf<M>& key)
2751 requires kShared
2752 {
2753 std::string encoded;
2754 try {
2755 encoded = ::morph::model::keyToString(key);
2756 } catch (...) {
2757 _bridge.refuseAttach(_binding, std::current_exception());
2758 return;
2759 }
2760 _bridge.template attachHandler<Model>(_binding, std::move(encoded));
2761 }
2762
2763 /// @brief This handler's current primary key, or `nullopt` if unattached.
2764 /// @tparam M Defaulted to `Model`; never named explicitly. See `attach`.
2765 /// @return The attached key, or `nullopt` before the first attach settles.
2766 template <typename M = Model>
2767 [[nodiscard]] std::optional<::morph::model::PrimaryKeyOf<M>> primary()
2768 requires kShared
2769 {
2770 auto raw = _bridge.bindingPrimary(_binding);
2771 if (raw.empty()) {
2772 return std::nullopt;
2773 }
2774 return ::morph::model::keyFromString<::morph::model::PrimaryKeyOf<M>>(raw);
2775 }
2776
2777 /// @brief Snapshot of the live shared instance keys for `Model`.
2778 ///
2779 /// Asynchronous even in local mode: the directory is backend state, and in
2780 /// remote mode answering costs a round trip. The result is a snapshot,
2781 /// stale the moment it arrives.
2782 ///
2783 /// @tparam M Defaulted to `Model`; never named explicitly. See `attach`.
2784 /// @return Completion resolving on the GUI executor with the live keys.
2785 template <typename M = Model>
2787 requires kShared
2788 {
2790 auto [comp, promise] = ::morph::async::Completion<std::vector<Key>>::makeSettleable(_guiExec);
2791 auto answer =
2792 std::make_shared<typename ::morph::async::Completion<std::vector<Key>>::Promise>(std::move(promise));
2793 // The backend's answer is delivered on the bridge's owner, which is
2794 // where this runs, so attaching to it here is attaching on its owner.
2795 _bridge.template instancesOf<Model>(_bridge.owner())
2796 .thenDetached([answer](const std::vector<std::string>& raws) {
2797 try {
2798 std::vector<Key> keys;
2799 keys.reserve(raws.size());
2800 for (const auto& raw : raws) {
2801 keys.push_back(::morph::model::keyFromString<Key>(raw));
2802 }
2803 answer->resolve(std::move(keys));
2804 } catch (...) {
2805 answer->reject(std::current_exception());
2806 }
2807 })
2808 .onErrorDetached([answer](const std::exception_ptr& error) { answer->reject(error); });
2809 return std::move(comp);
2810 }
2811
2812 /// @brief Type-erased execute: looks up the action by its registered
2813 /// string id and dispatches it through `ActionExecuteRegistry`.
2814 ///
2815 /// Use this only when the concrete `Action` type is not known at the
2816 /// call site (e.g. a schema-driven UI reading action names out of a
2817 /// JSON Schema at runtime). Prefer the templated `execute<Action>()`
2818 /// whenever the type is known at compile time.
2819 ///
2820 /// @param actionType Registered action type-id (the `NAME` passed to `BRIDGE_REGISTER_ACTION`).
2821 /// @param bodyJson JSON-encoded action body.
2822 /// @return Completion resolving with the JSON-encoded result.
2823 /// @throws std::runtime_error if `actionType` was never registered for `Model`.
2824 [[nodiscard]] ::morph::async::Completion<std::string> executeJson(std::string_view actionType,
2825 std::string_view bodyJson) {
2826 // Dispatches through the executor registered for this handler's own
2827 // Sharing policy (see `ActionExecuteRegistry::registerAction`'s doc
2828 // comment): a NoSharing-only executor would static_cast `this` to the
2829 // wrong BridgeHandler<Model, Sharing> instantiation for a shared
2830 // handler, silently skipping its attach/promote step.
2832 actionType, this, bodyJson);
2833 }
2834
2835 /// @brief Whether `Model` has an action registered under @p actionId, without
2836 /// invoking it.
2837 ///
2838 /// A pure existence check over `ActionExecuteRegistry` -- the same
2839 /// registry `executeJson` dispatches through.
2840 /// @param actionId Action type id to check.
2841 /// @return `true` if `executeJson(actionId, ...)` would find a registered action.
2842 [[nodiscard]] bool servesAction(std::string_view actionId) const noexcept {
2844 actionId);
2845 }
2846
2847 /// @brief The executor used to deliver this handler's `Completion` callbacks.
2848 /// @return The GUI/callback executor passed at construction.
2849 [[nodiscard]] ::morph::exec::IExecutor* guiExecutor() const noexcept { return _guiExec; }
2850
2851 /// @brief The executor this handler and its bridge belong to.
2852 /// @return The bridge's owner.
2853 [[nodiscard]] ::morph::exec::IExecutor& owner() const noexcept { return _affinity.owner(); }
2854
2855 /// @brief Whether the calling thread is on this handler's owner.
2856 /// @return True inside one of the owner's tasks, or on the thread that
2857 /// constructed the bridge outside them.
2858 [[nodiscard]] bool onOwner() const noexcept { return _affinity.here(); }
2859
2860 /// @brief Whether this handler currently has a live backend instance.
2861 ///
2862 /// A point-in-time atomic read, safe from any thread. Nothing needs to wait
2863 /// on it: a call made while the handler's bind is in flight is held until
2864 /// the bind settles.
2865 /// @return `true` if the handler is currently bound to a `ModelId`.
2866 [[nodiscard]] bool isBound() const noexcept { return Bridge::isBound(_binding); }
2867
2868 /// @brief Subscribes to results of type @p R produced on the attached instance.
2869 ///
2870 /// Fires whenever an `R` is produced on the instance this handler is
2871 /// attached to by a typed call — this handler's, another handler's sharing
2872 /// the instance, or another screen's. A `RawHandler`'s call on the same
2873 /// instance notifies nobody: its result is JSON text, not an `R`.
2874 ///
2875 /// One callback per `(handler, R)`: subscribing again replaces the previous
2876 /// one. Callbacks are delivered on this handler's executor. Failed actions
2877 /// notify nobody; delivery is best-effort and unbuffered, with no replay.
2878 ///
2879 /// @tparam R Result/state type to observe.
2880 /// @param cb Callable receiving the value by value on the GUI executor.
2881 template <typename R>
2882 void subscribe(std::function<void(R)> cb) {
2883 _bridge.addSubscription(
2884 _binding, std::type_index{typeid(R)},
2885 [cb = std::move(cb)](const std::any& boxed) { cb(std::any_cast<const R&>(boxed)); }, _guiExec);
2886 }
2887
2888 /// @brief Subscribes to results of type @p R, gated on @p scope.
2889 ///
2890 /// Same subscription as `subscribe<R>(cb)`, except that @p cb runs only
2891 /// while @p scope is alive and un-stopped. The sink itself is *not* pruned
2892 /// when the scope goes inactive: delivery is refused, the subscription
2893 /// entry stays until `unsubscribe<R>()` or handler destruction removes it.
2894 ///
2895 /// @tparam R Result/state type to observe.
2896 /// @param scope Receiver-owned gate; observed weakly, and only its current
2897 /// generation is captured (a later `reset()` retires this sink).
2898 /// @param cb Callable receiving the value by value on the GUI executor.
2899 template <typename R>
2900 void subscribe(const ::morph::async::CallbackScope& scope, std::function<void(R)> cb) {
2901 subscribe<R>(scope.token(), std::move(cb));
2902 }
2903
2904 /// @brief Subscribes to results of type @p R, gated on an already-issued @p token.
2905 ///
2906 /// The token-taking form of `subscribe<R>(const CallbackScope&, cb)`.
2907 ///
2908 /// @tparam R Result/state type to observe.
2909 /// @param token Gate observing some receiver's `CallbackScope`. A
2910 /// default-constructed token suppresses every delivery.
2911 /// @param cb Callable receiving the value by value on the GUI executor.
2912 template <typename R>
2913 void subscribe(::morph::async::CallbackToken token, std::function<void(R)> cb) {
2914 subscribe<R>(std::function<void(R)>{token.guard(std::move(cb))});
2915 }
2916
2917 /// @brief Removes this handler's subscription for @p R.
2918 /// @tparam R Result/state type to stop hearing about.
2919 template <typename R>
2921 _bridge.removeSubscription(_binding, std::type_index{typeid(R)});
2922 }
2923
2924 /// @brief Returns the underlying `HandlerBinding`.
2925 ///
2926 /// @return Shared pointer to the binding owned by this handler — a reference
2927 /// into the handler, valid only for as long as it is. Copy it to
2928 /// keep the binding past that point.
2929 [[nodiscard]] const std::shared_ptr<detail::HandlerBinding>& binding() const MORPH_LIFETIMEBOUND {
2930 return _binding;
2931 }
2932
2933private:
2934 Bridge& _bridge;
2935 // The bridge's liveness, checked by the destructor: a handler destroyed
2936 // after its bridge, on the owner, deregisters nothing.
2938 // The owner, kept by value so the destructor can check it without the bridge.
2939 ::morph::exec::detail::OwnerAffinity _affinity;
2940 ::morph::exec::IExecutor* _guiExec;
2941 std::shared_ptr<detail::HandlerBinding> _binding;
2942};
2943
2944/// @brief A handler for a model known only by its registered type id.
2945///
2946/// What a client compiled without the model's C++ type holds: binds through
2947/// `Bridge::bindByType` on construction, dispatches through
2948/// `Bridge::executeRaw`, and deregisters on destruction — on the owner, with
2949/// every guarantee a `BridgeHandler` has. Non-copyable and non-movable.
2951public:
2952 /// @brief Binds the model registered as @p typeId.
2953 /// @param bridge The bridge to bind on. Borrowed: it must outlive
2954 /// every call made on this handler.
2955 /// @param cbExec Executor the `Completion` callbacks are posted on.
2956 /// Borrowed: it must outlive this handler.
2957 /// @param typeId The model's registered type id.
2958 /// @param sharing Private or shared.
2959 /// @param instanceKey For a shared handler, the key to attach to now, or empty.
2960 /// @throws std::invalid_argument for a private handler with a key.
2961 /// @throws std::logic_error for a key given off the owner, inside a
2962 /// running action: attaching is the owner's.
2963 RawHandler(Bridge& bridge MORPH_LIFETIMEBOUND, ::morph::exec::IExecutor* cbExec MORPH_LIFETIMEBOUND,
2964 std::string typeId, BindSharing sharing = BindSharing::Private, std::string instanceKey = {})
2965 : _bridge{bridge},
2966 _liveness{bridge.liveness()},
2967 _affinity{bridge.affinity()},
2968 _cbExec{cbExec},
2969 _binding{bridge.bindByType(std::move(typeId), sharing, std::move(instanceKey))} {}
2970
2971 /// @brief Deregisters the binding, on the owner.
2972 ///
2973 /// Every call still held for a bind in flight is rejected with
2974 /// `HandlerDestroyedError`. Destroyed after its bridge — still on the
2975 /// owner — it finds the bridge's token expired and deregisters nothing.
2977 _affinity.note("RawHandler::~RawHandler");
2978 if (_liveness.active()) {
2979 _bridge.deregisterHandler(_binding);
2980 }
2981 }
2982
2983 RawHandler(const RawHandler&) = delete;
2984 RawHandler& operator=(const RawHandler&) = delete;
2985 RawHandler(RawHandler&&) = delete;
2986 RawHandler& operator=(RawHandler&&) = delete;
2987
2988 /// @brief Dispatches the action registered as @p actionId with @p bodyJson.
2989 /// @param actionId The action's registered type id.
2990 /// @param bodyJson The action's JSON body.
2991 /// @return Completion resolving with the reply's JSON text.
2992 [[nodiscard]] ::morph::async::Completion<std::string> execute(std::string actionId, std::string bodyJson) {
2993 return _bridge.executeRaw(_binding, std::move(actionId), std::move(bodyJson), _cbExec);
2994 }
2995
2996 /// @brief `execute`, cancellable through @p stop.
2997 /// @param actionId The action's registered type id.
2998 /// @param bodyJson The action's JSON body.
2999 /// @param stop The caller's cancel.
3000 /// @return Completion resolving with the reply's JSON text, or rejected
3001 /// with `core::async::OperationCancelled`.
3002 [[nodiscard]] ::morph::async::Completion<std::string> execute(std::string actionId, std::string bodyJson,
3003 ::core::async::StopToken stop) {
3004 return _bridge.executeRaw(_binding, std::move(actionId), std::move(bodyJson), _cbExec, std::move(stop));
3005 }
3006
3007 /// @brief `execute` on the instance for @p instanceKey, attaching this
3008 /// shared handler to it first; see `Bridge::executeRawOn`.
3009 ///
3010 /// The way to send a keyed action: a refused attach rejects the call
3011 /// rather than running it on the instance the handler held before.
3012 /// @param instanceKey Canonical string encoding of the primary key.
3013 /// @param actionId The action's registered type id.
3014 /// @param bodyJson The action's JSON body.
3015 /// @return Completion resolving with the reply's JSON text.
3016 /// @throws std::invalid_argument for an empty @p instanceKey.
3017 /// @throws std::logic_error on a private handler.
3018 [[nodiscard]] ::morph::async::Completion<std::string> executeOn(std::string instanceKey, std::string actionId,
3019 std::string bodyJson) {
3020 return _bridge.executeRawOn(_binding, std::move(instanceKey), std::move(actionId), std::move(bodyJson),
3021 _cbExec);
3022 }
3023
3024 /// @brief Attaches (or re-points) this shared handler to the instance for
3025 /// @p instanceKey, as `BridgeHandler::attach` does for a typed one.
3026 ///
3027 /// Issued, not awaited, and a refusal is logged rather than reported: a
3028 /// call that must run on @p instanceKey goes through `executeOn`.
3029 /// @param instanceKey Canonical string encoding of the primary key.
3030 /// @throws std::invalid_argument for an empty @p instanceKey.
3031 /// @throws std::logic_error on a private handler.
3032 void attach(std::string instanceKey) { _bridge.attach(_binding, std::move(instanceKey)); }
3033
3034 /// @brief This handler's current instance key.
3035 /// @return The attached key, or empty before the first attach settles.
3036 [[nodiscard]] std::string primary() const { return _bridge.bindingPrimary(_binding); }
3037
3038 /// @brief Whether the handler holds a live instance now.
3039 /// @return `true` when bound.
3040 [[nodiscard]] bool isBound() const noexcept { return Bridge::isBound(_binding); }
3041
3042 /// @brief The model's registered type id.
3043 /// @return The id given at construction.
3044 [[nodiscard]] const std::string& typeId() const noexcept MORPH_LIFETIMEBOUND { return _binding->typeId; }
3045
3046private:
3047 Bridge& _bridge;
3049 ::morph::exec::detail::OwnerAffinity _affinity;
3050 ::morph::exec::IExecutor* _cbExec;
3051 std::shared_ptr<detail::HandlerBinding> _binding;
3052};
3053
3054/// Out-of-line definition of ActionExecuteRegistry::registerAction.
3055/// Placed here after BridgeHandler is fully defined so we can safely cast and call its methods.
3056///
3057/// Builds one executor per `Sharing` policy the framework defines
3058/// (`NoSharing`, `AllowShared`) from the same generic-lambda template,
3059/// `static_cast`ing `handlerVoid` to the matching `BridgeHandler<Model,
3060/// Sharing>*` in each — see bridge.md's design-decision entry for why a single
3061/// `NoSharing`-only executor is unsound for a shared handler.
3062template <typename Model, typename Action>
3063inline void ActionExecuteRegistry::registerAction(std::string_view modelId, std::string_view actionId) {
3064 auto makeExecutor = []<typename Sharing>() {
3065 return [](void* handlerVoid, std::string_view bodyJson) -> ::morph::async::Completion<std::string> {
3066 auto* handler = static_cast<BridgeHandler<Model, Sharing>*>(handlerVoid);
3067 auto resultState = std::make_shared<::morph::async::detail::CompletionState<std::string>>();
3068 // Named before anything can settle it: a settle posts its delivery to
3069 // the owner the state already has, and a decode failure settles here.
3070 ::morph::async::Completion<std::string> completion{resultState, handler->guiExecutor()};
3071 try {
3072 Action action = ::morph::model::ActionTraits<Action>::fromJson(bodyJson);
3073 // Retag any Quantity fields to their declared precision so the stored
3074 // value matches the schema's advertised `x-decimalPlaces`, rather than
3075 // silently keeping whatever runtime `dp` the client sent. No-op for
3076 // actions with no Quantity members. See docs/spec/forms/forms.md.
3078 // Pre-decode wire validation seam: reject a Quantity field whose
3079 // engaged value falls outside its unit's declared bounds
3080 // (UnitTraits<E>::bounds), before the validator/business-rule
3081 // check below. No-op for actions with no Quantity members, or
3082 // whose units declare no bounds(). See docs/spec/forms/forms.md,
3083 // "Pre-decode wire validation". Thrown as QuantityDecodeError,
3084 // caught by the same catch block as every other decode/validation
3085 // failure on this path.
3087 // Overwrite any computed fields from their declared inputs -- a
3088 // computed field is never trusted from the client, on any path.
3089 // No-op for actions with no computedFields. See docs/spec/forms/forms.md.
3091 // Enforce the action's validator on the request/reply dispatch path,
3092 // just as the reactive `set<>` path does in
3093 // `morph::flows::FlowSession::set<>` (forms/flows.hpp), which fires a
3094 // step only once its `ActionValidator` is ready. Without this, a
3095 // submitted action that fails its readiness/validity check
3096 // (empty required Quantity, out-of-range field, …) would reach the
3097 // handler and either produce a silently wrong result or force every
3098 // handler to re-check by hand. `ActionValidator<Action>::ready`
3099 // auto-detects a `bool validate() const` member and defaults to
3100 // `true` for actions with no validator, so this is a no-op for
3101 // unvalidated actions and a hard gate for validated ones. An invalid
3102 // action resolves the completion through `setException` (a proper
3103 // error reply upstream), never a bad execution.
3105 throw std::invalid_argument{"action failed validation: " +
3107 }
3108 handler
3109 ->template execute<Action>(std::move(action))
3110 // NOLINTNEXTLINE(performance-unnecessary-value-param) — lambda captures the result by value
3111 .then([resultState](auto result) {
3112 // Guard the JSON forwarding for the same reason as executeVia:
3113 // a throwing resultToJson (or the move it does) must land on
3114 // the error sink, not escape the callback executor and either
3115 // hang the completion or terminate the Qt loop.
3116 try {
3117 resultState->setValue(::morph::model::ActionTraits<Action>::resultToJson(result));
3118 } catch (...) {
3119 resultState->setException(std::current_exception());
3120 }
3121 })
3122 .onError([resultState](const std::exception_ptr& err) { resultState->setException(err); });
3123 } catch (...) {
3124 resultState->setException(std::current_exception());
3125 }
3126 return completion;
3127 };
3128 };
3129 ::morph::model::detail::claimId(_types, "action",
3130 Key{.modelId = std::string{modelId},
3131 .actionId = std::string{actionId},
3132 .sharing = std::type_index{typeid(void)}},
3133 std::string{modelId} + "/" + std::string{actionId},
3134 std::type_index{typeid(Action)});
3135 _executors[Key{std::string{modelId}, std::string{actionId}, std::type_index{typeid(NoSharing)}}] =
3136 makeExecutor.template operator()<NoSharing>();
3137 _executors[Key{std::string{modelId}, std::string{actionId}, std::type_index{typeid(AllowShared)}}] =
3138 makeExecutor.template operator()<AllowShared>();
3139}
3140
3141} // namespace morph::bridge
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
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.
Move-only handle representing the eventual result of an asynchronous operation.
Completion & then(std::function< void(const T &)> handler)
Registers a success callback.
std::shared_ptr< detail::CompletionState< T > > state() const
Returns the underlying shared state (for advanced / internal use).
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
Type-erased, JSON-in/JSON-out execute path for actions whose concrete C++ type is only known by its r...
Definition bridge.hpp:62
std::function<::morph::async::Completion< std::string >(void *, std::string_view)> Executor
Deserialises bodyJson, dispatches through the handler's Bridge, and resolves with the JSON-encoded re...
Definition bridge.hpp:66
void registerAction(std::string_view modelId, std::string_view actionId)
Registers the executors for (Model, Action) under the given string ids.
Definition bridge.hpp:3063
static ActionExecuteRegistry & instance()
Returns the process-level singleton registry.
Definition bridge.hpp:209
bool contains(std::string_view modelId, std::string_view actionId) const noexcept
Checks whether an executor is registered for (modelId, actionId), specialised for the caller's own Sh...
Definition bridge.hpp:130
::morph::async::Completion< std::string > execute(std::string_view modelId, std::string_view actionId, void *handler, std::string_view bodyJson) const
Looks up and invokes the executor for (modelId, actionId), specialised for the caller's own Sharing p...
Definition bridge.hpp:101
RAII wrapper that binds a single model type to a Bridge.
Definition bridge.hpp:2583
static constexpr bool kShared
Whether this handler participates in the shared instance directory.
Definition bridge.hpp:2586
::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > execute(Action action)
Dispatches action via the underlying Bridge and returns a Completion.
Definition bridge.hpp:2667
void subscribe(std::function< void(R)> cb)
Subscribes to results of type R produced on the attached instance.
Definition bridge.hpp:2882
bool servesAction(std::string_view actionId) const noexcept
Whether Model has an action registered under actionId, without invoking it.
Definition bridge.hpp:2842
::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > execute(Action action, ::core::async::StopToken stop)
Dispatches action as execute(action) does, cancellable through stop.
Definition bridge.hpp:2716
const std::shared_ptr< detail::HandlerBinding > & binding() const
Returns the underlying HandlerBinding.
Definition bridge.hpp:2929
::morph::async::Completion< std::vector<::morph::model::PrimaryKeyOf< M > > > instances()
Snapshot of the live shared instance keys for Model.
Definition bridge.hpp:2786
void attach(const ::morph::model::PrimaryKeyOf< M > &key)
Attaches (or re-points) this handler to the instance for key.
Definition bridge.hpp:2750
bool isBound() const noexcept
Whether this handler currently has a live backend instance.
Definition bridge.hpp:2866
bool onOwner() const noexcept
Whether the calling thread is on this handler's owner.
Definition bridge.hpp:2858
::morph::async::Completion< std::string > executeJson(std::string_view actionType, std::string_view bodyJson)
Type-erased execute: looks up the action by its registered string id and dispatches it through Action...
Definition bridge.hpp:2824
::morph::exec::IExecutor * guiExecutor() const noexcept
The executor used to deliver this handler's Completion callbacks.
Definition bridge.hpp:2849
::morph::exec::IExecutor & owner() const noexcept
The executor this handler and its bridge belong to.
Definition bridge.hpp:2853
void subscribe(::morph::async::CallbackToken token, std::function< void(R)> cb)
Subscribes to results of type R, gated on an already-issued token.
Definition bridge.hpp:2913
std::optional<::morph::model::PrimaryKeyOf< M > > primary()
This handler's current primary key, or nullopt if unattached.
Definition bridge.hpp:2767
BridgeHandler(Bridge &bridge, ::morph::exec::IExecutor *guiExec)
Constructs and registers the handler using the default model factory.
Definition bridge.hpp:2602
void subscribe(const ::morph::async::CallbackScope &scope, std::function< void(R)> cb)
Subscribes to results of type R, gated on scope.
Definition bridge.hpp:2900
~BridgeHandler()
Deregisters the binding from the bridge, on the owner.
Definition bridge.hpp:2640
BridgeHandler(Bridge &bridge, ::morph::exec::IExecutor *guiExec, std::shared_ptr< detail::HandlerBinding > binding)
Constructs the handler with a pre-built binding (for dependency injection).
Definition bridge.hpp:2617
void unsubscribe()
Removes this handler's subscription for R.
Definition bridge.hpp:2920
Central dispatcher that routes typed actions to an IBackend.
Definition bridge.hpp:747
::morph::exec::IExecutor & owner() const noexcept
The executor this bridge belongs to.
Definition bridge.hpp:811
::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.
Definition bridge.hpp:1452
void assignHandlerPrimary(const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary)
Files binding's current instance under primary, in place.
Definition bridge.hpp:992
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.
Definition bridge.hpp:1633
std::shared_ptr< detail::HandlerBinding > registerSharedHandler()
Creates a shared, initially unattached binding for Model.
Definition bridge.hpp:861
::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.
Definition bridge.hpp:1409
std::shared_ptr< detail::HandlerBinding > registerHandler()
Creates and registers a new HandlerBinding for Model.
Definition bridge.hpp:825
::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.
Definition bridge.hpp:1712
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.
Definition bridge.hpp:1065
void switchBackend(std::shared_ptr<::morph::backend::detail::IBackend > newBackend)
Replaces the active backend with newBackend.
Definition bridge.hpp:1245
::morph::session::Principal currentPrincipal() const
Returns a copy of the currently installed Principal.
Definition bridge.hpp:1183
~Bridge()
Clears the backend's reconnect handler, rejects every call still waiting for a bind with BridgeDestro...
Definition bridge.hpp:786
::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 ano...
Definition bridge.hpp:1747
void deregisterHandler(const std::shared_ptr< detail::HandlerBinding > &binding)
Deregisters binding from the active backend and removes it from tracking.
Definition bridge.hpp:1348
void switchBackend(std::unique_ptr< Backend > newBackend)
Replaces the active backend with newBackend.
Definition bridge.hpp:1216
std::string bindingPrimary(const std::shared_ptr< detail::HandlerBinding > &binding) const
Returns binding's current primary key, or empty if unattached.
Definition bridge.hpp:1026
void registerHandler(const std::shared_ptr< detail::HandlerBinding > &binding)
Tracks binding and issues its bind on the active backend.
Definition bridge.hpp:839
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...
Definition bridge.hpp:895
::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 ...
Definition bridge.hpp:1673
void setDefaultSession(::morph::session::Context session)
Installs a default session context that executeVia stamps onto the ActionCall of every subsequent cal...
Definition bridge.hpp:1110
bool onOwner() const noexcept
Whether the calling thread is on this bridge's owner.
Definition bridge.hpp:816
void setExecuteDeadline(std::chrono::milliseconds deadline)
Sets (or disables) the client-side execute deadline.
Definition bridge.hpp:1140
static bool isBound(const std::shared_ptr< detail::HandlerBinding > &binding) noexcept
Whether binding currently has a live ModelId.
Definition bridge.hpp:1038
void attachHandler(const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary)
attach for a typed caller.
Definition bridge.hpp:917
Bridge(std::unique_ptr<::morph::backend::detail::IBackend > backend, ::morph::exec::IExecutor &owner)
Constructs a bridge that dispatches through backend, owned by owner.
Definition bridge.hpp:767
void setPrincipal(::morph::session::Principal principal)
Installs the verified Principal for this Bridge.
Definition bridge.hpp:1173
std::chrono::milliseconds executeDeadline() const
Returns the currently installed client-side execute deadline.
Definition bridge.hpp:1150
void publishResult(::morph::exec::detail::ModelId mid, std::type_index type, const std::any &value)
Delivers value to every subscriber attached to instance mid.
Definition bridge.hpp:1097
::morph::async::Completion< std::vector< std::string > > instancesOf(::morph::exec::IExecutor &cbExec)
Lists the live shared primary keys of Model on the active backend.
Definition bridge.hpp:1049
::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...
Definition bridge.hpp:1483
std::size_t pendingCalls() const noexcept
Returns the number of calls made through the bridge — typed (executeVia() and its keyed variants) or ...
Definition bridge.hpp:1200
void removeSubscription(const std::shared_ptr< detail::HandlerBinding > &binding, std::type_index type)
Removes binding's subscription for type, if any.
Definition bridge.hpp:1074
::morph::session::Context defaultSession() const
Returns a copy of the currently installed default session.
Definition bridge.hpp:1157
bool hasSubscribers() const noexcept
Whether any subscription is currently registered on this bridge.
Definition bridge.hpp:1084
A handler for a model known only by its registered type id.
Definition bridge.hpp:2950
std::string primary() const
This handler's current instance key.
Definition bridge.hpp:3036
::morph::async::Completion< std::string > execute(std::string actionId, std::string bodyJson)
Dispatches the action registered as actionId with bodyJson.
Definition bridge.hpp:2992
::morph::async::Completion< std::string > execute(std::string actionId, std::string bodyJson, ::core::async::StopToken stop)
execute, cancellable through stop.
Definition bridge.hpp:3002
const std::string & typeId() const noexcept
The model's registered type id.
Definition bridge.hpp:3044
~RawHandler()
Deregisters the binding, on the owner.
Definition bridge.hpp:2976
::morph::async::Completion< std::string > executeOn(std::string instanceKey, std::string actionId, std::string bodyJson)
execute on the instance for instanceKey, attaching this shared handler to it first; see Bridge::execu...
Definition bridge.hpp:3018
bool isBound() const noexcept
Whether the handler holds a live instance now.
Definition bridge.hpp:3040
void attach(std::string instanceKey)
Attaches (or re-points) this shared handler to the instance for instanceKey, as BridgeHandler::attach...
Definition bridge.hpp:3032
RawHandler(Bridge &bridge, ::morph::exec::IExecutor *cbExec, std::string typeId, BindSharing sharing=BindSharing::Private, std::string instanceKey={})
Binds the model registered as typeId.
Definition bridge.hpp:2963
JSON-Forms-style schema generation for morph actions.
void enforceQuantityBounds(const A &action)
Runs checkQuantityBounds<A>(action) and throws QuantityDecodeError naming the first out-of-bounds fie...
Definition forms.hpp:3563
constexpr void reconcileDeclaredPrecision(A &action)
Rounds every Quantity member of action to its declared precision, so the stored value matches the pre...
Definition forms.hpp:3468
constexpr void recomputeAll(A &action)
Recomputes every entry of A::computedFields in place on action.
Definition forms.hpp:3398
detail::KeyTypeOf< M >::type PrimaryKeyOf
The primary key type of a keyed model.
K keyFromString(std::string_view text)
Decodes a primary key from its canonical wire string.
std::string keyToString(const K &key)
Encodes a primary key as its canonical wire string.
"Am I on my owner?" for a component that belongs to one executor.
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_ZONE_TEXT(text)
Attaches text to the enclosing scope's MORPH_ZONE; an empty text attaches nothing.
Definition profiler.hpp:126
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Definition profiler.hpp:118
Thrown to in-flight Completions when Bridge::switchBackend() runs.
Definition backend.hpp:600
Thrown to in-flight Completions when Bridge is destroyed.
Definition backend.hpp:606
Thrown to every call a BridgeHandler still holds for its bind when the handler is destroyed before th...
Definition backend.hpp:613
BridgeHandler sharing policy: joins the shared instance directory.
Definition bridge.hpp:2563
BridgeHandler sharing policy: private, one instance per handler.
Definition bridge.hpp:2549
Abstract executor interface.
Definition executor.hpp:70
Declares where an action's model key comes from.
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Definition registry.hpp:48
static constexpr bool ready(const Action &action)
Returns true if the action is in a state that should be executed.
Definition registry.hpp:269
Thrown when a model id, or a model's action id, is registered for a type other than the one that alre...
Definition registry.hpp:286
Traits specialisation that maps a model type to its string type-id.
Definition registry.hpp:40
Per-call session context carried through the bridge to the model.
Definition session.hpp:26
The signed-in user's verified identity, readable outside a dispatch.
Definition session.hpp:78
A Bridge's instance subscriptions: at most one callback per (binding, result type),...
std::function< void()> Action
A widget event handler taking no arguments.
Definition view.hpp:33