Skip to main content

gpui_kit_grid/state/
pending.rs

1//! Commits waiting for their answer or for the store: the optimistic value
2//! a cell shows meanwhile, the error mark of a refused commit, and the same
3//! values for a copy and an export in the background.
4//!
5//! A commit (`LiveGridEvent::EditCommit`) is pending until the application
6//! answers it with `LiveGridState::resolve_edit`. `Ok` makes it accepted:
7//! the cell keeps showing the value until the store has it, or the store
8//! says otherwise, or `EditPolicy::settle_timeout` passes. `Err` drops it
9//! and marks the cell with the error. Sorting, filtering, grouping and
10//! aggregates read the store, never these values.
11
12use std::{hash::Hash, sync::Arc, time::Duration};
13
14use gpui_kit::{Context, Task};
15use rustc_hash::FxHashMap;
16use web_time::Instant;
17
18use super::{
19    ColumnOf, KeyOf, LiveGridDelegate, LiveGridState,
20    edit::{EditError, EditId, EditState},
21};
22use crate::{
23    model::{GridRow, OwnedValue},
24    store::StoreSnapshot,
25};
26
27/// The optimistic values of a grid at one moment, by row key: what a cell
28/// shows while its commit waits. Immutable, so a background copy or export
29/// reads the values of the moment its command ran.
30pub(crate) struct PendingValues<K, C> {
31    rows: FxHashMap<K, Box<[(C, OwnedValue)]>>,
32}
33
34impl<K, C> Default for PendingValues<K, C> {
35    fn default() -> Self {
36        Self {
37            rows: FxHashMap::default(),
38        }
39    }
40}
41
42impl<K: Eq + Hash, C> PendingValues<K, C> {
43    /// Returns true if no cell waits: O(1).
44    pub(crate) fn is_empty(&self) -> bool {
45        self.rows.is_empty()
46    }
47
48    /// The optimistic values of the row of `key`, by column.
49    pub(crate) fn get(&self, key: &K) -> Option<&[(C, OwnedValue)]> {
50        self.rows.get(key).map(|values| &**values)
51    }
52}
53
54/// One commit of a cell that waits.
55struct PendingEdit {
56    id: EditId,
57    value: OwnedValue,
58    /// `Pending` or `Accepted`.
59    state: EditState,
60    /// When the commit was accepted, and the store's value then.
61    accepted: Option<(Instant, OwnedValue)>,
62}
63
64/// The commits of one cell that wait, oldest first, and the mark of its
65/// latest refused commit.
66struct CellEdits<C> {
67    column: C,
68    edits: Vec<PendingEdit>,
69    /// `Failed`, and when the mark goes by itself.
70    failed: Option<(EditState, Option<Instant>)>,
71}
72
73impl<C> CellEdits<C> {
74    fn is_empty(&self) -> bool {
75        self.edits.is_empty() && self.failed.is_none()
76    }
77
78    /// The newest commit that waits: what the cell shows.
79    fn newest(&self) -> Option<&PendingEdit> {
80        self.edits.last()
81    }
82}
83
84/// What a cell's commits mark it with in a frame.
85#[derive(Clone, Copy)]
86pub(super) struct CellEditMarks<'a> {
87    /// The value the newest waiting commit shows.
88    pub(super) value: Option<&'a OwnedValue>,
89    /// The state the cell reads as ([`LiveGridState::edit_state`]).
90    pub(super) state: Option<&'a EditState>,
91    /// The newest commit still waits for an answer: its value is muted.
92    pub(super) pending: bool,
93    /// A commit of the cell was refused: the error mark shows.
94    pub(super) failed: bool,
95}
96
97/// The commits of a grid that wait for `LiveGridState::resolve_edit` or for
98/// the store, and the cells' error marks.
99pub(super) struct PendingEdits<K, C> {
100    rows: FxHashMap<K, Vec<CellEdits<C>>>,
101    /// Where each waiting commit is.
102    ids: FxHashMap<EditId, (K, C)>,
103    /// The values of the newest waiting commits, for a copy or an export;
104    /// made again when they change, never per frame.
105    values: Arc<PendingValues<K, C>>,
106    /// The deadline the timer waits for, and the timer.
107    timer: Option<(Instant, Task<()>)>,
108}
109
110impl<K, C> Default for PendingEdits<K, C> {
111    fn default() -> Self {
112        Self {
113            rows: FxHashMap::default(),
114            ids: FxHashMap::default(),
115            values: Arc::new(PendingValues::default()),
116            timer: None,
117        }
118    }
119}
120
121impl<K: Clone + Eq + Hash, C: PartialEq + Copy> PendingEdits<K, C> {
122    /// Returns true if no cell has a commit or a mark: a frame then checks
123    /// no cell.
124    pub(super) fn is_empty(&self) -> bool {
125        self.rows.is_empty()
126    }
127
128    fn cell(&self, key: &K, column: C) -> Option<&CellEdits<C>> {
129        self.rows
130            .get(key)?
131            .iter()
132            .find(|cell| cell.column == column)
133    }
134
135    fn cell_mut(&mut self, key: &K, column: C) -> &mut CellEdits<C> {
136        let cells = self.rows.entry(key.clone()).or_default();
137        let ix = match cells.iter().position(|cell| cell.column == column) {
138            Some(ix) => ix,
139            None => {
140                cells.push(CellEdits {
141                    column,
142                    edits: Vec::new(),
143                    failed: None,
144                });
145                cells.len() - 1
146            }
147        };
148        &mut cells[ix]
149    }
150
151    /// The value the cell of `key` and `column` shows while its newest
152    /// commit waits.
153    pub(super) fn overlay(&self, key: &K, column: C) -> Option<&OwnedValue> {
154        Some(&self.cell(key, column)?.newest()?.value)
155    }
156
157    /// What the cell's commits mark it with; `None` for a cell with none.
158    pub(super) fn marks(&self, key: &K, column: C) -> Option<CellEditMarks<'_>> {
159        let cell = self.cell(key, column)?;
160        let newest = cell.newest();
161        Some(CellEditMarks {
162            value: newest.map(|edit| &edit.value),
163            state: newest
164                .map(|edit| &edit.state)
165                .or_else(|| cell.failed.as_ref().map(|(state, _)| state)),
166            pending: newest.is_some_and(|edit| edit.state == EditState::Pending),
167            failed: cell.failed.is_some(),
168        })
169    }
170
171    /// The state the cell reads as: its newest waiting commit's, else its
172    /// error mark.
173    pub(super) fn state(&self, key: &K, column: C) -> Option<&EditState> {
174        self.marks(key, column)?.state
175    }
176
177    /// Commits that wait for an answer or for the store.
178    pub(super) fn count(&self) -> usize {
179        self.ids.len()
180    }
181
182    /// Adds a commit (after its `EditCommit`): the cell shows `value` until
183    /// it settles. The cell's error mark goes: it is edited again.
184    pub(super) fn begin(&mut self, id: EditId, key: K, column: C, value: OwnedValue) {
185        let cell = self.cell_mut(&key, column);
186        cell.failed = None;
187        cell.edits.push(PendingEdit {
188            id,
189            value,
190            state: EditState::Pending,
191            accepted: None,
192        });
193        self.ids.insert(id, (key, column));
194        self.publish();
195    }
196
197    /// Answers a commit: `Ok` accepts it at `now`, with the store's value
198    /// then; `Err` drops it and marks the cell, the mark going at
199    /// `mark_until`. Returns false for an unknown, answered or settled id.
200    pub(super) fn resolve(
201        &mut self,
202        id: EditId,
203        result: Result<(), EditError>,
204        now: Instant,
205        store_value: impl FnOnce(&K, C) -> Option<OwnedValue>,
206        mark_until: Option<Instant>,
207    ) -> bool {
208        let Some((key, column)) = self.ids.get(&id).cloned() else {
209            return false;
210        };
211        let store_value = store_value(&key, column);
212        let cell = self.cell_mut(&key, column);
213        let Some(ix) = cell
214            .edits
215            .iter()
216            .position(|edit| edit.id == id && edit.state == EditState::Pending)
217        else {
218            return false;
219        };
220        match result {
221            Ok(()) => {
222                let edit = &mut cell.edits[ix];
223                edit.state = EditState::Accepted;
224                edit.accepted = Some((now, store_value.unwrap_or_else(|| edit.value.clone())));
225            }
226            Err(error) => {
227                cell.edits.remove(ix);
228                cell.failed = Some((EditState::Failed(error), mark_until));
229                self.ids.remove(&id);
230            }
231        }
232        self.publish();
233        true
234    }
235
236    /// Takes the error mark off a cell. Returns false if it had none.
237    pub(super) fn clear_failed(&mut self, key: &K, column: C) -> bool {
238        let Some(cells) = self.rows.get_mut(key) else {
239            return false;
240        };
241        let Some(cell) = cells.iter_mut().find(|cell| cell.column == column) else {
242            return false;
243        };
244        if cell.failed.take().is_none() {
245            return false;
246        }
247        self.prune();
248        true
249    }
250
251    /// Settles what the store and the clock decide: rows that left the store
252    /// take their commits and marks with them; an accepted commit goes when
253    /// the store has its value, when the store's value changed since the
254    /// answer (the store said its own), or at `now` past `settle`; a mark
255    /// goes past its deadline. Returns true if anything went.
256    pub(super) fn follow<R>(
257        &mut self,
258        snapshot: &StoreSnapshot<R>,
259        now: Instant,
260        settle: Duration,
261    ) -> bool
262    where
263        R: GridRow<Key = K, Column = C>,
264        C: crate::model::GridColumns,
265    {
266        let before = (self.ids.len(), self.marks_count());
267        let mut gone: Vec<EditId> = Vec::new();
268        self.rows.retain(|key, cells| {
269            let Some(row) = snapshot.row_id(key).and_then(|id| snapshot.row(id)) else {
270                gone.extend(
271                    cells
272                        .iter()
273                        .flat_map(|cell| cell.edits.iter().map(|edit| edit.id)),
274                );
275                return false;
276            };
277            for cell in cells.iter_mut() {
278                if cell.edits.iter().any(|edit| edit.accepted.is_some()) {
279                    let store = OwnedValue::from_cell(row.value(cell.column));
280                    cell.edits.retain(|edit| {
281                        let Some((at, at_answer)) = &edit.accepted else {
282                            return true;
283                        };
284                        let settled = store.cmp_total(&edit.value).is_eq()
285                            || !store.cmp_total(at_answer).is_eq()
286                            || now >= *at + settle;
287                        if settled {
288                            gone.push(edit.id);
289                        }
290                        !settled
291                    });
292                }
293                if cell
294                    .failed
295                    .as_ref()
296                    .is_some_and(|(_, until)| until.is_some_and(|until| now >= until))
297                {
298                    cell.failed = None;
299                }
300            }
301            cells.retain(|cell| !cell.is_empty());
302            !cells.is_empty()
303        });
304        for id in gone {
305            self.ids.remove(&id);
306        }
307        let changed = before != (self.ids.len(), self.marks_count());
308        if changed {
309            self.publish();
310        }
311        changed
312    }
313
314    fn marks_count(&self) -> usize {
315        self.rows
316            .values()
317            .flatten()
318            .filter(|cell| cell.failed.is_some())
319            .count()
320    }
321
322    /// The nearest instant `follow` has something to do at, given `settle`:
323    /// an accepted commit's settle deadline or a mark's.
324    pub(super) fn next_deadline(&self, settle: Duration) -> Option<Instant> {
325        self.rows
326            .values()
327            .flatten()
328            .flat_map(|cell| {
329                cell.edits
330                    .iter()
331                    .filter_map(|edit| edit.accepted.as_ref().map(|(at, _)| *at + settle))
332                    .chain(cell.failed.as_ref().and_then(|(_, until)| *until))
333            })
334            .min()
335    }
336
337    /// Drops empty cells and rows, then publishes the values.
338    fn prune(&mut self) {
339        self.rows.retain(|_, cells| {
340            cells.retain(|cell| !cell.is_empty());
341            !cells.is_empty()
342        });
343        self.publish();
344    }
345
346    /// Makes the values a copy or an export reads again.
347    fn publish(&mut self) {
348        self.rows.retain(|_, cells| {
349            cells.retain(|cell| !cell.is_empty());
350            !cells.is_empty()
351        });
352        let rows = self
353            .rows
354            .iter()
355            .filter_map(|(key, cells)| {
356                let values: Box<[(C, OwnedValue)]> = cells
357                    .iter()
358                    .filter_map(|cell| Some((cell.column, cell.newest()?.value.clone())))
359                    .collect();
360                (!values.is_empty()).then(|| (key.clone(), values))
361            })
362            .collect();
363        self.values = Arc::new(PendingValues { rows });
364    }
365}
366
367impl<D: LiveGridDelegate> LiveGridState<D> {
368    /// Answers a commit (`LiveGridEvent::EditCommit::edit`). `Ok`: the
369    /// value was applied; the cell shows it until the store has it too, the
370    /// store says its own, or `EditPolicy::settle_timeout` passes. `Err`:
371    /// the cell goes back to the store's value and shows the error until
372    /// the cell is edited again, `clear_edit_error`, or
373    /// `EditPolicy::error_mark_timeout`. Returns false for an unknown id,
374    /// one answered already, or one settled: a removed row's commits are
375    /// dropped.
376    pub fn resolve_edit(
377        &mut self,
378        edit: EditId,
379        result: Result<(), EditError>,
380        cx: &mut Context<Self>,
381    ) -> bool {
382        let now = self.store.now();
383        let mark_until = self
384            .edit_setup
385            .policy()
386            .error_mark_timeout()
387            .map(|timeout| now + timeout);
388        let snapshot = self.snapshot.clone();
389        let resolved = self.pending_edits.resolve(
390            edit,
391            result,
392            now,
393            |key, column| {
394                let row = snapshot.row(snapshot.row_id(key)?)?;
395                Some(OwnedValue::from_cell(row.value(column)))
396            },
397            mark_until,
398        );
399        if resolved {
400            // A settle timeout of zero shows the store's value at once.
401            self.follow_pending_edits(cx);
402            cx.notify();
403        }
404        resolved
405    }
406
407    /// What a cell's edits look like now: waiting for an answer, accepted
408    /// and waiting for the store, or failed. `None` when the cell shows the
409    /// store's value with no error.
410    pub fn edit_state(&self, key: &KeyOf<D>, column: ColumnOf<D>) -> Option<&EditState> {
411        self.pending_edits.state(key, column)
412    }
413
414    /// Commits not answered yet, or accepted and waiting for the store.
415    pub fn pending_edit_count(&self) -> usize {
416        self.pending_edits.count()
417    }
418
419    /// Takes the error mark off a cell (`EditState::Failed`); the next edit
420    /// of the cell does it too. Returns false if the cell had none.
421    pub fn clear_edit_error(
422        &mut self,
423        key: &KeyOf<D>,
424        column: ColumnOf<D>,
425        cx: &mut Context<Self>,
426    ) -> bool {
427        let cleared = self.pending_edits.clear_failed(key, column);
428        if cleared {
429            cx.notify();
430        }
431        cleared
432    }
433
434    /// The optimistic values now: what cells with a waiting commit show, for
435    /// a copy and an export, which write what the screen shows. O(1): the
436    /// set is shared, not copied.
437    pub(crate) fn pending_values(&self) -> Arc<PendingValues<super::KeyOf<D>, super::ColumnOf<D>>> {
438        self.pending_edits.values.clone()
439    }
440
441    /// Sets the optimistic values as waiting commits would, for a test of a
442    /// copy or an export without real commits: the newest value of a cell
443    /// wins.
444    #[cfg(any(test, feature = "test-support"))]
445    pub fn set_test_pending_values(
446        &mut self,
447        values: impl IntoIterator<Item = (super::KeyOf<D>, super::ColumnOf<D>, OwnedValue)>,
448        cx: &mut Context<Self>,
449    ) {
450        let mut rows: FxHashMap<_, Vec<(super::ColumnOf<D>, OwnedValue)>> = FxHashMap::default();
451        for (key, column, value) in values {
452            let row = rows.entry(key).or_default();
453            row.retain(|(edited, _)| *edited != column);
454            row.push((column, value));
455        }
456        let rows = rows
457            .into_iter()
458            .map(|(key, values)| (key, values.into_boxed_slice()))
459            .collect();
460        self.pending_edits.values = Arc::new(PendingValues { rows });
461        cx.notify();
462    }
463
464    /// Adds a commit that went out: its cell shows the value until it
465    /// settles.
466    pub(super) fn begin_pending_edit(
467        &mut self,
468        edit: EditId,
469        key: KeyOf<D>,
470        column: ColumnOf<D>,
471        value: OwnedValue,
472        cx: &mut Context<Self>,
473    ) {
474        self.pending_edits.begin(edit, key, column, value);
475        cx.notify();
476    }
477
478    /// Settles the commits the store or the clock decided, after a new
479    /// snapshot or at a deadline, and waits for the next deadline with one
480    /// timer on the store's clock, as the highlight fade does.
481    pub(super) fn follow_pending_edits(&mut self, cx: &mut Context<Self>) {
482        if self.pending_edits.is_empty() {
483            self.pending_edits.timer = None;
484            return;
485        }
486        let now = self.store.now();
487        let settle = self.edit_setup.policy().settle_timeout();
488        if self.pending_edits.follow(&self.snapshot, now, settle) {
489            cx.notify();
490        }
491        let next = self.pending_edits.next_deadline(settle);
492        match next {
493            None => self.pending_edits.timer = None,
494            Some(deadline)
495                if self
496                    .pending_edits
497                    .timer
498                    .as_ref()
499                    .is_some_and(|(at, _)| *at == deadline) => {}
500            Some(deadline) => {
501                let wait = deadline.saturating_duration_since(now);
502                let task = cx.spawn(async move |this, cx| {
503                    cx.background_executor().timer(wait).await;
504                    let _ = this.update(cx, |state, cx| {
505                        state.pending_edits.timer = None;
506                        state.follow_pending_edits(cx);
507                    });
508                });
509                self.pending_edits.timer = Some((deadline, task));
510            }
511        }
512    }
513
514    /// A new order was shown: an edited row it no longer shows ends as
515    /// `EditPolicy::on_hidden` decides.
516    pub(super) fn follow_edit_shown(&mut self, cx: &mut Context<Self>) {
517        self.end_hidden_edit(cx);
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_pending_edits_stack() {
527        let now = Instant::now();
528        let mut pending = PendingEdits::<u32, u16>::default();
529        assert!(pending.is_empty());
530        assert_eq!(pending.overlay(&1, 0), None);
531        assert!(pending.values.is_empty());
532
533        // Two commits in one cell: the newer shows; both count.
534        pending.begin(EditId::new(1), 1, 0, OwnedValue::Int(5));
535        pending.begin(EditId::new(2), 1, 0, OwnedValue::Int(6));
536        assert_eq!(pending.overlay(&1, 0), Some(&OwnedValue::Int(6)));
537        assert_eq!(pending.count(), 2);
538        assert_eq!(pending.state(&1, 0), Some(&EditState::Pending));
539        assert_eq!(pending.values.get(&1), Some(&[(0, OwnedValue::Int(6))][..]));
540        assert_eq!(pending.overlay(&1, 1), None);
541        assert_eq!(pending.overlay(&2, 0), None);
542
543        // The older refused: the mark shows, the newer still shows.
544        let error = EditError::new("Price out of band");
545        assert!(pending.resolve(EditId::new(1), Err(error.clone()), now, |_, _| None, None));
546        let marks = pending.marks(&1, 0).expect("marks");
547        assert!(marks.failed && marks.pending);
548        assert_eq!(marks.value, Some(&OwnedValue::Int(6)));
549        assert_eq!(pending.count(), 1);
550        // The newer accepted, then its answer again: unknown.
551        assert!(pending.resolve(
552            EditId::new(2),
553            Ok(()),
554            now,
555            |_, _| Some(OwnedValue::Int(1)),
556            None
557        ));
558        assert_eq!(pending.state(&1, 0), Some(&EditState::Accepted));
559        assert!(!pending.resolve(EditId::new(7), Ok(()), now, |_, _| None, None));
560        assert_eq!(
561            pending.next_deadline(Duration::from_secs(5)),
562            Some(now + Duration::from_secs(5))
563        );
564        // The mark goes by a call; the cell keeps its accepted value.
565        assert!(pending.clear_failed(&1, 0));
566        assert!(!pending.clear_failed(&1, 0));
567        assert_eq!(pending.overlay(&1, 0), Some(&OwnedValue::Int(6)));
568    }
569}