10#include <core/async/Cancellation.hpp>
11#include <core/async/StopToken.hpp>
23#include <unordered_map>
27#include "../attributes.hpp"
28#include "../core/backend.hpp"
30#include "../session/session.hpp"
31#include "callback_scope.hpp"
32#include "completion.hpp"
37#include "registry.hpp"
40namespace morph::bridge {
66 using Executor = std::function<::morph::async::Completion<std::string>(
void*, std::string_view)>;
88 template <
typename Model,
typename Action>
89 void registerAction(std::string_view modelId, std::string_view actionId);
100 template <
typename Sharing>
102 void* handler, std::string_view bodyJson)
const {
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});
113 return iter->second(handler, bodyJson);
129 template <
typename Sharing>
130 [[nodiscard]]
bool contains(std::string_view modelId, std::string_view actionId)
const noexcept {
135 ::morph::model::detail::noteRegistryRead(
this == &
instance());
136 return _executors.contains(
137 KeyView{.modelId = modelId, .actionId = actionId, .sharing = std::type_index{
typeid(Sharing)}});
149 std::string actionId;
150 std::type_index sharing;
151 bool operator==(
const Key&)
const =
default;
162 std::string_view modelId;
163 std::string_view actionId;
164 std::type_index sharing;
169 using is_transparent = void;
171 std::size_t operator()(KeyView key)
const noexcept {
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));
186 std::size_t operator()(
const Key& key)
const noexcept {
187 return (*
this)(KeyView{.modelId = key.modelId, .actionId = key.actionId, .sharing = key.sharing});
195 using is_transparent = void;
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};
204 std::unordered_map<Key, Executor, KeyHash, KeyEqual> _executors;
206 std::unordered_map<Key, std::type_index, KeyHash, KeyEqual> _types;
216namespace morph::model::detail {
218template <
typename Model,
typename Action>
219inline bool registerActionExecutorOnce(std::string_view modelId, std::string_view actionId)
noexcept {
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.");
243 abortRegistration(error);
250namespace morph::bridge {
262struct MemberPointerTraits;
264template <
typename V,
typename A>
265struct MemberPointerTraits<V A::*> {
278struct HandlerBinding {
284 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> modelFactory;
295 std::string contextKey;
313 std::atomic<uint64_t> currentId{0};
316 bool bindInFlight =
false;
321 std::uint64_t bindGeneration = 0;
325 std::exception_ptr bindFailure;
330 std::function<void(std::exception_ptr)> afterBind;
335 std::deque<std::function<void(std::exception_ptr)>> waiting;
347inline std::string describeFailure(
const std::exception_ptr& failure) {
349 return "unknown error";
352 std::rethrow_exception(failure);
353 }
catch (
const std::exception& exc) {
356 return "unknown error";
364 ::morph::exec::detail::ModelId
id{};
366 std::exception_ptr failure;
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"})};
385 if (!state->ready.load(std::memory_order_acquire)) {
389 return BindOutcome{.id = *state->value, .failure =
nullptr};
391 state->onErrAttached.store(
true, std::memory_order_relaxed);
392 return BindOutcome{.id = {}, .failure = state->error};
396enum class Publish : std::uint8_t {
430class BridgeSink final :
public ::morph::async::detail::CompletionState<R>,
431 public ::morph::async::detail::ISettleSink {
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)},
460 void target(::morph::exec::detail::ModelId mid)
noexcept { _mid = mid; }
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);
480 [[nodiscard]]
bool alreadySettled()
const {
return this->ready.load(std::memory_order_acquire); }
490 _pendingCalls->fetch_sub(1, std::memory_order_relaxed);
498 void settleValue(std::shared_ptr<void> opaque)
override {
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());
509 forward(opaque,
false);
513 forward(opaque,
true);
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); });
522 void settleException(
const std::exception_ptr& exc)
override {
525 _pendingCalls->fetch_sub(1, std::memory_order_relaxed);
529 this->setException(exc);
537 void forward(
const std::shared_ptr<void>& opaque,
bool onOwner) {
542 auto*
const typedResult =
static_cast<R*
>(opaque.get());
543 if (onOwner && _liveness.active()) {
545 _onResult(*typedResult);
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});
554 this->setValue(std::move(*typedResult));
556 this->setException(std::current_exception());
567 if (_settled.test_and_set(std::memory_order_acq_rel)) {
570 if (_deadlineHandle && _schedulerRef) {
572 _schedulerRef->cancel(*_deadlineHandle);
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;
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;
609template <
typename Model,
typename Action>
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());
621 ::morph::forms::recomputeAll(actionRef);
623 throw ::morph::model::ValidationError{::morph::model::ModelTraits<Model>::typeId(),
624 ::morph::model::ActionTraits<Action>::typeId()};
626 task = holder.template into<Model>().execute(actionRef);
628 done(
nullptr, std::current_exception());
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) {
637 static_cast<void>(holder);
638 const Action& action = *
static_cast<const Action*
>(actionOwner.get());
645 if constexpr (::morph::model::detail::actionLoggable<Action>() == ::morph::model::Loggable::Yes) {
646 if (holder.hasActionLog()) {
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());
663 done(
nullptr, error);
669 std::shared_ptr<void> value;
670 std::string resultJson;
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);
683 value = std::move(typed);
684 }
catch (
const std::exception& exc) {
686 std::make_exception_ptr(::morph::model::ActionRecordingError{std::move(resultJson), exc.what()}));
689 done(
nullptr, std::current_exception());
692 done(std::move(value),
nullptr);
703 std::string modelTypeId;
705 std::string actionTypeId;
713enum class BindSharing : std::uint8_t {
767 Bridge(std::unique_ptr<::morph::backend::detail::IBackend> backend,
769 : _affinity{
owner}, _backend{std::shared_ptr<::morph::backend::detail::IBackend>(std::move(backend))} {
771 throw std::invalid_argument{
"Bridge: the backend is null"};
773 _backend->setOwner(_affinity);
774 installReconnectHandler(_backend);
775 _backend->setSession(_defaultSession);
787 note(
"Bridge::~Bridge");
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);
798 for (
auto& resume : rejected) {
801 _backend->cancelPending(destroyed);
816 [[nodiscard]]
bool onOwner() const noexcept {
return _affinity.here(); }
824 template <
typename Model>
826 auto binding = makeBinding<Model>(
false);
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,
849 runSuperseded(superseded);
860 template <
typename Model>
862 note(
"Bridge::registerSharedHandler");
863 auto binding = makeBinding<Model>(
true);
864 _handlers.push_back(binding);
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"};
899 if (!binding->shared) {
900 throw std::logic_error{
"Bridge::attach: the binding is private"};
902 attachBinding(binding, std::move(primary));
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());
922 attachBinding(binding, std::move(primary));
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)) {
938 runSuperseded(startBind(strong, attachRequest(*strong, primary), primary, {}));
945 [[nodiscard]]
static std::exception_ptr emptyKey() {
946 return std::make_exception_ptr(std::invalid_argument{
"Bridge: the instance key is empty"});
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) {
962 ::morph::log::logError(
"[bridge] attach of '" + strong->typeId +
963 "' refused: " + detail::describeFailure(refused));
964 if (strong->currentId.load() == 0U) {
965 strong->bindFailure = refused;
991 template <
typename Model>
993 note(
"Bridge::assignHandlerPrimary");
994 auto const raw = binding->currentId.load();
995 if (raw == 0U || primary.empty() || !binding->primary.empty()) {
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},
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);
1009 auto const token = _callbacks.token();
1011 .then([
this, token, weak, weakBackend, primary](::morph::exec::detail::ModelId) {
1012 if (token.active()) {
1013 applyPromote(weak, weakBackend,
nullptr, primary);
1016 .onError([
this, token, weak, weakBackend, primary](
const std::exception_ptr& failure) {
1017 if (token.active()) {
1018 applyPromote(weak, weakBackend, failure, primary);
1026 [[nodiscard]] std::string
bindingPrimary(
const std::shared_ptr<detail::HandlerBinding>& binding)
const {
1027 note(
"Bridge::bindingPrimary");
1028 return binding->primary;
1038 [[nodiscard]]
static bool isBound(
const std::shared_ptr<detail::HandlerBinding>& binding)
noexcept {
1039 return binding->currentId.load() != 0U;
1048 template <
typename Model>
1051 note(
"Bridge::instancesOf");
1065 void addSubscription(
const std::shared_ptr<detail::HandlerBinding>& binding, std::type_index type,
1067 note(
"Bridge::addSubscription");
1068 _subscriptions->addSubscription(binding, type, std::move(sink), exec);
1075 note(
"Bridge::removeSubscription");
1076 _subscriptions->removeSubscription(binding, type);
1084 [[nodiscard]]
bool hasSubscribers() const noexcept {
return _subscriptions->hasSubscribers(); }
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);
1111 note(
"Bridge::setDefaultSession");
1112 _defaultSession = session;
1113 _backend->setSession(std::move(session));
1141 note(
"Bridge::setExecuteDeadline");
1142 _executeDeadline = deadline;
1143 if (_executeDeadline.count() > 0 && !_timeoutScheduler) {
1144 _timeoutScheduler = std::make_shared<::morph::async::detail::TimeoutScheduler>();
1151 note(
"Bridge::executeDeadline");
1152 return _executeDeadline;
1158 note(
"Bridge::defaultSession");
1159 return _defaultSession;
1174 note(
"Bridge::setPrincipal");
1175 _principal = std::move(principal);
1184 note(
"Bridge::currentPrincipal");
1200 [[nodiscard]] std::size_t
pendingCalls() const noexcept {
return _pendingCalls->load(std::memory_order_relaxed); }
1214 template <
typename Backend>
1215 requires std::derived_from<Backend, ::morph::backend::detail::IBackend>
1217 switchBackend(std::shared_ptr<::morph::backend::detail::IBackend>{std::move(newBackend)});
1245 void switchBackend(std::shared_ptr<::morph::backend::detail::IBackend> newBackend) {
1246 note(
"Bridge::switchBackend");
1248 throw std::invalid_argument{
"Bridge::switchBackend: the backend is null"};
1250 auto next = std::move(newBackend);
1251 next->setOwner(_affinity);
1252 next->setSession(_defaultSession);
1255 std::shared_ptr<detail::HandlerBinding> binding;
1257 std::optional<detail::BindOutcome> settled;
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;
1264 for (
auto const& weak : _handlers) {
1265 auto binding = weak.lock();
1269 live.push_back(weak);
1270 if (holdsNothing(*binding)) {
1271 untouched.push_back(binding);
1274 auto completion = next->bindModel(rebindRequest(*binding), _affinity.owner());
1275 auto settled = detail::takeSettled(completion);
1276 bool const failed = settled && settled->failure;
1278 staging = settled->failure;
1280 issued.push_back(Issued{.binding = binding, .completion = std::move(completion), .settled = settled});
1286 staging = std::current_exception();
1289 rollback(next, issued);
1290 std::rethrow_exception(staging);
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));
1300 binding.bindFailure =
nullptr;
1301 if (entry.settled) {
1302 binding.currentId.store(entry.settled->id.v);
1303 binding.bindInFlight =
false;
1306 binding.currentId.store(0);
1307 binding.bindInFlight =
true;
1308 awaitBind(entry.binding, next, generation, std::move(entry.completion), std::nullopt);
1310 for (
auto const& binding : untouched) {
1311 if (binding->bindInFlight) {
1315 ++binding->bindGeneration;
1316 binding->bindInFlight =
false;
1317 if (
auto own = std::exchange(binding->afterBind, {})) {
1318 superseded.push_back(std::move(own));
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);
1331 for (
auto& resume : superseded) {
1334 for (
auto const& weak : _handlers) {
1335 if (
auto binding = weak.lock()) {
1336 drainWaiting(*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);
1356 _backend->deregisterModel(::morph::exec::detail::ModelId{raw});
1358 std::erase_if(_handlers, [&binding](
const auto& weak) {
1359 auto strong = weak.lock();
1360 return !strong || strong.get() == binding.get();
1363 for (
auto& resume : rejected) {
1408 template <
typename Model,
typename Action>
1411 std::function<
void(
const typename ::morph::model::ActionTraits<Action>::Result&)> onResult = {}) {
1414 note(
"Bridge::executeVia");
1415 auto sink = makeSink<Model, Action>(std::move(onResult));
1418 if (!binding->bindInFlight) {
1419 dispatchNow<Model, Action>(*binding, sink, std::move(action), cbExec,
false);
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{}));
1431 dispatchNow<Model, Action>(*strong, sink, std::move(*held), cbExec,
true);
1451 template <
typename Model,
typename Action>
1457 note(
"Bridge::executeVia");
1460 promise.reject(emptyKey());
1461 return std::move(refused);
1463 auto sink = makeSink<Model, Action>({});
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,
true);
1482 template <
typename Model,
typename Action>
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()) {
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)) {
1503 if (strong->currentId.load() != 0U) {
1504 dispatchNow<Model, Action>(*strong, sink, std::move(*held), cbExec,
true);
1507 runSuperseded(startBind(strong,
1508 ::morph::backend::detail::BindRequest{.typeId = strong->typeId,
1509 .factory = strong->modelFactory,
1510 .contextKey = strong->contextKey,
1513 std::nullopt, [
this, weak, sink, cbExec, held](
const std::exception_ptr& settled) {
1514 auto bound = weak.lock();
1515 if (rejectIfGone(sink, bound, settled)) {
1518 dispatchNow<Model, Action>(*bound, sink, std::move(*held), cbExec,
true);
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); };
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;
1560 call.deserializeResult = [](std::string_view json) -> std::shared_ptr<void> {
1561 return std::make_shared<std::string>(json);
1563 auto const& dispatcher = ::morph::model::detail::defaultDispatcher();
1564 if (!dispatcher.dispatchesAsync(action->modelTypeId, action->actionTypeId)) {
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));
1571 call.stopSource = std::move(stopSource);
1572 call.action = std::move(action);
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) {
1591 std::shared_ptr<void> boxed;
1593 boxed = std::make_shared<std::string>(std::move(result));
1595 done(
nullptr, std::current_exception());
1598 done(std::move(boxed),
nullptr);
1601 call.stopSource = std::move(stopSource);
1602 call.action = std::move(action);
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"};
1638 if (!instanceKey.empty() && !onOwner()) {
1639 throw std::logic_error{
"Bridge::bindByType: attach on the owner, not inside an action"};
1641 auto binding = makeBindingByType(std::move(typeId), sharing == BindSharing::Shared);
1642 adoptHandler(binding);
1643 if (!instanceKey.empty()) {
1644 attach(binding, std::move(instanceKey));
1674 std::string actionId, std::string bodyJson,
1677 note(
"Bridge::executeRaw");
1678 auto sink = makeSinkFor<std::string>({},
true, detail::Publish::No);
1681 auto makeCall = rawCallMaker(*binding, std::move(actionId), std::move(bodyJson), sink);
1682 if (!binding->bindInFlight) {
1683 dispatchWith(*binding, sink, makeCall, cbExec,
false);
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
1694 dispatchWith(*strong, sink, makeCall, cbExec,
true);
1713 std::string actionId, std::string bodyJson,
1715 ::core::async::StopToken stop) {
1716 if (stop.stop_requested()) {
1718 promise.reject(std::make_exception_ptr(::core::async::OperationCancelled{}));
1719 return std::move(cancelled);
1721 auto completion =
executeRaw(binding, std::move(actionId), std::move(bodyJson), cbExec);
1724 completion.state()->linkCancel(std::move(stop));
1748 std::string instanceKey, std::string actionId,
1750 if (instanceKey.empty()) {
1751 throw std::invalid_argument{
"Bridge::executeRawOn: the instance key is empty"};
1753 if (!binding->shared) {
1754 throw std::logic_error{
"Bridge::executeRawOn: the binding is private"};
1757 note(
"Bridge::executeRawOn");
1758 auto sink = makeSinkFor<std::string>({},
true, detail::Publish::No);
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,
true);
1770 template <
typename,
typename>
1774 using Resume = std::function<void(std::exception_ptr)>;
1778 void note(
char const* site)
const noexcept { _affinity.note(site); }
1788 [[nodiscard]] ::morph::exec::detail::OwnerAffinity affinity() const noexcept {
return _affinity; }
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;
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;
1825 void adoptHandler(
const std::shared_ptr<detail::HandlerBinding>& binding) {
1827 adoptOnOwner(binding);
1830 binding->bindInFlight =
true;
1831 _affinity.owner().post([
this, token = _callbacks.token(), binding] {
1832 if (!token.active()) {
1835 binding->bindInFlight =
false;
1836 adoptOnOwner(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);
1849 registerHandler(binding);
1856 static void whenIdle(detail::HandlerBinding& binding, Resume resume) {
1857 if (binding.bindInFlight) {
1858 binding.waiting.push_back(std::move(resume));
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();
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));
1884 for (
auto& resume : std::exchange(binding.waiting, {})) {
1885 out.push_back(std::move(resume));
1891 static void runSuperseded(
const Resume& superseded) {
1893 superseded(std::make_exception_ptr(std::runtime_error{
"bind superseded by a newer one"}));
1905 [[nodiscard]]
static bool holdsNothing(
const detail::HandlerBinding& binding) {
1906 return binding.shared && binding.primary.empty() && binding.currentId.load() == 0U;
1912 [[nodiscard]]
static std::exception_ptr notBound() {
1913 return std::make_exception_ptr(std::runtime_error(
"handler not bound"));
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)) {
1940 if (strong->primary == key && strong->currentId.load() != 0U) {
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)) {
1953 if (bound->primary != key) {
1954 sink->settleException(bound->bindFailure ? bound->bindFailure
1969 template <
typename Sink>
1970 static bool rejectIfGone(
const Sink& sink,
const std::shared_ptr<detail::HandlerBinding>& binding,
1971 const std::exception_ptr& failure) {
1973 sink->settleException(failure);
1977 sink->settleException(std::make_exception_ptr(::morph::backend::HandlerDestroyedError{}));
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{},
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,
2006 .current = ::morph::exec::detail::ModelId{binding.currentId.load()}};
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;
2033 completion = backend->bindModel(std::move(request), _affinity.owner());
2035 applyBind(binding, backend, generation, {}, std::current_exception(), identity);
2038 if (
auto settled = detail::takeSettled(completion)) {
2039 applyBind(binding, backend, generation, settled->id, settled->failure, identity);
2042 awaitBind(binding, backend, generation, std::move(completion), identity);
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();
2060 .
then([
this, token, weak, weakBackend, generation, identity](::morph::exec::detail::ModelId mid) {
2061 if (!token.active()) {
2063 if (
auto owner = weakBackend.lock()) {
2064 owner->deregisterModel(mid);
2068 applyBind(weak.lock(), weakBackend.lock(), generation, mid,
nullptr, identity);
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);
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);
2100 binding->bindInFlight =
false;
2102 binding->bindFailure = failure;
2103 ::morph::log::logError(
"[bridge] bind of '" + binding->typeId +
2104 "' failed: " + detail::describeFailure(failure));
2106 binding->currentId.store(mid.v);
2108 binding->contextKey = *identity;
2109 binding->primary = *identity;
2112 if (
auto own = std::exchange(binding->afterBind, {})) {
2115 drainWaiting(*binding);
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();
2132 ::morph::log::logError(
"[assignHandlerPrimary] promotion of '" + binding->typeId +
2133 "' failed: " + detail::describeFailure(failure));
2138 if (weakBackend.lock() != _backend || !binding->primary.empty()) {
2141 binding->contextKey = primary;
2142 binding->primary = primary;
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) {
2157 backend->deregisterModel(entry.settled->id);
2158 }
catch (
const std::exception& exc) {
2159 ::morph::log::logError(std::string{
"[switchBackend] rollback deregister failed: "} +
2165 std::weak_ptr<::morph::backend::detail::IBackend>
const weak{backend};
2167 .then([weak](::morph::exec::detail::ModelId mid) {
2168 if (
auto owner = weak.lock()) {
2169 owner->deregisterModel(mid);
2172 .onError([](
const std::exception_ptr&) {});
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);
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);
2203 sink->stopSource = std::make_shared<::core::async::StopSource>();
2216 template <
typename R>
2217 void armDeadline(
const std::shared_ptr<detail::BridgeSink<R>>& sink) {
2218 if (_executeDeadline.count() <= 0 || !_timeoutScheduler) {
2226 auto const handle = _timeoutScheduler->schedule(_executeDeadline, [sink, stopSource = sink->stopSource] {
2227 sink->setException(std::make_exception_ptr(::morph::backend::ClientTimeoutError{}));
2229 static_cast<void>(stopSource->request_stop());
2232 sink->armDeadline(handle, _timeoutScheduler);
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&>()))>;
2260 template <
typename Model,
typename Action>
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) {
2266 binding, sink, [&] {
return makeActionCall<Model, Action>(std::move(action), sink->stopSource); }, cbExec,
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) {
2288 note(
"Bridge::dispatch");
2289 auto const raw = binding.currentId.load();
2291 sink->settleException(binding.bindFailure ? binding.bindFailure : notBound());
2294 if (held && sink->alreadySettled()) {
2298 sink->target(::morph::exec::detail::ModelId{raw});
2299 auto call = std::forward<MakeCall>(makeCall)();
2300 call.session = _defaultSession;
2302 _backend->executeInto(::morph::exec::detail::ModelId{raw}, std::move(call), cbExec, sink);
2305 sink->settleException(std::current_exception());
2321 template <
typename Model,
typename Action>
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;
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) {
2345 return ::morph::model::ActionTraits<Action>::toJson(*
static_cast<const Action*
>(actionPtr));
2347 call.deserializeResult = [](std::string_view jsonStr) -> std::shared_ptr<void> {
2349 return std::make_shared<R>(::morph::model::ActionTraits<Action>::resultFromJson(jsonStr));
2351 if constexpr (taskHandler) {
2352 call.localOpAsync = &detail::localTaskOp<Model, Action>;
2353 call.stopSource = std::move(stopSource);
2355 call.localOp = [](::morph::model::detail::IModelHolder& holder,
void* actionPtr) -> std::shared_ptr<void> {
2383 ::morph::forms::recomputeAll(actionRef);
2385 throw ::morph::model::ValidationError{::morph::model::ModelTraits<Model>::typeId(),
2386 ::morph::model::ActionTraits<Action>::typeId()};
2388#ifdef MORPH_CLIENT_ONLY
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 "
2403 auto& model = holder.template into<Model>();
2417#pragma warning(push)
2418#pragma warning(disable : 4702)
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());
2449 if constexpr (::morph::model::detail::actionLoggable<Action>() == ::morph::model::Loggable::Yes) {
2450 if (holder.hasActionLog()) {
2451 std::string resultJson;
2453 resultJson = ::morph::model::ActionTraits<Action>::resultToJson(*result);
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()};
2469 if constexpr (!taskHandler) {
2470 static_cast<void>(stopSource);
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()) {
2486 &_affinity.owner());
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) {
2503 std::vector<Resume> superseded;
2504 for (
auto const& entry : _handlers) {
2505 auto binding = entry.lock();
2506 if (!binding || holdsNothing(*binding)) {
2509 binding->currentId.store(0);
2510 if (
auto own = startBind(binding, rebindRequest(*binding), std::nullopt, {})) {
2511 superseded.push_back(std::move(own));
2514 auto const dropped = std::make_exception_ptr(::morph::backend::DisconnectedError{});
2515 for (
auto& resume : superseded) {
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;
2528 std::chrono::milliseconds _executeDeadline{0};
2529 std::shared_ptr<::morph::async::detail::TimeoutScheduler> _timeoutScheduler;
2533 std::shared_ptr<detail::SubscriptionRegistry<detail::HandlerBinding>> _subscriptions{
2534 std::make_shared<detail::SubscriptionRegistry<detail::HandlerBinding>>()};
2537 std::shared_ptr<std::atomic<std::size_t>> _pendingCalls{std::make_shared<std::atomic<std::size_t>>(0)};
2540 ::morph::async::CallbackScope _callbacks;
2581template <
typename Model,
typename Sharing = NoSharing>
2586 static constexpr bool kShared = std::is_same_v<Sharing, AllowShared>;
2604 static_assert(!
kShared || ::morph::model::KeyedModel<Model>,
2605 "BridgeHandler<Model, AllowShared> requires Model to declare a PrimaryKey alias");
2618 std::shared_ptr<detail::HandlerBinding>
binding)
2620 _liveness{bridge.liveness()},
2621 _affinity{bridge.affinity()},
2623 _binding{std::move(
binding)} {
2624 _bridge.adoptHandler(_binding);
2626 if (_guiExec !=
nullptr && _guiExec != &_affinity.owner()) {
2629 _guiExec->post([affinity = _affinity] { affinity.note(
"BridgeHandler::guiExec"); });
2641 _affinity.note(
"BridgeHandler::~BridgeHandler");
2642 if (_liveness.active()) {
2643 _bridge.deregisterHandler(_binding);
2666 template <
typename Action>
2669 if constexpr (
kShared && ::morph::model::detail::PayloadKeyed<Action>) {
2679 promise.reject(std::current_exception());
2680 return std::move(failed);
2682 return _bridge.template executeAttachedVia<Model, Action>(_binding, std::move(action), _guiExec,
2684 }
else if constexpr (
kShared && ::morph::model::detail::ResultKeyed<Action>) {
2688 return _bridge.template executeCreatingVia<Model, Action>(_binding, std::move(action), _guiExec);
2690 return _bridge.template executeVia<Model, Action>(_binding, std::move(action), _guiExec);
2715 template <
typename Action>
2717 Action action, ::core::async::StopToken stop) {
2719 if (stop.stop_requested()) {
2721 promise.reject(std::make_exception_ptr(::core::async::OperationCancelled{}));
2722 return std::move(cancelled);
2724 auto completion =
execute(std::move(action));
2727 completion.
state()->linkCancel(std::move(stop));
2749 template <
typename M = Model>
2750 void attach(const ::morph::model::PrimaryKeyOf<M>& key)
2753 std::string encoded;
2757 _bridge.refuseAttach(_binding, std::current_exception());
2760 _bridge.template attachHandler<Model>(_binding, std::move(encoded));
2766 template <
typename M = Model>
2767 [[nodiscard]] std::optional<::morph::model::PrimaryKeyOf<M>>
primary()
2770 auto raw = _bridge.bindingPrimary(_binding);
2772 return std::nullopt;
2774 return ::morph::model::keyFromString<::morph::model::PrimaryKeyOf<M>>(raw);
2785 template <
typename M = Model>
2792 std::make_shared<typename ::morph::async::Completion<std::vector<Key>>::Promise>(std::move(promise));
2795 _bridge.template instancesOf<Model>(_bridge.owner())
2796 .thenDetached([answer](
const std::vector<std::string>& raws) {
2798 std::vector<Key> keys;
2799 keys.reserve(raws.size());
2800 for (
const auto& raw : raws) {
2803 answer->resolve(std::move(keys));
2805 answer->reject(std::current_exception());
2808 .onErrorDetached([answer](
const std::exception_ptr& error) { answer->reject(error); });
2809 return std::move(comp);
2825 std::string_view bodyJson) {
2832 actionType,
this, bodyJson);
2842 [[nodiscard]]
bool servesAction(std::string_view actionId)
const noexcept {
2858 [[nodiscard]]
bool onOwner() const noexcept {
return _affinity.here(); }
2881 template <
typename R>
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);
2899 template <
typename R>
2900 void subscribe(const ::morph::async::CallbackScope& scope, std::function<
void(R)> cb) {
2912 template <
typename R>
2919 template <
typename R>
2921 _bridge.removeSubscription(_binding, std::type_index{
typeid(R)});
2929 [[nodiscard]]
const std::shared_ptr<detail::HandlerBinding>&
binding() const MORPH_LIFETIMEBOUND {
2939 ::morph::exec::detail::OwnerAffinity _affinity;
2941 std::shared_ptr<detail::HandlerBinding> _binding;
2964 std::string
typeId, BindSharing sharing = BindSharing::Private, std::string instanceKey = {})
2966 _liveness{bridge.liveness()},
2967 _affinity{bridge.affinity()},
2969 _binding{bridge.bindByType(std::move(typeId), sharing, std::move(instanceKey))} {}
2977 _affinity.note(
"RawHandler::~RawHandler");
2978 if (_liveness.active()) {
2979 _bridge.deregisterHandler(_binding);
2993 return _bridge.executeRaw(_binding, std::move(actionId), std::move(bodyJson), _cbExec);
3003 ::core::async::StopToken stop) {
3004 return _bridge.executeRaw(_binding, std::move(actionId), std::move(bodyJson), _cbExec, std::move(stop));
3019 std::string bodyJson) {
3020 return _bridge.executeRawOn(_binding, std::move(instanceKey), std::move(actionId), std::move(bodyJson),
3032 void attach(std::string instanceKey) { _bridge.attach(_binding, std::move(instanceKey)); }
3036 [[nodiscard]] std::string
primary()
const {
return _bridge.bindingPrimary(_binding); }
3044 [[nodiscard]]
const std::string&
typeId() const noexcept MORPH_LIFETIMEBOUND {
return _binding->typeId; }
3049 ::morph::exec::detail::OwnerAffinity _affinity;
3051 std::shared_ptr<detail::HandlerBinding> _binding;
3062template <
typename Model,
typename Action>
3064 auto makeExecutor = []<
typename Sharing>() {
3067 auto resultState = std::make_shared<::morph::async::detail::CompletionState<std::string>>();
3105 throw std::invalid_argument{
"action failed validation: " +
3111 .then([resultState](
auto result) {
3119 resultState->setException(std::current_exception());
3122 .onError([resultState](
const std::exception_ptr& err) { resultState->setException(err); });
3124 resultState->setException(std::current_exception());
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)}}] =
constexpr Self self
The dialog or drawer a command list runs in.
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...
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...
void registerAction(std::string_view modelId, std::string_view actionId)
Registers the executors for (Model, Action) under the given string ids.
static ActionExecuteRegistry & instance()
Returns the process-level singleton registry.
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...
::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...
RAII wrapper that binds a single model type to a Bridge.
static constexpr bool kShared
Whether this handler participates in the shared instance directory.
::morph::async::Completion< typename ::morph::model::ActionTraits< Action >::Result > execute(Action action)
Dispatches action via the underlying Bridge and returns a Completion.
void subscribe(std::function< void(R)> cb)
Subscribes to results of type R produced on the attached instance.
bool servesAction(std::string_view actionId) const noexcept
Whether Model has an action registered under actionId, without invoking it.
::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.
const std::shared_ptr< detail::HandlerBinding > & binding() const
Returns the underlying HandlerBinding.
::morph::async::Completion< std::vector<::morph::model::PrimaryKeyOf< M > > > instances()
Snapshot of the live shared instance keys for Model.
void attach(const ::morph::model::PrimaryKeyOf< M > &key)
Attaches (or re-points) this handler to the instance for key.
bool isBound() const noexcept
Whether this handler currently has a live backend instance.
bool onOwner() const noexcept
Whether the calling thread is on this handler's owner.
::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...
::morph::exec::IExecutor * guiExecutor() const noexcept
The executor used to deliver this handler's Completion callbacks.
::morph::exec::IExecutor & owner() const noexcept
The executor this handler and its bridge belong to.
void subscribe(::morph::async::CallbackToken token, std::function< void(R)> cb)
Subscribes to results of type R, gated on an already-issued token.
std::optional<::morph::model::PrimaryKeyOf< M > > primary()
This handler's current primary key, or nullopt if unattached.
BridgeHandler(Bridge &bridge, ::morph::exec::IExecutor *guiExec)
Constructs and registers the handler using the default model factory.
void subscribe(const ::morph::async::CallbackScope &scope, std::function< void(R)> cb)
Subscribes to results of type R, gated on scope.
~BridgeHandler()
Deregisters the binding from the bridge, on the owner.
BridgeHandler(Bridge &bridge, ::morph::exec::IExecutor *guiExec, std::shared_ptr< detail::HandlerBinding > binding)
Constructs the handler with a pre-built binding (for dependency injection).
void unsubscribe()
Removes this handler's subscription for R.
Central dispatcher that routes typed actions to an IBackend.
::morph::exec::IExecutor & owner() const noexcept
The executor this bridge belongs to.
::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.
void assignHandlerPrimary(const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary)
Files binding's current instance under primary, in place.
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.
std::shared_ptr< detail::HandlerBinding > registerSharedHandler()
Creates a shared, initially unattached binding for Model.
::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.
std::shared_ptr< detail::HandlerBinding > registerHandler()
Creates and registers a new HandlerBinding for Model.
::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.
void addSubscription(const std::shared_ptr< detail::HandlerBinding > &binding, std::type_index type, std::function< void(const std::any &)> sink, ::morph::exec::IExecutor *exec)
Registers a result-type subscription for binding.
void switchBackend(std::shared_ptr<::morph::backend::detail::IBackend > newBackend)
Replaces the active backend with newBackend.
::morph::session::Principal currentPrincipal() const
Returns a copy of the currently installed Principal.
~Bridge()
Clears the backend's reconnect handler, rejects every call still waiting for a bind with BridgeDestro...
::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...
void deregisterHandler(const std::shared_ptr< detail::HandlerBinding > &binding)
Deregisters binding from the active backend and removes it from tracking.
void switchBackend(std::unique_ptr< Backend > newBackend)
Replaces the active backend with newBackend.
std::string bindingPrimary(const std::shared_ptr< detail::HandlerBinding > &binding) const
Returns binding's current primary key, or empty if unattached.
void registerHandler(const std::shared_ptr< detail::HandlerBinding > &binding)
Tracks binding and issues its bind on the active backend.
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...
::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 ...
void setDefaultSession(::morph::session::Context session)
Installs a default session context that executeVia stamps onto the ActionCall of every subsequent cal...
bool onOwner() const noexcept
Whether the calling thread is on this bridge's owner.
void setExecuteDeadline(std::chrono::milliseconds deadline)
Sets (or disables) the client-side execute deadline.
static bool isBound(const std::shared_ptr< detail::HandlerBinding > &binding) noexcept
Whether binding currently has a live ModelId.
void attachHandler(const std::shared_ptr< detail::HandlerBinding > &binding, std::string primary)
attach for a typed caller.
Bridge(std::unique_ptr<::morph::backend::detail::IBackend > backend, ::morph::exec::IExecutor &owner)
Constructs a bridge that dispatches through backend, owned by owner.
void setPrincipal(::morph::session::Principal principal)
Installs the verified Principal for this Bridge.
std::chrono::milliseconds executeDeadline() const
Returns the currently installed client-side execute deadline.
void publishResult(::morph::exec::detail::ModelId mid, std::type_index type, const std::any &value)
Delivers value to every subscriber attached to instance mid.
::morph::async::Completion< std::vector< std::string > > instancesOf(::morph::exec::IExecutor &cbExec)
Lists the live shared primary keys of Model on the active backend.
::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...
std::size_t pendingCalls() const noexcept
Returns the number of calls made through the bridge — typed (executeVia() and its keyed variants) or ...
void removeSubscription(const std::shared_ptr< detail::HandlerBinding > &binding, std::type_index type)
Removes binding's subscription for type, if any.
::morph::session::Context defaultSession() const
Returns a copy of the currently installed default session.
bool hasSubscribers() const noexcept
Whether any subscription is currently registered on this bridge.
A handler for a model known only by its registered type id.
std::string primary() const
This handler's current instance key.
::morph::async::Completion< std::string > execute(std::string actionId, std::string bodyJson)
Dispatches the action registered as actionId with bodyJson.
::morph::async::Completion< std::string > execute(std::string actionId, std::string bodyJson, ::core::async::StopToken stop)
execute, cancellable through stop.
const std::string & typeId() const noexcept
The model's registered type id.
~RawHandler()
Deregisters the binding, on the owner.
::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...
bool isBound() const noexcept
Whether the handler holds a live instance now.
void attach(std::string instanceKey)
Attaches (or re-points) this shared handler to the instance for instanceKey, as BridgeHandler::attach...
RawHandler(Bridge &bridge, ::morph::exec::IExecutor *cbExec, std::string typeId, BindSharing sharing=BindSharing::Private, std::string instanceKey={})
Binds the model registered as typeId.
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.
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Thrown to in-flight Completions when Bridge::switchBackend() runs.
Thrown to in-flight Completions when Bridge is destroyed.
Thrown to every call a BridgeHandler still holds for its bind when the handler is destroyed before th...
BridgeHandler sharing policy: joins the shared instance directory.
BridgeHandler sharing policy: private, one instance per handler.
Abstract executor interface.
Declares where an action's model key comes from.
Traits specialisation that maps an action type to its id, JSON codec, and result type.
static constexpr bool ready(const Action &action)
Returns true if the action is in a state that should be executed.
Thrown when a model id, or a model's action id, is registered for a type other than the one that alre...
Traits specialisation that maps a model type to its string type-id.
Per-call session context carried through the bridge to the model.
The signed-in user's verified identity, readable outside a dispatch.
A Bridge's instance subscriptions: at most one callback per (binding, result type),...
std::function< void()> Action
A widget event handler taking no arguments.