morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
morph::journal::OutboxRelay Struct Reference

Moves committed-but-unrelayed rows from a model's own outbox table to a durable IActionLog, then marks them relayed in the model's own store. More...

#include <outbox.hpp>

Public Member Functions

OutboxRelayResult relay ()
 Moves every row drainOutbox() currently reports to sink, then marks the whole batch relayed via markRelayed.

Public Attributes

std::function< std::vector< LogEntry >()> drainOutbox
 Pulls committed-but-unrelayed rows from the model's own store. Must be non-destructive: the same rows are re-drained until markRelayed records them, including across process restarts.
std::function< void(std::span< const LogEntry >)> markRelayed
 Marks rows relayed in the model's own store (by identity or LogEntry::idempotencyKey) so a later drainOutbox() call no longer returns them.
std::shared_ptr< IActionLog > sink
 Durable audit sink rows are forwarded to.

Detailed Description

Moves committed-but-unrelayed rows from a model's own outbox table to a durable IActionLog, then marks them relayed in the model's own store.

Closes the "two independent writes" gap between a model's own transactional store and the action log: a store-backed model writes its business tables and an outbox row (shaped like LogEntry, including a stable idempotencyKey) in one local transaction — see IModelHolder::setOutboxManaged — and OutboxRelay is the separate, asynchronous step that moves that row into the real durable sink.

drainOutbox/markRelayed are injected callables against the model's own store, exactly as morph::offline::ReconnectCoordinator::Deps and morph::offline::SyncWorker::ReplayFunction inject their side effects — morph::journal never touches the model's database. Mirroring ReconnectCoordinator::Deps, a null member is logged (via morph::log::logError) at the start of every relay() call but is not rejected — invoking a null member still throws (std::bad_function_call) or crashes as usual.

Crash safety
relay() is idempotent and safe to call repeatedly: it appends every row drainOutbox() currently reports to sink, flushes sink, and only then calls markRelayed with the whole batch. A crash between the sink append and markRelayed committing simply means the next relay() call sees the same row again from drainOutbox() — appending it again is a safe no-op as long as sink dedups by LogEntry::idempotencyKey (InMemoryActionLog and FileActionLog do this out of the box — see their class docs; SessionLog deliberately does not, since its contract is full fidelity with nothing dropped). This is at-least-once-plus-dedup, not a two-phase commit: sink and the model's own store are never committed as a single distributed transaction.

Definition at line 67 of file outbox.hpp.

Member Function Documentation

◆ relay()

OutboxRelayResult morph::journal::OutboxRelay::relay ( )
inline

Moves every row drainOutbox() currently reports to sink, then marks the whole batch relayed via markRelayed.

A no-op (returns {.relayed = 0} without touching sink or calling markRelayed) if drainOutbox() returns no rows.

markRelayed runs only after sink->flush() returns normally. An IActionLog that cannot make the batch durable throws (see IActionLog::flush), which propagates out of here before the rows are marked — so they stay in the outbox and a later relay() retries them. This is what keeps the relay at-least-once instead of at-most-once: were the failure swallowed, the rows would be recorded as relayed while nothing reached the sink, and nothing would ever surface the loss.

Runs on sink's owner (see IActionLog::owner()): sink->flush() answers only there, which is what lets its failure reach this call.

Returns
The number of rows relayed in this call.
Exceptions
NullSinkErrorif sink is null and drainOutbox() reports at least one row — thrown before sink is ever dereferenced, so no row is lost or marked relayed. A null sink with nothing to relay is still a no-op, exactly like an empty outbox with a real sink: nothing would touch sink either way.
std::exceptionpropagated from sink->append() or sink->flush(); the batch is left unmarked and therefore retryable.

Definition at line 106 of file outbox.hpp.

Member Data Documentation

◆ drainOutbox

std::function<std::vector<LogEntry>()> morph::journal::OutboxRelay::drainOutbox

Pulls committed-but-unrelayed rows from the model's own store. Must be non-destructive: the same rows are re-drained until markRelayed records them, including across process restarts.

Definition at line 71 of file outbox.hpp.

◆ markRelayed

std::function<void(std::span<const LogEntry>)> morph::journal::OutboxRelay::markRelayed

Marks rows relayed in the model's own store (by identity or LogEntry::idempotencyKey) so a later drainOutbox() call no longer returns them.

Definition at line 76 of file outbox.hpp.

◆ sink

std::shared_ptr<IActionLog> morph::journal::OutboxRelay::sink

Durable audit sink rows are forwarded to.

Definition at line 79 of file outbox.hpp.


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