6#include <core/platform/Clock.hpp>
23#include "../journal/action_log.hpp"
24#include "../session/session.hpp"
29namespace morph::model {
52template <
typename Tail>
53 requires std::invocable<Tail> && std::is_void_v<std::invoke_result_t<Tail>>
54void runPostCommitTail(Tail&& tail, std::string_view what)
noexcept {
56 std::invoke(std::forward<Tail>(tail));
57 }
catch (
const std::exception& error) {
58 ::morph::log::logError(
"{} committed, but its post-commit tail failed: {}", what, error.what());
60 ::morph::log::logError(
"{} committed, but its post-commit tail threw a non-std::exception", what);
79template <
typename Result,
typename Tail>
80 requires std::invocable<Tail> && std::convertible_to<std::invoke_result_t<Tail>,
Result>
81[[nodiscard]]
Result runPostCommitTail(Tail&& tail,
Result committed,
82 std::string_view what)
noexcept(std::is_nothrow_move_constructible_v<Result>) {
84 return std::invoke(std::forward<Tail>(tail));
85 }
catch (
const std::exception& error) {
86 ::morph::log::logError(
"{} committed, but its post-commit tail failed: {}", what, error.what());
88 ::morph::log::logError(
"{} committed, but its post-commit tail threw a non-std::exception", what);
95namespace morph::model::detail {
97template <
typename Model>
108struct IBackendChangedSink {
109 virtual ~IBackendChangedSink() =
default;
113 virtual void onBackendChanged() = 0;
121concept BackendChangedNotifiable =
requires(M& model) {
122 { model.onBackendChanged() } -> std::same_as<void>;
140concept ModelLevelActionLogAttachable =
141 requires(M& model, std::shared_ptr<::morph::journal::IActionLog> log, std::string key) {
142 { model.attachActionLog(log, key) } -> std::same_as<void>;
161concept ModelIdentityAttachable =
requires(M& model, std::string key) {
162 { model.attachIdentity(key) } -> std::same_as<void>;
168template <
typename M,
bool = BackendChangedNotifiable<M>>
169struct BackendChangedMixin {};
174struct BackendChangedMixin<M, true> : IBackendChangedSink {};
183 virtual ~IModelHolder() =
default;
186 [[nodiscard]]
virtual std::type_index type() const noexcept = 0;
195 [[nodiscard]] virtual
bool isBackendChangeAware() const noexcept = 0;
204 virtual
void onBackendChanged() {}
213 [[nodiscard]] ActionGate& actionGate() noexcept {
return _actionGate; }
219 template <
typename Model>
221 if (type() != std::type_index(
typeid(Model))) {
222 throw std::bad_cast{};
224 return static_cast<ModelHolder<Model>*
>(
this)->model;
250 void attachActionLog(std::shared_ptr<::morph::journal::IActionLog> log, std::string contextKey,
251 ::core::platform::WallClockRef wallClock = ::core::platform::defaultSystemWallClock()) {
252 onActionLogAttached(log, contextKey);
253 _actionLog = std::move(log);
254 _contextKey = std::move(contextKey);
255 _wallClock = wallClock;
276 void attachIdentity(std::string primaryKey) {
277 if (primaryKey.empty()) {
280 onIdentityAttached(primaryKey);
284 [[nodiscard]]
bool hasActionLog() const noexcept {
return static_cast<bool>(_actionLog); }
299 void setOutboxManaged(
bool outboxManaged)
noexcept { _outboxManaged = outboxManaged; }
302 [[nodiscard]]
bool isOutboxManaged() const noexcept {
return _outboxManaged; }
316 void recordIfAttached(::morph::journal::LogEntry entry) {
317 if (!_actionLog || _outboxManaged) {
321 if (
const auto* ctx = ::morph::session::current()) {
325 std::chrono::duration_cast<std::chrono::milliseconds>(_wallClock.now().time_since_epoch()).count();
326 _actionLog->append(std::move(entry));
341 virtual void onActionLogAttached(
const std::shared_ptr<::morph::journal::IActionLog>& log,
342 const std::string& contextKey) {
356 virtual void onIdentityAttached(
const std::string& primaryKey) { (void)primaryKey; }
359 ActionGate _actionGate;
360 std::shared_ptr<::morph::journal::IActionLog> _actionLog;
361 std::string _contextKey;
362 ::core::platform::WallClockRef _wallClock{::core::platform::defaultSystemWallClock()};
363 bool _outboxManaged{
false};
371template <
typename Model>
372struct ModelHolder : IModelHolder, BackendChangedMixin<Model> {
377 template <
typename... Args>
378 explicit ModelHolder(Args&&... args) : model{std::forward<Args>(args)...} {}
381 [[nodiscard]] std::type_index type() const noexcept
override {
return typeid(
Model); }
384 [[nodiscard]]
bool isBackendChangeAware() const noexcept
override {
return BackendChangedNotifiable<Model>; }
393 void onBackendChanged()
override {
394 if constexpr (BackendChangedNotifiable<Model>) {
395 model.onBackendChanged();
402 void onActionLogAttached(
const std::shared_ptr<::morph::journal::IActionLog>& log,
403 const std::string& contextKey)
override {
404 if constexpr (ModelLevelActionLogAttachable<Model>) {
405 model.attachActionLog(log, contextKey);
411 void onIdentityAttached(
const std::string& primaryKey)
override {
412 if constexpr (ModelIdentityAttachable<Model>) {
413 model.attachIdentity(primaryKey);
432 template <
typename Model>
433 static std::unique_ptr<IModelHolder> create() {
434 auto holder = std::make_unique<ModelHolder<Model>>();
435 if (
auto log = ::morph::journal::defaultActionLog()) {
436 holder->attachActionLog(std::move(log), {});
morph's strands: one per model instance, from core-cpp's core::async::KeyedStrands,...
std::string principal
Auth principal from morph::session::current(), if any. Empty if unset.
std::string entityKey
Stable identity of the model instance (e.g. an account id), stamped from the value passed to attachAc...
int64_t timestampMs
Wall-clock time of execution, milliseconds since the Unix epoch.
What drives a model's Task handler: the driver coroutine that starts and finishes it,...