Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
sorted_list.hpp
Go to the documentation of this file.
1#pragma once
2
3// SortedList<T> — a live-updating sorted view over an ObservableList<T>.
4//
5// Given a source list and a comparator, SortedList exposes the items of
6// the source, reordered to satisfy `comparator(a, b)` as a strict weak
7// ordering. Equivalent items (neither `cmp(a,b)` nor `cmp(b,a)` is
8// true) retain their source order — SortedList is a STABLE sort, and
9// stable at runtime too: no insert, replace, or ItemChanged ever
10// permutes two already-equivalent items against each other.
11//
12// Like FilteredList, every change to the source is translated into the
13// minimal incremental sequence of derived events; small source
14// mutations never escalate to a full Reset.
15//
16// Usage:
17//
18// auto source = std::make_shared<ObservableList<Task>>();
19// auto sorted = std::make_shared<SortedList<Task>>(
20// source,
21// [](const Task& a, const Task& b) {
22// return a.priority() < b.priority();
23// });
24//
25// source->push_back(std::make_shared<Task>(/*prio=*/5));
26// source->push_back(std::make_shared<Task>(/*prio=*/2));
27// // sorted sees: Insert(0), Insert(0) — the prio=2 ends up at index 0.
28//
29// source->at(0)->set_priority(10);
30// // if Task::on_changed propagates, sorted sees Move then ItemChanged
31// // so downstream views refresh the same object at its new position.
32//
33// Event-translation contract (mandatory; pinned by tests):
34//
35// Source event Derived behaviour
36// ──────────────── ──────────────────────────────────────────
37// Insert(i, x) binary_search → Insert(j, x) (always 1 event)
38// Remove(i) Remove(j) (always 1 event)
39// Replace(i, x_new) d_new == d_old → Replace(d_old, x_new)
40// otherwise → Remove(d_old), Insert(d_new)
41// ItemChanged(i) d_new == d_old → ItemChanged(d_old)
42// otherwise → Move(d_old, d_new, x),
43// ItemChanged(d_new)
44// Move(from, to) no event for distinct keys; equivalent keys
45// follow the new source order using Moves.
46// Reset Reset
47//
48// Objects may mutate before their notifications arrive, including several
49// distinct objects in one graph batch. Binary-search inputs are validated in
50// O(n); an unordered remainder uses a complete stable permutation repair.
51// Repair emits owning Moves (possibly for unchanged rows) plus the primary
52// content/structural event in replay order. It costs O(n log n + n*m), where
53// m is the number of Moves; ordinary ordered edits remain incremental.
54//
55// set_comparator() reshuffles the derived list against the new
56// comparator and emits a single Reset event (rather than trying to
57// emit a minimal-edit sequence — that would be O(n log n) to compute
58// and almost always dominated by the re-sort itself). The `reset`
59// event is the honest signal "layout changed wholesale".
60//
61// Stability contract:
62// Equivalent items (by the comparator) are ordered by ascending
63// source index, including after a source Move changes those indices.
64//
65// Thread-safety:
66// Same policy as FilteredList: internal `shared_mutex`, source
67// listener holds only weak_ptrs, emits happen after the mutation is
68// visible but before the lock is re-taken by a later event.
69
71#include "aria/list_source.hpp"
73#include "aria/subscription.hpp"
74#include "aria/detail/list_signal_mixin.hpp"
75#include "aria/detail/typed_signal.hpp"
76#include "aria/detail/list_replay.hpp"
77#include "aria/detail/list_update_queue.hpp"
78
79#include <algorithm>
80#include <cstddef>
81#include <functional>
82#include <type_traits>
83#include <memory>
84#include <mutex>
85#include <optional>
86#include <shared_mutex>
87#include <stdexcept>
88#include <utility>
89#include <vector>
90
91namespace aria {
92
93template<typename T, typename Source = ObservableList<T>>
96 : public detail::ListSignalMixin<SortedList<T, Source>, T> {
97 friend detail::ListSignalMixin<SortedList<T, Source>, T>;
98
99public:
100 using value_type = T;
106 using Comparator = aria::inplace_function<bool(const T&, const T&), 32>;
107 using Signal = detail::ListSignal<T>;
108
109private:
110 // Erase the whole order scan rather than every adjacent comparison.
111 // The scalar adapter and scan refer to the same owned callable instance.
112 struct ComparatorTag {};
113 struct ComparatorState {
114 using Scan = bool (*)(ComparatorState&, const std::vector<std::shared_ptr<T>>&,
115 const std::vector<std::size_t>&, std::optional<std::size_t>);
116 Comparator scalar;
117 void* target = nullptr;
118 Scan scan = nullptr;
119 bool operator()(const T& a, const T& b) const { return scalar(a, b); }
120 };
121
122 template<typename Fn>
123 static bool scan_order(Fn& comparator, const std::vector<std::shared_ptr<T>>& items,
124 const std::vector<std::size_t>& sources,
125 std::optional<std::size_t> skip) {
126 const auto size = items.size();
127 const auto skipped = skip.value_or(size);
128 const std::size_t first = skipped == 0 ? 1 : 0;
129 if (first >= size) { return true; }
130 const T* previous = items[first].get();
131 auto previous_source = sources[first];
132 for (auto d = first + 1; d < size; ++d) {
133 if (d == skipped) { continue; }
134 const T* current = items[d].get();
135 const auto current_source = sources[d];
136 const bool reversed = current_source < previous_source;
137 const T* lhs = reversed ? previous : current;
138 const T* rhs = reversed ? current : previous;
139 if (static_cast<bool>(std::invoke(comparator, *lhs, *rhs)) != reversed) { return false; }
140 previous = current;
141 previous_source = current_source;
142 }
143 return true;
144 }
145
146 static std::shared_ptr<ComparatorState> make_comparator(Comparator comparator) {
147 auto result = std::make_shared<ComparatorState>();
148 result->scalar = std::move(comparator);
149 result->scan = [](ComparatorState& self, const auto& items, const auto& sources, auto skip) {
150 return scan_order(self.scalar, items, sources, skip);
151 };
152 return result;
153 }
154
155 template<typename Fn>
156 static std::shared_ptr<ComparatorState> make_comparator(Fn&& comparator) {
157 using Stored = std::decay_t<Fn>;
158 // Retain the public small-callable size/alignment contract.
159 static_assert(sizeof(Stored) <= 32);
160 static_assert(alignof(Stored) <= alignof(std::max_align_t));
161 struct OwnedComparator {
162 ComparatorState state;
163 Stored callable;
164 // Fn is the enclosing forwarding-reference type, possibly an lvalue.
165 // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
166 explicit OwnedComparator(Fn&& fn) : callable(std::forward<Fn>(fn)) {}
167 };
168 auto owned = std::make_shared<OwnedComparator>(std::forward<Fn>(comparator));
169 auto& result = owned->state;
170 result.target = &owned->callable;
171 result.scalar = [target = &owned->callable](const T& a, const T& b) {
172 return static_cast<bool>(std::invoke(*target, a, b));
173 };
174 result.scan = [](ComparatorState& self, const auto& items, const auto& sources, auto skip) {
175 return scan_order(*static_cast<Stored*>(self.target), items, sources, skip);
176 };
177 return std::shared_ptr<ComparatorState>{owned, &owned->state};
178 }
179
180public:
181 SortedList(std::shared_ptr<Source> source, Comparator comparator)
182 : SortedList(std::move(source), make_comparator(std::move(comparator)), ComparatorTag{}) {}
183
184 template<typename Fn>
185 requires (!std::is_same_v<std::decay_t<Fn>, Comparator> &&
186 !std::is_pointer_v<std::decay_t<Fn>> &&
187 std::is_copy_constructible_v<std::decay_t<Fn>> &&
188 std::is_invocable_r_v<bool, std::decay_t<Fn>&, const T&, const T&>)
189 SortedList(std::shared_ptr<Source> source, Fn&& comparator)
190 : SortedList(std::move(source), make_comparator(std::forward<Fn>(comparator)), ComparatorTag{}) {}
191
192private:
193 SortedList(std::shared_ptr<Source> source, std::shared_ptr<ComparatorState> comparator, ComparatorTag /*tag*/)
194 : source_(std::move(source)),
195 signal_(std::make_shared<Signal>()),
196 state_(std::make_shared<SharedState>())
197 {
198 state_->comparator = std::move(comparator);
199
200 // Build the initial mapping from the source snapshot. See the
201 // FilteredList constructor for why we snapshot before
202 // subscribing; concurrent source mutations that arrive
203 // mid-construction are then delivered as normal listener
204 // callbacks on top of the known-good baseline.
205 {
206 std::unique_lock lk(state_->m);
207 state_->source_items = source_->snapshot();
208 rebuild_from_snapshot_unlocked_(*state_, state_->source_items);
209 }
210
211 // Subscribe on the source. The lambda holds only weak_ptrs.
212 std::weak_ptr<SharedState> weak_state = state_;
213 std::weak_ptr<Signal> weak_signal = signal_;
214 std::weak_ptr<Source> weak_source{source_};
215 source_sub_ = source_->observe(
216 [weak_state, weak_signal, weak_source](const ListChange<T>& ch) {
217 auto st = weak_state.lock();
218 auto sig = weak_signal.lock();
219 auto src = weak_source.lock();
220 if (!st || !sig || !src) return;
221 st->updates.submit([st, sig, ch] { dispatch_source_change_(*st, *sig, ch); });
222 });
223 }
224
225public:
226 ~SortedList() = default;
227
228 SortedList(const SortedList&) = delete;
229 SortedList& operator=(const SortedList&) = delete;
230
231 // ── Read surface ──────────────────────────────────────────────────
232 [[nodiscard]] std::size_t size() const {
233 std::shared_lock lk(state_->m);
234 return state_->items.size();
235 }
236
237 [[nodiscard]] bool empty() const { return size() == 0; }
238
239 [[nodiscard]] std::shared_ptr<T> at(std::size_t derived_index) const {
240 std::shared_lock lk(state_->m);
241 return state_->items.at(derived_index);
242 }
243
244 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
245 std::shared_lock lk(state_->m);
246 return state_->items;
247 }
248
249 [[nodiscard]] std::optional<std::size_t>
250 source_index_of(std::size_t derived_index) const {
251 std::shared_lock lk(state_->m);
252 if (derived_index >= state_->derived_to_source.size()) return std::nullopt;
253 return state_->derived_to_source[derived_index];
254 }
255
256 // ── Comparator replacement ────────────────────────────────────────
257 //
258 // Re-sorts the derived layout against the new comparator and emits
259 // a single Reset. An O(n log n) minimal-edit sequence could be
260 // computed, but the cost would dominate the re-sort itself and the
261 // wider Qt/AppKit adapter ecosystem handles Reset cleanly anyway.
262 void set_comparator(Comparator new_comparator) {
263 replace_comparator(make_comparator(std::move(new_comparator)));
264 }
265
266 template<typename Fn>
267 requires (!std::is_same_v<std::decay_t<Fn>, Comparator> &&
268 !std::is_pointer_v<std::decay_t<Fn>> &&
269 std::is_copy_constructible_v<std::decay_t<Fn>> &&
270 std::is_invocable_r_v<bool, std::decay_t<Fn>&, const T&, const T&>)
271 void set_comparator(Fn&& comparator) {
272 replace_comparator(make_comparator(std::forward<Fn>(comparator)));
273 }
274
275private:
276 void replace_comparator(std::shared_ptr<ComparatorState> comparator) {
277 auto state = state_;
278 auto signal = signal_;
279 state->updates.submit([state, signal, comparator = std::move(comparator)] {
280 SharedState work;
281 work.comparator = comparator;
282 rebuild_from_snapshot_unlocked_(work, state->source_items);
283 auto reset = ListChange<T>::reset(work.items);
284 commit(*state, work);
285 state->scratch.reset();
286 signal->emit(std::move(reset));
287 });
288 }
289
290 // Per-instance state shared with the source-listener lambda.
291 struct SharedState {
292 mutable std::shared_mutex m;
293 std::shared_ptr<ComparatorState> comparator;
294 detail::ListUpdateQueue updates;
295 std::vector<std::shared_ptr<T>> source_items;
296 bool needs_rebuild = false;
297 std::unique_ptr<SharedState> scratch;
298 // length == source.size(); value = derived index of that source slot.
299 std::vector<std::size_t> source_to_derived;
300 // length == derived.size(); value = source index.
301 std::vector<std::size_t> derived_to_source;
302 // parallel to derived_to_source; strong refs for at() / snapshot().
303 std::vector<std::shared_ptr<T>> items;
304 };
305
306 std::shared_ptr<Source> source_;
307 std::shared_ptr<Signal> signal_;
308 std::shared_ptr<SharedState> state_;
309 Subscription source_sub_;
310
311 // Rebuild all three parallel vectors from a fresh source snapshot.
312 // Caller must hold the unique lock on `st.m`. `snap` is taken
313 // from the source, whose lock is independent of ours.
314 static void rebuild_from_snapshot_unlocked_(
315 SharedState& st,
316 std::vector<std::shared_ptr<T>> snap)
317 {
318 std::vector<std::size_t> idx(snap.size());
319 for (std::size_t i = 0; i < snap.size(); ++i) idx[i] = i;
320 std::stable_sort(idx.begin(), idx.end(),
321 [&](std::size_t a, std::size_t b) {
322 return (*st.comparator)(*snap[a], *snap[b]);
323 });
324
325 st.source_to_derived.assign(snap.size(), 0);
326 st.derived_to_source.assign(idx.size(), 0);
327 st.items.clear();
328 st.items.reserve(idx.size());
329 for (std::size_t d = 0; d < idx.size(); ++d) {
330 const std::size_t s = idx[d];
331 st.derived_to_source[d] = s;
332 st.source_to_derived[s] = d;
333 st.items.push_back(snap[s]);
334 }
335 renumber_s2d_(st);
336 }
337
338 struct EmissionBuffer {
339 std::vector<ListChange<T>> events;
340 void emit(ListChange<T> event) { events.push_back(std::move(event)); }
341 void emit_batch(std::vector<ListChange<T>> batch) {
342 events.reserve(events.size() + batch.size());
343 for (auto& event : batch) { events.push_back(std::move(event)); }
344 }
345 };
346
347 static void commit(SharedState& st, SharedState& work) {
348 std::unique_lock lock(st.m);
349 st.comparator.swap(work.comparator);
350 st.source_to_derived.swap(work.source_to_derived);
351 st.derived_to_source.swap(work.derived_to_source);
352 st.items.swap(work.items);
353 st.needs_rebuild = false;
354 }
355
356 // Comparators run against a private working layout. Readers and callback
357 // reentry always see the previous committed layout; all events publish
358 // only after the complete update succeeds. The normal sorted update
359 // already scans O(n) rows, so staging preserves its asymptotic bound.
360 template<typename Value>
361 static void reserve_append(std::vector<Value>& values) {
362 if (values.size() != values.capacity()) { return; }
363 const auto capacity = values.capacity();
364 const auto maximum = values.max_size();
365 if (capacity == maximum) { throw std::length_error("SortedList capacity exceeded"); }
366 auto next = maximum;
367 if (capacity == 0) { next = 1; }
368 else if (capacity <= maximum / 2) { next = capacity * 2; }
369 values.reserve(next);
370 }
371
372 static void dispatch_source_change_(SharedState& st,
373 Signal& sig,
374 const ListChange<T>& ch) {
375 detail::replay_list_change(st.source_items, ch);
376 const bool rebuild = st.needs_rebuild;
377 st.needs_rebuild = true;
378 if (!rebuild && ch.kind == ListChangeKind::Insert && ch.index == st.items.size() &&
379 ordered_unlocked_(st)) {
380 const auto position = binary_search_insert_(st, *ch.item, ch.index);
381 std::vector<ListChange<T>> events;
382 events.push_back({ListChangeKind::Insert, position, ch.item, 0});
383 {
384 std::unique_lock lock(st.m);
385 // Complete every potentially throwing allocation before the
386 // first logical change. Shared-handle and index moves below
387 // are non-throwing; readers see either complete layout.
388 reserve_append(st.items);
389 reserve_append(st.source_to_derived);
390 reserve_append(st.derived_to_source);
391 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(position), ch.item);
392 st.derived_to_source.insert(
393 st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(position), ch.index);
394 st.source_to_derived.push_back(position);
395 renumber_s2d_(st, position);
396 st.needs_rebuild = false;
397 }
398 // A later non-append update can recreate its transactional cache.
399 // Do not retain a stale copy after a directly committed append.
400 st.scratch.reset();
401 sig.emit_batch(std::move(events));
402 return;
403 }
404 if (!st.scratch) {
405 auto scratch = std::make_unique<SharedState>();
406 scratch->comparator = st.comparator;
407 scratch->source_to_derived = st.source_to_derived;
408 scratch->derived_to_source = st.derived_to_source;
409 scratch->items = st.items;
410 st.scratch = std::move(scratch);
411 }
412 auto& work = *st.scratch;
413 EmissionBuffer buffered;
414 if (rebuild) {
415 rebuild_from_snapshot_unlocked_(work, st.source_items);
416 buffered.emit(ListChange<T>::reset(work.items));
417 } else {
418 switch (ch.kind) {
419 case ListChangeKind::Insert: handle_insert_(work, buffered, ch); break;
420 case ListChangeKind::Remove: handle_remove_(work, buffered, ch); break;
421 case ListChangeKind::Replace: handle_replace_(work, buffered, ch); break;
422 case ListChangeKind::ItemChanged: handle_item_changed_(work, buffered, ch); break;
423 case ListChangeKind::Move: handle_move_(work, buffered, ch); break;
424 case ListChangeKind::Reset: handle_reset_(work, buffered, ch); break;
425 }
426 }
427 commit(st, work);
428 // After the swap, work contains the previous public layout. Replay
429 // the successful delta into it for the next transaction. This reuses
430 // capacity and avoids copying every shared_ptr on every source edit.
431 // A cache-allocation failure may discard the workspace; the committed
432 // update and its notification remain valid.
433 try {
434 for (const auto& event : buffered.events) {
435 detail::replay_list_change(work.items, event);
436 }
437 work.source_to_derived = st.source_to_derived;
438 work.derived_to_source = st.derived_to_source;
439 work.comparator = st.comparator;
440 } catch (...) {
441 st.scratch.reset();
442 }
443 sig.emit_batch(std::move(buffered.events));
444 // Retired rows and comparator captures run outside the public lock.
445 }
446
468 static std::size_t binary_search_insert_(
469 const SharedState& st, const T& needle, std::size_t needle_src_i,
470 std::optional<std::size_t> skip_derived_idx = std::nullopt)
471 {
472 const auto& cmp = *st.comparator;
473 const auto& items = st.items;
474 const auto& d2s = st.derived_to_source;
475
476 // `real` translates from compressed (with d_old skipped) to
477 // real derived-index. Only used to access items[] / d2s[]
478 // during the search — the returned value stays in compressed
479 // coordinates.
480 auto real = [&](std::size_t compressed) -> std::size_t {
481 if (skip_derived_idx && compressed >= *skip_derived_idx)
482 return compressed + 1;
483 return compressed;
484 };
485 const std::size_t n_eff = items.size()
486 - (skip_derived_idx ? 1u : 0u);
487
488 std::size_t lo = 0;
489 std::size_t hi = n_eff;
490 while (lo < hi) {
491 const std::size_t mid = lo + (hi - lo) / 2;
492 const std::size_t r = real(mid);
493 const auto& mid_item = *items[r];
494 if (cmp(needle, mid_item)) {
495 hi = mid;
496 } else if (cmp(mid_item, needle)) {
497 lo = mid + 1;
498 } else {
499 // Equivalent keys — stability tie-breaker by source
500 // index.
501 if (needle_src_i < d2s[r]) hi = mid;
502 else lo = mid + 1;
503 }
504 }
505 return lo;
506 }
507
508 static void handle_insert_(SharedState& st, EmissionBuffer& sig,
509 const ListChange<T>& ch) {
510 std::unique_lock lk(st.m);
511 const std::size_t src_idx = ch.index;
512
513 // Shift all source indices >= src_idx by +1 (the source just
514 // grew one slot before/at them).
515 if (src_idx != st.items.size()) {
516 for (auto& s : st.derived_to_source) {
517 if (s >= src_idx) ++s;
518 }
519 }
520 st.source_to_derived.insert(st.source_to_derived.begin()
521 + static_cast<std::ptrdiff_t>(src_idx),
522 0);
523
524 // Resolve the current shared_ptr from the source. source has
525 // already released its own lock by the time emit() runs.
526 auto shared = ch.item;
527
528 const bool ordered = ordered_unlocked_(st);
529 const std::size_t d_idx = ordered ? binary_search_insert_(st, *shared, src_idx) : st.items.size();
530
531 st.derived_to_source.insert(st.derived_to_source.begin()
532 + static_cast<std::ptrdiff_t>(d_idx),
533 src_idx);
534 st.items.insert(st.items.begin()
535 + static_cast<std::ptrdiff_t>(d_idx),
536 shared);
537
538 // Rebuild s2d from d2s (the insert shifted all derived
539 // indices >= d_idx by +1).
540 renumber_s2d_(st, d_idx);
541
542 if (!ordered) {
543 auto moves = reorder_unlocked_(st);
544 moves.insert(moves.begin(), {ListChangeKind::Insert, d_idx, ch.item, 0});
545 lk.unlock();
546 sig.emit_batch(std::move(moves));
547 } else {
548 lk.unlock();
549 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
550 }
551 }
552
553 static void handle_remove_(SharedState& st, EmissionBuffer& sig,
554 const ListChange<T>& ch) {
555 std::unique_lock lk(st.m);
556 const std::size_t src_idx = ch.index;
557 if (src_idx >= st.source_to_derived.size()) return;
558
559 const std::size_t d_idx = st.source_to_derived[src_idx];
560 auto removed = st.items[d_idx];
561
562 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
563 st.derived_to_source.erase(
564 st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
565 st.source_to_derived.erase(
566 st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx));
567
568 // All source indices > src_idx shift down by 1.
569 for (auto& s : st.derived_to_source) {
570 if (s > src_idx) --s;
571 }
572 renumber_s2d_(st);
573
574 if (!ordered_unlocked_(st)) {
575 auto moves = reorder_unlocked_(st);
576 moves.insert(moves.begin(), {ListChangeKind::Remove, d_idx, removed, 0});
577 lk.unlock();
578 sig.emit_batch(std::move(moves));
579 } else {
580 lk.unlock();
581 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx, removed, 0});
582 }
583 }
584
585 static void handle_replace_(SharedState& st, EmissionBuffer& sig,
586 const ListChange<T>& ch) {
587 handle_slot_changed_(st, sig, ch,
588 /*new_ptr_from_src=*/true,
589 /*same_slot_kind=*/ListChangeKind::Replace,
590 /*cross_slot_use_move=*/false);
591 }
592
593 static void handle_item_changed_(SharedState& st, EmissionBuffer& sig,
594 const ListChange<T>& ch) {
595 handle_slot_changed_(st, sig, ch,
596 /*new_ptr_from_src=*/false,
597 /*same_slot_kind=*/ListChangeKind::ItemChanged,
598 /*cross_slot_use_move=*/true);
599 }
600
601 // Objects can change before their notifications arrive (graph batches,
602 // shared handles, or reentrant observers). Never binary-search until the
603 // remaining rows have been checked against their current values.
604 static bool ordered_unlocked_(const SharedState& st,
605 std::optional<std::size_t> skip = std::nullopt) {
606 return st.comparator->scan(*st.comparator, st.items, st.derived_to_source, skip);
607 }
608
609 // Move one row by shifting its interval once. Unlike a general rotate,
610 // this does not swap shared handles repeatedly around permutation cycles.
611 static void move_row_unlocked_(SharedState& st, std::size_t from, std::size_t to) noexcept {
612 auto item = std::move(st.items[from]);
613 const auto source_index = st.derived_to_source[from];
614 const auto first = static_cast<std::ptrdiff_t>(std::min(from, to));
615 const auto last = static_cast<std::ptrdiff_t>(std::max(from, to));
616 if (from < to) {
617 std::move(st.items.begin() + first + 1, st.items.begin() + last + 1, st.items.begin() + first);
618 std::move(st.derived_to_source.begin() + first + 1, st.derived_to_source.begin() + last + 1,
619 st.derived_to_source.begin() + first);
620 } else {
621 std::move_backward(st.items.begin() + first, st.items.begin() + last, st.items.begin() + last + 1);
622 std::move_backward(st.derived_to_source.begin() + first, st.derived_to_source.begin() + last,
623 st.derived_to_source.begin() + last + 1);
624 }
625 st.items[to] = std::move(item);
626 st.derived_to_source[to] = source_index;
627 for (auto d = std::min(from, to); d <= std::max(from, to); ++d)
628 st.source_to_derived[st.derived_to_source[d]] = d;
629 }
630
634 static std::vector<ListChange<T>> reorder_unlocked_(SharedState& st) {
635 auto before = [&](std::size_t a, std::size_t b) {
636 const auto& lhs = *st.items[st.source_to_derived[a]];
637 const auto& rhs = *st.items[st.source_to_derived[b]];
638 if ((*st.comparator)(lhs, rhs)) return true;
639 if ((*st.comparator)(rhs, lhs)) return false;
640 return a < b;
641 };
642 std::vector<ListChange<T>> events;
643 if (std::is_sorted(st.derived_to_source.begin(), st.derived_to_source.end(), before)) return events;
644 auto target = st.derived_to_source;
645 std::sort(target.begin(), target.end(), before);
646 events.reserve(target.size());
647 // Move the farther-displaced end of the interval into place. This
648 // avoids moving a long repeated block one occurrence at a time when
649 // shifting the few intervening rows represents the same permutation.
650 std::size_t first = 0, last = target.size();
651 while (first < last && target[first] == st.derived_to_source[first]) ++first;
652 while (last > first && target[last - 1] == st.derived_to_source[last - 1]) --last;
653 const bool reverse = st.source_to_derived[target[first]] - first <
654 last - 1 - st.source_to_derived[target[last - 1]];
655 auto place = [&](std::size_t to) {
656 const auto from = st.source_to_derived[target[to]];
657 if (from == to) return;
658 const auto item = st.items[from];
659 events.push_back({ListChangeKind::Move, to, item, from});
660 move_row_unlocked_(st, from, to);
661 };
662 if (reverse) { for (auto i = last; i-- > first;) place(i); }
663 else { for (auto i = first; i < last; ++i) place(i); }
664 return events;
665 }
666
677 static void handle_slot_changed_(SharedState& st, EmissionBuffer& sig,
678 const ListChange<T>& ch,
679 bool new_ptr_from_src,
680 ListChangeKind same_slot_kind,
681 bool cross_slot_use_move) {
682 std::unique_lock lk(st.m);
683 const std::size_t src_idx = ch.index;
684 if (src_idx >= st.source_to_derived.size()) return;
685
686 const std::size_t d_old = st.source_to_derived[src_idx];
687
688 // Replace carries the new owning handle; ItemChanged keeps identity.
689 auto previous = st.items[d_old];
690 auto fresh = new_ptr_from_src ? ch.item : previous;
691 if (new_ptr_from_src) st.items[d_old] = fresh;
692 if (!ordered_unlocked_(st, d_old)) {
693 auto moves = reorder_unlocked_(st);
694 if (new_ptr_from_src) {
695 // Replace the old logical row before replaying the permutation.
696 moves.insert(moves.begin(), {ListChangeKind::Replace, d_old, fresh, 0});
697 } else {
698 moves.push_back({ListChangeKind::ItemChanged, st.source_to_derived[src_idx], fresh, 0});
699 }
700 lk.unlock();
701 sig.emit_batch(std::move(moves));
702 return;
703 }
704
705 // Where does the (possibly mutated) item belong now? Skip the
706 // old slot in the search so we measure "new position if the
707 // old slot weren't there". Returned `p_small` is in the
708 // compressed (d_old-skipped) view: `p_small == d_old` means
709 // the item belongs right back where it was.
710 const std::size_t p_small = binary_search_insert_(
711 st, *fresh, src_idx, /*skip_derived_idx=*/d_old);
712
713 if (p_small == d_old) {
714 // Item stayed in place — one same-slot event.
715 lk.unlock();
716 sig.emit(ListChange<T>{same_slot_kind, d_old, fresh, 0});
717 return;
718 }
719
720 // Shift only the affected interval. Erase followed by insert would
721 // shift the unchanged suffix twice and renumber unaffected rows.
722 const std::size_t d_insert = p_small;
723 move_row_unlocked_(st, d_old, d_insert);
724
725 lk.unlock();
726 if (cross_slot_use_move) {
727 sig.emit_batch({ListChange<T>{ListChangeKind::Move, d_insert, fresh, d_old},
728 ListChange<T>{ListChangeKind::ItemChanged, d_insert, fresh, 0}});
729 } else {
730 sig.emit_batch({ListChange<T>{ListChangeKind::Remove, d_old, previous, 0},
731 ListChange<T>{ListChangeKind::Insert, d_insert, fresh, 0}});
732 }
733 }
734
738 static void handle_move_(SharedState& st, EmissionBuffer& sig,
739 const ListChange<T>& ch) {
740 std::unique_lock lk(st.m);
741 const std::size_t from = ch.from_index;
742 const std::size_t to = ch.index;
743 if (from == to) return;
744 if (from >= st.source_to_derived.size()) return;
745 if (to >= st.source_to_derived.size()) return;
746
747 // Shift all source indices that lived in [from+1..to] left by 1
748 // (if from < to) or all indices in [to..from-1] right by 1
749 // (if from > to). The moved item itself gets the new value
750 // `to`.
751 const std::size_t moved_d = st.source_to_derived[from];
752
753 if (from < to) {
754 for (auto& s : st.derived_to_source) {
755 if (s > from && s <= to) --s;
756 }
757 } else {
758 for (auto& s : st.derived_to_source) {
759 if (s >= to && s < from) ++s;
760 }
761 }
762 st.derived_to_source[moved_d] = to;
763 renumber_s2d_(st);
764 // A source Move normally changes only source-index metadata. Pending
765 // object-key writes can also have invalidated the visible key order.
766 if (!ordered_unlocked_(st)) {
767 auto moves = reorder_unlocked_(st);
768 lk.unlock();
769 sig.emit_batch(std::move(moves));
770 }
771 }
772
773 static void handle_reset_(SharedState& st, EmissionBuffer& sig,
774 const ListChange<T>& ch) {
775 ListChange<T> reset;
776 {
777 std::unique_lock lk(st.m);
778 rebuild_from_snapshot_unlocked_(st, *ch.snapshot);
779 reset = ListChange<T>::reset(st.items);
780 }
781 sig.emit(std::move(reset));
782 }
783
787 static void renumber_s2d_(SharedState& st, std::size_t first = 0) {
788 for (std::size_t d = first; d < st.derived_to_source.size(); ++d) {
789 st.source_to_derived[st.derived_to_source[d]] = d;
790 }
791 }
792};
793
794// ---------------------------------------------------------------------------
795// Factory helper — deduces the source type so pipelines stay readable.
796// See the note on `aria::filtered` in filtered_list.hpp.
797// ---------------------------------------------------------------------------
798template<typename Source,
799 typename Comparator,
800 typename T = list_source_value_t<Source>>
802[[nodiscard]] std::shared_ptr<SortedList<T, Source>>
803sorted(std::shared_ptr<Source> source, Comparator comparator) {
804 return std::make_shared<SortedList<T, Source>>(
805 std::move(source),
806 std::move(comparator));
807}
808
809} // namespace aria
~SortedList()=default
std::vector< std::shared_ptr< T > > snapshot() const
Definition sorted_list.hpp:244
aria::inplace_function< bool(const T &, const T &), 32 > Comparator
Strict weak ordering on items.
Definition sorted_list.hpp:106
bool empty() const
Definition sorted_list.hpp:237
std::shared_ptr< T > at(std::size_t derived_index) const
Definition sorted_list.hpp:239
void set_comparator(Comparator new_comparator)
Definition sorted_list.hpp:262
SortedList(std::shared_ptr< Source > source, Comparator comparator)
Definition sorted_list.hpp:181
SortedList & operator=(const SortedList &)=delete
detail::ListSignal< T > Signal
Definition sorted_list.hpp:107
std::size_t size() const
Definition sorted_list.hpp:232
SortedList(const SortedList &)=delete
std::optional< std::size_t > source_index_of(std::size_t derived_index) const
Definition sorted_list.hpp:250
T value_type
Definition sorted_list.hpp:100
SortedList(std::shared_ptr< Source > source, Fn &&comparator)
Definition sorted_list.hpp:189
void set_comparator(Fn &&comparator)
Definition sorted_list.hpp:271
Definition inplace_function.hpp:106
Definition list_source.hpp:80
@ Source
Definition node.hpp:88
Definition signal.hpp:12
auto batch(Fn &&fn) -> decltype(fn())
Sugar: batch([&]{ firstName = "..."; lastName = "..."; }).
Definition graph.hpp:369
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< Property< Acc > > scan(Property< T > &source, Acc seed, Reducer reduce)
Definition property_ops.hpp:242
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
std::shared_ptr< SortedList< T, Source > > sorted(std::shared_ptr< Source > source, Comparator comparator)
Definition sorted_list.hpp:803
Definition validation_key.hpp:110
static ListChange reset(Snapshot items)
Definition list_change.hpp:24