morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
edits.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/edits.hpp
6/// @brief Committing edited cells: one row's edits in order, stale and error
7/// state per cell, and the row patched with the server's reply.
8///
9/// A commit runs an injected function — the table's mutation — and hands it a
10/// completion. A success carries the row as the server returned it, which
11/// replaces the row through a `RowPatcher`; a failure carries the field error,
12/// which the cell shows. Edits to one row run one at a time, in the order they
13/// were committed, so a later edit never overtakes an earlier one. A cell is
14/// stale from its commit until its edit settles.
15///
16/// With an engine attached, a row holds its place while it has an edit in
17/// flight (`Engine::beginEdit`), which a `Deferred` reorder honours.
18///
19/// See `docs/spec/table/engine.md`.
20
21#include <cstddef>
22#include <deque>
23#include <expected>
24#include <functional>
25#include <map>
26#include <morph/core/callback_scope.hpp>
29#include <optional>
30#include <string>
31#include <string_view>
32#include <unordered_map>
33#include <unordered_set>
34#include <utility>
35#include <vector>
36
37namespace morph::table {
38
39/// @brief One edited cell to commit.
41 /// @brief The row's key.
43 /// @brief The column's id.
44 std::string column;
45 /// @brief The value the user entered.
47};
48
49/// @brief A server's refusal of an edit, attached to the cell.
50struct FieldError {
51 /// @brief The column the error is about.
52 std::string column;
53 /// @brief The message the cell shows.
54 std::string message;
55};
56
57/// @brief Settles one commit, on the owner thread: the row as the server
58/// returned it, or the field error.
59using EditDone = std::function<void(std::expected<std::vector<Cell>, FieldError>)>;
60
61/// @brief Runs a commit: the table's mutation for @p request, calling @p done
62/// once with its outcome.
63using EditCommit = std::function<void(EditRequest const& request, EditDone done)>;
64
65/// @brief Commits cell edits row by row, in order.
66///
67/// Owner-affine. A completion that arrives after the `CellEdits` is destroyed
68/// is ignored, and destroying it ends the engine edit of every row it still
69/// has in flight. The commit function, the patcher and the engine's handlers
70/// may commit again, or destroy this object, from inside a call it makes.
71class CellEdits {
72public:
73 /// @brief Builds the committer.
74 /// @param patcher Replaces a row with the server's reply; must outlive this object.
75 /// @param commit Runs the table's mutation.
76 /// @param engine Optional: the engine whose rows hold their place while
77 /// an edit is in flight; must outlive this object.
78 CellEdits(RowPatcher& patcher, EditCommit commit, Engine* engine = nullptr)
79 : _patcher{&patcher}, _commit{std::move(commit)}, _engine{engine} {}
80
81 CellEdits(CellEdits const&) = delete;
82 CellEdits& operator=(CellEdits const&) = delete;
83 CellEdits(CellEdits&&) = delete;
84 CellEdits& operator=(CellEdits&&) = delete;
85
86 /// @brief Ends the engine edit of every row with an edit in flight or
87 /// settling: its completion will be ignored, so nothing else would.
88 // NOLINTNEXTLINE(bugprone-exception-escape) -- a handler that throws from here terminates, as from any destructor
90 if (_engine == nullptr) {
91 return;
92 }
93 std::vector<RowId> rows{_ending.begin(), _ending.end()};
94 for (auto const& [row, queue] : _queues) {
95 rows.push_back(row);
96 }
97 _queues.clear();
98 _ending.clear();
99 for (auto const& row : rows) {
100 _engine->endEdit(row);
101 }
102 }
103
104 /// @brief Commits an edit: now, or after the row's earlier edits settle.
105 /// @param request The edit.
106 void commit(EditRequest request) {
107 auto& queue = _queues[request.row];
108 queue.push_back(std::move(request));
109 if (queue.size() == 1) {
110 start(queue.front().row);
111 }
112 }
113
114 /// @brief Whether a cell has an edit committed and not yet settled.
115 /// @param row The row's key.
116 /// @param column The column's id.
117 /// @return `true` while stale.
118 [[nodiscard]] bool stale(RowId const& row, std::string_view column) const {
119 auto const found = _queues.find(row);
120 if (found == _queues.end()) {
121 return false;
122 }
123 return std::ranges::any_of(found->second,
124 [&](EditRequest const& request) { return request.column == column; });
125 }
126
127 /// @brief The error a cell shows, until its next edit succeeds.
128 /// @param row The row's key.
129 /// @param column The column's id.
130 /// @return The message, or nothing.
131 [[nodiscard]] std::optional<std::string> error(RowId const& row, std::string_view column) const {
132 auto const found = _errors.find(row);
133 if (found == _errors.end()) {
134 return std::nullopt;
135 }
136 auto const cell = found->second.find(column);
137 return cell == found->second.end() ? std::nullopt : std::optional{cell->second};
138 }
139
140 /// @brief Edits committed and not yet settled, across rows.
141 /// @return The count.
142 [[nodiscard]] std::size_t unsettled() const {
143 std::size_t out = 0;
144 for (auto const& [row, queue] : _queues) {
145 out += queue.size();
146 }
147 return out;
148 }
149
150private:
151 void start(RowId const& row) {
152 if (_engine != nullptr) {
153 _engine->beginEdit(row);
154 }
155 // A copy: a commit that settles at once pops the queue's front, and the
156 // commit may still read its request afterwards.
157 auto const request = _queues.at(row).front();
158 // The commit runs last: it may settle, commit again or destroy this object.
159 _commit(request, _alive.guard([this, row](std::expected<std::vector<Cell>, FieldError> outcome) {
160 settle(row, std::move(outcome));
161 }));
162 }
163
164 // The queue is brought up to date before the patcher is called: patching
165 // notifies the source's listeners at once, and their handlers may commit
166 // to the same row or destroy this object.
167 void settle(RowId const& row, std::expected<std::vector<Cell>, FieldError> outcome) {
168 auto const found = _queues.find(row);
169 if (found == _queues.end() || found->second.empty()) {
170 return;
171 }
172 auto const request = std::move(found->second.front());
173 found->second.pop_front();
174 bool const more = !found->second.empty();
175 if (!more) {
176 _queues.erase(found);
177 _ending.insert(row);
178 }
179 if (outcome) {
180 if (auto const errors = _errors.find(row); errors != _errors.end()) {
181 errors->second.erase(request.column);
182 }
183 auto const alive = _alive.token();
184 _patcher->patchRow(row, std::move(*outcome));
185 if (alive.expired()) {
186 return;
187 }
188 } else {
189 auto const column = outcome.error().column.empty() ? request.column : outcome.error().column;
190 _errors[row].insert_or_assign(column, std::move(outcome.error().message));
191 }
192 if (more) {
193 start(row);
194 return;
195 }
196 _ending.erase(row);
197 if (_queues.contains(row)) {
198 // A commit made while the row was patched started its next edit.
199 return;
200 }
201 if (_engine != nullptr) {
202 _engine->endEdit(row);
203 }
204 }
205
206 RowPatcher* _patcher;
207 EditCommit _commit;
208 Engine* _engine;
209 std::unordered_map<RowId, std::deque<EditRequest>> _queues;
210 std::unordered_set<RowId> _ending; // last edit settled, row being patched
211 std::unordered_map<RowId, std::map<std::string, std::string, std::less<>>> _errors;
212 async::CallbackScope _alive;
213};
214
215} // namespace morph::table
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Definition author.hpp:3394
Handle< T > row()
The innermost row, {"row": ""}.
Definition author.hpp:1145
auto guard(F &&fn) const
Wraps fn so it runs only while this scope is alive and un-stopped.
Commits cell edits row by row, in order.
Definition edits.hpp:71
~CellEdits()
Ends the engine edit of every row with an edit in flight or settling: its completion will be ignored,...
Definition edits.hpp:89
std::optional< std::string > error(RowId const &row, std::string_view column) const
The error a cell shows, until its next edit succeeds.
Definition edits.hpp:131
std::size_t unsettled() const
Edits committed and not yet settled, across rows.
Definition edits.hpp:142
CellEdits(RowPatcher &patcher, EditCommit commit, Engine *engine=nullptr)
Builds the committer.
Definition edits.hpp:78
void commit(EditRequest request)
Commits an edit: now, or after the row's earlier edits settle.
Definition edits.hpp:106
bool stale(RowId const &row, std::string_view column) const
Whether a cell has an edit committed and not yet settled.
Definition edits.hpp:118
Sorts, filters and maps the view of one table.
Definition engine.hpp:661
void beginEdit(RowId const &key)
Marks a row as being edited: with Deferred reorder it keeps its place through updates until endEdit.
Definition engine.hpp:832
Replaces a row by key; what a committed cell edit patches.
The rows a table engine reads, and the services it is given.
std::variant< std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell > Cell
One cell as a data source hands it out.
std::variant< std::int64_t, std::string > RowId
A row's identity: the table's key, stable across reloads.
std::function< void(std::expected< std::vector< Cell >, FieldError >)> EditDone
Settles one commit, on the owner thread: the row as the server returned it, or the field error.
Definition edits.hpp:59
std::function< void(EditRequest const &request, EditDone done)> EditCommit
Runs a commit: the table's mutation for request, calling done once with its outcome.
Definition edits.hpp:63
The table engine: sort, filter and view mapping, on and off the owner.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
One edited cell to commit.
Definition edits.hpp:40
RowId row
The row's key.
Definition edits.hpp:42
Cell value
The value the user entered.
Definition edits.hpp:46
std::string column
The column's id.
Definition edits.hpp:44
A server's refusal of an edit, attached to the cell.
Definition edits.hpp:50
std::string column
The column the error is about.
Definition edits.hpp:52
std::string message
The message the cell shows.
Definition edits.hpp:54