Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
mapped_list.hpp
Go to the documentation of this file.
1#pragma once
2
3// MappedList<Source, Target> — a 1:1 projection of an ObservableList.
4//
5// Given a source list `ObservableList<Source>` and a mapper
6// `Target(const Source&)` (returning `shared_ptr<Target>`), MappedList
7// exposes a parallel list of Targets that tracks the source:
8//
9// source: [Person("alice"), Person("bob"), ...]
10// mapper: [](const Person& p) { return std::make_shared<PersonVM>(p); }
11// ↓
12// mapped: [PersonVM("alice"), PersonVM("bob"), ...]
13//
14// A MappedList is the canonical way to turn a domain-level list into
15// a view-model-level list without wiring per-row subscription glue by
16// hand. The derived list emits the same ListChange<Target> vocabulary
17// ObservableList / FilteredList / SortedList use, so adapters
18// (QtListModel etc.) can consume it identically.
19//
20// Identity preservation:
21// `st.targets[i]` IS the authoritative cache — there is no
22// separate `Source* → Target` map. Move / ItemChanged just leave
23// the slot alone (or move it), so the Target pointer that was
24// live before is still live after. Replace / Remove drop the
25// slot; external shared_ptr refs keep the Target alive if the
26// caller still holds one.
27//
28// ItemChanged policy:
29// By default, ItemChanged propagates as `ItemChanged(j, target_ptr)`
30// *without* re-running the mapper. Rationale: Targets are usually
31// long-lived ViewModels that already subscribe to the Source
32// themselves (via `Source::on_changed` inside `PersonVM`). A
33// re-map would churn the identity for no benefit.
34//
35// Call `MappedList<S,T>(src, mapper, /*remap_on_change=*/true)` to
36// invalidate the slot and re-invoke the mapper on every
37// ItemChanged — suitable when Target is a cheap, immutable
38// snapshot that must rebuild on each update.
39//
40// Event-translation contract:
41//
42// Source event Derived behaviour
43// ──────────────── ──────────────────────────────────────────
44// Insert(i, x) Insert(i, mapper(x))
45// Remove(i) Remove(i, old_target.get())
46// Replace(i, x_new) Replace(i, mapper(x_new))
47// ItemChanged(i) ItemChanged(i, target_ptr) by default;
48// Replace(i, new_target) with remap_on_change
49// Move(from, to) Move(from, to, target_ptr)
50// Reset Reset (fully rebuilt from the post-reset
51// source snapshot)
52//
53// Thread-safety and lifetime: same shape as FilteredList / SortedList.
54
56#include "aria/list_source.hpp"
58#include "aria/subscription.hpp"
59#include "aria/detail/list_signal_mixin.hpp"
60#include "aria/detail/typed_signal.hpp"
61#include "aria/detail/list_replay.hpp"
62
63#include <cstddef>
64#include <memory>
65#include <mutex>
66#include <shared_mutex>
67#include <utility>
68#include <vector>
69
70namespace aria {
71
72template<typename Source, typename Target,
73 typename SourceList = ObservableList<Source>>
76 : public detail::ListSignalMixin<MappedList<Source, Target, SourceList>,
77 Target> {
78 friend detail::ListSignalMixin<MappedList<Source, Target, SourceList>,
79 Target>;
80
81public:
84 using value_type = Target;
87 using Signal = detail::ListSignal<Target>;
88
100 MappedList(std::shared_ptr<SourceList> source,
101 Mapper mapper,
102 bool remap_on_change = false)
103 : source_(std::move(source)),
104 signal_(std::make_shared<Signal>()),
105 state_(std::make_shared<SharedState>())
106 {
107 state_->mapper = std::move(mapper);
108 state_->remap_on_change = remap_on_change;
109
110 // Initial snapshot — map each source item once.
111 {
112 std::unique_lock lk(state_->m);
113 auto snap = source_->snapshot();
114 state_->source_items = snap;
115 state_->targets.reserve(snap.size());
116 for (const auto& s : snap) {
117 state_->targets.push_back(state_->mapper(*s));
118 }
119 }
120
121 // Subscribe on source. Listener holds only weak_ptrs.
122 std::weak_ptr<SharedState> weak_state = state_;
123 std::weak_ptr<Signal> weak_signal = signal_;
124 std::weak_ptr<SourceList> weak_source{source_};
125 source_sub_ = source_->observe(
126 [weak_state, weak_signal, weak_source](const ListChange<Source>& ch) {
127 auto st = weak_state.lock();
128 auto sig = weak_signal.lock();
129 auto src = weak_source.lock();
130 if (!st || !sig || !src) return;
131 dispatch_source_change_(*st, *sig, ch);
132 });
133 }
134
135 ~MappedList() = default;
136
137 MappedList(const MappedList&) = delete;
138 MappedList& operator=(const MappedList&) = delete;
139
140 // ── Read surface ──────────────────────────────────────────────────
141 [[nodiscard]] std::size_t size() const {
142 std::shared_lock lk(state_->m);
143 return state_->targets.size();
144 }
145
146 [[nodiscard]] bool empty() const { return size() == 0; }
147
148 [[nodiscard]] std::shared_ptr<Target> at(std::size_t idx) const {
149 std::shared_lock lk(state_->m);
150 return state_->targets.at(idx);
151 }
152
153 [[nodiscard]] std::vector<std::shared_ptr<Target>> snapshot() const {
154 std::shared_lock lk(state_->m);
155 return state_->targets;
156 }
157
158private:
159 struct SharedState {
160 mutable std::shared_mutex m;
161 Mapper mapper;
162 bool remap_on_change{false};
163 std::vector<std::shared_ptr<Target>> targets;
164 // The source serializes its event stream. Retain a replay mirror so
165 // a failed mapper can recover from the next event, without reading
166 // a source snapshot that may already include a later batch edit.
167 std::vector<std::shared_ptr<Source>> source_items;
168 bool needs_rebuild = false;
169 };
170
171 std::shared_ptr<SourceList> source_;
172 std::shared_ptr<Signal> signal_;
173 std::shared_ptr<SharedState> state_;
174 Subscription source_sub_;
175
176 static void dispatch_source_change_(SharedState& st,
177 Signal& sig,
178 const ListChange<Source>& ch) {
179 detail::replay_list_change(st.source_items, ch);
180 if (st.needs_rebuild) {
181 handle_reset_(st, sig, ListChange<Source>::reset(st.source_items));
182 st.needs_rebuild = false;
183 return;
184 }
185 // If a mapper/allocation fails, the ABI boundary reports it. Preserve
186 // a valid old projection and rebuild before interpreting more indices.
187 st.needs_rebuild = true;
188 switch (ch.kind) {
189 case ListChangeKind::Insert: handle_insert_(st, sig, ch); break;
190 case ListChangeKind::Remove: handle_remove_(st, sig, ch); break;
191 case ListChangeKind::Replace: handle_replace_(st, sig, ch); break;
192 case ListChangeKind::ItemChanged: handle_item_changed_(st, sig, ch); break;
193 case ListChangeKind::Move: handle_move_(st, sig, ch); break;
194 case ListChangeKind::Reset: handle_reset_(st, sig, ch); break;
195 }
196 st.needs_rebuild = false;
197 }
198
199 static void handle_insert_(SharedState& st, Signal& sig,
200 const ListChange<Source>& ch) {
201 auto t = st.mapper(*ch.item);
202 {
203 std::unique_lock lk(st.m);
204 const std::size_t idx = ch.index;
205 st.targets.insert(st.targets.begin()
206 + static_cast<std::ptrdiff_t>(idx), t);
207 }
208 sig.emit(ListChange<Target>{ListChangeKind::Insert, ch.index, t, 0});
209 }
210
211 static void handle_remove_(SharedState& st, Signal& sig,
212 const ListChange<Source>& ch) {
213 std::shared_ptr<Target> removed;
214 {
215 std::unique_lock lk(st.m);
216 const std::size_t idx = ch.index;
217 if (idx >= st.targets.size()) return;
218 removed = st.targets[idx];
219 st.targets.erase(st.targets.begin()
220 + static_cast<std::ptrdiff_t>(idx));
221 }
222 sig.emit(ListChange<Target>{ListChangeKind::Remove, ch.index,
223 removed, 0});
224 }
225
226 static void handle_replace_(SharedState& st, Signal& sig,
227 const ListChange<Source>& ch) {
228 auto t = st.mapper(*ch.item);
229 std::shared_ptr<Target> retired;
230 {
231 std::unique_lock lk(st.m);
232 const std::size_t idx = ch.index;
233 if (idx >= st.targets.size()) return;
234 retired = std::exchange(st.targets[idx], t);
235 }
236 sig.emit(ListChange<Target>{ListChangeKind::Replace, ch.index,
237 t, 0});
238 }
239
240 static void handle_item_changed_(SharedState& st, Signal& sig,
241 const ListChange<Source>& ch) {
242 auto t = st.remap_on_change ? st.mapper(*ch.item) : std::shared_ptr<Target>{};
243 std::shared_ptr<Target> retired;
244 {
245 std::unique_lock lk(st.m);
246 const std::size_t idx = ch.index;
247 if (idx >= st.targets.size()) return;
248
249 if (st.remap_on_change) {
250 retired = std::exchange(st.targets[idx], t);
251 } else {
252 // Preserve Target identity; downstream observers that
253 // want "refresh" semantics should subscribe to the
254 // Source inside their Target.
255 t = st.targets[idx];
256 }
257 }
258 sig.emit(ListChange<Target>{st.remap_on_change ? ListChangeKind::Replace
260 ch.index, t, 0});
261 }
262
263 static void handle_move_(SharedState& st, Signal& sig,
264 const ListChange<Source>& ch) {
265 std::shared_ptr<Target> moved;
266 {
267 std::unique_lock lk(st.m);
268 const std::size_t from = ch.from_index;
269 const std::size_t to = ch.index;
270 if (from == to) return;
271 if (from >= st.targets.size() || to >= st.targets.size()) return;
272
273 moved = st.targets[from];
274 st.targets.erase(st.targets.begin()
275 + static_cast<std::ptrdiff_t>(from));
276 st.targets.insert(st.targets.begin()
277 + static_cast<std::ptrdiff_t>(to), moved);
278 }
279 sig.emit(ListChange<Target>{ListChangeKind::Move, ch.index,
280 moved, ch.from_index});
281 }
282
283 static void handle_reset_(SharedState& st, Signal& sig,
284 const ListChange<Source>& ch) {
285 std::vector<std::shared_ptr<Target>> replacement;
286 replacement.reserve(ch.snapshot->size());
287 for (const auto& item : *ch.snapshot) {
288 replacement.push_back(st.mapper(*item));
289 }
290 auto reset = ListChange<Target>::reset(replacement);
291 {
292 std::unique_lock lk(st.m);
293 st.targets.swap(replacement);
294 }
295 sig.emit(std::move(reset));
296 // Retired targets and their capture destructors run after unlocking.
297
298 }
299};
300
301// ---------------------------------------------------------------------------
302// Factory helper — deduces the source type so pipelines stay readable.
303// See the note on `aria::filtered` in filtered_list.hpp.
304// ---------------------------------------------------------------------------
305template<typename Target,
306 typename SourceList,
307 typename MapFn,
310[[nodiscard]] std::shared_ptr<MappedList<Source, Target, SourceList>>
311mapped(std::shared_ptr<SourceList> source,
312 MapFn mapper,
313 bool remap_on_change = false) {
315 return std::make_shared<Derived>(
316 std::move(source),
317 typename Derived::Mapper{std::move(mapper)},
318 remap_on_change);
319}
320
321} // namespace aria
Definition mapped_list.hpp:77
MappedList & operator=(const MappedList &)=delete
MappedList(std::shared_ptr< SourceList > source, Mapper mapper, bool remap_on_change=false)
Construct a MappedList.
Definition mapped_list.hpp:100
std::vector< std::shared_ptr< Target > > snapshot() const
Definition mapped_list.hpp:153
aria::inplace_function< std::shared_ptr< Target >(const Source &), 32 > Mapper
Owning, heap-free mapper handle (capacity 32 bytes).
Definition mapped_list.hpp:86
~MappedList()=default
bool empty() const
Definition mapped_list.hpp:146
Target value_type
Element type of the derived list — i.e.
Definition mapped_list.hpp:84
detail::ListSignal< Target > Signal
Definition mapped_list.hpp:87
MappedList(const MappedList &)=delete
std::size_t size() const
Definition mapped_list.hpp:141
std::shared_ptr< Target > at(std::size_t idx) const
Definition mapped_list.hpp:148
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
@ 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
std::shared_ptr< MappedList< Source, Target, SourceList > > mapped(std::shared_ptr< SourceList > source, MapFn mapper, bool remap_on_change=false)
Definition mapped_list.hpp:311
Definition validation_key.hpp:110
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
static ListChange reset(Snapshot items)
Definition list_change.hpp:24