morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
change_coalescer.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstddef>
5#include <cstdint>
6#include <set>
7#include <string>
8#include <string_view>
9#include <tuple>
10#include <unordered_map>
11#include <unordered_set>
12#include <utility>
13#include <vector>
14
15#include "../change_types.hpp"
16#include "../wire.hpp"
17
18/// @file
19/// @brief What `RemoteServer` has queued for one connection's next `changes`
20/// event.
21
22namespace morph::backend::detail {
23
24/// @brief The queue size at which `RemoteServer` sends a connection's queued
25/// changes without waiting for its coalescing window to close.
26///
27/// Counted as `ChangeCoalescer::keyCount()` counts. It bounds how much one event
28/// carries, so a burst of keyed writes is sent in pieces a client can refetch
29/// for, rather than held until one body nears `effects::kMaxChangeKeys`.
30inline constexpr std::size_t kCoalesceKeyLimit = 256;
31
32/// @brief The changes queued for one connection, merged into the body of its
33/// next `changes` event.
34///
35/// - **Records are merged per type.** Keys are appended in arrival order, each
36/// key text once.
37/// - **A whole-type change absorbs keys.** A record queued without keys (a
38/// `changes` effect, or a type whose keys were stripped for this recipient)
39/// stays whole-type: the keys queued before it are discarded and the keys
40/// queued after it are ignored. A merge never adds keys to a whole-type entry,
41/// so a recipient that was sent no keys for a record is sent none.
42/// - **Never more than `effects::kMaxChangeKeys` keys in one record.** A record
43/// whose merged keys would exceed it becomes whole-type, which over-notifies
44/// but never misses, and which the client's reader would otherwise refuse.
45/// - **Instance topics are deduplicated** on `{modelType, primary, instance}`.
46///
47/// Not synchronised: `RemoteServer` keeps one per connection on its strand.
48class ChangeCoalescer {
49public:
50 /// @brief Queues what one execute changed.
51 /// @param changes The changes to merge into the queue.
52 void add(::morph::effects::Changes const& changes) {
53 for (auto const& change : changes.records) {
54 addRecord(change);
55 }
56 for (auto const& change : changes.instances) {
57 addInstance(change.modelType, change.primary, change.instance);
58 }
59 }
60
61 /// @brief Queues one recipient's share of a fan-out, already narrowed for it.
62 /// @param body The topics this connection is to be sent.
63 void add(::morph::wire::ChangesBody const& body) {
64 for (auto const& change : body.records) {
65 addRecord(change);
66 }
67 for (auto const& topic : body.instances) {
68 addInstance(topic.modelType, topic.primary, topic.instance);
69 }
70 }
71
72 /// @brief How much is queued: one per key, plus one per whole-type record
73 /// and one per instance topic.
74 ///
75 /// A topic without keys is counted, so the count bounds the topics of the
76 /// next event as well as its keys.
77 /// @return The count `kCoalesceKeyLimit` is compared with.
78 [[nodiscard]] std::size_t keyCount() const { return _keyCount; }
79
80 /// @brief Whether nothing is queued.
81 /// @return `true` when `take()` would return an empty body.
82 [[nodiscard]] bool empty() const { return _records.empty() && _instances.empty(); }
83
84 /// @brief Hands over the merged body and empties the queue.
85 /// @return Every queued record, in the order its type was first queued, and
86 /// every distinct instance topic, in the order first queued.
87 [[nodiscard]] ::morph::wire::ChangesBody take() {
88 ::morph::wire::ChangesBody body;
89 body.records.reserve(_records.size());
90 for (auto& entry : _records) {
91 ::morph::effects::RecordChange change{.record = std::move(entry.record), .keys = std::nullopt};
92 if (!entry.whole) {
93 change.keys = std::move(entry.keys);
94 }
95 body.records.push_back(std::move(change));
96 }
97 body.instances = std::move(_instances);
98 _records.clear();
99 _recordIndex.clear();
100 _instances.clear();
101 _seenInstances.clear();
102 _keyCount = 0;
103 return body;
104 }
105
106private:
107 /// One record type's merged change.
108 struct Entry {
109 std::string record;
110 bool whole{false};
111 std::vector<glz::raw_json> keys;
112 std::unordered_set<std::string> seen;
113 };
114
115 void addRecord(::morph::effects::RecordChange const& change) {
116 auto [found, inserted] = _recordIndex.try_emplace(change.record, _records.size());
117 if (inserted) {
118 _records.push_back(Entry{.record = change.record, .whole = false, .keys = {}, .seen = {}});
119 }
120 Entry& entry = _records.at(found->second);
121 if (entry.whole) {
122 return;
123 }
124 if (!change.keys) {
125 makeWhole(entry);
126 return;
127 }
128 for (auto const& key : *change.keys) {
129 if (entry.seen.insert(key.str).second) {
130 entry.keys.push_back(key);
131 ++_keyCount;
132 }
133 }
134 if (entry.keys.size() > ::morph::effects::kMaxChangeKeys) {
135 makeWhole(entry);
136 }
137 }
138
139 void makeWhole(Entry& entry) {
140 _keyCount -= entry.keys.size();
141 ++_keyCount;
142 entry.whole = true;
143 entry.keys = {};
144 entry.seen = {};
145 }
146
147 void addInstance(std::string_view modelType, std::string_view primary, std::uint64_t instance) {
148 if (_seenInstances.emplace(std::string{modelType}, std::string{primary}, instance).second) {
149 _instances.push_back(::morph::wire::InstanceTopic{
150 .modelType = std::string{modelType}, .primary = std::string{primary}, .instance = instance});
151 ++_keyCount;
152 }
153 }
154
155 std::vector<Entry> _records;
156 std::unordered_map<std::string, std::size_t> _recordIndex;
157 std::vector<::morph::wire::InstanceTopic> _instances;
158 std::set<std::tuple<std::string, std::string, std::uint64_t>> _seenInstances;
159 std::size_t _keyCount{0};
160};
161
162} // namespace morph::backend::detail
std::vector< RecordChange > records
Record topics, one entry per record type.
std::vector< InstanceChange > instances
Instance topics.
std::string record
The record's wire name.
std::optional< std::vector< glz::raw_json > > keys
The keys of the rows that changed, each as raw JSON; absent when the whole type changed.
std::vector< effects::RecordChange > records
Record changes; a record with no keys changed as a whole.
Definition wire.hpp:701
std::vector< InstanceTopic > instances
Instance changes.
Definition wire.hpp:703