Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
grouped_list.hpp
Go to the documentation of this file.
1// ============================================================================
2// aria/derived/grouped_list.hpp
3// ----------------------------------------------------------------------------
4// `GroupedList<T, Key>` -- a derived list whose elements are
5// `Group<T, Key>` instances, each bundling a key + an
6// `ObservableList<T>` of the source items that share that key.
7// Joins the family of derived collections (`FilteredList` /
8// `SortedList` / `MappedList` / `DistinctList` / `PagedList`).
9//
10// Semantics (PGR-N IDs, "Pinned GRoup"):
11//
12// PGR-1 (canonical key). Each source item is mapped via
13// `Key key_of(const T&)`. Default Key = T -> identity, requires
14// T to be hashable + equality-comparable.
15//
16// PGR-2 (group identity). A group with a given key is created
17// lazily on first source insert under that key, removed when
18// the last item under that key is removed, and re-created if
19// a new item under that key arrives later. Identity is the
20// Group object's address; observers may bind to per-group
21// ObservableList<T> long-lived.
22//
23// PGR-3 (source-driven). Source insert / remove / replace / reset
24// propagate to the affected groups: the matched group's
25// inner list mutates; the outer GroupedList emits Insert /
26// Remove of `Group` only when groups appear / disappear.
27//
28// PGR-4 (order). Outer GroupedList orders groups by the
29// source position of each group's seed (the first source
30// item that ever entered that group, at the time of entry).
31// When the source inserts a new-group item between two
32// existing source positions p_left < p_right, the new
33// outer slot lands between the outer slots whose seed items
34// sit at p_left / p_right. This mirrors `DistinctList` PD-2
35// and matches what users expect from sectioned table views
36// (sections appear where their first row is). Inner list
37// ordering matches the source order of items in that group.
38//
39// Note: once a group is created, the outer position of the
40// group is *frozen* relative to the other live groups. If
41// the seed item is later removed and another item under the
42// same key remains, the surviving items keep the group at
43// its current outer slot rather than re-anchoring to the new
44// earliest member. This keeps the outer event stream stable
45// (no spurious Move events) and aligns with the DistinctList
46// promote-into-same-slot behaviour (PD-3 Replace).
47//
48// PGR-5 (lifetime). Source destruction is safe (weak source
49// observer); all surviving Group objects continue to answer
50// from their cached inner lists.
51//
52// PGR-6 (ItemChanged with key change). When T's `on_changed`
53// fires AND the new key differs from the current group, the
54// item is removed from the old group and re-inserted into
55// (or registered into) the new group. The outer list emits
56// Remove + Insert if a group disappears / appears as a
57// result; otherwise no outer event.
58// ============================================================================
59#pragma once
60
62#include "aria/list_source.hpp"
64#include "aria/subscription.hpp"
65#include "aria/detail/list_signal_mixin.hpp"
66
67#include <cstddef>
68#include <optional>
69#include <memory>
70#include <mutex>
71#include <shared_mutex>
72#include <unordered_map>
73#include <utility>
74#include <vector>
75
76namespace aria {
77
82template<typename T, typename Key>
83struct Group {
84 Key key;
85 std::shared_ptr<ObservableList<T>> items;
86};
87
88template<typename T, typename Key = T,
89 typename Source = ObservableList<T>>
92 : public detail::ListSignalMixin<GroupedList<T, Key, Source>,
93 Group<T, Key>> {
94 friend detail::ListSignalMixin<GroupedList<T, Key, Source>, Group<T, Key>>;
95
96public:
99 using KeyOf = aria::inplace_function<Key(const T&), 32>;
100 using Signal = detail::ListSignal<Group<T, Key>>;
101
102 GroupedList(std::shared_ptr<Source> source,
103 KeyOf key_of = default_key_of_())
104 : source_(std::move(source)),
105 signal_(std::make_shared<Signal>()),
106 state_(std::make_shared<SharedState>())
107 {
108 state_->key_of = std::move(key_of);
109 rebuild_initial_();
110
111 std::weak_ptr<SharedState> weak_state = state_;
112 std::weak_ptr<Signal> weak_signal = signal_;
113 std::weak_ptr<Source> weak_source{source_};
114 source_sub_ = source_->observe(
115 [weak_state, weak_signal, weak_source](const ListChange<T>& ch) {
116 auto st = weak_state.lock();
117 auto sig = weak_signal.lock();
118 auto src = weak_source.lock();
119 if (!st || !sig || !src) return;
120 handle_source_change_(*st, *sig, ch);
121 });
122 }
123
124 ~GroupedList() = default;
125
126 GroupedList(const GroupedList&) = delete;
128
129 // ── Read surface ──────────────────────────────────────────────────
130 [[nodiscard]] std::size_t size() const {
131 std::shared_lock lk(state_->m);
132 return state_->groups.size();
133 }
134
135 [[nodiscard]] bool empty() const { return size() == 0; }
136
137 [[nodiscard]] std::shared_ptr<Group<T, Key>> at(std::size_t idx) const {
138 std::shared_lock lk(state_->m);
139 return state_->groups.at(idx);
140 }
141
142 [[nodiscard]] std::vector<std::shared_ptr<Group<T, Key>>> snapshot() const {
143 std::shared_lock lk(state_->m);
144 return state_->groups;
145 }
146
150 [[nodiscard]] std::shared_ptr<Group<T, Key>> find(const Key& k) const {
151 std::shared_lock lk(state_->m);
152 auto it = state_->by_key.find(k);
153 if (it == state_->by_key.end()) return nullptr;
154 if (it->second >= state_->groups.size()) return nullptr;
155 return state_->groups[it->second];
156 }
157
158private:
159 struct SourceRow {
160 std::shared_ptr<T> item;
161 Key key;
162 };
163
164 struct InputChange {
165 ListChangeKind kind;
166 std::size_t index;
167 std::size_t from;
168 std::optional<SourceRow> row;
169 std::vector<SourceRow> reset;
170 };
171
172 struct SharedState {
173 mutable std::shared_mutex m;
174 KeyOf key_of;
175 std::vector<std::shared_ptr<Group<T, Key>>> groups;
176 std::unordered_map<Key, std::size_t> by_key;
177 // Source slots, including repeated handles. Replace carries the NEW
178 // pointer, so its old group/position must come from this cache.
179 std::vector<SourceRow> rows;
180 };
181
182 std::shared_ptr<Source> source_;
183 std::shared_ptr<Signal> signal_;
184 std::shared_ptr<SharedState> state_;
185 Subscription source_sub_;
186
187 static KeyOf default_key_of_() {
188 return [](const T& v) -> Key {
189 static_assert(std::is_same_v<Key, T>,
190 "GroupedList: provide a key extractor when Key differs from T.");
191 return v;
192 };
193 }
194
195 static std::vector<SourceRow> source_rows_(SharedState& st, Source& src) {
196 std::vector<SourceRow> rows;
197 for (auto& item : src.snapshot()) {
198 rows.push_back(SourceRow{item, st.key_of(*item)});
199 }
200 return rows;
201 }
202
203 static void rebuild_(SharedState& st, std::vector<SourceRow> rows) {
204 std::vector<std::shared_ptr<Group<T, Key>>> groups;
205 std::unordered_map<Key, std::size_t> by_key;
206 for (const auto& row : rows) {
207 auto [it, inserted] = by_key.emplace(row.key, groups.size());
208 if (inserted) {
209 groups.push_back(std::make_shared<Group<T, Key>>(
210 Group<T, Key>{row.key, std::make_shared<ObservableList<T>>()}));
211 }
212 // These new inner lists have no observers yet.
213 groups[it->second]->items->push_back(row.item);
214 }
215 std::unique_lock lk(st.m);
216 st.rows = std::move(rows);
217 st.groups = std::move(groups);
218 st.by_key = std::move(by_key);
219 }
220
221 void rebuild_initial_() {
222 rebuild_(*state_, source_rows_(*state_, *source_));
223 }
224
225 static void handle_source_change_(SharedState& st, Signal& sig,
226 const ListChange<T>& ch) {
227 InputChange event{ch.kind, ch.index, ch.from_index, {}, {}};
228 if (ch.kind == ListChangeKind::Insert ||
229 ch.kind == ListChangeKind::Replace ||
230 ch.kind == ListChangeKind::ItemChanged) {
231 auto item = ch.item;
232 event.row.emplace(SourceRow{item, st.key_of(*item)});
233 } else if (ch.kind == ListChangeKind::Reset) {
234 for (const auto& item : *ch.snapshot) event.reset.push_back(SourceRow{item, st.key_of(*item)});
235 }
236 apply_(st, sig, std::move(event));
237 }
238
239 // Caller holds st.m. Inner positions follow source slots, not pointer
240 // lookup, so two occurrences of the same shared_ptr remain distinct.
241 static std::size_t inner_position_(const SharedState& st,
242 const Key& key, std::size_t before) {
243 std::size_t count = 0;
244 for (std::size_t i = 0; i < before; ++i) {
245 if (st.rows[i].key == key) ++count;
246 }
247 return count;
248 }
249
250 static void insert_(SharedState& st, Signal& sig,
251 std::size_t index, SourceRow row) {
252 std::shared_ptr<Group<T, Key>> group;
253 std::size_t inner = 0;
254 std::size_t outer = 0;
255 bool created = false;
256 {
257 std::unique_lock lk(st.m);
258 const bool append = index == st.rows.size();
259 auto found = st.by_key.find(row.key);
260 if (found == st.by_key.end()) {
261 outer = st.groups.size();
262 if (!append) {
263 std::unordered_map<Key, bool> preceding;
264 for (std::size_t i = 0; i < index; ++i) {
265 preceding.emplace(st.rows[i].key, true);
266 }
267 outer = preceding.size();
268 }
269 group = std::make_shared<Group<T, Key>>(
270 Group<T, Key>{row.key, std::make_shared<ObservableList<T>>()});
271 if (!append) {
272 for (auto& [key, position] : st.by_key) {
273 if (position >= outer) ++position;
274 }
275 }
276 st.by_key.emplace(row.key, outer);
277 st.groups.insert(st.groups.begin() + static_cast<std::ptrdiff_t>(outer), group);
278 created = true;
279 } else {
280 group = st.groups[found->second];
281 // Source events are serialized through the complete inner
282 // notification. A tail insert therefore follows every member
283 // already in this group, including repeated handles.
284 inner = append ? group->items->size()
285 : inner_position_(st, row.key, index);
286 }
287 st.rows.insert(st.rows.begin() + static_cast<std::ptrdiff_t>(index), row);
288 }
289 group->items->insert(inner, std::move(row.item));
290 if (created) {
291 sig.emit(ListChange<Group<T, Key>>{ListChangeKind::Insert, outer, group, 0});
292 }
293 }
294
295 static void remove_(SharedState& st, Signal& sig, std::size_t index) {
296 std::shared_ptr<Group<T, Key>> group;
297 std::size_t inner;
298 {
299 std::unique_lock lk(st.m);
300 const auto& row = st.rows.at(index);
301 inner = inner_position_(st, row.key, index);
302 group = st.groups[st.by_key.at(row.key)];
303 st.rows.erase(st.rows.begin() + static_cast<std::ptrdiff_t>(index));
304 }
305 // ObservableList mutators synchronously notify their own observers.
306 // Never call them under st.m, even when the group will disappear.
307 group->items->remove_at(inner);
308 if (group->items->empty()) {
309 std::size_t outer;
310 {
311 std::unique_lock lk(st.m);
312 outer = st.by_key.at(group->key);
313 st.by_key.erase(group->key);
314 st.groups.erase(st.groups.begin() + static_cast<std::ptrdiff_t>(outer));
315 for (auto& [key, position] : st.by_key) {
316 if (position > outer) --position;
317 }
318 }
319 sig.emit(ListChange<Group<T, Key>>{ListChangeKind::Remove, outer, group, 0});
320 }
321 }
322
323 static void apply_(SharedState& st, Signal& sig, InputChange&& event) {
324 if (event.kind == ListChangeKind::ItemChanged) {
325 std::vector<std::size_t> occurrences;
326 {
327 std::shared_lock lk(st.m);
328 for (std::size_t i = 0; i < st.rows.size(); ++i) {
329 if (st.rows[i].item == event.row->item && st.rows[i].key != event.row->key) {
330 occurrences.push_back(i);
331 }
332 }
333 }
334 for (const auto index : occurrences) {
335 remove_(st, sig, index);
336 insert_(st, sig, index, *event.row);
337 }
338 return;
339 }
340 switch (event.kind) {
342 insert_(st, sig, event.index, std::move(*event.row));
343 return;
345 remove_(st, sig, event.index);
346 return;
349 std::shared_ptr<Group<T, Key>> group;
350 std::size_t inner = 0;
351 {
352 std::unique_lock lk(st.m);
353 auto& old = st.rows.at(event.index);
354 if (old.key == event.row->key) {
355 inner = inner_position_(st, old.key, event.index);
356 group = st.groups[st.by_key.at(old.key)];
357 old = *event.row;
358 }
359 }
360 if (group) {
361 if (event.kind == ListChangeKind::Replace) {
362 group->items->replace_at(inner, std::move(event.row->item));
363 }
364 // Same-key ItemChanged is emitted by the inner list's own
365 // item subscription; forwarding here would double-notify.
366 } else {
367 remove_(st, sig, event.index);
368 insert_(st, sig, event.index, std::move(*event.row));
369 }
370 return;
371 }
373 std::shared_ptr<Group<T, Key>> group;
374 std::size_t from;
375 std::size_t to;
376 {
377 std::unique_lock lk(st.m);
378 auto row = st.rows.at(event.from);
379 group = st.groups[st.by_key.at(row.key)];
380 from = inner_position_(st, row.key, event.from);
381 st.rows.erase(st.rows.begin() + static_cast<std::ptrdiff_t>(event.from));
382 to = inner_position_(st, row.key, event.index);
383 st.rows.insert(st.rows.begin() + static_cast<std::ptrdiff_t>(event.index), std::move(row));
384 }
385 group->items->move(from, to);
386 return;
387 }
389 rebuild_(st, std::move(event.reset));
390 {
391 std::shared_lock lock(st.m);
392 auto snapshot = st.groups;
393 lock.unlock();
394 sig.emit(ListChange<Group<T, Key>>::reset(std::move(snapshot)));
395 }
396 return;
397 }
398 }
399
400};
401
402// ---------------------------------------------------------------------------
403// Factory helper — deduces the source type so pipelines stay readable.
404// See the note on `aria::filtered` in filtered_list.hpp.
405// ---------------------------------------------------------------------------
406template<typename Key,
407 typename Source,
408 typename KeyFn,
409 typename T = list_source_value_t<Source>>
411[[nodiscard]] std::shared_ptr<GroupedList<T, Key, Source>>
412grouped(std::shared_ptr<Source> source, KeyFn key_of) {
413 using Derived = GroupedList<T, Key, Source>;
414 return std::make_shared<Derived>(
415 std::move(source), typename Derived::KeyOf{std::move(key_of)});
416}
417
418} // namespace aria
Definition grouped_list.hpp:93
GroupedList(std::shared_ptr< Source > source, KeyOf key_of=default_key_of_())
Definition grouped_list.hpp:102
std::vector< std::shared_ptr< Group< T, Key > > > snapshot() const
Definition grouped_list.hpp:142
GroupedList & operator=(const GroupedList &)=delete
std::size_t size() const
Definition grouped_list.hpp:130
std::shared_ptr< Group< T, Key > > at(std::size_t idx) const
Definition grouped_list.hpp:137
std::shared_ptr< Group< T, Key > > find(const Key &k) const
Find a group by key (returns nullptr if no such group exists currently).
Definition grouped_list.hpp:150
~GroupedList()=default
Group< T, Key > value_type
Definition grouped_list.hpp:97
bool empty() const
Definition grouped_list.hpp:135
GroupedList(const GroupedList &)=delete
detail::ListSignal< Group< T, Key > > Signal
Definition grouped_list.hpp:100
aria::inplace_function< Key(const T &), 32 > KeyOf
Owning, heap-free key extractor (capacity 32 bytes).
Definition grouped_list.hpp:99
Observable sequence of owning element handles.
Definition observable_list.hpp:40
Definition inplace_function.hpp:106
Definition list_source.hpp:80
@ Source
Definition node.hpp:88
Definition signal.hpp:12
typename detail::list_source_value_impl< std::remove_cvref_t< L > >::type list_source_value_t
Element type for a list source.
Definition list_source.hpp:76
std::shared_ptr< GroupedList< T, Key, Source > > grouped(std::shared_ptr< Source > source, KeyFn key_of)
Definition grouped_list.hpp:412
ListChangeKind
Definition list_change.hpp:10
@ Replace
Definition list_change.hpp:10
@ Remove
Definition list_change.hpp:10
@ Reset
Definition list_change.hpp:10
@ Move
Definition list_change.hpp:10
@ ItemChanged
Definition list_change.hpp:10
@ Insert
Definition list_change.hpp:10
Definition validation_key.hpp:110
One bucket in a GroupedList<T, Key>.
Definition grouped_list.hpp:83
std::shared_ptr< ObservableList< T > > items
Definition grouped_list.hpp:85
Key key
Definition grouped_list.hpp:84
An owning event in a sequential list edit stream.
Definition list_change.hpp:16