Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
filtered_list.hpp
Go to the documentation of this file.
1#pragma once
2
3// FilteredList<T> — a live-updating filtered view over an ObservableList<T>.
4//
5// Given a source list and a predicate, FilteredList exposes the subset of
6// items for which the predicate returns true, along with the same
7// observation surface (ListChange events, snapshot, at/size). Every change
8// to the source is translated into the minimal incremental sequence of
9// derived events; small source mutations **never** escalate to a full
10// Reset on the derived side.
11//
12// Usage:
13//
14// auto source = std::make_shared<ObservableList<Task>>();
15// auto active = std::make_shared<FilteredList<Task>>(
16// source,
17// [](const Task& t) { return !t.is_done(); });
18//
19// auto sub = active->observe([](const ListChange<Task>& ch) {
20// // UI-side handler; receives derived-coordinate events.
21// });
22//
23// source->push_back(std::make_shared<Task>("write docs"));
24// // → active sees Insert(0)
25// source->at(0)->mark_done();
26// // → active sees Remove(0) (if Task exposes on_changed)
27//
28// Event-translation contract (mandatory; pinned by tests):
29//
30// Source event Derived behaviour
31// ──────────────── ──────────────────────────────────────────
32// Insert(i, x) p(x) ? Insert(j, x) : nothing
33// Remove(i) was-in ? Remove(j) : nothing
34// Replace(i, x_new) in→in : Replace(j, x_new)
35// in→out : Remove(j)
36// out→in : Insert(j, x_new)
37// out→out: nothing
38// ItemChanged(i) in still in : ItemChanged(j)
39// in → out : Remove(j)
40// out → in : Insert(j, <cur>)
41// out still out: nothing
42// Move(from, to, x) in→in : Move(j_from, j_to, x)
43// (j_from / j_to recomputed from
44// the post-move layout)
45// items not in the filter produce no event
46// Reset always : Reset
47//
48// set_predicate() applies a membership diff: each source item that
49// transitions in↔out produces a single Insert / Remove on the derived
50// side. Items that stay in or stay out produce no event.
51//
52// Thread-safety: the derived list protects its own state with a
53// `shared_mutex`. The listener attached to the source is invoked on
54// whatever thread the source emits on (ObservableList emits AFTER
55// releasing its own mutex, so the listener may call back into the
56// source safely). FilteredList itself is graph-thread-agnostic;
57// callers who want UI-thread delivery should put a dispatcher between
58// the source emission and the FilteredList observer.
59//
60// Lifetime:
61// - FilteredList holds a strong shared_ptr to its source, so the source is
62// guaranteed to outlive the derived view. This holds transitively when
63// sources are chained: each link keeps the one below it alive.
64// - The source listener captures a weak_ptr<SharedState> and a
65// weak_ptr<Signal>; destroying the FilteredList mid-emission
66// degrades the listener to a no-op and the Subscription RAII
67// releases the connection cleanly.
68//
69// Chaining:
70// - The source type is a template parameter defaulting to
71// `ObservableList<T>` and constrained to `ListSourceOf<Source, T>`. Any
72// type satisfying that concept works, which is what makes
73// `FilteredList -> SortedList -> PagedList` pipelines possible. Prefer the
74// `filtered()` helper at the bottom of this file so the source type does
75// not have to be spelled out.
76
78#include "aria/list_source.hpp"
80#include "aria/subscription.hpp"
81#include "aria/detail/list_signal_mixin.hpp"
82#include "aria/detail/list_replay.hpp"
83#include "aria/detail/list_update_queue.hpp"
84#include "aria/detail/typed_signal.hpp"
85
86#include <algorithm>
87#include <cstddef>
88#include <memory>
89#include <mutex>
90#include <optional>
91#include <shared_mutex>
92#include <utility>
93#include <vector>
94
95namespace aria {
96
97template<typename T, typename Source = ObservableList<T>>
100 : public detail::ListSignalMixin<FilteredList<T, Source>, T> {
101 friend detail::ListSignalMixin<FilteredList<T, Source>, T>;
102
103public:
104 using value_type = T;
108 using Predicate = aria::inplace_function<bool(const T&), 32>;
109 using Signal = detail::ListSignal<T>;
110
111 FilteredList(std::shared_ptr<Source> source,
112 Predicate predicate)
113 : source_(std::move(source)),
114 signal_(std::make_shared<Signal>()),
115 state_(std::make_shared<SharedState>())
116 {
117 state_->predicate = std::make_shared<Predicate>(std::move(predicate));
118
119 // Construct on the source writer thread or while its writer is
120 // quiescent: snapshot + subscribe is not a cross-thread transaction.
121 // The signal watermark excludes an active batch already in snapshot.
122 {
123 std::unique_lock lk(state_->m);
124 auto snap = source_->snapshot();
125 state_->source_items = snap;
126 state_->source_to_derived.assign(snap.size(), std::nullopt);
127 for (std::size_t i = 0; i < snap.size(); ++i) {
128 if ((*state_->predicate)(*snap[i])) {
129 state_->source_to_derived[i] = state_->derived_to_source.size();
130 state_->derived_to_source.push_back(i);
131 state_->items.push_back(snap[i]);
132 }
133 }
134 }
135
136 // Subscribe on the source. The lambda holds only weak_ptrs so
137 // destroying the FilteredList while a notification is in
138 // flight is safe — the lock() attempt returns nullptr.
139 std::weak_ptr<SharedState> weak_state = state_;
140 std::weak_ptr<Signal> weak_signal = signal_;
141 std::weak_ptr<Source> weak_source{source_};
142 source_sub_ = source_->observe(
143 [weak_state, weak_signal, weak_source](const ListChange<T>& ch) {
144 auto st = weak_state.lock();
145 auto sig = weak_signal.lock();
146 auto src = weak_source.lock();
147 if (!st || !sig || !src) return;
148 st->updates.submit([st, sig, ch] { dispatch_source_change_(*st, *sig, ch); });
149 });
150 }
151
152 ~FilteredList() = default;
153
154 FilteredList(const FilteredList&) = delete;
156
157 // ── Read surface ──────────────────────────────────────────────────
158 [[nodiscard]] std::size_t size() const {
159 std::shared_lock lk(state_->m);
160 return state_->items.size();
161 }
162
163 [[nodiscard]] bool empty() const { return size() == 0; }
164
165 [[nodiscard]] std::shared_ptr<T> at(std::size_t derived_index) const {
166 std::shared_lock lk(state_->m);
167 return state_->items.at(derived_index);
168 }
169
170 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
171 std::shared_lock lk(state_->m);
172 return state_->items;
173 }
174
175 [[nodiscard]] std::optional<std::size_t>
176 source_index_of(std::size_t derived_index) const {
177 std::shared_lock lk(state_->m);
178 if (derived_index >= state_->derived_to_source.size()) return std::nullopt;
179 return state_->derived_to_source[derived_index];
180 }
181
182 // ── Predicate replacement (incremental diff) ──────────────────────
183 //
184 // For each source item, compute the new membership; emit Insert /
185 // Remove only for items whose membership changed. Items that stay
186 // in or stay out produce no event — if the caller wanted "refresh
187 // as if every in-item had changed", they should instead send a
188 // proper source-side mutation or iterate ItemChanged manually.
189 void set_predicate(Predicate new_predicate) {
190 auto state = state_;
191 auto signal = signal_;
192 auto predicate = std::make_shared<Predicate>(std::move(new_predicate));
193 state->updates.submit([state, signal, predicate = std::move(predicate)] {
194 replace_predicate(*state, *signal, predicate);
195 });
196 }
197
198private:
199 // Per-instance state shared with the source-listener lambda. Held
200 // as shared_ptr so the listener never dereferences a dangling
201 // control block.
202 struct SharedState {
203 mutable std::shared_mutex m;
204 std::shared_ptr<Predicate> predicate;
205 detail::ListUpdateQueue updates;
206 bool needs_rebuild = false;
207 // length == source.size(); nullopt means "not in derived".
208 std::vector<std::optional<std::size_t>> source_to_derived;
209 // length == derived.size(); value is the source index.
210 std::vector<std::size_t> derived_to_source;
211 // parallel to derived_to_source; strong refs for at() / snapshot().
212 std::vector<std::shared_ptr<T>> items;
213 std::vector<std::shared_ptr<T>> source_items;
214 };
215
216 std::shared_ptr<Source> source_;
217 std::shared_ptr<Signal> signal_;
218 std::shared_ptr<SharedState> state_;
219 Subscription source_sub_;
220
221 template<class Value>
222 static void reserve_one(std::vector<Value>& values) {
223 if (values.size() != values.capacity()) { return; }
224 const auto next = values.size() < values.max_size() / 2
225 ? std::max<std::size_t>(1, values.size() * 2) : values.max_size();
226 values.reserve(next);
227 }
228
229 static void replace_predicate(SharedState& st, Signal& signal,
230 std::shared_ptr<Predicate> predicate) {
231 // Only the update queue writes these vectors. Prepare against the
232 // current source mirror without holding the public readers' lock.
233 const auto& snap = st.source_items;
234 std::vector<std::optional<std::size_t>> s2d(snap.size(), std::nullopt);
235 std::vector<std::size_t> d2s;
236 std::vector<std::shared_ptr<T>> items;
237 std::vector<ListChange<T>> events;
238 d2s.reserve(snap.size());
239 items.reserve(snap.size());
240 events.reserve(snap.size());
241 std::size_t observed_pos = 0;
242 for (std::size_t i = 0; i < snap.size(); ++i) {
243 const bool was_in = i < st.source_to_derived.size() && st.source_to_derived[i].has_value();
244 const bool is_in = (*predicate)(*snap[i]);
245 if (is_in) {
246 s2d[i] = items.size();
247 d2s.push_back(i);
248 items.push_back(snap[i]);
249 }
250 if (was_in && !is_in) {
251 events.push_back({ListChangeKind::Remove, observed_pos, snap[i], 0});
252 } else if (!was_in && is_in) {
253 events.push_back({ListChangeKind::Insert, observed_pos, snap[i], 0});
254 }
255 if (is_in) { ++observed_pos; }
256 }
257 if (st.needs_rebuild) { events = {ListChange<T>::reset(items)}; }
258 {
259 std::unique_lock lock(st.m);
260 st.predicate.swap(predicate);
261 st.source_to_derived.swap(s2d);
262 st.derived_to_source.swap(d2s);
263 st.items.swap(items);
264 st.needs_rebuild = false;
265 }
266 signal.emit_batch(std::move(events));
267 // Retired items and callable captures are destroyed without the lock.
268 }
269
270 // ── Translation: one source event -> zero or one derived events ───
271 static void dispatch_source_change_(SharedState& st,
272 Signal& sig,
273 const ListChange<T>& ch) {
274 detail::replay_list_change(st.source_items, ch);
275 if (st.needs_rebuild) {
276 handle_reset_(st, sig, ListChange<T>::reset(st.source_items));
277 st.needs_rebuild = false;
278 return;
279 }
280 st.needs_rebuild = true;
281 switch (ch.kind) {
282 case ListChangeKind::Insert: handle_insert_(st, sig, ch); break;
283 case ListChangeKind::Remove: handle_remove_(st, sig, ch); break;
284 case ListChangeKind::Replace: handle_replace_(st, sig, ch); break;
285 case ListChangeKind::ItemChanged: handle_item_changed_(st, sig, ch); break;
286 case ListChangeKind::Move: handle_move_(st, sig, ch); break;
287 case ListChangeKind::Reset: handle_reset_(st, sig, ch); break;
288 }
289 st.needs_rebuild = false;
290 }
291
292 static void handle_insert_(SharedState& st, Signal& sig,
293 const ListChange<T>& ch) {
294 const bool is_in = (*st.predicate)(*ch.item);
295 std::unique_lock lk(st.m);
296 const std::size_t src_idx = ch.index;
297 // Allocate before modifying any parallel index vector.
298 reserve_one(st.source_to_derived);
299 if (is_in) {
300 reserve_one(st.items);
301 reserve_one(st.derived_to_source);
302 }
303 // Source indices are ordered, so this locates both the insertion
304 // position and the only suffix whose source indices change. A tail
305 // append leaves every existing mapping intact.
306 const auto position = st.derived_to_source.empty() || st.derived_to_source.back() < src_idx
307 ? st.derived_to_source.end()
308 : std::lower_bound(st.derived_to_source.begin(), st.derived_to_source.end(), src_idx);
309 const auto d_idx = static_cast<std::size_t>(position - st.derived_to_source.begin());
310 for (auto it = position; it != st.derived_to_source.end(); ++it) ++*it;
311 // Insert the new "not yet classified" source slot.
312 st.source_to_derived.insert(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx),
313 std::nullopt);
314
315 if (!is_in) return;
316
317 st.derived_to_source.insert(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx),
318 src_idx);
319
320 // The event owns the row even if the source has committed later edits.
321 auto shared = ch.item;
322 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx), std::move(shared));
323
324 for (std::size_t d = d_idx; d < st.derived_to_source.size(); ++d)
325 st.source_to_derived[st.derived_to_source[d]] = d;
326
327 lk.unlock();
328 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
329 }
330
331 static void handle_remove_(SharedState& st, Signal& sig,
332 const ListChange<T>& ch) {
333 std::unique_lock lk(st.m);
334 const std::size_t src_idx = ch.index;
335
336 if (src_idx >= st.source_to_derived.size()) return; // defensive
337
338 std::optional<std::size_t> maybe_d = st.source_to_derived[src_idx];
339 st.source_to_derived.erase(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx));
340
341 // All d2s entries > src_idx shifted by -1 (source collapsed).
342 for (auto& s : st.derived_to_source) {
343 if (s > src_idx) --s;
344 }
345
346 if (maybe_d) {
347 const std::size_t d_idx = *maybe_d;
348 auto removed = st.items[d_idx]; // keep alive past erase
349 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
350 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
351 renumber_s2d_(st);
352 lk.unlock();
353 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx,
354 removed, 0});
355 return;
356 }
357
358 renumber_s2d_(st);
359 }
360
372 static void handle_membership_transition_(SharedState& st, Signal& sig,
373 const ListChange<T>& ch,
374 ListChangeKind kind_for_in_in,
375 bool refresh_value) {
376 const bool is_in = (*st.predicate)(*ch.item);
377 std::shared_ptr<T> retired;
378 std::unique_lock lk(st.m);
379 const std::size_t src_idx = ch.index;
380
381 if (src_idx >= st.source_to_derived.size()) return;
382 const bool was_in = st.source_to_derived[src_idx].has_value();
383 if (!was_in && is_in) {
384 reserve_one(st.items);
385 reserve_one(st.derived_to_source);
386 }
387
388 if (!was_in && !is_in) return;
389
390 if (was_in && is_in) {
391 const std::size_t d_idx = *st.source_to_derived[src_idx];
392 if (refresh_value) {
393 retired = std::exchange(st.items[d_idx], ch.item);
394 }
395 lk.unlock();
396 sig.emit(ListChange<T>{kind_for_in_in, d_idx, ch.item, 0});
397 return;
398 }
399
400 if (was_in && !is_in) {
401 const std::size_t d_idx = *st.source_to_derived[src_idx];
402 auto removed = st.items[d_idx];
403 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
404 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
405 st.source_to_derived[src_idx] = std::nullopt;
406 renumber_s2d_(st);
407 lk.unlock();
408 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx,
409 removed, 0});
410 return;
411 }
412
413 // !was_in && is_in: item became visible.
414 std::size_t d_idx = 0;
415 for (std::size_t i = 0; i < src_idx; ++i) {
416 if (st.source_to_derived[i].has_value()) ++d_idx;
417 }
418 st.derived_to_source.insert(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx),
419 src_idx);
420 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx), ch.item);
421 st.source_to_derived[src_idx] = d_idx;
422 renumber_s2d_(st);
423 lk.unlock();
424 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
425 }
426
427 static void handle_replace_(SharedState& st, Signal& sig,
428 const ListChange<T>& ch) {
429 handle_membership_transition_(st, sig, ch,
431 /*refresh_value=*/true);
432 }
433
434 static void handle_item_changed_(SharedState& st, Signal& sig,
435 const ListChange<T>& ch) {
436 handle_membership_transition_(st, sig, ch,
438 /*refresh_value=*/false);
439 }
440
441 static void handle_move_(SharedState& st, Signal& sig,
442 const ListChange<T>& ch) {
443 std::unique_lock lk(st.m);
444 const std::size_t from = ch.from_index;
445 const std::size_t to = ch.index;
446
447 if (from == to) return;
448 if (from >= st.source_to_derived.size() ||
449 to >= st.source_to_derived.size()) {
450 return; // defensive; source mutation out of step
451 }
452
453 // Derived index of the moved item BEFORE shuffle (if any).
454 const std::optional<std::size_t> old_d = st.source_to_derived[from];
455
456 // Shuffle source_to_derived to mirror the source's own move.
457 auto moved_slot = st.source_to_derived[from];
458 st.source_to_derived.erase(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(from));
459 st.source_to_derived.insert(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(to),
460 moved_slot);
461
462 // If the moved item was in the filter, its derived position
463 // needs updating too. Compute the new derived index of the
464 // item now at source `to`.
465 if (old_d) {
466 // Determine new derived index by counting in-filter slots
467 // strictly before `to` in the new source layout.
468 std::size_t new_d = 0;
469 for (std::size_t i = 0; i < to; ++i) {
470 if (st.source_to_derived[i].has_value()) ++new_d;
471 }
472
473 // Physically move the item within derived_to_source and
474 // items to reflect the new position.
475 if (new_d != *old_d) {
476 auto moved_src = st.derived_to_source[*old_d];
477 auto moved_item = st.items[*old_d];
478 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(*old_d));
479 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(*old_d));
480 st.derived_to_source.insert(
481 st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(new_d), moved_src);
482 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(new_d), std::move(moved_item));
483 }
484
485 // Now renumber s2d to reflect both the source shuffle and
486 // the derived reordering we just did.
487 renumber_s2d_(st);
488
489 if (new_d != *old_d) {
490 lk.unlock();
491 sig.emit(ListChange<T>{ListChangeKind::Move, new_d,
492 ch.item, *old_d});
493 }
494 return;
495 }
496
497 // Moved item was not in the filter — the only change is which
498 // source indices each d2s entry refers to.
499 renumber_d2s_after_source_move_(st, from, to);
500 renumber_s2d_(st);
501 }
502
503 static void handle_reset_(SharedState& st, Signal& sig, const ListChange<T>& ch) {
504 const auto& snapshot = *ch.snapshot;
505 std::vector<std::optional<std::size_t>> s2d(snapshot.size(), std::nullopt);
506 std::vector<std::size_t> d2s;
507 std::vector<std::shared_ptr<T>> items;
508 d2s.reserve(snapshot.size());
509 items.reserve(snapshot.size());
510 for (std::size_t i = 0; i < snapshot.size(); ++i) {
511 if (!(*st.predicate)(*snapshot[i])) continue;
512 s2d[i] = items.size();
513 d2s.push_back(i);
514 items.push_back(snapshot[i]);
515 }
516 auto reset = ListChange<T>::reset(items);
517 {
518 std::unique_lock lock(st.m);
519 st.source_to_derived.swap(s2d);
520 st.derived_to_source.swap(d2s);
521 st.items.swap(items);
522 }
523 sig.emit(std::move(reset));
524 }
525
526 // Walk source_to_derived and assign successive derived indices to
527 // every non-nullopt slot. O(source.size()). Call after any
528 // structural mutation.
529 static void renumber_s2d_(SharedState& st) {
530 std::size_t d = 0;
531 for (std::size_t s = 0; s < st.source_to_derived.size(); ++s) {
532 auto& slot = st.source_to_derived[s];
533 if (slot.has_value()) {
534 slot = d;
535 st.derived_to_source[d++] = s;
536 }
537 }
538 }
539
540 // When a filtered-out item moves within the source, we still need
541 // to update the source indices stored in derived_to_source so that
542 // they refer to the right slots. This is the straightforward
543 // translation: values > from and <= to (moving backward) or
544 // >= to and < from (moving forward) get shifted by one.
545 static void renumber_d2s_after_source_move_(SharedState& st,
546 std::size_t from,
547 std::size_t to) {
548 if (from < to) {
549 for (auto& s : st.derived_to_source) {
550 if (s > from && s <= to) --s;
551 // NOTE: the moved item itself was NOT in derived,
552 // so we never encounter `s == from`.
553 }
554 } else {
555 for (auto& s : st.derived_to_source) {
556 if (s >= to && s < from) ++s;
557 }
558 }
559 }
560};
561
562// ---------------------------------------------------------------------------
563// Factory helper — deduces the source type so pipelines stay readable.
564//
565// Without it, chaining forces the caller to spell out every layer:
566//
567// auto f = std::make_shared<FilteredList<Task>>(src, pred);
568// auto s = std::make_shared<SortedList<Task, FilteredList<Task>>>(f, cmp);
569//
570// With it:
571//
572// auto f = aria::filtered(src, pred);
573// auto s = aria::sorted(f, cmp);
574//
575// Returns shared_ptr because every derived list takes its source as one, so
576// the result is immediately usable as the next link in the chain.
577// ---------------------------------------------------------------------------
578template<typename Source,
579 typename Predicate,
580 typename T = list_source_value_t<Source>>
582[[nodiscard]] std::shared_ptr<FilteredList<T, Source>>
583filtered(std::shared_ptr<Source> source, Predicate predicate) {
584 return std::make_shared<FilteredList<T, Source>>(
585 std::move(source),
586 typename FilteredList<T, Source>::Predicate{std::move(predicate)});
587}
588
589} // namespace aria
T value_type
Definition filtered_list.hpp:104
std::optional< std::size_t > source_index_of(std::size_t derived_index) const
Definition filtered_list.hpp:176
detail::ListSignal< T > Signal
Definition filtered_list.hpp:109
FilteredList & operator=(const FilteredList &)=delete
aria::inplace_function< bool(const T &), 32 > Predicate
Owning, heap-free predicate handle.
Definition filtered_list.hpp:108
void set_predicate(Predicate new_predicate)
Definition filtered_list.hpp:189
FilteredList(std::shared_ptr< Source > source, Predicate predicate)
Definition filtered_list.hpp:111
std::vector< std::shared_ptr< T > > snapshot() const
Definition filtered_list.hpp:170
FilteredList(const FilteredList &)=delete
std::size_t size() const
Definition filtered_list.hpp:158
std::shared_ptr< T > at(std::size_t derived_index) const
Definition filtered_list.hpp:165
~FilteredList()=default
bool empty() const
Definition filtered_list.hpp:163
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
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< FilteredList< T, Source > > filtered(std::shared_ptr< Source > source, Predicate predicate)
Definition filtered_list.hpp:583
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