Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
observable_list.hpp
Go to the documentation of this file.
1#pragma once
2
5#include "aria/detail/list_signal_mixin.hpp"
6#include "aria/detail/list_signal.hpp"
8
9#include <algorithm>
10#include <cstddef>
11#include <functional>
12#include <iterator>
13#include <limits>
14#include <memory>
15#include <mutex>
16#include <shared_mutex>
17#include <string>
18#include <type_traits>
19#include <unordered_map>
20#include <utility>
21#include <vector>
22
23namespace aria {
24
39template<typename T>
40class ObservableList : public detail::ListSignalMixin<ObservableList<T>, T> {
41 friend detail::ListSignalMixin<ObservableList<T>, T>;
42 using Event = ListChange<T>;
43 struct Record {
44 std::size_t index = 0;
45 std::size_t count = 0;
46 bool installing = false;
47 Subscription subscription;
48 };
49 struct SharedState {
50 mutable std::shared_mutex mutex;
51 std::recursive_mutex writer;
52 std::vector<std::shared_ptr<T>> items;
53 std::unordered_map<const T*, Record> records;
54 std::shared_ptr<detail::ListSignal<T>> signal = std::make_shared<detail::ListSignal<T>>();
55 };
56
57public:
58 using value_type = T;
59 using Signal = detail::ListSignal<T>;
60 ObservableList() = default;
65
66 [[nodiscard]] std::size_t size() const { return size_(state_); }
67 [[nodiscard]] bool empty() const { return size() == 0; }
68 [[nodiscard]] std::shared_ptr<T> at(std::size_t index) const {
69 std::shared_lock lock(state_->mutex);
70 return state_->items.at(index);
71 }
72 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
73 std::shared_lock lock(state_->mutex);
74 return state_->items;
75 }
76
78 public:
79 using value_type = std::shared_ptr<T>;
81 typename std::vector<std::shared_ptr<T>>::const_iterator;
83
84 explicit SnapshotRange(std::vector<std::shared_ptr<T>> data) noexcept
85 : data_(std::move(data)) {}
86
87 [[nodiscard]] const_iterator begin() const noexcept { return data_.begin(); }
88 [[nodiscard]] const_iterator end() const noexcept { return data_.end(); }
89 [[nodiscard]] std::size_t size() const noexcept { return data_.size(); }
90 [[nodiscard]] bool empty() const noexcept { return data_.empty(); }
91 [[nodiscard]] const std::shared_ptr<T>& operator[](std::size_t i) const {
92 return data_[i];
93 }
94
95 private:
96 std::vector<std::shared_ptr<T>> data_;
97 };
98
101 [[nodiscard]] SnapshotRange items() const { return SnapshotRange{snapshot()}; }
102
103 void push_back(std::shared_ptr<T> item) {
104 auto state = state_;
105 std::lock_guard sequence(state->writer);
106 std::size_t index;
107 {
108 std::unique_lock lock(state->mutex);
109 index = state->items.size();
110 state->items.push_back(item);
111 auto& record = state->records[item.get()];
112 record.index = index;
113 ++record.count;
114 }
115 emit_(state, Event{ListChangeKind::Insert, index, item, 0}, index + 1,
116 [state, item] { install_(state, item); });
117 }
118
119 template<typename... Args>
120 std::shared_ptr<T> emplace_back(Args&&... args) {
121 auto item = std::make_shared<T>(std::forward<Args>(args)...);
122 push_back(item);
123 return item;
124 }
125
126 void insert(std::size_t index, std::shared_ptr<T> item) {
127 auto state = state_;
128 std::lock_guard sequence(state->writer);
129 {
130 std::unique_lock lock(state->mutex);
131 index = std::min(index, state->items.size());
132 state->items.insert(state->items.begin() + static_cast<std::ptrdiff_t>(index), item);
133 ++state->records[item.get()].count;
134 reindex_(state, index);
135 }
136 emit_(state, Event{ListChangeKind::Insert, index, item, 0}, size_(state),
137 [state, item] { install_(state, item); });
138 }
139
141 template<typename InputIt>
142 void insert_range(std::size_t index, InputIt first, InputIt last) {
143 auto state = state_;
144 std::lock_guard sequence(state->writer);
145 std::vector<std::shared_ptr<T>> pending(first, last);
146 if (pending.empty()) return;
147 std::vector<Event> events;
148 events.reserve(pending.size());
149 {
150 std::unique_lock lock(state->mutex);
151 index = std::min(index, state->items.size());
152 state->items.insert(state->items.begin() + static_cast<std::ptrdiff_t>(index),
153 pending.begin(), pending.end());
154 for (std::size_t i = 0; i < pending.size(); ++i) {
155 ++state->records[pending[i].get()].count;
156 events.push_back({ListChangeKind::Insert, index + i, pending[i], 0});
157 }
158 reindex_(state, index);
159 }
160 emit_batch_(state, std::move(events), size_(state), [&] {
161 for (const auto& item : pending) install_(state, item);
162 });
163 }
164
165 void remove_at(std::size_t index) {
166 auto state = state_;
167 std::lock_guard sequence(state->writer);
168 std::shared_ptr<T> removed;
169 Subscription detached;
170 {
171 std::unique_lock lock(state->mutex);
172 if (index >= state->items.size()) return;
173 removed = state->items[index];
174 state->items.erase(state->items.begin() + static_cast<std::ptrdiff_t>(index));
175 detached = drop_(state, removed.get());
176 reindex_(state, index);
177 }
178 emit_(state, Event{ListChangeKind::Remove, index, removed, 0}, size_(state));
179 }
180
182 void remove_range(std::size_t index, std::size_t count) {
183 auto state = state_;
184 std::lock_guard sequence(state->writer);
185 std::vector<Event> events;
186 std::vector<Subscription> detached;
187 {
188 std::unique_lock lock(state->mutex);
189 if (index >= state->items.size()) return;
190 count = std::min(count, state->items.size() - index);
191 if (count == 0) return;
192 events.reserve(count);
193 for (std::size_t i = 0; i < count; ++i) {
194 events.push_back({ListChangeKind::Remove, index, state->items[index + i], 0});
195 }
196 bool repair_prefix = false;
197 for (const auto& event : events) {
198 const auto record = state->records.find(event.item.get());
199 if (--record->second.count == 0) {
200 if (record->second.subscription) detached.push_back(std::move(record->second.subscription));
201 state->records.erase(record);
202 } else if (record->second.index >= index && record->second.index < index + count) {
203 record->second.index = std::numeric_limits<std::size_t>::max();
204 repair_prefix = true;
205 }
206 }
207 const auto begin = state->items.begin() + static_cast<std::ptrdiff_t>(index);
208 state->items.erase(begin, begin + static_cast<std::ptrdiff_t>(count));
209 reindex_(state, index);
210 if (repair_prefix) {
211 for (std::size_t i = index; i-- > 0;) {
212 auto& record = state->records.at(state->items[i].get());
213 if (record.index == std::numeric_limits<std::size_t>::max()) record.index = i;
214 }
215 }
216 }
217 emit_batch_(state, std::move(events), size_(state));
218 }
219
220 template<std::predicate<const T&> Pred>
221 bool remove_first(Pred&& predicate) {
222 auto state = state_;
223 std::lock_guard sequence(state->writer);
224 std::size_t index;
225 {
226 std::shared_lock lock(state->mutex);
227 index = 0;
228 while (index < state->items.size() && !predicate(*state->items[index])) ++index;
229 if (index == state->items.size()) return false;
230 }
231 ObservableList current{state};
232 current.remove_at(index);
233 return true;
234 }
235
236 template<std::predicate<const T&> Pred>
237 std::size_t remove_all(Pred&& predicate) {
238 auto state = state_;
239 std::lock_guard sequence(state->writer);
240 std::vector<Event> events;
241 std::vector<Subscription> detached;
242 {
243 std::unique_lock lock(state->mutex);
244 std::vector<bool> remove;
245 remove.reserve(state->items.size());
246 // User code completes before any item/map is moved. A throwing
247 // predicate leaves the sequence and all subscriptions untouched.
248 std::size_t removed_count = 0;
249 std::size_t subscriptions = 0;
250 for (const auto& item : state->items) {
251 const bool selected = predicate(*item);
252 remove.push_back(selected);
253 if (selected) {
254 ++removed_count;
255 if constexpr (requires(T& value) { value.on_changed(std::declval<std::function<void(const T&)>>()); }) {
256 if (state->records.at(item.get()).subscription) ++subscriptions;
257 }
258 }
259 }
260 // Allocate event/detachment storage before changing any rows.
261 events.reserve(removed_count);
262 detached.reserve(subscriptions);
263 std::size_t write = 0;
264 for (std::size_t read = 0; read < state->items.size(); ++read) {
265 if (remove[read]) {
266 events.push_back({ListChangeKind::Remove, write, state->items[read], 0});
267 const auto record = state->records.find(state->items[read].get());
268 if (--record->second.count == 0) {
269 if (record->second.subscription) detached.push_back(std::move(record->second.subscription));
270 state->records.erase(record);
271 }
272 } else {
273 state->records.at(state->items[read].get()).index = write;
274 if (read != write) state->items[write] = std::move(state->items[read]);
275 ++write;
276 }
277 }
278 state->items.erase(state->items.begin() + static_cast<std::ptrdiff_t>(write), state->items.end());
279 }
280 const auto count = events.size();
281 emit_batch_(state, std::move(events), size_(state));
282 return count;
283 }
284
285 void replace_at(std::size_t index, std::shared_ptr<T> item) {
286 auto state = state_;
287 std::lock_guard sequence(state->writer);
288 std::shared_ptr<T> previous;
289 Subscription detached;
290 {
291 std::unique_lock lock(state->mutex);
292 if (index >= state->items.size()) return;
293 previous = std::exchange(state->items[index], item);
294 if (previous != item) {
295 detached = drop_(state, previous.get());
296 auto& record = state->records[item.get()];
297 record.index = record.count ? std::max(record.index, index) : index;
298 ++record.count;
299 }
300 }
301 emit_(state, Event{ListChangeKind::Replace, index, item, 0}, size_(state),
302 [state, item] { install_(state, item); });
303 }
304
305 void move(std::size_t from, std::size_t to) {
306 auto state = state_;
307 std::lock_guard sequence(state->writer);
308 std::shared_ptr<T> item;
309 {
310 std::unique_lock lock(state->mutex);
311 auto& items = state->items;
312 if (from == to || from >= items.size() || to >= items.size()) return;
313 item = items[from];
314 const auto first = items.begin() + static_cast<std::ptrdiff_t>(std::min(from, to));
315 const auto last = items.begin() + static_cast<std::ptrdiff_t>(std::max(from, to)) + 1;
316 std::rotate(first, from < to ? first + 1 : last - 1, last);
317 reindex_(state, std::min(from, to));
318 }
319 emit_(state, Event{ListChangeKind::Move, to, item, from}, size_(state));
320 }
321
322 void clear() {
323 auto state = state_;
324 std::lock_guard sequence(state->writer);
325 std::vector<std::shared_ptr<T>> removed;
326 std::unordered_map<const T*, Record> detached;
327 {
328 std::unique_lock lock(state->mutex);
329 removed.swap(state->items);
330 detached.swap(state->records);
331 }
332 emit_(state, Event::cleared(), 0);
333 }
334
336 const void* operator()(const T& value) const noexcept { return &value; }
337 };
338
343 template<typename KeyFn = AddressIdentity>
344 std::size_t reconcile(std::vector<std::shared_ptr<T>> next, KeyFn key_of = {}) {
345 using Key = std::decay_t<std::invoke_result_t<KeyFn, const T&>>;
346 auto state = state_;
347 std::lock_guard sequence(state->writer);
348 ObservableList current{state};
349 std::erase(next, std::shared_ptr<T>{});
350 std::unordered_map<Key, std::size_t> wanted;
351 wanted.reserve(next.size());
352 bool duplicates = false;
353 for (std::size_t i = 0; i < next.size(); ++i) {
354 if (!wanted.emplace(key_of(*next[i]), i).second) duplicates = true;
355 }
356 std::size_t count = 0;
357 state->signal->batch([&] {
358 if (duplicates) {
359 current.clear();
360 current.insert_range(0, next.begin(), next.end());
361 count = next.size() + 1;
362 } else {
363 const auto before = current.snapshot();
364 for (std::size_t i = before.size(); i-- > 0;) {
365 if (!wanted.contains(key_of(*before[i]))) { current.remove_at(i); ++count; }
366 }
367 for (std::size_t target = 0; target < next.size(); ++target) {
368 const auto key = key_of(*next[target]);
369 std::size_t found;
370 std::size_t length;
371 std::shared_ptr<T> item;
372 {
373 std::shared_lock lock(state->mutex);
374 length = state->items.size();
375 found = target;
376 while (found < length && key_of(*state->items[found]) != key) ++found;
377 if (found < length) item = state->items[found];
378 }
379 if (found == length) { current.insert(target, next[target]); ++count; }
380 else {
381 if (found != target) { current.move(found, target); ++count; }
382 if (item != next[target]) { current.replace_at(target, next[target]); ++count; }
383 }
384 }
385 while (current.size() > next.size()) { current.remove_at(current.size() - 1); ++count; }
386 }
387 });
388 return count;
389 }
390
391 [[nodiscard]] std::size_t index_of(const T* item) const {
392 std::shared_lock lock(state_->mutex);
393 const auto found = state_->records.find(item);
394 return found == state_->records.end() ? state_->items.size() : found->second.index;
395 }
396 [[nodiscard]] bool contains(const T* item) const {
397 std::shared_lock lock(state_->mutex);
398 return state_->records.contains(item);
399 }
400
401private:
402 std::shared_ptr<SharedState> state_ = std::make_shared<SharedState>();
403 std::shared_ptr<Signal> signal_ = state_->signal;
404 explicit ObservableList(std::shared_ptr<SharedState> state)
405 : state_(std::move(state)), signal_(state_->signal) {}
406
407 static std::size_t size_(const std::shared_ptr<SharedState>& state) {
408 std::shared_lock lock(state->mutex);
409 return state->items.size();
410 }
411 static void reindex_(const std::shared_ptr<SharedState>& state, std::size_t from) {
412 for (std::size_t i = from; i < state->items.size(); ++i) state->records.at(state->items[i].get()).index = i;
413 }
414 // Caller holds the structural lock; release returned subscriptions outside.
415 static Subscription drop_(const std::shared_ptr<SharedState>& state, const T* item) {
416 const auto found = state->records.find(item);
417 if (--found->second.count == 0) {
418 auto subscription = std::move(found->second.subscription);
419 state->records.erase(found);
420 return subscription;
421 }
422 // Only duplicate removals require this fallback. Unique tail removal
423 // stays O(1), while repeated handles retain their last valid index.
424 for (std::size_t i = state->items.size(); i-- > 0;) {
425 if (state->items[i].get() == item) { found->second.index = i; break; }
426 }
427 return {};
428 }
429 template<typename U = T>
430 static auto subscribe_(const std::shared_ptr<SharedState>& owner, U* item)
431 -> decltype(item->on_changed(std::declval<std::function<void(const U&)>>()), Subscription{}) {
432 std::weak_ptr<SharedState> weak = owner;
433 const T* raw = item;
434 return item->on_changed([weak, raw](const U&) {
435 const auto state = weak.lock();
436 if (!state) return;
437 std::lock_guard sequence(state->writer);
438 Event event;
439 std::vector<Event> repeated;
440 {
441 std::shared_lock lock(state->mutex);
442 const auto found = state->records.find(raw);
443 if (found == state->records.end()) return;
444 if (found->second.count == 1) {
445 const auto index = found->second.index;
446 event = Event{ListChangeKind::ItemChanged, index, state->items[index], 0};
447 } else {
448 repeated.reserve(found->second.count);
449 for (std::size_t i = 0; i < state->items.size(); ++i) {
450 if (state->items[i].get() == raw) repeated.push_back({ListChangeKind::ItemChanged, i, state->items[i], 0});
451 }
452 }
453 }
454 if (repeated.empty()) emit_(state, std::move(event), size_(state));
455 else emit_batch_(state, std::move(repeated), size_(state));
456 });
457 }
458 static Subscription subscribe_(const std::shared_ptr<SharedState>&, ...) { return {}; }
459
460 static void install_(const std::shared_ptr<SharedState>& state, const std::shared_ptr<T>& item) {
461 if (!item) return;
462 {
463 std::unique_lock lock(state->mutex);
464 const auto found = state->records.find(item.get());
465 if (found == state->records.end() || found->second.subscription || found->second.installing) return;
466 found->second.installing = true;
467 }
468 Subscription subscription;
469 try { subscription = subscribe_(state, item.get()); }
470 catch (...) {
471 std::unique_lock lock(state->mutex);
472 const auto found = state->records.find(item.get());
473 if (found != state->records.end()) found->second.installing = false;
474 throw;
475 }
476 std::unique_lock lock(state->mutex);
477 const auto found = state->records.find(item.get());
478 if (found == state->records.end()) return;
479 found->second.installing = false;
480 found->second.subscription = std::move(subscription);
481 }
482
483 static void trace_(const Event& event, std::size_t length) {
484 if (!::aria::has_trace_sink()) return;
485 const char* name = "Reset";
486 switch (event.kind) {
487 case ListChangeKind::Insert: name = "Insert"; break;
488 case ListChangeKind::Remove: name = "Remove"; break;
489 case ListChangeKind::Replace: name = "Replace"; break;
490 case ListChangeKind::Move: name = "Move"; break;
491 case ListChangeKind::ItemChanged: name = "ItemChanged"; break;
492 case ListChangeKind::Reset: break;
493 }
494 ::aria::trace::List payload{std::string{name}, event.index, event.from_index, length};
496 }
497 struct NoPreparation {
498 void operator()() const noexcept {}
499 };
500
501 template<typename Prepare = NoPreparation>
502 static void emit_(const std::shared_ptr<SharedState>& state, Event event,
503 std::size_t length, Prepare prepare = {}) {
504 state->signal->emit(event, std::move(prepare));
505 trace_(event, length);
506 }
507 template<typename Prepare = NoPreparation>
508 static void emit_batch_(const std::shared_ptr<SharedState>& state, std::vector<Event> events,
509 std::size_t length, Prepare prepare = {}) {
511 state->signal->emit_batch(events, std::move(prepare));
512 for (const auto& event : events) trace_(event, length);
513 } else state->signal->emit_batch(std::move(events), std::move(prepare));
514 }
515
516};
517
518} // namespace aria
Definition observable_list.hpp:77
SnapshotRange(std::vector< std::shared_ptr< T > > data) noexcept
Definition observable_list.hpp:84
bool empty() const noexcept
Definition observable_list.hpp:90
std::shared_ptr< T > value_type
Definition observable_list.hpp:79
const std::shared_ptr< T > & operator[](std::size_t i) const
Definition observable_list.hpp:91
const_iterator begin() const noexcept
Definition observable_list.hpp:87
std::size_t size() const noexcept
Definition observable_list.hpp:89
const_iterator end() const noexcept
Definition observable_list.hpp:88
typename std::vector< std::shared_ptr< T > >::const_iterator const_iterator
Definition observable_list.hpp:80
const_iterator iterator
Definition observable_list.hpp:82
Observable sequence of owning element handles.
Definition observable_list.hpp:40
std::vector< std::shared_ptr< T > > snapshot() const
Definition observable_list.hpp:72
std::size_t reconcile(std::vector< std::shared_ptr< T > > next, KeyFn key_of={})
Reconcile by unique key.
Definition observable_list.hpp:344
SnapshotRange items() const
Return a thread-safe, std::ranges-compatible snapshot range over the element handles.
Definition observable_list.hpp:101
std::size_t remove_all(Pred &&predicate)
Definition observable_list.hpp:237
std::size_t size() const
Definition observable_list.hpp:66
void insert_range(std::size_t index, InputIt first, InputIt last)
One O(n + k) insertion and k owning Insert events, in forward order.
Definition observable_list.hpp:142
ObservableList(const ObservableList &)=delete
ObservableList()=default
void push_back(std::shared_ptr< T > item)
Definition observable_list.hpp:103
std::shared_ptr< T > emplace_back(Args &&... args)
Definition observable_list.hpp:120
bool remove_first(Pred &&predicate)
Definition observable_list.hpp:221
void replace_at(std::size_t index, std::shared_ptr< T > item)
Definition observable_list.hpp:285
void remove_range(std::size_t index, std::size_t count)
Removes in forward event order, each at the same replay pivot.
Definition observable_list.hpp:182
void remove_at(std::size_t index)
Definition observable_list.hpp:165
void clear()
Definition observable_list.hpp:322
std::shared_ptr< T > at(std::size_t index) const
Definition observable_list.hpp:68
detail::ListSignal< T > Signal
Definition observable_list.hpp:59
void move(std::size_t from, std::size_t to)
Definition observable_list.hpp:305
std::size_t index_of(const T *item) const
Definition observable_list.hpp:391
ObservableList & operator=(const ObservableList &)=delete
bool empty() const
Definition observable_list.hpp:67
bool contains(const T *item) const
Definition observable_list.hpp:396
ObservableList & operator=(ObservableList &&)=delete
void insert(std::size_t index, std::shared_ptr< T > item)
Definition observable_list.hpp:126
T value_type
Definition observable_list.hpp:58
ObservableList(ObservableList &&)=delete
RAII handle to a single subscription.
Definition subscription.hpp:44
Definition signal.hpp:12
void publish_trace_unchecked(const TraceEvent &event) noexcept
Publish an already-built event using one owning sink snapshot.
Definition diagnostics.hpp:293
bool has_trace_sink() noexcept
True iff a sink is currently installed.
Definition diagnostics.hpp:286
@ List
ObservableList / FilteredList / SortedList / MappedList.
Definition diagnostics.hpp:72
@ 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
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
static ListChange cleared()
Definition list_change.hpp:29
Definition observable_list.hpp:335
const void * operator()(const T &value) const noexcept
Definition observable_list.hpp:336