Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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/typed_signal.hpp"
84
85#include <algorithm>
86#include <cstddef>
87#include <memory>
88#include <mutex>
89#include <optional>
90#include <shared_mutex>
91#include <utility>
92#include <vector>
93
94namespace aria {
95
96template<typename T, typename Source = ObservableList<T>>
99 : public detail::ListSignalMixin<FilteredList<T, Source>, T> {
100 friend detail::ListSignalMixin<FilteredList<T, Source>, T>;
101
102public:
103 using value_type = T;
107 using Predicate = aria::inplace_function<bool(const T&), 32>;
108 using Signal = detail::ListSignal<T>;
109
110 FilteredList(std::shared_ptr<Source> source,
111 Predicate predicate)
112 : source_(std::move(source)),
113 signal_(std::make_shared<Signal>()),
114 state_(std::make_shared<SharedState>())
115 {
116 state_->predicate = std::move(predicate);
117
118 // Construct on the source writer thread or while its writer is
119 // quiescent: snapshot + subscribe is not a cross-thread transaction.
120 // The signal watermark excludes an active batch already in snapshot.
121 {
122 std::unique_lock lk(state_->m);
123 auto snap = source_->snapshot();
124 state_->source_items = snap;
125 state_->source_to_derived.assign(snap.size(), std::nullopt);
126 for (std::size_t i = 0; i < snap.size(); ++i) {
127 if (state_->predicate(*snap[i])) {
128 state_->source_to_derived[i] = state_->derived_to_source.size();
129 state_->derived_to_source.push_back(i);
130 state_->items.push_back(snap[i]);
131 }
132 }
133 }
134
135 // Subscribe on the source. The lambda holds only weak_ptrs so
136 // destroying the FilteredList while a notification is in
137 // flight is safe — the lock() attempt returns nullptr.
138 std::weak_ptr<SharedState> weak_state = state_;
139 std::weak_ptr<Signal> weak_signal = signal_;
140 std::weak_ptr<Source> weak_source{source_};
141 source_sub_ = source_->observe(
142 [weak_state, weak_signal, weak_source](const ListChange<T>& ch) {
143 auto st = weak_state.lock();
144 auto sig = weak_signal.lock();
145 auto src = weak_source.lock();
146 if (!st || !sig || !src) return;
147 dispatch_source_change_(*st, *sig, ch);
148 });
149 }
150
151 ~FilteredList() = default;
152
153 FilteredList(const FilteredList&) = delete;
155
156 // ── Read surface ──────────────────────────────────────────────────
157 [[nodiscard]] std::size_t size() const {
158 std::shared_lock lk(state_->m);
159 return state_->items.size();
160 }
161
162 [[nodiscard]] bool empty() const { return size() == 0; }
163
164 [[nodiscard]] std::shared_ptr<T> at(std::size_t derived_index) const {
165 std::shared_lock lk(state_->m);
166 return state_->items.at(derived_index);
167 }
168
169 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
170 std::shared_lock lk(state_->m);
171 return state_->items;
172 }
173
174 [[nodiscard]] std::optional<std::size_t>
175 source_index_of(std::size_t derived_index) const {
176 std::shared_lock lk(state_->m);
177 if (derived_index >= state_->derived_to_source.size()) return std::nullopt;
178 return state_->derived_to_source[derived_index];
179 }
180
181 // ── Predicate replacement (incremental diff) ──────────────────────
182 //
183 // For each source item, compute the new membership; emit Insert /
184 // Remove only for items whose membership changed. Items that stay
185 // in or stay out produce no event — if the caller wanted "refresh
186 // as if every in-item had changed", they should instead send a
187 // proper source-side mutation or iterate ItemChanged manually.
188 void set_predicate(Predicate new_predicate) {
189 auto signal = signal_;
190 std::vector<ListChange<T>> emissions;
191
192 {
193 std::unique_lock lk(state_->m);
194 state_->predicate = std::move(new_predicate);
195
196 const auto& snap = state_->source_items;
197
198 // Build new mapping in a single pass; the OLD mapping is
199 // still in `state_->source_to_derived` at this point so we
200 // can detect membership transitions.
201 std::vector<std::optional<std::size_t>> new_s2d(snap.size(), std::nullopt);
202 std::vector<std::size_t> new_d2s;
203 std::vector<std::shared_ptr<T>> new_items;
204 new_d2s.reserve(snap.size());
205 new_items.reserve(snap.size());
206
207 // Emission indices must follow D-11 "as observed": each event's
208 // index reflects the derived list as the OBSERVER sees it at the
209 // moment of that emit, not the pre-change or post-change layout.
210 //
211 // We therefore walk the source in order and maintain
212 // `observed_pos` — the index, in the observer's incrementally
213 // rebuilt mirror, of the next element that survives. Items that
214 // stay in advance it; a Remove leaves it alone (the mirror just
215 // shrank at that spot); an Insert lands at it and advances it.
216 //
217 // Getting this wrong is not a cosmetic bug. The previous version
218 // emitted Remove with the OLD derived index and Insert with the
219 // NEW one, mixing two coordinate systems: for source [A,B,C] all
220 // passing, with a new predicate that keeps only C, it emitted
221 // Remove(0), Remove(1) — walking the observer's mirror
222 // [A,B,C] -> [B,C] -> [B], while the real state is [C]. The
223 // mirror was then permanently wrong with no event to repair it.
224 // The correct stream here is Remove(0), Remove(0).
225 std::size_t observed_pos = 0;
226
227 for (std::size_t i = 0; i < snap.size(); ++i) {
228 const bool was_in = (i < state_->source_to_derived.size())
229 && state_->source_to_derived[i].has_value();
230 const bool is_in = state_->predicate(*snap[i]);
231
232 if (is_in) {
233 new_s2d[i] = new_d2s.size();
234 new_d2s.push_back(i);
235 new_items.push_back(snap[i]);
236 }
237
238 if (was_in && !is_in) {
239 // Dropped out: the observer removes at `observed_pos`,
240 // and everything after it shifts down — so
241 // `observed_pos` stays put for the next candidate.
242 emissions.push_back({
244 observed_pos,
245 snap[i], 0});
246 } else if (!was_in && is_in) {
247 // Newly admitted: lands at `observed_pos` in the mirror.
248 emissions.push_back({
250 observed_pos,
251 snap[i], 0});
252 ++observed_pos;
253 } else if (was_in && is_in) {
254 // Unchanged member: no event, but it occupies a slot in
255 // the observer's mirror.
256 ++observed_pos;
257 }
258 // (!was_in && !is_in): absent before and after — no slot.
259 }
260
261 state_->source_to_derived = std::move(new_s2d);
262 state_->derived_to_source = std::move(new_d2s);
263 state_->items = std::move(new_items);
264 }
265
266 // Emit outside the lock. Observers that call back into at() /
267 // snapshot() see the new state, matching ObservableList's own
268 // post-mutation emission contract.
269 signal->emit_batch(std::move(emissions));
270 }
271
272private:
273 // Per-instance state shared with the source-listener lambda. Held
274 // as shared_ptr so the listener never dereferences a dangling
275 // control block.
276 struct SharedState {
277 mutable std::shared_mutex m;
278 Predicate predicate;
279 // length == source.size(); nullopt means "not in derived".
280 std::vector<std::optional<std::size_t>> source_to_derived;
281 // length == derived.size(); value is the source index.
282 std::vector<std::size_t> derived_to_source;
283 // parallel to derived_to_source; strong refs for at() / snapshot().
284 std::vector<std::shared_ptr<T>> items;
285 std::vector<std::shared_ptr<T>> source_items;
286 };
287
288 std::shared_ptr<Source> source_;
289 std::shared_ptr<Signal> signal_;
290 std::shared_ptr<SharedState> state_;
291 Subscription source_sub_;
292
293 // ── Translation: one source event -> zero or one derived events ───
294 static void dispatch_source_change_(SharedState& st,
295 Signal& sig,
296 const ListChange<T>& ch) {
297 {
298 std::unique_lock lock(st.m);
299 detail::replay_list_change(st.source_items, ch);
300 }
301 switch (ch.kind) {
302 case ListChangeKind::Insert: handle_insert_(st, sig, ch); return;
303 case ListChangeKind::Remove: handle_remove_(st, sig, ch); return;
304 case ListChangeKind::Replace: handle_replace_(st, sig, ch); return;
305 case ListChangeKind::ItemChanged: handle_item_changed_(st, sig, ch); return;
306 case ListChangeKind::Move: handle_move_(st, sig, ch); return;
307 case ListChangeKind::Reset: handle_reset_(st, sig, ch); return;
308 }
309 }
310
311 static void handle_insert_(SharedState& st, Signal& sig,
312 const ListChange<T>& ch) {
313 std::unique_lock lk(st.m);
314 const std::size_t src_idx = ch.index;
315
316 const bool is_in = st.predicate(*ch.item);
317 // Source indices are ordered, so this locates both the insertion
318 // position and the only suffix whose source indices change. A tail
319 // append leaves every existing mapping intact.
320 const auto position = st.derived_to_source.empty() || st.derived_to_source.back() < src_idx
321 ? st.derived_to_source.end()
322 : std::lower_bound(st.derived_to_source.begin(), st.derived_to_source.end(), src_idx);
323 const auto d_idx = static_cast<std::size_t>(position - st.derived_to_source.begin());
324 for (auto it = position; it != st.derived_to_source.end(); ++it) ++*it;
325 // Insert the new "not yet classified" source slot.
326 st.source_to_derived.insert(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx),
327 std::nullopt);
328
329 if (!is_in) return;
330
331 st.derived_to_source.insert(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx),
332 src_idx);
333
334 // The event owns the row even if the source has committed later edits.
335 auto shared = ch.item;
336 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx), std::move(shared));
337
338 for (std::size_t d = d_idx; d < st.derived_to_source.size(); ++d)
339 st.source_to_derived[st.derived_to_source[d]] = d;
340
341 lk.unlock();
342 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
343 }
344
345 static void handle_remove_(SharedState& st, Signal& sig,
346 const ListChange<T>& ch) {
347 std::unique_lock lk(st.m);
348 const std::size_t src_idx = ch.index;
349
350 if (src_idx >= st.source_to_derived.size()) return; // defensive
351
352 std::optional<std::size_t> maybe_d = st.source_to_derived[src_idx];
353 st.source_to_derived.erase(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx));
354
355 // All d2s entries > src_idx shifted by -1 (source collapsed).
356 for (auto& s : st.derived_to_source) {
357 if (s > src_idx) --s;
358 }
359
360 if (maybe_d) {
361 const std::size_t d_idx = *maybe_d;
362 auto removed = st.items[d_idx]; // keep alive past erase
363 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
364 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
365 renumber_s2d_(st);
366 lk.unlock();
367 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx,
368 removed, 0});
369 return;
370 }
371
372 renumber_s2d_(st);
373 }
374
386 static void handle_membership_transition_(SharedState& st, Signal& sig,
387 const ListChange<T>& ch,
388 ListChangeKind kind_for_in_in,
389 bool refresh_value) {
390 std::unique_lock lk(st.m);
391 const std::size_t src_idx = ch.index;
392
393 if (src_idx >= st.source_to_derived.size()) return;
394
395 const bool was_in = st.source_to_derived[src_idx].has_value();
396 const bool is_in = st.predicate(*ch.item);
397
398 if (!was_in && !is_in) return;
399
400 if (was_in && is_in) {
401 const std::size_t d_idx = *st.source_to_derived[src_idx];
402 if (refresh_value) {
403 st.items[d_idx] = ch.item;
404 }
405 lk.unlock();
406 sig.emit(ListChange<T>{kind_for_in_in, d_idx, ch.item, 0});
407 return;
408 }
409
410 if (was_in && !is_in) {
411 const std::size_t d_idx = *st.source_to_derived[src_idx];
412 auto removed = st.items[d_idx];
413 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
414 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
415 st.source_to_derived[src_idx] = std::nullopt;
416 renumber_s2d_(st);
417 lk.unlock();
418 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx,
419 removed, 0});
420 return;
421 }
422
423 // !was_in && is_in: item became visible.
424 std::size_t d_idx = 0;
425 for (std::size_t i = 0; i < src_idx; ++i) {
426 if (st.source_to_derived[i].has_value()) ++d_idx;
427 }
428 st.derived_to_source.insert(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx),
429 src_idx);
430 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx), ch.item);
431 st.source_to_derived[src_idx] = d_idx;
432 renumber_s2d_(st);
433 lk.unlock();
434 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
435 }
436
437 static void handle_replace_(SharedState& st, Signal& sig,
438 const ListChange<T>& ch) {
439 handle_membership_transition_(st, sig, ch,
441 /*refresh_value=*/true);
442 }
443
444 static void handle_item_changed_(SharedState& st, Signal& sig,
445 const ListChange<T>& ch) {
446 handle_membership_transition_(st, sig, ch,
448 /*refresh_value=*/false);
449 }
450
451 static void handle_move_(SharedState& st, Signal& sig,
452 const ListChange<T>& ch) {
453 std::unique_lock lk(st.m);
454 const std::size_t from = ch.from_index;
455 const std::size_t to = ch.index;
456
457 if (from == to) return;
458 if (from >= st.source_to_derived.size() ||
459 to >= st.source_to_derived.size()) {
460 return; // defensive; source mutation out of step
461 }
462
463 // Derived index of the moved item BEFORE shuffle (if any).
464 const std::optional<std::size_t> old_d = st.source_to_derived[from];
465
466 // Shuffle source_to_derived to mirror the source's own move.
467 auto moved_slot = st.source_to_derived[from];
468 st.source_to_derived.erase(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(from));
469 st.source_to_derived.insert(st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(to),
470 moved_slot);
471
472 // If the moved item was in the filter, its derived position
473 // needs updating too. Compute the new derived index of the
474 // item now at source `to`.
475 if (old_d) {
476 // Determine new derived index by counting in-filter slots
477 // strictly before `to` in the new source layout.
478 std::size_t new_d = 0;
479 for (std::size_t i = 0; i < to; ++i) {
480 if (st.source_to_derived[i].has_value()) ++new_d;
481 }
482
483 // Physically move the item within derived_to_source and
484 // items to reflect the new position.
485 if (new_d != *old_d) {
486 auto moved_src = st.derived_to_source[*old_d];
487 auto moved_item = st.items[*old_d];
488 st.derived_to_source.erase(st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(*old_d));
489 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(*old_d));
490 st.derived_to_source.insert(
491 st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(new_d), moved_src);
492 st.items.insert(st.items.begin() + static_cast<std::ptrdiff_t>(new_d), std::move(moved_item));
493 }
494
495 // Now renumber s2d to reflect both the source shuffle and
496 // the derived reordering we just did.
497 renumber_s2d_(st);
498
499 if (new_d != *old_d) {
500 lk.unlock();
501 sig.emit(ListChange<T>{ListChangeKind::Move, new_d,
502 ch.item, *old_d});
503 }
504 return;
505 }
506
507 // Moved item was not in the filter — the only change is which
508 // source indices each d2s entry refers to.
509 renumber_d2s_after_source_move_(st, from, to);
510 renumber_s2d_(st);
511 }
512
513 static void handle_reset_(SharedState& st, Signal& sig, const ListChange<T>& ch) {
514 const auto& snapshot = *ch.snapshot;
515 std::unique_lock lk(st.m);
516 st.source_to_derived.clear();
517 st.derived_to_source.clear();
518 st.items.clear();
519 st.source_to_derived.resize(snapshot.size(), std::nullopt);
520 for (std::size_t i = 0; i < snapshot.size(); ++i) {
521 if (!st.predicate(*snapshot[i])) continue;
522 st.source_to_derived[i] = st.items.size();
523 st.derived_to_source.push_back(i);
524 st.items.push_back(snapshot[i]);
525 }
526 auto reset = ListChange<T>::reset(st.items);
527 lk.unlock();
528 sig.emit(std::move(reset));
529 }
530
531 // Walk source_to_derived and assign successive derived indices to
532 // every non-nullopt slot. O(source.size()). Call after any
533 // structural mutation.
534 static void renumber_s2d_(SharedState& st) {
535 std::size_t d = 0;
536 for (std::size_t s = 0; s < st.source_to_derived.size(); ++s) {
537 auto& slot = st.source_to_derived[s];
538 if (slot.has_value()) {
539 slot = d;
540 st.derived_to_source[d++] = s;
541 }
542 }
543 }
544
545 // When a filtered-out item moves within the source, we still need
546 // to update the source indices stored in derived_to_source so that
547 // they refer to the right slots. This is the straightforward
548 // translation: values > from and <= to (moving backward) or
549 // >= to and < from (moving forward) get shifted by one.
550 static void renumber_d2s_after_source_move_(SharedState& st,
551 std::size_t from,
552 std::size_t to) {
553 if (from < to) {
554 for (auto& s : st.derived_to_source) {
555 if (s > from && s <= to) --s;
556 // NOTE: the moved item itself was NOT in derived,
557 // so we never encounter `s == from`.
558 }
559 } else {
560 for (auto& s : st.derived_to_source) {
561 if (s >= to && s < from) ++s;
562 }
563 }
564 }
565};
566
567// ---------------------------------------------------------------------------
568// Factory helper — deduces the source type so pipelines stay readable.
569//
570// Without it, chaining forces the caller to spell out every layer:
571//
572// auto f = std::make_shared<FilteredList<Task>>(src, pred);
573// auto s = std::make_shared<SortedList<Task, FilteredList<Task>>>(f, cmp);
574//
575// With it:
576//
577// auto f = aria::filtered(src, pred);
578// auto s = aria::sorted(f, cmp);
579//
580// Returns shared_ptr because every derived list takes its source as one, so
581// the result is immediately usable as the next link in the chain.
582// ---------------------------------------------------------------------------
583template<typename Source,
584 typename Predicate,
585 typename T = list_source_value_t<Source>>
587[[nodiscard]] std::shared_ptr<FilteredList<T, Source>>
588filtered(std::shared_ptr<Source> source, Predicate predicate) {
589 return std::make_shared<FilteredList<T, Source>>(
590 std::move(source),
591 typename FilteredList<T, Source>::Predicate{std::move(predicate)});
592}
593
594} // namespace aria
T value_type
Definition filtered_list.hpp:103
std::optional< std::size_t > source_index_of(std::size_t derived_index) const
Definition filtered_list.hpp:175
detail::ListSignal< T > Signal
Definition filtered_list.hpp:108
FilteredList & operator=(const FilteredList &)=delete
aria::inplace_function< bool(const T &), 32 > Predicate
Owning, heap-free predicate handle.
Definition filtered_list.hpp:107
void set_predicate(Predicate new_predicate)
Definition filtered_list.hpp:188
FilteredList(std::shared_ptr< Source > source, Predicate predicate)
Definition filtered_list.hpp:110
std::vector< std::shared_ptr< T > > snapshot() const
Definition filtered_list.hpp:169
FilteredList(const FilteredList &)=delete
std::size_t size() const
Definition filtered_list.hpp:157
std::shared_ptr< T > at(std::size_t derived_index) const
Definition filtered_list.hpp:164
~FilteredList()=default
bool empty() const
Definition filtered_list.hpp:162
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:588
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