Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
paged_list.hpp
Go to the documentation of this file.
1// ============================================================================
2// aria/derived/paged_list.hpp
3// ----------------------------------------------------------------------------
4// `PagedList<T>` -- a window onto a slice [page_index*page_size,
5// (page_index+1)*page_size) of an upstream `ObservableList<T>`.
6// Joins the family of derived collections and follows the
7// incremental contract of LD-2 / LD-7.
8//
9// Semantics (PG-N IDs):
10//
11// PG-1 (window). The derived list mirrors the source slice in
12// source order. Items outside the window are not observable
13// through PagedList.
14//
15// PG-2 (live page properties). `page_index` and `page_size` are
16// public `Property`s. Changing either re-windows synchronously
17// and emits an Insert/Remove/Move diff.
18//
19// PG-3 (source-driven update). Source insert / remove / replace /
20// item-changed events that fall inside the current window
21// propagate to the derived list with their derived-position
22// translated to window-local coordinates. Events outside the
23// window may slide the window content (insert before window
24// pushes a new last-item in; remove before window pulls an
25// item in from the next page).
26//
27// PG-4 (page count). `page_count()` reports the number of pages
28// for the current source size + page_size, using
29// ceil-division. `is_last_page()` is a convenience.
30//
31// PG-5 (lifetime). Source destruction is safe (weak source
32// observer); the cached window vector is preserved.
33// ============================================================================
34#pragma once
35
36#include "aria/list_source.hpp"
38#include "aria/property.hpp"
39#include "aria/subscription.hpp"
40#include "aria/detail/list_signal_mixin.hpp"
41
42#include <algorithm>
43#include <cstddef>
44#include <optional>
45#include <functional>
46#include <memory>
47#include <mutex>
48#include <shared_mutex>
49#include <unordered_set>
50#include <utility>
51#include <vector>
52
53namespace aria {
54
55template<typename T, typename Source = ObservableList<T>>
58 : public detail::ListSignalMixin<PagedList<T, Source>, T> {
59 friend detail::ListSignalMixin<PagedList<T, Source>, T>;
60
61public:
62 using value_type = T;
63 using Signal = detail::ListSignal<T>;
64
68 PagedList(std::shared_ptr<Source> source,
69 std::size_t initial_page_size,
70 std::size_t initial_page_index = 0)
71 : page_size_prop_{initial_page_size == 0 ? std::size_t{1}
72 : initial_page_size},
73 page_index_prop_{initial_page_index},
74 source_(std::move(source)),
75 signal_(std::make_shared<Signal>()),
76 state_(std::make_shared<SharedState>())
77 {
78 rebuild_window_();
79
80 std::weak_ptr<Source> weak_source{source_};
81 std::weak_ptr<bool> weak_alive = alive_;
82 source_sub_ = source_->observe(
83 [this, weak_source, weak_alive](const ListChange<T>& ch) {
84 auto alive = weak_alive.lock();
85 if (!alive || !*alive) return;
86 if (!weak_source.lock()) return;
87 handle_source_change_(ch);
88 });
89
90 page_index_sub_ = page_index_prop_.on_changed(
91 [this, weak_alive](std::size_t /*v*/) {
92 auto alive = weak_alive.lock();
93 if (alive && *alive) rebuild_and_emit_();
94 });
95 page_size_sub_ = page_size_prop_.on_changed(
96 [this, weak_alive](std::size_t /*v*/) {
97 auto alive = weak_alive.lock();
98 if (alive && *alive) rebuild_and_emit_();
99 });
100 }
101
102 ~PagedList() { *alive_ = false; }
103
104 PagedList(const PagedList&) = delete;
105 PagedList& operator=(const PagedList&) = delete;
106
107 // ── Read surface ──────────────────────────────────────────────────
108 [[nodiscard]] std::size_t size() const {
109 std::shared_lock lk(state_->m);
110 return state_->window.size();
111 }
112
113 [[nodiscard]] bool empty() const { return size() == 0; }
114
115 [[nodiscard]] std::shared_ptr<T> at(std::size_t derived_pos) const {
116 std::shared_lock lk(state_->m);
117 return state_->window.at(derived_pos);
118 }
119
120 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
121 std::shared_lock lk(state_->m);
122 return state_->window;
123 }
124
127 [[nodiscard]] std::size_t page_count() const {
128 const std::size_t total = source_->size();
129 const std::size_t ps = std::max<std::size_t>(1, page_size_prop_.peek());
130 return total / ps + (total % ps != 0 ? 1 : 0);
131 }
132
133 [[nodiscard]] bool is_last_page() const {
134 const std::size_t pc = page_count();
135 return pc == 0 || page_index_prop_.peek() >= pc - 1;
136 }
137
138 // ── Public live properties (PG-2) -----------------------------------
140 [[nodiscard]] Property<std::size_t>& page_size() noexcept {
141 return page_size_prop_;
142 }
143 [[nodiscard]] const Property<std::size_t>& page_size() const noexcept {
144 return page_size_prop_;
145 }
146
148 [[nodiscard]] Property<std::size_t>& page_index() noexcept {
149 return page_index_prop_;
150 }
151 [[nodiscard]] const Property<std::size_t>& page_index() const noexcept {
152 return page_index_prop_;
153 }
154
155private:
156 struct InputChange {
157 std::optional<ListChange<T>> source_change;
158 std::size_t page_size;
159 std::size_t page_index;
160 };
161
162 struct SharedState {
163 mutable std::shared_mutex m;
164 std::vector<std::shared_ptr<T>> window;
165 // Upstream may emit a multi-event diff after computing its final
166 // snapshot. Replaying the source slots prevents a refill from using
167 // that final snapshot for every intermediate Remove/Move.
168 std::vector<std::shared_ptr<T>> source_items;
169 std::size_t page_size = 1;
170 std::size_t page_index = 0;
171 };
172
173 Property<std::size_t> page_size_prop_;
174 Property<std::size_t> page_index_prop_;
175
176 std::shared_ptr<Source> source_;
177 std::shared_ptr<Signal> signal_;
178 std::shared_ptr<SharedState> state_;
179 std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
180
181 Subscription source_sub_;
182 Subscription page_index_sub_;
183 Subscription page_size_sub_;
184
185 static std::vector<std::shared_ptr<T>> compute_window_(
186 const std::vector<std::shared_ptr<T>>& items,
187 std::size_t page_size, std::size_t page_index) {
188 const auto size = std::max<std::size_t>(1, page_size);
189 // Check before multiplying; both page parameters are public size_t.
190 if (items.empty() || page_index > (items.size() - 1) / size) return {};
191 const auto start = page_index * size;
192 const auto count = std::min(size, items.size() - start);
193 const auto first = items.begin() + static_cast<std::ptrdiff_t>(start);
194 return {first, first + static_cast<std::ptrdiff_t>(count)};
195 }
196
197 void rebuild_window_() {
198 auto items = source_->snapshot();
199 const auto page_size = std::max<std::size_t>(1, page_size_prop_.peek());
200 const auto page_index = page_index_prop_.peek();
201 auto window = compute_window_(items, page_size, page_index);
202 std::unique_lock lk(state_->m);
203 state_->source_items = std::move(items);
204 state_->window = std::move(window);
205 state_->page_size = page_size;
206 state_->page_index = page_index;
207 }
208
209 void rebuild_and_emit_() {
210 auto state = state_;
211 auto signal = signal_;
212 apply_(*state, *signal, InputChange{
213 {}, page_size_prop_.peek(), page_index_prop_.peek()});
214 }
215
216 void handle_source_change_(const ListChange<T>& change) {
217 auto state = state_;
218 auto signal = signal_;
219 apply_(*state, *signal, InputChange{change, page_size_prop_.peek(), page_index_prop_.peek()});
220 }
221
222 static void apply_(SharedState& state, Signal& signal, InputChange event) {
223 std::vector<std::shared_ptr<T>> before;
224 std::vector<std::shared_ptr<T>> after;
225 std::optional<ListChange<T>> direct;
226 bool same_page = false;
227 {
228 std::unique_lock lk(state.m);
229 auto& items = state.source_items;
230 const auto page_size = std::max<std::size_t>(1, event.page_size);
231 same_page = state.page_size == page_size && state.page_index == event.page_index;
232 if (event.source_change &&
233 event.source_change->kind == ListChangeKind::Insert &&
234 event.source_change->index == items.size() &&
235 same_page &&
236 state.window.size() == page_size) {
237 // A full, unchanged page cannot see a tail insertion. Still
238 // replay the owned item so later page changes/refills use it.
239 // Compare applied parameters: a reactive batch may already
240 // have changed the Properties without re-windowing yet.
241 items.push_back(event.source_change->item);
242 return;
243 }
244 before = state.window;
245 if (event.source_change) {
246 const auto& ch = *event.source_change;
247 const auto pos = static_cast<std::ptrdiff_t>(ch.index);
248 switch (ch.kind) {
249 case ListChangeKind::Insert: items.insert(items.begin() + pos, ch.item); break;
250 case ListChangeKind::Remove: items.erase(items.begin() + pos); break;
251 case ListChangeKind::Replace: items.at(ch.index) = ch.item; break;
252 case ListChangeKind::ItemChanged: break;
253 case ListChangeKind::Reset: items = *ch.snapshot; break;
255 auto moved = items.at(ch.from_index);
256 items.erase(items.begin() + static_cast<std::ptrdiff_t>(ch.from_index));
257 items.insert(items.begin() + pos, std::move(moved));
258 break;
259 }
260 }
261 }
262 after = compute_window_(items, event.page_size, event.page_index);
263 state.window = after;
264 state.page_size = page_size;
265 state.page_index = event.page_index;
266 if (event.source_change) {
267 const auto& ch = *event.source_change;
268 if (ch.kind == ListChangeKind::Reset) {
269 direct = ListChange<T>::reset(after);
270 } else if ((ch.kind == ListChangeKind::Replace ||
271 ch.kind == ListChangeKind::ItemChanged) && !after.empty()) {
272 const auto start = event.page_index * std::max<std::size_t>(1, event.page_size);
273 if (ch.index >= start && ch.index - start < after.size()) {
274 direct = ListChange<T>{ch.kind, ch.index - start, ch.item, 0};
275 }
276 }
277 }
278 }
279 if (direct && (same_page || direct->kind == ListChangeKind::Reset))
280 signal.emit(*direct);
281 else
282 emit_diff_(signal, before, after, std::move(direct));
283 }
284
285 // Keep surviving handles in place whenever possible; repeated handles
286 // retain their multiplicity. Both snapshots own all emitted payloads.
287 static void emit_diff_(Signal& sig,
288 const std::vector<std::shared_ptr<T>>& before,
289 const std::vector<std::shared_ptr<T>>& after,
290 std::optional<ListChange<T>> refresh = {}) {
291 std::vector<ListChange<T>> changes;
292 changes.reserve(before.size() + after.size());
293 std::unordered_set<const T*> in_after;
294 in_after.reserve(after.size());
295 for (const auto& p : after) in_after.insert(p.get());
296
297 std::vector<std::shared_ptr<T>> work = before;
298 for (std::ptrdiff_t i = static_cast<std::ptrdiff_t>(work.size()) - 1;
299 i >= 0; --i) {
300 const auto u = static_cast<std::size_t>(i);
301 if (!in_after.count(work[u].get())) {
302 changes.push_back(ListChange<T>{ListChangeKind::Remove, u,
303 work[u], 0});
304 work.erase(work.begin() + i);
305 }
306 }
307 for (std::size_t i = 0; i < after.size(); ++i) {
308 const T* want = after[i].get();
309 if (i < work.size() && work[i].get() == want) continue;
310
311 const auto pos = work.begin() + static_cast<std::ptrdiff_t>(i);
312 const auto existing = std::find_if(pos, work.end(),
313 [want](const auto& item) { return item.get() == want; });
314 if (existing != work.end()) {
315 const auto from = static_cast<std::size_t>(existing - work.begin());
316 std::rotate(pos, existing, existing + 1);
317 changes.push_back(ListChange<T>{ListChangeKind::Move, i, after[i], from});
318 continue;
319 }
320
321 work.insert(work.begin() + static_cast<std::ptrdiff_t>(i),
322 after[i]);
323 changes.push_back(ListChange<T>{ListChangeKind::Insert, i,
324 after[i], 0});
325 }
326
327 // Membership alone does not account for repeated handles. A
328 // smaller window may retain an identity but fewer occurrences.
329 while (work.size() > after.size()) {
330 const auto index = work.size() - 1;
331 auto removed = work.back();
332 work.pop_back();
333 changes.push_back(ListChange<T>{ListChangeKind::Remove, index,
334 removed, 0});
335 }
336 // A source content event may also apply pending page parameters.
337 // Re-window first, then refresh even when the identities match. Keep
338 // both in one batch so reentrant emissions cannot split their order.
339 if (refresh) changes.push_back(std::move(*refresh));
340 sig.emit_batch(std::move(changes));
341 }
342};
343
344// ---------------------------------------------------------------------------
345// Factory helper — deduces the source type so pipelines stay readable.
346// See the note on `aria::filtered` in filtered_list.hpp.
347// ---------------------------------------------------------------------------
348template<typename Source, typename T = list_source_value_t<Source>>
350[[nodiscard]] std::shared_ptr<PagedList<T, Source>>
351paged(std::shared_ptr<Source> source,
352 std::size_t page_size,
353 std::size_t page_index = 0) {
354 return std::make_shared<PagedList<T, Source>>(
355 std::move(source), page_size, page_index);
356}
357
358} // namespace aria
~PagedList()
Definition paged_list.hpp:102
Property< std::size_t > & page_size() noexcept
Window size in items per page. Set to drive a re-window.
Definition paged_list.hpp:140
PagedList & operator=(const PagedList &)=delete
const Property< std::size_t > & page_index() const noexcept
Definition paged_list.hpp:151
std::size_t page_count() const
Number of pages for the current source size + page_size.
Definition paged_list.hpp:127
const Property< std::size_t > & page_size() const noexcept
Definition paged_list.hpp:143
PagedList(const PagedList &)=delete
Property< std::size_t > & page_index() noexcept
0-based page index. Set to drive a re-window.
Definition paged_list.hpp:148
bool is_last_page() const
Definition paged_list.hpp:133
detail::ListSignal< T > Signal
Definition paged_list.hpp:63
T value_type
Definition paged_list.hpp:62
std::shared_ptr< T > at(std::size_t derived_pos) const
Definition paged_list.hpp:115
std::size_t size() const
Definition paged_list.hpp:108
bool empty() const
Definition paged_list.hpp:113
std::vector< std::shared_ptr< T > > snapshot() const
Definition paged_list.hpp:120
PagedList(std::shared_ptr< Source > source, std::size_t initial_page_size, std::size_t initial_page_index=0)
Construct a PagedList.
Definition paged_list.hpp:68
Definition property.hpp:103
Definition list_source.hpp:80
Definition signal.hpp:12
std::shared_ptr< PagedList< T, Source > > paged(std::shared_ptr< Source > source, std::size_t page_size, std::size_t page_index=0)
Definition paged_list.hpp:351
@ 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 reset(Snapshot items)
Definition list_change.hpp:24